package main import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" stdruntime "runtime" "time" "github.com/wailsapp/wails/v2/pkg/runtime" "kenaito-notebook/models" ) // 配置后端地址 const defaultServerURL = "http://localhost:8080" type App struct { ctx context.Context serverURL string http *http.Client token string } func NewApp() *App { return &App{ serverURL: defaultServerURL, http: &http.Client{Timeout: 30 * time.Second}, } } func (a *App) startup(ctx context.Context) { a.ctx = ctx log.Println("[App] Starting up, server:", a.serverURL) } func (a *App) shutdown(ctx context.Context) { log.Println("[App] Shutting down") } // doRequest 发起 HTTP 请求的通用方法 func (a *App) doRequest(method, path string, body interface{}) ([]byte, error) { var bodyReader io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, a.serverURL+path, bodyReader) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } if a.token != "" { req.Header.Set("Authorization", "Bearer "+a.token) } resp, err := a.http.Do(req) if err != nil { return nil, fmt.Errorf("server connection failed: %w", err) } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode >= 400 { var errResp struct { Error string `json:"error"` } if json.Unmarshal(data, &errResp) == nil && errResp.Error != "" { return nil, fmt.Errorf(errResp.Error) } return nil, fmt.Errorf("server returned %d", resp.StatusCode) } return data, nil } // Register 用户注册 func (a *App) Register(email, password, code string) (string, error) { body := map[string]string{ "email": email, "password": password, "code": code, } data, err := a.doRequest("POST", "/api/auth/register", body) if err != nil { return "", err } var resp struct { Token string `json:"token"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } a.token = resp.Token return resp.Token, nil } // SendVerifyCode 发送验证码 func (a *App) SendVerifyCode(email, purpose string) error { body := map[string]string{ "email": email, "purpose": purpose, } _, err := a.doRequest("POST", "/api/auth/send-code", body) return err } // VerifyCode 验证验证码 func (a *App) VerifyCode(email, code, purpose string) error { body := map[string]string{ "email": email, "code": code, "purpose": purpose, } _, err := a.doRequest("POST", "/api/auth/verify-code", body) return err } // ResetPassword 重置密码 func (a *App) ResetPassword(email, code, newPassword string) error { body := map[string]string{ "email": email, "code": code, "newPassword": newPassword, } _, err := a.doRequest("POST", "/api/auth/reset-password", body) return err } // Login 用户登录 func (a *App) Login(email, password string) (string, error) { body := map[string]string{ "email": email, "password": password, } data, err := a.doRequest("POST", "/api/auth/login", body) if err != nil { return "", err } var resp struct { Token string `json:"token"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } a.token = resp.Token return resp.Token, nil } // Logout 退出登录 func (a *App) Logout() { a.token = "" } // GetUser 获取当前用户信息 func (a *App) GetUser() (map[string]interface{}, error) { data, err := a.doRequest("GET", "/api/auth/me", nil) if err != nil { return nil, err } var user map[string]interface{} if err := json.Unmarshal(data, &user); err != nil { return nil, err } return user, nil } // IsLoggedIn 是否已登录 func (a *App) IsLoggedIn() bool { return a.token != "" } // GetNoteTree 获取笔记树 func (a *App) GetNoteTree() ([]*models.NoteTree, error) { data, err := a.doRequest("GET", "/api/notes/tree", nil) if err != nil { return nil, err } var result []*models.NoteTree if err := json.Unmarshal(data, &result); err != nil { return nil, err } return result, nil } // GetNote 获取单个笔记 func (a *App) GetNote(id int64) (*models.Note, error) { data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/%d", id), nil) if err != nil { return nil, err } var note models.Note if err := json.Unmarshal(data, ¬e); err != nil { return nil, err } return ¬e, nil } // CreateNote 创建笔记或文件夹 func (a *App) CreateNote(parentID int64, title string, isFolder bool) (*models.Note, error) { body := map[string]interface{}{ "parentId": parentID, "title": title, "isFolder": isFolder, } data, err := a.doRequest("POST", "/api/notes", body) if err != nil { return nil, err } var note models.Note if err := json.Unmarshal(data, ¬e); err != nil { return nil, err } return ¬e, nil } // UpdateNote 更新笔记 func (a *App) UpdateNote(id int64, title, content, tag string) error { body := map[string]string{ "title": title, "content": content, "tag": tag, } _, err := a.doRequest("PUT", fmt.Sprintf("/api/notes/%d", id), body) return err } // DeleteNote 删除笔记 func (a *App) DeleteNote(id int64) error { _, err := a.doRequest("DELETE", fmt.Sprintf("/api/notes/%d", id), nil) return err } // SearchNotes 搜索笔记 func (a *App) SearchNotes(keyword string) ([]*models.Note, error) { data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/search?q=%s", keyword), nil) if err != nil { return nil, err } var notes []*models.Note if err := json.Unmarshal(data, ¬es); err != nil { return nil, err } return notes, nil } // GetTrashNotes 获取回收站中的笔记 func (a *App) GetTrashNotes() ([]*models.Note, error) { data, err := a.doRequest("GET", "/api/trash", nil) if err != nil { return nil, err } var notes []*models.Note if err := json.Unmarshal(data, ¬es); err != nil { return nil, err } return notes, nil } // RestoreNote 恢复笔记 func (a *App) RestoreNote(id int64) error { _, err := a.doRequest("POST", fmt.Sprintf("/api/trash/%d/restore", id), nil) return err } // PermanentDeleteNote 永久删除笔记 func (a *App) PermanentDeleteNote(id int64) error { _, err := a.doRequest("DELETE", fmt.Sprintf("/api/trash/%d", id), nil) return err } // EmptyTrash 清空回收站 func (a *App) EmptyTrash() error { _, err := a.doRequest("DELETE", "/api/trash", nil) return err } // SaveImage 保存图片 func (a *App) SaveImage(base64Data string) (string, error) { body := map[string]string{"data": base64Data} data, err := a.doRequest("POST", "/api/files/image", body) if err != nil { return "", err } var resp struct { URL string `json:"url"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } return resp.URL, nil } // SaveFile 保存任意文件 func (a *App) SaveFile(base64Data, filename string) (string, error) { body := map[string]string{ "data": base64Data, "filename": filename, } data, err := a.doRequest("POST", "/api/files/upload", body) if err != nil { return "", err } var resp struct { URL string `json:"url"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } return resp.URL, nil } // GetFileServerURL 获取文件服务器地址 func (a *App) GetFileServerURL() string { data, err := a.doRequest("GET", "/api/files/server-url", nil) if err != nil { return "" } var resp struct { URL string `json:"url"` } if err := json.Unmarshal(data, &resp); err != nil { return "" } return resp.URL } // GetAssetsPath 获取资源路径 func (a *App) GetAssetsPath() string { data, err := a.doRequest("GET", "/api/files/assets-path", nil) if err != nil { return "" } var resp struct { Path string `json:"path"` } if err := json.Unmarshal(data, &resp); err != nil { return "" } return resp.Path } // SelectDirectory 弹出目录选择对话框 func (a *App) SelectDirectory(title string) (string, error) { result, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ Title: title, }) if err != nil { return "", err } if result == "" { return "", fmt.Errorf("用户取消选择") } return result, nil } // SelectSaveFile 弹出文件保存对话框 func (a *App) SelectSaveFile(title, defaultFilename string) (string, error) { result, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ Title: title, DefaultFilename: defaultFilename, }) if err != nil { return "", err } if result == "" { return "", fmt.Errorf("用户取消保存") } return result, nil } // RevealInFinder 在 Finder 中显示文件 func (a *App) RevealInFinder(path string) { var cmd *exec.Cmd switch stdruntime.GOOS { case "darwin": cmd = exec.Command("open", "-R", path) case "windows": cmd = exec.Command("explorer", "/select,", path) default: cmd = exec.Command("xdg-open", filepath.Dir(path)) } cmd.Start() } // ExportHTML 导出 HTML func (a *App) ExportHTML(title, content string) (string, error) { body := map[string]string{"title": title, "content": content} data, err := a.doRequest("POST", "/api/export/html", body) if err != nil { return "", err } var resp struct { FilePath string `json:"filePath"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } defaultFilename := title + ".html" savePath, err := a.SelectSaveFile("导出 HTML 文件", defaultFilename) if err != nil { return "", err } if filepath.Ext(savePath) != ".html" { savePath += ".html" } if err := copyFile(resp.FilePath, savePath); err != nil { return "", err } return savePath, nil } // ExportMarkdown 导出 Markdown func (a *App) ExportMarkdown(title, content string) (string, error) { body := map[string]string{"title": title, "content": content} data, err := a.doRequest("POST", "/api/export/markdown", body) if err != nil { return "", err } var resp struct { FilePath string `json:"filePath"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } defaultFilename := title + ".md" savePath, err := a.SelectSaveFile("导出 Markdown 文件", defaultFilename) if err != nil { return "", err } if filepath.Ext(savePath) != ".md" { savePath += ".md" } if err := copyFile(resp.FilePath, savePath); err != nil { return "", err } return savePath, nil } // ExportFolderMarkdown 导出整个文件夹为 ZIP func (a *App) ExportFolderMarkdown(folderID int64) (string, error) { data, err := a.doRequest("POST", fmt.Sprintf("/api/export/folder-zip/%d", folderID), nil) if err != nil { return "", err } var resp struct { FilePath string `json:"filePath"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } savePath, err := a.SelectSaveFile("导出文件夹为 ZIP", "export.zip") if err != nil { return "", err } if filepath.Ext(savePath) != ".zip" { savePath += ".zip" } if err := copyFile(resp.FilePath, savePath); err != nil { return "", err } return savePath, nil } // ExportAllMarkdown 导出所有笔记为 ZIP func (a *App) ExportAllMarkdown() (string, error) { data, err := a.doRequest("POST", "/api/export/all-zip", nil) if err != nil { return "", err } var resp struct { FilePath string `json:"filePath"` } if err := json.Unmarshal(data, &resp); err != nil { return "", err } savePath, err := a.SelectSaveFile("导出所有笔记为 ZIP", "所有笔记.zip") if err != nil { return "", err } if filepath.Ext(savePath) != ".zip" { savePath += ".zip" } if err := copyFile(resp.FilePath, savePath); err != nil { return "", err } return savePath, nil } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err }