- 后端 goldmark 渲染管线:/docs/:slug 页面 + /docs/search-data.json 搜索索引, manifest.json 驱动分组导航,正文剥 H1 避免与页面头重复 - 三栏布局(分组侧栏/内容/本页目录)+ 顶栏(品牌/版本徽章/搜索面板/官网入口), docs.css 品牌绿版 + docs.js(代码复制/轻量高亮/提示块/TOC 滚动高亮/移动端抽屉) - 替换 docsify;无上一页时空占位无边框;visual-judge 两轮评审(初版 fail → 修复后 pass)
111 lines
4.0 KiB
Go
111 lines
4.0 KiB
Go
package controllers
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
|
|
"lokistack/services"
|
|
"lokistack/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// 文档站控制器:模板内嵌(三栏布局,参考 opsxapi/opsxshell 文档站,品牌绿)
|
|
var docsTemplate = template.Must(template.New("docs").Parse(`<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{.Page.Title}} · LokiStack 文档</title>
|
|
<meta name="description" content="LokiStack 用户手册 — Loki 日志管理平台:向导式部署、采集策略、日志查询与分析、告警通知。">
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23059669'/%3E%3Ctext x='32' y='44' font-size='34' font-weight='800' fill='%23fff' text-anchor='middle' font-family='sans-serif'%3EL%3C/text%3E%3C/svg%3E">
|
|
<link rel="stylesheet" href="/docs/assets/docs.css">
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="topbar-inner">
|
|
<button class="menu-btn" id="menu-btn" aria-label="打开导航"><svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></svg></button>
|
|
<a class="brand" href="/docs">
|
|
<span class="brand-logo">L</span>
|
|
<span class="brand-name">LokiStack <em>文档</em></span>
|
|
</a>
|
|
<span class="ver-badge">v{{.Version}}</span>
|
|
<div class="search">
|
|
<input id="search-input" type="search" placeholder="搜索文档…" autocomplete="off" spellcheck="false">
|
|
<div class="search-panel" id="search-panel" hidden></div>
|
|
</div>
|
|
<nav class="top-links">
|
|
<a href="/homepage">官网</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="layout" id="layout">
|
|
<aside class="sidebar" id="sidebar">
|
|
{{range .Manifest.Groups}}
|
|
<div class="nav-group">
|
|
<div class="nav-group-title">{{.Name}}</div>
|
|
{{range .Pages}}
|
|
<a class="nav-link{{if eq .Slug $.Page.Slug}} active{{end}}" href="/docs/{{.Slug}}">{{.Title}}</a>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
<div class="sidebar-foot">© 2026 LokiStack · Loki 日志管理平台</div>
|
|
</aside>
|
|
|
|
<main class="content">
|
|
<div class="breadcrumb">文档 <span>/</span> {{.Page.Group}} <span>/</span> {{.Page.Title}}</div>
|
|
<h1 class="page-title">{{.Page.Title}}</h1>
|
|
{{if .Page.Updated}}<div class="page-meta">更新于 {{.Page.Updated}}</div>{{end}}
|
|
<article class="md">
|
|
{{.Page.Content}}
|
|
</article>
|
|
|
|
<div class="pn-nav">
|
|
{{with .Page.Prev}}
|
|
<a class="pn prev" href="/docs/{{.Slug}}"><span class="pn-label">← 上一页</span><span class="pn-title">{{.Title}}</span></a>
|
|
{{else}}<span class="pn-empty"></span>{{end}}
|
|
{{with .Page.Next}}
|
|
<a class="pn next" href="/docs/{{.Slug}}"><span class="pn-label">下一页 →</span><span class="pn-title">{{.Title}}</span></a>
|
|
{{else}}<span class="pn-empty"></span>{{end}}
|
|
</div>
|
|
</main>
|
|
|
|
<aside class="toc">
|
|
<div class="toc-title">本页目录</div>
|
|
<nav class="toc-nav">
|
|
{{range .Page.TOC}}<a class="toc-l{{.Level}}" href="#{{.ID}}">{{.Text}}</a>
|
|
{{end}}
|
|
</nav>
|
|
</aside>
|
|
</div>
|
|
|
|
<script src="/docs/assets/docs.js" defer></script>
|
|
</body>
|
|
</html>`))
|
|
|
|
type DocsController struct{}
|
|
|
|
func NewDocsController() *DocsController { return &DocsController{} }
|
|
|
|
func (ctrl *DocsController) Home(c *gin.Context) {
|
|
c.Redirect(http.StatusFound, "/docs/"+services.DocsFirstSlug())
|
|
}
|
|
|
|
func (ctrl *DocsController) Page(c *gin.Context) {
|
|
page, manifest, err := services.DocsRenderPage(c.Param("slug"))
|
|
if err != nil {
|
|
utils.NotFound(c, "文档页面不存在")
|
|
return
|
|
}
|
|
_ = docsTemplate.Execute(c.Writer, gin.H{"Page": page, "Manifest": manifest, "Version": manifest.ProductVersion})
|
|
}
|
|
|
|
func (ctrl *DocsController) SearchData(c *gin.Context) {
|
|
data, err := services.DocsSearchData()
|
|
if err != nil {
|
|
data = []map[string]string{}
|
|
}
|
|
c.JSON(http.StatusOK, data)
|
|
}
|