feat: 将"关于"从系统托盘原生对话框迁移到前端页面
移除系统托盘右键菜单中的"关于"选项及各平台原生对话框实现, 在前端新增 /about 路由和关于页面展示品牌信息,侧边栏增加关于导航入口
This commit is contained in:
@@ -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, "\"", "\\\"")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
30
frontend/src/pages/About/index.tsx
Normal file
30
frontend/src/pages/About/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
26
openspec/specs/about-page/spec.md
Normal file
26
openspec/specs/about-page/spec.md
Normal 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 居中展示
|
||||||
@@ -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 对话框降级策略
|
||||||
|
|
||||||
|
|||||||
@@ -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 页面
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user