Files
opsxapi/server/internal/server/shared_docs.go
T
logan 2bb4ef7a95 桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
2026-10-16 03:03:36 +08:00

66 lines
3.8 KiB
Go

package server
import (
"database/sql"
"encoding/json"
"html/template"
"net/http"
"github.com/gin-gonic/gin"
"opsxapi/server/internal/store"
)
type sharedDocument struct {
Name string `json:"name"`
Endpoints []struct {
Method string `json:"method"`
Path string `json:"path"`
Name string `json:"name"`
Description string `json:"description"`
Parameters []struct {
Name string `json:"name"`
In string `json:"in"`
Required bool `json:"required"`
Description string `json:"description"`
Example string `json:"example"`
} `json:"parameters"`
RequestBody *struct {
ContentType string `json:"contentType"`
Example string `json:"example"`
} `json:"requestBody"`
Responses []struct {
Status string `json:"status"`
Description string `json:"description"`
ContentType string `json:"contentType"`
Example string `json:"example"`
} `json:"responses"`
MockURL string `json:"mockUrl"`
} `json:"endpoints"`
}
var sharedDocsTemplate = 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>{{.Name}} · API 文档</title><style>body{font:15px/1.6 system-ui,sans-serif;margin:0;background:#0d1524;color:#e6edf8}header{padding:20px 28px;border-bottom:1px solid #33445d}main{display:grid;grid-template-columns:minmax(190px,260px) minmax(0,1fr);max-width:1300px;margin:auto}nav{padding:20px;border-right:1px solid #33445d}nav a{display:block;color:#93c5fd;padding:5px 0;text-decoration:none;overflow-wrap:anywhere}article{padding:14px 28px;border-bottom:1px solid #33445d}code,pre{font:13px/1.5 ui-monospace,monospace;overflow-wrap:anywhere}pre{white-space:pre-wrap;background:#162238;border-radius:8px;padding:14px;overflow:auto}section{min-width:0}small,.muted{color:#aabbd3}h2{overflow-wrap:anywhere}table{border-collapse:collapse;width:100%}td,th{padding:8px;border-bottom:1px solid #33445d;text-align:left}a{color:#93c5fd}@media(max-width:680px){main{display:block}nav{border-right:0;border-bottom:1px solid #33445d}}</style></head><body><header><h1>{{.Name}}</h1><small>自托管 API 文档 · 只读快照</small></header><main><nav>{{range $i,$e:=.Endpoints}}<a href="#api-{{$i}}">{{$e.Method}} {{$e.Path}}</a>{{end}}</nav><section>{{range $i,$e:=.Endpoints}}<article id="api-{{$i}}"><h2>{{$e.Name}}</h2><p><code>{{$e.Method}} {{$e.Path}}</code></p><p class="muted">{{$e.Description}}</p>{{if $e.Parameters}}<h3>请求参数</h3><table><tr><th>参数</th><th>位置</th><th>必填</th><th>示例</th><th>说明</th></tr>{{range $e.Parameters}}<tr><td>{{.Name}}</td><td>{{.In}}</td><td>{{if .Required}}是{{else}}否{{end}}</td><td>{{.Example}}</td><td>{{.Description}}</td></tr>{{end}}</table>{{end}}{{with $e.RequestBody}}<h3>请求体 · {{.ContentType}}</h3><pre>{{.Example}}</pre>{{end}}{{range $e.Responses}}<h3>HTTP {{.Status}} · {{.ContentType}}</h3><p>{{.Description}}</p><pre>{{.Example}}</pre>{{end}}{{if $e.MockURL}}<h3>Mock 地址</h3><p><code>{{$e.MockURL}}</code></p>{{end}}</article>{{end}}</section></main></body></html>`))
func registerSharedDocs(r *gin.Engine, s *store.Store) {
r.GET("/docs/:token", func(c *gin.Context) {
record, err := s.PublishedDocByToken(c.Param("token"))
if err == sql.ErrNoRows {
c.AbortWithStatus(404)
return
}
if err != nil {
c.AbortWithStatus(500)
return
}
var doc sharedDocument
if json.Unmarshal([]byte(record.Content), &doc) != nil {
c.AbortWithStatus(500)
return
}
c.Header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
c.Header("X-Content-Type-Options", "nosniff")
c.Status(http.StatusOK)
c.Header("Content-Type", "text/html; charset=utf-8")
_ = sharedDocsTemplate.Execute(c.Writer, doc)
})
}