更新若干功能

This commit is contained in:
李琦
2026-08-14 17:54:06 +08:00
parent 89bc265f68
commit ce00c9d193
18 changed files with 779 additions and 158 deletions

143
shell.go
View File

@@ -4,12 +4,14 @@ import (
_ "embed"
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"view/service"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
//go:embed build/appicon.png
@@ -26,6 +28,9 @@ type shellText struct {
account, accountLogin, traySync, profile, logout string
trayProjects, trayAllProjects string
trayTodos, trayTickets, trayMessages, trayAutostart string
trayToday, trayQuick, trayStatusSyncing string
trayStatusOnline, trayStatusOffline, trayStatusSignedOut string
trayStatusError, trayPending, trayUnread, trayDueToday string
}
func shellTexts(locale string) shellText {
@@ -38,6 +43,10 @@ func shellTexts(locale string) shellText {
account: "Account", accountLogin: "Sign In / Register", traySync: "Sync Now", profile: "Profile", logout: "Sign Out",
trayProjects: "Open Project", trayAllProjects: "All Projects…",
trayTodos: "Todos", trayTickets: "Tickets", trayMessages: "Messages", trayAutostart: "Start at Login",
trayToday: "Today", trayQuick: "Quick Open",
trayStatusSyncing: "Syncing…", trayStatusOnline: "Online", trayStatusOffline: "Offline",
trayStatusSignedOut: "Signed out", trayStatusError: "Sync error",
trayPending: "Pending %d", trayUnread: "Unread %d", trayDueToday: "Due today %d",
}
}
return shellText{
@@ -48,6 +57,10 @@ func shellTexts(locale string) shellText {
account: "账号", accountLogin: "登录 / 注册", traySync: "立即同步", profile: "个人中心", logout: "退出登录",
trayProjects: "打开项目", trayAllProjects: "全部项目…",
trayTodos: "待办事项", trayTickets: "需求工单", trayMessages: "消息中心", trayAutostart: "开机启动",
trayToday: "今日任务", trayQuick: "快捷入口",
trayStatusSyncing: "同步中…", trayStatusOnline: "在线", trayStatusOffline: "离线",
trayStatusSignedOut: "未登录", trayStatusError: "同步异常",
trayPending: "待推送 %d", trayUnread: "未读消息 %d", trayDueToday: "今日到期 %d",
}
}
@@ -73,9 +86,53 @@ func (a *App) SetupShell(wapp *application.App, win *application.WebviewWindow)
})
a.shell.tray = wapp.SystemTray.New()
a.shell.tray.SetIcon(appIcon)
a.shell.tray.OnClick(a.showMainWindow)
a.shell.tray.OnDoubleClick(a.showMainWindow)
a.wireNotificationClicks()
a.RefreshShell()
}
// wireNotificationClicks 点击系统通知时唤起窗口并跳到对应页面。
func (a *App) wireNotificationClicks() {
if a.notifier == nil {
return
}
a.notifier.OnNotificationResponse(func(result notifications.NotificationResult) {
if result.Error != nil {
return
}
application.InvokeAsync(func() {
a.showMainWindow()
path := notificationNavPath(result.Response)
if path != "" {
a.emit("menu:navigate", path)
}
})
})
}
func notificationNavPath(r notifications.NotificationResponse) string {
if r.UserInfo != nil {
if p, ok := r.UserInfo["path"].(string); ok && p != "" {
return p
}
}
switch {
case strings.HasPrefix(r.ID, "cc-todo_due-"):
return "/todos"
case strings.HasPrefix(r.ID, "cc-ticket_due-"):
return "/tickets"
case strings.HasPrefix(r.ID, "cc-team-"):
return "/team/tasks"
case strings.HasPrefix(r.ID, "cc-sync-"):
return "/messages"
case strings.HasPrefix(r.ID, "cc-analysis-"):
return "/logs"
default:
return "/messages"
}
}
// RefreshShell 按当前语言与登录态重建原生菜单与托盘菜单(设置/登录变更后调用)。
func (a *App) RefreshShell() {
if a.shell == nil {
@@ -89,14 +146,36 @@ func (a *App) RefreshShell() {
}
t := shellTexts(locale)
menu := a.buildAppMenu(t, theme, locale)
// macOS 用全局应用菜单Windows 已走无边框自定义顶栏,不再 SetMenu。
a.shell.wapp.Menu.SetApplicationMenu(menu)
// Windows/Linux 的菜单栏挂在窗口上macOS 用全局应用菜单SetMenu 是 no-op
if a.shell.win != nil {
a.shell.win.SetMenu(menu)
a.refreshTray(t, locale)
}
// refreshTray 更新托盘 tooltip / 状态色点图标 / 右键菜单。
func (a *App) refreshTray(t shellText, locale string) {
if a.shell == nil || a.shell.tray == nil {
return
}
a.shell.tray.SetTooltip("年糕崽崽项目管理PMS")
a.shell.tray.SetMenu(a.buildTrayMenu(t))
st := a.collectTrayStatus()
a.shell.tray.SetTooltip(st.tooltip(locale))
a.shell.tray.SetIcon(trayIconWithBadge(appIcon, st.badgeColor()))
a.shell.tray.SetMenu(a.buildTrayMenu(t, st))
a.shell.tray.OnClick(a.showMainWindow)
a.shell.tray.OnDoubleClick(a.showMainWindow)
}
// RefreshTrayStatus 轻量刷新托盘状态(同步/消息变更时调用,不重建应用菜单)。
func (a *App) RefreshTrayStatus() {
if a.shell == nil {
return
}
locale := "zh-CN"
if a.store != nil {
if st, e := a.store.Settings(); e == nil {
locale = st.Locale
}
}
a.refreshTray(shellTexts(locale), locale)
}
func (a *App) buildAppMenu(t shellText, theme, locale string) *application.Menu {
@@ -190,12 +269,24 @@ func (a *App) trayQuickSync() {
}
// trayProjectLimit 是托盘「打开项目」子菜单最多列出的项目数,超出走「全部项目…」。
const trayProjectLimit = 12
const trayProjectLimit = 8
func (a *App) buildTrayMenu(t shellText) *application.Menu {
func (a *App) buildTrayMenu(t shellText, st trayStatus) *application.Menu {
menu := a.shell.wapp.Menu.New()
menu.Add(t.trayShow).OnClick(func(*application.Context) { a.showMainWindow() })
menu.AddSeparator()
// 状态区:点击状态行触发同步(或登录)。
statusLabel := trayStatusLabel(t, st)
menu.Add(statusLabel).OnClick(func(*application.Context) { a.trayQuickSync() })
if st.unread > 0 {
menu.Add(fmt.Sprintf(t.trayUnread, st.unread)).OnClick(func(*application.Context) { a.navigateTo("/messages") })
}
if st.todayDue > 0 {
menu.Add(fmt.Sprintf(t.trayDueToday, st.todayDue)).OnClick(func(*application.Context) { a.navigateTo("/today") })
}
menu.AddSeparator()
if projects, e := a.ListProjects(); e == nil && len(projects) > 0 {
sub := menu.AddSubmenu(t.trayProjects)
for i, p := range projects {
@@ -210,14 +301,19 @@ func (a *App) buildTrayMenu(t shellText) *application.Menu {
sub.Add(t.trayAllProjects).OnClick(func(*application.Context) { a.navigateTo("/projects") })
}
}
menu.Add(t.trayTodos).OnClick(func(*application.Context) { a.navigateTo("/todos") })
menu.Add(t.trayTickets).OnClick(func(*application.Context) { a.navigateTo("/tickets") })
menu.Add(t.trayMessages).OnClick(func(*application.Context) { a.navigateTo("/messages") })
quick := menu.AddSubmenu(t.trayQuick)
quick.Add(t.trayToday).OnClick(func(*application.Context) { a.navigateTo("/today") })
quick.Add(t.trayTodos).OnClick(func(*application.Context) { a.navigateTo("/todos") })
quick.Add(t.trayTickets).OnClick(func(*application.Context) { a.navigateTo("/tickets") })
quick.Add(t.trayMessages).OnClick(func(*application.Context) { a.navigateTo("/messages") })
quick.Add(t.aiHub).OnClick(func(*application.Context) { a.navigateTo("/ai") })
menu.AddSeparator()
menu.Add(t.trayBatch).OnClick(func(*application.Context) { _, _ = a.StartBatchAnalysis() })
menu.Add(t.traySync).OnClick(func(*application.Context) { a.trayQuickSync() })
menu.Add(t.trayBatch).OnClick(func(*application.Context) { _, _ = a.StartBatchAnalysis() })
menu.AddSeparator()
// 勾选态取自系统注册状态;点击写入后重建托盘,让勾选跟随真实结果。
autostart, autoErr := a.GetAutostart()
menu.AddCheckbox(t.trayAutostart, autoErr == nil && autostart).OnClick(func(*application.Context) {
_ = a.SetAutostart(!autostart)
@@ -228,6 +324,29 @@ func (a *App) buildTrayMenu(t shellText) *application.Menu {
return menu
}
func trayStatusLabel(t shellText, st trayStatus) string {
var core string
switch {
case st.syncing:
core = t.trayStatusSyncing
case !st.loggedIn:
core = t.trayStatusSignedOut
case st.lastErr != "":
core = t.trayStatusError
case st.online:
core = t.trayStatusOnline
if st.username != "" {
core = st.username + " · " + core
}
default:
core = t.trayStatusOffline
}
if st.pending > 0 {
core += " · " + fmt.Sprintf(t.trayPending, st.pending)
}
return core
}
// navigateTo 唤起主窗口并让前端路由跳到指定页面(托盘/原生菜单共用)。
func (a *App) navigateTo(path string) {
a.showMainWindow()