feat: v3 增强 - FTS5全文搜索+双向链接/知识图谱+自动保存草稿+标签管理+实时预览编辑器+PWA

- FTS5中文分词搜索(需-tags=sqlite_fts5)
- [[wiki链接]]反向链接+SVG知识图谱
- 自动保存草稿(不触发版本历史)
- 标签重命名/合并/删除+使用统计
- 后台编辑实时预览+格式工具栏
- PWA manifest+service worker+移动端适配
- 冒烟测试扩至66例全通过
This commit is contained in:
Your Name
2026-08-11 12:38:41 +08:00
parent cbfaac3d4f
commit b93c6f1e16
14 changed files with 1720 additions and 16 deletions
+129
View File
@@ -661,3 +661,132 @@ func sanitizeFileName(name string) string {
}
return name
}
// ─────────────── 自动保存草稿 ───────────────
// SaveDraft 保存笔记草稿(自动保存)
func (h *NoteHandler) SaveDraft(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
var req struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
if err := h.svc.SaveDraft(uint(id), req.Content); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, nil)
}
// DiscardDraft 清除指定笔记的草稿
func (h *NoteHandler) DiscardDraft(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
if err := h.svc.ClearDraft(uint(id)); err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, nil)
}
// ─────────────── 双向链接 / 知识图谱 ───────────────
// GetBacklinks 获取指定笔记的反向链接列表
func (h *NoteHandler) GetBacklinks(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fail(c, http.StatusBadRequest, "无效的笔记 ID")
return
}
links, err := h.svc.GetBacklinks(uint(id), "")
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, links)
}
// GetKnowledgeGraph 获取知识图谱数据(节点 + 边)
func (h *NoteHandler) GetKnowledgeGraph(c *gin.Context) {
graph, err := h.svc.GetKnowledgeGraph()
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, graph)
}
// ─────────────── 标签管理 ───────────────
// GetTagUsage 获取标签及使用次数
func (h *NoteHandler) GetTagUsage(c *gin.Context) {
usage, err := h.svc.GetTagUsage()
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
success(c, usage)
}
// RenameTag 重命名标签
func (h *NoteHandler) RenameTag(c *gin.Context) {
var req struct {
OldName string `json:"old_name" binding:"required"`
NewName string `json:"new_name" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.RenameTag(req.OldName, req.NewName)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
}
success(c, gin.H{"changed": changed})
}
// MergeTag 合并标签(from → to
func (h *NoteHandler) MergeTag(c *gin.Context) {
var req struct {
From string `json:"from" binding:"required"`
To string `json:"to" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.MergeTag(req.From, req.To)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
}
success(c, gin.H{"changed": changed})
}
// DeleteTag 删除标签(从所有笔记中移除)
func (h *NoteHandler) DeleteTag(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, "请求参数错误")
return
}
changed, err := h.svc.DeleteTag(req.Name)
if err != nil {
fail(c, http.StatusBadRequest, err.Error())
return
}
success(c, gin.H{"changed": changed})
}