Ver código fonte

Merge branch 'develop5.0-tmd' of http://106.12.23.251:10000/TEAMMODEL/TEAMModelOS into develop5.0-tmd

XW 4 anos atrás
pai
commit
0a6fa5da17
54 arquivos alterados com 1882 adições e 391 exclusões
  1. 16 0
      Client/Client.csproj
  2. 67 0
      Client/Program.cs
  3. 143 0
      Client/SSE/ConnectedState.cs
  4. 55 0
      Client/SSE/ConnectingState.cs
  5. 37 0
      Client/SSE/DisconnectedState.cs
  6. 116 0
      Client/SSE/EventSource.cs
  7. 14 0
      Client/SSE/EventSourceState.cs
  8. 15 0
      Client/SSE/IConnectionState.cs
  9. 17 0
      Client/SSE/IServerResponse.cs
  10. 15 0
      Client/SSE/IWebRequester.cs
  11. 12 0
      Client/SSE/IWebRequesterFactory.cs
  12. 39 0
      Client/SSE/ServerResponse.cs
  13. 26 0
      Client/SSE/ServerSentEvent.cs
  14. 17 0
      Client/SSE/ServerSentEventReceivedEventArgs.cs
  15. 16 0
      Client/SSE/StateChangedEventArgs.cs
  16. 47 0
      Client/SSE/StringSplitter.cs
  17. 38 0
      Client/SSE/WatchDog.cs
  18. 30 0
      Client/SSE/WebRequester.cs
  19. 16 0
      Client/SSE/WebRequesterFactory.cs
  20. 1 0
      TEAMModelOS.SDK/TEAMModelOS.SDK.csproj
  21. 7 1
      TEAMModelOS.sln
  22. 3 0
      TEAMModelOS/ClientApp/src/api/syllabus.js
  23. 1 0
      TEAMModelOS/ClientApp/src/assets/login/icon_student.svg
  24. 1 0
      TEAMModelOS/ClientApp/src/assets/login/icon_teacher.svg
  25. 1 0
      TEAMModelOS/ClientApp/src/assets/login/ies5_logo_2.svg
  26. BIN
      TEAMModelOS/ClientApp/src/assets/login/login_bg.jpg
  27. 1 1
      TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.less
  28. 23 10
      TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.vue
  29. 3 3
      TEAMModelOS/ClientApp/src/components/selflearn/NewChooseContent.vue
  30. 142 47
      TEAMModelOS/ClientApp/src/components/syllabus/DragTree.vue
  31. 17 17
      TEAMModelOS/ClientApp/src/components/syllabus/InviteTeacher.vue
  32. 1 0
      TEAMModelOS/ClientApp/src/locale/lang/zh-CN/survey.js
  33. 46 1
      TEAMModelOS/ClientApp/src/locale/lang/zh-CN/syllabus.js
  34. 1 0
      TEAMModelOS/ClientApp/src/locale/lang/zh-TW/survey.js
  35. 46 2
      TEAMModelOS/ClientApp/src/locale/lang/zh-TW/syllabus.js
  36. 4 0
      TEAMModelOS/ClientApp/src/utils/editorLangTw.js
  37. 1 1
      TEAMModelOS/ClientApp/src/utils/editorTools.js
  38. 6 0
      TEAMModelOS/ClientApp/src/utils/evTools.js
  39. 2 0
      TEAMModelOS/ClientApp/src/utils/public.js
  40. 1 2
      TEAMModelOS/ClientApp/src/view/answersheet/index.vue
  41. 3 0
      TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue
  42. 14 5
      TEAMModelOS/ClientApp/src/view/evaluation/components/BaseRepair.vue
  43. 6 3
      TEAMModelOS/ClientApp/src/view/evaluation/index/CreatePaper.vue
  44. 5 6
      TEAMModelOS/ClientApp/src/view/login/page/Student.vue
  45. 1 1
      TEAMModelOS/ClientApp/src/view/login/page/Teacher.less
  46. 4 5
      TEAMModelOS/ClientApp/src/view/login/page/Teacher.vue
  47. 172 15
      TEAMModelOS/ClientApp/src/view/login/test.vue
  48. 10 7
      TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.less
  49. 374 219
      TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue
  50. 1 1
      TEAMModelOS/ClientApp/src/view/teachcontent/index.vue
  51. 161 0
      TEAMModelOS/Controllers/Client/HiScanController.cs
  52. 19 16
      TEAMModelOS/Controllers/Core/BlobController.cs
  53. 27 28
      TEAMModelOS/Controllers/School/StudentController.cs
  54. 41 0
      TEAMModelOS/Startup.cs

+ 16 - 0
Client/Client.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.3" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
+    <PackageReference Include="Twilio" Version="5.55.0" />
+  </ItemGroup>
+
+</Project>

+ 67 - 0
Client/Program.cs

@@ -0,0 +1,67 @@
+using Client.SSE;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Security.Policy;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Client
+{
+    class Program
+    {    
+        static void Main(string[] args)
+        {
+            //SSE
+            try
+            {
+                var cts = new CancellationTokenSource();
+                var header = new Dictionary<string, string>() { { "X-Auth-Name", "IES5" } };
+                //var sse = new EventSource(new Uri("https://localhost:5001/service/sse"), headeer, 5000);
+                var sse = new EventSource(new Uri("https://api2.teammodel.net/service/sse"), header, 5000);
+
+                sse.StateChanged += Sse_StateChanged;
+                sse.EventReceived += Sse_EventReceived;
+                sse.Start(cts.Token);
+
+                //cts?.Cancel(); 中斷SSE Client連線
+            }
+            catch (TaskCanceledException ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            catch (AggregateException ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine(ex.Message);
+            }
+            Console.ReadLine();
+        }
+
+        private static void Sse_EventReceived(object sender, ServerSentEventReceivedEventArgs e)
+        {
+            var mm = e.Message;
+            Console.WriteLine(mm.Data);
+        }
+
+
+        private static void Sse_StateChanged(object sender, StateChangedEventArgs e)
+        {
+            //CONNECTING 0 連線中
+            //OPEN 1 開啟
+            //CLOSED 2 關閉
+            var aa = e.State;
+
+            Console.WriteLine(aa.ToString());
+        }
+
+    }
+}

+ 143 - 0
Client/SSE/ConnectedState.cs

@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Net;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Diagnostics;
+
+namespace Client.SSE
+{
+    public class ConnectedState : IConnectionState
+    {    
+
+        private IWebRequesterFactory mWebRequesterFactory;
+        private ServerSentEvent mSse = null;
+        private string mRemainingText = string.Empty;   // the text that is not ended with a lineending char is saved for next call.
+        private IServerResponse mResponse;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State { get { return EventSourceState.OPEN; } }
+
+        public ConnectedState(IServerResponse response, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            mResponse = response;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> msgReceived, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {   
+            Task<IConnectionState> t = new Task<IConnectionState>(() =>
+            {
+                //using (mResponse)
+                {
+                    //using (var stream = mResponse.GetResponseStream())
+                    var stream = mResponse.GetResponseStream();
+                    {
+                        byte[] buffer = new byte[1024 * 8];
+                        var taskRead = stream.ReadAsync(buffer, 0, buffer.Length, cancelToken);
+
+                        try
+                        {
+                            taskRead.Wait(cancelToken);
+                        }
+                        catch (Exception ex)
+                        {
+                            Trace.WriteLine(ex, "ConnectedState.Run");
+                        }
+                        if (!cancelToken.IsCancellationRequested)
+                        {
+                            int bytesRead = taskRead.Result;
+                            if (bytesRead > 0) // stream has not reached the end yet
+                            {
+                                //Console.WriteLine("ReadCallback {0} bytesRead", bytesRead);
+                                string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
+                                text = mRemainingText + text;
+                                string[] lines = StringSplitter.SplitIntoLines(text, out mRemainingText);
+                                foreach (string line in lines)
+                                {
+                                    if (cancelToken.IsCancellationRequested) break;
+
+                                    // Dispatch message if empty lne
+                                    if (string.IsNullOrEmpty(line.Trim()) && mSse != null)
+                                    {
+                                        Trace.WriteLine("SSE Message received");
+                                        msgReceived(mSse);
+                                        mSse = null;
+                                    }
+                                    else if (line.StartsWith(":"))
+                                    {
+                                        // This a comment, just log it.
+                                        Trace.WriteLine("SSE A comment was received: " + line);
+                                    }
+                                    else
+                                    {
+                                        string fieldName = String.Empty;
+                                        string fieldValue = String.Empty;
+                                        if (line.Contains(':'))
+                                        {
+                                            int index = line.IndexOf(':');
+                                            fieldName = line.Substring(0, index);
+                                            fieldValue = line.Substring(index + 1).TrimStart();
+                                        }
+                                        else
+                                            fieldName = line;
+
+                                        if (String.Compare(fieldName, "event", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.EventType = fieldValue;
+                                        }
+                                        else if (String.Compare(fieldName, "data", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.Data = fieldValue + '\n';
+                                        }
+                                        else if (String.Compare(fieldName, "id", true) == 0)
+                                        {
+                                            mSse = mSse ?? new ServerSentEvent();
+                                            mSse.LastEventId = fieldValue;
+                                        }
+                                        else if (String.Compare(fieldName, "retry", true) == 0)
+                                        {
+                                            int parsedRetry;
+                                            if (int.TryParse(fieldValue, out parsedRetry))
+                                            {
+                                                mSse = mSse ?? new ServerSentEvent();
+                                                mSse.Retry = parsedRetry;
+                                            }
+                                        }
+                                        else
+                                        {
+                                            // Ignore this, just log it
+                                            Trace.WriteLine("SSE A unknown line was received: " + line);
+                                        }
+                                    }
+                                }
+
+                                if (!cancelToken.IsCancellationRequested)
+                                    return this;
+                            }
+                            else // end of the stream reached
+                            {
+                                Trace.WriteLine("SSE No bytes read. End of stream.");
+                            }
+                        }
+
+                        //stream.Dispose()
+                        //stream.Close();
+                        //mResponse.Close();
+                        //mResponse.Dispose();
+                        return new DisconnectedState(mResponse.ResponseUri, mWebRequesterFactory, headers);
+                    }
+                }
+            });
+
+            t.Start();
+            return t;
+        }
+    }
+}

+ 55 - 0
Client/SSE/ConnectingState.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Net;
+using System.IO;
+using System.Threading;
+using System.Diagnostics;
+
+namespace Client.SSE
+{
+    public class ConnectingState : IConnectionState
+    {      
+
+        private Uri mUrl;
+        private IWebRequesterFactory mWebRequesterFactory;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State { get { return EventSourceState.CONNECTING; } }
+
+        public ConnectingState(Uri url, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            if (url == null) throw new ArgumentNullException("Url cant be null");
+            if (webRequesterFactory == null) throw new ArgumentNullException("Factory cant be null");
+            mUrl = url;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> donothing, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {
+            IWebRequester requester = mWebRequesterFactory.Create();
+            var taskResp = requester.Get(mUrl, headers);
+
+            return taskResp.ContinueWith<IConnectionState>(tsk => 
+            {
+                if (tsk.Status == TaskStatus.RanToCompletion && !cancelToken.IsCancellationRequested)
+                {
+                    IServerResponse response = tsk.Result;
+                    if (response.StatusCode == HttpStatusCode.OK)
+                    {
+                        return new ConnectedState(response, mWebRequesterFactory, headers);
+                    }
+                    else
+                    {
+                        Trace.WriteLine("Failed to connect to: " + mUrl.ToString() + response ?? (" Http statuscode: " + response.StatusCode));
+                    }
+                }
+
+                return new DisconnectedState(mUrl, mWebRequesterFactory, headers);
+            });
+        }
+    }
+}

+ 37 - 0
Client/SSE/DisconnectedState.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class DisconnectedState : IConnectionState 
+    {
+        private Uri mUrl;
+        private IWebRequesterFactory mWebRequesterFactory;
+        private Dictionary<string, string> headers;
+
+        public EventSourceState State
+        {
+            get { return EventSourceState.CLOSED; }
+        }
+
+        public DisconnectedState(Uri url, IWebRequesterFactory webRequesterFactory, Dictionary<string, string> headers)
+        {
+            if (url == null) throw new ArgumentNullException("Url cant be null");
+            mUrl = url;
+            mWebRequesterFactory = webRequesterFactory;
+            this.headers = headers;
+        }
+
+        public Task<IConnectionState> Run(Action<ServerSentEvent> donothing, CancellationToken cancelToken, Dictionary<string, string> headers)
+        {
+            if(cancelToken.IsCancellationRequested)
+                return Task.Factory.StartNew<IConnectionState>(() => { return new DisconnectedState(mUrl, mWebRequesterFactory, headers); });
+            else
+                return Task.Factory.StartNew<IConnectionState>(() => { return new ConnectingState(mUrl, mWebRequesterFactory, headers); });
+        }
+    }
+}

+ 116 - 0
Client/SSE/EventSource.cs

@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+namespace Client.SSE
+{
+    public class EventSource
+    {   
+        public event EventHandler<StateChangedEventArgs> StateChanged;
+        public event EventHandler<ServerSentEventReceivedEventArgs> EventReceived;
+
+        public CancellationTokenSource CancellationToken { get; set; }
+
+        private IWebRequesterFactory _webRequesterFactory = new WebRequesterFactory();
+        private int _timeout = 0;
+        public Uri Url { get; private set; }
+        public EventSourceState State { get { return CurrentState.State; } }
+        public string LastEventId { get; private set; }
+        private IConnectionState mCurrentState = null;
+        private CancellationToken mStopToken;
+        private CancellationTokenSource mTokenSource = new CancellationTokenSource();
+        private Dictionary<string, string> _headers;
+
+        private IConnectionState CurrentState
+        {
+            get { return mCurrentState; }
+            set
+            {
+                if (!value.Equals(mCurrentState))
+                {
+                    StringBuilder sb = new StringBuilder("State changed from ");
+                    sb.Append(mCurrentState == null ? "Unknown" : mCurrentState.State.ToString());
+                    sb.Append(" to ");
+                    sb.Append(value == null ? "Unknown" : value.State.ToString());
+                    Trace.WriteLine(sb.ToString());
+                    mCurrentState = value;
+                    OnStateChanged(mCurrentState.State);
+                }
+            }
+        }
+
+        public EventSource(Uri url, int timeout)
+        {
+            Initialize(url, timeout);
+        }
+
+        public EventSource(Uri url, Dictionary<string, string> headers, int timeout)
+        {
+            _headers = headers;
+            Initialize(url, timeout);
+        }
+
+        /// <summary>
+        /// Constructor for testing purposes
+        /// </summary>
+        /// <param name="factory">The factory that generates the WebRequester to use.</param>
+        public EventSource(Uri url, IWebRequesterFactory factory)
+        {
+            _webRequesterFactory = factory;
+            Initialize(url, 0);
+        }
+
+        private void Initialize(Uri url, int timeout)
+        {
+            _timeout = timeout;
+            Url = url;
+            CurrentState = new DisconnectedState(Url, _webRequesterFactory, _headers);
+            Trace.WriteLine("SSE EventSource created for " + url.ToString());
+        }
+
+
+        /// <summary>
+        /// Start the EventSource. 
+        /// </summary>
+        /// <param name="stopToken">Cancel this token to stop the EventSource.</param>
+        public void Start(CancellationToken stopToken)
+        {
+            if (State == EventSourceState.CLOSED)
+            {
+                mStopToken = stopToken;
+                mTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken);
+                Run();
+            }
+        }
+
+        protected void Run()
+        {
+            if (mTokenSource.IsCancellationRequested && CurrentState.State == EventSourceState.CLOSED)
+                return;
+
+            mCurrentState.Run(this.OnEventReceived, mTokenSource.Token, _headers).ContinueWith(cs =>
+            {
+                CurrentState = cs.Result;
+                Run();
+            });
+        }
+
+        protected void OnEventReceived(ServerSentEvent sse)
+        {
+            if (EventReceived != null)
+            {
+                EventReceived(this, new ServerSentEventReceivedEventArgs(sse));
+            }
+        }
+
+        protected void OnStateChanged(EventSourceState newState)
+        {
+            if (StateChanged != null)
+            {
+                StateChanged(this, new StateChangedEventArgs(newState));
+            }
+        }
+    }
+}

+ 14 - 0
Client/SSE/EventSourceState.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public enum EventSourceState
+    {
+        CONNECTING,
+        OPEN,
+        CLOSED 
+    }
+}

+ 15 - 0
Client/SSE/IConnectionState.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public interface IConnectionState
+    {
+        EventSourceState State { get; }
+        Task<IConnectionState> Run(Action<ServerSentEvent> MsgReceivedCallback, CancellationToken cancelToken, Dictionary<string, string> headers);
+    }
+}

+ 17 - 0
Client/SSE/IServerResponse.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace Client.SSE
+{
+    public interface IServerResponse
+    {
+        HttpStatusCode StatusCode { get; }
+
+        System.IO.Stream GetResponseStream();
+
+        Uri ResponseUri { get; }
+    }
+}

+ 15 - 0
Client/SSE/IWebRequester.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public interface IWebRequester
+    {
+        Task<IServerResponse> Get(Uri url, Dictionary<string, string> headers = null);
+
+    }
+}

