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

- 迁移 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

+1
View File
@@ -14,6 +14,7 @@ internal/
├── usergroup/ 用户组管理
├── note/ 便签
├── site/ 站点信息
├── nav/ 头部导航链接
├── file/ 文件上传、删除、查看与本地存储
├── avatar/ 当前用户头像
├── httpx/ HTTP 公共能力:ErrorResponse、分页、ID 解析
+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": {
+375
View File
@@ -531,6 +531,279 @@
}
}
},
"/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": [
@@ -1862,6 +2135,52 @@
}
}
},
"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": {
@@ -1968,6 +2287,62 @@
}
}
},
"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": {
+252
View File
@@ -105,6 +105,36 @@ definitions:
uploader_id:
type: integer
type: object
model.NavLink:
properties:
created_at:
type: string
id:
type: integer
open_in_new_window:
type: boolean
sort:
type: integer
status:
type: integer
translations:
items:
$ref: '#/definitions/model.NavLinkTranslation'
type: array
updated_at:
type: string
url:
type: string
type: object
model.NavLinkTranslation:
properties:
label:
type: string
locale:
type: string
nav_link_id:
type: integer
type: object
model.Note:
properties:
content:
@@ -176,6 +206,47 @@ definitions:
updated_at:
type: string
type: object
nav.Request:
properties:
open_in_new_window:
example: false
type: boolean
sort:
example: 0
type: integer
status:
enum:
- 0
- 1
example: 1
type: integer
translations:
items:
$ref: '#/definitions/nav.TranslationRequest'
minItems: 1
type: array
url:
example: /profile
maxLength: 512
type: string
required:
- translations
- url
type: object
nav.TranslationRequest:
properties:
label:
example: 首页
maxLength: 100
type: string
locale:
example: zh-CN
maxLength: 10
type: string
required:
- label
- locale
type: object
note.ListResponse:
properties:
items:
@@ -715,6 +786,187 @@ paths:
summary: Update current user avatar
tags:
- user
/nav-links:
get:
description: Public header navigation links (status enabled), ordered by sort
ASC then id ASC, with translations.
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.NavLink'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/httpx.ErrorResponse'
summary: List nav links
tags:
- public
post:
consumes:
- application/json
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.
parameters:
- description: Nav link payload
in: body
name: link
required: true
schema:
$ref: '#/definitions/nav.Request'
produces:
- application/json
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'
security:
- BearerAuth: []
summary: Create a nav link
tags:
- admin
/nav-links/{id}:
delete:
description: Admin only. Delete a header navigation link and its translations.
parameters:
- description: Nav link ID
example: 1
in: path
name: id
required: true
type: integer
produces:
- application/json
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'
security:
- BearerAuth: []
summary: Delete a nav link
tags:
- admin
put:
consumes:
- application/json
description: Admin only. Update a header navigation link; translations are replaced
by the provided list.
parameters:
- description: Nav link ID
example: 1
in: path
name: id
required: true
type: integer
- description: Nav link payload
in: body
name: link
required: true
schema:
$ref: '#/definitions/nav.Request'
produces:
- application/json
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'
security:
- BearerAuth: []
summary: Update a nav link
tags:
- admin
/nav-links/list:
get:
description: Admin only. List all header navigation links including disabled
ones, with translations.
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.NavLink'
type: array
"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'
security:
- BearerAuth: []
summary: List all nav links
tags:
- admin
/notes:
get:
description: List notes ordered by id DESC. page starts at 1; page_size is 1-100,