更新若干功能
This commit is contained in:
319
workbench.go
Normal file
319
workbench.go
Normal file
@@ -0,0 +1,319 @@
|
||||
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)
|
||||
if notify && a.notifier != nil {
|
||||
_ = a.notifier.SendNotification(notifications.NotificationOptions{
|
||||
ID: fmt.Sprintf("cc-%s-%d", kind, m.ID),
|
||||
Title: title,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 到期巡检 ----------
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user