修复日程提醒功能

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