修复日程提醒功能

This commit is contained in:
2026-07-20 11:41:39 +08:00
parent 7ab6958538
commit f00ba9f9cc
19 changed files with 1735 additions and 1725 deletions
+1 -1
View File
@@ -1 +1 @@
{"pid":54204,"startedAt":1784192140605} {"pid":4264,"startedAt":1784510683025}
+8
View File
@@ -22,6 +22,14 @@ export default defineConfig({
}, },
}, },
server: { server: {
host: '127.0.0.1',
port: 4001, port: 4001,
proxy: {
'/api': {
target: 'http://127.0.0.1:9000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}, },
}); });
+168 -109
View File
@@ -1,109 +1,168 @@
package controllers package controllers
import ( import (
"time" "time"
"server/models" "server/models"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
) )
type ApiReminderController struct { type ApiReminderController struct {
beego.Controller beego.Controller
} }
// AckReminder GET /api/schedule/reminder/ack // AckReminder GET /api/schedule/reminder/ack
// 邮件/Bark 客户端访问此接口进行提醒确认 // 邮件/Bark 客户端访问此接口进行提醒确认
func (c *ApiReminderController) AckReminder() { // backend 与 platform 使用各自的提醒表,这里按 token 自动识别来源。
token := c.GetString("token") func (c *ApiReminderController) AckReminder() {
if token == "" { token := c.GetString("token")
c.Ctx.Output.SetStatus(400) if token == "" {
_ = c.Ctx.Output.Body([]byte("Invalid request: missing token")) c.Ctx.Output.SetStatus(400)
return _ = c.Ctx.Output.Body([]byte("Invalid request: missing token"))
} return
}
var reminder models.PlatformScheduleReminder
err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)). type reminderState struct {
Filter("ack_token", token). AckStatus int8
Filter("is_deleted", 0). ScheduleID uint64
One(&reminder) RemindStatus int8
if err != nil { }
c.Ctx.Output.SetStatus(404)
_ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired")) var state reminderState
return table := ""
}
var backendReminder models.BackendScheduleReminder
if reminder.AckStatus == 1 { if err := models.Orm.QueryTable(new(models.BackendScheduleReminder)).
// 已经确认过了,直接显示已确认成功的 HTML Filter("ack_token", token).
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8") Filter("is_deleted", 0).
_ = c.Ctx.Output.Body([]byte(` One(&backendReminder); err == nil {
<!DOCTYPE html> state = reminderState{
<html> AckStatus: backendReminder.AckStatus,
<head> ScheduleID: backendReminder.ScheduleID,
<meta charset="utf-8"> RemindStatus: backendReminder.RemindStatus,
<title>确认收到提醒</title> }
<style> table = "backend"
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; } } else {
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; } var platformReminder models.PlatformScheduleReminder
h2 { color: #67C23A; } if err := models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
</style> Filter("ack_token", token).
</head> Filter("is_deleted", 0).
<body> One(&platformReminder); err != nil {
<div class="card"> c.Ctx.Output.SetStatus(404)
<h2>提示</h2> _ = c.Ctx.Output.Body([]byte("Error: reminder task not found or token has expired"))
<p>该日程提醒在此之前已确认过了。</p> return
<p style="color: #909399; font-size: 14px;">无需重复点击,感谢您的使用!</p> }
</div> state = reminderState{
</body> AckStatus: platformReminder.AckStatus,
</html> ScheduleID: platformReminder.ScheduleID,
`)) RemindStatus: platformReminder.RemindStatus,
return }
} table = "platform"
}
// 更新确认状态为已确认,置 remind_status 为已结束(2)
now := time.Now() if state.AckStatus == 1 {
reminder.AckStatus = 1 writeReminderAckAlreadyConfirmed(c)
reminder.AckTime = &now return
reminder.RemindStatus = 2 }
reminder.UpdateTime = now
now := time.Now()
_, err = models.Orm.Update(&reminder, "AckStatus", "AckTime", "RemindStatus", "UpdateTime") var err error
if err != nil { if table == "backend" {
c.Ctx.Output.SetStatus(500) _, err = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
_ = c.Ctx.Output.Body([]byte("Database error, please try again later")) Filter("ack_token", token).
return Filter("is_deleted", 0).
} Update(map[string]interface{}{
"AckStatus": int8(1),
// 统一关闭该日程下的所有其他待提醒/提醒中渠道,防止重复打扰 "AckTime": now,
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). "RemindStatus": int8(2),
Filter("ScheduleID", reminder.ScheduleID). "UpdateTime": now,
Filter("RemindStatus__in", 0, 1). })
Update(map[string]interface{}{ } else {
"RemindStatus": int8(2), _, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)).
"UpdateTime": now, Filter("ack_token", token).
}) Filter("is_deleted", 0).
Update(map[string]interface{}{
// 成功确认 "AckStatus": int8(1),
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8") "AckTime": now,
_ = c.Ctx.Output.Body([]byte(` "RemindStatus": int8(2),
<!DOCTYPE html> "UpdateTime": now,
<html> })
<head> }
<meta charset="utf-8"> if err != nil {
<title>确认成功</title> c.Ctx.Output.SetStatus(500)
<style> _ = c.Ctx.Output.Body([]byte("Database error, please try again later"))
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; } return
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; } }
h2 { color: #67C23A; }
</style> // 只关闭命中来源表中、同一日程的其他待提醒记录,避免两套表互相影响。
</head> if table == "backend" {
<body> _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
<div class="card"> Filter("schedule_id", state.ScheduleID).
<h2>确认成功</h2> Filter("remind_status__in", 0, 1).
<p>您已成功确认收到该日程提醒!</p> Update(map[string]interface{}{
<p style="color: #909399; font-size: 14px;">系统已停止向您重复推送,感谢您的配合。</p> "RemindStatus": int8(2),
</div> "UpdateTime": now,
</body> })
</html> } 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(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认收到提醒</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>提示</h2>
<p>该日程提醒在此之前已确认过了。</p>
<p style="color: #909399; font-size: 14px;">无需重复点击,感谢您的使用!</p>
</div>
</body>
</html>
`))
}
func writeReminderAckSuccess(c *ApiReminderController) {
c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8")
_ = c.Ctx.Output.Body([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>确认成功</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #f5f7fa; color: #303133; }
.card { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); display: inline-block; max-width: 400px; }
h2 { color: #67C23A; }
</style>
</head>
<body>
<div class="card">
<h2>确认成功</h2>
<p>您已成功确认收到该日程提醒!</p>
<p style="color: #909399; font-size: 14px;">系统已停止向您重复推送,感谢您的配合。</p>
</div>
</body>
</html>
`))
}
+51 -39
View File
@@ -13,6 +13,7 @@ import (
"server/models" "server/models"
"server/pkg/jwtutil" "server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
) )
@@ -23,18 +24,18 @@ type AppReminderController struct {
func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) { func (c *AppReminderController) appClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization") auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" { if auth == "" {
return nil, fmt.Errorf("未登录") return nil, orm.ErrNoRows
} }
parts := strings.SplitN(auth, " ", 2) parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" { if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误") return nil, orm.ErrNoRows
} }
claims, err := jwtutil.ParseToken(parts[1]) claims, err := jwtutil.ParseToken(parts[1])
if err != nil { if err != nil {
return nil, fmt.Errorf("无效的token") return nil, err
} }
if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" { if claims.UserType != "backend" && claims.UserType != "app" && claims.UserType != "platform" {
return nil, fmt.Errorf("无权访问") return nil, orm.ErrNoRows
} }
return claims, nil return claims, nil
} }
@@ -71,7 +72,7 @@ type appSchedulePayload struct {
func (c *AppReminderController) GetList() { func (c *AppReminderController) GetList() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -87,8 +88,9 @@ func (c *AppReminderController) GetList() {
pageSize = 20 pageSize = 20
} }
qs := models.Orm.QueryTable(new(models.PlatformSchedule)). qs := models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("user_id", claims.UserID) Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId)
if keyword != "" { if keyword != "" {
qs = qs.Filter("content__contains", keyword) qs = qs.Filter("content__contains", keyword)
@@ -96,17 +98,20 @@ func (c *AppReminderController) GetList() {
total, _ := qs.Count() total, _ := qs.Count()
var schedules []models.PlatformSchedule var schedules []models.BackendSchedule
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&schedules) _, 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()) c.jsonErr(500, 500, "查询失败: "+err.Error())
return return
} }
if schedules == nil {
schedules = []models.BackendSchedule{}
}
list := make([]map[string]interface{}, 0, len(schedules)) list := make([]map[string]interface{}, 0, len(schedules))
for _, s := range schedules { for _, s := range schedules {
var reminders []models.PlatformScheduleReminder var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", s.ID). Filter("schedule_id", s.ID).
Filter("is_deleted", 0). Filter("is_deleted", 0).
All(&reminders) All(&reminders)
@@ -166,7 +171,7 @@ func (c *AppReminderController) GetList() {
func (c *AppReminderController) GetDetail() { func (c *AppReminderController) GetDetail() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -177,24 +182,25 @@ func (c *AppReminderController) GetDetail() {
return return
} }
var schedule models.PlatformSchedule var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)). err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id). Filter("id", id).
Filter("user_id", claims.UserID). Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule) One(&schedule)
if err != nil { if err != nil {
c.jsonErr(404, 404, "日程未找到") c.jsonErr(404, 404, "日程未找到")
return return
} }
var reminders []models.PlatformScheduleReminder var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", schedule.ID). Filter("schedule_id", schedule.ID).
Filter("is_deleted", 0). Filter("is_deleted", 0).
All(&reminders) All(&reminders)
channels := make([]string, 0, len(reminders)) channels := make([]string, 0, len(reminders))
var first models.PlatformScheduleReminder var first models.BackendScheduleReminder
for _, r := range reminders { for _, r := range reminders {
channels = append(channels, r.RemindChannel) channels = append(channels, r.RemindChannel)
first = r first = r
@@ -236,7 +242,7 @@ func (c *AppReminderController) GetDetail() {
func (c *AppReminderController) Create() { func (c *AppReminderController) Create() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -275,7 +281,8 @@ func (c *AppReminderController) Create() {
title = "日程提醒" title = "日程提醒"
} }
schedule := models.PlatformSchedule{ schedule := models.BackendSchedule{
Tid: claims.TenantId,
Title: title, Title: title,
Content: content, Content: content,
ScheduleTime: schedTime, ScheduleTime: schedTime,
@@ -298,7 +305,8 @@ func (c *AppReminderController) Create() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.PlatformScheduleReminder{ reminder := models.BackendScheduleReminder{
Tid: claims.TenantId,
ScheduleID: uint64(schedID), ScheduleID: uint64(schedID),
RemindChannel: ch, RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes, AdvanceMinutes: p.AdvanceMinutes,
@@ -336,7 +344,7 @@ func (c *AppReminderController) Create() {
func (c *AppReminderController) Update() { func (c *AppReminderController) Update() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -364,18 +372,19 @@ func (c *AppReminderController) Update() {
return return
} }
var schedule models.PlatformSchedule var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)). err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id). Filter("id", id).
Filter("user_id", claims.UserID). Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule) One(&schedule)
if err != nil { if err != nil {
c.jsonErr(404, 404, "日程未找到") c.jsonErr(404, 404, "日程未找到")
return return
} }
var reminders []models.PlatformScheduleReminder var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id). Filter("schedule_id", id).
Filter("is_deleted", 0). Filter("is_deleted", 0).
All(&reminders) 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). Filter("schedule_id", id).
Update(map[string]interface{}{ Update(map[string]interface{}{
"IsDeleted": 1, "IsDeleted": 1,
@@ -432,7 +441,8 @@ func (c *AppReminderController) Update() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.PlatformScheduleReminder{ reminder := models.BackendScheduleReminder{
Tid: claims.TenantId,
ScheduleID: id, ScheduleID: id,
RemindChannel: ch, RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes, AdvanceMinutes: p.AdvanceMinutes,
@@ -470,7 +480,7 @@ func (c *AppReminderController) Update() {
func (c *AppReminderController) Delete() { func (c *AppReminderController) Delete() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -481,18 +491,19 @@ func (c *AppReminderController) Delete() {
return return
} }
var schedule models.PlatformSchedule var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)). err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id). Filter("id", id).
Filter("user_id", claims.UserID). Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule) One(&schedule)
if err != nil { if err != nil {
c.jsonErr(404, 404, "日程未找到") c.jsonErr(404, 404, "日程未找到")
return return
} }
var reminders []models.PlatformScheduleReminder var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id). Filter("schedule_id", id).
Filter("is_deleted", 0). Filter("is_deleted", 0).
All(&reminders) All(&reminders)
@@ -512,9 +523,9 @@ func (c *AppReminderController) Delete() {
return 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 { if err == nil {
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id). Filter("schedule_id", id).
Update(map[string]interface{}{ Update(map[string]interface{}{
"IsDeleted": 1, "IsDeleted": 1,
@@ -530,7 +541,7 @@ func (c *AppReminderController) Delete() {
func (c *AppReminderController) ToggleComplete() { func (c *AppReminderController) ToggleComplete() {
claims, err := c.appClaims() claims, err := c.appClaims()
if err != nil { if err != nil {
c.jsonErr(401, 401, err.Error()) c.jsonErr(401, 401, "未登录或无权限")
return return
} }
@@ -541,18 +552,19 @@ func (c *AppReminderController) ToggleComplete() {
return return
} }
var schedule models.PlatformSchedule var schedule models.BackendSchedule
err = models.Orm.QueryTable(new(models.PlatformSchedule)). err = models.Orm.QueryTable(new(models.BackendSchedule)).
Filter("id", id). Filter("id", id).
Filter("user_id", claims.UserID). Filter("user_id", claims.UserID).
Filter("tid", claims.TenantId).
One(&schedule) One(&schedule)
if err != nil { if err != nil {
c.jsonErr(404, 404, "日程未找到") c.jsonErr(404, 404, "日程未找到")
return return
} }
var reminders []models.PlatformScheduleReminder var reminders []models.BackendScheduleReminder
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
Filter("schedule_id", id). Filter("schedule_id", id).
Filter("is_deleted", 0). Filter("is_deleted", 0).
All(&reminders) All(&reminders)
+35 -28
View File
@@ -210,13 +210,13 @@ func (c *BackendReminderController) GetReminderDetail() {
} }
data := map[string]interface{}{ data := map[string]interface{}{
"id": schedule.ID, "id": schedule.ID,
"title": schedule.Title, "title": schedule.Title,
"content": schedule.Content, "content": schedule.Content,
"schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"), "schedule_time": schedule.ScheduleTime.Format("2006-01-02 15:04:05"),
"remind_channels": channels, "remind_channels": channels,
"receiver_targets": targets, "receiver_targets": targets,
"is_finished": isFinished, "is_finished": isFinished,
} }
if first.ID > 0 { if first.ID > 0 {
data["advance_minutes"] = first.AdvanceMinutes data["advance_minutes"] = first.AdvanceMinutes
@@ -295,16 +295,16 @@ func (c *BackendReminderController) CreateReminder() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.BackendScheduleReminder{ reminder := models.BackendScheduleReminder{
Tid: claims.TenantId, Tid: claims.TenantId,
ScheduleID: uint64(schedID), ScheduleID: uint64(schedID),
RemindChannel: ch, RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes, AdvanceMinutes: p.AdvanceMinutes,
NextRemindTime: firstSendTime, NextRemindTime: firstSendTime,
ReceiverUserID: uint64(claims.UserID), ReceiverUserID: uint64(claims.UserID),
ReceiverTarget: target, ReceiverTarget: target,
RemindStatus: 0, RemindStatus: 0,
CreateTime: time.Now(), CreateTime: time.Now(),
UpdateTime: time.Now(), UpdateTime: time.Now(),
} }
if ch == "EMAIL" || ch == "BARK" { if ch == "EMAIL" || ch == "BARK" {
@@ -433,16 +433,16 @@ func (c *BackendReminderController) UpdateReminder() {
firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute) firstSendTime := schedTime.Add(-time.Duration(p.AdvanceMinutes) * time.Minute)
reminder := models.BackendScheduleReminder{ reminder := models.BackendScheduleReminder{
Tid: claims.TenantId, Tid: claims.TenantId,
ScheduleID: id, ScheduleID: id,
RemindChannel: ch, RemindChannel: ch,
AdvanceMinutes: p.AdvanceMinutes, AdvanceMinutes: p.AdvanceMinutes,
NextRemindTime: firstSendTime, NextRemindTime: firstSendTime,
ReceiverUserID: schedule.UserID, ReceiverUserID: schedule.UserID,
ReceiverTarget: target, ReceiverTarget: target,
RemindStatus: 0, RemindStatus: 0,
CreateTime: time.Now(), CreateTime: time.Now(),
UpdateTime: time.Now(), UpdateTime: time.Now(),
} }
if ch == "EMAIL" || ch == "BARK" { if ch == "EMAIL" || ch == "BARK" {
@@ -703,13 +703,20 @@ func (c *BackendReminderController) TestReminder() {
} }
dummyToken := "test-token-for-verification" dummyToken := "test-token-for-verification"
reminder := &models.PlatformScheduleReminder{ reminder := &models.BackendScheduleReminder{
Tid: claims.TenantId,
RemindChannel: ch, RemindChannel: ch,
ReceiverUserID: uint64(claims.UserID), ReceiverUserID: uint64(claims.UserID),
AckToken: &dummyToken, 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 := "发送成功" msg := "发送成功"
if !success { if !success {
msg = "发送失败" msg = "发送失败"
File diff suppressed because it is too large Load Diff
+388 -371
View File
@@ -1,371 +1,388 @@
package services package services
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"server/models" "server/models"
) )
// ReminderSender 提醒发送接口 type ReminderData struct {
type ReminderSender interface { RemindChannel string
Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (success bool, err error) ReceiverUserID uint64
} ReceiverTarget *string
AckToken *string
// SMSSender 短信发送实现 Tid int
type SMSSender struct{} }
func (s *SMSSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) { // ReminderSender 提醒发送接口
backendURL, apiKey, err := getDefaultSystemSMSConfig() type ReminderSender interface {
if err != nil { Send(ctx context.Context, reminder ReminderData, title, content string) (success bool, err error)
return false, err }
}
phone := "" // SMSSender 短信发送实现
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { type SMSSender struct{}
phone = *reminder.ReceiverTarget
} else { func (s *SMSSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
var user models.AdminUser backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil { if err != nil {
phone = *user.Phone return false, err
} }
} phone := ""
if phone == "" { if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
return false, fmt.Errorf("未配置手机号") phone = *reminder.ReceiverTarget
} } else {
var user models.AdminUser
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks" if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Phone != nil {
payload := map[string]interface{}{ phone = *user.Phone
"phone": phone, }
"content": title + ": " + content, }
} if phone == "" {
bs, _ := json.Marshal(payload) return false, fmt.Errorf("未配置手机号")
}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs)) enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
if err != nil { payload := map[string]interface{}{
return false, err "phone": phone,
} "content": title + ": " + content,
req.Header.Set("Content-Type", "application/json") }
req.Header.Set("X-Api-Key", apiKey) bs, _ := json.Marshal(payload)
resp, err := client.Do(req) client := &http.Client{Timeout: 10 * time.Second}
if err != nil { req, err := http.NewRequestWithContext(ctx, "POST", enqueueURL, bytes.NewReader(bs))
return false, err if err != nil {
} return false, err
defer resp.Body.Close() }
req.Header.Set("Content-Type", "application/json")
if resp.StatusCode != http.StatusOK { req.Header.Set("X-Api-Key", apiKey)
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes)) resp, err := client.Do(req)
} if err != nil {
return false, err
return true, nil }
} defer resp.Body.Close()
// EmailSender 邮件发送实现 if resp.StatusCode != http.StatusOK {
type EmailSender struct{} bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("网关返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
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 true, nil
return false, fmt.Errorf("未配置系统邮箱") }
}
emailCfg := emails[0] // EmailSender 邮件发送实现
if emailCfg.FromAddress == "" || emailCfg.Host == "" { type EmailSender struct{}
return false, fmt.Errorf("未配置系统邮箱")
} func (s *EmailSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
emails, err := ListSystemEmails()
toEmail := "" if err != nil || len(emails) == 0 {
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { return false, fmt.Errorf("未配置系统邮箱")
toEmail = *reminder.ReceiverTarget }
} else { emailCfg := emails[0]
var user models.AdminUser if emailCfg.FromAddress == "" || emailCfg.Host == "" {
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil { return false, fmt.Errorf("未配置系统邮箱")
toEmail = *user.Email }
}
} toEmail := ""
if toEmail == "" { if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
return false, fmt.Errorf("未配置收件邮箱") toEmail = *reminder.ReceiverTarget
} } else {
var user models.AdminUser
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn") if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id", reminder.ReceiverUserID).One(&user); err == nil && user.Email != nil {
ackToken := "" toEmail = *user.Email
if reminder.AckToken != nil { }
ackToken = *reminder.AckToken }
} if toEmail == "" {
return false, fmt.Errorf("未配置收件邮箱")
// 构造 HTML 邮件 }
htmlBody := fmt.Sprintf(`
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 5px; max-width: 600px; margin: 0 auto;"> sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
<h2 style="color: #409EFF; margin-bottom: 20px;">日程提醒%s</h2> ackToken := ""
<p style="font-size: 16px; line-height: 1.6; color: #333;">%s</p> if reminder.AckToken != nil {
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;" /> ackToken = *reminder.AckToken
`, title, content) }
if ackToken != "" { // 构造 HTML 邮件
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken) htmlBody := fmt.Sprintf(`
htmlBody += fmt.Sprintf(` <div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 5px; max-width: 600px; margin: 0 auto;">
<div style="text-align: center; margin-top: 30px;"> <h2 style="color: #409EFF; margin-bottom: 20px;">日程提醒%s</h2>
<a href="%s" target="_blank" style="background-color: #409EFF; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;"> <p style="font-size: 16px; line-height: 1.6; color: #333;">%s</p>
收到确认此提醒 <hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;" />
</a> `, title, content)
</div>
<p style="font-size: 12px; color: #999; text-align: center; margin-top: 15px;">确认收到后系统将不再向您发送该日程的重复提醒</p> if ackToken != "" {
`, ackURL) ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
} htmlBody += fmt.Sprintf(`
<div style="text-align: center; margin-top: 30px;">
htmlBody += "</div>" <a href="%s" target="_blank" style="background-color: #409EFF; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
收到确认此提醒
cfg := SMTPConfig{ </a>
FromAddress: emailCfg.FromAddress, </div>
Host: emailCfg.Host, <p style="font-size: 12px; color: #999; text-align: center; margin-top: 15px;">确认收到后系统将不再向您发送该日程的重复提醒</p>
Port: emailCfg.Port, `, ackURL)
Password: emailCfg.Password, }
Encryption: emailCfg.Encryption,
Timeout: emailCfg.Timeout, htmlBody += "</div>"
}
if emailCfg.FromName != nil { cfg := SMTPConfig{
cfg.FromName = *emailCfg.FromName FromAddress: emailCfg.FromAddress,
} Host: emailCfg.Host,
Port: emailCfg.Port,
err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody) Password: emailCfg.Password,
if err != nil { Encryption: emailCfg.Encryption,
return false, err Timeout: emailCfg.Timeout,
} }
if emailCfg.FromName != nil {
return true, nil cfg.FromName = *emailCfg.FromName
} }
// BarkSender Bark 推送实现 err = SendHTMLEmailSMTP(cfg, toEmail, title, htmlBody)
type BarkSender struct{} if err != nil {
return false, err
func (s *BarkSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) { }
deviceKey := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" { return true, nil
deviceKey = *reminder.ReceiverTarget }
} else {
deviceKey = models.GetPlatformSettingValue("bark_device_key", "") // BarkSender Bark 推送实现
} type BarkSender struct{}
if deviceKey == "" {
return false, fmt.Errorf("Bark 设备 Key 未配置") func (s *BarkSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
} deviceKey := ""
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app") deviceKey = *reminder.ReceiverTarget
sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn") } else {
ackToken := "" deviceKey = models.GetPlatformSettingValue("bark_device_key", "")
if reminder.AckToken != nil { }
ackToken = *reminder.AckToken if deviceKey == "" {
} return false, fmt.Errorf("Bark 设备 Key 未配置")
}
baseURL := strings.TrimRight(serverURL, "/")
escapedTitle := url.PathEscape(title) serverURL := models.GetPlatformSettingValue("bark_server_url", "https://api.day.app")
pushContent := content sysDomain := models.GetPlatformSettingValue("system_domain", "https://api.yunzer.cn")
if ackToken != "" { ackToken := ""
pushContent += "\n确认收到请点击→" if reminder.AckToken != nil {
} ackToken = *reminder.AckToken
escapedContent := url.PathEscape(pushContent) }
barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent) baseURL := strings.TrimRight(serverURL, "/")
escapedTitle := url.PathEscape(title)
if ackToken != "" { pushContent := content
ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken) if ackToken != "" {
// Bark 官方推送支持 url 参数 pushContent += "\n确认收到请点击→"
barkURL += "?url=" + url.QueryEscape(ackURL) }
} escapedContent := url.PathEscape(pushContent)
client := &http.Client{Timeout: 10 * time.Second} barkURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, deviceKey, escapedTitle, escapedContent)
req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
if err != nil { if ackToken != "" {
return false, err ackURL := fmt.Sprintf("%s/api/schedule/reminder/ack?token=%s", strings.TrimRight(sysDomain, "/"), ackToken)
} // Bark 官方推送支持 url 参数
barkURL += "?url=" + url.QueryEscape(ackURL)
resp, err := client.Do(req) }
if err != nil {
return false, err client := &http.Client{Timeout: 10 * time.Second}
} req, err := http.NewRequestWithContext(ctx, "GET", barkURL, nil)
defer resp.Body.Close() if err != nil {
return false, err
if resp.StatusCode != http.StatusOK { }
bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes)) resp, err := client.Do(req)
} if err != nil {
return false, err
return true, nil }
} defer resp.Body.Close()
// SiteMsgSender 站内信发送实现 if resp.StatusCode != http.StatusOK {
type SiteMsgSender struct{} bodyBytes, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("Bark返回HTTP状态码: %d, 返回内容: %s", resp.StatusCode, string(bodyBytes))
func (s *SiteMsgSender) Send(ctx context.Context, reminder *models.PlatformScheduleReminder, title, content string) (bool, error) { }
now := time.Now()
msg := &models.SystemReminderList{ return true, nil
Title: title, }
Content: content,
SenderID: 0, // SiteMsgSender 站内信发送实现
SenderType: "system", type SiteMsgSender struct{}
ReceiverID: reminder.ReceiverUserID,
ReceiverType: "platform", // 平台端用户 func (s *SiteMsgSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
IsRead: 0, now := time.Now()
CreateTime: &now, msg := &models.SystemReminderList{
} Title: title,
_, err := models.Orm.Insert(msg) Content: content,
if err != nil { SenderID: 0,
return false, err SenderType: "system",
} ReceiverID: reminder.ReceiverUserID,
return true, nil ReceiverType: "tenant", // backend/租户用户
} IsRead: 0,
CreateTime: &now,
// generateUUID 生成一个安全的随机 UUID 字符 TargetType: "tenant",
func generateUUID() string { TargetTenantID: uint64(reminder.Tid),
b := make([]byte, 16) }
_, _ = rand.Read(b) _, err := models.Orm.Insert(msg)
b[6] = (b[6] & 0x0f) | 0x40 if err != nil {
b[8] = (b[8] & 0x3f) | 0x80 return false, err
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) }
} return true, nil
}
// StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
func StartReminderScheduler(stopChan chan struct{}) { // generateUUID 生成一个安全的随机 UUID 字符
ticker := time.NewTicker(1 * time.Minute) func generateUUID() string {
go func() { b := make([]byte, 16)
for { _, _ = rand.Read(b)
select { b[6] = (b[6] & 0x0f) | 0x40
case <-ticker.C: b[8] = (b[8] & 0x3f) | 0x80
scanAndSendReminders() return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
case <-stopChan: }
ticker.Stop()
return // StartReminderScheduler 启动定时提醒调度器 (1分钟一次的 Ticker)
} func StartReminderScheduler(stopChan chan struct{}) {
} ticker := time.NewTicker(1 * time.Minute)
}() go func() {
} for {
select {
func scanAndSendReminders() { case <-ticker.C:
// 1. 生成唯一扫描批次号用于抢占锁定 scanAndSendReminders()
scanBatch := generateUUID() case <-stopChan:
now := time.Now() ticker.Stop()
return
// 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) func scanAndSendReminders() {
AND is_deleted = 0 // 1. 生成唯一扫描批次号用于抢占锁定
AND (scan_lock = '' OR scan_lock IS NULL) scanBatch := generateUUID()
`, scanBatch, now).Exec() now := time.Now()
if err != nil {
return // 2. 抢占待处理的数据(乐观锁防并发重复发送)
} _, err := models.Orm.Raw(`
UPDATE yz_backend_schedule_reminder
// 3. 查询自己锁定成功的数据 SET scan_lock = ?, update_time = NOW()
var list []models.PlatformScheduleReminder WHERE next_remind_time <= ?
_, err = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). AND remind_status IN (0, 1)
Filter("scan_lock", scanBatch). AND is_deleted = 0
Filter("remind_status__in", 0, 1). AND (scan_lock = '' OR scan_lock IS NULL)
Filter("is_deleted", 0). `, scanBatch, now).Exec()
All(&list) if err != nil {
if err != nil || len(list) == 0 { return
return }
}
// 3. 查询自己锁定成功的数据
// 实例分发发送 var list []models.BackendScheduleReminder
senders := map[string]ReminderSender{ _, err = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
"SMS": &SMSSender{}, Filter("scan_lock", scanBatch).
"EMAIL": &EmailSender{}, Filter("remind_status__in", 0, 1).
"BARK": &BarkSender{}, Filter("is_deleted", 0).
"SITE_MSG": &SiteMsgSender{}, All(&list)
} if err != nil || len(list) == 0 {
return
for i := range list { }
reminder := &list[i]
// 实例分发发送
// 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒" senders := map[string]ReminderSender{
var schedule models.PlatformSchedule "SMS": &SMSSender{},
err := models.Orm.QueryTable(new(models.PlatformSchedule)). "EMAIL": &EmailSender{},
Filter("id", reminder.ScheduleID). "BARK": &BarkSender{},
One(&schedule) "SITE_MSG": &SiteMsgSender{},
title := "日程提醒" }
content := "您有一个待处理的日程时间已到,请注意查收。"
if err == nil { for i := range list {
content = schedule.Content reminder := &list[i]
}
// 3.1 获取日程信息(主要拿 Content,Title 统一为 "日程提醒"
sender, ok := senders[reminder.RemindChannel] var schedule models.BackendSchedule
if !ok { err := models.Orm.QueryTable(new(models.BackendSchedule)).
// 未知渠道,直接强制置为结束 Filter("id", reminder.ScheduleID).
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). One(&schedule)
Filter("id", reminder.ID). title := "日程提醒"
Update(map[string]interface{}{ content := "您有一个待处理的日程时间已到,请注意查收。"
"remind_status": 2, if err == nil {
"scan_lock": "", content = schedule.Content
"update_time": time.Now(), }
})
continue sender, ok := senders[reminder.RemindChannel]
} if !ok {
// 未知渠道,直接强制置为结束
// 执行发送 _, _ = models.Orm.QueryTable(new(models.BackendScheduleReminder)).
ctx := context.Background() Filter("id", reminder.ID).
success, sendErr := sender.Send(ctx, reminder, title, content) Update(map[string]interface{}{
"remind_status": 2,
// 3.2 记录发送流水日志 "scan_lock": "",
sendResult := int8(0) "update_time": time.Now(),
var failReason *string })
if success { continue
sendResult = 1 }
} else if sendErr != nil {
errStr := sendErr.Error() // 执行发送
if len(errStr) > 255 { ctx := context.Background()
errStr = errStr[:255] success, sendErr := sender.Send(ctx, ReminderData{
} RemindChannel: reminder.RemindChannel,
failReason = &errStr ReceiverUserID: reminder.ReceiverUserID,
} ReceiverTarget: reminder.ReceiverTarget,
AckToken: reminder.AckToken,
logRow := &models.PlatformScheduleReminderSendLog{ Tid: reminder.Tid,
ReminderID: reminder.ID, }, title, content)
SendTime: time.Now(),
SendResult: sendResult, // 3.2 记录发送流水日志
FailReason: failReason, sendResult := int8(0)
} var failReason *string
_, _ = models.Orm.Insert(logRow) if success {
sendResult = 1
// 3.3 根据发送渠道分类更新提醒状态和下一次发送时间 } else if sendErr != nil {
newSendCount := reminder.SendCount + 1 errStr := sendErr.Error()
newStatus := reminder.RemindStatus if len(errStr) > 255 {
errStr = errStr[:255]
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" { }
// 一次性发送:发送后直接置为结束 failReason = &errStr
newStatus = 2 }
} else {
// 重复发送渠道 EMAIL / BARK logRow := &models.BackendScheduleReminderSendLog{
// 如果还没被 Ack,且没有达到 max_send_count,继续提醒 Tid: reminder.Tid,
if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount { ReminderID: reminder.ID,
newStatus = 1 // 提醒中 SendTime: time.Now(),
// 更新下次发送时间 SendResult: sendResult,
reminder.NextRemindTime = time.Now().Add(time.Duration(reminder.RepeatIntervalMinutes) * time.Minute) FailReason: failReason,
} else { }
// 达到最大上限或者已 Ack _, _ = models.Orm.Insert(logRow)
newStatus = 2
} // 3.3 根据发送渠道分类更新提醒状态和下一次发送时间
} newSendCount := reminder.SendCount + 1
newStatus := reminder.RemindStatus
// 3.4 回写主表记录
_, _ = models.Orm.QueryTable(new(models.PlatformScheduleReminder)). if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
Filter("id", reminder.ID). // 一次性发送:发送后直接置为结束
Update(map[string]interface{}{ newStatus = 2
"SendCount": newSendCount, } else {
"NextRemindTime": reminder.NextRemindTime, // 重复发送渠道 EMAIL / BARK
"RemindStatus": newStatus, // 如果还没被 Ack,且没有达到 max_send_count,继续提醒
"ScanLock": "", // 释放扫描锁 if reminder.AckStatus == 0 && newSendCount < reminder.MaxSendCount {
"UpdateTime": time.Now(), 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(),
})
}
}
+170 -300
View File
@@ -9,24 +9,14 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@tiptap/extension-color": "^3.27.4", "@tiptap/extension-color": "^3.28.0",
"@tiptap/extension-highlight": "^3.27.4", "@tiptap/extension-image": "^3.28.0",
"@tiptap/extension-image": "^3.27.4", "@tiptap/extension-link": "^3.28.0",
"@tiptap/extension-link": "^3.27.4", "@tiptap/extension-text-align": "^3.28.0",
"@tiptap/extension-placeholder": "^3.27.4", "@tiptap/extension-text-style": "^3.28.0",
"@tiptap/extension-subscript": "^3.27.4", "@tiptap/extension-underline": "^3.28.0",
"@tiptap/extension-superscript": "^3.27.4", "@tiptap/starter-kit": "^3.28.0",
"@tiptap/extension-table": "^3.27.4", "@tiptap/vue-3": "^3.28.0",
"@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",
"@umoteam/editor": "^10.2.1", "@umoteam/editor": "^10.2.1",
"axios": "^1.13.1", "axios": "^1.13.1",
"chart": "^0.1.2", "chart": "^0.1.2",
@@ -1174,49 +1164,49 @@
] ]
}, },
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/core/-/core-3.28.0.tgz",
"integrity": "sha512-8W/GwlEn0JwNdpyVfTWcXwHYUpj9BWwO++YxtizmgjJzlwigSh7/xLVJMwVykuQHQ2fCq5rkUvmBRtpHOMLUQA==", "integrity": "sha512-gUuD5WAYfbDxNSSJya/emh2KSzXZXLUYKW4fEnc1AQ5FE2twzh4LJ9UlKFIawigrUCAksWI5Fy1hRbv+5m4ZdQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-blockquote/-/extension-blockquote-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-blockquote/-/extension-blockquote-3.28.0.tgz",
"integrity": "sha512-d1tOHgP3R5cOE+Ot8qL/dkLXRByajgn+j6cCXHqDtmJO2wsK9knmbKQ0SEjbKrU6OgHrTnY/EotNxBEBW9HGoA==", "integrity": "sha512-y8Xi6Z3AQLXedz0J8Z3kDsu7SKBmL50gKVXx3RK1Oo1cuCGhNChm3syChkAz3PLAbrPUM5rvJDSZdF2jUrDnng==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-bold": { "node_modules/@tiptap/extension-bold": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-bold/-/extension-bold-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-bold/-/extension-bold-3.28.0.tgz",
"integrity": "sha512-wTtJUUAxCAZ01ICH2DNlOBzzHKRQ1ZST8aRYtIhBPzqEUhnJaKGcjnDB4X49fqPi48iXaPxzhsInDl+rVUujWg==", "integrity": "sha512-JhZQmr0AU741bOwjVMfuwJdK4g0TQwwPbeca9aqKHv5zvZw4i4G9G6fESVyMFc3Yag1ffpnq5EtNldHKTnMhlw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-bubble-menu": { "node_modules/@tiptap/extension-bubble-menu": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.28.0.tgz",
"integrity": "sha512-Poy7xwcD3POG5ew/TW7mYXv7m++vCchvHxPUqIfnTxBxvvvqDZkPYFWZS1lvPrSBtm1DcfUTQAgVutM5NDZ99Q==", "integrity": "sha512-7AUNoHj2K4XLKCgW4uspeD/ENejPu2BeHvLTsiOoIO+XXDNSi2j0bbaIS8f2s/qnFirscwp/sbznp1qph/76qg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -1227,48 +1217,48 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-bullet-list": { "node_modules/@tiptap/extension-bullet-list": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.28.0.tgz",
"integrity": "sha512-rvja0N1RnwGJAVwDdbUfDIJ4NoT+KjPFaZudKiPuEMfMHfbqe4xcbbC2hsfs61JNcl2xmx+ohV6lzD9YxxJl1w==", "integrity": "sha512-dSVuNiH7PFMmWGkNER9vfUb5bXUHDARvHbJ3l+APEb+ZiusVN1KFdM3nd/RsW/Rt5Gd3XwlIEU/c2Uf7cxYDcw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.27.4" "@tiptap/extension-list": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-code": { "node_modules/@tiptap/extension-code": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-code/-/extension-code-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-code/-/extension-code-3.28.0.tgz",
"integrity": "sha512-aPc7opCR1ylK4m4c2lsjLsGpEBD1fLQQKWd5PbZiJvrTF8gkdGZlYLt9A6VukpxeJyHhb22Jaj4fxgKmGMeTtw==", "integrity": "sha512-AUw2Acof3CQE6Q6Y22saK4lyg0nkeJ4XXniU65TdAi/9TE66TGiO4NQJJNNPgu8+VMEwl5j8VsIFK8sfTcNYtg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-code-block": { "node_modules/@tiptap/extension-code-block": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-code-block/-/extension-code-block-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-code-block/-/extension-code-block-3.28.0.tgz",
"integrity": "sha512-a5caWfWN6Z6usy48vzJDDOhWoA6+rFFCHGpQM7jXn/7rRzYPcvBzTZUGptjEbltj4YqtrQ2tVwTJcCtbb+mknA==", "integrity": "sha512-bHITDzT0umTLh+SiEVQzIXkCMt/TzbPM1+HQy9ZxgQDHAJj/vdmfNAnP3RCOrz4lETXyhNQ2b6kxeu3lWckxyA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-code-block-lowlight": { "node_modules/@tiptap/extension-code-block-lowlight": {
@@ -1305,16 +1295,16 @@
} }
}, },
"node_modules/@tiptap/extension-color": { "node_modules/@tiptap/extension-color": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-color/-/extension-color-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-color/-/extension-color-3.28.0.tgz",
"integrity": "sha512-uGbgErKGKO4OTBGqnXOA1CGXa3IqoBaiPBGcHu/px01fS8EVFu0A7q5HS6vxFiMK6qW5x71sEpCA+FUINMAb3g==", "integrity": "sha512-YbUMUdrgU04GDYoii1wuFVkiTSQoaEQh04kwgQayEQ46aUBEnnaJitWdp39aGMWYTIXD/c9K9cr5Vg8GSKcVxA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-text-style": "3.27.4" "@tiptap/extension-text-style": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-details": { "node_modules/@tiptap/extension-details": {
@@ -1333,16 +1323,16 @@
} }
}, },
"node_modules/@tiptap/extension-document": { "node_modules/@tiptap/extension-document": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-document/-/extension-document-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-document/-/extension-document-3.28.0.tgz",
"integrity": "sha512-7nAqgfkgb9HADBeCTnOHuTiyZuxfxvMPT3nH4OZeY+cmtkI1On3QffqlmtcUPvNbkhT3o9ehA1hVfCnQ1Ye4LQ==", "integrity": "sha512-5SAKtlB1Tr/eZFhISL86HqmUup6rItvPtuPyRjmZo/VgbMwXEwiyAh1sMgFp5VNaeSMM14vJSyQpjhSaOmaBqg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-drag-handle": { "node_modules/@tiptap/extension-drag-handle": {
@@ -1382,22 +1372,22 @@
} }
}, },
"node_modules/@tiptap/extension-dropcursor": { "node_modules/@tiptap/extension-dropcursor": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.28.0.tgz",
"integrity": "sha512-RiZasQJuUTUO3aME16Bn8eJH7cYnvhT5JCFDFq0ya/1iFI9wUQA2NJC5tb5TrZ74+sQwkYU9VzexnchM481Y9w==", "integrity": "sha512-JA+dXQjjfJBpD0nVsZeddGVXRsmanESM9+8CtM35hI9wJnYMXay/B+5GUhswO20zhT3zhB2ZOTGe9knu6A8nIQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extensions": "3.27.4" "@tiptap/extensions": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-floating-menu": { "node_modules/@tiptap/extension-floating-menu": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.28.0.tgz",
"integrity": "sha512-tnZywwoNDuEcUZmYYIztXl3PpIKUq+gKeaYPuZhpYEVTThU44tzK3ZuFOmd+qf2aAa1MQwxKWqUuLpNK77bwNw==", "integrity": "sha512-59SECvJq3pQfeJBuydEdhQqrpl0oDlk8N1Ovs1Si3/fJa6rEQAJB2zIvcpBHky9Z+5JodI2eueDnv8eimiyGbg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"funding": { "funding": {
@@ -1406,87 +1396,74 @@
}, },
"peerDependencies": { "peerDependencies": {
"@floating-ui/dom": "^1.0.0", "@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-gapcursor": { "node_modules/@tiptap/extension-gapcursor": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.28.0.tgz",
"integrity": "sha512-svLwSKcFhzpcJeXvxxKkRFuQpykmXrQefVhEsaXq0L95yJIIAGKMRmQC3mxKdzL2j0P9cY7V41bNVSyOAyvclw==", "integrity": "sha512-Wo73kM4q8z4Va9i4+0n1cO0UEMVUVBHPqM6cAqEYXjB4T48LQkKjRsiQydCezm185rQAsmm1dyi72gtvVQfdMg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extensions": "3.27.4" "@tiptap/extensions": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-hard-break": { "node_modules/@tiptap/extension-hard-break": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-hard-break/-/extension-hard-break-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-hard-break/-/extension-hard-break-3.28.0.tgz",
"integrity": "sha512-W+Z9pmDgqjbdu3NeZOQrzA15iM4w60Yd8l2CYzxcdApPVIfYzb2S3a7+u1RqW9wnTYb6xyZjASmFNfxXS4P4cg==", "integrity": "sha512-JHIFjX1luJgQ4VFEkIeXZpbc54Qiw+69P8FmlazYTh7AIJ6iLCpMFgQFELnivEAZ+/vS0lMPQubg0rCeM3UrHw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-heading": { "node_modules/@tiptap/extension-heading": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-heading/-/extension-heading-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-heading/-/extension-heading-3.28.0.tgz",
"integrity": "sha512-RgvpxzuYk6QEK+az+eiXpWvGlUso42zNcGnjyUrvskoZjS47MbhSg8ylRYQSRtXE0ETlXhAx4J7iGlGr72kyIw==", "integrity": "sha512-rKjGG/Ik2lNaXBNVmONEDm5DBfGDLM1NWgSf5RRfBISrH8Lm1xqFruOB9tIaB0YUoSV3SBOiwTF9svmzvKSoRA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
}
},
"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"
} }
}, },
"node_modules/@tiptap/extension-horizontal-rule": { "node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.28.0.tgz",
"integrity": "sha512-2eQU/55nE5mhMJHALtLMuBL3dcVJUDVVT7n+uZYMaYE63BtCvC4VS08YLFSR7JZSVJIlgVAmdt5nAw0B+rEPNA==", "integrity": "sha512-neBCMWprkppQUoLWyeJZDHqBw+xKX2oNhLD9MbFlhKIr092zgfP4mpCvQnSkn1OHl156KzZMp070+NyLBjgQ7A==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-image": { "node_modules/@tiptap/extension-image": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-image/-/extension-image-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-image/-/extension-image-3.28.0.tgz",
"integrity": "sha512-yQ8CazyOL4z1/NbV1NLGv6DvchVhOXHH3uQ7md5VX/IGZruFpnm8IpF9MDpdAUxUFmTsXJDYyO2lWGO1PYWG8A==", "integrity": "sha512-aKGCdEjyujUcQ6tEtq4EPbKxEC16QsKapv3DeZs1c/UHFiEUalbbPHBnxjGnupiLz75IR+07GYqBoZ81kSCxiA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-invisible-characters": { "node_modules/@tiptap/extension-invisible-characters": {
@@ -1505,22 +1482,22 @@
} }
}, },
"node_modules/@tiptap/extension-italic": { "node_modules/@tiptap/extension-italic": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-italic/-/extension-italic-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-italic/-/extension-italic-3.28.0.tgz",
"integrity": "sha512-PeZT4XbyxAp7Lqo/hfA1k5LI27g1RlgS+YgXp2CeHXIrUfSpO5HlZXh02Bvb0pOdl3RFw2tEKtlHzjt8Y1+Nwg==", "integrity": "sha512-Yur/ELz6dNVKQC7m8wGjtoLFxamGjJmA0rUEEOacTH6J39aFmcSxYkEGNDkYHbZFyHFYqbej6eI3VE0h08O0mg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-link": { "node_modules/@tiptap/extension-link": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-link/-/extension-link-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-link/-/extension-link-3.28.0.tgz",
"integrity": "sha512-6K/FkNwMLWWQbNWKlycrUPTN7YcyVFdFwZncoBXe5WyarRjLTGw7ywafnCI9PDIWSq7ttzVL4NgjN2IN8kBXww==", "integrity": "sha512-hy/PqSeTyl317yseFcHGE+3XVfQKQYaL4uZuj27xlrcO7MZ15drt1h9sUZy1FTBF3mr8676pQu7aoEm/Ww4ZRA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"linkifyjs": "^4.3.3" "linkifyjs": "^4.3.3"
@@ -1530,48 +1507,48 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-list": { "node_modules/@tiptap/extension-list": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-list/-/extension-list-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-list/-/extension-list-3.28.0.tgz",
"integrity": "sha512-A0BgmRO1RE0yLCx9w7GQITtKfS9wLE5cdngSYDiSpwulcXJhJjKm5mZ4OUZmks2VN4HO5jMl2BWCGt2NSDhA+w==", "integrity": "sha512-zQ66i5DuhVOndmZ0d7H475Y5Yb+BMx+zfCjFkCzEc6WVef5MumtxBVTkGLQ0ibxUaD71s4QQZbjHWagpaTg0eQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-list-item": { "node_modules/@tiptap/extension-list-item": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-list-item/-/extension-list-item-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-item/-/extension-list-item-3.28.0.tgz",
"integrity": "sha512-z5TVuPw2mkK0B/x+gFg3uUV7tBdaElDFg0zVgnXZCqlSVTLfIyInOOnG5LTWoAd9BdzBjGrzE3PohDcLVDDGBQ==", "integrity": "sha512-GbXrnMac6knSetZY33NHwOrgXFPqH28uCklQqmOZQlsrdGqezDKk31qoEMr4GsKW9n/lViAeuc07oIzwLwCMng==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.27.4" "@tiptap/extension-list": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-list-keymap": { "node_modules/@tiptap/extension-list-keymap": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.28.0.tgz",
"integrity": "sha512-on7JNDi7Eqz7UdZeZdiO83bQHo0flVDHzjmtR+v/nrCGW9H15D3CHs5+4ozLDiCvTK8tbkBuut/l9AWNxcCE/Q==", "integrity": "sha512-u9Wks8c/eQAv0RkULNggOyTKDD69WVbcV9H5cbasPDUbq5XbEFVa9ajxhEGfMmQ5et3EX0QL8qBi50K0GqbIjA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.27.4" "@tiptap/extension-list": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-mathematics": { "node_modules/@tiptap/extension-mathematics": {
@@ -1619,123 +1596,42 @@
} }
}, },
"node_modules/@tiptap/extension-ordered-list": { "node_modules/@tiptap/extension-ordered-list": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.28.0.tgz",
"integrity": "sha512-bHwLiof0FqJfWzB0act7oEKMTZatEKQ4IYCvmyF5EktjMs4kxEatkPp4Yx/1LSYSjLy1MMT7oLELyaz2FFYyXA==", "integrity": "sha512-OZNYrKKL9D01ySOujlNbKlLS9/3wGgKNYeoHZUg0e+Ta8WPVvL3W1zH6TgiU5sF2ZSGUBoq3w7mdaO/iROlUkw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.27.4" "@tiptap/extension-list": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-paragraph": { "node_modules/@tiptap/extension-paragraph": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-paragraph/-/extension-paragraph-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-paragraph/-/extension-paragraph-3.28.0.tgz",
"integrity": "sha512-8Dnr1J5s/s4XYYuEF3b784NnCxLjXOlQpmGyXRxTAzW7JaOP08tIUJWVNvSMekfXc2vXa33HUbqjxyyWZEQ6LQ==", "integrity": "sha512-8UMlQIjzqf6r2hSJ/VeJ00RqaOrOB1J5rTk02Vcw2pYeHkOqp+B0ff0HFu4abT9x1q4oXrBAgMsxRtgh/kR14Q==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
}
},
"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"
} }
}, },
"node_modules/@tiptap/extension-strike": { "node_modules/@tiptap/extension-strike": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-strike/-/extension-strike-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-strike/-/extension-strike-3.28.0.tgz",
"integrity": "sha512-8OXwcPKuV3ToBBgyvDxH1jQdObK5FIKCGiyIim6qNWiOpi9BhM3XYD+aO1khjv8qIjtoI/DYbizF4ewj09fX2g==", "integrity": "sha512-VVj2ZZU9QYqiHLcjqMqRYvuHSsokj/AgUl+6TzLrKjlWwyZ18D4H2vkyI0g70iVTKnUpZ3sEy8WA/rqjgBjqBg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
}
},
"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"
} }
}, },
"node_modules/@tiptap/extension-table-of-contents": { "node_modules/@tiptap/extension-table-of-contents": {
@@ -1768,82 +1664,56 @@
"uuid": "dist/bin/uuid" "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": { "node_modules/@tiptap/extension-text": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-text/-/extension-text-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-text/-/extension-text-3.28.0.tgz",
"integrity": "sha512-lKQH/hP4FBXsziHypd6Ywj8JFvMLM5GVkK1xsH6yApNuXbHq95rd42ZOYWpYILIBib7tlaz93z61d74UrJiuiw==", "integrity": "sha512-Jqp1LgfY1mnp4TQHoy+vnHyfn6qnnAM6nHgGwbY5zA8b/Xf1Etll6pziTx9p1J1qcfAgSrnjOyAmA++H7VQqww==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-text-align": { "node_modules/@tiptap/extension-text-align": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-text-align/-/extension-text-align-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-align/-/extension-text-align-3.28.0.tgz",
"integrity": "sha512-ArfL7GLOXCSmrmiBiaWwAf7RPHXDdxLGPru29qKDLQDthjXcNOdlwlPBbolRu7mXwlSmaYpqWGrG75/AEat3mw==", "integrity": "sha512-P+KQMTtCpLFq9dRvy/fbyu2BGALulT/1HtsLbpFG4tKATaKa+cFnJ7rLZF7b3AdAMDNtXdnivxAtixAMBt2QWw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-text-style": { "node_modules/@tiptap/extension-text-style": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-text-style/-/extension-text-style-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-text-style/-/extension-text-style-3.28.0.tgz",
"integrity": "sha512-Wmj64TQXY85gc7lUNbubW32sDCnVOJlpGraMeARRC9Z0EKBX1JpAxddo/64F17bn3kzLxXVszBFyLRcNgTdU2g==", "integrity": "sha512-UreMyqZSv770+CXJK/l1qeHUdaNMQAVkxSOPWpiY9FiT8B4OQyjA7WBOlWrzbvVTzfJvXWk2Y3SWgVRx+/Cd9A==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
}
},
"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"
} }
}, },
"node_modules/@tiptap/extension-underline": { "node_modules/@tiptap/extension-underline": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extension-underline/-/extension-underline-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extension-underline/-/extension-underline-3.28.0.tgz",
"integrity": "sha512-nRJGvRyEXDtINlHTW+C2oWcL3vmX1URVxAPpkD3Zwn5Rb/vEeOU/pk/w97I0iid816MR4iVbvl1XhbUVegK9gQ==", "integrity": "sha512-rwxCS6vTh2DJkNIYQX7JSrrRSmm76e2y48aZeuZO5ShkvLWMuJ3/zqDHRxAQVDiJhuE2Cp8NrTmPYlUc9YrT0A==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4" "@tiptap/core": "3.28.0"
} }
}, },
"node_modules/@tiptap/extension-unique-id": { "node_modules/@tiptap/extension-unique-id": {
@@ -1877,23 +1747,23 @@
} }
}, },
"node_modules/@tiptap/extensions": { "node_modules/@tiptap/extensions": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/extensions/-/extensions-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/extensions/-/extensions-3.28.0.tgz",
"integrity": "sha512-d8opkg2iGtVwJmNGIqv0blfRxnvWOJp1brz+Z8CsP4ojSS2ZtaE46d6JSQ5OeJ7nMpjhT+9wh4UQcA7OSEO59w==", "integrity": "sha512-DJT1khCK+O/pT1gQlAnoKAx6zwDkgv7GtnhSfkqm1/4KHC3x+SSJnBIgBl9oGHymA+DxWbW1EULU4V3d/kT2uQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4" "@tiptap/pm": "3.28.0"
} }
}, },
"node_modules/@tiptap/pm": { "node_modules/@tiptap/pm": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.28.0.tgz",
"integrity": "sha512-UB8lcyomfWk7YGI2PZKNqcYXfyRA+PFj+QntlsUXyrsiA5JJIaE8SHKYjxKlGG/xtW3EtPm1b0p38T9Mk4xiFw==", "integrity": "sha512-ALcpwZMUdat9gjJKlpscpoqXStoLhU246LPEVBDvJdIsoUKvUu3MrzfXik2Y8mtSGfhjtm9O2TRkWxQiFVMwsQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.4.1", "prosemirror-changeset": "^2.4.1",
@@ -1916,35 +1786,35 @@
} }
}, },
"node_modules/@tiptap/starter-kit": { "node_modules/@tiptap/starter-kit": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/starter-kit/-/starter-kit-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/starter-kit/-/starter-kit-3.28.0.tgz",
"integrity": "sha512-/sb6rFxNt5BO4hWpUwvHh+Yh1kNyCQuuz3oDpGef5HUUjSdu9p9rfNiHWIUKBadK8VXuw5es7N+UlZ4hma+gvA==", "integrity": "sha512-pqvFpValV+ONtlu3l4s73ROm/tEjoGMmt6uHmSJaLbqTcMHkdWuR3flJ/5brr4IDHe1foxmNicMbRxlje7yBYw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "^3.27.4", "@tiptap/core": "^3.28.0",
"@tiptap/extension-blockquote": "^3.27.4", "@tiptap/extension-blockquote": "^3.28.0",
"@tiptap/extension-bold": "^3.27.4", "@tiptap/extension-bold": "^3.28.0",
"@tiptap/extension-bullet-list": "^3.27.4", "@tiptap/extension-bullet-list": "^3.28.0",
"@tiptap/extension-code": "^3.27.4", "@tiptap/extension-code": "^3.28.0",
"@tiptap/extension-code-block": "^3.27.4", "@tiptap/extension-code-block": "^3.28.0",
"@tiptap/extension-document": "^3.27.4", "@tiptap/extension-document": "^3.28.0",
"@tiptap/extension-dropcursor": "^3.27.4", "@tiptap/extension-dropcursor": "^3.28.0",
"@tiptap/extension-gapcursor": "^3.27.4", "@tiptap/extension-gapcursor": "^3.28.0",
"@tiptap/extension-hard-break": "^3.27.4", "@tiptap/extension-hard-break": "^3.28.0",
"@tiptap/extension-heading": "^3.27.4", "@tiptap/extension-heading": "^3.28.0",
"@tiptap/extension-horizontal-rule": "^3.27.4", "@tiptap/extension-horizontal-rule": "^3.28.0",
"@tiptap/extension-italic": "^3.27.4", "@tiptap/extension-italic": "^3.28.0",
"@tiptap/extension-link": "^3.27.4", "@tiptap/extension-link": "^3.28.0",
"@tiptap/extension-list": "^3.27.4", "@tiptap/extension-list": "^3.28.0",
"@tiptap/extension-list-item": "^3.27.4", "@tiptap/extension-list-item": "^3.28.0",
"@tiptap/extension-list-keymap": "^3.27.4", "@tiptap/extension-list-keymap": "^3.28.0",
"@tiptap/extension-ordered-list": "^3.27.4", "@tiptap/extension-ordered-list": "^3.28.0",
"@tiptap/extension-paragraph": "^3.27.4", "@tiptap/extension-paragraph": "^3.28.0",
"@tiptap/extension-strike": "^3.27.4", "@tiptap/extension-strike": "^3.28.0",
"@tiptap/extension-text": "^3.27.4", "@tiptap/extension-text": "^3.28.0",
"@tiptap/extension-underline": "^3.27.4", "@tiptap/extension-underline": "^3.28.0",
"@tiptap/extensions": "^3.27.4", "@tiptap/extensions": "^3.28.0",
"@tiptap/pm": "^3.27.4" "@tiptap/pm": "^3.28.0"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1966,22 +1836,22 @@
} }
}, },
"node_modules/@tiptap/vue-3": { "node_modules/@tiptap/vue-3": {
"version": "3.27.4", "version": "3.28.0",
"resolved": "https://registry.npmmirror.com/@tiptap/vue-3/-/vue-3-3.27.4.tgz", "resolved": "https://registry.npmmirror.com/@tiptap/vue-3/-/vue-3-3.28.0.tgz",
"integrity": "sha512-fTz21viWZlpZz7PPdHJde+kzYmgRXBivVLoC2KGEeltqsREa8JR0v/eUDbzeMjj6AIajlNDtmn96I3h14gZJLg==", "integrity": "sha512-twb3T6hofM0ajncCSgrLYIjfn86WfR2NVXteurdNOcnkJCIMTij5OhO/xn0z7y8+lp3TMkF6H5kajUpivBpnzw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.27.4", "@tiptap/extension-bubble-menu": "^3.28.0",
"@tiptap/extension-floating-menu": "^3.27.4" "@tiptap/extension-floating-menu": "^3.28.0"
}, },
"peerDependencies": { "peerDependencies": {
"@floating-ui/dom": "^1.0.0", "@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.27.4", "@tiptap/core": "3.28.0",
"@tiptap/pm": "3.27.4", "@tiptap/pm": "3.28.0",
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
@@ -7229,7 +7099,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
+8
View File
@@ -11,6 +11,14 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@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", "@umoteam/editor": "^10.2.1",
"axios": "^1.13.1", "axios": "^1.13.1",
"chart": "^0.1.2", "chart": "^0.1.2",
+26 -23
View File
@@ -379,9 +379,9 @@ onMounted(async () => {
.welcome-section { .welcome-section {
margin-bottom: 16px; margin-bottom: 16px;
padding: 32px; padding: 32px;
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px; 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-content {
.welcome-title { .welcome-title {
@@ -408,63 +408,65 @@ onMounted(async () => {
.stat-card { .stat-card {
background: var(--el-bg-color); background: var(--el-bg-color);
border-radius: 12px; border-radius: 16px;
padding: 24px; padding: 28px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 20px;
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: all 0.3s; transition: all 0.3s ease;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
transform: translateY(-4px); transform: translateY(-6px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
border-color: var(--el-color-primary-light-7); border-color: var(--el-color-primary-light-7);
} }
.stat-icon-wrapper { .stat-icon-wrapper {
width: 56px; width: 60px;
height: 56px; height: 60px;
border-radius: 12px; border-radius: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.el-icon { .el-icon {
color: #fff; color: #fff;
font-size: 28px;
} }
} }
&.income .stat-icon-wrapper { &.income .stat-icon-wrapper {
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} }
&.users .stat-icon-wrapper { &.users .stat-icon-wrapper {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%); background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
} }
&.orders .stat-icon-wrapper { &.orders .stat-icon-wrapper {
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%); background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
} }
&.active .stat-icon-wrapper { &.active .stat-icon-wrapper {
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
} }
&.knowledge .stat-icon-wrapper { &.knowledge .stat-icon-wrapper {
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} }
&.employees .stat-icon-wrapper { &.employees .stat-icon-wrapper {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%); background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
} }
&.tenants .stat-icon-wrapper { &.tenants .stat-icon-wrapper {
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%); background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
} }
.stat-content { .stat-content {
@@ -692,15 +694,16 @@ onMounted(async () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; color: #fff;
font-weight: 500;
&.operation { &.operation {
background-color: rgba(79, 132, 255, 0.2); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #4f84ff; color: #fff;
} }
&.access { &.access {
background-color: rgba(85, 190, 130, 0.2); background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: #55be82; color: #fff;
} }
} }
+3 -3
View File
@@ -29,14 +29,14 @@ export default defineConfig({
server: { server: {
host: "127.0.0.1", host: "127.0.0.1",
port: 4000, port: 4000,
// 开发时前端在 5000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=8081 // 开发时前端在 4000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000
proxy: { proxy: {
"/platform": { "/platform": {
target: "http://127.0.0.1:8081", target: "http://127.0.0.1:9000",
changeOrigin: true, changeOrigin: true,
}, },
"/backend": { "/backend": {
target: "http://127.0.0.1:8081", target: "http://127.0.0.1:9000",
changeOrigin: true, changeOrigin: true,
}, },
}, },
+11 -6
View File
@@ -1,13 +1,13 @@
{ {
"name" : "uniapp", "name" : "uniapp",
"appid" : "", "appid" : "__UNI__D80959E",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
"transformPx" : false, "transformPx" : false,
"uniStatistics": { "uniStatistics" : {
"enable": false, "enable" : false,
"debug": false "debug" : false
}, },
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
@@ -57,7 +57,9 @@
"mp-weixin" : { "mp-weixin" : {
"appid" : "", "appid" : "",
"setting" : { "setting" : {
"urlCheck" : false "urlCheck" : false,
"minified" : true,
"postcss" : true
}, },
"usingComponents" : true, "usingComponents" : true,
"mergeVirtualHostAttributes" : true "mergeVirtualHostAttributes" : true
@@ -74,5 +76,8 @@
"usingComponents" : true, "usingComponents" : true,
"mergeVirtualHostAttributes" : true "mergeVirtualHostAttributes" : true
}, },
"vueVersion" : "3" "vueVersion" : "3",
"h5" : {
"title" : "云泽系统"
}
} }
+18 -68
View File
@@ -1,17 +1,8 @@
<template> <template>
<view class="page"> <view class="page">
<view class="header"> <view class="header">
<view class="header-row">
<view class="user-info">
<text class="greeting">{{ greeting }}</text>
<text class="username">{{ user?.nickname || '云泽用户' }}</text>
</view>
<view class="avatar">
<text class="avatar-text">{{ avatarText }}</text>
</view>
</view>
<view class="search-bar"> <view class="search-bar">
<FaIcon name="magnifying-glass" color="#909399" :size="16" /> <FaIcon name="magnifying-glass" color="#ffffff" :size="16" />
<text class="search-text">搜索功能数据...</text> <text class="search-text">搜索功能数据...</text>
</view> </view>
</view> </view>
@@ -84,18 +75,6 @@ import AppTabbar from '@/components/AppTabbar.vue'
const user = ref(null) 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 noteCount = ref(0)
const scheduleCount = ref(0) const scheduleCount = ref(0)
const schedulePending = ref(0) const schedulePending = ref(0)
@@ -199,60 +178,25 @@ function onActivityTap(item) {
.header { .header {
flex-shrink: 0; flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 24rpx; padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 20rpx;
background: $color-card; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
box-shadow: $shadow-header; box-shadow: 0 4rpx 16rpx rgba(79, 172, 254, 0.15);
} position: relative;
.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;
} }
.search-bar { .search-bar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
background: $color-bg-page; background: rgba(255, 255, 255, 0.2);
border-radius: $radius-md; border-radius: $radius-full;
padding: 20rpx 24rpx; padding: 16rpx 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.3);
} }
.search-text { .search-text {
font-size: 28rpx; font-size: 28rpx;
color: $color-text-muted; color: rgba(255, 255, 255, 0.7);
} }
.main-scroll { .main-scroll {
@@ -344,8 +288,14 @@ function onActivityTap(item) {
} }
.quick-icon { .quick-icon {
@include icon-box(88rpx); width: 88rpx;
border-radius: $radius-lg; 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 { .quick-name {
+3 -3
View File
@@ -242,7 +242,7 @@ function onItemTap(item) {
transform: translateY(-50%); transform: translateY(-50%);
width: 6rpx; width: 6rpx;
height: 36rpx; height: 36rpx;
background: $color-primary; background: linear-gradient(180deg, #4facfe 0%, #00f2fe 100%);
border-radius: 0 4rpx 4rpx 0; border-radius: 0 4rpx 4rpx 0;
} }
} }
@@ -270,10 +270,10 @@ function onItemTap(item) {
border-radius: $radius-md; border-radius: $radius-md;
&.active { &.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 { .item-name {
color: $color-primary; color: #4facfe;
} }
} }
+77 -21
View File
@@ -22,7 +22,7 @@
</view> </view>
</view> </view>
<view class="main-scroll"> <view class="main-scroll" style="margin-top:10rpx">
<view class="menu-group" v-for="(group, gIndex) in menuGroups" :key="gIndex"> <view class="menu-group" v-for="(group, gIndex) in menuGroups" :key="gIndex">
<view <view
class="menu-item" class="menu-item"
@@ -167,26 +167,55 @@ function handleLogout() {
.profile-header { .profile-header {
flex-shrink: 0; flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 36rpx; padding: calc(var(--status-bar-height, 44px) + 24rpx) $page-padding-x 40rpx;
background: $color-primary; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 400rpx;
height: 400rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
pointer-events: none;
}
.profile-header::after {
content: '';
position: absolute;
bottom: -30%;
left: -20%;
width: 350rpx;
height: 350rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
pointer-events: none;
} }
.profile-info { .profile-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24rpx; gap: 24rpx;
position: relative;
z-index: 1;
} }
.profile-avatar { .profile-avatar {
width: 112rpx; width: 120rpx;
height: 112rpx; height: 120rpx;
border-radius: 50%; border-radius: 50%;
background: rgba(255, 255, 255, 0.2); background: linear-gradient(135deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.15) 100%);
border: 4rpx solid rgba(255, 255, 255, 0.35); border: 3rpx solid rgba(255, 255, 255, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.15);
} }
.avatar-text { .avatar-text {
@@ -231,9 +260,11 @@ function handleLogout() {
.profile-stats { .profile-stats {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin-top: 36rpx; margin-top: 40rpx;
padding-top: 28rpx; padding-top: 32rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.2); border-top: 1rpx solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 1;
} }
.profile-stat { .profile-stat {
@@ -263,13 +294,21 @@ function handleLogout() {
@include card; @include card;
margin-bottom: 24rpx; margin-bottom: 24rpx;
overflow: hidden; overflow: hidden;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s ease;
}
.menu-group:active {
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
} }
.menu-item { .menu-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 28rpx 24rpx; padding: 32rpx 28rpx;
transition: background 0.2s ease, transform 0.2s ease;
& + & { & + & {
border-top: 1rpx solid $color-divider; border-top: 1rpx solid $color-divider;
@@ -277,6 +316,7 @@ function handleLogout() {
&:active { &:active {
background: $color-bg-page; background: $color-bg-page;
transform: translateX(4rpx);
} }
} }
@@ -289,11 +329,19 @@ function handleLogout() {
} }
.menu-icon { .menu-icon {
@include icon-box(64rpx); width: 56rpx;
height: 56rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, rgba(79, 172, 254, 0.1) 0%, rgba(0, 242, 254, 0.1) 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
} }
.menu-title { .menu-title {
font-size: 28rpx; font-size: 30rpx;
font-weight: 500;
color: $color-text; color: $color-text;
} }
@@ -306,31 +354,39 @@ function handleLogout() {
.menu-badge { .menu-badge {
font-size: 20rpx; font-size: 20rpx;
font-weight: 600;
color: #ffffff; color: #ffffff;
background: $color-primary; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
padding: 4rpx 12rpx; padding: 6rpx 14rpx;
border-radius: $radius-full; border-radius: 12rpx;
min-width: 32rpx; min-width: 36rpx;
text-align: center; text-align: center;
line-height: 1.4; line-height: 1.4;
box-shadow: 0 4rpx 12rpx rgba(79, 172, 254, 0.3);
} }
.logout-btn { .logout-btn {
@include card; @include card;
height: 96rpx; height: 100rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 8rpx; margin-bottom: 12rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, rgba(245, 108, 108, 0.1) 0%, rgba(245, 108, 108, 0.05) 100%);
border: 1rpx solid rgba(245, 108, 108, 0.2);
transition: all 0.3s ease;
} }
.logout-btn-hover { .logout-btn-hover {
background: $color-bg-page !important; background: linear-gradient(135deg, rgba(245, 108, 108, 0.2) 0%, rgba(245, 108, 108, 0.1) 100%) !important;
box-shadow: 0 4rpx 16rpx rgba(245, 108, 108, 0.15) !important;
} }
.logout-text { .logout-text {
font-size: 30rpx; font-size: 32rpx;
color: $color-text-secondary; font-weight: 600;
color: #f56c6c;
pointer-events: none; pointer-events: none;
} }
+18 -87
View File
@@ -15,29 +15,14 @@
<view class="field content-field"> <view class="field content-field">
<text class="field-label">内容</text> <text class="field-label">内容</text>
<view class="editor-wrap"> <editor
<editor id="noteEditor"
id="noteEditor" class="note-editor"
class="note-editor" :placeholder="'记录你的想法...'"
:placeholder="'记录你的想法...'" :value="form.content"
:value="form.content" @ready="onEditorReady"
@ready="onEditorReady" @input="onEditorInput"
@input="onEditorInput" />
@focus="editorFocused = true"
@blur="editorFocused = false"
/>
<view class="editor-toolbar">
<view
v-for="btn in toolbarBtns"
:key="btn.name"
class="toolbar-btn"
:class="{ active: btn.active }"
@tap="execCommand(btn)"
>
<text>{{ btn.label }}</text>
</view>
</view>
</view>
</view> </view>
<view class="switch-row" @tap="form.pinned = !form.pinned"> <view class="switch-row" @tap="form.pinned = !form.pinned">
@@ -71,7 +56,6 @@ import { formatDate } from '@/utils/date.js'
const noteId = ref('') const noteId = ref('')
const saving = ref(false) const saving = ref(false)
const editorCtx = ref(null) const editorCtx = ref(null)
const editorFocused = ref(false)
const form = reactive({ const form = reactive({
title: '', title: '',
content: '', content: '',
@@ -81,28 +65,6 @@ const meta = reactive({
updatedAt: 0 updatedAt: 0
}) })
const toolbarBtns = reactive([
{ name: 'bold', label: 'B', active: false, value: 'bold' },
{ name: 'italic', label: 'I', active: false, value: 'italic' },
{ name: 'underline', label: 'U', active: false, value: 'underline' },
{ name: 'strike', label: 'S', active: false, value: 'strikeThrough' },
{ name: 'header', label: 'H', active: false, value: 'header' },
{ name: 'list', label: '•', active: false, value: 'insertUnorderedList' },
{ name: 'indent', label: '→', active: false, value: 'indent' },
{ name: 'outdent', label: '←', active: false, value: 'outdent' },
{ name: 'divider', label: '—', active: false, value: 'insertHorizontalRule' },
])
onLoad(async (query) => {
if (query?.id) {
noteId.value = query.id
uni.setNavigationBarTitle({ title: '编辑笔记' })
await loadNote(query.id)
} else {
uni.setNavigationBarTitle({ title: '新建笔记' })
}
})
function onEditorReady() { function onEditorReady() {
uni.createSelectorQuery().select('#noteEditor').context((res) => { uni.createSelectorQuery().select('#noteEditor').context((res) => {
if (res && res.context) { if (res && res.context) {
@@ -118,14 +80,15 @@ function onEditorInput(e) {
form.content = e.detail.html || '' form.content = e.detail.html || ''
} }
function execCommand(btn) { onLoad(async (query) => {
if (!editorCtx.value) return if (query?.id) {
if (btn.name === 'header') { noteId.value = query.id
editorCtx.value.format('header', 'H2') uni.setNavigationBarTitle({ title: '编辑笔记' })
await loadNote(query.id)
} else { } else {
editorCtx.value.format(btn.value) uni.setNavigationBarTitle({ title: '新建笔记' })
} }
} })
async function loadNote(id) { async function loadNote(id) {
try { try {
@@ -246,17 +209,9 @@ function onDelete() {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-bottom: 0; padding: 24rpx 28rpx 0 28rpx;
overflow: hidden; overflow: hidden;
} border-top: 1rpx solid $color-divider;
.editor-wrap {
background: $color-bg-page;
border-radius: $radius-md;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
} }
.note-editor { .note-editor {
@@ -268,32 +223,8 @@ function onDelete() {
color: $color-text; color: $color-text;
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto; overflow-y: auto;
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
padding: 16rpx 20rpx;
border-top: 1rpx solid $color-divider;
background: $color-card;
}
.toolbar-btn {
padding: 10rpx 20rpx;
border-radius: $radius-sm;
font-size: 24rpx;
color: $color-text-secondary;
background: $color-bg-page; background: $color-bg-page;
border-radius: $radius-md;
&.active {
color: $color-primary;
background: $color-primary-bg;
}
&:active {
opacity: 0.7;
}
} }
.placeholder { .placeholder {
+95 -3
View File
@@ -65,6 +65,38 @@
</view> </view>
</picker> </picker>
</view> </view>
<view class="field">
<text class="field-label">重复提醒间隔分钟</text>
<view class="number-input">
<button class="number-btn" @tap="form.repeatIntervalMinutes = Math.max(0, form.repeatIntervalMinutes - 1)" :disabled="isCompleted"></button>
<input
v-model.number="form.repeatIntervalMinutes"
type="number"
class="number-field"
:disabled="isCompleted"
@blur="form.repeatIntervalMinutes = Math.max(0, Math.min(1440, form.repeatIntervalMinutes))"
/>
<button class="number-btn" @tap="form.repeatIntervalMinutes = Math.min(1440, form.repeatIntervalMinutes + 1)" :disabled="isCompleted">+</button>
</view>
<text class="form-tip">0表示不重复仅发送一次</text>
</view>
<view class="field">
<text class="field-label">最多提醒次数</text>
<view class="number-input">
<button class="number-btn" @tap="form.maxSendCount = Math.max(1, form.maxSendCount - 1)" :disabled="isCompleted"></button>
<input
v-model.number="form.maxSendCount"
type="number"
class="number-field"
:disabled="isCompleted"
@blur="form.maxSendCount = Math.max(1, Math.min(100, form.maxSendCount))"
/>
<button class="number-btn" @tap="form.maxSendCount = Math.min(100, form.maxSendCount + 1)" :disabled="isCompleted">+</button>
</view>
<text class="form-tip">最多发送的提醒次数</text>
</view>
</view> </view>
<view v-if="!isCompleted" class="footer"> <view v-if="!isCompleted" class="footer">
@@ -99,7 +131,9 @@ const form = reactive({
date: '', date: '',
time: '', time: '',
remindChannels: ['app'], remindChannels: ['app'],
remindMinutes: 15 remindMinutes: 0,
repeatIntervalMinutes: 0,
maxSendCount: 1
}) })
const remindIndex = computed(() => { const remindIndex = computed(() => {
@@ -134,7 +168,9 @@ async function loadSchedule(id) {
form.date = formatDate(d) form.date = formatDate(d)
form.time = `${pad(d.getHours())}:${pad(d.getMinutes())}` form.time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
form.remindChannels = [...(item.remindChannels || [])] form.remindChannels = [...(item.remindChannels || [])]
form.remindMinutes = item.remindMinutes ?? 15 form.remindMinutes = item.remindMinutes ?? 0
form.repeatIntervalMinutes = item.repeatIntervalMinutes ?? 0
form.maxSendCount = item.maxSendCount ?? 1
isCompleted.value = !!item.completed isCompleted.value = !!item.completed
} catch (e) { } catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' }) uni.showToast({ title: '加载失败', icon: 'none' })
@@ -200,7 +236,9 @@ async function saveData(datetime) {
content: form.content, content: form.content,
datetime, datetime,
remindChannels: [...form.remindChannels], remindChannels: [...form.remindChannels],
remindMinutes: form.remindMinutes remindMinutes: form.remindMinutes,
repeatIntervalMinutes: form.repeatIntervalMinutes,
maxSendCount: form.maxSendCount
} }
try { try {
if (scheduleId.value) { if (scheduleId.value) {
@@ -334,6 +372,60 @@ async function saveData(datetime) {
} }
} }
.number-input {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.number-btn {
width: 60rpx;
height: 60rpx;
border-radius: $radius-md;
background: $color-bg-page;
border: 1rpx solid $color-divider;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $color-text;
flex-shrink: 0;
&:active:not(:disabled) {
background: $color-primary-bg;
color: $color-primary;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.number-field {
flex: 1;
height: 60rpx;
padding: 0 16rpx;
background: $color-bg-page;
border: 1rpx solid $color-divider;
border-radius: $radius-md;
font-size: 28rpx;
color: $color-text;
text-align: center;
&:disabled {
opacity: 0.5;
}
}
.form-tip {
display: block;
font-size: 22rpx;
color: $color-text-muted;
margin-top: 8rpx;
}
.footer { .footer {
position: fixed; position: fixed;
left: 0; left: 0;
+4 -4
View File
@@ -1,7 +1,7 @@
// 标准商务主题单色 + 灰阶无渐变 // 标准商务主题色系 + 灰阶
$color-primary: #3c9cff; $color-primary: #4facfe;
$color-primary-dark: #2b8ae8; $color-primary-dark: #00f2fe;
$color-primary-bg: #ecf5ff; $color-primary-bg: #e0f7ff;
$color-bg-page: #f5f7fa; $color-bg-page: #f5f7fa;
$color-card: #ffffff; $color-card: #ffffff;
$color-text: #303133; $color-text: #303133;
+15 -28
View File
@@ -1,29 +1,16 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni' import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig(({ mode }) => { export default defineConfig({
const env = loadEnv(mode, process.cwd(), '') plugins: [
const apiTarget = (env.VITE_API_BASE_URL || 'http://localhost:9000').replace(/\/$/, '') uni()
],
return { css: {
plugins: [uni()], preprocessorOptions: {
server: { scss: {
proxy: { // 关闭废弃 API 警告
// 勿用 /api:会与 uniapp/api/ 源码目录冲突,导致 request.js 等模块 404 silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'],
'/proxy-api': { }
target: apiTarget, }
changeOrigin: true, }
rewrite: (path) => path.replace(/^\/proxy-api/, '') })
}
}
},
css: {
preprocessorOptions: {
scss: {
// 关闭废弃 API 警告
silenceDeprecations: ['legacy-js-api', 'color-functions', 'import']
}
}
}
}
})