+ 12 - 0
Client/SSE/IWebRequesterFactory.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public interface IWebRequesterFactory
+    {
+        IWebRequester Create();
+    }
+}

+ 39 - 0
Client/SSE/ServerResponse.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerResponse : IServerResponse
+    {
+        private System.Net.HttpWebResponse mHttpResponse;
+
+        public ServerResponse(System.Net.WebResponse webResponse)
+        {
+            this.mHttpResponse = webResponse as HttpWebResponse;
+        }
+
+        public HttpStatusCode StatusCode
+        {
+            get
+            {
+                return mHttpResponse.StatusCode;
+            }
+        }
+
+        public System.IO.Stream GetResponseStream()
+        {
+            return mHttpResponse.GetResponseStream();
+        }
+
+        public Uri ResponseUri
+        {
+            get
+            {
+                return mHttpResponse.ResponseUri;
+            }
+        }
+    }
+}

+ 26 - 0
Client/SSE/ServerSentEvent.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerSentEvent
+    {
+        public string LastEventId { get; set; }
+        public string EventType { get; set; }
+        public string Data { get; set; }
+        public int? Retry { get; set; }
+
+        public override string ToString()
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.Append("EventType: ").Append(EventType).AppendLine();
+            sb.Append("Data: ").Append(Data).AppendLine();
+            sb.Append("LastEventId: ").Append(LastEventId).AppendLine();
+            if(Retry.HasValue)
+                sb.Append("Retry: ").Append(Retry.Value).AppendLine();
+            return sb.ToString();
+        }
+    }
+}

+ 17 - 0
Client/SSE/ServerSentEventReceivedEventArgs.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class ServerSentEventReceivedEventArgs : EventArgs
+    {
+        public ServerSentEvent Message { get; private set; }
+        public ServerSentEventReceivedEventArgs(ServerSentEvent message)
+        {
+            Message = message;
+        }
+
+    }
+}

+ 16 - 0
Client/SSE/StateChangedEventArgs.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Client.SSE
+{
+    public class StateChangedEventArgs : EventArgs
+    {
+        public EventSourceState State { get; private set; }
+        public StateChangedEventArgs(EventSourceState state)
+        {
+            State = state;
+        }
+    }
+}

+ 47 - 0
Client/SSE/StringSplitter.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class StringSplitter
+    {
+        public static string[] SplitIntoLines(string text, out string remainingText)
+        {
+            List<string> lines = new List<string>();
+
+            //bool endFound = false;
+            //bool searchingForFirstChar = true;
+            int lineLength = 0;
+            char previous = char.MinValue;
+            for (int i = 0; i < text.Length; i++)
+            {
+                char c = text[i];
+                if (c == '\n' || c == '\r')
+                {
+                    bool isCRLFPair = previous=='\r' && c == '\n';
+
+                    if (!isCRLFPair)
+                    {
+                        string line = text.Substring(i - lineLength, lineLength);
+                        lines.Add(line);
+                    }
+
+                    lineLength = 0;
+                }
+                else
+                {
+                    lineLength++;
+                }
+                previous = c;
+            }
+
+            // Save the last chars that is not followed by a lineending.
+            remainingText = text.Substring(text.Length - lineLength);
+
+            return lines.ToArray();
+        }
+    }
+}

+ 38 - 0
Client/SSE/WatchDog.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class Watchdog
+    {
+        private long _timeout;
+        private Timer _timer;
+        public event EventHandler TimerExpired;
+
+        public void Start()
+        {
+            _timer = new Timer(new TimerCallback(OnTimerExpired), null, 0, _timeout);
+        }
+
+        public void Reset()
+        {
+            _timer.Change(0, _timeout);
+        }
+
+        private void OnTimerExpired(object State)
+        {
+            _timer.Change(Timeout.Infinite, Timeout.Infinite);
+            if (TimerExpired != null)
+                TimerExpired(this, new EventArgs());
+        }
+
+        public Watchdog(long timeout)
+        {
+            if (timeout < 1) throw new ArgumentOutOfRangeException("timeout", "timeout muste be greater than zero.");
+        }
+    }
+}

+ 30 - 0
Client/SSE/WebRequester.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class WebRequester : IWebRequester
+    {
+        public Task<IServerResponse> Get(Uri url, Dictionary<string, string> headers = null)
+        {
+            var wreq = (HttpWebRequest)WebRequest.Create(url);
+            wreq.Method = "GET";
+            wreq.Proxy = null;
+
+            if (headers != null)
+            {
+                foreach (var header in headers)
+                {
+                    wreq.Headers.Add(header.Key, header.Value);
+                }
+            }
+
+            var taskResp = Task.Factory.FromAsync<WebResponse>(wreq.BeginGetResponse,
+                wreq.EndGetResponse, null).ContinueWith<IServerResponse>(t => new ServerResponse(t.Result));
+            return taskResp;
+
+        }
+    }
+}

+ 16 - 0
Client/SSE/WebRequesterFactory.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Client.SSE
+{
+    public class WebRequesterFactory : IWebRequesterFactory
+    {
+        public IWebRequester Create()
+        {
+            return new WebRequester();
+        }
+    }
+}

+ 1 - 0
TEAMModelOS.SDK/TEAMModelOS.SDK.csproj

@@ -21,6 +21,7 @@
     <PackageReference Include="ClouDASLibx" Version="1.2.7" />
     <PackageReference Include="DocumentFormat.OpenXml" Version="2.12.3" />
     <PackageReference Include="HtmlAgilityPack" Version="1.11.32" />
+    <PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="6.0.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.10" />
     <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
     <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.4" />

+ 7 - 1
TEAMModelOS.sln

