From 65532aec992306f69e578ed9b155aa27e0d6ac30 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Fri, 16 Jan 2026 13:52:32 +0800 Subject: [PATCH] :art: Support exporting .sy.zip after selecting multiple documents https://github.com/siyuan-note/siyuan/issues/14484 Signed-off-by: Daniel <845765@qq.com> --- app/src/menus/navigation.ts | 15 ++++++++++++++- kernel/api/export.go | 26 +++++++++++++++++++++++--- kernel/api/router.go | 1 + kernel/model/export.go | 29 +++++++++++++---------------- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/app/src/menus/navigation.ts b/app/src/menus/navigation.ts index 96d5e5414..fa4b7f26e 100644 --- a/app/src/menus/navigation.ts +++ b/app/src/menus/navigation.ts @@ -4,7 +4,7 @@ import {FileFilter, ipcRenderer} from "electron"; import * as path from "path"; /// #endif import {MenuItem} from "./Menu"; -import {getDisplayName, getNotebookName, getTopPaths, useShell, pathPosix} from "../util/pathName"; +import {getDisplayName, getNotebookName, getTopPaths, pathPosix, useShell} from "../util/pathName"; import {hideMessage, showMessage} from "../dialog/message"; import {fetchPost, fetchSyncPost} from "../util/fetch"; import {onGetnotebookconf} from "./onGetnotebookconf"; @@ -158,6 +158,19 @@ const initMultiMenu = (selectItemElements: NodeListOf, app: App) => { type: "submenu", icon: "iconUpload", submenu: [{ + id: "exportSiYuanZip", + label: "SiYuan .sy.zip", + icon: "iconSiYuan", + click: () => { + const msgId = showMessage(window.siyuan.languages.exporting, -1); + fetchPost("/api/export/exportSYs", { + ids: blockIDs, + }, response => { + hideMessage(msgId); + openByMobile(response.data.zip); + }); + } + }, { id: "exportMarkdown", label: "Markdown .zip", icon: "iconMarkdown", diff --git a/kernel/api/export.go b/kernel/api/export.go index f0c857060..c62cba92f 100644 --- a/kernel/api/export.go +++ b/kernel/api/export.go @@ -406,6 +406,27 @@ func exportNotebookSY(c *gin.Context) { } } +func exportSYs(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + arg, ok := util.JsonArg(c, ret) + if !ok { + return + } + + idsArg := arg["ids"].([]interface{}) + var ids []string + for _, id := range idsArg { + ids = append(ids, id.(string)) + } + + zipPath := model.ExportSYs(ids) + ret.Data = map[string]interface{}{ + "zip": zipPath, + } +} + func exportSY(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) @@ -416,10 +437,9 @@ func exportSY(c *gin.Context) { } id := arg["id"].(string) - name, zipPath := model.ExportSY(id) + zipPath := model.ExportSYs([]string{id}) ret.Data = map[string]interface{}{ - "name": name, - "zip": zipPath, + "zip": zipPath, } } diff --git a/kernel/api/router.go b/kernel/api/router.go index b85699b88..61b53393b 100644 --- a/kernel/api/router.go +++ b/kernel/api/router.go @@ -315,6 +315,7 @@ func ServeAPI(ginServer *gin.Engine) { ginServer.Handle("POST", "/api/export/exportNotebookMd", model.CheckAuth, model.CheckAdminRole, exportNotebookMd) ginServer.Handle("POST", "/api/export/exportMds", model.CheckAuth, model.CheckAdminRole, exportMds) ginServer.Handle("POST", "/api/export/exportMd", model.CheckAuth, model.CheckAdminRole, exportMd) + ginServer.Handle("POST", "/api/export/exportSYs", model.CheckAuth, model.CheckAdminRole, exportSYs) ginServer.Handle("POST", "/api/export/exportSY", model.CheckAuth, model.CheckAdminRole, exportSY) ginServer.Handle("POST", "/api/export/exportNotebookSY", model.CheckAuth, model.CheckAdminRole, exportNotebookSY) ginServer.Handle("POST", "/api/export/exportMdContent", model.CheckAuth, model.CheckAdminRole, exportMdContent) diff --git a/kernel/model/export.go b/kernel/model/export.go index f255a14a7..720ddc586 100644 --- a/kernel/model/export.go +++ b/kernel/model/export.go @@ -451,27 +451,24 @@ func ExportNotebookSY(id string) (zipPath string) { return } -func ExportSY(id string) (name, zipPath string) { - block := treenode.GetBlockTree(id) - if nil == block { - logging.LogErrorf("not found block [%s]", id) - return - } - - boxID := block.BoxID - box := Conf.Box(boxID) +func ExportSYs(ids []string) (zipPath string) { + block := treenode.GetBlockTree(ids[0]) + box := Conf.Box(block.BoxID) baseFolderName := path.Base(block.HPath) if "." == baseFolderName { baseFolderName = path.Base(block.Path) } - rootPath := block.Path - docPaths := []string{rootPath} - docFiles := box.ListFiles(strings.TrimSuffix(block.Path, ".sy")) - for _, docFile := range docFiles { - docPaths = append(docPaths, docFile.path) + + var docPaths []string + bts := treenode.GetBlockTrees(ids) + for _, bt := range bts { + docPaths = append(docPaths, bt.Path) + docFiles := box.ListFiles(strings.TrimSuffix(bt.Path, ".sy")) + for _, docFile := range docFiles { + docPaths = append(docPaths, docFile.path) + } } - zipPath = exportSYZip(boxID, path.Dir(rootPath), baseFolderName, docPaths) - name = util.GetTreeID(block.Path) + zipPath = exportSYZip(block.BoxID, path.Dir(block.Path), baseFolderName, docPaths) return }