This commit is contained in:
2026-06-30 11:51:34 +07:00
parent fd08bee7ab
commit 0abf48ea2d
28 changed files with 3514 additions and 37 deletions

View File

@@ -1,6 +1,7 @@
package main
import (
"archive/zip"
"bytes"
"context"
"crypto/aes"
@@ -11,8 +12,10 @@ import (
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
@@ -127,6 +130,15 @@ type StudentShiftSnapshot struct {
AttendanceLabel string `json:"attendanceLabel"`
}
type StudentExamSnapshot struct {
ExamRoomID uint `json:"examRoomId"`
ExamName string `json:"examName"`
QuizURL string `json:"quizUrl"`
PaperSent bool `json:"paperSent"`
PaperTitle string `json:"paperTitle"`
Submitted bool `json:"submitted"`
}
type StudentDashboardSnapshot struct {
SessionDate string `json:"sessionDate"`
MonitorMode string `json:"monitorMode"`
@@ -141,6 +153,7 @@ type StudentDashboardSnapshot struct {
InScheduleNow bool `json:"inScheduleNow"`
BlockerActive bool `json:"blockerActive"`
Shifts []StudentShiftSnapshot `json:"shifts"`
Exam *StudentExamSnapshot `json:"exam,omitempty"`
}
func NewApp() *App {
@@ -480,6 +493,7 @@ func (a *App) GetStats() map[string]any {
"inScheduleNow": a.dashboard.InScheduleNow,
"blockerActive": a.dashboard.BlockerActive,
"shifts": shifts,
"exam": a.dashboard.Exam,
}
}
@@ -917,6 +931,8 @@ func (a *App) fetchStudentStatus(studentID int64) {
a.statusMsg = fmt.Sprintf("%s · Ngoài giờ học", res.ClassName)
} else if res.ClassName != "" && res.MonitorMode == "not_configured" {
a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
} else if res.MonitorMode == "exam" {
a.statusMsg = res.MonitorLabel
}
a.mu.Unlock()
}
@@ -992,6 +1008,16 @@ func (a *App) connectWS() {
a.alertChatIncoming(from, preview)
}
runtime.EventsEmit(a.ctx, "chat:message", msg.Data)
case "exam:paper-sent":
runtime.EventsEmit(a.ctx, "exam:paper-sent", msg.Data)
a.mu.Lock()
if a.dashboard.Exam != nil {
a.dashboard.Exam.PaperSent = true
if t, ok := msg.Data["paperTitle"].(string); ok {
a.dashboard.Exam.PaperTitle = t
}
}
a.mu.Unlock()
}
}
}()
@@ -1188,3 +1214,182 @@ func (a *App) SendChatMessage(body string) error {
}
return nil
}
func (a *App) OpenExamQuiz() error {
a.mu.Lock()
var url string
if a.dashboard.Exam != nil {
url = strings.TrimSpace(a.dashboard.Exam.QuizURL)
}
a.mu.Unlock()
if url == "" {
return errors.New("Phòng thi chưa cấu hình link trắc nghiệm")
}
return openPrivateBrowser(url)
}
func openPrivateBrowser(url string) error {
tries := [][]string{
{"cmd", "/c", "start", "", "chrome", "--incognito", "--new-window", "--disk-cache-size=1", url},
{"cmd", "/c", "start", "", "msedge", "--inprivate", "--new-window", url},
{"cmd", "/c", "start", "", url},
}
var lastErr error
for _, args := range tries {
if err := exec.Command(args[0], args[1:]...).Start(); err != nil {
lastErr = err
continue
}
return nil
}
if lastErr != nil {
return lastErr
}
return errors.New("không mở được trình duyệt")
}
func (a *App) OpenExamPaper() error {
a.mu.Lock()
student := a.student
exam := a.dashboard.Exam
a.mu.Unlock()
if student == nil {
return errors.New("chưa đăng nhập")
}
if exam == nil || !exam.PaperSent {
return errors.New("Giảng viên chưa gửi đề")
}
dlURL := fmt.Sprintf("http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=pdf", student.StudentID)
resp, err := http.Get(dlURL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("không tải được đề")
}
dir := filepath.Join(os.TempDir(), "SimpleCareExam")
_ = os.MkdirAll(dir, 0755)
dest := filepath.Join(dir, fmt.Sprintf("de_thi_%d.pdf", exam.ExamRoomID))
f, err := os.Create(dest)
if err != nil {
return err
}
if _, err := io.Copy(f, resp.Body); err != nil {
f.Close()
return err
}
f.Close()
return exec.Command("cmd", "/c", "start", "", dest).Start()
}
func (a *App) SubmitExamWork() (string, error) {
a.mu.Lock()
student := a.student
exam := a.dashboard.Exam
a.mu.Unlock()
if student == nil {
return "", errors.New("chưa đăng nhập")
}
if exam == nil {
return "", errors.New("không trong giờ thi")
}
dir, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Chọn folder bài làm để nộp",
})
if err != nil {
return "", err
}
if dir == "" {
return "", errors.New("đã hủy")
}
zipPath := filepath.Join(os.TempDir(), fmt.Sprintf("exam_submit_%d_%d.zip", exam.ExamRoomID, time.Now().Unix()))
if err := zipFolder(dir, zipPath); err != nil {
return "", err
}
defer os.Remove(zipPath)
f, err := os.Open(zipPath)
if err != nil {
return "", err
}
defer f.Close()
var body bytes.Buffer
w := multipart.NewWriter(&body)
_ = w.WriteField("studentRkId", fmt.Sprintf("%d", student.StudentID))
part, err := w.CreateFormFile("submission", filepath.Base(zipPath))
if err != nil {
return "", err
}
if _, err := io.Copy(part, f); err != nil {
return "", err
}
_ = w.Close()
req, err := http.NewRequest("POST", "http://127.0.0.1:8080/api/student/exam/submit", &body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var res struct {
OK bool `json:"ok"`
FileName string `json:"fileName"`
Error string `json:"error"`
}
_ = json.NewDecoder(resp.Body).Decode(&res)
if resp.StatusCode >= 300 || !res.OK {
if res.Error != "" {
return "", errors.New(res.Error)
}
return "", errors.New("nộp bài thất bại")
}
a.mu.Lock()
if a.dashboard.Exam != nil {
a.dashboard.Exam.Submitted = true
}
a.mu.Unlock()
return res.FileName, nil
}
func zipFolder(srcDir, destZip string) error {
out, err := os.Create(destZip)
if err != nil {
return err
}
defer out.Close()
zw := zip.NewWriter(out)
defer zw.Close()
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rel, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
hdr, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
hdr.Name = rel
hdr.Method = zip.Deflate
w, err := zw.CreateHeader(hdr)
if err != nil {
return err
}
rf, err := os.Open(path)
if err != nil {
return err
}
defer rf.Close()
_, err = io.Copy(w, rf)
return err
})
}