新增头部导航链接配置与多语言支持

- 迁移 v10 新增 nav_links 与 nav_link_translations 两张表,文案按语言存储
- 公开 GET /api/nav-links 返回启用链接(按 sort/id 排序);管理员可增删改查与启停
- URL 仅允许站内路径、http(s) 与 mailto,拒绝 javascript: 等危险协议;译文替换与校验
- 前端删除写死的“主页”按钮,头部按当前语言渲染动态链接,新窗口加 rel=noopener noreferrer
- 后台管理页新增“头部导航链接”卡片(三语文案、URL、打开方式、排序、状态),改动即时生效
- 补充 nav 接口与迁移测试、三语文案,重新生成 Swagger 文档
This commit is contained in:
2026-09-21 21:26:30 +08:00
parent 067ade546f
commit 4449250e97
19 files changed
+2198 -13

No files matched your search

+375
View File
@@ -538,6 +538,279 @@ const docTemplate = `{
}
}
},
"/nav-links": {
"get": {
"description": "Public header navigation links (status enabled), ordered by sort ASC then id ASC, with translations.",
"produces": [
"application/json"
],
"tags": [
"public"
],
"summary": "List nav links",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.NavLink"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Admin only. Create a header navigation link. url accepts site-relative paths (/...), http(s) URLs, and mailto links; translations must include at least one non-empty label.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Create a nav link",
"parameters": [
{
"description": "Nav link payload",
"name": "link",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/nav.Request"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/model.NavLink"
}
},
"400": {
"description": "invalid request",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"401": {
"description": "unauthorized or session expired",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"403": {
"description": "admin permission required or account disabled",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
}
}
}
},
"/nav-links/list": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Admin only. List all header navigation links including disabled ones, with translations.",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "List all nav links",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.NavLink"
}
}
},
"401": {
"description": "unauthorized or session expired",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"403": {
"description": "admin permission required or account disabled",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
}
}
}
},
"/nav-links/{id}": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Admin only. Update a header navigation link; translations are replaced by the provided list.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Update a nav link",
"parameters": [
{
"type": "integer",
"example": 1,
"description": "Nav link ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Nav link payload",
"name": "link",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/nav.Request"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.NavLink"
}
},
"400": {
"description": "invalid request or id",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"401": {
"description": "unauthorized or session expired",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"403": {
"description": "admin permission required or account disabled",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"404": {
"description": "record not found",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Admin only. Delete a header navigation link and its translations.",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Delete a nav link",
"parameters": [
{
"type": "integer",
"example": 1,
"description": "Nav link ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "Deleted"
},
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"401": {
"description": "unauthorized or session expired",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"403": {
"description": "admin permission required or account disabled",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"404": {
"description": "record not found",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpx.ErrorResponse"
}
}
}
}
},
"/notes": {
"get": {
"security": [
@@ -1869,6 +2142,52 @@ const docTemplate = `{
}
}
},
"model.NavLink": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"open_in_new_window": {
"type": "boolean"
},
"sort": {
"type": "integer"
},
"status": {
"type": "integer"
},
"translations": {
"type": "array",
"items": {
"$ref": "#/definitions/model.NavLinkTranslation"
}
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"model.NavLinkTranslation": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"locale": {
"type": "string"
},
"nav_link_id": {
"type": "integer"
}
}
},
"model.Note": {
"type": "object",
"properties": {
@@ -1975,6 +2294,62 @@ const docTemplate = `{
}
}
},
"nav.Request": {
"type": "object",
"required": [
"translations",
"url"
],
"properties": {
"open_in_new_window": {
"type": "boolean",
"example": false
},
"sort": {
"type": "integer",
"example": 0
},
"status": {
"type": "integer",
"enum": [
0,
1
],
"example": 1
},
"translations": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/nav.TranslationRequest"
}
},
"url": {
"type": "string",
"maxLength": 512,
"example": "/profile"
}
}
},
"nav.TranslationRequest": {
"type": "object",
"required": [
"label",
"locale"
],
"properties": {
"label": {
"type": "string",
"maxLength": 100,
"example": "首页"
},
"locale": {
"type": "string",
"maxLength": 10,
"example": "zh-CN"
}
}
},
"note.ListResponse": {
"type": "object",
"properties": {