feat: cron task style fix
This commit is contained in:
@@ -13,5 +13,6 @@ func Migrate() error {
|
|||||||
&models.EnvironmentVariable{},
|
&models.EnvironmentVariable{},
|
||||||
&models.Setting{},
|
&models.Setting{},
|
||||||
&models.LoginLog{},
|
&models.LoginLog{},
|
||||||
|
&models.SendStats{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"baihu/internal/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendStats 任务执行统计
|
||||||
|
type SendStats struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
TaskID uint `json:"task_id" gorm:"index"`
|
||||||
|
Status string `json:"status" gorm:"size:20;not null"` // success, failed
|
||||||
|
Num int `json:"num" gorm:"default:0"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SendStats) TableName() string {
|
||||||
|
return constant.TablePrefix + "send_stats"
|
||||||
|
}
|
||||||
@@ -151,7 +151,7 @@ func Setup(c *Controllers) *gin.Engine {
|
|||||||
files.POST("/delete", c.File.DeleteFile)
|
files.POST("/delete", c.File.DeleteFile)
|
||||||
files.POST("/rename", c.File.RenameFile)
|
files.POST("/rename", c.File.RenameFile)
|
||||||
files.POST("/upload", c.File.UploadArchive)
|
files.POST("/upload", c.File.UploadArchive)
|
||||||
files.POST("/upload-files", c.File.UploadFiles)
|
files.POST("/uploadfiles", c.File.UploadFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log routes
|
// Log routes
|
||||||
@@ -169,11 +169,11 @@ func Setup(c *Controllers) *gin.Engine {
|
|||||||
settings := authorized.Group("/settings")
|
settings := authorized.Group("/settings")
|
||||||
{
|
{
|
||||||
settings.POST("/password", c.Settings.ChangePassword)
|
settings.POST("/password", c.Settings.ChangePassword)
|
||||||
settings.POST("/clean-logs", c.Settings.CleanLogs)
|
settings.POST("/cleanlogs", c.Settings.CleanLogs)
|
||||||
settings.GET("/site", c.Settings.GetSiteSettings)
|
settings.GET("/site", c.Settings.GetSiteSettings)
|
||||||
settings.PUT("/site", c.Settings.UpdateSiteSettings)
|
settings.PUT("/site", c.Settings.UpdateSiteSettings)
|
||||||
settings.GET("/about", c.Settings.GetAbout)
|
settings.GET("/about", c.Settings.GetAbout)
|
||||||
settings.GET("/login-logs", c.Settings.GetLoginLogs)
|
settings.GET("/loginlogs", c.Settings.GetLoginLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,91 @@ type ExecutionResult struct {
|
|||||||
End time.Time
|
End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecutionCallback 任务执行完成后的回调函数类型
|
||||||
|
type ExecutionCallback func(taskID uint, command string, result *ExecutionResult)
|
||||||
|
|
||||||
// ExecutorService handles task execution
|
// ExecutorService handles task execution
|
||||||
type ExecutorService struct {
|
type ExecutorService struct {
|
||||||
taskService *TaskService
|
taskService *TaskService
|
||||||
results []ExecutionResult
|
results []ExecutionResult
|
||||||
runningTasks map[int]bool // 正在运行的任务
|
runningTasks map[int]bool
|
||||||
|
callbacks []ExecutionCallback
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExecutorService creates a new executor service
|
// NewExecutorService creates a new executor service
|
||||||
func NewExecutorService(taskService *TaskService) *ExecutorService {
|
func NewExecutorService(taskService *TaskService) *ExecutorService {
|
||||||
return &ExecutorService{
|
es := &ExecutorService{
|
||||||
taskService: taskService,
|
taskService: taskService,
|
||||||
results: make([]ExecutionResult, 0),
|
results: make([]ExecutionResult, 0),
|
||||||
runningTasks: make(map[int]bool),
|
runningTasks: make(map[int]bool),
|
||||||
|
callbacks: make([]ExecutionCallback, 0),
|
||||||
|
}
|
||||||
|
// 注册默认回调
|
||||||
|
es.RegisterCallback(es.saveTaskLogCallback)
|
||||||
|
es.RegisterCallback(es.updateStatsCallback)
|
||||||
|
return es
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCallback 注册执行完成回调
|
||||||
|
func (es *ExecutorService) RegisterCallback(cb ExecutionCallback) {
|
||||||
|
es.mu.Lock()
|
||||||
|
es.callbacks = append(es.callbacks, cb)
|
||||||
|
es.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeCallbacks 执行所有回调
|
||||||
|
func (es *ExecutorService) executeCallbacks(taskID uint, command string, result *ExecutionResult) {
|
||||||
|
es.mu.RLock()
|
||||||
|
callbacks := make([]ExecutionCallback, len(es.callbacks))
|
||||||
|
copy(callbacks, es.callbacks)
|
||||||
|
es.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, cb := range callbacks {
|
||||||
|
cb(taskID, command, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveTaskLogCallback 保存任务日志的回调
|
||||||
|
func (es *ExecutorService) saveTaskLogCallback(taskID uint, command string, result *ExecutionResult) {
|
||||||
|
output := result.Output
|
||||||
|
if result.Error != "" {
|
||||||
|
output += "\n[ERROR]\n" + result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
compressed, err := utils.CompressToBase64(output)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to compress log: %v", err)
|
||||||
|
compressed = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "success"
|
||||||
|
if !result.Success {
|
||||||
|
status = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
taskLog := &models.TaskLog{
|
||||||
|
TaskID: taskID,
|
||||||
|
Command: command,
|
||||||
|
Output: compressed,
|
||||||
|
Status: status,
|
||||||
|
Duration: result.End.Sub(result.Start).Milliseconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(taskLog).Error; err != nil {
|
||||||
|
logger.Errorf("Failed to save task log: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateStatsCallback 更新统计数据的回调
|
||||||
|
func (es *ExecutorService) updateStatsCallback(taskID uint, _ string, result *ExecutionResult) {
|
||||||
|
status := "success"
|
||||||
|
if !result.Success {
|
||||||
|
status = "failed"
|
||||||
|
}
|
||||||
|
sendStatsService := NewSendStatsService()
|
||||||
|
if err := sendStatsService.IncrementStats(taskID, status); err != nil {
|
||||||
|
logger.Errorf("Failed to update stats: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +143,8 @@ func (es *ExecutorService) ExecuteTask(taskID int) *ExecutionResult {
|
|||||||
delete(es.runningTasks, taskID)
|
delete(es.runningTasks, taskID)
|
||||||
es.mu.Unlock()
|
es.mu.Unlock()
|
||||||
|
|
||||||
// Save log to database
|
// 执行回调
|
||||||
es.saveTaskLog(uint(taskID), task.Command, result)
|
es.executeCallbacks(uint(taskID), task.Command, result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -97,11 +168,9 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim
|
|||||||
Start: time.Now(),
|
Start: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a context with timeout
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
shell, args := utils.GetShellCommand(command)
|
shell, args := utils.GetShellCommand(command)
|
||||||
cmd := exec.CommandContext(ctx, shell, args...)
|
cmd := exec.CommandContext(ctx, shell, args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -111,7 +180,6 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
result.End = time.Now()
|
result.End = time.Now()
|
||||||
|
|
||||||
// Process results
|
|
||||||
result.Output = stdout.String()
|
result.Output = stdout.String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
@@ -123,10 +191,8 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim
|
|||||||
result.Success = true
|
result.Success = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store result
|
|
||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
es.results = append(es.results, *result)
|
es.results = append(es.results, *result)
|
||||||
// Keep only the last 100 results to prevent memory issues
|
|
||||||
if len(es.results) > 100 {
|
if len(es.results) > 100 {
|
||||||
es.results = es.results[1:]
|
es.results = es.results[1:]
|
||||||
}
|
}
|
||||||
@@ -149,35 +215,3 @@ func (es *ExecutorService) GetLastResults(count int) []ExecutionResult {
|
|||||||
copy(results, es.results[start:])
|
copy(results, es.results[start:])
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveTaskLog saves execution log to database with gzip+base64 compression
|
|
||||||
func (es *ExecutorService) saveTaskLog(taskID uint, command string, result *ExecutionResult) {
|
|
||||||
output := result.Output
|
|
||||||
if result.Error != "" {
|
|
||||||
output += "\n[ERROR]\n" + result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compress output
|
|
||||||
compressed, err := utils.CompressToBase64(output)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to compress log: %v", err)
|
|
||||||
compressed = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "success"
|
|
||||||
if !result.Success {
|
|
||||||
status = "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
taskLog := &models.TaskLog{
|
|
||||||
TaskID: taskID,
|
|
||||||
Command: command,
|
|
||||||
Output: compressed,
|
|
||||||
Status: status,
|
|
||||||
Duration: result.End.Sub(result.Start).Milliseconds(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := database.DB.Create(taskLog).Error; err != nil {
|
|
||||||
logger.Errorf("Failed to save task log: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"baihu/internal/database"
|
||||||
|
"baihu/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendStatsService struct{}
|
||||||
|
|
||||||
|
func NewSendStatsService() *SendStatsService {
|
||||||
|
return &SendStatsService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementStats 增加任务执行统计
|
||||||
|
func (s *SendStatsService) IncrementStats(taskID uint, status string) error {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
startOfDay, _ := time.ParseInLocation("2006-01-02", today, time.Local)
|
||||||
|
|
||||||
|
var stats models.SendStats
|
||||||
|
result := database.DB.Where("task_id = ? AND status = ? AND created_at >= ?", taskID, status, startOfDay).First(&stats)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
// 不存在则创建
|
||||||
|
stats = models.SendStats{
|
||||||
|
TaskID: taskID,
|
||||||
|
Status: status,
|
||||||
|
Num: 1,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
return database.DB.Create(&stats).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存在则增加计数
|
||||||
|
return database.DB.Model(&stats).Update("num", stats.Num+1).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatsByTaskID 获取任务的统计数据
|
||||||
|
func (s *SendStatsService) GetStatsByTaskID(taskID uint) []models.SendStats {
|
||||||
|
var stats []models.SendStats
|
||||||
|
database.DB.Where("task_id = ?", taskID).Order("created_at DESC").Find(&stats)
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTodayStats 获取今日统计
|
||||||
|
func (s *SendStatsService) GetTodayStats() []models.SendStats {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
startOfDay, _ := time.ParseInLocation("2006-01-02", today, time.Local)
|
||||||
|
|
||||||
|
var stats []models.SendStats
|
||||||
|
database.DB.Where("created_at >= ?", startOfDay).Find(&stats)
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecentStats 获取最近N天的统计
|
||||||
|
func (s *SendStatsService) GetRecentStats(days int) []models.SendStats {
|
||||||
|
startDate := time.Now().AddDate(0, 0, -days)
|
||||||
|
|
||||||
|
var stats []models.SendStats
|
||||||
|
database.DB.Where("created_at >= ?", startDate).Order("created_at DESC").Find(&stats)
|
||||||
|
return stats
|
||||||
|
}
|
||||||
@@ -108,7 +108,7 @@ export const api = {
|
|||||||
changePassword: (data: { old_password: string; new_password: string }) =>
|
changePassword: (data: { old_password: string; new_password: string }) =>
|
||||||
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
|
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
cleanLogs: (days: number) =>
|
cleanLogs: (days: number) =>
|
||||||
request<{ deleted: number }>('/settings/clean-logs', { method: 'POST', body: JSON.stringify({ days }) }),
|
request<{ deleted: number }>('/settings/cleanlogs', { method: 'POST', body: JSON.stringify({ days }) }),
|
||||||
getSite: () => request<SiteSettings>('/settings/site'),
|
getSite: () => request<SiteSettings>('/settings/site'),
|
||||||
getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'),
|
getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'),
|
||||||
updateSite: (data: SiteSettings) =>
|
updateSite: (data: SiteSettings) =>
|
||||||
@@ -119,7 +119,7 @@ export const api = {
|
|||||||
if (params?.page) query.set('page', String(params.page))
|
if (params?.page) query.set('page', String(params.page))
|
||||||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||||||
if (params?.username) query.set('username', params.username)
|
if (params?.username) query.set('username', params.username)
|
||||||
return request<LoginLogListResponse>(`/settings/login-logs?${query}`)
|
return request<LoginLogListResponse>(`/settings/loginlogs?${query}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
@@ -157,7 +157,7 @@ export const api = {
|
|||||||
}
|
}
|
||||||
if (targetPath) formData.append('path', targetPath)
|
if (targetPath) formData.append('path', targetPath)
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}/files/upload-files`, {
|
const res = await fetch(`${BASE_URL}/files/uploadfiles`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: formData
|
body: formData
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const navItems = [
|
|||||||
{ to: '/history', icon: ScrollText, label: '执行历史', exact: true },
|
{ to: '/history', icon: ScrollText, label: '执行历史', exact: true },
|
||||||
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
||||||
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
||||||
{ to: '/login-logs', icon: KeyRound, label: '登录日志', exact: true },
|
{ to: '/loginlogs', icon: KeyRound, label: '登录日志', exact: true },
|
||||||
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const router = createRouter({
|
|||||||
{ path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') },
|
{ path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') },
|
||||||
{ path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') },
|
{ path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') },
|
||||||
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
||||||
{ path: 'login-logs', name: 'login-logs', component: () => import('@/views/login-logs/LoginLogs.vue') },
|
{ path: 'loginlogs', name: 'loginlogs', component: () => import('@/views/loginlogs/LoginLogs.vue') },
|
||||||
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
||||||
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -117,12 +117,12 @@ onMounted(loadLogs)
|
|||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<!-- 日志列表 -->
|
<!-- 日志列表 -->
|
||||||
<div class="flex-1 rounded-lg border bg-card">
|
<div class="flex-1 min-w-0 rounded-lg border bg-card">
|
||||||
<!-- 表头 -->
|
<!-- 表头 -->
|
||||||
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
|
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
|
||||||
<span class="w-12 shrink-0">ID</span>
|
<span class="w-12 shrink-0">ID</span>
|
||||||
<span class="w-32 shrink-0">任务名称</span>
|
<span class="w-32 shrink-0">任务名称</span>
|
||||||
<span class="flex-1">命令</span>
|
<span :class="selectedLog ? 'w-40 shrink-0' : 'flex-1'">命令</span>
|
||||||
<span class="w-12 shrink-0 text-center">状态</span>
|
<span class="w-12 shrink-0 text-center">状态</span>
|
||||||
<span class="w-20 text-right shrink-0">耗时</span>
|
<span class="w-20 text-right shrink-0">耗时</span>
|
||||||
<span class="w-40 text-right shrink-0">执行时间</span>
|
<span class="w-40 text-right shrink-0">执行时间</span>
|
||||||
@@ -143,7 +143,7 @@ onMounted(loadLogs)
|
|||||||
>
|
>
|
||||||
<span class="w-12 shrink-0 text-muted-foreground text-sm">#{{ log.id }}</span>
|
<span class="w-12 shrink-0 text-muted-foreground text-sm">#{{ log.id }}</span>
|
||||||
<span class="w-32 font-medium truncate shrink-0 text-sm">{{ log.task_name }}</span>
|
<span class="w-32 font-medium truncate shrink-0 text-sm">{{ log.task_name }}</span>
|
||||||
<code class="flex-1 text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded">{{ log.command }}</code>
|
<code :class="['text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded', selectedLog ? 'w-40 shrink-0' : 'flex-1']">{{ log.command }}</code>
|
||||||
<span class="w-12 flex justify-center shrink-0">
|
<span class="w-12 flex justify-center shrink-0">
|
||||||
<span :class="['w-2 h-2 rounded-full', log.status === 'success' ? 'bg-green-500' : log.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500']" />
|
<span :class="['w-2 h-2 rounded-full', log.status === 'success' ? 'bg-green-500' : log.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500']" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -58,15 +58,6 @@ function handlePageChange(page: number) {
|
|||||||
loadLogs()
|
loadLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBrowserInfo(userAgent: string): string {
|
|
||||||
if (!userAgent) return '未知'
|
|
||||||
if (userAgent.includes('Chrome')) return 'Chrome'
|
|
||||||
if (userAgent.includes('Firefox')) return 'Firefox'
|
|
||||||
if (userAgent.includes('Safari')) return 'Safari'
|
|
||||||
if (userAgent.includes('Edge')) return 'Edge'
|
|
||||||
return '其他'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadLogs)
|
onMounted(loadLogs)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -98,9 +89,8 @@ onMounted(loadLogs)
|
|||||||
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
|
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
|
||||||
<span class="w-24 shrink-0">用户名</span>
|
<span class="w-24 shrink-0">用户名</span>
|
||||||
<span class="w-32 shrink-0">IP 地址</span>
|
<span class="w-32 shrink-0">IP 地址</span>
|
||||||
<span class="w-20 shrink-0">浏览器</span>
|
|
||||||
<span class="w-16 shrink-0 text-center">状态</span>
|
<span class="w-16 shrink-0 text-center">状态</span>
|
||||||
<span class="flex-1">消息</span>
|
<span class="flex-1">User Agent</span>
|
||||||
<span class="w-40 shrink-0 text-right">时间</span>
|
<span class="w-40 shrink-0 text-right">时间</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
@@ -115,13 +105,12 @@ onMounted(loadLogs)
|
|||||||
>
|
>
|
||||||
<span class="w-24 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
|
<span class="w-24 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
|
||||||
<code class="w-32 shrink-0 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">{{ log.ip }}</code>
|
<code class="w-32 shrink-0 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">{{ log.ip }}</code>
|
||||||
<span class="w-20 shrink-0 text-xs text-muted-foreground">{{ getBrowserInfo(log.user_agent) }}</span>
|
|
||||||
<span class="w-16 shrink-0 flex justify-center">
|
<span class="w-16 shrink-0 flex justify-center">
|
||||||
<Badge :variant="log.status === 'success' ? 'default' : 'destructive'" class="text-xs">
|
<Badge :variant="log.status === 'success' ? 'default' : 'destructive'" class="text-xs">
|
||||||
{{ log.status === 'success' ? '成功' : '失败' }}
|
{{ log.status === 'success' ? '成功' : '失败' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 text-sm text-muted-foreground truncate">{{ log.message }}</span>
|
<span class="flex-1 text-xs text-muted-foreground truncate" :title="log.user_agent">{{ log.user_agent || '-' }}</span>
|
||||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
|
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@ onMounted(loadTasks)
|
|||||||
<span class="w-12 shrink-0">ID</span>
|
<span class="w-12 shrink-0">ID</span>
|
||||||
<span class="w-40 shrink-0">名称</span>
|
<span class="w-40 shrink-0">名称</span>
|
||||||
<span class="flex-1">命令</span>
|
<span class="flex-1">命令</span>
|
||||||
<span class="w-28 shrink-0">定时规则</span>
|
<span class="w-32 shrink-0">定时规则</span>
|
||||||
<span class="w-40 shrink-0">上次执行</span>
|
<span class="w-40 shrink-0">上次执行</span>
|
||||||
<span class="w-40 shrink-0">下次执行</span>
|
<span class="w-40 shrink-0">下次执行</span>
|
||||||
<span class="w-12 shrink-0 text-center">状态</span>
|
<span class="w-12 shrink-0 text-center">状态</span>
|
||||||
@@ -158,7 +158,7 @@ onMounted(loadTasks)
|
|||||||
<span class="w-12 shrink-0 text-muted-foreground text-sm">#{{ task.id }}</span>
|
<span class="w-12 shrink-0 text-muted-foreground text-sm">#{{ task.id }}</span>
|
||||||
<span class="w-40 font-medium truncate shrink-0 text-sm">{{ task.name }}</span>
|
<span class="w-40 font-medium truncate shrink-0 text-sm">{{ task.name }}</span>
|
||||||
<code class="flex-1 text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded">{{ task.command }}</code>
|
<code class="flex-1 text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded">{{ task.command }}</code>
|
||||||
<code class="w-28 shrink-0 text-muted-foreground text-xs bg-muted px-2 py-1 rounded">{{ task.schedule }}</code>
|
<code class="w-36 shrink-0 text-muted-foreground text-xs bg-muted px-2 py-1 rounded">{{ task.schedule }}</code>
|
||||||
<span class="w-40 shrink-0 text-muted-foreground text-xs">{{ task.last_run || '-' }}</span>
|
<span class="w-40 shrink-0 text-muted-foreground text-xs">{{ task.last_run || '-' }}</span>
|
||||||
<span class="w-40 shrink-0 text-muted-foreground text-xs">{{ task.next_run || '-' }}</span>
|
<span class="w-40 shrink-0 text-muted-foreground text-xs">{{ task.next_run || '-' }}</span>
|
||||||
<span class="w-12 flex justify-center shrink-0 cursor-pointer" @click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
|
<span class="w-12 flex justify-center shrink-0 cursor-pointer" @click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
|
||||||
|
|||||||
Reference in New Issue
Block a user