This commit is contained in:
2026-06-30 13:08:18 +07:00
parent 795726aea5
commit 1c2bfde005
18 changed files with 1504 additions and 90 deletions

View File

@@ -8,6 +8,7 @@ import (
"crypto/cipher"
"crypto/rand"
"encoding/json"
"encoding/base64"
"errors"
"fmt"
"html"
@@ -17,6 +18,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
@@ -85,9 +87,10 @@ type App struct {
ctx context.Context
student *StudentData
mu sync.Mutex
sessionPath string
statsPath string
wsConn *websocket.Conn
sessionPath string
statsPath string
webviewDataPath string
wsConn *websocket.Conn
wsConnected bool
wifiSSID string
wifiBSSID string
@@ -165,11 +168,14 @@ func NewApp() *App {
configDir = filepath.Dir(exePath)
}
appDir := filepath.Join(configDir, "SimpleCare")
webviewDir := filepath.Join(appDir, "WebView2")
_ = os.MkdirAll(appDir, 0755)
_ = os.MkdirAll(webviewDir, 0755)
return &App{
sessionPath: filepath.Join(appDir, "student_session.json"),
statsPath: filepath.Join(appDir, "student_stats.json"),
sessionPath: filepath.Join(appDir, "student_session.json"),
statsPath: filepath.Join(appDir, "student_stats.json"),
webviewDataPath: webviewDir,
lastSyncTime: time.Now(),
expectingLogin: false,
}
@@ -1284,17 +1290,36 @@ func (a *App) examStudentContext() (int64, *StudentExamSnapshot, error) {
return a.student.StudentID, a.dashboard.Exam, nil
}
// ReturnToDashboard quay lại giao diện chính của app (giữ cookie WebView2).
func (a *App) ReturnToDashboard() {
runtime.WindowReloadApp(a.ctx)
}
func isLocalExamURL(raw string) bool {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil || u.Host == "" {
return false
}
host := strings.ToLower(u.Hostname())
return host == "127.0.0.1" || host == "localhost"
}
func (a *App) openExamWebView(targetURL, title string) error {
targetURL = strings.TrimSpace(targetURL)
if targetURL == "" {
return errors.New("không có nội dung để mở")
}
wrapper := fmt.Sprintf(
"http://127.0.0.1:34115/exam-view?url=%s&title=%s",
url.QueryEscape(targetURL),
url.QueryEscape(title),
)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper))
if isLocalExamURL(targetURL) {
wrapper := fmt.Sprintf(
"http://127.0.0.1:34115/exam-view?url=%s&title=%s",
url.QueryEscape(targetURL),
url.QueryEscape(title),
)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", wrapper))
return nil
}
// Trang thi bên ngoài: mở trực tiếp trong WebView (first-party cookies → giữ phiên đăng nhập).
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", targetURL))
return nil
}
@@ -1306,7 +1331,7 @@ func (a *App) GetExamPaperViewURL() (string, error) {
if !exam.PaperSent {
return "", errors.New("Giảng viên chưa gửi đề")
}
return fmt.Sprintf("http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=pdf", studentID), nil
return fmt.Sprintf("http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=pdf&view=1", studentID), nil
}
func (a *App) GetExamQuizViewURL() (string, error) {
@@ -1355,6 +1380,9 @@ func (a *App) GetExamPaperFiles() (map[string]any, error) {
if u, ok := m["url"].(string); ok && u != "" && !strings.HasPrefix(u, "http") {
m["url"] = base + u
}
if u, ok := m["downloadUrl"].(string); ok && u != "" && !strings.HasPrefix(u, "http") {
m["downloadUrl"] = base + u
}
raw[i] = m
}
payload["resources"] = raw
@@ -1393,7 +1421,7 @@ func (a *App) OpenExamResource(fileID uint) error {
return errors.New("tài nguyên không hợp lệ")
}
viewURL := fmt.Sprintf(
"http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d",
"http://127.0.0.1:8080/api/student/exam/download?studentRkId=%d&kind=resource&fileId=%d&view=1",
studentID, fileID,
)
title := "Tài nguyên đề thi"
@@ -1418,8 +1446,47 @@ func (a *App) OpenExamResource(fileID uint) error {
return a.openExamWebView(viewURL, title)
}
func examResourceDownloadDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
candidates := []string{
filepath.Join(home, "Downloads"),
filepath.Join(home, "Desktop"),
home,
}
for _, dir := range candidates {
if st, err := os.Stat(dir); err == nil && st.IsDir() {
return dir, nil
}
}
return "", errors.New("không tìm thấy thư mục Downloads hoặc Desktop")
}
func uniqueFilePath(path string) string {
if _, err := os.Stat(path); os.IsNotExist(err) {
return path
}
ext := filepath.Ext(path)
base := strings.TrimSuffix(filepath.Base(path), ext)
dir := filepath.Dir(path)
for i := 1; i < 100; i++ {
candidate := filepath.Join(dir, fmt.Sprintf("%s (%d)%s", base, i, ext))
if _, err := os.Stat(candidate); os.IsNotExist(err) {
return candidate
}
}
return filepath.Join(dir, fmt.Sprintf("%s_%d%s", base, time.Now().Unix(), ext))
}
func openFolderInExplorer(filePath string) {
dir := filepath.Dir(filePath)
_ = exec.Command("explorer", dir).Start()
}
func (a *App) DownloadExamResource(fileID uint) (string, error) {
studentID, exam, err := a.examStudentContext()
studentID, _, err := a.examStudentContext()
if err != nil {
return "", err
}
@@ -1457,15 +1524,11 @@ func (a *App) DownloadExamResource(fileID uint) (string, error) {
if resp.StatusCode != http.StatusOK {
return "", errors.New("không tải được tài nguyên")
}
configDir, err := os.UserConfigDir()
dir, err := examResourceDownloadDir()
if err != nil {
return "", err
}
dir := filepath.Join(configDir, "SimpleCare", "ExamResources", fmt.Sprintf("room_%d", exam.ExamRoomID))
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
dest := filepath.Join(dir, fileName)
dest := uniqueFilePath(filepath.Join(dir, fileName))
f, err := os.Create(dest)
if err != nil {
return "", err
@@ -1475,9 +1538,45 @@ func (a *App) DownloadExamResource(fileID uint) (string, error) {
return "", err
}
f.Close()
openFolderInExplorer(dest)
return dest, nil
}
// LoadExamViewFile tải file đề/tài nguyên qua Go (không mở URL trực tiếp trong WebView).
func (a *App) LoadExamViewFile(fileURL string) (map[string]any, error) {
fileURL = strings.TrimSpace(fileURL)
if !strings.HasPrefix(fileURL, "http://127.0.0.1:8080/api/student/exam/download") {
return nil, errors.New("URL không hợp lệ")
}
if _, _, err := a.examStudentContext(); err != nil {
return nil, err
}
resp, err := http.Get(fileURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("không tải được file")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
mime := strings.TrimSpace(resp.Header.Get("Content-Type"))
if mime == "" {
mime = "application/octet-stream"
}
// Bỏ charset nếu có
if i := strings.Index(mime, ";"); i >= 0 {
mime = strings.TrimSpace(mime[:i])
}
return map[string]any{
"data": base64.StdEncoding.EncodeToString(body),
"mime": mime,
}, nil
}
func (a *App) SubmitExamWork() (string, error) {
a.mu.Lock()
student := a.student