chore: opt middle screen style
This commit is contained in:
@@ -155,16 +155,18 @@ onMounted(() => {
|
||||
<main class="flex-1 flex flex-col min-w-0 relative">
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="h-14 border-b border-slate-200/60 dark:border-white/10 bg-background flex items-center justify-between px-4 lg:px-6 shrink-0 sticky top-0 z-30">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 sm:gap-3 flex-1 min-w-0 mr-4">
|
||||
<Button variant="ghost" size="icon" class="h-9 w-9 lg:hidden shrink-0" @click="mobileMenuOpen = true">
|
||||
<Menu class="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
<span class="text-sm text-muted-foreground truncate" :title="sentence">
|
||||
<span class="hidden sm:inline">{{ sentence }}</span>
|
||||
<span class="sm:hidden">{{ sentenceContent }}</span>
|
||||
</span>
|
||||
<div class="flex flex-col sm:flex-row sm:items-baseline sm:gap-2 truncate">
|
||||
<span class="text-sm text-muted-foreground truncate font-medium" :title="sentence">
|
||||
<span class="hidden sm:inline">{{ sentence }}</span>
|
||||
<span class="sm:hidden">{{ sentenceContent }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1 sm:gap-2.5 shrink-0">
|
||||
<SystemNotice />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
@@ -108,42 +108,46 @@ onMounted(loadLogs)
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="filterUsername" placeholder="搜索用户名..." class="h-9 pl-9 w-full sm:w-56 text-sm"
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filterUsername" placeholder="搜索用户名..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" :disabled="loading"
|
||||
title="刷新">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" :disabled="loading"
|
||||
title="刷新">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<!-- ========== 1. 大屏表头 (Large >= 1024px) ========== -->
|
||||
<div class="hidden lg:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-24 sm:w-32 shrink-0">用户名</span>
|
||||
<span class="w-32 sm:w-40 shrink-0">IP 地址</span>
|
||||
<span class="w-16 shrink-0 text-center">状态</span>
|
||||
<span class="w-32 shrink-0">用户信息</span>
|
||||
<span class="w-40 shrink-0">IP 地址</span>
|
||||
<span class="flex-1 min-w-0">User Agent</span>
|
||||
<span class="w-40 shrink-0 text-right">登录时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y">
|
||||
<!-- ========== 2. 中屏表头 (Medium 640px - 1024px) ========== -->
|
||||
<div class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-24 shrink-0">用户信息</span>
|
||||
<span class="w-32 shrink-0">IP 地址</span>
|
||||
<span class="flex-1 min-w-0">设备信息 (UA)</span>
|
||||
<span class="w-40 shrink-0 text-right">登录时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<div class="divide-y text-sm">
|
||||
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无登录日志
|
||||
</div>
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
<!-- ========== 1. 小屏布局 (Small < 640px) - 用户调好 ========== -->
|
||||
<div v-for="(log, index) in logs" :key="`small-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 mr-2">
|
||||
@@ -169,24 +173,41 @@ onMounted(loadLogs)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm pl-1">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<span class="w-24 sm:w-32 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
|
||||
<div class="w-32 sm:w-40 shrink-0">
|
||||
<code
|
||||
class="text-xs text-muted-foreground bg-muted px-2 py-1 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors inline-block"
|
||||
<div v-for="log in logs" :key="`medium-${log.id}`"
|
||||
class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2.5 hover:bg-muted/50 transition-colors">
|
||||
<div class="w-24 shrink-0 flex items-center gap-2 min-w-0">
|
||||
<span :class="['h-1.5 w-1.5 rounded-full shrink-0', log.status === 'success' ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.3)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.3)]']"></span>
|
||||
<span class="font-medium truncate text-sm">{{ log.username }}</span>
|
||||
</div>
|
||||
<div class="w-32 shrink-0">
|
||||
<code class="text-[11px] text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors inline-block tabular-nums"
|
||||
@click="showIpInfo(log.ip)">{{ log.ip }}</code>
|
||||
</div>
|
||||
<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 class="flex-1 min-w-0 text-xs text-muted-foreground line-clamp-1">
|
||||
{{ log.user_agent || '-' }}
|
||||
</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">
|
||||
{{ log.created_at }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ========== 3. 大屏布局 (Large >= 1024px) - 用户调好 ========== -->
|
||||
<div v-for="(log, index) in logs" :key="`large-${log.id}`"
|
||||
class="hidden lg:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm pl-1">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<div class="w-32 shrink-0 flex items-center gap-2 min-w-0 font-medium text-sm">
|
||||
<span :class="['h-2 w-2 rounded-full shrink-0', log.status === 'success' ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.3)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.3)]']"></span>
|
||||
<span class="truncate">{{ log.username }}</span>
|
||||
</div>
|
||||
<div class="w-40 shrink-0">
|
||||
<code
|
||||
class="text-xs text-muted-foreground bg-muted px-2 py-1 rounded truncate cursor-pointer hover:bg-muted/80 transition-colors inline-block tabular-nums"
|
||||
@click="showIpInfo(log.ip)">{{ log.ip }}</code>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0 text-xs text-muted-foreground truncate">
|
||||
<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>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">{{ log.created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
|
||||
@@ -168,16 +168,16 @@ function onDialogClose(open: boolean) {
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full sm:w-56 text-sm"
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<div class="relative flex-1 sm:flex-none sm:w-28">
|
||||
<Select :model-value="filters.level" @update:model-value="handleLevelChange">
|
||||
<SelectTrigger class="h-9 w-full sm:w-28 text-sm">
|
||||
<SelectTrigger class="h-9 w-full text-sm bg-muted/20 border-muted-foreground/10">
|
||||
<SelectValue placeholder="级别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -188,17 +188,16 @@ function onDialogClose(open: boolean) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="fetchLogs" :disabled="loading"
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0 sm:flex" @click="fetchLogs" :disabled="loading"
|
||||
title="刷新">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog :open="showClearConfirm" @update:open="showClearConfirm = $event">
|
||||
<Button variant="outline"
|
||||
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20"
|
||||
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20 w-full sm:w-auto"
|
||||
@click="showClearConfirm = true">
|
||||
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline"
|
||||
style="padding-left: 2px;">清空记录</span>
|
||||
<Trash2 class="h-4 w-4 mr-2" /> <span>清空记录</span>
|
||||
</Button>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -218,24 +217,29 @@ function onDialogClose(open: boolean) {
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<!-- ========== 1. 大屏表头 (Large >= 1024px) ========== -->
|
||||
<div class="hidden lg:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-12 sm:w-16 shrink-0 text-center">级别</span>
|
||||
<span class="w-40 sm:w-56 shrink-0">事件标题</span>
|
||||
<span class="w-56 shrink-0 px-2 pl-8">事件信息</span>
|
||||
<span class="flex-1 min-w-0 px-2">详情内容</span>
|
||||
<span class="w-40 shrink-0 text-right">发生时间</span>
|
||||
</div>
|
||||
|
||||
<!-- ========== 2. 中屏表头 (Medium 640px - 1024px) ========== -->
|
||||
<div class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-48 shrink-0">事件信息</span>
|
||||
<span class="flex-1 min-w-0">详情内容</span>
|
||||
<span class="w-40 shrink-0 text-right">发生时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<!-- 列表内容 -->
|
||||
<div class="divide-y">
|
||||
<div v-if="logs.length === 0 && !loading" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无系统事件
|
||||
</div>
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
<!-- ========== 1. 小屏布局 (Small < 640px) - 用户调好 ========== -->
|
||||
<div v-for="(log, index) in logs" :key="`small-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
@@ -257,23 +261,39 @@ function onDialogClose(open: boolean) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
<!-- ========== 2. 中屏布局 (Medium 640px - 1024px) - 新抽取优化 ========== -->
|
||||
<div v-for="log in logs" :key="`medium-${log.id}`"
|
||||
class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2.5 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm tabular-nums pl-1">#{{ total - (filters.page - 1) * pageSize - index }}</span>
|
||||
<span class="w-12 sm:w-16 shrink-0 flex justify-center">
|
||||
<component :is="getLevelIcon(log.level)" :class="['h-4 w-4',
|
||||
<div class="w-48 shrink-0 flex items-center gap-3 min-w-0 font-medium text-sm">
|
||||
<component :is="getLevelIcon(log.level)" :class="['h-3.5 w-3.5 shrink-0 opacity-80',
|
||||
log.level === LOG_LEVEL.INFO ? 'text-blue-500' :
|
||||
log.level === LOG_LEVEL.WARNING ? 'text-yellow-500' : 'text-red-500']" />
|
||||
<span class="truncate" :title="log.title">{{ log.title }}</span>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0 text-sm text-muted-foreground line-clamp-1" :title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-sm truncate" :title="log.title">{{
|
||||
log.title }}</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-for="(log, index) in logs" :key="`large-${log.id}`"
|
||||
class="hidden lg:flex items-center gap-4 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-16 shrink-0 text-muted-foreground text-sm tabular-nums pl-1">#{{ total - (filters.page - 1) * pageSize - index }}</span>
|
||||
<div class="w-56 shrink-0 flex items-center gap-3 min-w-0 font-medium text-sm">
|
||||
<component :is="getLevelIcon(log.level)" :class="['h-4 w-4 shrink-0 opacity-80',
|
||||
log.level === LOG_LEVEL.INFO ? 'text-blue-500' :
|
||||
log.level === LOG_LEVEL.WARNING ? 'text-yellow-500' : 'text-red-500']" />
|
||||
<span class="truncate" :title="log.title">{{ log.title }}</span>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0 text-sm text-muted-foreground truncate"
|
||||
:title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums">
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -165,16 +165,16 @@ function onDialogClose(open: boolean) {
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full sm:w-56 text-sm"
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<div class="relative flex-1 sm:flex-none sm:w-28">
|
||||
<Select :model-value="filters.status" @update:model-value="handleStatusChange">
|
||||
<SelectTrigger class="h-9 w-full sm:w-28 text-sm">
|
||||
<SelectTrigger class="h-9 w-full text-sm bg-muted/20 border-muted-foreground/10">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -184,16 +184,16 @@ function onDialogClose(open: boolean) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="fetchLogs" :disabled="loading"
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0 sm:flex" @click="fetchLogs" :disabled="loading"
|
||||
title="刷新">
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog :open="showClearConfirm" @update:open="showClearConfirm = $event">
|
||||
<Button variant="outline"
|
||||
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20"
|
||||
class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20 w-full sm:w-auto"
|
||||
@click="showClearConfirm = true">
|
||||
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline" style="padding-left: 2px;">清空记录</span>
|
||||
<Trash2 class="h-4 w-4 mr-2" /> <span>清空记录</span>
|
||||
</Button>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -213,24 +213,29 @@ function onDialogClose(open: boolean) {
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<!-- ========== 1. 大屏表头 (Large >= 1024px) ========== -->
|
||||
<div class="hidden lg:flex items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-16 shrink-0 pl-1">序号</span>
|
||||
<span class="w-56 shrink-0">标题及渠道</span>
|
||||
<span class="flex-1 min-w-0">内容详情</span>
|
||||
<span class="w-16 shrink-0 text-center">状态</span>
|
||||
<span class="w-56 shrink-0 px-2 pl-6">标题及渠道</span>
|
||||
<span class="flex-1 min-w-0 px-2 font-not-medium">内容详情</span>
|
||||
<span class="w-40 shrink-0 text-right">发送时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y">
|
||||
<!-- ========== 2. 中屏表头 (Medium 640px - 1024px) ========== -->
|
||||
<div class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2 border-b bg-muted/20 text-sm text-muted-foreground font-medium">
|
||||
<span class="w-48 shrink-0">项目</span>
|
||||
<span class="flex-1 min-w-0">内容摘要</span>
|
||||
<span class="w-40 shrink-0 text-right">时间信息</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<div class="divide-y text-sm">
|
||||
<div v-if="logs.length === 0 && !loading" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无推送记录
|
||||
</div>
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`mobile-${log.id}`"
|
||||
<!-- ========== 1. 小屏布局 (Small < 640px) - 用户调好 ========== -->
|
||||
<div v-for="(log, index) in logs" :key="`small-${log.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors cursor-pointer group" @click="showDetail(log)"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']">
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
@@ -261,23 +266,40 @@ function onDialogClose(open: boolean) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(log, index) in logs" :key="`desktop-${log.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
<!-- ========== 2. 中屏布局 (Medium 640px - 1024px) - 新抽取优化 ========== -->
|
||||
<div v-for="log in logs" :key="`medium-${log.id}`"
|
||||
class="hidden sm:flex lg:hidden items-center gap-4 px-4 py-2.5 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<span class="w-16 shrink-0 text-muted-foreground text-sm pl-1">#{{ getLogIndex(index) }}</span>
|
||||
<span class="w-56 shrink-0 font-medium text-sm truncate" :title="log.title">
|
||||
<span v-if="log.channel_name" class="mr-1 text-muted-foreground">[{{ log.channel_name }}]</span>{{ log.title
|
||||
}}
|
||||
<div class="w-48 shrink-0 flex items-center gap-3 min-w-0">
|
||||
<span :class="['h-2 w-2 rounded-full shrink-0', log.status === LOG_STATUS.SUCCESS ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.3)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.3)]']"></span>
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<span class="font-medium truncate text-sm" :title="log.title">{{ log.title }}</span>
|
||||
<span v-if="log.channel_name" class="text-[10px] text-muted-foreground opacity-60">[{{ log.channel_name }}]</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0 text-xs text-muted-foreground line-clamp-1" :title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ========== 3. 大屏布局 (Large >= 1024px) - 用户调好 ========== -->
|
||||
<div v-for="(log, index) in logs" :key="`large-${log.id}`"
|
||||
class="hidden lg:flex items-center gap-4 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-16 shrink-0 text-muted-foreground text-sm tabular-nums pl-1">#{{ getLogIndex(index) }}</span>
|
||||
<div class="w-56 shrink-0 flex items-center gap-3 min-w-0 font-medium text-sm">
|
||||
<span :class="['h-2 w-2 rounded-full shrink-0', log.status === LOG_STATUS.SUCCESS ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.3)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.3)]']"></span>
|
||||
<span class="truncate" :title="log.title">
|
||||
<span v-if="log.channel_name" class="mr-1 text-muted-foreground opacity-60">[{{ log.channel_name }}]</span>{{ log.title }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0 text-sm text-muted-foreground truncate" :title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
<span class="w-16 shrink-0 flex justify-center">
|
||||
<span
|
||||
:class="['h-2 w-2 rounded-full', log.status === LOG_STATUS.SUCCESS ? 'bg-green-500' : 'bg-red-500']"></span>
|
||||
</span>
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">
|
||||
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground tabular-nums opacity-60">
|
||||
{{ formatDate(log.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+169
-184
@@ -16,6 +16,7 @@ import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE, TASK_STATUS } from '@/constants'
|
||||
import TextOverflow from '@/components/TextOverflow.vue'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -260,7 +261,8 @@ async function viewLogs(taskId: string) {
|
||||
if (res.data && res.data.length > 0) {
|
||||
const latestLog = res.data[0]
|
||||
if (!latestLog) return
|
||||
latestLogTitle.value = latestLog.task_name || ''
|
||||
const timeStr = latestLog.created_at ? format(new Date(latestLog.created_at), 'yyyy/MM/dd HH:mm:ss') : ''
|
||||
latestLogTitle.value = `${latestLog.task_name || ''}${timeStr ? ` (${timeStr})` : ''}`
|
||||
latestLogStatus.value = latestLog.status || ''
|
||||
selectedLogId.value = latestLog.id
|
||||
logContent.value = ''
|
||||
@@ -324,31 +326,31 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col shrink-0">
|
||||
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">定时任务</h2>
|
||||
<p class="text-muted-foreground text-sm">管理和调度自动化执行任务</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2.5 w-full md:w-auto">
|
||||
<div class="flex flex-col sm:flex-row gap-3 w-full sm:flex-1 sm:justify-end">
|
||||
<!-- 搜索与标签 -->
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto">
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-40 text-sm"
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2 w-full sm:w-auto">
|
||||
<div class="relative w-full sm:w-36 md:w-40 lg:w-48 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<div class="relative flex-1 sm:flex-none">
|
||||
<Tag class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input v-model="filterTags" placeholder="搜索标签..." class="h-9 pl-9 w-full sm:w-32 text-sm"
|
||||
<div class="relative w-full sm:w-32 md:w-36 lg:w-40 group">
|
||||
<Tag class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filterTags" placeholder="搜索标签..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:justify-end">
|
||||
<!-- 移动端类型切换 -->
|
||||
<div class="md:hidden flex-1 shrink-0">
|
||||
<div class="lg:hidden flex-1 shrink-0">
|
||||
<Select v-model="filterType" @update:model-value="(_v: any) => handleTypeChange()">
|
||||
<SelectTrigger class="h-9 w-full text-sm">
|
||||
<SelectTrigger class="h-9 w-full text-sm bg-muted/20 border-muted-foreground/10">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -359,7 +361,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
</div>
|
||||
|
||||
<div v-if="filterAgentId"
|
||||
class="hidden sm:flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm shrink-0">
|
||||
class="hidden xl:flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm shrink-0">
|
||||
<Server class="h-3.5 w-3.5" />
|
||||
<span>{{ filterAgentName }}</span>
|
||||
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
|
||||
@@ -369,30 +371,29 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||
</Button>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0 justify-end">
|
||||
<!-- 动态新增按钮 -->
|
||||
<Button variant="outline" class="shrink-0 px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete">
|
||||
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">批量删除</span>
|
||||
<div class="flex items-center gap-2 shrink-0 ml-auto sm:ml-0">
|
||||
<Button variant="outline" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete" title="批量删除">
|
||||
<Trash2 class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">批量删除</span>
|
||||
</Button>
|
||||
<Button v-if="filterType === TASK_TYPE.NORMAL" @click="openCreate" class="shrink-0 px-3 h-9 shadow-sm">
|
||||
<Plus class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">新建任务</span>
|
||||
<Button v-if="filterType === TASK_TYPE.NORMAL" @click="openCreate" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm font-medium" title="新建任务">
|
||||
<Plus class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">新建任务</span>
|
||||
</Button>
|
||||
<Button v-else-if="filterType === TASK_TYPE.REPO" @click="openCreateRepo" class="shrink-0 px-3 h-9 shadow-sm">
|
||||
<GitBranch class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">同步仓库</span>
|
||||
<Button v-else-if="filterType === TASK_TYPE.REPO" @click="openCreateRepo" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm font-medium" title="同步仓库">
|
||||
<GitBranch class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">同步仓库</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端类型切换移到后面 -->
|
||||
<Tabs :model-value="filterType" @update:model-value="(v: string | number) => { filterType = String(v); handleTypeChange() }" class="shrink-0 hidden md:block">
|
||||
<TabsList class="h-9 p-1 bg-muted/30 border">
|
||||
<TabsTrigger :value="TASK_TYPE.NORMAL" class="px-4 h-7 text-[13px]">定时任务</TabsTrigger>
|
||||
<TabsTrigger :value="TASK_TYPE.REPO" class="px-4 h-7 text-[13px]">仓库同步</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<!-- 桌面端类型切换 -->
|
||||
<Tabs :model-value="filterType" @update:model-value="(v: string | number) => { filterType = String(v); handleTypeChange() }" class="shrink-0 hidden lg:block">
|
||||
<TabsList class="h-9 p-1 bg-muted/30 border">
|
||||
<TabsTrigger :value="TASK_TYPE.NORMAL" class="px-4 h-7 text-[13px]">定时任务</TabsTrigger>
|
||||
<TabsTrigger :value="TASK_TYPE.REPO" class="px-4 h-7 text-[13px]">仓库同步</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移动端 agent 过滤标签 -->
|
||||
<!-- 移动端/平板 agent 过滤标签 -->
|
||||
<div v-if="filterAgentId"
|
||||
class="flex sm:hidden items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm w-fit mt-1">
|
||||
class="flex lg:hidden items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm w-fit mt-1">
|
||||
<Server class="h-3.5 w-3.5" />
|
||||
<span>{{ filterAgentName }}</span>
|
||||
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
|
||||
@@ -400,191 +401,175 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border bg-card">
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||
<span class="w-8 shrink-0 text-center">类型</span>
|
||||
<span class="w-40 md:w-48 lg:w-56 shrink-0">名称</span>
|
||||
<span class="w-32 shrink-0 hidden md:block">执行位置</span>
|
||||
<span class="w-8 shrink-0 text-center">状态</span>
|
||||
<span class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<Terminal class="h-3.5 w-3.5 opacity-50" />命令/地址
|
||||
</span>
|
||||
<span class="w-28 shrink-0 hidden md:block">定时规则</span>
|
||||
<span class="w-40 shrink-0 hidden lg:block">执行时间</span>
|
||||
<span class="w-36 shrink-0 text-center">操作</span>
|
||||
<div class="rounded-lg border bg-card overflow-hidden">
|
||||
<!-- ========== 1. 大屏布局 (Large >= 1024px) - 用户调好 ========== -->
|
||||
<div class="hidden lg:block">
|
||||
<!-- 表头 -->
|
||||
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||
<span class="w-8 shrink-0 text-center">类型</span>
|
||||
<span class="w-56 shrink-0">名称</span>
|
||||
<span class="w-32 shrink-0">执行位置</span>
|
||||
<span class="w-8 shrink-0 text-center">状态</span>
|
||||
<span class="flex-1 min-w-0 flex items-center gap-1.5 line-clamp-1">
|
||||
<Terminal class="h-3.5 w-3.5 opacity-50" />命令/地址
|
||||
</span>
|
||||
<span class="w-28 shrink-0">定时规则</span>
|
||||
<span class="w-40 shrink-0">执行时间</span>
|
||||
<span class="w-36 shrink-0 text-center">操作</span>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y text-sm">
|
||||
<div v-for="(task, index) in tasks" :key="`large-${task.id}`"
|
||||
class="flex items-center gap-4 px-4 py-1.5 hover:bg-muted/30 transition-colors">
|
||||
<div class="w-12 shrink-0 pl-1 text-muted-foreground tabular-nums">#{{ total - (currentPage - 1) * pageSize - index }}</div>
|
||||
<span class="w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
|
||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
||||
<Terminal v-else class="h-4 w-4 text-primary" />
|
||||
</span>
|
||||
<div class="w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden">
|
||||
<span class="font-medium truncate cursor-help" :title="task.name">{{ task.name }}</span>
|
||||
<div v-if="task.tags" class="flex items-center gap-1 overflow-hidden">
|
||||
<span v-for="tag in task.tags.split(',').filter(Boolean).slice(0, 3)" :key="tag"
|
||||
class="truncate text-[10px] leading-none px-1 py-0.5 bg-secondary text-secondary-foreground rounded border">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="w-32 shrink-0 flex items-center gap-1 text-xs" :title="getExecutorName(task)">
|
||||
<Monitor v-if="!task.agent_id" class="h-3 w-3 text-muted-foreground" />
|
||||
<template v-else>
|
||||
<Wifi v-if="getExecutorStatus(task) === 'online'" class="h-3 w-3 text-green-500" />
|
||||
<WifiOff v-else class="h-3 w-3 text-muted-foreground" />
|
||||
</template>
|
||||
<span class="truncate">{{ getExecutorName(task) }}</span>
|
||||
</span>
|
||||
<span class="w-8 flex justify-center shrink-0 cursor-pointer group" @click="toggleTask(task, !task.enabled)">
|
||||
<div v-if="task.enabled" class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
<code class="flex-1 min-w-0 text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate" />
|
||||
</code>
|
||||
<div class="w-28 shrink-0 flex flex-col items-start justify-center gap-1 overflow-hidden">
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-2 py-1 rounded-md">服务启动时</span>
|
||||
<code v-else-if="task.schedule" class="text-muted-foreground text-xs bg-muted/40 px-1.5 py-0.5 rounded truncate font-mono tabular-nums">{{ task.schedule }}</code>
|
||||
</div>
|
||||
<div class="w-40 shrink-0 flex flex-col justify-center gap-0.5 text-[11px] text-muted-foreground tabular-nums">
|
||||
<span class="truncate">上: {{ task.last_run || '-' }}</span>
|
||||
<span class="truncate">下: {{ task.next_run || '-' }}</span>
|
||||
</div>
|
||||
<span class="w-36 shrink-0 flex justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Play v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewLogs(task.id)"><ScrollText class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(task)"><Pencil class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="duplicateTask(task)"><Copy class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(task.id)"><Trash2 class="h-3.5 w-3.5" /></Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y">
|
||||
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||
暂无任务
|
||||
<!-- ========== 2. 中屏布局 (Medium 640px - 1024px) - 新抽取优化 ========== -->
|
||||
<div class="hidden sm:block lg:hidden">
|
||||
<!-- 表头 -->
|
||||
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||
<span class="w-48 shrink-0">任务信息</span>
|
||||
<span class="flex-1 min-w-0">命令内容</span>
|
||||
<span class="w-8 shrink-0 text-center">状态</span>
|
||||
<span class="w-24 shrink-0 text-center">操作</span>
|
||||
</div>
|
||||
|
||||
<!-- 小屏卡片布局 -->
|
||||
<div v-for="(task, index) in tasks" :key="`mobile-${task.id}`"
|
||||
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
|
||||
<!-- 标题行 -->
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
||||
<span class="text-xs text-muted-foreground tabular-nums flex-shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y text-sm">
|
||||
<div v-for="task in tasks" :key="`medium-${task.id}`"
|
||||
class="flex items-center gap-4 px-4 py-2.5 hover:bg-muted/30 transition-colors">
|
||||
<div class="w-48 shrink-0 flex items-center gap-2 overflow-hidden">
|
||||
<span class="shrink-0" :title="getTaskTypeTitle(task.type || 'task')">
|
||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
||||
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
||||
</span>
|
||||
<span class="font-bold text-sm truncate" :title="task.name">{{ task.name }}</span>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="font-medium truncate">{{ task.name }}</span>
|
||||
<span v-if="task.schedule" class="text-[10px] text-muted-foreground font-mono truncate">{{ task.schedule }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="cursor-pointer group"
|
||||
@click="toggleTask(task, !task.enabled)"
|
||||
:title="task.enabled ? '点击禁用' : '点击启用'">
|
||||
<div v-if="task.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
<code class="flex-1 min-w-0 text-[11px] text-muted-foreground bg-muted/20 px-2 py-1 rounded truncate">
|
||||
{{ task.command }}
|
||||
</code>
|
||||
<span class="w-8 flex justify-center shrink-0 cursor-pointer group" @click="toggleTask(task, !task.enabled)">
|
||||
<div v-if="task.enabled" class="h-6 w-6 rounded-md bg-green-500/5 flex items-center justify-center">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500" />
|
||||
</div>
|
||||
<div v-else class="h-6 w-6 rounded-md bg-muted flex items-center justify-center">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
<div class="w-24 shrink-0 flex justify-center gap-0.5">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Play v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="viewLogs(task.id)"><ScrollText class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="openEdit(task)"><Pencil class="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情信息列表 -->
|
||||
<!-- ========== 3. 小屏布局 (Small < 640px) - 用户调好 ========== -->
|
||||
<div class="divide-y sm:hidden">
|
||||
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">暂无任务</div>
|
||||
<div v-for="(task, index) in tasks" :key="`small-${task.id}`" class="p-3 hover:bg-muted/50 transition-colors">
|
||||
<div class="flex items-start justify-between mb-3 border-b border-border/40 pb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
||||
<span class="text-xs text-muted-foreground tabular-nums flex-shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
<span class="shrink-0">
|
||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
||||
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
||||
</span>
|
||||
<span class="font-bold text-sm truncate">{{ task.name }}</span>
|
||||
</div>
|
||||
<span @click="toggleTask(task, !task.enabled)" class="cursor-pointer">
|
||||
<div v-if="task.enabled" class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center"><Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" /></div>
|
||||
<div v-else class="h-6 w-6 rounded-md bg-muted flex items-center justify-center"><ZapOff class="h-3.5 w-3.5 text-muted-foreground" /></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-3 px-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-10 shrink-0 font-medium opacity-70">定时:</span>
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP"
|
||||
class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded font-medium">服务启动时</span>
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded font-medium">服务启动时</span>
|
||||
<span v-else-if="task.schedule" class="text-xs text-foreground bg-muted/40 px-1.5 py-0.5 rounded">{{ task.schedule }}</span>
|
||||
<span v-else class="text-muted-foreground opacity-50">-</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">命令:</span>
|
||||
<div class="flex-1 min-w-0 overflow-hidden text-foreground">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="task.tags" class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">标签:</span>
|
||||
<div class="flex flex-wrap gap-1 flex-1">
|
||||
<span v-for="tag in task.tags.split(',').filter(Boolean)" :key="tag"
|
||||
class="text-[9px] leading-none px-1.5 py-0.5 bg-primary/10 text-primary/80 rounded-full border border-primary/20 font-semibold">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 overflow-hidden text-foreground"><TextOverflow :text="task.command" class="truncate opacity-80" /></div>
|
||||
</div>
|
||||
<div v-if="task.remark" class="flex items-start gap-3">
|
||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">备注:</span>
|
||||
<span class="flex-1 text-[11px] truncate">{{ task.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮行 -->
|
||||
<div class="flex items-center justify-end gap-0.5 pt-2 border-t border-border/40">
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="runTask(task.id)" title="执行" :disabled="executingTaskId === task.id">
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 animate-spin" />
|
||||
<Play v-else class="h-3 w-3" />执行
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="viewLogs(task.id)" title="日志">
|
||||
<ScrollText class="h-3 w-3" />日志
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="openEdit(task)" title="编辑">
|
||||
<Pencil class="h-3 w-3" />编辑
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="duplicateTask(task)" title="克隆">
|
||||
<Copy class="h-3 w-3" />克隆
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1 text-destructive" @click="confirmDelete(task.id)" title="删除">
|
||||
<Trash2 class="h-3 w-3" />删除
|
||||
</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="viewLogs(task.id)"><ScrollText class="h-3 w-3" />日志</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1" @click="openEdit(task)"><Pencil class="h-3 w-3" />编辑</Button>
|
||||
<Button variant="ghost" class="h-7 px-1.5 text-[10px] gap-1 text-destructive" @click="confirmDelete(task.id)"><Trash2 class="h-3 w-3" />删除</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏行布局 -->
|
||||
<div v-for="(task, index) in tasks" :key="`desktop-${task.id}`"
|
||||
class="hidden sm:flex items-center gap-4 px-4 py-1.5 hover:bg-muted/30 transition-colors">
|
||||
<div class="w-12 shrink-0 pl-1">
|
||||
<span class="text-muted-foreground text-sm">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||
</div>
|
||||
<span class="w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
|
||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
||||
<Terminal v-else class="h-4 w-4 text-primary" />
|
||||
</span>
|
||||
<div class="w-40 md:w-48 lg:w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden">
|
||||
<span class="font-medium truncate text-sm cursor-help" :title="task.name">{{ task.name }}</span>
|
||||
<div v-if="task.tags" class="flex items-center gap-1 overflow-hidden" :title="task.tags">
|
||||
<span v-for="tag in task.tags.split(',').filter(Boolean).slice(0, 3)" :key="tag"
|
||||
class="truncate text-[10px] leading-none px-1 py-0.5 bg-secondary text-secondary-foreground rounded border">
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span v-if="task.tags.split(',').filter(Boolean).length > 3" class="text-[10px] text-muted-foreground">...</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="w-32 shrink-0 hidden md:flex items-center gap-1 text-xs" :title="getExecutorName(task)">
|
||||
<Monitor v-if="!task.agent_id" class="h-3 w-3 text-muted-foreground" />
|
||||
<template v-else>
|
||||
<Wifi v-if="getExecutorStatus(task) === 'online'" class="h-3 w-3 text-green-500" />
|
||||
<WifiOff v-else class="h-3 w-3 text-muted-foreground" />
|
||||
</template>
|
||||
<span class="truncate">{{ getExecutorName(task) }}</span>
|
||||
</span>
|
||||
<span class="w-8 flex justify-center shrink-0 cursor-pointer group"
|
||||
@click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
|
||||
<div v-if="task.enabled"
|
||||
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
|
||||
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
|
||||
</div>
|
||||
<div v-else
|
||||
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
|
||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</span>
|
||||
<code class="flex-1 min-w-0 text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded overflow-hidden">
|
||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate" />
|
||||
</code>
|
||||
<div class="w-28 shrink-0 hidden md:flex flex-col items-start justify-center gap-1 overflow-hidden">
|
||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP"
|
||||
class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-2 py-1 rounded-md whitespace-nowrap font-medium">服务启动时</span>
|
||||
<code v-else-if="task.schedule"
|
||||
class="text-muted-foreground text-xs bg-muted/40 px-1.5 py-0.5 rounded truncate max-w-full"
|
||||
:title="task.schedule">{{ task.schedule }}</code>
|
||||
</div>
|
||||
<div class="w-40 shrink-0 hidden lg:flex flex-col justify-center gap-0.5">
|
||||
<span class="text-[11px] text-muted-foreground truncate" :title="'上次执行: ' + (task.last_run || '-')">
|
||||
上: {{ task.last_run || '-' }}
|
||||
</span>
|
||||
<span class="text-[11px] text-muted-foreground truncate" :title="'下次执行: ' + (task.next_run || '-')">
|
||||
下: {{ task.next_run || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="w-36 shrink-0 flex justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="runTask(task.id)" title="执行"
|
||||
:disabled="executingTaskId === task.id">
|
||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Play v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewLogs(task.id)" title="日志">
|
||||
<ScrollText class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(task)" title="编辑">
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="duplicateTask(task)" title="克隆">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive"
|
||||
@click="confirmDelete(task.id)" title="删除">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<Pagination :total="total" :page="currentPage" @update:page="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination :total="total" :page="currentPage" @update:page="handlePageChange" />
|
||||
|
||||
<!-- 普通任务弹窗 -->
|
||||
<TaskDialog v-model:open="showTaskDialog" :task="editingTask" :is-edit="isEdit" @saved="loadTasks" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user