add
This commit is contained in:
205
client/app.go
205
client/app.go
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user