feat: opt message pages

This commit is contained in:
engigu
2026-03-09 22:30:05 +08:00
parent 564f39133f
commit 1584528bdf
6 changed files with 82 additions and 33 deletions
+11 -10
View File
@@ -6,16 +6,17 @@ import (
// AppLog 统一应用日志与通知记录
type AppLog struct {
ID string `json:"id" gorm:"primaryKey;size:20"`
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
Title string `json:"title" gorm:"size:255"` // 消息标题
Content BigText `json:"content"` // 详细内容/Payload
Level string `json:"level" gorm:"size:20;index"` // 级别:constant.LogLevelInfo, constant.LogLevelWarning, constant.LogLevelError
Status string `json:"status" gorm:"size:20;index"` // 状态:系统通知为 constant.LogStatusRead/constant.LogStatusUnread,推送为 constant.LogStatusSuccess/constant.LogStatusFailed
RefID string `json:"ref_id" gorm:"size:50;index"` // 关联对象ID(选填,比如绑定的通知渠道ID、任务ID等)
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
CreatedAt LocalTime `json:"created_at" gorm:"index"`
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
ID string `json:"id" gorm:"primaryKey;size:20"`
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
Title string `json:"title" gorm:"size:255"` // 消息标题
Content BigText `json:"content"` // 详细内容/Payload
Level string `json:"level" gorm:"size:20;index"` // 级别:constant.LogLevelInfo, constant.LogLevelWarning, constant.LogLevelError
Status string `json:"status" gorm:"size:20;index"` // 状态:系统通知为 constant.LogStatusRead/constant.LogStatusUnread,推送为 constant.LogStatusSuccess/constant.LogStatusFailed
RefID string `json:"ref_id" gorm:"size:50;index"` // 关联对象ID(选填,比如绑定的通知渠道ID、任务ID等)
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
CreatedAt LocalTime `json:"created_at" gorm:"index"`
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
ChannelName string `json:"channel_name" gorm:"-"` // 推送记录的关联渠道名称(动态查询)
}
func (AppLog) TableName() string {
+39 -9
View File
@@ -38,24 +38,53 @@ func (s *AppLogService) Add(log *models.AppLog) error {
func (s *AppLogService) List(category string, status string, level string, page, pageSize int, keyword string) ([]models.AppLog, int64, error) {
var logs []models.AppLog
var total int64
query := database.DB.Model(&models.AppLog{})
db := database.DB
// 如果是推送日志,尝试关联查询渠道名称
if category == constant.LogCategoryPushLog {
db = db.Table(models.AppLog{}.TableName() + " AS al").
Select("al.*, nw.name as channel_name").
Joins(fmt.Sprintf("LEFT JOIN %s AS nw ON al.ref_id = nw.id", models.NotifyWay{}.TableName()))
} else {
db = db.Model(&models.AppLog{})
}
if category != "" {
query = query.Where("category = ?", category)
if category == constant.LogCategoryPushLog {
db = db.Where("al.category = ?", category)
} else {
db = db.Where("category = ?", category)
}
}
if status != "" {
query = query.Where("status = ?", status)
field := "status"
if category == constant.LogCategoryPushLog {
field = "al.status"
}
db = db.Where(field+" = ?", status)
}
if level != "" {
query = query.Where("level = ?", level)
field := "level"
if category == constant.LogCategoryPushLog {
field = "al.level"
}
db = db.Where(field+" = ?", level)
}
if keyword != "" {
query = query.Where("(title LIKE ? OR content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
if category == constant.LogCategoryPushLog {
db = db.Where("(al.title LIKE ? OR al.content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
} else {
db = db.Where("(title LIKE ? OR content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
}
}
query.Count(&total)
db.Count(&total)
offset := (page - 1) * pageSize
err := query.Order("created_at desc").Offset(offset).Limit(pageSize).Find(&logs).Error
order := "created_at desc"
if category == constant.LogCategoryPushLog {
order = "al.created_at desc"
}
err := db.Order(order).Offset(offset).Limit(pageSize).Scan(&logs).Error
return logs, total, err
}
@@ -171,7 +200,7 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
content, _ := payload["content"].(string)
success, _ := payload["success"].(bool)
errorMsg, _ := payload["error_msg"].(string)
channelName, _ := payload["channel_name"].(string)
channelID, _ := payload["channel_id"].(string)
status := constant.LogStatusSuccess
level := constant.LogLevelInfo
@@ -182,10 +211,11 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
s.Add(&models.AppLog{
Category: constant.LogCategoryPushLog,
Title: fmt.Sprintf("[%s] %s", channelName, title),
Title: title,
Content: models.BigText(content),
Level: level,
Status: status,
RefID: channelID,
ErrorMsg: models.BigText(errorMsg),
})
})
+3 -2
View File
@@ -203,15 +203,16 @@ func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMe
Title: msg.Title,
Text: msg.Text,
})
payload := map[string]interface{}{
"title": msg.Title,
"content": msg.Text,
"channel_id": channel.ID,
"channel_name": channel.Name,
"success": false,
"error_msg": "",
}
if err != nil {
payload["error_msg"] = err.Error()
eventbus.DefaultBus.Publish(eventbus.Event{
+1
View File
@@ -589,6 +589,7 @@ export interface AppLog {
level: string
status: string
ref_id: string
channel_name?: string
error_msg: string
created_at: string
read_at: string | null
@@ -289,9 +289,11 @@ function onDialogClose(open: boolean) {
内容详情
</div>
<div class="p-6">
<pre v-if="detailDialogProps.content"
class="text-xs font-mono bg-muted/30 p-4 rounded-lg border border-muted/50 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.content }}</pre>
<div v-else class="text-xs text-muted-foreground italic py-2">无内容</div>
<div v-if="detailDialogProps.content"
class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
{{ detailDialogProps.content }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无内容</div>
</div>
<template v-if="detailDialogProps.error">
@@ -300,8 +302,11 @@ function onDialogClose(open: boolean) {
错误信息
</div>
<div class="p-6">
<pre
class="text-xs font-mono bg-red-500/5 text-red-600/90 p-4 rounded-lg border border-red-500/20 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.error }}</pre>
<div v-if="detailDialogProps.error"
class="text-sm text-red-700 bg-red-500/10 p-5 rounded-xl border border-red-200/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
{{ detailDialogProps.error }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无错误信息</div>
</div>
</template>
</div>
+18 -7
View File
@@ -226,8 +226,10 @@ function onDialogClose(open: boolean) {
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
<span class="w-12 sm:w-16 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ getLogIndex(index) }}</span>
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">{{ log.title
}}</span>
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">
<span v-if="log.channel_name" class="mr-1 text-muted-foreground">[{{ log.channel_name }}]</span>{{ log.title
}}
</span>
<span class="hidden sm:flex sm:flex-1 text-xs sm:text-sm text-muted-foreground truncate" :title="log.content">
{{ log.content || '-' }}
</span>
@@ -270,6 +272,10 @@ function onDialogClose(open: boolean) {
<span class="text-muted-foreground">标题</span>
<span class="font-medium text-foreground">{{ detailDialogProps.title }}</span>
</div>
<div v-if="selectedLog?.channel_name" class="flex justify-between items-center text-sm">
<span class="text-muted-foreground">发送渠道</span>
<span class="font-medium text-foreground">{{ selectedLog.channel_name }}</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-muted-foreground">发生时间</span>
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ? formatDate(selectedLog.created_at)
@@ -284,9 +290,11 @@ function onDialogClose(open: boolean) {
推送内容
</div>
<div class="p-6">
<pre v-if="detailDialogProps.content"
class="text-xs font-mono bg-muted/30 p-4 rounded-lg border border-muted/50 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.content }}</pre>
<div v-else class="text-xs text-muted-foreground italic py-2">无推送内容</div>
<div v-if="detailDialogProps.content"
class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
{{ detailDialogProps.content }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无推送内容</div>
</div>
<template v-if="detailDialogProps.error">
@@ -295,8 +303,11 @@ function onDialogClose(open: boolean) {
错误信息
</div>
<div class="p-6">
<pre
class="text-xs font-mono bg-red-500/5 text-red-600/90 p-4 rounded-lg border border-red-500/20 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.error }}</pre>
<div v-if="detailDialogProps.error"
class="text-sm text-red-700 bg-red-500/10 p-5 rounded-xl border border-red-200/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
{{ detailDialogProps.error }}
</div>
<div v-else class="text-sm text-muted-foreground italic py-2">无错误信息</div>
</div>
</template>
</div>