From f00ba9f9cc0ef18f2953a8a2a91a503b1b37a5f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com>
Date: Mon, 20 Jul 2026 11:41:39 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A5=E7=A8=8B=E6=8F=90?=
=?UTF-8?q?=E9=86=92=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.mimocode/.cron-lock | 2 +-
backend/vite.config.js | 8 +
go/controllers/api_reminder.go | 277 ++++--
go/controllers/app_reminder.go | 90 +-
go/controllers/backend_reminder.go | 63 +-
go/controllers/platform_reminder.go | 1267 ++++++++++++------------
go/services/reminder_scheduler.go | 759 +++++++-------
platform/package-lock.json | 470 ++++-----
platform/package.json | 8 +
platform/src/views/dashboard/index.vue | 49 +-
platform/vite.config.js | 6 +-
uniapp/manifest.json | 17 +-
uniapp/pages/dashboard/dashboard.vue | 86 +-
uniapp/pages/features/features.vue | 6 +-
uniapp/pages/profile/profile.vue | 98 +-
uniapp/pages/tools/notepad/edit.vue | 105 +-
uniapp/pages/tools/schedule/edit.vue | 98 +-
uniapp/src/styles/theme.scss | 8 +-
uniapp/vite.config.js | 43 +-
19 files changed, 1735 insertions(+), 1725 deletions(-)
diff --git a/.mimocode/.cron-lock b/.mimocode/.cron-lock
index 4873726..bf7d9c6 100644
--- a/.mimocode/.cron-lock
+++ b/.mimocode/.cron-lock
@@ -1 +1 @@
-{"pid":54204,"startedAt":1784192140605}
\ No newline at end of file
+{"pid":4264,"startedAt":1784510683025}
\ No newline at end of file
diff --git a/backend/vite.config.js b/backend/vite.config.js
index 64a4864..ea0e2a5 100644
--- a/backend/vite.config.js
+++ b/backend/vite.config.js
@@ -22,6 +22,14 @@ export default defineConfig({
},
},
server: {
+ host: '127.0.0.1',
port: 4001,
+ proxy: {
+ '/api': {
+ target: 'http://127.0.0.1:9000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ }
+ }
},
});
diff --git a/go/controllers/api_reminder.go b/go/controllers/api_reminder.go
index 7ac822b..af468ef 100644
--- a/go/controllers/api_reminder.go
+++ b/go/controllers/api_reminder.go
@@ -1,109 +1,168 @@
-package controllers
-
-import (
- "time"
-
- "server/models"
-
- beego "github.com/beego/beego/v2/server/web"
-)
-
-type ApiReminderController struct {
- beego.Controller
-}
-
-// AckReminder GET /api/schedule/reminder/ack
-// 邮件/Bark 客户端访问此接口进行提醒确认
-func (c *ApiReminderController) AckReminder() {
- token := c.GetString("token")
- if token == "" {
- c.Ctx.Output.SetStatus(400)
- _ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
- return
- }
-
- var reminder models.PlatformScheduleReminder
- err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("ack_token", token).
- Filter("is_deleted", 0).
- One(&reminder)
- if err != nil {
- c.Ctx.Output.SetStatus(404)
- _ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
- return
- }
-
- if reminder.AckStatus == 1 {
- // 已经确认过了,直接显示已确认成功的 HTML
- c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
- _ = c.Ctx.Output.Body([]byte(`
-
-
-
-
- 确认收到提醒
-
-
-
-
-
提示
-
该日程提醒在此之前已确认过了。
-
无需重复点击,感谢您的使用!
-
-
-
- `))
- return
- }
-
- // 更新确认状态为已确认,置 remind_status 为已结束(2)
- now := time.Now()
- reminder.AckStatus = 1
- reminder.AckTime = &now
- reminder.RemindStatus = 2
- reminder.UpdateTime = now
-
- _, err = models.Orm.Update(&reminder, "AckStatus", "AckTime", "RemindStatus", "UpdateTime")
- if err != nil {
- c.Ctx.Output.SetStatus(500)
- _ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
- return
- }
-
- // 统一关闭该日程下的所有其他待提醒/提醒中渠道,防止重复打扰
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("ScheduleID", reminder.ScheduleID).
- Filter("RemindStatus__in", 0, 1).
- Update(map[string]interface{}{
- "RemindStatus": int8(2),
- "UpdateTime": now,
- })
-
- // 成功确认
- c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
- _ = c.Ctx.Output.Body([]byte(`
-
-
-
-
- 确认成功
-
-
-
-
-
确认成功
-
您已成功确认收到该日程提醒!
-
系统已停止向您重复推送,感谢您的配合。
-
-
-
- `))
-}
+package controllers
+
+import (
+ "time"
+
+ "server/models"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type ApiReminderController struct {
+ beego.Controller
+}
+
+// AckReminder GET /api/schedule/reminder/ack
+// 邮件/Bark 客户端访问此接口进行提醒确认。
+// backend 与 platform 使用各自的提醒表,这里按 token 自动识别来源。
+func (c *ApiReminderController) AckReminder() {
+ token := c.GetString("token")
+ if token == "" {
+ c.Ctx.Output.SetStatus(400)
+ _ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
+ return
+ }
+
+ type reminderState struct {
+ AckStatus int8
+ ScheduleID uint64
+ RemindStatus int8
+ }
+
+ var state reminderState
+ table := ""
+
+ var backendReminder models.BackendScheduleReminder
+ if err := models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("ack_token", token).
+ Filter("is_deleted", 0).
+ One(&backendReminder); err == nil {
+ state = reminderState{
+ AckStatus: backendReminder.AckStatus,
+ ScheduleID: backendReminder.ScheduleID,
+ RemindStatus: backendReminder.RemindStatus,
+ }
+ table = "backend"
+ } else {
+ var platformReminder models.PlatformScheduleReminder
+ if err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("ack_token", token).
+ Filter("is_deleted", 0).
+ One(&platformReminder); err != nil {
+ c.Ctx.Output.SetStatus(404)
+ _ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
+ return
+ }
+ state = reminderState{
+ AckStatus: platformReminder.AckStatus,
+ ScheduleID: platformReminder.ScheduleID,
+ RemindStatus: platformReminder.RemindStatus,
+ }
+ table = "platform"
+ }
+
+ if state.AckStatus == 1 {
+ writeReminderAckAlreadyConfirmed(c)
+ return
+ }
+
+ now := time.Now()
+ var err error
+ if table == "backend" {
+ _, err = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("ack_token", token).
+ Filter("is_deleted", 0).
+ Update(map[string]interface{}{
+ "AckStatus": int8(1),
+ "AckTime": now,
+ "RemindStatus": int8(2),
+ "UpdateTime": now,
+ })
+ } else {
+ _, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("ack_token", token).
+ Filter("is_deleted", 0).
+ Update(map[string]interface{}{
+ "AckStatus": int8(1),
+ "AckTime": now,
+ "RemindStatus": int8(2),
+ "UpdateTime": now,
+ })
+ }
+ if err != nil {
+ c.Ctx.Output.SetStatus(500)
+ _ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
+ return
+ }
+
+ // 只关闭命中来源表中、同一日程的其他待提醒记录,避免两套表互相影响。
+ if table == "backend" {
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("schedule_id", state.ScheduleID).
+ Filter("remind_status__in", 0, 1).
+ Update(map[string]interface{}{
+ "RemindStatus": int8(2),
+ "UpdateTime": now,
+ })
+ } else {
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", state.ScheduleID).
+ Filter("remind_status__in", 0, 1).
+ Update(map[string]interface{}{
+ "RemindStatus": int8(2),
+ "UpdateTime": now,
+ })
+ }
+
+ writeReminderAckSuccess(c)
+}
+
+func writeReminderAckAlreadyConfirmed(c *ApiReminderController) {
+ c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
+ _ = c.Ctx.Output.Body([]byte(`
+
+
+
+
+ 确认收到提醒
+
+
+
+
+
提示
+
该日程提醒在此之前已确认过了。
+
无需重复点击,感谢您的使用!
+
+
+
+ `))
+}
+
+func writeReminderAckSuccess(c *ApiReminderController) {
+ c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
+ _ = c.Ctx.Output.Body([]byte(`
+
+
+
+
+ 确认成功
+
+
+
+
+
确认成功
+
您已成功确认收到该日程提醒!
+
系统已停止向您重复推送,感谢您的配合。
+
+
+
+ `))
+}
diff --git a/go/controllers/app_reminder.go b/go/controllers/app_reminder.go
index 56579a0..f17edbe 100644
--- a/go/controllers/app_reminder.go
+++ b/go/controllers/app_reminder.go
@@ -13,6 +13,7 @@ import (
"server/models"
"server/pkg/jwtutil"
+ "github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
@@ -23,18 +24,18 @@ type AppReminderController struct {
func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
- return nil, fmt.Errorf("未登录")
+ return nil, orm.ErrNoRows
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
- return nil, fmt.Errorf("认证信息格式错误")
+ return nil, orm.ErrNoRows
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
- return nil, fmt.Errorf("无效的token")
+ return nil, err
}
if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" {
- return nil, fmt.Errorf("无权访问")
+ return nil, orm.ErrNoRows
}
return claims, nil
}
@@ -71,7 +72,7 @@ type appSchedulePayload struct {
func (c *AppReminderController) GetList() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -87,8 +88,9 @@ func (c *AppReminderController) GetList() {
pageSize = 20
}
- qs := models.Orm.QueryTable(new(models.PlatformSchedule)).
- Filter("user_id", claims.UserID)
+ qs := models.Orm.QueryTable(new(models.BackendSchedule)).
+ Filter("user_id", claims.UserID).
+ Filter("tid", claims.TenantId)
if keyword != "" {
qs = qs.Filter("content__contains", keyword)
@@ -96,17 +98,20 @@ func (c *AppReminderController) GetList() {
total, _ := qs.Count()
- var schedules []models.PlatformSchedule
+ var schedules []models.BackendSchedule
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules)
- if err != nil {
+ if err != nil && err != orm.ErrNoRows {
c.jsonErr(500, 500, "查询失败: "+err.Error())
return
}
+ if schedules == nil {
+ schedules = []models.BackendSchedule{}
+ }
list := make([]map[string]interface{}, 0, len(schedules))
for _, s := range schedules {
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ var reminders []models.BackendScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", s.ID).
Filter("is_deleted", 0).
All(&reminders)
@@ -166,7 +171,7 @@ func (c *AppReminderController) GetList() {
func (c *AppReminderController) GetDetail() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -177,24 +182,25 @@ func (c *AppReminderController) GetDetail() {
return
}
- var schedule models.PlatformSchedule
- err = models.Orm.QueryTable(new(models.PlatformSchedule)).
+ var schedule models.BackendSchedule
+ err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
+ Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ var reminders []models.BackendScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", schedule.ID).
Filter("is_deleted", 0).
All(&reminders)
channels := make([]string, 0, len(reminders))
- var first models.PlatformScheduleReminder
+ var first models.BackendScheduleReminder
for _, r := range reminders {
channels = append(channels, r.RemindChannel)
first = r
@@ -236,7 +242,7 @@ func (c *AppReminderController) GetDetail() {
func (c *AppReminderController) Create() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -275,7 +281,8 @@ func (c *AppReminderController) Create() {
title = "日程提醒"
}
- schedule := models.PlatformSchedule{
+ schedule := models.BackendSchedule{
+ Tid: claims.TenantId,
Title: title,
Content: content,
ScheduleTime: schedTime,
@@ -298,7 +305,8 @@ func (c *AppReminderController) Create() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
- reminder := models.PlatformScheduleReminder{
+ reminder := models.BackendScheduleReminder{
+ Tid: claims.TenantId,
ScheduleID: uint64(schedID),
RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes,
@@ -336,7 +344,7 @@ func (c *AppReminderController) Create() {
func (c *AppReminderController) Update() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -364,18 +372,19 @@ func (c *AppReminderController) Update() {
return
}
- var schedule models.PlatformSchedule
- err = models.Orm.QueryTable(new(models.PlatformSchedule)).
+ var schedule models.BackendSchedule
+ err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
+ Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ var reminders []models.BackendScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
@@ -414,7 +423,7 @@ func (c *AppReminderController) Update() {
}
// 软删除旧提醒,重建
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Update(map[string]interface{}{
"IsDeleted": 1,
@@ -432,7 +441,8 @@ func (c *AppReminderController) Update() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
- reminder := models.PlatformScheduleReminder{
+ reminder := models.BackendScheduleReminder{
+ Tid: claims.TenantId,
ScheduleID: id,
RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes,
@@ -470,7 +480,7 @@ func (c *AppReminderController) Update() {
func (c *AppReminderController) Delete() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -481,18 +491,19 @@ func (c *AppReminderController) Delete() {
return
}
- var schedule models.PlatformSchedule
- err = models.Orm.QueryTable(new(models.PlatformSchedule)).
+ var schedule models.BackendSchedule
+ err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
+ Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ var reminders []models.BackendScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
@@ -512,9 +523,9 @@ func (c *AppReminderController) Delete() {
return
}
- _, err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete()
+ _, err = models.Orm.QueryTable(new(models.BackendSchedule)).Filter("id", id).Delete()
if err == nil {
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Update(map[string]interface{}{
"IsDeleted": 1,
@@ -530,7 +541,7 @@ func (c *AppReminderController) Delete() {
func (c *AppReminderController) ToggleComplete() {
claims, err := c.appClaims()
if err != nil {
- c.jsonErr(401, 401, err.Error())
+ c.jsonErr(401, 401, "未登录或无权限")
return
}
@@ -541,18 +552,19 @@ func (c *AppReminderController) ToggleComplete() {
return
}
- var schedule models.PlatformSchedule
- err = models.Orm.QueryTable(new(models.PlatformSchedule)).
+ var schedule models.BackendSchedule
+ err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id).
Filter("user_id", claims.UserID).
+ Filter("tid", claims.TenantId).
One(&schedule)
if err != nil {
c.jsonErr(404, 404, "日程未找到")
return
}
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ var reminders []models.BackendScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id).
Filter("is_deleted", 0).
All(&reminders)
diff --git a/go/controllers/backend_reminder.go b/go/controllers/backend_reminder.go
index 77e43d1..20064e7 100644
--- a/go/controllers/backend_reminder.go
+++ b/go/controllers/backend_reminder.go
@@ -210,13 +210,13 @@ func (c *BackendReminderController) GetReminderDetail() {
}
data := map[string]interface{}{
- "id": schedule.ID,
- "title": schedule.Title,
- "content": schedule.Content,
- "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
- "remind_channels": channels,
+ "id": schedule.ID,
+ "title": schedule.Title,
+ "content": schedule.Content,
+ "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
+ "remind_channels": channels,
"receiver_targets": targets,
- "is_finished": isFinished,
+ "is_finished": isFinished,
}
if first.ID > 0 {
data["advance_minutes"] = first.AdvanceMinutes
@@ -295,16 +295,16 @@ func (c *BackendReminderController) CreateReminder() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.BackendScheduleReminder{
- Tid: claims.TenantId,
- ScheduleID: uint64(schedID),
- RemindChannel: ch,
- AdvanceMinutes: p.AdvanceMinutes,
- NextRemindTime: firstSendTime,
- ReceiverUserID: uint64(claims.UserID),
- ReceiverTarget: target,
- RemindStatus: 0,
- CreateTime: time.Now(),
- UpdateTime: time.Now(),
+ Tid: claims.TenantId,
+ ScheduleID: uint64(schedID),
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: uint64(claims.UserID),
+ ReceiverTarget: target,
+ RemindStatus: 0,
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
}
if ch == "EMAIL" || ch == "BARK" {
@@ -433,16 +433,16 @@ func (c *BackendReminderController) UpdateReminder() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.BackendScheduleReminder{
- Tid: claims.TenantId,
- ScheduleID: id,
- RemindChannel: ch,
- AdvanceMinutes: p.AdvanceMinutes,
- NextRemindTime: firstSendTime,
- ReceiverUserID: schedule.UserID,
- ReceiverTarget: target,
- RemindStatus: 0,
- CreateTime: time.Now(),
- UpdateTime: time.Now(),
+ Tid: claims.TenantId,
+ ScheduleID: id,
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: schedule.UserID,
+ ReceiverTarget: target,
+ RemindStatus: 0,
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
}
if ch == "EMAIL" || ch == "BARK" {
@@ -703,13 +703,20 @@ func (c *BackendReminderController) TestReminder() {
}
dummyToken := "test-token-for-verification"
- reminder := &models.PlatformScheduleReminder{
+ reminder := &models.BackendScheduleReminder{
+ Tid: claims.TenantId,
RemindChannel: ch,
ReceiverUserID: uint64(claims.UserID),
AckToken: &dummyToken,
}
- success, sendErr := sender.Send(context.Background(), reminder, "[测试]"+p.Title, p.Content)
+ success, sendErr := sender.Send(context.Background(), services.ReminderData{
+ RemindChannel: reminder.RemindChannel,
+ ReceiverUserID: reminder.ReceiverUserID,
+ ReceiverTarget: reminder.ReceiverTarget,
+ AckToken: reminder.AckToken,
+ Tid: reminder.Tid,
+ }, "[测试]"+p.Title, p.Content)
msg := "发送成功"
if !success {
msg = "发送失败"
diff --git a/go/controllers/platform_reminder.go b/go/controllers/platform_reminder.go
index df7361f..e8164c8 100644
--- a/go/controllers/platform_reminder.go
+++ b/go/controllers/platform_reminder.go
@@ -1,631 +1,636 @@
-package controllers
-
-import (
- "context"
- "crypto/rand"
- "encoding/json"
- "fmt"
- "io"
- "strconv"
- "strings"
- "time"
-
- "server/models"
- "server/pkg/jwtutil"
- "server/services"
-
- beego "github.com/beego/beego/v2/server/web"
-)
-
-type PlatformReminderController struct {
- beego.Controller
-}
-
-func (c *PlatformReminderController) platformClaims() (*jwtutil.Claims, error) {
- auth := c.Ctx.Request.Header.Get("Authorization")
- if auth == "" {
- return nil, fmt.Errorf("未登录")
- }
- parts := strings.SplitN(auth, " ", 2)
- if len(parts) != 2 || parts[0] != "Bearer" {
- return nil, fmt.Errorf("认证信息格式错误")
- }
- claims, err := jwtutil.ParseToken(parts[1])
- if err != nil {
- return nil, fmt.Errorf("无效的token")
- }
- if claims.UserType != "platform" {
- return nil, fmt.Errorf("无权访问")
- }
- return claims, nil
-}
-
-func (c *PlatformReminderController) jsonErr(httpStatus, bizCode int, msg string) {
- c.Ctx.Output.SetStatus(httpStatus)
- c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
- _ = c.ServeJSON()
-}
-
-func (c *PlatformReminderController) ok(data interface{}) {
- c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
- _ = c.ServeJSON()
-}
-
-// generateToken 生成一个随机的 ack_token
-func generateToken() string {
- b := make([]byte, 16)
- _, _ = rand.Read(b)
- b[6] = (b[6] & 0x0f) | 0x40
- b[8] = (b[8] & 0x3f) | 0x80
- return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
-}
-
-type reminderFormPayload struct {
- Title string `json:"title"`
- Content string `json:"content"`
- ScheduleTime string `json:"schedule_time"`
- RemindChannels []string `json:"remind_channels"` // EMAIL, BARK, SMS, SITE_MSG
- AdvanceMinutes int `json:"advance_minutes"`
- RepeatIntervalMinutes int `json:"repeat_interval_minutes"`
- MaxSendCount int `json:"max_send_count"`
- ReceiverUserID uint64 `json:"receiver_user_id"`
- ReceiverTargets map[string]string `json:"receiver_targets"` // "SMS": "1380...", "EMAIL": "...", "BARK": "..."
-}
-
-// GetReminderList GET /platform/reminder/list
-func (c *PlatformReminderController) GetReminderList() {
- if _, err := c.platformClaims(); err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- page, _ := c.GetInt("page", 1)
- pageSize, _ := c.GetInt("pageSize", 20)
- if page < 1 {
- page = 1
- }
- if pageSize < 1 {
- pageSize = 20
- }
-
- // 联表获取日程及提醒信息
- var schedules []models.PlatformSchedule
- qs := models.Orm.QueryTable(new(models.PlatformSchedule))
- total, _ := qs.Count()
-
- _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules)
- if err != nil {
- c.jsonErr(500, 500, "查询失败: "+err.Error())
- return
- }
-
- list := make([]map[string]interface{}, 0, len(schedules))
- for _, s := range schedules {
- // 查询该日程关联的所有提醒记录
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", s.ID).
- Filter("is_deleted", 0).
- All(&reminders)
-
- channels := make([]string, 0, len(reminders))
- isFinished := true
- if len(reminders) == 0 {
- isFinished = false
- } else {
- for _, r := range reminders {
- channels = append(channels, r.RemindChannel)
- if r.RemindStatus != 2 {
- isFinished = false
- }
- }
- }
-
- item := map[string]interface{}{
- "id": s.ID,
- "title": s.Title,
- "content": s.Content,
- "schedule_time": s.ScheduleTime.Format("2006-01-02 15:04:05"),
- "remind_channels": channels,
- "user_id": s.UserID,
- "is_finished": isFinished,
- }
- if len(reminders) > 0 {
- first := reminders[0]
- item["advance_minutes"] = first.AdvanceMinutes
- item["repeat_interval_minutes"] = first.RepeatIntervalMinutes
- item["max_send_count"] = first.MaxSendCount
- item["receiver_user_id"] = first.ReceiverUserID
- }
- list = append(list, item)
- }
-
- c.ok(map[string]interface{}{
- "list": list,
- "total": total,
- "page": page,
- "pageSize": pageSize,
- })
-}
-
-// GetReminderDetail GET /platform/reminder/:id
-func (c *PlatformReminderController) GetReminderDetail() {
- if _, err := c.platformClaims(); err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- idStr := c.Ctx.Input.Param(":id")
- id, _ := strconv.ParseUint(idStr, 10, 64)
- if id == 0 {
- c.jsonErr(400, 400, "无效的ID")
- return
- }
-
- var schedule models.PlatformSchedule
- err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
- if err != nil {
- c.jsonErr(404, 404, "日程未找到")
- return
- }
-
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", schedule.ID).
- Filter("is_deleted", 0).
- All(&reminders)
-
- channels := make([]string, 0, len(reminders))
- targets := make(map[string]string)
- var first models.PlatformScheduleReminder
-
- for _, r := range reminders {
- channels = append(channels, r.RemindChannel)
- if r.ReceiverTarget != nil {
- targets[r.RemindChannel] = *r.ReceiverTarget
- }
- first = r
- }
-
- isFinished := true
- if len(reminders) == 0 {
- isFinished = false
- } else {
- for _, r := range reminders {
- if r.RemindStatus != 2 {
- isFinished = false
- break
- }
- }
- }
-
- data := map[string]interface{}{
- "id": schedule.ID,
- "title": schedule.Title,
- "content": schedule.Content,
- "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
- "remind_channels": channels,
- "receiver_targets": targets,
- "is_finished": isFinished,
- }
- if first.ID > 0 {
- data["advance_minutes"] = first.AdvanceMinutes
- data["repeat_interval_minutes"] = first.RepeatIntervalMinutes
- data["max_send_count"] = first.MaxSendCount
- data["receiver_user_id"] = first.ReceiverUserID
- }
-
- c.ok(data)
-}
-
-// CreateReminder POST /platform/reminder
-func (c *PlatformReminderController) CreateReminder() {
- claims, err := c.platformClaims()
- if err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- raw, err := io.ReadAll(c.Ctx.Request.Body)
- if err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
- var p reminderFormPayload
- if err := json.Unmarshal(raw, &p); err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
-
- if strings.TrimSpace(p.ScheduleTime) == "" {
- c.jsonErr(400, 400, "日程发生时间不能为空")
- return
- }
-
- schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
- if err != nil {
- c.jsonErr(400, 400, "日程时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
- return
- }
-
- // 1. 插入日程主表
- schedule := models.PlatformSchedule{
- Title: "日程提醒",
- Content: p.Content,
- ScheduleTime: schedTime,
- UserID: uint64(claims.UserID),
- }
- schedID, err := models.Orm.Insert(&schedule)
- if err != nil {
- c.jsonErr(500, 500, "保存日程失败: "+err.Error())
- return
- }
-
- // 2. 根据选中的渠道循环创建提醒
- for _, ch := range p.RemindChannels {
- ch = strings.ToUpper(strings.TrimSpace(ch))
- if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
- continue
- }
-
- targetVal := p.ReceiverTargets[ch]
- var target *string
- if targetVal != "" {
- target = &targetVal
- }
-
- // 计算首次发送时间
- firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
-
- reminder := models.PlatformScheduleReminder{
- ScheduleID: uint64(schedID),
- RemindChannel: ch,
- AdvanceMinutes: p.AdvanceMinutes,
- NextRemindTime: firstSendTime,
- ReceiverUserID: uint64(claims.UserID), // 谁创建的就发给谁
- ReceiverTarget: target,
- RemindStatus: 0, // 待提醒
- CreateTime: time.Now(),
- UpdateTime: time.Now(),
- }
-
- if ch == "EMAIL" || ch == "BARK" {
- token := generateToken()
- reminder.AckToken = &token
- reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
- reminder.MaxSendCount = p.MaxSendCount
- if reminder.MaxSendCount <= 0 {
- reminder.MaxSendCount = 1
- }
- } else {
- // SMS 或 SITE_MSG
- reminder.RepeatIntervalMinutes = 0
- reminder.MaxSendCount = 1
- }
-
- _, err = models.Orm.Insert(&reminder)
- if err != nil {
- c.jsonErr(500, 500, "创建提醒失败: "+err.Error())
- return
- }
- }
-
- c.ok(map[string]interface{}{"schedule_id": schedID})
-}
-
-// UpdateReminder PUT /platform/reminder/:id
-func (c *PlatformReminderController) UpdateReminder() {
- if _, err := c.platformClaims(); err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- idStr := c.Ctx.Input.Param(":id")
- id, _ := strconv.ParseUint(idStr, 10, 64)
- if id == 0 {
- c.jsonErr(400, 400, "无效的ID")
- return
- }
-
- raw, err := io.ReadAll(c.Ctx.Request.Body)
- if err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
- var p reminderFormPayload
- if err := json.Unmarshal(raw, &p); err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
-
- schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
- if err != nil {
- c.jsonErr(400, 400, "日程时间格式不合法")
- return
- }
-
- // 1. 更新日程详情
- var schedule models.PlatformSchedule
- err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
- if err != nil {
- c.jsonErr(404, 404, "日程未找到")
- return
- }
-
- // 检查是否所有关联的提醒都已结束
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", id).
- Filter("is_deleted", 0).
- All(&reminders)
- isFinished := true
- if len(reminders) == 0 {
- isFinished = false
- } else {
- for _, r := range reminders {
- if r.RemindStatus != 2 {
- isFinished = false
- break
- }
- }
- }
- if isFinished {
- c.jsonErr(400, 400, "该日程提醒已全部结束,无法编辑")
- return
- }
- schedule.Title = "日程提醒"
- schedule.Content = p.Content
- schedule.ScheduleTime = schedTime
- _, err = models.Orm.Update(&schedule, "Title", "Content", "ScheduleTime")
- if err != nil {
- c.jsonErr(500, 500, "更新失败")
- return
- }
-
- // 2. 软删除原本的所有提醒
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", id).
- Update(map[string]interface{}{
- "IsDeleted": 1,
- "UpdateTime": time.Now(),
- })
-
- // 3. 重新建立提醒
- for _, ch := range p.RemindChannels {
- ch = strings.ToUpper(strings.TrimSpace(ch))
- if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
- continue
- }
-
- targetVal := p.ReceiverTargets[ch]
- var target *string
- if targetVal != "" {
- target = &targetVal
- }
-
- firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
-
- reminder := models.PlatformScheduleReminder{
- ScheduleID: id,
- RemindChannel: ch,
- AdvanceMinutes: p.AdvanceMinutes,
- NextRemindTime: firstSendTime,
- ReceiverUserID: schedule.UserID, // 谁创建的就发给谁
- ReceiverTarget: target,
- RemindStatus: 0,
- CreateTime: time.Now(),
- UpdateTime: time.Now(),
- }
-
- if ch == "EMAIL" || ch == "BARK" {
- token := generateToken()
- reminder.AckToken = &token
- reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
- reminder.MaxSendCount = p.MaxSendCount
- if reminder.MaxSendCount <= 0 {
- reminder.MaxSendCount = 1
- }
- } else {
- reminder.RepeatIntervalMinutes = 0
- reminder.MaxSendCount = 1
- }
-
- _, err = models.Orm.Insert(&reminder)
- if err != nil {
- c.jsonErr(500, 500, "重新创建提醒失败")
- return
- }
- }
-
- c.ok(nil)
-}
-
-// DeleteReminder DELETE /platform/reminder/:id
-func (c *PlatformReminderController) DeleteReminder() {
- if _, err := c.platformClaims(); err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- idStr := c.Ctx.Input.Param(":id")
- id, _ := strconv.ParseUint(idStr, 10, 64)
- if id == 0 {
- c.jsonErr(400, 400, "无效的ID")
- return
- }
-
- // 检查是否所有关联的提醒都已结束
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", id).
- Filter("is_deleted", 0).
- All(&reminders)
- isFinished := true
- if len(reminders) == 0 {
- isFinished = false
- } else {
- for _, r := range reminders {
- if r.RemindStatus != 2 {
- isFinished = false
- break
- }
- }
- }
- if isFinished {
- c.jsonErr(400, 400, "该日程提醒已全部结束,无法删除")
- return
- }
-
- // 软删除日程
- // 这里的 id 既可以是主表的 id,也可以是日程的 id
- // 我们如果是管理页面,都是基于日程维度的,所以这里 id 指代 schedule_id
- _, err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete()
- if err == nil {
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", id).
- Update(map[string]interface{}{
- "IsDeleted": 1,
- "UpdateTime": time.Now(),
- })
- }
-
- c.ok(nil)
-}
-
-type reminderBatchDeletePayload struct {
- Ids []uint64 `json:"ids"`
-}
-
-// BatchDeleteReminder POST /platform/reminder/batchDelete
-func (c *PlatformReminderController) BatchDeleteReminder() {
- if _, err := c.platformClaims(); err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- raw, err := io.ReadAll(c.Ctx.Request.Body)
- if err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
- var p reminderBatchDeletePayload
- if err := json.Unmarshal(raw, &p); err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
-
- if len(p.Ids) == 0 {
- c.ok(nil)
- return
- }
-
- // 检查选中的日程是否有任何一个是全部结束的,防误操作
- for _, scheduleID := range p.Ids {
- var reminders []models.PlatformScheduleReminder
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id", scheduleID).
- Filter("is_deleted", 0).
- All(&reminders)
- isFinished := true
- if len(reminders) == 0 {
- isFinished = false
- } else {
- for _, r := range reminders {
- if r.RemindStatus != 2 {
- isFinished = false
- break
- }
- }
- }
- if isFinished {
- c.jsonErr(400, 400, fmt.Sprintf("选中的日程ID %d 的提醒已全部结束,无法删除", scheduleID))
- return
- }
- }
-
- // 批量软删除
- _, _ = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id__in", p.Ids).Delete()
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("schedule_id__in", p.Ids).
- Update(map[string]interface{}{
- "IsDeleted": 1,
- "UpdateTime": time.Now(),
- })
-
- c.ok(nil)
-}
-
-type reminderTestPayload struct {
- Title string `json:"title"`
- Content string `json:"content"`
- RemindChannels []string `json:"remind_channels"`
-}
-
-// TestReminder POST /platform/reminder/test
-func (c *PlatformReminderController) TestReminder() {
- claims, err := c.platformClaims()
- if err != nil {
- c.jsonErr(401, 401, err.Error())
- return
- }
-
- raw, err := io.ReadAll(c.Ctx.Request.Body)
- if err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
- var p reminderTestPayload
- if err := json.Unmarshal(raw, &p); err != nil {
- c.jsonErr(400, 400, "参数错误")
- return
- }
-
- if strings.TrimSpace(p.Title) == "" {
- p.Title = "测试提醒"
- }
- if strings.TrimSpace(p.Content) == "" {
- p.Content = "这是一条验证日程提醒配置的测试通知。"
- }
-
- senders := map[string]services.ReminderSender{
- "SMS": &services.SMSSender{},
- "EMAIL": &services.EmailSender{},
- "BARK": &services.BarkSender{},
- "SITE_MSG": &services.SiteMsgSender{},
- }
-
- type TestResult struct {
- Channel string `json:"channel"`
- Success bool `json:"success"`
- Msg string `json:"msg"`
- }
- results := make([]TestResult, 0)
-
- for _, ch := range p.RemindChannels {
- ch = strings.ToUpper(strings.TrimSpace(ch))
- sender, ok := senders[ch]
- if !ok {
- results = append(results, TestResult{Channel: ch, Success: false, Msg: "不支持的提醒渠道"})
- continue
- }
-
- dummyToken := "test-token-for-verification"
- reminder := &models.PlatformScheduleReminder{
- RemindChannel: ch,
- ReceiverUserID: uint64(claims.UserID),
- AckToken: &dummyToken,
- }
-
- success, sendErr := sender.Send(context.Background(), reminder, "[测试]"+p.Title, p.Content)
- msg := "发送成功"
- if !success {
- msg = "发送失败"
- if sendErr != nil {
- msg = sendErr.Error()
- }
- }
- results = append(results, TestResult{Channel: ch, Success: success, Msg: msg})
- }
-
- c.ok(results)
-}
+package controllers
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
+ "server/models"
+ "server/pkg/jwtutil"
+ "server/services"
+
+ beego "github.com/beego/beego/v2/server/web"
+)
+
+type PlatformReminderController struct {
+ beego.Controller
+}
+
+func (c *PlatformReminderController) platformClaims() (*jwtutil.Claims, error) {
+ auth := c.Ctx.Request.Header.Get("Authorization")
+ if auth == "" {
+ return nil, fmt.Errorf("未登录")
+ }
+ parts := strings.SplitN(auth, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return nil, fmt.Errorf("认证信息格式错误")
+ }
+ claims, err := jwtutil.ParseToken(parts[1])
+ if err != nil {
+ return nil, fmt.Errorf("无效的token")
+ }
+ if claims.UserType != "platform" {
+ return nil, fmt.Errorf("无权访问")
+ }
+ return claims, nil
+}
+
+func (c *PlatformReminderController) jsonErr(httpStatus, bizCode int, msg string) {
+ c.Ctx.Output.SetStatus(httpStatus)
+ c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
+ _ = c.ServeJSON()
+}
+
+func (c *PlatformReminderController) ok(data interface{}) {
+ c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
+ _ = c.ServeJSON()
+}
+
+// generateToken 生成一个随机的 ack_token
+func generateToken() string {
+ b := make([]byte, 16)
+ _, _ = rand.Read(b)
+ b[6] = (b[6] & 0x0f) | 0x40
+ b[8] = (b[8] & 0x3f) | 0x80
+ return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
+}
+
+type reminderFormPayload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ ScheduleTime string `json:"schedule_time"`
+ RemindChannels []string `json:"remind_channels"` // EMAIL, BARK, SMS, SITE_MSG
+ AdvanceMinutes int `json:"advance_minutes"`
+ RepeatIntervalMinutes int `json:"repeat_interval_minutes"`
+ MaxSendCount int `json:"max_send_count"`
+ ReceiverUserID uint64 `json:"receiver_user_id"`
+ ReceiverTargets map[string]string `json:"receiver_targets"` // "SMS": "1380...", "EMAIL": "...", "BARK": "..."
+}
+
+// GetReminderList GET /platform/reminder/list
+func (c *PlatformReminderController) GetReminderList() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ page, _ := c.GetInt("page", 1)
+ pageSize, _ := c.GetInt("pageSize", 20)
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 {
+ pageSize = 20
+ }
+
+ // 联表获取日程及提醒信息
+ var schedules []models.PlatformSchedule
+ qs := models.Orm.QueryTable(new(models.PlatformSchedule))
+ total, _ := qs.Count()
+
+ _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules)
+ if err != nil {
+ c.jsonErr(500, 500, "查询失败: "+err.Error())
+ return
+ }
+
+ list := make([]map[string]interface{}, 0, len(schedules))
+ for _, s := range schedules {
+ // 查询该日程关联的所有提醒记录
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", s.ID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+
+ channels := make([]string, 0, len(reminders))
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ channels = append(channels, r.RemindChannel)
+ if r.RemindStatus != 2 {
+ isFinished = false
+ }
+ }
+ }
+
+ item := map[string]interface{}{
+ "id": s.ID,
+ "title": s.Title,
+ "content": s.Content,
+ "schedule_time": s.ScheduleTime.Format("2006-01-02 15:04:05"),
+ "remind_channels": channels,
+ "user_id": s.UserID,
+ "is_finished": isFinished,
+ }
+ if len(reminders) > 0 {
+ first := reminders[0]
+ item["advance_minutes"] = first.AdvanceMinutes
+ item["repeat_interval_minutes"] = first.RepeatIntervalMinutes
+ item["max_send_count"] = first.MaxSendCount
+ item["receiver_user_id"] = first.ReceiverUserID
+ }
+ list = append(list, item)
+ }
+
+ c.ok(map[string]interface{}{
+ "list": list,
+ "total": total,
+ "page": page,
+ "pageSize": pageSize,
+ })
+}
+
+// GetReminderDetail GET /platform/reminder/:id
+func (c *PlatformReminderController) GetReminderDetail() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ var schedule models.PlatformSchedule
+ err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
+ if err != nil {
+ c.jsonErr(404, 404, "日程未找到")
+ return
+ }
+
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", schedule.ID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+
+ channels := make([]string, 0, len(reminders))
+ targets := make(map[string]string)
+ var first models.PlatformScheduleReminder
+
+ for _, r := range reminders {
+ channels = append(channels, r.RemindChannel)
+ if r.ReceiverTarget != nil {
+ targets[r.RemindChannel] = *r.ReceiverTarget
+ }
+ first = r
+ }
+
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+
+ data := map[string]interface{}{
+ "id": schedule.ID,
+ "title": schedule.Title,
+ "content": schedule.Content,
+ "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
+ "remind_channels": channels,
+ "receiver_targets": targets,
+ "is_finished": isFinished,
+ }
+ if first.ID > 0 {
+ data["advance_minutes"] = first.AdvanceMinutes
+ data["repeat_interval_minutes"] = first.RepeatIntervalMinutes
+ data["max_send_count"] = first.MaxSendCount
+ data["receiver_user_id"] = first.ReceiverUserID
+ }
+
+ c.ok(data)
+}
+
+// CreateReminder POST /platform/reminder
+func (c *PlatformReminderController) CreateReminder() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderFormPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if strings.TrimSpace(p.ScheduleTime) == "" {
+ c.jsonErr(400, 400, "日程发生时间不能为空")
+ return
+ }
+
+ schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
+ if err != nil {
+ c.jsonErr(400, 400, "日程时间格式不合法,支持 YYYY-MM-DD HH:mm:ss")
+ return
+ }
+
+ // 1. 插入日程主表
+ schedule := models.PlatformSchedule{
+ Title: "日程提醒",
+ Content: p.Content,
+ ScheduleTime: schedTime,
+ UserID: uint64(claims.UserID),
+ }
+ schedID, err := models.Orm.Insert(&schedule)
+ if err != nil {
+ c.jsonErr(500, 500, "保存日程失败: "+err.Error())
+ return
+ }
+
+ // 2. 根据选中的渠道循环创建提醒
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
+ continue
+ }
+
+ targetVal := p.ReceiverTargets[ch]
+ var target *string
+ if targetVal != "" {
+ target = &targetVal
+ }
+
+ // 计算首次发送时间
+ firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
+
+ reminder := models.PlatformScheduleReminder{
+ ScheduleID: uint64(schedID),
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: uint64(claims.UserID), // 谁创建的就发给谁
+ ReceiverTarget: target,
+ RemindStatus: 0, // 待提醒
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
+ }
+
+ if ch == "EMAIL" || ch == "BARK" {
+ token := generateToken()
+ reminder.AckToken = &token
+ reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
+ reminder.MaxSendCount = p.MaxSendCount
+ if reminder.MaxSendCount <= 0 {
+ reminder.MaxSendCount = 1
+ }
+ } else {
+ // SMS 或 SITE_MSG
+ reminder.RepeatIntervalMinutes = 0
+ reminder.MaxSendCount = 1
+ }
+
+ _, err = models.Orm.Insert(&reminder)
+ if err != nil {
+ c.jsonErr(500, 500, "创建提醒失败: "+err.Error())
+ return
+ }
+ }
+
+ c.ok(map[string]interface{}{"schedule_id": schedID})
+}
+
+// UpdateReminder PUT /platform/reminder/:id
+func (c *PlatformReminderController) UpdateReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderFormPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ schedTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.ScheduleTime, time.Local)
+ if err != nil {
+ c.jsonErr(400, 400, "日程时间格式不合法")
+ return
+ }
+
+ // 1. 更新日程详情
+ var schedule models.PlatformSchedule
+ err = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).One(&schedule)
+ if err != nil {
+ c.jsonErr(404, 404, "日程未找到")
+ return
+ }
+
+ // 检查是否所有关联的提醒都已结束
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, "该日程提醒已全部结束,无法编辑")
+ return
+ }
+ schedule.Title = "日程提醒"
+ schedule.Content = p.Content
+ schedule.ScheduleTime = schedTime
+ _, err = models.Orm.Update(&schedule, "Title", "Content", "ScheduleTime")
+ if err != nil {
+ c.jsonErr(500, 500, "更新失败")
+ return
+ }
+
+ // 2. 软删除原本的所有提醒
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+
+ // 3. 重新建立提醒
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
+ continue
+ }
+
+ targetVal := p.ReceiverTargets[ch]
+ var target *string
+ if targetVal != "" {
+ target = &targetVal
+ }
+
+ firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
+
+ reminder := models.PlatformScheduleReminder{
+ ScheduleID: id,
+ RemindChannel: ch,
+ AdvanceMinutes: p.AdvanceMinutes,
+ NextRemindTime: firstSendTime,
+ ReceiverUserID: schedule.UserID, // 谁创建的就发给谁
+ ReceiverTarget: target,
+ RemindStatus: 0,
+ CreateTime: time.Now(),
+ UpdateTime: time.Now(),
+ }
+
+ if ch == "EMAIL" || ch == "BARK" {
+ token := generateToken()
+ reminder.AckToken = &token
+ reminder.RepeatIntervalMinutes = p.RepeatIntervalMinutes
+ reminder.MaxSendCount = p.MaxSendCount
+ if reminder.MaxSendCount <= 0 {
+ reminder.MaxSendCount = 1
+ }
+ } else {
+ reminder.RepeatIntervalMinutes = 0
+ reminder.MaxSendCount = 1
+ }
+
+ _, err = models.Orm.Insert(&reminder)
+ if err != nil {
+ c.jsonErr(500, 500, "重新创建提醒失败")
+ return
+ }
+ }
+
+ c.ok(nil)
+}
+
+// DeleteReminder DELETE /platform/reminder/:id
+func (c *PlatformReminderController) DeleteReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ idStr := c.Ctx.Input.Param(":id")
+ id, _ := strconv.ParseUint(idStr, 10, 64)
+ if id == 0 {
+ c.jsonErr(400, 400, "无效的ID")
+ return
+ }
+
+ // 检查是否所有关联的提醒都已结束
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, "该日程提醒已全部结束,无法删除")
+ return
+ }
+
+ // 软删除日程
+ // 这里的 id 既可以是主表的 id,也可以是日程的 id
+ // 我们如果是管理页面,都是基于日程维度的,所以这里 id 指代 schedule_id
+ _, err := models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id", id).Delete()
+ if err == nil {
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", id).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+ }
+
+ c.ok(nil)
+}
+
+type reminderBatchDeletePayload struct {
+ Ids []uint64 `json:"ids"`
+}
+
+// BatchDeleteReminder POST /platform/reminder/batchDelete
+func (c *PlatformReminderController) BatchDeleteReminder() {
+ if _, err := c.platformClaims(); err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderBatchDeletePayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if len(p.Ids) == 0 {
+ c.ok(nil)
+ return
+ }
+
+ // 检查选中的日程是否有任何一个是全部结束的,防误操作
+ for _, scheduleID := range p.Ids {
+ var reminders []models.PlatformScheduleReminder
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id", scheduleID).
+ Filter("is_deleted", 0).
+ All(&reminders)
+ isFinished := true
+ if len(reminders) == 0 {
+ isFinished = false
+ } else {
+ for _, r := range reminders {
+ if r.RemindStatus != 2 {
+ isFinished = false
+ break
+ }
+ }
+ }
+ if isFinished {
+ c.jsonErr(400, 400, fmt.Sprintf("选中的日程ID %d 的提醒已全部结束,无法删除", scheduleID))
+ return
+ }
+ }
+
+ // 批量软删除
+ _, _ = models.Orm.QueryTable(new(models.PlatformSchedule)).Filter("id__in", p.Ids).Delete()
+ _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
+ Filter("schedule_id__in", p.Ids).
+ Update(map[string]interface{}{
+ "IsDeleted": 1,
+ "UpdateTime": time.Now(),
+ })
+
+ c.ok(nil)
+}
+
+type reminderTestPayload struct {
+ Title string `json:"title"`
+ Content string `json:"content"`
+ RemindChannels []string `json:"remind_channels"`
+}
+
+// TestReminder POST /platform/reminder/test
+func (c *PlatformReminderController) TestReminder() {
+ claims, err := c.platformClaims()
+ if err != nil {
+ c.jsonErr(401, 401, err.Error())
+ return
+ }
+
+ raw, err := io.ReadAll(c.Ctx.Request.Body)
+ if err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+ var p reminderTestPayload
+ if err := json.Unmarshal(raw, &p); err != nil {
+ c.jsonErr(400, 400, "参数错误")
+ return
+ }
+
+ if strings.TrimSpace(p.Title) == "" {
+ p.Title = "测试提醒"
+ }
+ if strings.TrimSpace(p.Content) == "" {
+ p.Content = "这是一条验证日程提醒配置的测试通知。"
+ }
+
+ senders := map[string]services.ReminderSender{
+ "SMS": &services.SMSSender{},
+ "EMAIL": &services.EmailSender{},
+ "BARK": &services.BarkSender{},
+ "SITE_MSG": &services.SiteMsgSender{},
+ }
+
+ type TestResult struct {
+ Channel string `json:"channel"`
+ Success bool `json:"success"`
+ Msg string `json:"msg"`
+ }
+ results := make([]TestResult, 0)
+
+ for _, ch := range p.RemindChannels {
+ ch = strings.ToUpper(strings.TrimSpace(ch))
+ sender, ok := senders[ch]
+ if !ok {
+ results = append(results, TestResult{Channel: ch, Success: false, Msg: "不支持的提醒渠道"})
+ continue
+ }
+
+ dummyToken := "test-token-for-verification"
+ reminder := &models.PlatformScheduleReminder{
+ RemindChannel: ch,
+ ReceiverUserID: uint64(claims.UserID),
+ AckToken: &dummyToken,
+ }
+
+ success, sendErr := sender.Send(context.Background(), services.ReminderData{
+ RemindChannel: reminder.RemindChannel,
+ ReceiverUserID: reminder.ReceiverUserID,
+ ReceiverTarget: reminder.ReceiverTarget,
+ AckToken: reminder.AckToken,
+ }, "[测试]"+p.Title, p.Content)
+ msg := "发送成功"
+ if !success {
+ msg = "发送失败"
+ if sendErr != nil {
+ msg = sendErr.Error()
+ }
+ }
+ results = append(results, TestResult{Channel: ch, Success: success, Msg: msg})
+ }
+
+ c.ok(results)
+}
diff --git a/go/services/reminder_scheduler.go b/go/services/reminder_scheduler.go
index 39ea447..a724264 100644
--- a/go/services/reminder_scheduler.go
+++ b/go/services/reminder_scheduler.go
@@ -1,371 +1,388 @@
-package services
-
-import (
- "bytes"
- "context"
- "crypto/rand"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
-
- "server/models"
-)
-
-// ReminderSender 提醒发送接口
-type ReminderSender interface {
- Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error)
-}
-
-// SMSSender 短信发送实现
-type SMSSender struct{}
-
-func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
- backendURL, apiKey, err := getDefaultSystemSMSConfig()
- if err != nil {
- return false, err
- }
- phone := ""
- if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
- phone = *reminder.ReceiverTarget
- } else {
- var user models.AdminUser
- if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
- phone = *user.Phone
- }
- }
- if phone == "" {
- return false, fmt.Errorf("未配置手机号")
- }
-
- enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
- payload := map[string]interface{}{
- "phone": phone,
- "content": title + ": " + content,
- }
- bs, _ := json.Marshal(payload)
-
- client := &http.Client{Timeout: 10 * time.Second}
- req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
- if err != nil {
- return false, err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Api-Key", apiKey)
-
- resp, err := client.Do(req)
- if err != nil {
- return false, err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- bodyBytes, _ := io.ReadAll(resp.Body)
- return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
- }
-
- return true, nil
-}
-
-// EmailSender 邮件发送实现
-type EmailSender struct{}
-
-func (s *EmailSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
- emails, err := ListSystemEmails()
- if err != nil || len(emails) == 0 {
- return false, fmt.Errorf("未配置系统邮箱")
- }
- emailCfg := emails[0]
- if emailCfg.FromAddress == "" || emailCfg.Host == "" {
- return false, fmt.Errorf("未配置系统邮箱")
- }
-
- toEmail := ""
- if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
- toEmail = *reminder.ReceiverTarget
- } else {
- var user models.AdminUser
- if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
- toEmail = *user.Email
- }
- }
- if toEmail == "" {
- return false, fmt.Errorf("未配置收件邮箱")
- }
-
- sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
- ackToken := ""
- if reminder.AckToken != nil {
- ackToken = *reminder.AckToken
- }
-
- // 构造 HTML 邮件
- htmlBody := fmt.Sprintf(`
-
-
日程提醒:%s
-
%s
-
- `, title, content)
-
- if ackToken != "" {
- ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
- htmlBody += fmt.Sprintf(`
-
-
确认收到后,系统将不再向您发送该日程的重复提醒。
- `, ackURL)
- }
-
- htmlBody += "
"
-
- cfg := SMTPConfig{
- FromAddress: emailCfg.FromAddress,
- Host: emailCfg.Host,
- Port: emailCfg.Port,
- Password: emailCfg.Password,
- Encryption: emailCfg.Encryption,
- Timeout: emailCfg.Timeout,
- }
- if emailCfg.FromName != nil {
- cfg.FromName = *emailCfg.FromName
- }
-
- err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
- if err != nil {
- return false, err
- }
-
- return true, nil
-}
-
-// BarkSender Bark 推送实现
-type BarkSender struct{}
-
-func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
- deviceKey := ""
- if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
- deviceKey = *reminder.ReceiverTarget
- } else {
- deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
- }
- if deviceKey == "" {
- return false, fmt.Errorf("Bark 设备 Key 未配置")
- }
-
- serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
- sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
- ackToken := ""
- if reminder.AckToken != nil {
- ackToken = *reminder.AckToken
- }
-
- baseURL := strings.TrimRight(serverURL, "/")
- escapedTitle := url.PathEscape(title)
- pushContent := content
- if ackToken != "" {
- pushContent += "\n确认收到请点击→"
- }
- escapedContent := url.PathEscape(pushContent)
-
- barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
-
- if ackToken != "" {
- ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
- // Bark 官方推送支持 url 参数
- barkURL += "?url=" + url.QueryEscape(ackURL)
- }
-
- client := &http.Client{Timeout: 10 * time.Second}
- req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
- if err != nil {
- return false, err
- }
-
- resp, err := client.Do(req)
- if err != nil {
- return false, err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- bodyBytes, _ := io.ReadAll(resp.Body)
- return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
- }
-
- return true, nil
-}
-
-// SiteMsgSender 站内信发送实现
-type SiteMsgSender struct{}
-
-func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) {
- now := time.Now()
- msg := &models.SystemReminderList{
- Title: title,
- Content: content,
- SenderID: 0,
- SenderType: "system",
- ReceiverID: reminder.ReceiverUserID,
- ReceiverType: "platform", // 平台端用户
- IsRead: 0,
- CreateTime: &now,
- }
- _, err := models.Orm.Insert(msg)
- if err != nil {
- return false, err
- }
- return true, nil
-}
-
-// generateUUID 生成一个安全的随机 UUID 字符
-func generateUUID() string {
- b := make([]byte, 16)
- _, _ = rand.Read(b)
- b[6] = (b[6] & 0x0f) | 0x40
- b[8] = (b[8] & 0x3f) | 0x80
- return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
-}
-
-// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
-func StartReminderScheduler(stopChan chan struct{}) {
- ticker := time.NewTicker(1 * time.Minute)
- go func() {
- for {
- select {
- case <-ticker.C:
- scanAndSendReminders()
- case <-stopChan:
- ticker.Stop()
- return
- }
- }
- }()
-}
-
-func scanAndSendReminders() {
- // 1. 生成唯一扫描批次号用于抢占锁定
- scanBatch := generateUUID()
- now := time.Now()
-
- // 2. 抢占待处理的数据(乐观锁防并发重复发送)
- _, err := models.Orm.Raw(`
- UPDATE yz_platform_schedule_reminder
- SET scan_lock = ?, update_time = NOW()
- WHERE next_remind_time <= ?
- AND remind_status IN (0, 1)
- AND is_deleted = 0
- AND (scan_lock = '' OR scan_lock IS NULL)
- `, scanBatch, now).Exec()
- if err != nil {
- return
- }
-
- // 3. 查询自己锁定成功的数据
- var list []models.PlatformScheduleReminder
- _, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("scan_lock", scanBatch).
- Filter("remind_status__in", 0, 1).
- Filter("is_deleted", 0).
- All(&list)
- if err != nil || len(list) == 0 {
- return
- }
-
- // 实例分发发送
- senders := map[string]ReminderSender{
- "SMS": &SMSSender{},
- "EMAIL": &EmailSender{},
- "BARK": &BarkSender{},
- "SITE_MSG": &SiteMsgSender{},
- }
-
- for i := range list {
- reminder := &list[i]
-
- // 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒")
- var schedule models.PlatformSchedule
- err := models.Orm.QueryTable(new(models.PlatformSchedule)).
- Filter("id", reminder.ScheduleID).
- One(&schedule)
- title := "日程提醒"
- content := "您有一个待处理的日程时间已到,请注意查收。"
- if err == nil {
- content = schedule.Content
- }
-
- sender, ok := senders[reminder.RemindChannel]
- if !ok {
- // 未知渠道,直接强制置为结束
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("id", reminder.ID).
- Update(map[string]interface{}{
- "remind_status": 2,
- "scan_lock": "",
- "update_time": time.Now(),
- })
- continue
- }
-
- // 执行发送
- ctx := context.Background()
- success, sendErr := sender.Send(ctx, reminder, title, content)
-
- // 3.2 记录发送流水日志
- sendResult := int8(0)
- var failReason *string
- if success {
- sendResult = 1
- } else if sendErr != nil {
- errStr := sendErr.Error()
- if len(errStr) > 255 {
- errStr = errStr[:255]
- }
- failReason = &errStr
- }
-
- logRow := &models.PlatformScheduleReminderSendLog{
- ReminderID: reminder.ID,
- SendTime: time.Now(),
- SendResult: sendResult,
- FailReason: failReason,
- }
- _, _ = models.Orm.Insert(logRow)
-
- // 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
- newSendCount := reminder.SendCount + 1
- newStatus := reminder.RemindStatus
-
- if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
- // 一次性发送:发送后直接置为结束
- newStatus = 2
- } else {
- // 重复发送渠道 EMAIL / BARK
- // 如果还没被 Ack,且没有达到 max_send_count,继续提醒
- if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
- newStatus = 1 // 提醒中
- // 更新下次发送时间
- reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
- } else {
- // 达到最大上限或者已 Ack
- newStatus = 2
- }
- }
-
- // 3.4 回写主表记录
- _, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
- Filter("id", reminder.ID).
- Update(map[string]interface{}{
- "SendCount": newSendCount,
- "NextRemindTime": reminder.NextRemindTime,
- "RemindStatus": newStatus,
- "ScanLock": "", // 释放扫描锁
- "UpdateTime": time.Now(),
- })
- }
-}
+package services
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "server/models"
+)
+
+type ReminderData struct {
+ RemindChannel string
+ ReceiverUserID uint64
+ ReceiverTarget *string
+ AckToken *string
+ Tid int
+}
+
+// ReminderSender 提醒发送接口
+type ReminderSender interface {
+ Send(ctx context.Context, reminder ReminderData, title, content string) (success bool, err error)
+}
+
+// SMSSender 短信发送实现
+type SMSSender struct{}
+
+func (s *SMSSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
+ backendURL, apiKey, err := getDefaultSystemSMSConfig()
+ if err != nil {
+ return false, err
+ }
+ phone := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ phone = *reminder.ReceiverTarget
+ } else {
+ var user models.AdminUser
+ if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
+ phone = *user.Phone
+ }
+ }
+ if phone == "" {
+ return false, fmt.Errorf("未配置手机号")
+ }
+
+ enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
+ payload := map[string]interface{}{
+ "phone": phone,
+ "content": title + ": " + content,
+ }
+ bs, _ := json.Marshal(payload)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
+ if err != nil {
+ return false, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Api-Key", apiKey)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ return true, nil
+}
+
+// EmailSender 邮件发送实现
+type EmailSender struct{}
+
+func (s *EmailSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
+ emails, err := ListSystemEmails()
+ if err != nil || len(emails) == 0 {
+ return false, fmt.Errorf("未配置系统邮箱")
+ }
+ emailCfg := emails[0]
+ if emailCfg.FromAddress == "" || emailCfg.Host == "" {
+ return false, fmt.Errorf("未配置系统邮箱")
+ }
+
+ toEmail := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ toEmail = *reminder.ReceiverTarget
+ } else {
+ var user models.AdminUser
+ if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
+ toEmail = *user.Email
+ }
+ }
+ if toEmail == "" {
+ return false, fmt.Errorf("未配置收件邮箱")
+ }
+
+ sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
+ ackToken := ""
+ if reminder.AckToken != nil {
+ ackToken = *reminder.AckToken
+ }
+
+ // 构造 HTML 邮件
+ htmlBody := fmt.Sprintf(`
+
+
日程提醒:%s
+
%s
+
+ `, title, content)
+
+ if ackToken != "" {
+ ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
+ htmlBody += fmt.Sprintf(`
+
+
确认收到后,系统将不再向您发送该日程的重复提醒。
+ `, ackURL)
+ }
+
+ htmlBody += "
"
+
+ cfg := SMTPConfig{
+ FromAddress: emailCfg.FromAddress,
+ Host: emailCfg.Host,
+ Port: emailCfg.Port,
+ Password: emailCfg.Password,
+ Encryption: emailCfg.Encryption,
+ Timeout: emailCfg.Timeout,
+ }
+ if emailCfg.FromName != nil {
+ cfg.FromName = *emailCfg.FromName
+ }
+
+ err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+// BarkSender Bark 推送实现
+type BarkSender struct{}
+
+func (s *BarkSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
+ deviceKey := ""
+ if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
+ deviceKey = *reminder.ReceiverTarget
+ } else {
+ deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
+ }
+ if deviceKey == "" {
+ return false, fmt.Errorf("Bark 设备 Key 未配置")
+ }
+
+ serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
+ sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
+ ackToken := ""
+ if reminder.AckToken != nil {
+ ackToken = *reminder.AckToken
+ }
+
+ baseURL := strings.TrimRight(serverURL, "/")
+ escapedTitle := url.PathEscape(title)
+ pushContent := content
+ if ackToken != "" {
+ pushContent += "\n确认收到请点击→"
+ }
+ escapedContent := url.PathEscape(pushContent)
+
+ barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
+
+ if ackToken != "" {
+ ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
+ // Bark 官方推送支持 url 参数
+ barkURL += "?url=" + url.QueryEscape(ackURL)
+ }
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ return true, nil
+}
+
+// SiteMsgSender 站内信发送实现
+type SiteMsgSender struct{}
+
+func (s *SiteMsgSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
+ now := time.Now()
+ msg := &models.SystemReminderList{
+ Title: title,
+ Content: content,
+ SenderID: 0,
+ SenderType: "system",
+ ReceiverID: reminder.ReceiverUserID,
+ ReceiverType: "tenant", // backend/租户用户
+ IsRead: 0,
+ CreateTime: &now,
+ TargetType: "tenant",
+ TargetTenantID: uint64(reminder.Tid),
+ }
+ _, err := models.Orm.Insert(msg)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+// generateUUID 生成一个安全的随机 UUID 字符
+func generateUUID() string {
+ b := make([]byte, 16)
+ _, _ = rand.Read(b)
+ b[6] = (b[6] & 0x0f) | 0x40
+ b[8] = (b[8] & 0x3f) | 0x80
+ return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
+}
+
+// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
+func StartReminderScheduler(stopChan chan struct{}) {
+ ticker := time.NewTicker(1 * time.Minute)
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+ scanAndSendReminders()
+ case <-stopChan:
+ ticker.Stop()
+ return
+ }
+ }
+ }()
+}
+
+func scanAndSendReminders() {
+ // 1. 生成唯一扫描批次号用于抢占锁定
+ scanBatch := generateUUID()
+ now := time.Now()
+
+ // 2. 抢占待处理的数据(乐观锁防并发重复发送)
+ _, err := models.Orm.Raw(`
+ UPDATE yz_backend_schedule_reminder
+ SET scan_lock = ?, update_time = NOW()
+ WHERE next_remind_time <= ?
+ AND remind_status IN (0, 1)
+ AND is_deleted = 0
+ AND (scan_lock = '' OR scan_lock IS NULL)
+ `, scanBatch, now).Exec()
+ if err != nil {
+ return
+ }
+
+ // 3. 查询自己锁定成功的数据
+ var list []models.BackendScheduleReminder
+ _, err = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("scan_lock", scanBatch).
+ Filter("remind_status__in", 0, 1).
+ Filter("is_deleted", 0).
+ All(&list)
+ if err != nil || len(list) == 0 {
+ return
+ }
+
+ // 实例分发发送
+ senders := map[string]ReminderSender{
+ "SMS": &SMSSender{},
+ "EMAIL": &EmailSender{},
+ "BARK": &BarkSender{},
+ "SITE_MSG": &SiteMsgSender{},
+ }
+
+ for i := range list {
+ reminder := &list[i]
+
+ // 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒")
+ var schedule models.BackendSchedule
+ err := models.Orm.QueryTable(new(models.BackendSchedule)).
+ Filter("id", reminder.ScheduleID).
+ One(&schedule)
+ title := "日程提醒"
+ content := "您有一个待处理的日程时间已到,请注意查收。"
+ if err == nil {
+ content = schedule.Content
+ }
+
+ sender, ok := senders[reminder.RemindChannel]
+ if !ok {
+ // 未知渠道,直接强制置为结束
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("id", reminder.ID).
+ Update(map[string]interface{}{
+ "remind_status": 2,
+ "scan_lock": "",
+ "update_time": time.Now(),
+ })
+ continue
+ }
+
+ // 执行发送
+ ctx := context.Background()
+ success, sendErr := sender.Send(ctx, ReminderData{
+ RemindChannel: reminder.RemindChannel,
+ ReceiverUserID: reminder.ReceiverUserID,
+ ReceiverTarget: reminder.ReceiverTarget,
+ AckToken: reminder.AckToken,
+ Tid: reminder.Tid,
+ }, title, content)
+
+ // 3.2 记录发送流水日志
+ sendResult := int8(0)
+ var failReason *string
+ if success {
+ sendResult = 1
+ } else if sendErr != nil {
+ errStr := sendErr.Error()
+ if len(errStr) > 255 {
+ errStr = errStr[:255]
+ }
+ failReason = &errStr
+ }
+
+ logRow := &models.BackendScheduleReminderSendLog{
+ Tid: reminder.Tid,
+ ReminderID: reminder.ID,
+ SendTime: time.Now(),
+ SendResult: sendResult,
+ FailReason: failReason,
+ }
+ _, _ = models.Orm.Insert(logRow)
+
+ // 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
+ newSendCount := reminder.SendCount + 1
+ newStatus := reminder.RemindStatus
+
+ if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
+ // 一次性发送:发送后直接置为结束
+ newStatus = 2
+ } else {
+ // 重复发送渠道 EMAIL / BARK
+ // 如果还没被 Ack,且没有达到 max_send_count,继续提醒
+ if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
+ newStatus = 1 // 提醒中
+ // 更新下次发送时间
+ reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute)
+ } else {
+ // 达到最大上限或者已 Ack
+ newStatus = 2
+ }
+ }
+
+ // 3.4 回写主表记录
+ _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
+ Filter("id", reminder.ID).
+ Update(map[string]interface{}{
+ "SendCount": newSendCount,
+ "NextRemindTime": reminder.NextRemindTime,
+ "RemindStatus": newStatus,
+ "ScanLock": "", // 释放扫描锁
+ "UpdateTime": time.Now(),
+ })
+ }
+}
diff --git a/platform/package-lock.json b/platform/package-lock.json
index 239b5b1..5990ee2 100644
--- a/platform/package-lock.json
+++ b/platform/package-lock.json
@@ -9,24 +9,14 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
- "@tiptap/extension-color": "^3.27.4",
- "@tiptap/extension-highlight": "^3.27.4",
- "@tiptap/extension-image": "^3.27.4",
- "@tiptap/extension-link": "^3.27.4",
- "@tiptap/extension-placeholder": "^3.27.4",
- "@tiptap/extension-subscript": "^3.27.4",
- "@tiptap/extension-superscript": "^3.27.4",
- "@tiptap/extension-table": "^3.27.4",
- "@tiptap/extension-table-cell": "^3.27.4",
- "@tiptap/extension-table-header": "^3.27.4",
- "@tiptap/extension-table-row": "^3.27.4",
- "@tiptap/extension-text-align": "^3.27.4",
- "@tiptap/extension-text-style": "^3.27.4",
- "@tiptap/extension-typography": "^3.27.4",
- "@tiptap/extension-underline": "^3.27.4",
- "@tiptap/pm": "^3.27.4",
- "@tiptap/starter-kit": "^3.27.4",
- "@tiptap/vue-3": "^3.27.4",
+ "@tiptap/extension-color": "^3.28.0",
+ "@tiptap/extension-image": "^3.28.0",
+ "@tiptap/extension-link": "^3.28.0",
+ "@tiptap/extension-text-align": "^3.28.0",
+ "@tiptap/extension-text-style": "^3.28.0",
+ "@tiptap/extension-underline": "^3.28.0",
+ "@tiptap/starter-kit": "^3.28.0",
+ "@tiptap/vue-3": "^3.28.0",
"@umoteam/editor": "^10.2.1",
"axios": "^1.13.1",
"chart": "^0.1.2",
@@ -1174,49 +1164,49 @@
]
},
"node_modules/@tiptap/core": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.27.4.tgz",
- "integrity": "sha512-8W/GwlEn0JwNdpyVfTWcXwHYUpj9BWwO++YxtizmgjJzlwigSh7/xLVJMwVykuQHQ2fCq5rkUvmBRtpHOMLUQA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.28.0.tgz",
+ "integrity": "sha512-gUuD5WAYfbDxNSSJya/emh2KSzXZXLUYKW4fEnc1AQ5FE2twzh4LJ9UlKFIawigrUCAksWI5Fy1hRbv+5m4ZdQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/pm": "3.27.4"
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-blockquote": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-blockquote/-/extension-blockquote-3.27.4.tgz",
- "integrity": "sha512-d1tOHgP3R5cOE+Ot8qL/dkLXRByajgn+j6cCXHqDtmJO2wsK9knmbKQ0SEjbKrU6OgHrTnY/EotNxBEBW9HGoA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-blockquote/-/extension-blockquote-3.28.0.tgz",
+ "integrity": "sha512-y8Xi6Z3AQLXedz0J8Z3kDsu7SKBmL50gKVXx3RK1Oo1cuCGhNChm3syChkAz3PLAbrPUM5rvJDSZdF2jUrDnng==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-bold": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-bold/-/extension-bold-3.27.4.tgz",
- "integrity": "sha512-wTtJUUAxCAZ01ICH2DNlOBzzHKRQ1ZST8aRYtIhBPzqEUhnJaKGcjnDB4X49fqPi48iXaPxzhsInDl+rVUujWg==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-bold/-/extension-bold-3.28.0.tgz",
+ "integrity": "sha512-JhZQmr0AU741bOwjVMfuwJdK4g0TQwwPbeca9aqKHv5zvZw4i4G9G6fESVyMFc3Yag1ffpnq5EtNldHKTnMhlw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.27.4.tgz",
- "integrity": "sha512-Poy7xwcD3POG5ew/TW7mYXv7m++vCchvHxPUqIfnTxBxvvvqDZkPYFWZS1lvPrSBtm1DcfUTQAgVutM5NDZ99Q==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.28.0.tgz",
+ "integrity": "sha512-7AUNoHj2K4XLKCgW4uspeD/ENejPu2BeHvLTsiOoIO+XXDNSi2j0bbaIS8f2s/qnFirscwp/sbznp1qph/76qg==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1227,48 +1217,48 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-bullet-list": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.4.tgz",
- "integrity": "sha512-rvja0N1RnwGJAVwDdbUfDIJ4NoT+KjPFaZudKiPuEMfMHfbqe4xcbbC2hsfs61JNcl2xmx+ohV6lzD9YxxJl1w==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.28.0.tgz",
+ "integrity": "sha512-dSVuNiH7PFMmWGkNER9vfUb5bXUHDARvHbJ3l+APEb+ZiusVN1KFdM3nd/RsW/Rt5Gd3XwlIEU/c2Uf7cxYDcw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extension-list": "3.27.4"
+ "@tiptap/extension-list": "3.28.0"
}
},
"node_modules/@tiptap/extension-code": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-code/-/extension-code-3.27.4.tgz",
- "integrity": "sha512-aPc7opCR1ylK4m4c2lsjLsGpEBD1fLQQKWd5PbZiJvrTF8gkdGZlYLt9A6VukpxeJyHhb22Jaj4fxgKmGMeTtw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-code/-/extension-code-3.28.0.tgz",
+ "integrity": "sha512-AUw2Acof3CQE6Q6Y22saK4lyg0nkeJ4XXniU65TdAi/9TE66TGiO4NQJJNNPgu8+VMEwl5j8VsIFK8sfTcNYtg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-code-block": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-code-block/-/extension-code-block-3.27.4.tgz",
- "integrity": "sha512-a5caWfWN6Z6usy48vzJDDOhWoA6+rFFCHGpQM7jXn/7rRzYPcvBzTZUGptjEbltj4YqtrQ2tVwTJcCtbb+mknA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-code-block/-/extension-code-block-3.28.0.tgz",
+ "integrity": "sha512-bHITDzT0umTLh+SiEVQzIXkCMt/TzbPM1+HQy9ZxgQDHAJj/vdmfNAnP3RCOrz4lETXyhNQ2b6kxeu3lWckxyA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-code-block-lowlight": {
@@ -1305,16 +1295,16 @@
}
},
"node_modules/@tiptap/extension-color": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-color/-/extension-color-3.27.4.tgz",
- "integrity": "sha512-uGbgErKGKO4OTBGqnXOA1CGXa3IqoBaiPBGcHu/px01fS8EVFu0A7q5HS6vxFiMK6qW5x71sEpCA+FUINMAb3g==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-color/-/extension-color-3.28.0.tgz",
+ "integrity": "sha512-YbUMUdrgU04GDYoii1wuFVkiTSQoaEQh04kwgQayEQ46aUBEnnaJitWdp39aGMWYTIXD/c9K9cr5Vg8GSKcVxA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extension-text-style": "3.27.4"
+ "@tiptap/extension-text-style": "3.28.0"
}
},
"node_modules/@tiptap/extension-details": {
@@ -1333,16 +1323,16 @@
}
},
"node_modules/@tiptap/extension-document": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-document/-/extension-document-3.27.4.tgz",
- "integrity": "sha512-7nAqgfkgb9HADBeCTnOHuTiyZuxfxvMPT3nH4OZeY+cmtkI1On3QffqlmtcUPvNbkhT3o9ehA1hVfCnQ1Ye4LQ==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-document/-/extension-document-3.28.0.tgz",
+ "integrity": "sha512-5SAKtlB1Tr/eZFhISL86HqmUup6rItvPtuPyRjmZo/VgbMwXEwiyAh1sMgFp5VNaeSMM14vJSyQpjhSaOmaBqg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-drag-handle": {
@@ -1382,22 +1372,22 @@
}
},
"node_modules/@tiptap/extension-dropcursor": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.4.tgz",
- "integrity": "sha512-RiZasQJuUTUO3aME16Bn8eJH7cYnvhT5JCFDFq0ya/1iFI9wUQA2NJC5tb5TrZ74+sQwkYU9VzexnchM481Y9w==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.28.0.tgz",
+ "integrity": "sha512-JA+dXQjjfJBpD0nVsZeddGVXRsmanESM9+8CtM35hI9wJnYMXay/B+5GUhswO20zhT3zhB2ZOTGe9knu6A8nIQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extensions": "3.27.4"
+ "@tiptap/extensions": "3.28.0"
}
},
"node_modules/@tiptap/extension-floating-menu": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.27.4.tgz",
- "integrity": "sha512-tnZywwoNDuEcUZmYYIztXl3PpIKUq+gKeaYPuZhpYEVTThU44tzK3ZuFOmd+qf2aAa1MQwxKWqUuLpNK77bwNw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.28.0.tgz",
+ "integrity": "sha512-59SECvJq3pQfeJBuydEdhQqrpl0oDlk8N1Ovs1Si3/fJa6rEQAJB2zIvcpBHky9Z+5JodI2eueDnv8eimiyGbg==",
"license": "MIT",
"optional": true,
"funding": {
@@ -1406,87 +1396,74 @@
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-gapcursor": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.4.tgz",
- "integrity": "sha512-svLwSKcFhzpcJeXvxxKkRFuQpykmXrQefVhEsaXq0L95yJIIAGKMRmQC3mxKdzL2j0P9cY7V41bNVSyOAyvclw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.28.0.tgz",
+ "integrity": "sha512-Wo73kM4q8z4Va9i4+0n1cO0UEMVUVBHPqM6cAqEYXjB4T48LQkKjRsiQydCezm185rQAsmm1dyi72gtvVQfdMg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extensions": "3.27.4"
+ "@tiptap/extensions": "3.28.0"
}
},
"node_modules/@tiptap/extension-hard-break": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-hard-break/-/extension-hard-break-3.27.4.tgz",
- "integrity": "sha512-W+Z9pmDgqjbdu3NeZOQrzA15iM4w60Yd8l2CYzxcdApPVIfYzb2S3a7+u1RqW9wnTYb6xyZjASmFNfxXS4P4cg==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-hard-break/-/extension-hard-break-3.28.0.tgz",
+ "integrity": "sha512-JHIFjX1luJgQ4VFEkIeXZpbc54Qiw+69P8FmlazYTh7AIJ6iLCpMFgQFELnivEAZ+/vS0lMPQubg0rCeM3UrHw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-heading": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-heading/-/extension-heading-3.27.4.tgz",
- "integrity": "sha512-RgvpxzuYk6QEK+az+eiXpWvGlUso42zNcGnjyUrvskoZjS47MbhSg8ylRYQSRtXE0ETlXhAx4J7iGlGr72kyIw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-heading/-/extension-heading-3.28.0.tgz",
+ "integrity": "sha512-rKjGG/Ik2lNaXBNVmONEDm5DBfGDLM1NWgSf5RRfBISrH8Lm1xqFruOB9tIaB0YUoSV3SBOiwTF9svmzvKSoRA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-highlight": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-highlight/-/extension-highlight-3.27.4.tgz",
- "integrity": "sha512-STRX1qJLhTZslBF8fEE5qpTGrFd/g7Ufidjxt84p0uT8FrtcbfPUwyweeYwhZp7Iw8n+qODGYnheJTOpfaW2JA==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.4.tgz",
- "integrity": "sha512-2eQU/55nE5mhMJHALtLMuBL3dcVJUDVVT7n+uZYMaYE63BtCvC4VS08YLFSR7JZSVJIlgVAmdt5nAw0B+rEPNA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.28.0.tgz",
+ "integrity": "sha512-neBCMWprkppQUoLWyeJZDHqBw+xKX2oNhLD9MbFlhKIr092zgfP4mpCvQnSkn1OHl156KzZMp070+NyLBjgQ7A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-image": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-image/-/extension-image-3.27.4.tgz",
- "integrity": "sha512-yQ8CazyOL4z1/NbV1NLGv6DvchVhOXHH3uQ7md5VX/IGZruFpnm8IpF9MDpdAUxUFmTsXJDYyO2lWGO1PYWG8A==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-image/-/extension-image-3.28.0.tgz",
+ "integrity": "sha512-aKGCdEjyujUcQ6tEtq4EPbKxEC16QsKapv3DeZs1c/UHFiEUalbbPHBnxjGnupiLz75IR+07GYqBoZ81kSCxiA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-invisible-characters": {
@@ -1505,22 +1482,22 @@
}
},
"node_modules/@tiptap/extension-italic": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-italic/-/extension-italic-3.27.4.tgz",
- "integrity": "sha512-PeZT4XbyxAp7Lqo/hfA1k5LI27g1RlgS+YgXp2CeHXIrUfSpO5HlZXh02Bvb0pOdl3RFw2tEKtlHzjt8Y1+Nwg==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-italic/-/extension-italic-3.28.0.tgz",
+ "integrity": "sha512-Yur/ELz6dNVKQC7m8wGjtoLFxamGjJmA0rUEEOacTH6J39aFmcSxYkEGNDkYHbZFyHFYqbej6eI3VE0h08O0mg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-link": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-link/-/extension-link-3.27.4.tgz",
- "integrity": "sha512-6K/FkNwMLWWQbNWKlycrUPTN7YcyVFdFwZncoBXe5WyarRjLTGw7ywafnCI9PDIWSq7ttzVL4NgjN2IN8kBXww==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-link/-/extension-link-3.28.0.tgz",
+ "integrity": "sha512-hy/PqSeTyl317yseFcHGE+3XVfQKQYaL4uZuj27xlrcO7MZ15drt1h9sUZy1FTBF3mr8676pQu7aoEm/Ww4ZRA==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.3.3"
@@ -1530,48 +1507,48 @@
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-list": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-list/-/extension-list-3.27.4.tgz",
- "integrity": "sha512-A0BgmRO1RE0yLCx9w7GQITtKfS9wLE5cdngSYDiSpwulcXJhJjKm5mZ4OUZmks2VN4HO5jMl2BWCGt2NSDhA+w==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-list/-/extension-list-3.28.0.tgz",
+ "integrity": "sha512-zQ66i5DuhVOndmZ0d7H475Y5Yb+BMx+zfCjFkCzEc6WVef5MumtxBVTkGLQ0ibxUaD71s4QQZbjHWagpaTg0eQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/extension-list-item": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-item/-/extension-list-item-3.27.4.tgz",
- "integrity": "sha512-z5TVuPw2mkK0B/x+gFg3uUV7tBdaElDFg0zVgnXZCqlSVTLfIyInOOnG5LTWoAd9BdzBjGrzE3PohDcLVDDGBQ==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-item/-/extension-list-item-3.28.0.tgz",
+ "integrity": "sha512-GbXrnMac6knSetZY33NHwOrgXFPqH28uCklQqmOZQlsrdGqezDKk31qoEMr4GsKW9n/lViAeuc07oIzwLwCMng==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extension-list": "3.27.4"
+ "@tiptap/extension-list": "3.28.0"
}
},
"node_modules/@tiptap/extension-list-keymap": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.4.tgz",
- "integrity": "sha512-on7JNDi7Eqz7UdZeZdiO83bQHo0flVDHzjmtR+v/nrCGW9H15D3CHs5+4ozLDiCvTK8tbkBuut/l9AWNxcCE/Q==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.28.0.tgz",
+ "integrity": "sha512-u9Wks8c/eQAv0RkULNggOyTKDD69WVbcV9H5cbasPDUbq5XbEFVa9ajxhEGfMmQ5et3EX0QL8qBi50K0GqbIjA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extension-list": "3.27.4"
+ "@tiptap/extension-list": "3.28.0"
}
},
"node_modules/@tiptap/extension-mathematics": {
@@ -1619,123 +1596,42 @@
}
},
"node_modules/@tiptap/extension-ordered-list": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.4.tgz",
- "integrity": "sha512-bHwLiof0FqJfWzB0act7oEKMTZatEKQ4IYCvmyF5EktjMs4kxEatkPp4Yx/1LSYSjLy1MMT7oLELyaz2FFYyXA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.28.0.tgz",
+ "integrity": "sha512-OZNYrKKL9D01ySOujlNbKlLS9/3wGgKNYeoHZUg0e+Ta8WPVvL3W1zH6TgiU5sF2ZSGUBoq3w7mdaO/iROlUkw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/extension-list": "3.27.4"
+ "@tiptap/extension-list": "3.28.0"
}
},
"node_modules/@tiptap/extension-paragraph": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-paragraph/-/extension-paragraph-3.27.4.tgz",
- "integrity": "sha512-8Dnr1J5s/s4XYYuEF3b784NnCxLjXOlQpmGyXRxTAzW7JaOP08tIUJWVNvSMekfXc2vXa33HUbqjxyyWZEQ6LQ==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-paragraph/-/extension-paragraph-3.28.0.tgz",
+ "integrity": "sha512-8UMlQIjzqf6r2hSJ/VeJ00RqaOrOB1J5rTk02Vcw2pYeHkOqp+B0ff0HFu4abT9x1q4oXrBAgMsxRtgh/kR14Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-placeholder": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-placeholder/-/extension-placeholder-3.27.4.tgz",
- "integrity": "sha512-7hBoFLeddCv1WzkqB0x3coZ1Hp9WZ9wLoRXIUtUhRKMpzFq2IlTtW1iw88g9pTJnL98bCCElN4DZ4mYtaQvmgA==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/extensions": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-strike": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-strike/-/extension-strike-3.27.4.tgz",
- "integrity": "sha512-8OXwcPKuV3ToBBgyvDxH1jQdObK5FIKCGiyIim6qNWiOpi9BhM3XYD+aO1khjv8qIjtoI/DYbizF4ewj09fX2g==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-strike/-/extension-strike-3.28.0.tgz",
+ "integrity": "sha512-VVj2ZZU9QYqiHLcjqMqRYvuHSsokj/AgUl+6TzLrKjlWwyZ18D4H2vkyI0g70iVTKnUpZ3sEy8WA/rqjgBjqBg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-subscript": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-subscript/-/extension-subscript-3.27.4.tgz",
- "integrity": "sha512-gs0AexP7GsE9vgdkwhqYgrKu7kD96PjmgCorsGLdA60m71eTavm6j8NN1m1nvcDPO1sTpP8ACs2mhipj+WS5Iw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-superscript": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-superscript/-/extension-superscript-3.27.4.tgz",
- "integrity": "sha512-krlpnLeNBF6hVbQDrkuoXiy52v5YctoGPkLnxmftYNtloVrikRpgpetTRsFh0P6hLwv6g/g8SzoM23iMBB3S4A==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-table": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-table/-/extension-table-3.27.4.tgz",
- "integrity": "sha512-ejQjqt8GjUn4YswG/SsiLr/W3LZApZGUEDW0N7NoOduE0dBZ/pVJHPuqWu33kK+phJjSNCIN+bSAkoWE01rZSw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-table-cell": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-table-cell/-/extension-table-cell-3.27.4.tgz",
- "integrity": "sha512-1B7J4ZiaXaGxT2IB3hrwtz0433bFVSWsDB+B125vt2DZQr9bgdX40GLesSPlymqOdMxKJwksCPMan2ON2LkVAg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/extension-table": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-table-header": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-table-header/-/extension-table-header-3.27.4.tgz",
- "integrity": "sha512-V/O690Z6VcHMAWDfgVFiOCwx3eE2/HM2gH+Fp0eQe3WnajQj2DPViW4VWR/rNVl3DspHoL/EU+eEsnhcvO34Vw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/extension-table": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-table-of-contents": {
@@ -1768,82 +1664,56 @@
"uuid": "dist/bin/uuid"
}
},
- "node_modules/@tiptap/extension-table-row": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-table-row/-/extension-table-row-3.27.4.tgz",
- "integrity": "sha512-2RTJtcy90Tc2+HpW1JyKMTwg/zz4u6hHof4CTcJS/eXLk3g1L60DDzYvz4GzBxQvsOT3QVcbaU9QOk9A3Sx7Fg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/extension-table": "3.27.4"
- }
- },
"node_modules/@tiptap/extension-text": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-text/-/extension-text-3.27.4.tgz",
- "integrity": "sha512-lKQH/hP4FBXsziHypd6Ywj8JFvMLM5GVkK1xsH6yApNuXbHq95rd42ZOYWpYILIBib7tlaz93z61d74UrJiuiw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-text/-/extension-text-3.28.0.tgz",
+ "integrity": "sha512-Jqp1LgfY1mnp4TQHoy+vnHyfn6qnnAM6nHgGwbY5zA8b/Xf1Etll6pziTx9p1J1qcfAgSrnjOyAmA++H7VQqww==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-text-align": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-align/-/extension-text-align-3.27.4.tgz",
- "integrity": "sha512-ArfL7GLOXCSmrmiBiaWwAf7RPHXDdxLGPru29qKDLQDthjXcNOdlwlPBbolRu7mXwlSmaYpqWGrG75/AEat3mw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-align/-/extension-text-align-3.28.0.tgz",
+ "integrity": "sha512-P+KQMTtCpLFq9dRvy/fbyu2BGALulT/1HtsLbpFG4tKATaKa+cFnJ7rLZF7b3AdAMDNtXdnivxAtixAMBt2QWw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-text-style": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-style/-/extension-text-style-3.27.4.tgz",
- "integrity": "sha512-Wmj64TQXY85gc7lUNbubW32sDCnVOJlpGraMeARRC9Z0EKBX1JpAxddo/64F17bn3kzLxXVszBFyLRcNgTdU2g==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-style/-/extension-text-style-3.28.0.tgz",
+ "integrity": "sha512-UreMyqZSv770+CXJK/l1qeHUdaNMQAVkxSOPWpiY9FiT8B4OQyjA7WBOlWrzbvVTzfJvXWk2Y3SWgVRx+/Cd9A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
- }
- },
- "node_modules/@tiptap/extension-typography": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-typography/-/extension-typography-3.27.4.tgz",
- "integrity": "sha512-Sn4HZOKpcFR0m7msSGWNEunRobiXk8bs78e8+qJHa3yoxPfXsFoZySEgbD6TO7JjzQtVG927FQLCXQTzK7Q0Tg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/ueberdosis"
- },
- "peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-underline": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extension-underline/-/extension-underline-3.27.4.tgz",
- "integrity": "sha512-nRJGvRyEXDtINlHTW+C2oWcL3vmX1URVxAPpkD3Zwn5Rb/vEeOU/pk/w97I0iid816MR4iVbvl1XhbUVegK9gQ==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extension-underline/-/extension-underline-3.28.0.tgz",
+ "integrity": "sha512-rwxCS6vTh2DJkNIYQX7JSrrRSmm76e2y48aZeuZO5ShkvLWMuJ3/zqDHRxAQVDiJhuE2Cp8NrTmPYlUc9YrT0A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4"
+ "@tiptap/core": "3.28.0"
}
},
"node_modules/@tiptap/extension-unique-id": {
@@ -1877,23 +1747,23 @@
}
},
"node_modules/@tiptap/extensions": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/extensions/-/extensions-3.27.4.tgz",
- "integrity": "sha512-d8opkg2iGtVwJmNGIqv0blfRxnvWOJp1brz+Z8CsP4ojSS2ZtaE46d6JSQ5OeJ7nMpjhT+9wh4UQcA7OSEO59w==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/extensions/-/extensions-3.28.0.tgz",
+ "integrity": "sha512-DJT1khCK+O/pT1gQlAnoKAx6zwDkgv7GtnhSfkqm1/4KHC3x+SSJnBIgBl9oGHymA+DxWbW1EULU4V3d/kT2uQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4"
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0"
}
},
"node_modules/@tiptap/pm": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.27.4.tgz",
- "integrity": "sha512-UB8lcyomfWk7YGI2PZKNqcYXfyRA+PFj+QntlsUXyrsiA5JJIaE8SHKYjxKlGG/xtW3EtPm1b0p38T9Mk4xiFw==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.28.0.tgz",
+ "integrity": "sha512-ALcpwZMUdat9gjJKlpscpoqXStoLhU246LPEVBDvJdIsoUKvUu3MrzfXik2Y8mtSGfhjtm9O2TRkWxQiFVMwsQ==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.4.1",
@@ -1916,35 +1786,35 @@
}
},
"node_modules/@tiptap/starter-kit": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/starter-kit/-/starter-kit-3.27.4.tgz",
- "integrity": "sha512-/sb6rFxNt5BO4hWpUwvHh+Yh1kNyCQuuz3oDpGef5HUUjSdu9p9rfNiHWIUKBadK8VXuw5es7N+UlZ4hma+gvA==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/starter-kit/-/starter-kit-3.28.0.tgz",
+ "integrity": "sha512-pqvFpValV+ONtlu3l4s73ROm/tEjoGMmt6uHmSJaLbqTcMHkdWuR3flJ/5brr4IDHe1foxmNicMbRxlje7yBYw==",
"license": "MIT",
"dependencies": {
- "@tiptap/core": "^3.27.4",
- "@tiptap/extension-blockquote": "^3.27.4",
- "@tiptap/extension-bold": "^3.27.4",
- "@tiptap/extension-bullet-list": "^3.27.4",
- "@tiptap/extension-code": "^3.27.4",
- "@tiptap/extension-code-block": "^3.27.4",
- "@tiptap/extension-document": "^3.27.4",
- "@tiptap/extension-dropcursor": "^3.27.4",
- "@tiptap/extension-gapcursor": "^3.27.4",
- "@tiptap/extension-hard-break": "^3.27.4",
- "@tiptap/extension-heading": "^3.27.4",
- "@tiptap/extension-horizontal-rule": "^3.27.4",
- "@tiptap/extension-italic": "^3.27.4",
- "@tiptap/extension-link": "^3.27.4",
- "@tiptap/extension-list": "^3.27.4",
- "@tiptap/extension-list-item": "^3.27.4",
- "@tiptap/extension-list-keymap": "^3.27.4",
- "@tiptap/extension-ordered-list": "^3.27.4",
- "@tiptap/extension-paragraph": "^3.27.4",
- "@tiptap/extension-strike": "^3.27.4",
- "@tiptap/extension-text": "^3.27.4",
- "@tiptap/extension-underline": "^3.27.4",
- "@tiptap/extensions": "^3.27.4",
- "@tiptap/pm": "^3.27.4"
+ "@tiptap/core": "^3.28.0",
+ "@tiptap/extension-blockquote": "^3.28.0",
+ "@tiptap/extension-bold": "^3.28.0",
+ "@tiptap/extension-bullet-list": "^3.28.0",
+ "@tiptap/extension-code": "^3.28.0",
+ "@tiptap/extension-code-block": "^3.28.0",
+ "@tiptap/extension-document": "^3.28.0",
+ "@tiptap/extension-dropcursor": "^3.28.0",
+ "@tiptap/extension-gapcursor": "^3.28.0",
+ "@tiptap/extension-hard-break": "^3.28.0",
+ "@tiptap/extension-heading": "^3.28.0",
+ "@tiptap/extension-horizontal-rule": "^3.28.0",
+ "@tiptap/extension-italic": "^3.28.0",
+ "@tiptap/extension-link": "^3.28.0",
+ "@tiptap/extension-list": "^3.28.0",
+ "@tiptap/extension-list-item": "^3.28.0",
+ "@tiptap/extension-list-keymap": "^3.28.0",
+ "@tiptap/extension-ordered-list": "^3.28.0",
+ "@tiptap/extension-paragraph": "^3.28.0",
+ "@tiptap/extension-strike": "^3.28.0",
+ "@tiptap/extension-text": "^3.28.0",
+ "@tiptap/extension-underline": "^3.28.0",
+ "@tiptap/extensions": "^3.28.0",
+ "@tiptap/pm": "^3.28.0"
},
"funding": {
"type": "github",
@@ -1966,22 +1836,22 @@
}
},
"node_modules/@tiptap/vue-3": {
- "version": "3.27.4",
- "resolved": "https://registry.npmmirror.com/@tiptap/vue-3/-/vue-3-3.27.4.tgz",
- "integrity": "sha512-fTz21viWZlpZz7PPdHJde+kzYmgRXBivVLoC2KGEeltqsREa8JR0v/eUDbzeMjj6AIajlNDtmn96I3h14gZJLg==",
+ "version": "3.28.0",
+ "resolved": "https://registry.npmmirror.com/@tiptap/vue-3/-/vue-3-3.28.0.tgz",
+ "integrity": "sha512-twb3T6hofM0ajncCSgrLYIjfn86WfR2NVXteurdNOcnkJCIMTij5OhO/xn0z7y8+lp3TMkF6H5kajUpivBpnzw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"optionalDependencies": {
- "@tiptap/extension-bubble-menu": "^3.27.4",
- "@tiptap/extension-floating-menu": "^3.27.4"
+ "@tiptap/extension-bubble-menu": "^3.28.0",
+ "@tiptap/extension-floating-menu": "^3.28.0"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
- "@tiptap/core": "3.27.4",
- "@tiptap/pm": "3.27.4",
+ "@tiptap/core": "3.28.0",
+ "@tiptap/pm": "3.28.0",
"vue": "^3.0.0"
}
},
@@ -7229,7 +7099,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
diff --git a/platform/package.json b/platform/package.json
index f4ee1bb..1dfa4e5 100644
--- a/platform/package.json
+++ b/platform/package.json
@@ -11,6 +11,14 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
+ "@tiptap/extension-color": "^3.28.0",
+ "@tiptap/extension-image": "^3.28.0",
+ "@tiptap/extension-link": "^3.28.0",
+ "@tiptap/extension-text-align": "^3.28.0",
+ "@tiptap/extension-text-style": "^3.28.0",
+ "@tiptap/extension-underline": "^3.28.0",
+ "@tiptap/starter-kit": "^3.28.0",
+ "@tiptap/vue-3": "^3.28.0",
"@umoteam/editor": "^10.2.1",
"axios": "^1.13.1",
"chart": "^0.1.2",
diff --git a/platform/src/views/dashboard/index.vue b/platform/src/views/dashboard/index.vue
index 9e83654..e1bcac7 100644
--- a/platform/src/views/dashboard/index.vue
+++ b/platform/src/views/dashboard/index.vue
@@ -379,9 +379,9 @@ onMounted(async () => {
.welcome-section {
margin-bottom: 16px;
padding: 32px;
- background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
- box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.25);
.welcome-content {
.welcome-title {
@@ -408,63 +408,65 @@ onMounted(async () => {
.stat-card {
background: var(--el-bg-color);
- border-radius: 12px;
- padding: 24px;
+ border-radius: 16px;
+ padding: 28px;
display: flex;
align-items: center;
- gap: 16px;
+ gap: 20px;
border: 1px solid var(--el-border-color-lighter);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
- transition: all 0.3s;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+ transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
- transform: translateY(-4px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ transform: translateY(-6px);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
border-color: var(--el-color-primary-light-7);
}
.stat-icon-wrapper {
- width: 56px;
- height: 56px;
- border-radius: 12px;
+ width: 60px;
+ height: 60px;
+ border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.el-icon {
color: #fff;
+ font-size: 28px;
}
}
&.income .stat-icon-wrapper {
- background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.users .stat-icon-wrapper {
- background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.orders .stat-icon-wrapper {
- background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.active .stat-icon-wrapper {
- background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
&.knowledge .stat-icon-wrapper {
- background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.employees .stat-icon-wrapper {
- background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.tenants .stat-icon-wrapper {
- background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-content {
@@ -692,15 +694,16 @@ onMounted(async () => {
align-items: center;
justify-content: center;
color: #fff;
+ font-weight: 500;
&.operation {
- background-color: rgba(79, 132, 255, 0.2);
- color: #4f84ff;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
}
&.access {
- background-color: rgba(85, 190, 130, 0.2);
- color: #55be82;
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+ color: #fff;
}
}
diff --git a/platform/vite.config.js b/platform/vite.config.js
index a82e8a7..ee05411 100644
--- a/platform/vite.config.js
+++ b/platform/vite.config.js
@@ -29,14 +29,14 @@ export default defineConfig({
server: {
host: "127.0.0.1",
port: 4000,
- // 开发时前端在 5000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=8081)
+ // 开发时前端在 4000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000)
proxy: {
"/platform": {
- target: "http://127.0.0.1:8081",
+ target: "http://127.0.0.1:9000",
changeOrigin: true,
},
"/backend": {
- target: "http://127.0.0.1:8081",
+ target: "http://127.0.0.1:9000",
changeOrigin: true,
},
},
diff --git a/uniapp/manifest.json b/uniapp/manifest.json
index ad7fa81..64600f2 100644
--- a/uniapp/manifest.json
+++ b/uniapp/manifest.json
@@ -1,13 +1,13 @@
{
"name" : "uniapp",
- "appid" : "",
+ "appid" : "__UNI__D80959E",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
- "uniStatistics": {
- "enable": false,
- "debug": false
+ "uniStatistics" : {
+ "enable" : false,
+ "debug" : false
},
/* 5+App特有相关 */
"app-plus" : {
@@ -57,7 +57,9 @@
"mp-weixin" : {
"appid" : "",
"setting" : {
- "urlCheck" : false
+ "urlCheck" : false,
+ "minified" : true,
+ "postcss" : true
},
"usingComponents" : true,
"mergeVirtualHostAttributes" : true
@@ -74,5 +76,8 @@
"usingComponents" : true,
"mergeVirtualHostAttributes" : true
},
- "vueVersion" : "3"
+ "vueVersion" : "3",
+ "h5" : {
+ "title" : "云泽系统"
+ }
}
diff --git a/uniapp/pages/dashboard/dashboard.vue b/uniapp/pages/dashboard/dashboard.vue
index 88fc78e..2bdd7c6 100644
--- a/uniapp/pages/dashboard/dashboard.vue
+++ b/uniapp/pages/dashboard/dashboard.vue
@@ -1,17 +1,8 @@
@@ -84,18 +75,6 @@ import AppTabbar from '@/components/AppTabbar.vue'
const user = ref(null)
-const greeting = computed(() => {
- const hour = new Date().getHours()
- if (hour < 12) return '早上好'
- if (hour < 18) return '下午好'
- return '晚上好'
-})
-
-const avatarText = computed(() => {
- const name = user.value?.nickname || '云'
- return name.charAt(0).toUpperCase()
-})
-
const noteCount = ref(0)
const scheduleCount = ref(0)
const schedulePending = ref(0)
@@ -199,60 +178,25 @@ function onActivityTap(item) {
.header {
flex-shrink: 0;
- padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 24rpx;
- background: $color-card;
- box-shadow: $shadow-header;
-}
-
-.header-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 28rpx;
-}
-
-.greeting {
- display: block;
- font-size: 26rpx;
- color: $color-text-muted;
-}
-
-.username {
- display: block;
- font-size: 40rpx;
- font-weight: 600;
- color: $color-text;
- margin-top: 4rpx;
-}
-
-.avatar {
- width: 88rpx;
- height: 88rpx;
- border-radius: 50%;
- background: $color-primary-bg;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.avatar-text {
- font-size: 34rpx;
- font-weight: 600;
- color: $color-primary;
+ padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 20rpx;
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ box-shadow: 0 4rpx 16rpx rgba(79, 172, 254, 0.15);
+ position: relative;
}
.search-bar {
display: flex;
align-items: center;
gap: 16rpx;
- background: $color-bg-page;
- border-radius: $radius-md;
- padding: 20rpx 24rpx;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: $radius-full;
+ padding: 16rpx 24rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.search-text {
font-size: 28rpx;
- color: $color-text-muted;
+ color: rgba(255, 255, 255, 0.7);
}
.main-scroll {
@@ -344,8 +288,14 @@ function onActivityTap(item) {
}
.quick-icon {
- @include icon-box(88rpx);
- border-radius: $radius-lg;
+ width: 88rpx;
+ height: 88rpx;
+ border-radius: 12rpx;
+ background: linear-gradient(135deg, rgba(79, 172, 254, 0.15) 0%, rgba(0, 242, 254, 0.15) 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
}
.quick-name {
diff --git a/uniapp/pages/features/features.vue b/uniapp/pages/features/features.vue
index 92d21e7..ae5dd3d 100644
--- a/uniapp/pages/features/features.vue
+++ b/uniapp/pages/features/features.vue
@@ -242,7 +242,7 @@ function onItemTap(item) {
transform: translateY(-50%);
width: 6rpx;
height: 36rpx;
- background: $color-primary;
+ background: linear-gradient(180deg, #4facfe 0%, #00f2fe 100%);
border-radius: 0 4rpx 4rpx 0;
}
}
@@ -270,10 +270,10 @@ function onItemTap(item) {
border-radius: $radius-md;
&.active {
- background: $color-primary-bg;
+ background: linear-gradient(135deg, rgba(79, 172, 254, 0.08) 0%, rgba(0, 242, 254, 0.08) 100%);
.item-name {
- color: $color-primary;
+ color: #4facfe;
}
}
diff --git a/uniapp/pages/profile/profile.vue b/uniapp/pages/profile/profile.vue
index e0e45f9..62a3180 100644
--- a/uniapp/pages/profile/profile.vue
+++ b/uniapp/pages/profile/profile.vue
@@ -22,7 +22,7 @@
-
+
+
+
+ 重复提醒间隔(分钟)
+
+
+
+
+
+ 0表示不重复,仅发送一次
+
+
+
+ 最多提醒次数
+
+
+
+
+
+ 最多发送的提醒次数
+