diff --git a/.gitignore b/.gitignore index e57407c..96d9e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ fun-cli *.test +/fun diff --git a/README.md b/README.md index a79c679..545d07f 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,15 @@ fun docs -upstream http://127.0.0.1:9000 # 指定后端地址 fun docs -no-run # 后端已在别处运行 ``` -- 浅色标准文档风格:侧栏服务/方法树(stream/void 徽标)+ 搜索 +- 浅色文档风格:侧栏服务/方法树(stream 徽标)+ 搜索(`/` 快速聚焦) +- **分享**:标题旁一键复制深链(`#服务.方法?req=...`),自带请求体,打开即还原 - 参数表:字段/类型/示例值/说明;枚举下拉、嵌套 DTO 分层、可空标注 +- **说明**:方法说明与字段备注可编辑,自动保存到 `fun-docs-edits.json` + (启动时自动创建,`-edits` 可改路径);返回类型递归展示 - **请求**:表单实时生成 JSON,可直接编辑后发送 - **State**:"添加字段"式键值编辑(存 localStorage),值注入请求 `state` 对象—— 键以后端 Guard 读取为准,前端只负责填值 -- **响应**:格式化 JSON;NDJSON 流式方法逐行实时追加 +- **响应**:格式化 JSON 语法高亮;NDJSON 流式方法逐行实时追加 - 文档站反向代理 `/cell` 到后端,同源无 CORS 问题 ## 元数据 JSON diff --git a/cmd/fun/docs.go b/cmd/fun/docs.go index 6e804de..8da747d 100644 --- a/cmd/fun/docs.go +++ b/cmd/fun/docs.go @@ -15,6 +15,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "sync" "syscall" ) @@ -28,6 +29,7 @@ func cmdDocs(args []string) { from := fs.String("from", "", "从元数据 JSON 文件读取") pkg := fs.String("p", ".", "业务 main 包路径(用于采集元数据与自动启动后端)") noStart := fs.Bool("no-run", false, "不自动启动后端(后端已在别处运行时使用)") + editsPath := fs.String("edits", "fun-docs-edits.json", "说明/备注编辑存档文件(启动时自动创建)") fs.Parse(args) var meta *FunMeta @@ -76,6 +78,8 @@ func cmdDocs(args []string) { } proxy := httputil.NewSingleHostReverseProxy(target) + loadOrCreateEdits(*editsPath) + metaJSON := renderMetaJSON(meta) mux := http.NewServeMux() @@ -88,6 +92,39 @@ func cmdDocs(args []string) { case "/fun-meta.json": w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write(metaJSON) + case "/fun-docs-edits.json": + editsMu.RLock() + data, _ := json.Marshal(edits) + editsMu.RUnlock() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write(data) + case "/fun-docs-edits": + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + Key string `json:"key"` + Entry json.RawMessage `json:"entry"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Key == "" { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + editsMu.Lock() + if len(req.Entry) == 0 || string(req.Entry) == "null" { + delete(edits, req.Key) + } else { + edits[req.Key] = req.Entry + } + data, _ := json.MarshalIndent(edits, "", " ") + editsMu.Unlock() + if err := os.WriteFile(*editsPath, data, 0o644); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(`{"ok":true}`)) default: proxy.ServeHTTP(w, r) } @@ -110,9 +147,10 @@ func cmdDocs(args []string) { ───────────────────────────────────────── 文档站: http://%s 后端代理: %s → %s + 说明存档: %s ───────────────────────────────────────── %d 个服务 / %d 个方法。Ctrl+C 退出(连同自动拉起的后端) -`, *addr, *upstream, *upstream, len(meta.Services), nMethods) +`, *addr, *upstream, *upstream, *editsPath, len(meta.Services), nMethods) if err := http.ListenAndServe(*addr, mux); err != nil { fmt.Fprintln(os.Stderr, "fun: 文档站启动失败:", err) @@ -121,6 +159,27 @@ func cmdDocs(args []string) { } } +// 说明/备注编辑存档:map[服务.方法] -> {note, fields:{路径:备注}} +var ( + editsMu sync.RWMutex + edits = map[string]json.RawMessage{} +) + +func loadOrCreateEdits(path string) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + os.WriteFile(path, []byte("{}\n"), 0o644) + return + } + fmt.Fprintln(os.Stderr, "fun: 读取编辑存档失败:", err) + return + } + if err := json.Unmarshal(data, &edits); err != nil { + fmt.Fprintln(os.Stderr, "fun: 编辑存档格式无效,按空处理:", err) + } +} + func renderMetaJSON(meta *FunMeta) []byte { data, err := json.Marshal(meta) if err != nil { diff --git a/cmd/fun/docsui/index.html b/cmd/fun/docsui/index.html index 55f9fe5..88f1d7a 100644 --- a/cmd/fun/docsui/index.html +++ b/cmd/fun/docsui/index.html @@ -6,122 +6,278 @@
在左侧选择一个方法查看文档
+按/快速搜索
+