From 4449250e97bdfd64b9349b2baab95d922a23f9ef Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 21 Sep 2026 21:26:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=A4=B4=E9=83=A8=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E9=93=BE=E6=8E=A5=E9=85=8D=E7=BD=AE=E4=B8=8E=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移 v10 新增 nav_links 与 nav_link_translations 两张表,文案按语言存储 - 公开 GET /api/nav-links 返回启用链接(按 sort/id 排序);管理员可增删改查与启停 - URL 仅允许站内路径、http(s) 与 mailto,拒绝 javascript: 等危险协议;译文替换与校验 - 前端删除写死的“主页”按钮,头部按当前语言渲染动态链接,新窗口加 rel=noopener noreferrer - 后台管理页新增“头部导航链接”卡片(三语文案、URL、打开方式、排序、状态),改动即时生效 - 补充 nav 接口与迁移测试、三语文案,重新生成 Swagger 文档 --- docs/backend-development.md | 1 + docs/docs.go | 375 +++++++++++++++++ docs/swagger.json | 375 +++++++++++++++++ docs/swagger.yaml | 252 ++++++++++++ frontend/src/api/nav.ts | 49 +++ .../src/components/admin/NavLinksCard.vue | 380 ++++++++++++++++++ frontend/src/components/layout/AppHeader.vue | 41 +- frontend/src/i18n/locales/en-US.ts | 36 +- frontend/src/i18n/locales/ja-JP.ts | 36 +- frontend/src/i18n/locales/zh-CN.ts | 36 +- frontend/src/main.ts | 4 + frontend/src/stores/nav.ts | 19 + frontend/src/views/AdminSiteView.vue | 3 + internal/api/api.go | 9 +- internal/database/database_test.go | 6 + internal/database/migrate.go | 7 + internal/model/nav_link.go | 31 ++ internal/nav/nav.go | 348 ++++++++++++++++ internal/nav/nav_test.go | 203 ++++++++++ 19 files changed, 2198 insertions(+), 13 deletions(-) create mode 100644 frontend/src/api/nav.ts create mode 100644 frontend/src/components/admin/NavLinksCard.vue create mode 100644 frontend/src/stores/nav.ts create mode 100644 internal/model/nav_link.go create mode 100644 internal/nav/nav.go create mode 100644 internal/nav/nav_test.go diff --git a/docs/backend-development.md b/docs/backend-development.md index 3a03fc3..94d082d 100644 --- a/docs/backend-development.md +++ b/docs/backend-development.md @@ -14,6 +14,7 @@ internal/ ├── usergroup/ 用户组管理 ├── note/ 便签 ├── site/ 站点信息 +├── nav/ 头部导航链接 ├── file/ 文件上传、删除、查看与本地存储 ├── avatar/ 当前用户头像 ├── httpx/ HTTP 公共能力:ErrorResponse、分页、ID 解析 diff --git a/docs/docs.go b/docs/docs.go index e725527..2e90a2c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index 9939e14..ba2265c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1c3fe20..1f2263c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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, diff --git a/frontend/src/api/nav.ts b/frontend/src/api/nav.ts new file mode 100644 index 0000000..2d166f0 --- /dev/null +++ b/frontend/src/api/nav.ts @@ -0,0 +1,49 @@ +import { request } from './http' + +export interface NavTranslation { + locale: string + label: string +} + +export interface NavLink { + id: number + url: string + open_in_new_window: boolean + sort: number + status: number + translations: NavTranslation[] +} + +export interface NavLinkPayload { + url: string + open_in_new_window: boolean + sort: number + status: number + translations: NavTranslation[] +} + +export function getNavLinks(): Promise { + return request('/nav-links') +} + +export function getAllNavLinks(): Promise { + return request('/nav-links/list') +} + +export function createNavLink(payload: NavLinkPayload): Promise { + return request('/nav-links', { + method: 'POST', + body: JSON.stringify(payload), + }) +} + +export function updateNavLink(id: number, payload: NavLinkPayload): Promise { + return request(`/nav-links/${id}`, { + method: 'PUT', + body: JSON.stringify(payload), + }) +} + +export function deleteNavLink(id: number): Promise { + return request(`/nav-links/${id}`, { method: 'DELETE' }) +} diff --git a/frontend/src/components/admin/NavLinksCard.vue b/frontend/src/components/admin/NavLinksCard.vue new file mode 100644 index 0000000..38c1209 --- /dev/null +++ b/frontend/src/components/admin/NavLinksCard.vue @@ -0,0 +1,380 @@ + + + diff --git a/frontend/src/components/layout/AppHeader.vue b/frontend/src/components/layout/AppHeader.vue index 3e34583..8e85866 100644 --- a/frontend/src/components/layout/AppHeader.vue +++ b/frontend/src/components/layout/AppHeader.vue @@ -2,19 +2,35 @@ import { onMounted, onUnmounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { RouterLink, useRoute, useRouter } from 'vue-router' +import type { NavLink } from '@/api/nav' import { useAuthStore } from '@/stores/auth' +import { useNavStore } from '@/stores/nav' import { useSiteStore } from '@/stores/site' -const { t } = useI18n() +const { t, locale } = useI18n() const route = useRoute() const router = useRouter() const auth = useAuthStore() +const nav = useNavStore() const site = useSiteStore() const keyword = ref('') const menuOpen = ref(false) const menuRef = ref(null) +function isInternal(url: string): boolean { + return url.startsWith('/') +} + +function navLabel(link: NavLink): string { + const current = link.translations.find((item) => item.locale === locale.value)?.label + if (current) { + return current + } + const fallback = link.translations.find((item) => item.locale === 'zh-CN')?.label + return fallback ?? link.translations[0]?.label ?? '' +} + function onDocumentClick(event: MouseEvent) { if (menuRef.value && !menuRef.value.contains(event.target as Node)) { menuOpen.value = false @@ -47,8 +63,27 @@ function logout() { {{ site.name }} -