From e4967b99eea75e8da983147419bc200d48263118 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 11 Sep 2026 16:25:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=A4=87=E6=A1=88=E5=8F=B7?= =?UTF-8?q?=E5=8F=AF=E7=BC=96=E8=BE=91=EF=BC=8C=E9=A1=B5=E8=84=9A=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=94=B1=E6=9C=8D=E5=8A=A1=E7=AB=AF=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 备案号原来只能写死在 pageInfo.footer 的 HTML 里。现在 /admin/ 站点信息 拆出「ICP 备案号」「公安备案号」两个输入框,页脚 HTML 由服务端 beianFooter() 统一按模板生成(含 html.EscapeString 转义), 自定义页脚 HTML 降级为「高级」折叠项;旧 footer 会被解析回填到字段。 conf.example.yml 里重复的 .navi-search-bar 规则已由 web/navi.css 接管,删除。 --- admin-server/main.go | 41 +++++++++++++++++++++++++ admin-server/main_test.go | 63 +++++++++++++++++++++++++++++++++++++++ admin/index.html | 33 +++++++++++++++++--- conf.example.yml | 2 +- 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 admin-server/main_test.go diff --git a/admin-server/main.go b/admin-server/main.go index e445c40..d26ada9 100644 --- a/admin-server/main.go +++ b/admin-server/main.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "html" "io" "log" "net/http" @@ -475,6 +476,33 @@ func handleSyncInfo(w http.ResponseWriter, r *http.Request) { }) } +// beianFooter 按固定模板生成页脚备案号 HTML。 +// +// 备案号在 /admin/ 里就是两个普通输入框,模板统一由服务端生成, +// 用户不必写 HTML;类名与 web/navi.css 里的 .navi-footer 对应。 +// footer 字段本身仍然保留:传了自定义 HTML 就以它为准(高级用法)。 +func beianFooter(icp, gongan string) string { + icp = strings.TrimSpace(icp) + gongan = strings.TrimSpace(gongan) + parts := make([]string, 0, 2) + if icp != "" { + parts = append(parts, ``+html.EscapeString(icp)+``) + } + if gongan != "" { + parts = append(parts, ``+html.EscapeString(gongan)+``) + } + if len(parts) == 0 { + return "" + } + return `` +} + +// mapString 读取 map 里的字符串字段,缺失或类型不符时返回空串。 +func mapString(m map[string]any, key string) string { + v, _ := m[key].(string) + return v +} + // ── 站点信息:标题 / 副标题 / 顶部导航 / 页脚 ── // 这些东西原先只能在 conf.yml 里手改,现在可在 /admin/ 编辑。 func handlePageInfo(w http.ResponseWriter, r *http.Request) { @@ -501,6 +529,8 @@ func handlePageInfo(w http.ResponseWriter, r *http.Request) { Title *string `json:"title"` Description *string `json:"description"` Footer *string `json:"footer"` + ICP *string `json:"icp"` + Gongan *string `json:"gongan"` NavLinks *[]map[string]any `json:"navLinks"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { @@ -516,6 +546,17 @@ func handlePageInfo(w http.ResponseWriter, r *http.Request) { if body.Footer != nil { cfg.PageInfo["footer"] = *body.Footer } + // 备案号单独存字段,便于在 /admin/ 里直接编辑,而不是让用户写 HTML。 + if body.ICP != nil { + cfg.PageInfo["icp"] = *body.ICP + } + if body.Gongan != nil { + cfg.PageInfo["gongan"] = *body.Gongan + } + // 页脚为空(留空 = 交给模板)时,按备案号重新生成。 + if strings.TrimSpace(mapString(cfg.PageInfo, "footer")) == "" { + cfg.PageInfo["footer"] = beianFooter(mapString(cfg.PageInfo, "icp"), mapString(cfg.PageInfo, "gongan")) + } if body.NavLinks != nil { cfg.PageInfo["navLinks"] = *body.NavLinks } diff --git a/admin-server/main_test.go b/admin-server/main_test.go new file mode 100644 index 0000000..7487c5f --- /dev/null +++ b/admin-server/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "strings" + "testing" +) + +func TestBeianFooter(t *testing.T) { + cases := []struct { + name, icp, gongan string + wantParts []string + wantEmpty bool + }{ + {name: "both", icp: "京ICP备00000000号-1", gongan: "京公网安备00000000000000号", + wantParts: []string{"class=\"navi-footer\"", "beian.miit.gov.cn", "京ICP备00000000号-1", "navi-footer-sep", "beian.mps.gov.cn", "京公网安备00000000000000号"}}, + {name: "only icp", icp: " 京ICP备12345678号 ", wantParts: []string{"class=\"navi-footer\"", "京ICP备12345678号"}}, + {name: "only gongan", gongan: "沪公网安备31010102002555号", wantParts: []string{"class=\"navi-footer\"", "沪公网安备31010102002555号"}}, + {name: "blank", icp: " ", gongan: "", wantEmpty: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := beianFooter(tc.icp, tc.gongan) + if tc.wantEmpty { + if got != "" { + t.Fatalf("want empty footer, got %q", got) + } + return + } + for _, p := range tc.wantParts { + if !strings.Contains(got, p) { + t.Fatalf("footer %q missing %q", got, p) + } + } + if strings.Contains(got, " ") { + t.Fatalf("footer keeps stray whitespace: %q", got) + } + }) + } +} + +// 备案号里若混入引号/尖括号,必须被转义,不能原样拼进 HTML。 +func TestBeianFooterEscapesInput(t *testing.T) { + got := beianFooter(`">`, "") + if strings.Contains(got, "