feat: opt pages display style

This commit is contained in:
engigu
2025-12-24 10:48:23 +08:00
parent e3131e15dc
commit 05aeea6863
9 changed files with 94 additions and 27 deletions
+17 -14
View File
@@ -26,29 +26,32 @@ func NewDashboardController(cronService *services.CronService, executorService *
}
type StatsResponse struct {
Tasks int64 `json:"tasks"`
Scripts int64 `json:"scripts"`
Envs int64 `json:"envs"`
Logs int64 `json:"logs"`
Scheduled int `json:"scheduled"`
Running int `json:"running"`
Tasks int64 `json:"tasks"`
TodayExecs int64 `json:"today_execs"`
Envs int64 `json:"envs"`
Logs int64 `json:"logs"`
Scheduled int `json:"scheduled"`
Running int `json:"running"`
}
func (dc *DashboardController) GetStats(c *gin.Context) {
var taskCount, scriptCount, envCount, logCount int64
var taskCount, envCount, logCount, todayExecs int64
database.DB.Model(&models.Task{}).Count(&taskCount)
database.DB.Model(&models.Script{}).Count(&scriptCount)
database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount)
database.DB.Model(&models.TaskLog{}).Count(&logCount)
// 今日执行总数
today := time.Now().Format("2006-01-02")
database.DB.Model(&models.SendStats{}).Where("day = ?", today).Select("COALESCE(SUM(num), 0)").Scan(&todayExecs)
stats := StatsResponse{
Tasks: taskCount,
Scripts: scriptCount,
Envs: envCount,
Logs: logCount,
Scheduled: dc.cronService.GetScheduledCount(),
Running: dc.executorService.GetRunningCount(),
Tasks: taskCount,
TodayExecs: todayExecs,
Envs: envCount,
Logs: logCount,
Scheduled: dc.cronService.GetScheduledCount(),
Running: dc.executorService.GetRunningCount(),
}
utils.Success(c, stats)
+1 -1
View File
@@ -259,7 +259,7 @@ export interface EnvListResponse {
export interface Stats {
tasks: number
scripts: number
today_execs: number
envs: number
logs: number
scheduled: number
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
const props = withDefaults(
defineProps<{
text: string
title?: string
}>(),
{
title: '详情'
}
)
const showDialog = ref(false)
function handleClick() {
if (props.text && props.text !== '-') {
showDialog.value = true
}
}
</script>
<template>
<span
class="truncate block cursor-pointer hover:text-primary"
:title="text || '-'"
@click="handleClick"
>
{{ text || '-' }}
</span>
<Dialog v-model:open="showDialog">
<DialogContent class="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{{ title }}</DialogTitle>
</DialogHeader>
<div class="max-h-[400px] overflow-y-auto">
<pre class="text-sm whitespace-pre-wrap break-all font-mono bg-muted p-3 rounded-lg">{{ text }}</pre>
</div>
</DialogContent>
</Dialog>
</template>
+3 -3
View File
@@ -1,19 +1,19 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ListTodo, FileCode, Variable, Clock, Play, ScrollText } from 'lucide-vue-next'
import { ListTodo, Variable, Clock, Play, ScrollText } from 'lucide-vue-next'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { api, type Stats, type DailyStats, type TaskStatsItem } from '@/api'
import ApexCharts from 'apexcharts'
const router = useRouter()
const stats = ref<Stats>({ tasks: 0, scripts: 0, envs: 0, logs: 0, scheduled: 0, running: 0 })
const stats = ref<Stats>({ tasks: 0, today_execs: 0, envs: 0, logs: 0, scheduled: 0, running: 0 })
const sendStats = ref<DailyStats[]>([])
const taskStats = ref<TaskStatsItem[]>([])
const chartsLoaded = ref(false)
const statItems = [
{ key: 'today_execs', label: '今日执行', icon: Play, route: '/history' },
{ key: 'tasks', label: '任务总数', icon: ListTodo, route: '/tasks' },
{ key: 'scripts', label: '脚本数量', icon: FileCode, route: '/editor' },
{ key: 'envs', label: '环境变量', icon: Variable, route: '/environments' },
{ key: 'logs', label: '日志总数', icon: ScrollText, route: '/history' },
{ key: 'scheduled', label: '调度注册', icon: Clock, route: '/tasks' },
+7 -2
View File
@@ -9,6 +9,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Trash2, Package, Search, RefreshCw, Loader2, Download, FileText, RotateCw } from 'lucide-vue-next'
import { api, type Dependency } from '@/api'
import TextOverflow from '@/components/TextOverflow.vue'
import { toast } from 'vue-sonner'
const activeTab = ref('py')
@@ -205,9 +206,13 @@ onMounted(loadDeps)
:key="dep.id"
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors"
>
<span class="flex-1 font-mono text-sm">{{ dep.name }}</span>
<span class="flex-1 font-mono text-sm truncate">
<TextOverflow :text="dep.name" title="包名" />
</span>
<span class="w-32 text-sm text-muted-foreground">{{ dep.version || '-' }}</span>
<span class="w-48 text-sm text-muted-foreground truncate hidden md:block" :title="dep.remark">{{ dep.remark || '-' }}</span>
<span class="w-48 text-sm text-muted-foreground truncate hidden md:block">
<TextOverflow :text="dep.remark || '-'" title="备注" />
</span>
<span class="w-24 flex justify-center gap-1">
<Button v-if="dep.log" variant="ghost" size="icon" class="h-7 w-7" @click="showLog(dep)">
<FileText class="h-4 w-4" />
+5 -2
View File
@@ -8,6 +8,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import Pagination from '@/components/Pagination.vue'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search } from 'lucide-vue-next'
import TextOverflow from '@/components/TextOverflow.vue'
import { api, type EnvVar } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
@@ -139,9 +140,11 @@ onMounted(loadEnvVars)
>
<code class="w-48 font-medium truncate shrink-0 text-xs bg-muted px-2 py-1 rounded">{{ env.name }}</code>
<span class="flex-1 font-mono text-muted-foreground truncate text-xs">
{{ showValues[env.id] ? env.value : maskValue(env.value) }}
<TextOverflow :text="showValues[env.id] ? env.value : maskValue(env.value)" title="变量值" />
</span>
<span class="w-48 shrink-0 text-muted-foreground truncate text-sm hidden md:block">
<TextOverflow :text="env.remark || '-'" title="备注" />
</span>
<span class="w-48 shrink-0 text-muted-foreground truncate text-sm hidden md:block">{{ env.remark || '-' }}</span>
<span class="w-24 shrink-0 flex justify-center gap-1">
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleShow(env.id)" :title="showValues[env.id] ? '隐藏' : '显示'">
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
+7 -2
View File
@@ -10,6 +10,7 @@ import { api, type TaskLog, type LogDetail } from '@/api'
import { toast } from 'vue-sonner'
import pako from 'pako'
import { useSiteSettings } from '@/composables/useSiteSettings'
import TextOverflow from '@/components/TextOverflow.vue'
const route = useRoute()
const { pageSize } = useSiteSettings()
@@ -162,8 +163,12 @@ watch(() => route.query.task_id, (newTaskId) => {
@click="selectLog(log)"
>
<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>
<code :class="['text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded', selectedLog ? 'w-40 shrink-0 hidden sm:block' : 'flex-1']">{{ log.command }}</code>
<span class="w-32 font-medium truncate shrink-0 text-sm">
<TextOverflow :text="log.task_name" title="任务名称" />
</span>
<code :class="['text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded', selectedLog ? 'w-40 shrink-0 hidden sm:block' : 'flex-1']">
<TextOverflow :text="log.command" title="执行命令" />
</code>
<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>
+4 -1
View File
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import Pagination from '@/components/Pagination.vue'
import { RefreshCw, Search } from 'lucide-vue-next'
import TextOverflow from '@/components/TextOverflow.vue'
import { api } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
@@ -107,7 +108,9 @@ onMounted(loadLogs)
<span class="w-16 shrink-0 flex justify-center">
<span :class="['h-2 w-2 rounded-full', log.status === 'success' ? 'bg-green-500' : 'bg-red-500']"></span>
</span>
<span class="flex-1 text-xs text-muted-foreground truncate hidden md:block" :title="log.user_agent">{{ log.user_agent || '-' }}</span>
<span class="flex-1 text-xs text-muted-foreground truncate hidden md:block">
<TextOverflow :text="log.user_agent || '-'" title="User Agent" />
</span>
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
</div>
</div>
+7 -2
View File
@@ -15,6 +15,7 @@ import { api, type Task, type EnvVar } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
import { useRouter } from 'vue-router'
import TextOverflow from '@/components/TextOverflow.vue'
const router = useRouter()
const { pageSize } = useSiteSettings()
@@ -250,8 +251,12 @@ onMounted(() => {
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors"
>
<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>
<code class="flex-1 text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded">{{ task.command }}</code>
<span class="w-40 font-medium truncate shrink-0 text-sm">
<TextOverflow :text="task.name" title="任务名称" />
</span>
<code class="flex-1 text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded">
<TextOverflow :text="task.command" title="执行命令" />
</code>
<code class="w-36 shrink-0 text-muted-foreground text-xs bg-muted px-2 py-1 rounded hidden md:block">{{ task.schedule }}</code>
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.last_run || '-' }}</span>
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.next_run || '-' }}</span>