@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelGrpc", "TEAMModelG
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelFunction", "TEAMModelFunction\TEAMModelFunction.csproj", "{78470113-6261-4F9A-9EF3-E315F060813D}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TEAMModelAPI", "TEAMModelAPI\TEAMModelAPI.csproj", "{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TEAMModelAPI", "TEAMModelAPI\TEAMModelAPI.csproj", "{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{E1A7F6EA-319E-4582-A800-A04DEB8284D9}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +41,10 @@ Global
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2146FEEC-7178-4141-A8C7-CBEBAEE404A6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E1A7F6EA-319E-4582-A800-A04DEB8284D9}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

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

@@ -31,6 +31,9 @@ export default {
 	ShareAgree:function(data) {
 	    return post('/teacher/share/agree-share', data)
 	},
+	CheckLink:function(data) {
+	    return post('/common/syllabus/check-link', data)
+	},
 	// 查找知识块数量
 	FindBlockCount: function (data) {
 		return post('/knowledges/find-count', data)

Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/icon_student.svg


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/icon_teacher.svg


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
TEAMModelOS/ClientApp/src/assets/login/ies5_logo_2.svg


BIN
TEAMModelOS/ClientApp/src/assets/login/login_bg.jpg


+ 1 - 1
TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.less

@@ -9,7 +9,7 @@
     &-mark{
         position: fixed;
         left:30px;
-        top:30px;
+        top:20px;
     }
     &-schoolName{
         position: fixed;

+ 23 - 10
TEAMModelOS/ClientApp/src/components/public/frontEndMain/Index.vue

@@ -1,12 +1,22 @@
 <style lang="less" scoped>
 @import "./Index.less";
 .footer-info-item {
-    color: white;
+    color: #b8b8b8;
     margin-right: 40px;
-    font-size: 15px;
+    font-size: 12px;
+}
+.login-title {
+    text-align: left;
+    color: #e0e0e0;
+    font-size: 19px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    margin-left: 20px;
+    letter-spacing: 1px;
+    display: inline-block;
 }
 </style>
-
 <style lang="less">
 .demo-spin-icon-load {
     animation: ani-demo-spin 1s linear infinite;
@@ -60,7 +70,10 @@
 <template>
     <div id="login" class="login" :style="{backgroundImage:bgImg}">
         <div class="login-mark">
-            <img width="125" src="@/assets/login/1-2.png">
+            <img height="42" src="@/assets/login/ies5_logo_2.svg">
+            <span class="login-title">
+                醍摩豆云平台
+            </span>
         </div>
         <div calss="login-schoolName">
             {{ schoolName }}
@@ -110,7 +123,7 @@ export default {
     data() {
         return {
             schoolName: '',
-            bgImg:''
+            bgImg: ''
         }
     },
     computed: {
@@ -154,23 +167,23 @@ export default {
         this.setLoginSchoolCode()
         // this.bgImg = `url(${require('@/assets/login/1-1.jpg')})`             
     },
-    mounted(){
+    mounted() {
 
     },
     watch: {
         $route: {
             handler(n, o) {
                 if (n.name == 'login') {
-                    this.bgImg = `url(${require('@/assets/login/1-1.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/login_bg.jpg')})`
                 } else if (n.name == 'loginTeacher') {
-                    this.bgImg = `url(${require('@/assets/login/3-1.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/3-1.jpg')})`
                 } else {
-                    this.bgImg = `url(${require('@/assets/login/2-2.jpg')})` 
+                    this.bgImg = `url(${require('@/assets/login/2-2.jpg')})`
                 }
                 console.log(n)
             },
             deep: true,
-            immediate:true
+            immediate: true
         }
     }
 }

+ 3 - 3
TEAMModelOS/ClientApp/src/components/selflearn/NewChooseContent.vue

@@ -106,7 +106,7 @@
             </TabPane>
             <!-- 选择题库 -->
             <TabPane label="题目" name="question" v-if="showQuestion" tab="chooseContent">
-                <div class="tab-wrap">
+                <div class="tab-wrap" v-if="tabName === 'question'">
                     <vuescroll>
                         <ExerciseList ref="exListRef" @chooseQuChange="chooseQuChange"></ExerciseList>
 						<!-- <BaseExerciseList></BaseExerciseList> -->
@@ -115,7 +115,7 @@
             </TabPane>
 			<!-- 选择试卷 -->
 			<TabPane label="试卷" name="paper" v-if="showPaper" tab="chooseContent">
-			    <div class="tab-wrap">
+			    <div class="tab-wrap" v-if="tabName === 'paper'">
 			        <vuescroll>
 						<BasePaperList ref="paperListRef" chooseModel @previewPaper="onPreviewPaper"></BasePaperList>
 			        </vuescroll>
@@ -415,7 +415,7 @@ export default {
                     this.getFileList()
                     break
                 case 'question':
-                    this.queryQuestionByPage()
+                    // this.queryQuestionByPage()
                     break
                 default:
                     break

+ 142 - 47
TEAMModelOS/ClientApp/src/components/syllabus/DragTree.vue

@@ -43,10 +43,10 @@
 		
 		<!-- 新增或者编辑弹窗 -->
 		<Modal v-model="chapterCopyModal" width="500" footer-hide class="tree-modal add-volume-modal choose-content-modal">
-			<div class="modal-header" slot="header">复制节点</div>
+			<div class="modal-header" slot="header">{{ $t('syllabus.tree.copyNode') }}</div>
 			<div class="modal-content">
-				<p class="node-title">当前章节: <span class="node-name">{{ curChapter.title }}</span></p>
-				<p class="node-title" style="display: inline-block;">目标册别:</p>
+				<p class="node-title">{{ $t('syllabus.tree.curChapter') }}: <span class="node-name">{{ curChapter.title }}</span></p>
+				<p class="node-title" style="display: inline-block;">{{ $t('syllabus.tree.targetV') }}:</p>
 				<Select v-model="copyTargetVolume" style="width: 300px;">
 					<Option v-for="(item,index) in volumeList" :value="index" :key="index">{{item.name}}</Option>
 				</Select>
@@ -179,16 +179,16 @@
 			onIgnoreShare(data){
 				this.$Modal.confirm({
 					title: this.$t('syllabus.tree.removeTitle'),
-					content: '确认忽略该章节吗?',
+					content: this.$t('syllabus.tree.ignoreTip'),
 					onOk: () => {
 						this.$api.syllabus.ShareAgree({
 							"code": this.$store.state.userInfo.TEAMModelId,
-							"id": data.id,
+							"ids": [data.id],
 							"type": "share",
 							"opt": "ignore"
 						}).then(res => {
 							if(res.status === 200){
-								this.$Message.success('操作成功!')
+								this.$Message.success(this.$t('syllabus.doSuc'))
 								this.$parent.getShareVolumeList(true)
 							}
 						})
@@ -201,7 +201,7 @@
 					this.curChapter = data
 					this.chapterCopyModal = true
 				}else{
-					this.$Message.warning('请先创建您的个人课纲!')
+					this.$Message.warning(this.$t('syllabus.tree.noVTip'))
 				}
 			},
 			/* 复制章节的业务逻辑 */
@@ -216,10 +216,10 @@
 				// 在复制章节过程中 如果章节节点以及子节点有关联试题试卷 则需要询问用户是否进行入库操作
 				if(hasItemOrPaper){
 					this.$Modal.confirm({
-						title: '提示',
-						content: '该章节中有关联试题试卷信息,是否需要同步到您的个人试题试卷库?',
-						okText: '同步并复制',
-						cancelText: '不需要',
+						title: this.$t('syllabus.tip'),
+						content: this.$t('syllabus.tree.hasItemTip'),
+						okText: this.$t('syllabus.tree.okText'),
+						cancelText: this.$t('syllabus.tree.cancelText'),
 						onOk: async () => {
 							let copyResult = await this.doCopyResources(allRNodes,true)
 							console.log('复制后的回调',copyResult)
@@ -241,16 +241,17 @@
 				console.log(JSON.stringify(this.flatRNodes))
 				let upsertParams = [{
 					id: this.curChapter.id,
+					codeval: this.curCode,
 					volumeId: targetVolume.id,
 					scope: targetVolume.scope,
 					trees: [this.refreshCopyChapter(this.curChapter)]
 				}]
 				this.$api.syllabus.UpsertTree(upsertParams).then((res) => {
 					if (!res.error && res) {
-						this.$Message.success("复制成功");
+						this.$Message.success(this.$t('syllabus.tree.copySuc'));
 						this.chapterCopyModal = false
 					} else {
-						this.$Message.error("获取数据失败");
+						this.$Message.error(this.$t('syllabus.saveFailTip'));
 					}
 				}).catch(e => {
 					this.$Message.error(e);
@@ -470,49 +471,134 @@
 			// 删除节点操作
 			remove(node, data) {
 				let isFirstLevel = this.isFirstLevel(data)
+				let rNodeLinks = isFirstLevel ? this.getAllRNodes(data).map(i => i.link) : data.rnodes.map(i => i.link)
+				console.log(rNodeLinks)
 				this.$Modal.confirm({
 					title: this.$t('syllabus.tree.removeTitle'),
 					content: this.$t('syllabus.tree.removeConfirm'),
-					onOk: () => {
+					onOk: async () => {
 						const parent = node.parent
 						const children = parent.data.children || parent.data
 						const index = children.findIndex(d => d.id === data.id)
-						// 如果是删除的第一层的节点 则直接访问API进行删除 如果不是 则记录子节点的PID
-						if(isFirstLevel){
-							this.$api.syllabus.DeleteTree({
-								id:data.id,
-								code:this.volume.id,
-								scope:this.volume.scope
-							}).then(res => {
-								if (!res.error) {
-									if(res.code === 404){
-										this.$parent.modifyIdArr  = this.$parent.modifyIdArr.filter(i => i !== data.id)
-									}
-									children.splice(index, 1)
-									this.$nextTick().then(() => {
-										const firstNode = document.querySelector('.el-tree-node')
-										firstNode.click();
-									})
-									this.$Message.success(this.$t('syllabus.tree.removeSucTip'))
-								} else {
-									this.$Message.warning(res.error);
-								}
-							}).catch(err => {
-								this.$Message.error(err);
-							})
-						}else{
-							children.splice(index, 1)
-							this.$Message.success(this.$t('syllabus.tree.removeSucTip'))
-							this.$parent.hasModify = true
-							this.$emit('addModifyId',this.getChapterIdById(data.id))
+						this.$parent.curNode = {
+							id:'',
+							rnodes:[]
 						}
+						this.doDeleteBlobResource(rNodeLinks,data).then(result => {
+							// 如果是删除的第一层的节点 则直接访问API进行删除 如果不是 则记录子节点的PID
+							if(isFirstLevel){
+								this.$api.syllabus.DeleteTree({
+									id:data.id,
+									code:this.curCode,
+									scope:this.volume.scope
+								}).then(res => {
+									if (!res.error) {
+										if(res.code === 404){
+											this.$parent.modifyIdArr  = this.$parent.modifyIdArr.filter(i => i !== data.id)
+										}
+										children.splice(index, 1)
+										this.$nextTick().then(() => {
+											const firstNode = document.querySelector('.el-tree-node')
+											firstNode && firstNode.click();
+										})
+										this.$Message.success(this.$t('syllabus.tree.removeSucTip'))
+									} else {
+										this.$Message.warning(res.error);
+									}
+								}).catch(err => {
+									this.$Message.error(err);
+								})
+							}else{
+								children.splice(index, 1)
+								this.$Message.success(this.$t('syllabus.tree.removeSucTip'))
+								this.$parent.hasModify = true
+								this.$emit('addModifyId',this.getChapterIdById(data.id))
+								this.$nextTick().then(() => {
+									const firstNode = document.querySelector('.el-tree-node')
+									firstNode && firstNode.click();
+								})
+							}
+						})
+					}
+				})
+			},
+			/* 删除对应关联的资源 */
+			doDeleteBlobResource(links,data){
+				return new Promise((r,j) => {
+					if(!links.length) r(200)
+					let syllabusLinks = links.filter(i => i.includes('/syllabus/'))
+					if(syllabusLinks.length){
+						this.$api.syllabus.CheckLink({
+							"links": syllabusLinks,
+							"code": this.curCode,
+							"scope": this.isSchool ? 'school' : 'private'
+						}).then(res => {
+							console.log(res)
+							let needDeleteLink = []
+							let curVolumeId = this.volume.id
+							let curChapterId = this.getChapterIdById(data.id)
+							let curNodeId = data.id
+							// 如果删除的是章节 则判断是否有其他章节引用了资源 如果没有 则要进行blob删除
+							if(curChapterId === curNodeId){
+								syllabusLinks.forEach(i => {
+									if(!res.links.filter(j => j.link === i && j.syllabusId !== curChapterId).length){
+										needDeleteLink.push(i)
+									}
+								})
+							}else{
+								syllabusLinks.forEach(i => {
+									if(!res.links.filter(j => j.link === i && j.syllabusId !== curChapterId && j.treeid !== curNodeId).length){
+										needDeleteLink.push(i)
+									}
+								})
+							}
+							if(needDeleteLink.length){
+								let promiseArr = []
+								needDeleteLink.forEach(i => {
+									promiseArr.push(this.deleteBlobPrefix(i))
+								})
+								Promise.all(promiseArr).then(result => {
+									r(result)
+								}).catch(err => {
+									j(err)
+								})
+							}else{
+								r(200)
+							}
+						}).catch(e => {
+							j(e)
+						})
+					}else{
+						r(200)
 					}
 				})
 			},
+			
+			/* 删除blob指定试题目录下所有 */
+			deleteBlobPrefix(link) {
+				return new Promise((resolve, reject) => {
+					this.$api.blob.deletePrefix({
+						"cntr": this.curCode,
+						"prefix": link.substring(1)
+					}).then(
+						(res) => {
+							if (!res.error) {
+								resolve(200)
+							} else {
+								resolve(500)
+							}
+						},
+						(err) => {
+							reject(err)
+						}
+					)
+				})
+			},
+			
 			// 点击添加展开弹窗
 			onAddNode(node,data, e) {
 				if(node.level === 3){
-					this.$Message.warning('每个章节最多只能有3级!')
+					this.$Message.warning(this.$t('syllabus.tree.nodeCountTip'))
 					return
 				}
 				this.isEditItem = false
@@ -575,6 +661,7 @@
 			/* 获取整个树的章节与子节点归属 */
 			getAllChild(arr){
 				let result = []
+				if(!arr.length) return result
 				arr.forEach(item => {
 					result.push({
 						chapterId:item.id,
@@ -631,11 +718,13 @@
 					return nodeData.creatorId === this.$store.state.userInfo.TEAMModelId
 				}
 			},
+			curCode() {
+				return this.isSchool ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
+			},
 			inShareView(){
 				return !this.isSchool && this.$parent.activeTab === 'fromShare'
 			},
 			volumeList(){
-				console.log(this.$parent)
 				return this.$parent.myVolumeList
 			}
 		},
@@ -643,15 +732,21 @@
 			// 监听课纲数据变化
 			treeData: {
 				handler: function(n, o) {
+					console.log(n);
 					// 以下为拼接树形数据以及册别数据
 					this.treeDatas = n.map(i => {
-						i.trees[0].auth = i.auth
-						return i.trees[0]
+						if(i.trees.length){
+							i.trees[0].auth = i.auth
+							return i.trees[0]
+						}else{
+							return {}
+						}
 					})
+					console.log(this.treeDatas);
 					this.getAllChild(this.treeDatas)
 					this.$nextTick().then(() => {
 						const firstNode = document.querySelector('.el-tree-node')
-						firstNode.click();
+						firstNode && firstNode.click();
 					})
 				},
 				immediate: true,

+ 17 - 17
TEAMModelOS/ClientApp/src/components/syllabus/InviteTeacher.vue

@@ -1,19 +1,19 @@
 <template>
 	<div class="it-container">
 		<div class="node-info">
-			<span>当前选择章节:</span>
+			<span>{{ $t('syllabus.curChapter') }}:</span>
 			<span class="node-name">{{ nodeInfo.title }}</span>
 			<br>
 			<br>
 			<div style="display: inline-block;" v-if="!isSchool">
-				<span>当前章节已分享教师:</span>
+				<span>{{ $t('syllabus.sharedTeacher') }}:</span>
 				<!-- <span v-for="(item,index) in nodeInfo.auth" :key="item.tmdid" class="share-item">{{ item.tmdname }}( {{ item.tmdid }})</span> -->
-				<span v-if="!nodeInfo.auth || !nodeInfo.auth.length" style="color: #dacaca;">暂无数据</span>
+				<span v-if="!nodeInfo.auth || !nodeInfo.auth.length" style="color: #dacaca;">{{ $t('syllabus.noData') }}</span>
 				<Tag closable color="success" v-for="(item,index) in nodeInfo.auth" :key="item.tmdid" @on-close="doDelShare(item,index)">{{ item.tmdname }}( {{ item.tmdid }})</Tag>
 			</div>
 		</div>
 		<div class="search-wrap" v-if="isSchool">
-			<Input v-model="searchVal"  placeholder="输入教师名字或ID查询..."
+			<Input v-model="searchVal"  :placeholder="$t('syllabus.itPlace1')"
 			icon="ios-close-circle-outline" @on-click="onCloseSearch" @on-change="onSearchChange"/>
 		</div>
 		<div class="teacher-wrap">
@@ -32,17 +32,17 @@
 			</Table>
 			<div class="search-id-wrap" v-else>
 				<div class="id-search">
-					<Input v-model="searchIdVal" placeholder="搜索教师..." search @on-search="onIdSearch"/>
+					<Input v-model="searchIdVal" :placeholder="$t('syllabus.itPlace2')" search @on-search="onIdSearch"/>
 					<p v-html="$t('teachermgmt.addTeacher.content')"></p>
-					<p v-if="curTeacher && curTeacher.id" class="search-result-text">搜索结果</p>
+					<p v-if="curTeacher && curTeacher.id" class="search-result-text">{{ $t('syllabus.searchResult') }}</p>
 					<div v-if="curTeacher && curTeacher.id" class="search-result-wrap">
 						<PersonalPhoto :name="curTeacher.name" :picture="curTeacher.picture" />
 						<p class="t-name">{{ curTeacher.name }}</p>
 						<p class="t-id">{{ curTeacher.id }}</p>
 					</div>
-					<p v-if="!curTeacher && hasSearchResult" class="search-none">暂未查询到相关结果</p>
-					<p v-if="hasSearchResult" class="re-search" @click="onReSearch">重新搜索</p>
-					<Button type="success" :laading="isShareLoading" v-if="hasSearchResult"  @click="doShare">确认分享</Button>
+					<p v-if="!curTeacher && hasSearchResult" class="search-none">{{ $t('syllabus.noSearchResult') }}</p>
+					<p v-if="hasSearchResult" class="re-search" @click="onReSearch">{{ $t('syllabus.reSearch') }}</p>
+					<Button type="success" :laading="isShareLoading" v-if="hasSearchResult"  @click="doShare">{{ $t('syllabus.confirmShare') }}</Button>
 				</div>
 			</div>
 		</div>
@@ -102,7 +102,7 @@
 					  }
 					},
 					{
-						title: '是否共编',
+						title: this.$t('syllabus.isCoEdit'),
 						key:'action',
 						slot: 'action'
 					}
@@ -166,8 +166,8 @@
 			handleBeforeChange(){
 				return new Promise((resolve) => {
 					this.$Modal.confirm({
-						title: '修改确认',
-						content: '确认修改当前用户的共编状态?',
+						title: this.$t('syllabus.modifyTip'),
+						content: this.$t('syllabus.modifyText'),
 						onOk: () => {
 							resolve();
 						}
@@ -177,7 +177,7 @@
 			/* 修改共编权限 */
 			onSwitchChange(val,isAdd){
 				this.sendShareApi(val.id,val.name,false,isAdd).then(res => {
-					this.$Message.success(isAdd ? '授权成功!' : '取消授权成功!')
+					this.$Message.success(isAdd ? this.$t('syllabus.authSuc') : this.$t('syllabus.authFail'))
 					if(isAdd){
 						this.nodeInfo.auth.push({
 							tmdid:val.id,
@@ -196,7 +196,7 @@
 				this.isShareLoading = true
 				this.sendShareApi(val.id,val.name,true,true).then(res => {
 					setTimeout(() => {
-						this.$Message.success('分享成功!')
+						this.$Message.success(this.$t('syllabus.shareSuc'))
 						if(!this.nodeInfo.auth.map(i => i.tmdid).includes(val.id)){
 							this.nodeInfo.auth.push({
 								tmdid:val.id,
@@ -215,12 +215,12 @@
 			/* 取消分享操作 */
 			doDelShare(val,index){
 				this.$Modal.confirm({
-					title: '取消分享',
-					content: '确认取消对' + val.tmdname + '的分享?',
+					title: this.$t('syllabus.cancelShare'),
+					content:this.$t('syllabus.cancelTip1') + val.tmdname + this.$t('syllabus.cancelTip2'),
 					onOk: () => {
 						this.sendShareApi(val.tmdid,val.tmdname,true,false).then(res => {
 							this.nodeInfo.auth.splice(index,1)
-							this.$Message.success('取消分享成功!')
+							this.$Message.success(this.$t('syllabus.shareFail'))
 							this.$forceUpdate()
 						})
 					}

+ 1 - 0
TEAMModelOS/ClientApp/src/locale/lang/zh-CN/survey.js

@@ -9,6 +9,7 @@ export default {
 	save:'保存问卷',
 	cancelEdit:'取消编辑',
 	surveyResult:'问卷数据',
+	surveyProgress:'问卷进度',
 	addItem:'新增题目',
 	single:'单选题',
 	multiple:'多选题',

+ 46 - 1
TEAMModelOS/ClientApp/src/locale/lang/zh-CN/syllabus.js

@@ -1,8 +1,17 @@
 export default{
+	noPreview:'该类型文件不支持预览!',
 	praviteSyllabus:'个人课纲',
+	fromCreate:'我创建的课纲',
+	fromShare:'他人分享的课纲',
 	btnSave:'存储变更',
 	volumeList:'册别清单',
+	search:'搜索',
+	edit:'编辑',
+	delete:'删除',
+	add:'新建册别',
 	place1:'输入册别名称...',
+	isDel:'已失效',
+	deleteDelV:'删除失效册别',
 	syllabusMenu:'课纲目录',
 	relate:'关联资源',
 	addResource:'添加资源',
@@ -31,11 +40,17 @@ export default{
 	count:'题数',
 	deleteVolume:'删除册别',
 	deleteConfirm:'确认删除该册别?',
+	deleteIsDelConfirm:'确认删除该失效册别?',
 	deleteSuc:'删除成功',
 	deleteFail:'删除失败',
+	isDelTip:'该册别已被创建者移除',
 	uploadSuc:'上传成功',
+	tip:'提示',
+	copyTip1:'校本试卷库中已存在名称为',
+	copyTip2:'的试卷,是否继续操作覆盖原试卷?',
 	removeConfirm:'确定要移除该资源吗?',
 	doSuc:'操作成功',
+	saveFailTip:'保存失败',
 	isExistVolume:'已存在相同册别',
     tree:{
 		hasResource:'有关联资源',
@@ -58,5 +73,35 @@ export default{
 		nodeNameTip:'节点名称不能为空',
 		editSucTip:'编辑成功',
 		addSucTip:'添加成功',
-	}
+		copyNode:'复制节点',
+		curChapter:'当前章节',
+		targetV:'目标册别',
+		ignoreTip:'确认忽略该章节?',
+		noVTip:'请先创建您的个人课纲!',
+		hasItemTip:'该章节中有关联试题试卷信息,是否需要同步到您的个人试题试卷库?',
+		okText:'同步并复制',
+		cancelText:'不需要',
+		copySuc:'复制成功',
+		nodeCountTip:'每个章节最多只能有3级!'
+	},
+	curChapter:'当前选择章节',
+	sharedTeacher:'当前章节已分享教师',
+	noData:'暂无数据',
+	itPlace1:'输入教师名字或ID查询...',
+	itPlace2:'搜索教师...',
+	searchResult:'搜索结果',
+	noSearchResult:'暂未查询到相关结果',
+	reSearch:'重新搜索',
+	confirmShare:'确认分享',
+	isCoEdit:'是否共编',
+	modifyTip:'修改确认',
+	modifyText:'确认修改当前用户的共编状态?',
+	authSuc:'授权成功',
+	authFail:'取消授权成功',
+	shareSuc:'分享成功',
+	shareFail:'取消分享成功',
+	cancelShare:'取消分享',
+	cancelTip1:'确认取消对',
+	cancelTip2:'的分享?',
+	
 }

+ 1 - 0
TEAMModelOS/ClientApp/src/locale/lang/zh-TW/survey.js

@@ -9,6 +9,7 @@ export default {
 	save: '儲存問卷',
 	cancelEdit: '取消編輯',
 	surveyResult: '問卷數據',
+	surveyProgress:'問卷進度',
 	addItem: '新增題目',
 	single: '單選題',
 	multiple: '複選题',

+ 46 - 2
TEAMModelOS/ClientApp/src/locale/lang/zh-TW/syllabus.js

@@ -1,8 +1,17 @@
 export default {
+	noPreview:'該類型檔案不支持預覽!',
 	praviteSyllabus: '個人課綱',
+	fromCreate: '我創建的課綱',
+	fromShare: '他人分享的課綱',
 	btnSave: '存儲變更',
 	volumeList: '册別清單',
+	search: '蒐索',
+	edit: '編輯',
+	delete: '删除',
+	add: '新建册別',
 	place1: '輸入册別名稱…',
+	isDel: '已失效',
+	deleteDelV: '删除失效册別',
 	syllabusMenu: '課綱目錄',
 	relate: '關聯資源',
 	addResource: '添加資源',
@@ -31,15 +40,21 @@ export default {
 	count: '題數',
 	deleteVolume: '删除册別',
 	deleteConfirm: '確認删除該册別?',
+	deleteIsDelConfirm: '確認删除該失效册別?',
 	deleteSuc: '删除成功',
 	deleteFail: '删除失敗',
+	isDelTip: '該册別已被創建者移除',
 	uploadSuc: '上傳成功',
+	tip: '提示',
+	copyTip1: '校本試卷庫中已存在名稱為',
+	copyTip2: '的試卷,是否繼續操作覆蓋原試卷?',
 	removeConfirm: '確定要移除該資源嗎?',
 	doSuc: '操作成功',
+	saveFailTip: '保存失敗',
 	isExistVolume: '已存在相同册別',
 	tree: {
 		hasResource: '有關聯資源',
-		hasCoEdit:'有共編許可權',
+		hasCoEdit: '有共編許可權',
 		edit: '編輯',
 		add: '添加',
 		remove: '删除',
@@ -58,5 +73,34 @@ export default {
 		nodeNameTip: '節點名稱不能為空',
 		editSucTip: '編輯成功',
 		addSucTip: '添加成功',
-	}
+		copyNode: '複製節點',
+		curChapter: '當前章節',
+		targetV: '目標册別',
+		ignoreTip: '確認忽略該章節?',
+		noVTip: '請先創建您的個人課綱!',
+		hasItemTip: '該章節中有關聯試題試卷資訊,是否需要同步到您的個人試題試卷庫?',
+		okText: '同步並複製',
+		cancelText: '不需要',
+		copySuc: '複製成功',
+		nodeCountTip: '每個章節最多只能有3級!'
+	},
+	curChapter: '當前選擇章節',
+	sharedTeacher: '當前章節已分享教師',
+	noData: '暫無數據',
+	itPlace1: '輸入教師名字或ID査詢…',
+	itPlace2: '蒐索教師…',
+	searchResult: '搜索結果',
+	noSearchResult: '暫未査詢到相關結果',
+	reSearch: '重新搜索',
+	confirmShare: '確認分享',
+	isCoEdit: '是否共編',
+	modifyTip: '修改確認',
+	modifyText: '確認修改當前用戶的共編狀態?',
+	authSuc: '授權成功',
+	authFail: '取消授權成功',
+	shareSuc: '分享成功',
+	shareFail: '取消分享成功',
+	cancelShare: '取消分享',
+	cancelTip1: '確認取消對',
+	cancelTip2: '的分享?',
 }

+ 4 - 0
TEAMModelOS/ClientApp/src/utils/editorLangTw.js

@@ -62,6 +62,10 @@ let editor_tw_config = {
                 },
             },
             panelMenus: {
+				删除:' 删除 ',
+				formula:{
+					插入公式:'插入公式',
+				},
                 emoticon: {
                     默认: '默認',
                     新浪: '新浪',

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

@@ -409,7 +409,7 @@ export default {
 		      // 公式输入插件
 		      constructor(editors) {
 		        const $elem = $(
-		        	'<div class="w-e-menu" style="color:red"><i class="ivu-icon ivu-icon-logo-tumblr" style="font-size: 20px;"></i></div>'
+		        	'<div class="w-e-menu" style="color:red"><i class="icon iconfont icon-function" style="font-size: 20px;font-weight:bold"></i></div>'
 		        );
 		        super($elem, editors);
 		      }

+ 6 - 0
TEAMModelOS/ClientApp/src/utils/evTools.js

@@ -360,6 +360,8 @@ export default {
 			try{
 				let jsonInfo = await $tools.getFile(fullPath)
 				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = 'private'
+				jsonData.code = tmdId
 				// 获取试卷包含的试题数据并包装好
 				if(jsonData.slides && jsonData.slides.length){
 					let promiseArr = []
@@ -423,6 +425,8 @@ export default {
 			try{
 				let jsonInfo = await $tools.getFile(blobHost + paper.blob + '/index.json' + sasString.sas)
 				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = curScope
+				jsonData.code = paper.code
 				// 获取试卷包含的试题数据并包装好
 				if(jsonData.slides && jsonData.slides.length){
 					let promiseArr = []
@@ -489,6 +493,8 @@ export default {
 			try {
 				let jsonInfo = await $tools.getFile(sasString.url + '/' + paper.code + paper.blob + '/index.json' + sasString.sas)
 				let jsonData = JSON.parse(jsonInfo)
+				jsonData.scope = curScope
+				jsonData.code = paper.code
 				// 获取试卷包含的试题数据并包装好
 				if (jsonData.slides && jsonData.slides.length) {
 					jsonData.item = []

+ 2 - 0
TEAMModelOS/ClientApp/src/utils/public.js

@@ -281,9 +281,11 @@ export default {
 						break;
 					case 404:
 						Message.error('未访问到资源!')
+						reject(404)
 						break;
 					case 403:
 						Message.error('授权异常,无法访问!')
+						reject(403)
 						break;		
 					default:
 						break;

+ 1 - 2
TEAMModelOS/ClientApp/src/view/answersheet/index.vue

@@ -393,8 +393,7 @@
 			})
 		},
 		beforeRouteLeave(to, from, next) {
-			if(to.name === 'newSchoolPaper' || to.name === 'newPrivatePaper' || to.name === 'schoplBank' || to.name === 'personalBank'){
-				console.error(to)
+			if(to.name === 'newSchoolPaper' || to.name === 'newPrivatePaper' || to.name === 'schoolBank' || to.name === 'personalBank'){
 				// 设置下一个路由的 meta
 				to.meta.isKeep = true;  // 让 A 缓存,即不刷新
 			}

+ 3 - 0
TEAMModelOS/ClientApp/src/view/evaluation/bank/index.vue

@@ -149,6 +149,9 @@
 				// 设置下一个路由的 meta
 				to.meta.isKeep = false;  // 让 A 缓存,即不刷新
 			}
+			if(to.name === 'answerSheet'){
+				from.meta.isKeep = true
+			}
 			next();
 		},
 		watch: {

+ 14 - 5
TEAMModelOS/ClientApp/src/view/evaluation/components/BaseRepair.vue

@@ -51,9 +51,11 @@
 		    <NewChooseContent :showSyllabus="isFalse"
 		                   :showOther="isFalse"
 		                   :showQuestion="isFalse"
+						   :showPaper="isFalse"
 						   :defaultFiles="curRepair.blobUrl"
 						   ref="chooseContentRef"
-		                   @on-file-change="onSelectFile"></NewChooseContent>
+		                   @on-file-change="onSelectFile"
+						   v-if="isRelatedContent"></NewChooseContent>
 		
 		    <Button class="modal-btn" :loading="isLoading" @click="onConfirmRelate">{{$t('evaluation.confirm')}}</Button>
 		</Modal>
@@ -71,7 +73,7 @@
 			<p style="margin: 15px 2px;">{{ $t('evaluation.repairResourse.link')}}{{ isSiteLink ?  '' : $t('evaluation.repairResourse.tip1')}}</p>
 			
 			<!-- 选择内容 -->
-			<Button type="info" @click="isRelatedContent = true" v-if="isSiteLink">{{$t('evaluation.newExercise.chooseContent')}}</Button>
+			<Button type="info" @click="doSelectContent" v-if="isSiteLink">{{$t('evaluation.newExercise.chooseContent')}}</Button>
 			<!-- 手动输入 -->
 			<Input v-model="curOutLink" v-if="!isSiteLink" :placeholder="$t('evaluation.repairResourse.place2')" @on-enter="onAddOutLink"/>
 			<!-- 链接link列表 -->
@@ -135,6 +137,15 @@
 				this.defaultFiles = []
 			},
 			
+			doSelectContent(){
+				this.isRelatedContent = true
+				this.$nextTick(() => {
+					if(this.$refs.chooseContentRef){
+						this.$refs.chooseContentRef.clickTab('content')
+					}
+				})
+			},
+			
 			/* 回车添加外部资源链接 */
 			onAddOutLink(){
 				if(this.isURL(this.curOutLink)){
@@ -219,9 +230,7 @@
 		},
 
 		mounted() {
-			if(this.$refs.chooseContentRef){
-				this.$refs.chooseContentRef.clickTab('content')
-			}
+			
 
 		},
 		watch:{

+ 6 - 3
TEAMModelOS/ClientApp/src/view/evaluation/index/CreatePaper.vue

@@ -264,6 +264,9 @@
 			/* 给导入的试题 补充最新的试卷学段年级以及科目信息 */
 			refreshImportItems(list) {
 				return new Promise((r, reject) => {
+					console.log(this.schoolInfo);
+					console.log(this.evaluationInfo);
+					console.log(this.evaluationInfo.paperPeriod);
 					let objectiveTypes = ['single', 'multiple']
 					let code = this.isSchool ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo
 						.TEAMModelId
@@ -1166,18 +1169,18 @@
 
 		beforeRouteEnter(to, from, next) {
 			if (from.name === 'answerSheet' && (to.name === 'newSchoolPaper' || to.name === 'newPrivatePaper')) {
-				console.log('xxxxxxxxxxxx',to)
 				to.meta.isKeep = true
 			}
 			next()
 		},
 		beforeRouteLeave(to, from, next) {
 			if(to.name === 'answerSheet'){
-				console.error(to)
-				console.error(from)
 				// 设置下一个路由的 meta
 				from.meta.isKeep = true;  // 让 A 缓存,即不刷新
 			}
+			if(to.name === 'schoolBank' || to.name === 'personalBank'){
+				to.meta.isKeep = false
+			}
 			next();
 		},
 		watch: {

+ 5 - 6
TEAMModelOS/ClientApp/src/view/login/page/Student.vue

@@ -7,7 +7,7 @@
     margin-top: -100px;
 }
 .left-box {
-    padding: 0px 55px;
+    padding: 0px 70px;
     height: 400px;
     display: flex;
     align-items: center;
@@ -23,7 +23,7 @@
     min-width: 600px;
 }
 .student-login-img {
-    width: 180px;
+    width: 140px;
 }
 .teacher-login-title {
     color: white;
@@ -77,10 +77,9 @@
 }
 .client-label {
     color: white;
-    font-size: 30px;
+    font-size: 28px;
     text-align: center;
-    margin-top: -20px;
-    margin-left: -7px;
+    margin-top: 20px;
 }
 </style>
 
@@ -168,7 +167,7 @@
     <div class="loginDiv">
         <div class="left-box">
             <!-- 这张图片需要裁剪顶部 -->
-            <img src="@/assets/login/2-1.png" class="student-login-img" style="margin-top:-30px">
+            <img src="@/assets/login/icon_student.svg" class="student-login-img" style="margin-top:-30px">
             <p class="client-label">学生端</p>
         </div>
         <div class="right-box">

+ 1 - 1
TEAMModelOS/ClientApp/src/view/login/page/Teacher.less

@@ -30,7 +30,7 @@
     }
 }
 .teacher-login-img{
-    width: 180px;
+    width: 110px;
 }
 .title{
     display: flex;

+ 4 - 5
TEAMModelOS/ClientApp/src/view/login/page/Teacher.vue

@@ -7,7 +7,7 @@
     margin-top: -100px;
 }
 .left-box {
-    padding: 0px 55px;
+    padding: 0px 80px;
     height: 370px;
     display: flex;
     flex-direction: column;
@@ -70,10 +70,9 @@
 }
 .client-label {
     color: white;
-    font-size: 30px;
+    font-size: 28px;
     text-align: center;
-    margin-top: -20px;
-    margin-left: -7px;
+    margin-top: 20px;
 }
 </style>
 
@@ -160,7 +159,7 @@
     <div class="loginDiv">
         <div class="left-box">
             <!-- 这张图片需要裁剪顶部 -->
-            <img src="@/assets/login/3-2.png" class="teacher-login-img" style="margin-top:-30px">
+            <img src="@/assets/login/icon_teacher.svg" class="teacher-login-img">
             <p class="client-label">教师端</p>
         </div>
         <div class="right-box" v-show="!qrloginFlag">

+ 172 - 15
TEAMModelOS/ClientApp/src/view/login/test.vue

@@ -71,20 +71,12 @@
 }
 .login-main-wrap {
     display: flex;
-    justify-content: space-evenly;
+    justify-content: center;
     width: 100%;
     height: 600px;
     flex-direction: row;
     align-items: center;
-}
-.login-img-wrap {
-    width: 45%;
-    max-width: 600px;
-    height: 100%;
-    padding: 12px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+    margin-top: -50px;
 }
 
 .login-main-img {
@@ -101,6 +93,7 @@
     justify-content: space-evenly;
     align-items: center;
     margin-top: 0px;
+    margin-left: 2%;
 }
 .enter-box {
     width: 150px;
@@ -129,28 +122,191 @@
     color: white;
     font-size: 200px;
 }
+.login-img-wrap {
+    width: 45%;
+    max-width: 760px;
+    height: 500px;
+    padding: 12px;
+    margin-right: 5%;
+}
+.swiper-item-box {
+    width: 100%;
+    min-height: 500px;
+    .title {
+        font-size: 40px;
+        color: #fff;
+        line-height: 1.1;
+        letter-spacing: 1px;
+        font-weight: 600;
+    }
+    .cont {
+        margin-top: 30px;
+        margin-left: 15px;
+    }
+    .cont-title {
+        font-size: 26px;
+        color: #fff;
+        line-height: 1.1;
+        font-weight: 500;
+        margin-bottom: 7px;
+        letter-spacing: 1px;
+    }
+    .cont-text {
+        font-size: 16px;
+        margin-bottom: 3px;
+        color: #9e9e9e;
+        letter-spacing: 1px;
+        &::before {
+            content: "|";
+            margin-right: 11px;
+            font-weight: 900;
+        }
+    }
+    .border1::before {
+        color: #de5c53;
+    }
+    .border2::before {
+        color: #fcdc6b;
+    }
+    .border3::before {
+        color: #25ca92;
+    }
+    .border4::before {
+        color: #3eaefe;
+    }
+
+    .link {
+        margin-top: 50px;
+    }
+}
+.logintype {
+    .title {
+        display: block;
+        margin-bottom: 17px;
+        letter-spacing: 1px;
+        color: #9e9e9e;
+        font-size: 13px;
+    }
+    .login-box {
+        min-width: 520px;
+        background-color: rgba(66, 69, 81, 0.9);
+        width: 100%;
+        height: 100px;
+        display: flex;
+        align-items: center;
+        border-left: 5px solid #3dadff;
+        border-radius: 5px;
+        border-right: 5px solid #3dadff;
+        margin-bottom: 25px;
+        cursor: pointer;
+        box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
+        &:hover {
+            background-color: rgba(75, 79, 96, 0.9);
+        }
+    }
+    .login-box-icon {
+        width: 20%;
+        float: left;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        border-right: 1px solid #616161;
+    }
+    .login-box-cont {
+        width: 80%;
+        float: left;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 17px;
+    }
+    .main {
+        color: #bdbdbd;
+        font-size: 12px;
+    }
+    .subtitle {
+        display: block;
+        font-size: 30px;
+        color: #fff;
+        font-weight: 600;
+        margin-bottom: 6px;
+    }
+}
 </style>
 
 <template>
     <div class="login-main-wrap">
         <div class="login-img-wrap">
-            <img src="@/assets/login/1-3.png" class="login-main-img">
+            <!-- <img src="@/assets/login/1-3.png" class="login-main-img"> -->
+            <!-- 轮播区域 -->
+            <Carousel v-model="curPage" loop dots="outside" arrow="never">
+                <CarouselItem>
+                    <div class="swiper-item-box">
+                        <div class="title">
+                            歡迎來到醍摩豆 5
+                            <br>
+                            新時代智慧教育之旅 就此展開
+                        </div>
+                        <div class="cont">
+                            <div class="cont-title">HiTeach 5 智慧教學系統</div>
+                            <p class="cont-text border1">實踐差異化教學,現代化因材施教</p>
+                            <p class="cont-text border2">線上線下,混和式教學系統首選</p>
+                            <p class="cont-text border3">合作學習,素養導向教學的全面應用</p>
+                            <p class="cont-text border4">看見每個學生思考</p>
+                        </div>
+                        <div class="cont">
+                            <div class="cont-title">醍摩豆雲平台 IES 5</div>
+                            <p class="cont-text border1">新架構與新技術,速度升級更有感</p>
+                            <p class="cont-text border2">兼容各式媒體與檔案,雲端資源庫隨取即用</p>
+                            <p class="cont-text border3">題庫組卷、閱卷系統,評量應用更多元</p>
+                            <p class="cont-text border4">博拉圖學情分析,學生成績精準提升</p>
+                        </div>
+                        <div class="link">
+                            <a target="_block" href="https://www.habook.com/zh-tw/news.php?act=view&amp;id=408">
+                                了解更多關於醍摩豆 5
+                                <i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 19px;"></i>
+                            </a>
+                        </div>
+                    </div>
+
+                </CarouselItem>
+            </Carousel>
         </div>
         <div class="login-enter-wrap">
-            <div class="enter-box" @click="loginTo('teacher')">
+            <div class="logintype">
+                <span class="title">請選擇您的身份進行登入</span>
+                <div class="login-box" @click="loginTo('teacher')">
+                    <div class="login-box-icon"><img width="40" src="@/assets/login/icon_teacher.svg"></div>
+                    <div class="login-box-cont">
+                        <div class="main">
+                            <span class="subtitle">教師身份</span>
+                            以教師身份登入IES 5智慧教學服務,即刻存取雲端服務
+                        </div>
+                        <i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 35px; color: rgb(189, 189, 189);"></i>
+                    </div>
+                </div>
+                <div class="login-box" @click="loginTo('student')">
+                    <div class="login-box-icon"><img width="40" src="@/assets/login/icon_student.svg"></div>
+                    <div class="login-box-cont">
+                        <div class="main"><span class="subtitle">學生身份</span> 以學生身份登入AClass ONE智慧學伴服務,完成學習任務與測驗 </div><i class="ivu-icon ivu-icon-ios-arrow-forward" style="font-size: 35px; color: rgb(189, 189, 189);"></i>
+                    </div>
+                </div>
+                <div class="link" style="text-align: right; letter-spacing: 1px;"><span style="margin-right: 20px;">還沒有帳號嗎?</span>點此<a href="/regist" class="" style="text-decoration: underline; color: rgb(41, 114, 169);">免費註冊</a></div>
+            </div>
+            <!-- <div class="enter-box" @click="loginTo('teacher')">
                 <img style="display: block;width: 100%;" src="@/assets/login/1-5.png">
-                <!-- <Icon custom="iconfont icon-teacher" class="icon-to-img"></Icon> -->
                 <p class="enter-type-btn">
                     教师端
                 </p>
             </div>
             <div class="enter-box" @click="loginTo('student')">
                 <img style="display: block;width: 100%;" src="@/assets/login/1-4.png">
-                <!-- <Icon custom="iconfont icon-student1" class="icon-to-img"></Icon> -->
                 <p class="enter-type-btn">
                     学生端
                 </p>
-            </div>
+            </div> -->
         </div>
         <!-- 提示視窗 -->
         <Modal v-model="identityFlag" width="360" :mask-closable="false" :closable="false" class-name="identityModal">
@@ -169,6 +325,7 @@ import { User } from '@/service/User'
 export default {
     data() {
         return {
+            curPage: 0,
             defaultSchool: {
                 name: '',
                 code: ''

+ 10 - 7
TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.less

@@ -197,7 +197,7 @@
 			.syllabus-tree-box{
 				height: 100%;
 				overflow: auto;
-				padding-bottom: 80px;
+				padding-bottom: 50px;
 			}
 			
 			.node-resource-box{
@@ -208,21 +208,23 @@
 				
 				.node-resource-item{
 					position: relative;
-					border: 1px solid #5d5d5d;
-					padding: 15px 20px;
-					margin-top: 5px;
+					border-bottom: 1px solid #3a3a3a;
+					padding: 10px 20px;
 					color: #DDDDDD;
 					display: flex;
 					align-items: center;
-					background: #282828;
-					box-shadow: inset 0px 0px 0px 4px #2d2d2d;
 					
 					&:hover{
+						background-color: #1d1d1d;
 						.node-resource-tools{
 							display: flex;
 						}
 					}
 					
+					&:last-child{
+						border-bottom: none;
+					}
+					
 					img{
 						width: 25px;
 						height: 25;
@@ -234,6 +236,7 @@
 						right: 30px;
 						align-items: center;
 						display: none;
+						font-size: 12px;
 						
 						.node-resource-tool{
 							display: flex;
@@ -244,7 +247,7 @@
 						
 						.ivu-icon{
 							color: #ddd;
-							font-size: 20px;
+							font-size: 16px;
 							margin-right: 5px;
 						}
 					}

Diferenças do arquivo suprimidas por serem muito extensas
+ 374 - 219
TEAMModelOS/ClientApp/src/view/syllabus/Syllabus.vue


+ 1 - 1
TEAMModelOS/ClientApp/src/view/teachcontent/index.vue

@@ -970,7 +970,7 @@ export default {
                     "name": this.routerScope == 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId,
                     "type": this.activeType,
                     "scope": this.routerScope,
-                    "periodId": this.filterPeriod
+                    "periodId": this.routerScope == 'school' ? this.filterPeriod : ''
                 }
                 this.$api.blob.listBlobInfo(rd, this.urlString, this.containerName).then(
                     res => {

+ 161 - 0
TEAMModelOS/Controllers/Client/HiScanController.cs

@@ -0,0 +1,161 @@
+using Azure.Cosmos;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using TEAMModelOS.Models.Dto;
+using TEAMModelOS.SDK.Models;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.Context.Constant.Common;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.AzureCosmos.Inner;
+using TEAMModelOS.SDK.Extension;
+using TEAMModelOS.SDK.Helper.Common.CollectionHelper;
+using TEAMModelOS.SDK.Helper.Common.StringHelper;
+using TEAMModelOS.Models;
+using Microsoft.Extensions.Options;
+using TEAMModelOS.SDK.Models.Cosmos;
+using Microsoft.AspNetCore.Authorization;
+using TEAMModelOS.Filter;
+using StackExchange.Redis;
+using TEAMModelOS.SDK.Models.Cosmos.Common.Inner;
+using TEAMModelOS.Services.Common;
+using System.IO;
+using System.Dynamic;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Sas;
+using Lib.AspNetCore.ServerSentEvents;
+
+namespace TEAMModelOS.Controllers.Core
+{
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    //[Authorize(Roles = "HiTool")]
+    [Route("hiscan")]
+    [ApiController]
+    public class HiScanController : ControllerBase
+    {
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly AzureCosmosFactory _azureCosmos;
+        private readonly SnowflakeId _snowflakeId;
+        private readonly AzureServiceBusFactory _serviceBus;
+        private readonly DingDing _dingDing;
+        private readonly Option _option;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly ServerSentEventsService _sse;
+        public HiScanController(AzureCosmosFactory azureCosmos, AzureServiceBusFactory serviceBus, SnowflakeId snowflakeId, DingDing dingDing, IOptionsSnapshot<Option> option,
+           AzureRedisFactory azureRedis, AzureStorageFactory azureStorage, ServerSentEventsService sse)
+        {
+            _azureCosmos = azureCosmos;
+            _serviceBus = serviceBus;
+            _snowflakeId = snowflakeId;
+            _dingDing = dingDing;
+            _option = option?.Value;
+            _azureRedis = azureRedis;
+            _azureStorage = azureStorage;
+            _sse = sse;
+        }
+
+        ///<summary>
+        ///查询教师的阅卷任务列表
+        /// </summary>
+        /// <data>
+        ///    ! "code":"tmdid"
+        /// </data>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [ProducesDefaultResponseType]
+        [HttpPost("verify-qrcode")]
+       // [AuthToken(Roles = "teacher,admin")]
+        public async Task<IActionResult> VerifyQrcode(JsonElement request)
+        {
+            try {
+                if (!request.TryGetProperty("sid", out JsonElement sid)) return BadRequest();
+                if (!request.TryGetProperty("id_token", out JsonElement id_token)) return BadRequest();
+                if (!request.TryGetProperty("dev", out JsonElement dev)) return BadRequest();
+                IServerSentEventsClient sseClient;
+                if (Guid.TryParse($"{sid}", out Guid guid) && (sseClient = _sse.GetClient(guid)) != null) {
+                    //var clientName = sseClient.GetProperty<string>("NAME");
+                    //var clientDID= sseClient.GetProperty<string>("DID");
+                   // var isHiTeach = clientName.Contains("HiScan", StringComparison.OrdinalIgnoreCase);
+                    var jwt = new JwtSecurityToken(id_token.GetString());
+                    //TODO 此驗證IdToken先簡單檢查,後面需向Core ID新API,驗證Token
+                    if (!jwt.Payload.Iss.Equals("account.teammodel", StringComparison.OrdinalIgnoreCase)) return BadRequest();
+                    var id = jwt.Payload.Sub;
+                    jwt.Payload.TryGetValue("name", out object name);
+                    jwt.Payload.TryGetValue("picture", out object picture);
+                    List<object> schools = new List<object>();
+                    // object schools = null;
+                    string defaultschool = null;
+                    //TODO 取得Teacher 個人相關數據(課程清單、虛擬教室清單、歷史紀錄清單等),學校數據另外API處理,多校切換時不同
+                    var client = _azureCosmos.GetCosmosClient();
+                    var response = await client.GetContainer("TEAMModelOS", "Teacher").ReadItemStreamAsync(id, new PartitionKey("Base"));
+                    int size = 0;
+                    //老師個人資料(含初始化)
+                    if (response.Status == 200)
+                    {
+                        var json = await JsonDocument.ParseAsync(response.ContentStream);
+                        if (json.RootElement.TryGetProperty("schools", out JsonElement value))
+                        {
+                            if (json.RootElement.TryGetProperty("size", out JsonElement _size) && _size.ValueKind.Equals(JsonValueKind.Number))
+                            {
+                                size = _size.GetInt32();
+                            }
+                            foreach (var obj in value.EnumerateArray())
+                            {
+                                string statusNow = obj.GetProperty("status").ToString();
+                                //正式加入才会有
+                                if (statusNow == "join")
+                                {
+                                    dynamic schoolExtobj = new ExpandoObject();
+                                    var schoolJson = await client.GetContainer("TEAMModelOS", "School").ReadItemStreamAsync($"{obj.GetProperty("schoolId")}", new PartitionKey("Base"));
+                                    var school = await JsonDocument.ParseAsync(schoolJson.ContentStream);
+                                    schoolExtobj.schoolId = obj.GetProperty("schoolId");
+                                    schoolExtobj.name = obj.GetProperty("name");
+                                    schoolExtobj.status = obj.GetProperty("status");
+                                    if (obj.TryGetProperty("time", out JsonElement time))
+                                    {
+                                        schoolExtobj.time = obj.GetProperty("time");
+                                    }
+
+                                    schoolExtobj.picture = school.RootElement.GetProperty("picture");
+                                    schools.Add(schoolExtobj);
+                                    //如果有申请或者加入学校,但是未分配空间则都可以得到1G免费空间
+                                    if (size == 0)
+                                    {
+                                        size = 1;
+                                        Teacher tech = await client.GetContainer("TEAMModelOS", "Teacher").ReadItemAsync<Teacher>(id, new PartitionKey("Base"));
+                                        tech.size = size;
+                                        await client.GetContainer("TEAMModelOS", "Teacher").ReplaceItemAsync<Teacher>(tech, id, new PartitionKey("Base"));
+                                    }
+                                }
+                            }
+                        }
+                        //預設學校ID
+                        if (json.RootElement.TryGetProperty("defaultSchool", out JsonElement valueD) && !string.IsNullOrEmpty(valueD.ToString()))
+                        {
+                            defaultschool = valueD.ToString();
+                        }
+                        //換取AuthToken,提供給前端
+                        var auth_token = JwtAuthExtension.CreateAuthToken(_option.HostName, id, name?.ToString(), picture?.ToString(), _option.JwtSecretKey, roles: new[] { "teacher" });
+                        await sseClient.SendEventAsync(new { auth_token, schools }.ToJsonString());
+                        return Ok(new { auth_token, schools });
+                    }
+                    else
+                    {
+                        return Ok(new { status = 404 });
+                    }
+                }
+            } catch (Exception ex ) {
+                await _dingDing.SendBotMsg($"IES5,{_option.Location},hiscan/verify-qrcode()\n{ex.Message}{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
+                return BadRequest();
+            }
+           return Ok();
+        }
+    }
+}

+ 19 - 16
TEAMModelOS/Controllers/Core/BlobController.cs

@@ -532,17 +532,20 @@ namespace TEAMModelOS.Controllers.Core
                 request.TryGetProperty("scope", out JsonElement scope);
                 request.TryGetProperty("periodId", out JsonElement periodId);
                 var client = _azureCosmos.GetCosmosClient();
-                var queryslt = $"SELECT  value(c) FROM c join A1 in  c.periodId  WHERE c.type='{type}' and  A1 in ('{periodId}')";
+              
                 if (scope.GetString().Equals("school"))
                 {
-                    await foreach (var item in client.GetContainer("TEAMModelOS", "School").GetItemQueryIterator<Bloblog>(queryText: queryslt, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Bloblog-{name}") }))
+                    var queryslt = new StringBuilder($"SELECT  value(c) FROM c join A1 in  c.periodId  WHERE c.type='{type}' and  A1 in ('{periodId}') ");
+                     
+                    await foreach (var item in client.GetContainer("TEAMModelOS", "School").GetItemQueryIterator<Bloblog>(queryText: queryslt.ToString(), requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Bloblog-{name}") }))
                     {
                         bloblogs.Add(item);
                     }
                 }
-                else if (scope.GetString().Equals("teacher"))
+                else if (scope.GetString().Equals("private"))
                 {
-                    await foreach (var item in client.GetContainer("TEAMModelOS", "Teacher").GetItemQueryIterator<Bloblog>(queryText: queryslt, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Bloblog-{name}") }))
+                    var queryslt = new StringBuilder($"SELECT  value(c) FROM c   WHERE c.type='{type}'  ");
+                    await foreach (var item in client.GetContainer("TEAMModelOS", "Teacher").GetItemQueryIterator<Bloblog>(queryText: queryslt.ToString(), requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Bloblog-{name}") }))
                     {
                         bloblogs.Add(item);
                     }
@@ -614,21 +617,21 @@ namespace TEAMModelOS.Controllers.Core
                             {
                                 item.time = now;
                                 item.size = size != null && size.HasValue ? size.Value : 0;
-                                item.periodId = periodId.ValueKind.Equals(JsonValueKind.Array) ? periodId.ToObject<List<string>>() : new List<string> { "" };
-                                item.subjectId = subjectId.ValueKind.Equals(JsonValueKind.Array) ? subjectId.ToObject<List<string>>() : new List<string> { "" };
-                                item.gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) ? gradeId.ToObject<List<string>>() : new List<string> { "" };
+                                item.periodId = periodId.ValueKind.Equals(JsonValueKind.Array) && periodId.ToObject<List<string>>().IsNotEmpty() ? periodId.ToObject<List<string>>() : new List<string> { "" };
+                                item.subjectId = subjectId.ValueKind.Equals(JsonValueKind.Array) && subjectId.ToObject<List<string>>().IsNotEmpty() ? subjectId.ToObject<List<string>>() : new List<string> { "" };
+                                item.gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) && gradeId.ToObject<List<string>>().IsNotEmpty() ? gradeId.ToObject<List<string>>() : new List<string> { "" };
                                 await client.GetContainer("TEAMModelOS", "School").ReplaceItemAsync<Bloblog>(item, item.id, new Azure.Cosmos.PartitionKey(item.code));
                                 exsit = true;
                             }
                         }
-                        else if (scope.GetString().Equals("teacher")) {
+                        else if (scope.GetString().Equals("private")) {
                             await foreach (var item in client.GetContainer("TEAMModelOS", "Teacher").GetItemQueryIterator<Bloblog>(queryText: queryslt, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Bloblog-{name}") }))
                             {
                                 item.time = now;
                                 item.size = size != null && size.HasValue ? size.Value : 0;
-                                item.periodId = periodId.ValueKind.Equals(JsonValueKind.Array) ? periodId.ToObject<List<string>>() : new List<string> { "" };
-                                item.subjectId = subjectId.ValueKind.Equals(JsonValueKind.Array) ? subjectId.ToObject<List<string>>() : new List<string> { "" };
-                                item.gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) ? gradeId.ToObject<List<string>>() : new List<string> { "" };
+                                item.periodId = periodId.ValueKind.Equals(JsonValueKind.Array) && periodId.ToObject<List<string>>().IsNotEmpty() ? periodId.ToObject<List<string>>() : new List<string> { "" };
+                                item.subjectId = subjectId.ValueKind.Equals(JsonValueKind.Array) && subjectId.ToObject<List<string>>().IsNotEmpty() ? subjectId.ToObject<List<string>>() : new List<string> { "" };
+                                item.gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) && gradeId.ToObject<List<string>>().IsNotEmpty() ? gradeId.ToObject<List<string>>() : new List<string> { "" };
                                 await client.GetContainer("TEAMModelOS", "Teacher").ReplaceItemAsync<Bloblog>(item, item.id, new Azure.Cosmos.PartitionKey(item.code));
                                 exsit = true;
                             }
@@ -645,16 +648,16 @@ namespace TEAMModelOS.Controllers.Core
                             url = url,
                             time = now,
                             size = size != null && size.HasValue ? size.Value : 0,
-                            periodId = periodId.ValueKind.Equals(JsonValueKind.Array) ? periodId.ToObject<List<string>>() : new List<string> { "" } ,
-                            subjectId= subjectId.ValueKind.Equals(JsonValueKind.Array) ? subjectId.ToObject<List<string>>() : new List<string> { ""},
-                            gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) ? gradeId.ToObject<List<string>>() : new List<string> { "" },
+                            periodId = periodId.ValueKind.Equals(JsonValueKind.Array) && periodId.ToObject<List<string>>().IsNotEmpty() ? periodId.ToObject<List<string>>() : new List<string> { "" } ,
+                            subjectId= subjectId.ValueKind.Equals(JsonValueKind.Array)&& subjectId.ToObject<List<string>>() .IsNotEmpty()? subjectId.ToObject<List<string>>() : new List<string> { ""},
+                            gradeId = gradeId.ValueKind.Equals(JsonValueKind.Array) && gradeId.ToObject<List<string>>().IsNotEmpty() ? gradeId.ToObject<List<string>>() : new List<string> { "" },
                             type = u
                         };
                         if (scope.GetString().Equals("school"))
                         {
                             await client.GetContainer("TEAMModelOS", "School").CreateItemAsync(blob, new Azure.Cosmos.PartitionKey(blob.code));
                         }
-                        else if (scope.GetString().Equals("teacher"))
+                        else if (scope.GetString().Equals("private"))
                         {
                             await client.GetContainer("TEAMModelOS", "Teacher").CreateItemAsync(blob, new Azure.Cosmos.PartitionKey(blob.code));
                         }
@@ -666,7 +669,7 @@ namespace TEAMModelOS.Controllers.Core
                     {
                         await client.GetContainer("TEAMModelOS", "School").DeleteItemStreamAsync($"{id}", new Azure.Cosmos.PartitionKey($"Bloblog-{name}"));
                     }
-                    else if (scope.GetString().Equals("teacher"))
+                    else if (scope.GetString().Equals("private"))
                     {
                         await client.GetContainer("TEAMModelOS", "Teacher").DeleteItemStreamAsync($"{id}", new Azure.Cosmos.PartitionKey($"Bloblog-{name}"));
                     }

+ 27 - 28
TEAMModelOS/Controllers/School/StudentController.cs

@@ -129,7 +129,7 @@ namespace TEAMModelOS.Controllers
         /// <param name="students"></param>
         /// <returns></returns>
         private (Dictionary<string, (string name, string no, int year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)> studs,
-            Dictionary<string, (string className, string periodId, int gradeIndex, int year)> classInfo,
+            Dictionary<string, (string className, string periodId, int gradeIndex, int year, string no )> classInfo,
             Dictionary<string, List<(string id, string no)>> classStudNo,
             List<string> errorYear,
             List<string> duplId) doSortImpStuds(string schoolId, JsonElement.ArrayEnumerator students)
@@ -140,7 +140,7 @@ namespace TEAMModelOS.Controllers
             Dictionary<string, (string name, string no, int  year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)> dicStuds = new Dictionary<string, (string name, string no, int year, string salt, string pw, string classNo, string className, string periodId, int gradeIndex)>();
 
             //存放教室資訊用 key:classNo value:className
-            Dictionary<string, (string className, string periodId, int gradeIndex,int year)> dicClassInfo = new Dictionary<string, (string className, string periodId, int gradeIndex, int year)>();
+            Dictionary<string, (string className, string periodId, int gradeIndex,int year,string no)> dicClassInfo = new Dictionary<string, (string className, string periodId, int gradeIndex, int year,string no)>();
             //存放欲加入該間教室的學生座號清單 key:classNo value:no list
             Dictionary<string, List<(string id, string no)>> dicClassStudNo = new Dictionary<string, List<(string id, string no)>>();
             //存放輸入id重複
@@ -220,8 +220,8 @@ namespace TEAMModelOS.Controllers
                         {
                            
                             studentInfo.className = tmpClassName.GetString();
-                            if (!dicClassInfo.ContainsKey(tmpClassNo.GetString()))
-                            { dicClassInfo.Add(tmpClassNo.GetString(), (tmpClassName.GetString(), studentInfo.periodId, studentInfo.gradeIndex, year)); }
+                            if (!dicClassInfo.ContainsKey($"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"))
+                            { dicClassInfo.Add($"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}", (tmpClassName.GetString(), studentInfo.periodId, studentInfo.gradeIndex, year, tmpClassNo.GetString())); }
                         }
                     }
                     
@@ -246,9 +246,9 @@ namespace TEAMModelOS.Controllers
             {
                 var sortedImpData = doSortImpStuds(schoolId, students);
 
-                List<string> classNos = sortedImpData.classInfo.Select(o => o.Key).ToList();
+                //var   classNos = sortedImpData.classInfo.Select(o => new {key= o.Key, periodId=o.Value.periodId,index= o.Value.gradeIndex,year = o.Value.year }).ToList();
                 //抓到教室資訊
-                var classInfos = await getClassInfoUseNo(schoolId, classNos);
+                var classInfos = await getClassInfoUseNo(schoolId, sortedImpData.classInfo);
                 //取出已存在教室的classId,後面查座號要用。
 
                 List<Task> tasks = new List<Task>();
@@ -259,22 +259,30 @@ namespace TEAMModelOS.Controllers
                 Dictionary<string, (string classId, string className, string periodId, string gradeId, int year)> classNoId = new Dictionary<string, (string classId, string className, string periodId, string gradeId, int year)>();
                 foreach (var classInfo in classInfos)
                 {
-                    string classGradeId = (classInfo.Value.TryGetProperty("gradeId", out JsonElement classGradeIdJson)) ? classGradeIdJson.GetString() : null;
-                    int classYear = (classInfo.Value.TryGetProperty("year", out JsonElement classYearJson)) ? classYearJson.GetInt32() : 0;
+                    string classGradeId = classInfo.Value.gradeId;
+                    int classYear = classInfo.Value.year;
                     classNoId.Add(classInfo.Key,
-                        (classInfo.Value.GetProperty("id").GetString(), classInfo.Value.GetProperty("name").GetString(), classInfo.Value.GetProperty("periodId").GetString(), classGradeId, classYear));
+                        (classInfo.Value.id, classInfo.Value.name, classInfo.Value.periodId, classGradeId, classYear));
                     tasks.Add(
                         Task.Run(
                             async () =>
                             {
                                 //(id,no)
-                                var studNo = await checkStudNo(schoolId, classInfo.Value.GetProperty("id").GetString());
+                                var studNo = await checkStudNo(schoolId, classInfo.Value.id);
                                 classStudNos.Add(classInfo.Key, studNo);
                             }));
                 }
 
                 //這邊整理出不存在的教室,之後創建新教室用(比對classNo)。
-                var nonexistentClassNo = classNos.Except(classInfos.Select(o => o.Key).ToList());
+                //var nonexistentClassNo = classNos.Except(classInfos.Select(o => o.Key).ToList());
+                List<string> exsitkey =  new List<string>();
+                foreach (var classInfo in classInfos)
+                {
+                    //$"{studentInfo.periodId}_{year}_{tmpClassNo.GetString()}"
+                    var key = $"{classInfo.Value.periodId}_{classInfo.Value.year}_{classInfo.Value.no}";
+                    exsitkey.Add(key);
+                }
+                var nonexistentClassNo = exsitkey.Except(sortedImpData.classInfo.Select(o => o.Key).ToList());
                 if (nonexistentClassNo.Count() != 0)
                 {
                     var gradesInfo = await getGrades(schoolId);
@@ -1186,29 +1194,20 @@ namespace TEAMModelOS.Controllers
         /// 取得教室資訊,使用classNo進行查詢。
         /// </summary>
         /// <returns></returns>
-        private async Task<Dictionary<string, JsonElement>> getClassInfoUseNo(string schoolId, List<string> classNos)
+        private async Task<Dictionary<string, Class>> getClassInfoUseNo(string schoolId, Dictionary<string, (string className, string periodId, int gradeIndex, int year,string no)> classNos)
         {
             try
             {
+                Dictionary<string, Class> dicClassInfo = new Dictionary<string, Class>();
                 if (!(classNos == null || classNos.Count == 0))
                 {
-                    string queryText = $"SELECT * FROM c WHERE c.code = 'Class-{schoolId}' AND c.no IN ({string.Join(",", classNos.Select(o => $"'{o}'"))})";
-
-                    Dictionary<string, JsonElement> dicClassInfo = new Dictionary<string, JsonElement>();
-
-                    await foreach (Response item in _azureCosmos.GetCosmosClient().GetContainer("TEAMModelOS", "School")
-                        .GetItemQueryStreamIterator(queryText: queryText, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schoolId}") }))
-                    {
-                        using var json = await JsonDocument.ParseAsync(item.ContentStream);
-                        if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                   
+                    foreach (var key in classNos.Keys) {
+                        string queryText = $"SELECT * FROM c WHERE c.code = 'Class-{schoolId}' AND c.no='{classNos[key].no}' and c.year={classNos[key].year} and c.periodId='{classNos[key].periodId}' ";
+                        await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer("TEAMModelOS", "School")
+                       .GetItemQueryIterator<Class>(queryText: queryText, requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey($"Class-{schoolId}") }))
                         {
-                            var classInfos = json.RootElement.GetProperty("Documents").EnumerateArray();
-                            while (classInfos.MoveNext())
-                            {
-                                JsonElement account = classInfos.Current;
-                                string no = account.GetProperty("no").GetString();
-                                dicClassInfo.Add(no, account.Clone());
-                            }
+                            dicClassInfo[item.id] = item;
                         }
                     }
                     return dicClassInfo;

+ 41 - 0
TEAMModelOS/Startup.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using HTEXLib.Builders;
 using HTEXLib.Translator;
+using Lib.AspNetCore.ServerSentEvents;
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
@@ -19,12 +20,14 @@ using Microsoft.AspNetCore.SpaServices;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Primitives;
 using Microsoft.IdentityModel.Tokens;
 using TEAMModelOS.Models;
 using TEAMModelOS.SDK;
 using TEAMModelOS.SDK.Context.Attributes.Azure;
 using TEAMModelOS.SDK.Context.Configuration;
 using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.Extension;
 using TEAMModelOS.SDK.Models.Service;
 using VueCliMiddleware;
 
@@ -110,6 +113,35 @@ namespace TEAMModelOS
             //注入word 標籤解析
             string path = $"{ environment.ContentRootPath}/JsonFile/Core";
             services.AddHtexTranslator(path);
+            services.AddServerSentEvents(o =>
+            {
+                o.KeepaliveMode = ServerSentEventsKeepaliveMode.Always;
+                o.OnClientConnected = async (service, client) =>
+                {
+                    //if (client.Request.Headers.TryGetValue("X-Auth-Name", out StringValues name))
+                    //{
+                    //    client.Client.SetProperty("NAME", name.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-DID", out StringValues did))
+                    //{
+                    //    client.Client.SetProperty("DID", did.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-CID", out StringValues cid))
+                    //{
+                    //    client.Client.SetProperty("CID", cid.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-PIN", out StringValues pin))
+                    //{
+                    //    client.Client.SetProperty("PIN", pin.ToString());
+                    //}
+                    //if (client.Request.Headers.TryGetValue("X-Auth-APP", out StringValues app))
+                    //{
+                    //    client.Client.SetProperty("APP", app.ToString());
+                    //}
+
+                    await client.Client.SendEventAsync(new { sid = client.Client.Id.ToString() }.ToJsonString());
+                };
+            });
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -139,6 +171,15 @@ namespace TEAMModelOS
             app.UseEndpoints(endpoints =>
             {
                 endpoints.MapControllers();
+                endpoints.MapServerSentEvents("/service/sse", new ServerSentEventsOptions
+                {
+                    //Authorization = ServerSentEventsAuthorization.Default,
+                    OnPrepareAccept = response =>
+                    {
+                        response.Headers.Append("Cache-Control", "no-cache");
+                        response.Headers.Append("X-Accel-Buffering", "no");
+                    }
+                });
 #if DEBUG
                 endpoints.MapToVueCliProxy(
                     "{*path}",