mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-16 22:50:13 +01:00
Improve the outline panel (#15814)
* ✨ 实现大纲持久化 - 添加折叠状态变化的实时保存功能 - 在文档更新时恢复折叠状态 - 保存拖拽前的折叠状态并在拖拽后恢复 - 更新本地存储结构以支持折叠状态 * 🎨 clean code * ✨ 在`data/storage`文件夹下创建`outline/${docID}.json`文件,存储标题outline展开信息 * ✨ alt click折叠/展开统计标题 * ✨添加层级控制功能 - 新增层级控制滑条,允许用户展开指定层级 * 🌐 添加多语言支持的展开层级功能 * ✨ f添加层级重置显示功能 - 新增 resetLevelDisplay 方法以重置层级显示状态 - 更新层级控制的初始化逻辑,默认不显示层级 - 在文档切换时重置层级显示状态 * ✨优化层级控制功能 - 添加用户主动层级控制标记 - 修改层级显示重置逻辑,仅在非用户操作时重置 - 更新层级控制滑条的点击事件处理 * ♻️ 重构大纲存储逻辑 - 合并大纲存储为单一文件 outline.json * ♻️ outline.json 单行存储 * ♻️ outline.json参考recent-doc.json,只存储前1000个文档信息,每次新增信息会把数据放在最前面 * ♻️单行存储json * ♻️ 增加outline.json存储限制至2000个 * ✨ 新增`保持当前标题展开`按钮,`保持全部展开`改为`全部展开`按钮 * ✨ 保持当前标题展开优化 - 超过两级折叠,也能都展开 - 如果父节点折叠,展开时自动折叠兄弟节点,只展开当前节点路径,如果父节点是展开状态,则不影响兄弟节点折叠状态 * 🔥 移除层级文本 * ✨ 右键点击toggle时展开所有子标题 * ✨ 右键click点击折叠图标,会折叠/展开所有子标题 * ✨ 大纲支持筛选功能 * ✨ feat(大纲): 优化筛选功能以显示所有子标题 - 添加 showAllDescendants 函数以显示所有子标题 - 修改 processUL 函数以在父标题命中时显示所有子标题 - 确保未命中的子标题隐藏 * ✨ 优化大纲筛选 - ✅如果标题命中筛选,这个标题的所有父标题展开,以显示出这个标题位置 - ✅如果父标题命中筛选,子项都没有命中筛选,则折叠全部子项(依然可以展开显示) - ✅如果父标题命中筛选,子项也有命中筛选,则展开命中的子项,其他无关子项隐藏 * 💄 展开层级改为按钮,原先的圆点样式碍眼 * 💄 展开层级改为按钮,原先的圆点样式碍眼 * 💄 style(菜单): 优化展开层级菜单的样式和位置 - 添加图标以增强可视化 - 调整菜单弹出位置以改善用户体验 * ♻️ refactor(大纲): 优化标题级别获取逻辑 - 调整展开到指定层级的逻辑,使用标题级别进行判断 * 🎨 * ✨ feat(大纲): 添加右键菜单功能 - 实现右键点击标题时显示上下文菜单 - 增加标题升级、降级、插入、删除等操作 - 基于标题级别展开/折叠同级标题 * ✨ feat(大纲): 添加子标题功能 - 在右键菜单中添加“添加子标题”选项 - 实现子标题的添加逻辑,支持最大级别为H6 - 使用当前标题作为父标题,插入新子标题 * ✨ feat(大纲): "添加子标题"确保父标题展开状态 * ✨ feat(大纲): 使用expandIds方式保存父标题展开状态 - 确保父标题保持展开状态 - 保存展开状态到持久化存储 - 移除冗余的状态保存逻辑 * ✨ feat(大纲): 调整右键菜单顺序,将“全部折叠”功能移至“全部展开”之后 * 🌐 i18n optimization
This commit is contained in:
parent
0211e04c2b
commit
7545c2517f
19 changed files with 1768 additions and 55 deletions
|
|
@ -78,6 +78,9 @@ func ServeAPI(ginServer *gin.Engine) {
|
|||
ginServer.Handle("POST", "/api/storage/getCriteria", model.CheckAuth, getCriteria)
|
||||
ginServer.Handle("POST", "/api/storage/removeCriterion", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeCriterion)
|
||||
ginServer.Handle("POST", "/api/storage/getRecentDocs", model.CheckAuth, getRecentDocs)
|
||||
ginServer.Handle("POST", "/api/storage/getOutlineStorage", model.CheckAuth, getOutlineStorage)
|
||||
ginServer.Handle("POST", "/api/storage/setOutlineStorage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setOutlineStorage)
|
||||
ginServer.Handle("POST", "/api/storage/removeOutlineStorage", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, removeOutlineStorage)
|
||||
|
||||
ginServer.Handle("POST", "/api/account/login", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login)
|
||||
ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkActivationcode)
|
||||
|
|
|
|||
|
|
@ -180,3 +180,59 @@ func getLocalStorage(c *gin.Context) {
|
|||
data := model.GetLocalStorage()
|
||||
ret.Data = data
|
||||
}
|
||||
|
||||
func getOutlineStorage(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
docID := arg["docID"].(string)
|
||||
data, err := model.GetOutlineStorage(docID)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
ret.Data = data
|
||||
}
|
||||
|
||||
func setOutlineStorage(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
docID := arg["docID"].(string)
|
||||
val := arg["val"].(interface{})
|
||||
err := model.SetOutlineStorage(docID, val)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func removeOutlineStorage(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
docID := arg["docID"].(string)
|
||||
err := model.RemoveOutlineStorage(docID)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ type RecentDoc struct {
|
|||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type OutlineDoc struct {
|
||||
DocID string `json:"docID"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
var recentDocLock = sync.Mutex{}
|
||||
|
||||
func RemoveRecentDoc(ids []string) {
|
||||
|
|
@ -402,3 +407,124 @@ func getLocalStorage() (ret map[string]interface{}) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
var outlineStorageLock = sync.Mutex{}
|
||||
|
||||
func GetOutlineStorage(docID string) (ret map[string]interface{}, err error) {
|
||||
outlineStorageLock.Lock()
|
||||
defer outlineStorageLock.Unlock()
|
||||
|
||||
ret = map[string]interface{}{}
|
||||
outlineDocs, err := getOutlineDocs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, doc := range outlineDocs {
|
||||
if doc.DocID == docID {
|
||||
ret = doc.Data
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetOutlineStorage(docID string, val interface{}) (err error) {
|
||||
outlineStorageLock.Lock()
|
||||
defer outlineStorageLock.Unlock()
|
||||
|
||||
outlineDoc := &OutlineDoc{
|
||||
DocID: docID,
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if valMap, ok := val.(map[string]interface{}); ok {
|
||||
outlineDoc.Data = valMap
|
||||
}
|
||||
|
||||
outlineDocs, err := getOutlineDocs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果文档已存在,先移除旧的
|
||||
for i, doc := range outlineDocs {
|
||||
if doc.DocID == docID {
|
||||
outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 将新的文档信息添加到最前面
|
||||
outlineDocs = append([]*OutlineDoc{outlineDoc}, outlineDocs...)
|
||||
|
||||
// 限制为2000个文档
|
||||
if 2000 < len(outlineDocs) {
|
||||
outlineDocs = outlineDocs[:2000]
|
||||
}
|
||||
|
||||
err = setOutlineDocs(outlineDocs)
|
||||
return
|
||||
}
|
||||
|
||||
func RemoveOutlineStorage(docID string) (err error) {
|
||||
outlineStorageLock.Lock()
|
||||
defer outlineStorageLock.Unlock()
|
||||
|
||||
outlineDocs, err := getOutlineDocs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i, doc := range outlineDocs {
|
||||
if doc.DocID == docID {
|
||||
outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = setOutlineDocs(outlineDocs)
|
||||
return
|
||||
}
|
||||
|
||||
func setOutlineDocs(outlineDocs []*OutlineDoc) (err error) {
|
||||
dirPath := filepath.Join(util.DataDir, "storage")
|
||||
if err = os.MkdirAll(dirPath, 0755); err != nil {
|
||||
logging.LogErrorf("create storage [outline] dir failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := gulu.JSON.MarshalJSON(outlineDocs)
|
||||
if err != nil {
|
||||
logging.LogErrorf("marshal storage [outline] failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
lsPath := filepath.Join(dirPath, "outline.json")
|
||||
err = filelock.WriteFile(lsPath, data)
|
||||
if err != nil {
|
||||
logging.LogErrorf("write storage [outline] failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getOutlineDocs() (ret []*OutlineDoc, err error) {
|
||||
ret = []*OutlineDoc{}
|
||||
dataPath := filepath.Join(util.DataDir, "storage/outline.json")
|
||||
if !filelock.IsExist(dataPath) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := filelock.ReadFile(dataPath)
|
||||
if err != nil {
|
||||
logging.LogErrorf("read storage [outline] failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
||||
logging.LogErrorf("unmarshal storage [outline] failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue