From cd7b73fae6ae7e7ec47294820cb0bc60ad99b423 Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Wed, 11 Feb 2026 22:48:57 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=98=E7=9B=98?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E8=B7=AF=E7=94=B1=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/desktop/launcher.go | 69 ++++++++++++++++++++++++++++++++ internal/desktop/tray_windows.go | 32 ++++++++++++--- launcher/script.js | 43 +++++++++++++++++++- main.go | 8 +--- 4 files changed, 140 insertions(+), 12 deletions(-) diff --git a/internal/desktop/launcher.go b/internal/desktop/launcher.go index 007633bd..73cea37e 100644 --- a/internal/desktop/launcher.go +++ b/internal/desktop/launcher.go @@ -6,8 +6,11 @@ import ( "fmt" "log" "net/http" + "net/url" "os" "path/filepath" + "strconv" + "strings" "sync" "time" @@ -374,6 +377,72 @@ func (a *LauncherApp) ShowWindow() { } } +// OpenHome 打开应用首页(供菜单/托盘调用) +func (a *LauncherApp) OpenHome() { + a.OpenRoute("/") +} + +// OpenSettings 打开应用设置页(供菜单/托盘调用) +func (a *LauncherApp) OpenSettings() { + a.OpenRoute("/settings") +} + +// OpenRoute 打开应用内路由 +// - 服务已就绪:直接跳转到 http://localhost:/ +// - 服务未就绪:跳转到 launcher,并携带 target 参数等待启动完成后自动跳转 +func (a *LauncherApp) OpenRoute(route string) { + if a.ctx == nil { + log.Printf("[Launcher] Skip OpenRoute(%q): context not ready", route) + return + } + + normalizedRoute := normalizeRoutePath(route) + a.ShowWindow() + + status := a.CheckServerStatus() + if status.Ready { + targetURL := joinRouteURL(a.GetServerAddress(), normalizedRoute) + runtime.WindowExecJS(a.ctx, buildLocationScript(targetURL)) + return + } + + launcherURL := buildLauncherURLWithTarget(normalizedRoute) + runtime.WindowExecJS(a.ctx, buildLocationScript(launcherURL)) +} + +func normalizeRoutePath(route string) string { + trimmed := strings.TrimSpace(route) + if trimmed == "" || trimmed == "/" { + return "/" + } + + if !strings.HasPrefix(trimmed, "/") { + return "/" + trimmed + } + + return trimmed +} + +func joinRouteURL(baseURL string, route string) string { + if route == "/" { + return strings.TrimRight(baseURL, "/") + } + + return strings.TrimRight(baseURL, "/") + route +} + +func buildLauncherURLWithTarget(route string) string { + if route == "/" { + return "wails://wails/index.html" + } + + return "wails://wails/index.html?target=" + url.QueryEscape(route) +} + +func buildLocationScript(targetURL string) string { + return "window.location.href = " + strconv.Quote(targetURL) + ";" +} + // HideWindow 隐藏窗口(供托盘调用) func (a *LauncherApp) HideWindow() { if a.ctx != nil { diff --git a/internal/desktop/tray_windows.go b/internal/desktop/tray_windows.go index 815f3f0d..f9c78fc6 100644 --- a/internal/desktop/tray_windows.go +++ b/internal/desktop/tray_windows.go @@ -7,6 +7,7 @@ import ( _ "embed" "fmt" "log" + "time" "github.com/getlantern/systray" "github.com/wailsapp/wails/v2/pkg/runtime" @@ -108,27 +109,48 @@ func (t *TrayManager) handleMenuEvents() { // showWindow 显示窗口 func (t *TrayManager) showWindow() { + if t.app != nil { + t.app.ShowWindow() + return + } + runtime.WindowShow(t.ctx) runtime.WindowUnminimise(t.ctx) } // openSettings 打开设置页面 func (t *TrayManager) openSettings() { + if t.app != nil { + t.app.OpenSettings() + return + } + runtime.WindowShow(t.ctx) runtime.WindowUnminimise(t.ctx) // 通过 JS 导航到设置页面 - runtime.WindowExecJS(t.ctx, `window.location.href = 'wails://wails/index.html?page=settings';`) + runtime.WindowExecJS(t.ctx, `window.location.href = 'wails://wails/index.html?target=%2Fsettings';`) } // restartServer 重启服务器 func (t *TrayManager) restartServer() { if t.app != nil { log.Println("[Tray] Restarting server...") - t.app.RestartServer() - // 延迟更新状态 + if err := t.app.RestartServer(); err != nil { + log.Printf("[Tray] Restart server failed: %v", err) + t.menuServerStatus.SetTitle("服务器状态: 重启失败") + return + } + + // 延迟更新状态,避免重启期间显示异常状态 go func() { - // 等待服务器重启 - t.UpdateStatus() + for range 20 { + t.UpdateStatus() + status := t.app.CheckServerStatus() + if status.Ready || status.Error != "" { + return + } + time.Sleep(500 * time.Millisecond) + } }() } } diff --git a/launcher/script.js b/launcher/script.js index d51b20f2..470febad 100644 --- a/launcher/script.js +++ b/launcher/script.js @@ -42,6 +42,40 @@ let checkTimer = null; let startTime = Date.now(); + function normalizeTargetPath(path) { + if (!path || path === '/') { + return '/'; + } + return path.startsWith('/') ? path : `/${path}`; + } + + function getTargetPathFromUrl() { + const params = new URLSearchParams(window.location.search); + const queryTarget = params.get('target'); + if (queryTarget) { + return normalizeTargetPath(queryTarget); + } + + if (window.location.hash && window.location.hash.startsWith('#target=')) { + const hashTarget = decodeURIComponent(window.location.hash.slice('#target='.length)); + return normalizeTargetPath(hashTarget); + } + + return '/'; + } + + function clearTargetPathInUrl() { + const params = new URLSearchParams(window.location.search); + if (!params.has('target')) { + return; + } + + params.delete('target'); + const query = params.toString(); + const next = query ? `?${query}` : window.location.pathname; + history.replaceState(null, '', next); + } + // ==================== Page Navigation ==================== function showPage(name) { @@ -137,7 +171,14 @@ if (status.Ready && status.RedirectURL) { clearInterval(checkTimer); - redirectTo(status.RedirectURL); + const targetPath = getTargetPathFromUrl(); + if (targetPath && targetPath !== '/') { + clearTargetPathInUrl(); + } + const targetURL = targetPath === '/' + ? status.RedirectURL + : `${status.RedirectURL}${targetPath}`; + redirectTo(targetURL); return; } diff --git a/main.go b/main.go index b25c0dce..cacba09a 100644 --- a/main.go +++ b/main.go @@ -62,14 +62,10 @@ func main() { // File Menu fileMenu := appMenu.AddSubmenu("File") fileMenu.AddText("Home", keys.CmdOrCtrl("h"), func(_ *menu.CallbackData) { - if appCtx != nil { - runtime.WindowExecJS(appCtx, `window.location.href = 'wails://wails/index.html';`) - } + app.OpenHome() }) fileMenu.AddText("Settings", keys.CmdOrCtrl(","), func(_ *menu.CallbackData) { - if appCtx != nil { - runtime.WindowExecJS(appCtx, `window.location.href = 'wails://wails/index.html?page=settings';`) - } + app.OpenSettings() }) fileMenu.AddSeparator() fileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) { From 4d3424275e0b62b3ca8f5cf2e6077b330fd01fb4 Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Wed, 11 Feb 2026 23:09:17 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E6=B8=85=E7=90=86URL=E4=B8=AD?= =?UTF-8?q?=E7=9A=84target=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- launcher/script.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/launcher/script.js b/launcher/script.js index 470febad..886a44a8 100644 --- a/launcher/script.js +++ b/launcher/script.js @@ -65,14 +65,37 @@ } function clearTargetPathInUrl() { - const params = new URLSearchParams(window.location.search); - if (!params.has('target')) { + const queryParams = new URLSearchParams(window.location.search); + const hasQueryTarget = queryParams.has('target'); + if (hasQueryTarget) { + queryParams.delete('target'); + } + + const rawHash = window.location.hash.startsWith('#') + ? window.location.hash.slice(1) + : window.location.hash; + + let cleanHash = ''; + let hasHashTarget = false; + if (rawHash) { + const hashParams = new URLSearchParams(rawHash); + hasHashTarget = hashParams.has('target'); + + if (hasHashTarget) { + hashParams.delete('target'); + const rebuiltHash = hashParams.toString(); + cleanHash = rebuiltHash ? `#${rebuiltHash}` : ''; + } else { + cleanHash = `#${rawHash}`; + } + } + + if (!hasQueryTarget && !hasHashTarget) { return; } - params.delete('target'); - const query = params.toString(); - const next = query ? `?${query}` : window.location.pathname; + const query = queryParams.toString(); + const next = `${window.location.pathname}${query ? `?${query}` : ''}${cleanHash}`; history.replaceState(null, '', next); } From 547bf9ec026dbb2e6b8eba319ef7a9334cbf5536 Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Thu, 12 Feb 2026 00:47:37 +0800 Subject: [PATCH 3/5] =?UTF-8?q?style:=20=E4=BC=98=E5=8C=96=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=BA=95=E9=83=A8=E5=BC=8F=E6=A0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/app-sidebar/nav-user.tsx | 288 ++++++++++-------- 1 file changed, 158 insertions(+), 130 deletions(-) diff --git a/web/src/components/layout/app-sidebar/nav-user.tsx b/web/src/components/layout/app-sidebar/nav-user.tsx index 8c71f271..58a09b6e 100644 --- a/web/src/components/layout/app-sidebar/nav-user.tsx +++ b/web/src/components/layout/app-sidebar/nav-user.tsx @@ -1,14 +1,14 @@ 'use client'; -import { Moon, Sun, Laptop, Languages, Sparkles, Gem } from 'lucide-react'; +import { Moon, Sun, Laptop, Languages, Sparkles, Gem, Github, ChevronsUp } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useTheme } from '@/components/theme-provider'; import type { Theme } from '@/lib/theme'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuGroup, @@ -22,15 +22,24 @@ import { } from '@/components/ui/dropdown-menu'; import { SidebarMenu, - SidebarMenuButton, SidebarMenuItem, useSidebar, } from '@/components/ui/sidebar'; export function NavUser() { - const { isMobile } = useSidebar(); + const { isMobile, state } = useSidebar(); const { t, i18n } = useTranslation(); const { theme, setTheme } = useTheme(); + const isCollapsed = !isMobile && state === 'collapsed'; + const currentLanguage = (i18n.resolvedLanguage || i18n.language || 'en').toLowerCase().startsWith('zh') + ? 'zh' + : 'en'; + const currentLanguageLabel = + currentLanguage === 'zh' ? t('settings.languages.zh') : t('settings.languages.en'); + + const handleToggleLanguage = () => { + i18n.changeLanguage(currentLanguage === 'zh' ? 'en' : 'zh'); + }; const user = { name: 'Maxx', @@ -40,135 +49,154 @@ export function NavUser() { return ( - - ( - - - - - {user.name.substring(0, 2).toUpperCase()} - - - +
+ + + + + + + + ( + + )} + /> + + + +
+ + + + {user.name.substring(0, 2).toUpperCase()} + + +
+ {user.name} +
+
+
+ +
+ + + + {theme === 'light' ? ( + + ) : theme === 'dark' ? ( + + ) : theme === 'hermes' || theme === 'tiffany' ? ( + + ) : ( + + )} + {t('nav.theme')} + + + + setTheme(v as Theme)}> + + {t('settings.themeDefault')} + + + + {t('settings.theme.light')} + + + + {t('settings.theme.dark')} + + + + {t('settings.theme.system')} + + + + {t('settings.themeLuxury')} + + + + {t('settings.theme.hermes')} + + + + {t('settings.theme.tiffany')} + + + + + + +
+
+
); From fd55ded331761804dbbc97cfa56f65dae32f0808 Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Sun, 22 Feb 2026 14:09:04 +0800 Subject: [PATCH 4/5] fix: resolve Recharts ResponsiveContainer width/height -1 error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use fixed pixel height instead of percentage for ResponsiveContainer - Add minHeight guard to ChartContainer's ResponsiveContainer - Upgrade bytedance/sonic v1.14.2 → v1.15.0 for Go 1.26.0 compat - Add keepPreviousData to usage stats query Co-Authored-By: Claude Opus 4.6 --- go.mod | 4 ++-- go.sum | 8 ++++---- web/src/components/ui/chart.tsx | 4 +++- web/src/hooks/queries/use-usage-stats.ts | 3 ++- web/src/pages/stats/index.tsx | 4 ++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 6940b280..2801d6cf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( github.com/andybalholm/brotli v1.2.0 - github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic v1.15.0 github.com/getlantern/systray v1.2.2 github.com/glebarez/sqlite v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.0 @@ -26,7 +26,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index fc0da3b6..41c65c49 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/web/src/components/ui/chart.tsx b/web/src/components/ui/chart.tsx index 8d4c4b28..e8c21046 100644 --- a/web/src/components/ui/chart.tsx +++ b/web/src/components/ui/chart.tsx @@ -63,7 +63,9 @@ function ChartContainer({ {...props} > - {children} + + {children} + ); diff --git a/web/src/hooks/queries/use-usage-stats.ts b/web/src/hooks/queries/use-usage-stats.ts index 60b24db1..97c2f4db 100644 --- a/web/src/hooks/queries/use-usage-stats.ts +++ b/web/src/hooks/queries/use-usage-stats.ts @@ -3,7 +3,7 @@ * 支持多层级时间粒度聚合 */ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { keepPreviousData, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getTransport, type UsageStatsFilter, type StatsGranularity } from '@/lib/transport'; // Query Keys @@ -117,6 +117,7 @@ export function useUsageStats(filter?: UsageStatsFilter) { return useQuery({ queryKey: usageStatsKeys.list(filter), queryFn: () => getTransport().getUsageStats(filter), + placeholderData: keepPreviousData, }); } diff --git a/web/src/pages/stats/index.tsx b/web/src/pages/stats/index.tsx index fae74c8c..8492d8b6 100644 --- a/web/src/pages/stats/index.tsx +++ b/web/src/pages/stats/index.tsx @@ -898,8 +898,8 @@ export function StatsPage() { -
- +
+ Date: Sun, 22 Feb 2026 14:40:13 +0800 Subject: [PATCH 5/5] fix(web): sort projects by createdAt to ensure consistent order Closes #204 Co-Authored-By: Claude Opus 4.6 --- web/src/pages/projects/index.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/web/src/pages/projects/index.tsx b/web/src/pages/projects/index.tsx index 8b968d78..b9d6f33b 100644 --- a/web/src/pages/projects/index.tsx +++ b/web/src/pages/projects/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -41,6 +41,24 @@ export function ProjectsPage() { navigate(`/projects/${id}`); }; + const sortedProjects = useMemo(() => { + if (!projects) { + return undefined; + } + return projects.slice().sort((a, b) => { + const timeA = Number.isFinite(new Date(a.createdAt).getTime()) + ? new Date(a.createdAt).getTime() + : 0; + const timeB = Number.isFinite(new Date(b.createdAt).getTime()) + ? new Date(b.createdAt).getTime() + : 0; + if (timeA !== timeB) { + return timeA - timeB; + } + return a.id - b.id; + }); + }, [projects]); + return (
- ) : projects && projects.length > 0 ? ( + ) : sortedProjects && sortedProjects.length > 0 ? (
- {projects.map((project) => ( + {sortedProjects.map((project) => (