Files
code-utils/workbench.go
2026-08-14 17:54:06 +08:00

331 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"strconv"
"time"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
// ---------- Todo 绑定 ----------
func (a *App) ListTodos(status string, projectID int64) ([]Todo, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListTodos(status, projectID)
}
func (a *App) SaveTodo(t Todo) (Todo, error) {
if e := a.ready(); e != nil {
return t, e
}
return a.store.SaveTodo(t)
}
func (a *App) SetTodoStatus(id int64, status string) error {
if e := a.ready(); e != nil {
return e
}
return a.store.SetTodoStatus(id, status)
}
func (a *App) DeleteTodo(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteTodo(id)
}
// ---------- 工单绑定 ----------
func (a *App) ListTickets(status string, projectID int64) ([]Ticket, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListTickets(status, projectID)
}
func (a *App) SaveTicket(t Ticket) (Ticket, error) {
if e := a.ready(); e != nil {
return t, e
}
return a.store.SaveTicket(t)
}
func (a *App) SetTicketStatus(id int64, status string) error {
if e := a.ready(); e != nil {
return e
}
return a.store.SetTicketStatus(id, status)
}
func (a *App) DeleteTicket(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteTicket(id)
}
// ---------- 记事本与收藏 ----------
func (a *App) GetNote() (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.GetNote()
}
func (a *App) SaveNote(content string) (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.SaveNote(content)
}
func (a *App) ListNotes(limit int) ([]Note, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListNotes(limit)
}
// SaveNoteByID 保存指定笔记id=0 新建。
func (a *App) SaveNoteByID(id int64, content string) (Note, error) {
if e := a.ready(); e != nil {
return Note{}, e
}
return a.store.SaveNoteByID(id, content)
}
func (a *App) DeleteNote(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.DeleteNote(id)
}
func (a *App) ListFavorites() ([]int64, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListFavorites()
}
func (a *App) ToggleFavorite(projectID int64) (bool, error) {
if e := a.ready(); e != nil {
return false, e
}
return a.store.ToggleFavorite(projectID)
}
// ---------- 消息中心 ----------
func (a *App) ListMessages(limit int64) ([]Message, error) {
if e := a.ready(); e != nil {
return nil, e
}
return a.store.ListMessages(limit)
}
func (a *App) UnreadMessageCount() (int64, error) {
if e := a.ready(); e != nil {
return 0, e
}
return a.store.UnreadMessageCount(), nil
}
func (a *App) MarkMessageRead(id int64) error {
if e := a.ready(); e != nil {
return e
}
return a.store.MarkMessageRead(id)
}
func (a *App) MarkAllMessagesRead() error {
if e := a.ready(); e != nil {
return e
}
return a.store.MarkAllMessagesRead()
}
func (a *App) ClearMessages() error {
if e := a.ready(); e != nil {
return e
}
return a.store.ClearMessages()
}
// pushMessage 写入消息中心、广播给前端,并可选发送系统通知。
func (a *App) pushMessage(kind, title, body, sourceType string, sourceID int64, notify bool) {
if a.store == nil {
return
}
m, e := a.store.AddMessage(kind, title, body, sourceType, sourceID)
if e != nil {
return
}
a.emit("message:new", m)
a.RefreshTrayStatus()
if notify && a.notifier != nil {
path := "/messages"
switch sourceType {
case "todo":
path = "/todos"
case "ticket":
path = "/tickets"
case "team":
path = "/team/tasks"
}
_ = a.notifier.SendNotification(notifications.NotificationOptions{
ID: fmt.Sprintf("cc-%s-%d", kind, m.ID),
Title: title,
Body: body,
Data: map[string]interface{}{"path": path, "kind": kind, "sourceType": sourceType, "sourceId": sourceID},
})
}
}
// ---------- 到期巡检 ----------
// parseDue 解析待办/工单的排期时间(本地时区),支持日期或日期+时间。
func parseDue(v string) (time.Time, bool) {
for _, layout := range []string{"2006-01-02T15:04", "2006-01-02 15:04", "2006-01-02"} {
if t, e := time.ParseInLocation(layout, v, time.Local); e == nil {
// 纯日期视为当天 23:59 截止
if layout == "2006-01-02" {
t = t.Add(23*time.Hour + 59*time.Minute)
}
return t, true
}
}
return time.Time{}, false
}
// runReminderLoop 每分钟巡检到期 todo/工单24 小时内到期与已逾期各提醒一次。
func (a *App) runReminderLoop() {
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-a.ctx.Done():
return
case <-t.C:
a.checkDueReminders()
}
}
}
const (
remindedSoon = 1
remindedOverdue = 2
)
// reminderTexts 返回按当前语言本地化的提醒文案:[todo 逾期, todo 将到期, 工单逾期, 工单将到期, "截止"前缀]。
func (a *App) reminderTexts() [5]string {
locale := "zh-CN"
if st, e := a.store.Settings(); e == nil {
locale = st.Locale
}
if locale == "en" {
return [5]string{"Todo overdue", "Todo due soon", "Ticket overdue", "Ticket due soon", "due"}
}
return [5]string{"待办已逾期", "待办即将到期", "工单已逾期", "工单即将到期", "截止"}
}
func (a *App) checkDueReminders() {
if a.store == nil || a.bootstrap.State != BootstrapReady {
return
}
txt := a.reminderTexts()
now := time.Now()
type row struct {
id int64
title string
due string
reminded int64
}
scan := func(query string) []row {
rows, e := a.store.db.Query(query)
if e != nil {
return nil
}
defer rows.Close()
out := []row{}
for rows.Next() {
var x row
if rows.Scan(&x.id, &x.title, &x.due, &x.reminded) == nil {
out = append(out, x)
}
}
return out
}
for _, x := range scan(`SELECT id,title,due_at,reminded FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`) {
due, ok := parseDue(x.due)
if !ok {
continue
}
if now.After(due) && x.reminded&remindedOverdue == 0 {
a.pushMessage("todo_due", txt[0], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true)
_, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id)
} else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 {
a.pushMessage("todo_due", txt[1], x.title+" ("+txt[4]+" "+x.due+")", "todo", x.id, true)
_, _ = a.store.db.Exec(`UPDATE todos SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id)
}
}
for _, x := range scan(`SELECT id,title,due_at,reminded FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`) {
due, ok := parseDue(x.due)
if !ok {
continue
}
if now.After(due) && x.reminded&remindedOverdue == 0 {
a.pushMessage("ticket_due", txt[2], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true)
_, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedOverdue, x.id)
} else if !now.After(due) && due.Sub(now) <= 24*time.Hour && x.reminded&remindedSoon == 0 {
a.pushMessage("ticket_due", txt[3], x.title+" ("+txt[4]+" "+x.due+")", "ticket", x.id, true)
_, _ = a.store.db.Exec(`UPDATE tickets SET reminded=reminded|? WHERE id=?`, remindedSoon, x.id)
}
}
}
// ---------- 导航徽标 ----------
// NavBadges 左侧导航徽标数据(一次查询打包返回)。
type NavBadges struct {
Unread int `json:"unread"` // 消息中心:未读
TodosOpen int `json:"todosOpen"` // 待办事项:未完成
TicketsActive int `json:"ticketsActive"` // 需求工单:开放 + 进行中
TodayDue int `json:"todayDue"` // 今日任务:逾期 + 今日截止(未完成)
CalendarDot bool `json:"calendarDot"` // 排期日历:今天有到期事项
TeamAssigned int `json:"teamAssigned"` // 团队任务:指派给我未完成(同步时缓存)
}
func (a *App) GetNavBadges() (NavBadges, error) {
if e := a.ready(); e != nil {
return NavBadges{}, e
}
var b NavBadges
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM messages WHERE is_read=0`).Scan(&b.Unread)
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM todos WHERE deleted=0 AND status IN ('open','doing')`).Scan(&b.TodosOpen)
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM tickets WHERE deleted=0 AND status IN ('open','in_progress')`).Scan(&b.TicketsActive)
b.TodayDue = a.countDueToday()
b.CalendarDot = b.TodayDue > 0
if a.syncUserID() > 0 {
b.TeamAssigned, _ = strconv.Atoi(a.store.Meta("team_assigned_cache"))
}
return b, nil
}
// countDueToday 统计逾期与今日截止的未完成待办/工单。
func (a *App) countDueToday() int {
now := time.Now()
end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
n := 0
for _, q := range []string{
`SELECT due_at FROM todos WHERE deleted=0 AND status!='done' AND due_at!=''`,
`SELECT due_at FROM tickets WHERE deleted=0 AND status IN ('open','in_progress') AND due_at!=''`,
} {
rows, e := a.store.db.Query(q)
if e != nil {
continue
}
for rows.Next() {
var v string
if rows.Scan(&v) != nil {
continue
}
if due, ok := parseDue(v); ok && !due.After(end) {
n++
}
}
rows.Close()
}
return n
}