diff --git a/backend/cmd/desktop/dialog_darwin.go b/backend/cmd/desktop/dialog_darwin.go index baa7fc1..8ea2062 100644 --- a/backend/cmd/desktop/dialog_darwin.go +++ b/backend/cmd/desktop/dialog_darwin.go @@ -18,14 +18,6 @@ func showError(title, message string) { } } -func showAbout() { - script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`, - escapeAppleScript(aboutMessage()), escapeAppleScript(appAboutTitle)) - if err := exec.Command("osascript", "-e", script).Run(); err != nil { - dialogLogger().Warn("显示关于对话框失败", zap.Error(err)) - } -} - func escapeAppleScript(s string) string { s = strings.ReplaceAll(s, "\\", "\\\\") s = strings.ReplaceAll(s, "\"", "\\\"") diff --git a/backend/cmd/desktop/dialog_linux.go b/backend/cmd/desktop/dialog_linux.go index ee678d4..d52f946 100644 --- a/backend/cmd/desktop/dialog_linux.go +++ b/backend/cmd/desktop/dialog_linux.go @@ -65,21 +65,3 @@ func showError(title, message string) { dialogLogger().Error("无法显示错误对话框") } } - -func showAbout() { - switch dialogTool { - case toolZenity: - exec.Command("zenity", "--info", - fmt.Sprintf("--title=%s", appAboutTitle), - fmt.Sprintf("--text=%s", aboutMessage())).Run() - case toolKdialog: - exec.Command("kdialog", "--msgbox", aboutMessage(), "--title", appAboutTitle).Run() - case toolNotifySend: - exec.Command("notify-send", appAboutTitle, aboutMessage()).Run() - case toolXmessage: - exec.Command("xmessage", "-center", - fmt.Sprintf("%s: %s", appAboutTitle, aboutMessage())).Run() - default: - dialogLogger().Info(appAboutTitle) - } -} diff --git a/backend/cmd/desktop/dialog_windows.go b/backend/cmd/desktop/dialog_windows.go index f95b915..cfc72cc 100644 --- a/backend/cmd/desktop/dialog_windows.go +++ b/backend/cmd/desktop/dialog_windows.go @@ -21,10 +21,6 @@ func showError(title, message string) { messageBox(title, message, MB_ICONERROR) } -func showAbout() { - messageBox(appAboutTitle, aboutMessage(), MB_ICONINFORMATION) -} - func messageBox(title, message string, flags uint) { titlePtr, _ := syscall.UTF16PtrFromString(title) messagePtr, _ := syscall.UTF16PtrFromString(message) diff --git a/backend/cmd/desktop/main.go b/backend/cmd/desktop/main.go index d2a3545..cd3df37 100644 --- a/backend/cmd/desktop/main.go +++ b/backend/cmd/desktop/main.go @@ -287,8 +287,6 @@ func setupSystray(port int) { mPort := systray.AddMenuItem(fmt.Sprintf("端口: %d", port), "") mPort.Disable() systray.AddSeparator() - mAbout := systray.AddMenuItem("关于", "") - systray.AddSeparator() mQuit := systray.AddMenuItem("退出", "停止服务并退出") go func() { @@ -298,8 +296,6 @@ func setupSystray(port int) { if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil { zapLogger.Warn("打开浏览器失败", zap.Error(err)) } - case <-mAbout.ClickedCh: - showAbout() case <-mQuit.ClickedCh: doShutdown() systray.Quit() diff --git a/backend/cmd/desktop/messagebox_test.go b/backend/cmd/desktop/messagebox_test.go index 94dfc9b..acd2ba5 100644 --- a/backend/cmd/desktop/messagebox_test.go +++ b/backend/cmd/desktop/messagebox_test.go @@ -13,7 +13,3 @@ func TestMessageBoxW_WindowsOnly(t *testing.T) { func TestShowError_WindowsBranch(t *testing.T) { showError("测试错误", "这是一条测试错误消息") } - -func TestShowAbout_WindowsBranch(t *testing.T) { - showAbout() -} diff --git a/backend/cmd/desktop/metadata.go b/backend/cmd/desktop/metadata.go index 100cc66..6f5ac5a 100644 --- a/backend/cmd/desktop/metadata.go +++ b/backend/cmd/desktop/metadata.go @@ -3,11 +3,6 @@ package main const ( appName = "Nex" appTooltip = appName - appAboutTitle = "关于 " + appName appDescription = "AI Gateway - 统一的大模型 API 网关" appWebsite = "https://github.com/nex/gateway" ) - -func aboutMessage() string { - return appName + "\n\n" + appDescription + "\n\n" + appWebsite -} diff --git a/backend/cmd/desktop/metadata_test.go b/backend/cmd/desktop/metadata_test.go index 8827705..87db580 100644 --- a/backend/cmd/desktop/metadata_test.go +++ b/backend/cmd/desktop/metadata_test.go @@ -2,13 +2,6 @@ package main import "testing" -func TestAboutMessage(t *testing.T) { - expected := "Nex\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway" - if got := aboutMessage(); got != expected { - t.Fatalf("aboutMessage() = %q, want %q", got, expected) - } -} - func TestDesktopMetadata(t *testing.T) { if appName != "Nex" { t.Fatalf("appName = %q, want %q", appName, "Nex") @@ -17,8 +10,4 @@ func TestDesktopMetadata(t *testing.T) { if appTooltip != appName { t.Fatalf("appTooltip = %q, want %q", appTooltip, appName) } - - if appAboutTitle != "关于 Nex" { - t.Fatalf("appAboutTitle = %q, want %q", appAboutTitle, "关于 Nex") - } } diff --git a/frontend/src/components/AppLayout/index.tsx b/frontend/src/components/AppLayout/index.tsx index 463ce75..f860d2e 100644 --- a/frontend/src/components/AppLayout/index.tsx +++ b/frontend/src/components/AppLayout/index.tsx @@ -1,6 +1,13 @@ import { useState } from 'react' import { Outlet, useLocation, useNavigate } from 'react-router' -import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react' +import { + ServerIcon, + ChartLineIcon, + SettingIcon, + InfoCircleIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from 'tdesign-icons-react' import { Layout, Menu, Button } from 'tdesign-react' const { MenuItem } = Menu @@ -14,6 +21,7 @@ export function AppLayout() { if (location.pathname === '/providers') return '供应商管理' if (location.pathname === '/stats') return '用量统计' if (location.pathname === '/settings') return '设置' + if (location.pathname === '/about') return '关于' return 'AI Gateway' } @@ -70,6 +78,9 @@ export function AppLayout() { }> 设置 + }> + 关于 + diff --git a/frontend/src/pages/About/index.tsx b/frontend/src/pages/About/index.tsx new file mode 100644 index 0000000..0f85318 --- /dev/null +++ b/frontend/src/pages/About/index.tsx @@ -0,0 +1,30 @@ +import { Card } from 'tdesign-react' + +export default function AboutPage() { + return ( + + + Nex + + AI Gateway - 统一的大模型 API 网关 + + + https://github.com/nex/gateway + + + + ) +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 05dc5b4..5ae32df 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -6,6 +6,7 @@ import { AppLayout } from '@/components/AppLayout' const ProvidersPage = lazy(() => import('@/pages/Providers')) const StatsPage = lazy(() => import('@/pages/Stats')) const SettingsPage = lazy(() => import('@/pages/Settings')) +const AboutPage = lazy(() => import('@/pages/About')) const NotFound = lazy(() => import('@/pages/NotFound')) export function AppRoutes() { @@ -17,6 +18,7 @@ export function AppRoutes() { } /> } /> } /> + } /> } /> diff --git a/openspec/specs/about-page/spec.md b/openspec/specs/about-page/spec.md new file mode 100644 index 0000000..4b87dd0 --- /dev/null +++ b/openspec/specs/about-page/spec.md @@ -0,0 +1,26 @@ +# 关于页面 + +## Purpose + +TBD - 提供关于页面展示项目品牌信息 + +## Requirements + +### Requirement: 关于页面 + +前端 SHALL 提供关于页面,使用 TDesign Card 组件居中展示项目品牌信息(应用名称、描述、项目链接)。 + +#### Scenario: 显示关于页面 + +- **WHEN** 用户访问 `/about` 路径 +- **THEN** 前端 SHALL 显示关于页面 +- **THEN** 页面 SHALL 展示应用名称"Nex" +- **THEN** 页面 SHALL 展示应用描述"AI Gateway - 统一的大模型 API 网关" +- **THEN** 页面 SHALL 展示项目链接"https://github.com/nex/gateway" + +#### Scenario: 页面布局 + +- **WHEN** 渲染关于页面 +- **THEN** 页面 SHALL 使用 TDesign Card 组件作为容器 +- **THEN** Card SHALL 设置 `bordered={false}` +- **THEN** 内容 SHALL 居中展示 diff --git a/openspec/specs/desktop-app/spec.md b/openspec/specs/desktop-app/spec.md index 9ed2594..e235309 100644 --- a/openspec/specs/desktop-app/spec.md +++ b/openspec/specs/desktop-app/spec.md @@ -51,7 +51,6 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源 - **AND** 菜单包含"打开管理界面"选项 - **AND** 菜单包含"状态: 运行中"选项(禁用状态) - **AND** 菜单包含"端口: 9826"选项(禁用状态) -- **AND** 菜单包含"关于"选项 - **AND** 菜单包含"退出"选项 #### Scenario: 打开管理界面 @@ -140,19 +139,9 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源 - **AND** 包含 `Contents/Resources/icon.icns` 图标 - **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标) -### Requirement: 关于对话框 - -系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll` 的 `MessageBoxW` API 实现。 - -#### Scenario: 显示关于 - -- **WHEN** 用户点击托盘菜单"关于" -- **THEN** 显示对话框包含应用名称、项目链接 -- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现 - ### Requirement: Windows 原生对话框 -系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。 +系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误对话框,替代 `msg *` 命令。 #### Scenario: 错误提示对话框 @@ -162,18 +151,10 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源 - **AND** 对话框包含错误描述文本 - **AND** 对话框显示错误图标(MB_ICONERROR) -#### Scenario: 关于对话框 - -- **WHEN** 用户在 Windows 上点击托盘菜单"关于" -- **THEN** 使用 `MessageBoxW` 显示模态对话框 -- **AND** 对话框标题栏显示"关于 Nex Gateway" -- **AND** 对话框包含应用信息文本 -- **AND** 对话框显示信息图标(MB_ICONINFORMATION) - #### Scenario: 非 Windows 平台不受影响 - **WHEN** 应用运行在 macOS 或 Linux 上 -- **THEN** 错误和关于对话框仍使用平台原有实现(osascript / zenity) +- **THEN** 错误对话框仍使用平台原有实现(osascript / zenity) ### Requirement: Linux 对话框降级策略 diff --git a/openspec/specs/frontend/spec.md b/openspec/specs/frontend/spec.md index 342cd5b..f76ea94 100644 --- a/openspec/specs/frontend/spec.md +++ b/openspec/specs/frontend/spec.md @@ -409,7 +409,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **WHEN** 渲染侧边栏 - **THEN** 侧边栏顶部 SHALL 显示应用名称/Logo - **THEN** 侧边栏 SHALL 包含导航菜单 -- **THEN** 导航菜单项 SHALL 包含:供应商管理(ServerIcon 图标)、用量统计(ChartLineIcon 图标)、设置(SettingIcon 图标) +- **THEN** 导航菜单项 SHALL 包含:供应商管理(ServerIcon 图标)、用量统计(ChartLineIcon 图标)、设置(SettingIcon 图标)、关于(InfoCircleIcon 图标) #### Scenario: 导航菜单交互 @@ -418,7 +418,9 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **WHEN** 用户点击导航中的"用量统计" - **THEN** 前端 SHALL 导航到 \`/stats\` 并高亮当前菜单项 - **WHEN** 用户点击导航中的"设置" -- **THEN** 前端 SHALL 导航到 \`/settings\` 并高亮当前菜单项 +- **THEN** 前端 SHALL 导航到 `/settings` 并高亮当前菜单项 +- **WHEN** 用户点击导航中的"关于" +- **THEN** 前端 SHALL 导航到 `/about` 并高亮当前菜单项 ### Requirement: 提供导航 @@ -430,6 +432,8 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** 前端 SHALL 使用 React Router v7 Library 模式(BrowserRouter) - **THEN** `/providers` 路径 SHALL 显示供应商管理页面 - **THEN** `/stats` 路径 SHALL 显示用量统计页面 +- **THEN** `/settings` 路径 SHALL 显示设置页面 +- **THEN** `/about` 路径 SHALL 显示关于页面 - **THEN** `/` 路径 SHALL 重定向到 `/providers` - **THEN** 不存在的路径 SHALL 显示 404 页面
+ AI Gateway - 统一的大模型 API 网关 +