1
0

feat: 将"关于"从系统托盘原生对话框迁移到前端页面

移除系统托盘右键菜单中的"关于"选项及各平台原生对话框实现,
在前端新增 /about 路由和关于页面展示品牌信息,侧边栏增加关于导航入口
This commit is contained in:
2026-04-24 23:17:22 +08:00
parent b9b487c591
commit 9105a36097
13 changed files with 78 additions and 78 deletions

View File

@@ -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 { func escapeAppleScript(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\") s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\"", "\\\"") s = strings.ReplaceAll(s, "\"", "\\\"")

View File

@@ -65,21 +65,3 @@ func showError(title, message string) {
dialogLogger().Error("无法显示错误对话框") 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)
}
}

View File

@@ -21,10 +21,6 @@ func showError(title, message string) {
messageBox(title, message, MB_ICONERROR) messageBox(title, message, MB_ICONERROR)
} }
func showAbout() {
messageBox(appAboutTitle, aboutMessage(), MB_ICONINFORMATION)
}
func messageBox(title, message string, flags uint) { func messageBox(title, message string, flags uint) {
titlePtr, _ := syscall.UTF16PtrFromString(title) titlePtr, _ := syscall.UTF16PtrFromString(title)
messagePtr, _ := syscall.UTF16PtrFromString(message) messagePtr, _ := syscall.UTF16PtrFromString(message)

View File

@@ -287,8 +287,6 @@ func setupSystray(port int) {
mPort := systray.AddMenuItem(fmt.Sprintf("端口: %d", port), "") mPort := systray.AddMenuItem(fmt.Sprintf("端口: %d", port), "")
mPort.Disable() mPort.Disable()
systray.AddSeparator() systray.AddSeparator()
mAbout := systray.AddMenuItem("关于", "")
systray.AddSeparator()
mQuit := systray.AddMenuItem("退出", "停止服务并退出") mQuit := systray.AddMenuItem("退出", "停止服务并退出")
go func() { go func() {
@@ -298,8 +296,6 @@ func setupSystray(port int) {
if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil { if err := openBrowser(fmt.Sprintf("http://localhost:%d", port)); err != nil {
zapLogger.Warn("打开浏览器失败", zap.Error(err)) zapLogger.Warn("打开浏览器失败", zap.Error(err))
} }
case <-mAbout.ClickedCh:
showAbout()
case <-mQuit.ClickedCh: case <-mQuit.ClickedCh:
doShutdown() doShutdown()
systray.Quit() systray.Quit()

View File

@@ -13,7 +13,3 @@ func TestMessageBoxW_WindowsOnly(t *testing.T) {
func TestShowError_WindowsBranch(t *testing.T) { func TestShowError_WindowsBranch(t *testing.T) {
showError("测试错误", "这是一条测试错误消息") showError("测试错误", "这是一条测试错误消息")
} }
func TestShowAbout_WindowsBranch(t *testing.T) {
showAbout()
}

View File

@@ -3,11 +3,6 @@ package main
const ( const (
appName = "Nex" appName = "Nex"
appTooltip = appName appTooltip = appName
appAboutTitle = "关于 " + appName
appDescription = "AI Gateway - 统一的大模型 API 网关" appDescription = "AI Gateway - 统一的大模型 API 网关"
appWebsite = "https://github.com/nex/gateway" appWebsite = "https://github.com/nex/gateway"
) )
func aboutMessage() string {
return appName + "\n\n" + appDescription + "\n\n" + appWebsite
}

View File

@@ -2,13 +2,6 @@ package main
import "testing" 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) { func TestDesktopMetadata(t *testing.T) {
if appName != "Nex" { if appName != "Nex" {
t.Fatalf("appName = %q, want %q", appName, "Nex") t.Fatalf("appName = %q, want %q", appName, "Nex")
@@ -17,8 +10,4 @@ func TestDesktopMetadata(t *testing.T) {
if appTooltip != appName { if appTooltip != appName {
t.Fatalf("appTooltip = %q, want %q", appTooltip, appName) t.Fatalf("appTooltip = %q, want %q", appTooltip, appName)
} }
if appAboutTitle != "关于 Nex" {
t.Fatalf("appAboutTitle = %q, want %q", appAboutTitle, "关于 Nex")
}
} }

View File

@@ -1,6 +1,13 @@
import { useState } from 'react' import { useState } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router' 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' import { Layout, Menu, Button } from 'tdesign-react'
const { MenuItem } = Menu const { MenuItem } = Menu
@@ -14,6 +21,7 @@ export function AppLayout() {
if (location.pathname === '/providers') return '供应商管理' if (location.pathname === '/providers') return '供应商管理'
if (location.pathname === '/stats') return '用量统计' if (location.pathname === '/stats') return '用量统计'
if (location.pathname === '/settings') return '设置' if (location.pathname === '/settings') return '设置'
if (location.pathname === '/about') return '关于'
return 'AI Gateway' return 'AI Gateway'
} }
@@ -70,6 +78,9 @@ export function AppLayout() {
<MenuItem value='/settings' icon={<SettingIcon />}> <MenuItem value='/settings' icon={<SettingIcon />}>
</MenuItem> </MenuItem>
<MenuItem value='/about' icon={<InfoCircleIcon />}>
</MenuItem>
</Menu> </Menu>
</Layout.Aside> </Layout.Aside>
<Layout style={{ marginLeft: asideWidth }}> <Layout style={{ marginLeft: asideWidth }}>

View File

@@ -0,0 +1,30 @@
import { Card } from 'tdesign-react'
export default function AboutPage() {
return (
<Card bordered={false}>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4rem 0',
}}
>
<h1 style={{ margin: 0, fontSize: '2rem' }}>Nex</h1>
<p style={{ margin: '0.5rem 0 0', color: 'var(--td-text-color-secondary)', fontSize: '1rem' }}>
AI Gateway - API
</p>
<a
href='https://github.com/nex/gateway'
target='_blank'
rel='noopener noreferrer'
style={{ marginTop: '1rem', color: 'var(--td-brand-color)' }}
>
https://github.com/nex/gateway
</a>
</div>
</Card>
)
}

View File

@@ -6,6 +6,7 @@ import { AppLayout } from '@/components/AppLayout'
const ProvidersPage = lazy(() => import('@/pages/Providers')) const ProvidersPage = lazy(() => import('@/pages/Providers'))
const StatsPage = lazy(() => import('@/pages/Stats')) const StatsPage = lazy(() => import('@/pages/Stats'))
const SettingsPage = lazy(() => import('@/pages/Settings')) const SettingsPage = lazy(() => import('@/pages/Settings'))
const AboutPage = lazy(() => import('@/pages/About'))
const NotFound = lazy(() => import('@/pages/NotFound')) const NotFound = lazy(() => import('@/pages/NotFound'))
export function AppRoutes() { export function AppRoutes() {
@@ -17,6 +18,7 @@ export function AppRoutes() {
<Route path='providers' element={<ProvidersPage />} /> <Route path='providers' element={<ProvidersPage />} />
<Route path='stats' element={<StatsPage />} /> <Route path='stats' element={<StatsPage />} />
<Route path='settings' element={<SettingsPage />} /> <Route path='settings' element={<SettingsPage />} />
<Route path='about' element={<AboutPage />} />
<Route path='*' element={<NotFound />} /> <Route path='*' element={<NotFound />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -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 居中展示

View File

@@ -51,7 +51,6 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
- **AND** 菜单包含"打开管理界面"选项 - **AND** 菜单包含"打开管理界面"选项
- **AND** 菜单包含"状态: 运行中"选项(禁用状态) - **AND** 菜单包含"状态: 运行中"选项(禁用状态)
- **AND** 菜单包含"端口: 9826"选项(禁用状态) - **AND** 菜单包含"端口: 9826"选项(禁用状态)
- **AND** 菜单包含"关于"选项
- **AND** 菜单包含"退出"选项 - **AND** 菜单包含"退出"选项
#### Scenario: 打开管理界面 #### Scenario: 打开管理界面
@@ -140,19 +139,9 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
- **AND** 包含 `Contents/Resources/icon.icns` 图标 - **AND** 包含 `Contents/Resources/icon.icns` 图标
- **AND** `Info.plist``LSUIElement``true`(不显示 Dock 图标) - **AND** `Info.plist``LSUIElement``true`(不显示 Dock 图标)
### Requirement: 关于对话框
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll``MessageBoxW` API 实现。
#### Scenario: 显示关于
- **WHEN** 用户点击托盘菜单"关于"
- **THEN** 显示对话框包含应用名称、项目链接
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
### Requirement: Windows 原生对话框 ### Requirement: Windows 原生对话框
系统 SHALL 在 Windows 上使用 `user32.dll``MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。 系统 SHALL 在 Windows 上使用 `user32.dll``MessageBoxW` API 显示错误对话框,替代 `msg *` 命令。
#### Scenario: 错误提示对话框 #### Scenario: 错误提示对话框
@@ -162,18 +151,10 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
- **AND** 对话框包含错误描述文本 - **AND** 对话框包含错误描述文本
- **AND** 对话框显示错误图标MB_ICONERROR - **AND** 对话框显示错误图标MB_ICONERROR
#### Scenario: 关于对话框
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
- **THEN** 使用 `MessageBoxW` 显示模态对话框
- **AND** 对话框标题栏显示"关于 Nex Gateway"
- **AND** 对话框包含应用信息文本
- **AND** 对话框显示信息图标MB_ICONINFORMATION
#### Scenario: 非 Windows 平台不受影响 #### Scenario: 非 Windows 平台不受影响
- **WHEN** 应用运行在 macOS 或 Linux 上 - **WHEN** 应用运行在 macOS 或 Linux 上
- **THEN** 错误和关于对话框仍使用平台原有实现osascript / zenity - **THEN** 错误对话框仍使用平台原有实现osascript / zenity
### Requirement: Linux 对话框降级策略 ### Requirement: Linux 对话框降级策略

View File

@@ -409,7 +409,7 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
- **WHEN** 渲染侧边栏 - **WHEN** 渲染侧边栏
- **THEN** 侧边栏顶部 SHALL 显示应用名称/Logo - **THEN** 侧边栏顶部 SHALL 显示应用名称/Logo
- **THEN** 侧边栏 SHALL 包含导航菜单 - **THEN** 侧边栏 SHALL 包含导航菜单
- **THEN** 导航菜单项 SHALL 包含供应商管理ServerIcon 图标、用量统计ChartLineIcon 图标、设置SettingIcon 图标) - **THEN** 导航菜单项 SHALL 包含供应商管理ServerIcon 图标、用量统计ChartLineIcon 图标、设置SettingIcon 图标)、关于InfoCircleIcon 图标)
#### Scenario: 导航菜单交互 #### Scenario: 导航菜单交互
@@ -418,7 +418,9 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
- **WHEN** 用户点击导航中的"用量统计" - **WHEN** 用户点击导航中的"用量统计"
- **THEN** 前端 SHALL 导航到 \`/stats\` 并高亮当前菜单项 - **THEN** 前端 SHALL 导航到 \`/stats\` 并高亮当前菜单项
- **WHEN** 用户点击导航中的"设置" - **WHEN** 用户点击导航中的"设置"
- **THEN** 前端 SHALL 导航到 \`/settings\` 并高亮当前菜单项 - **THEN** 前端 SHALL 导航到 `/settings` 并高亮当前菜单项
- **WHEN** 用户点击导航中的"关于"
- **THEN** 前端 SHALL 导航到 `/about` 并高亮当前菜单项
### Requirement: 提供导航 ### Requirement: 提供导航
@@ -430,6 +432,8 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
- **THEN** 前端 SHALL 使用 React Router v7 Library 模式BrowserRouter - **THEN** 前端 SHALL 使用 React Router v7 Library 模式BrowserRouter
- **THEN** `/providers` 路径 SHALL 显示供应商管理页面 - **THEN** `/providers` 路径 SHALL 显示供应商管理页面
- **THEN** `/stats` 路径 SHALL 显示用量统计页面 - **THEN** `/stats` 路径 SHALL 显示用量统计页面
- **THEN** `/settings` 路径 SHALL 显示设置页面
- **THEN** `/about` 路径 SHALL 显示关于页面
- **THEN** `/` 路径 SHALL 重定向到 `/providers` - **THEN** `/` 路径 SHALL 重定向到 `/providers`
- **THEN** 不存在的路径 SHALL 显示 404 页面 - **THEN** 不存在的路径 SHALL 显示 404 页面