refactor: 拆分ScriptManager.tsx(546→411行)提取ScriptStatsCards+ScriptCard+ScriptExecutionsTable子组件
This commit is contained in:
1 parent
aeb2611149
commit
2582f12c36
4 files changed
+245
-169
No files matched your search
@@ -0,0 +1,72 @@
|
|||||||
|
// ScriptCard — 单个脚本卡片(含行操作按钮) // Logan.Li
|
||||||
|
import { FileCode, Eye, Play, Edit, Copy, Trash2, Loader2 } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { type Script, getTypeBadge } from './script-helpers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
script: Script;
|
||||||
|
selected: boolean;
|
||||||
|
executing: boolean;
|
||||||
|
onToggleSelect: () => void;
|
||||||
|
onView: () => void;
|
||||||
|
onExecute: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
onClone: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本卡片:复选框 + 名称 + 类型徽章 + 内容预览 + 元信息 + 行操作 // Logan.Li
|
||||||
|
export default function ScriptCard({
|
||||||
|
script: sc, selected, executing, onToggleSelect,
|
||||||
|
onView, onExecute, onEdit, onClone, onDelete,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<Checkbox
|
||||||
|
checked={selected}
|
||||||
|
onCheckedChange={onToggleSelect}
|
||||||
|
className="border-[var(--dg-border)]"
|
||||||
|
/>
|
||||||
|
<FileCode className="w-5 h-5 text-purple-500 shrink-0" />
|
||||||
|
<CardTitle className="text-base truncate">{sc.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
{getTypeBadge(sc.type)}
|
||||||
|
</div>
|
||||||
|
<CardDescription className="line-clamp-2">{sc.description || '暂无描述'}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="bg-muted/40 rounded-md p-2 mb-3 font-mono text-xs text-muted-foreground overflow-hidden">
|
||||||
|
<pre className="whitespace-pre-wrap line-clamp-3">{sc.content}</pre>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
|
||||||
|
<span>执行 {sc.run_count} 次 · v{sc.version}</span>
|
||||||
|
<span>超时 {sc.timeout}s</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button variant="ghost" size="sm" title="查看内容" onClick={onView}>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" title="执行" disabled={executing} onClick={onExecute}>
|
||||||
|
{executing
|
||||||
|
? <Loader2 className="w-4 h-4 animate-spin text-blue-600" />
|
||||||
|
: <Play className="w-4 h-4 text-green-600" />}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" title="编辑" onClick={onEdit}>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" title="复制" onClick={onClone}>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" title="删除" onClick={onDelete}>
|
||||||
|
<Trash2 className="w-4 h-4 text-red-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// ScriptExecutionsTable — 脚本执行记录表格 // Logan.Li
|
||||||
|
import { RotateCcw, Terminal } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { type ExecutionRecord, getStatusBadge, formatDate } from './script-helpers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
executions: ExecutionRecord[];
|
||||||
|
onRefresh: () => void;
|
||||||
|
onViewLogs: (exec: ExecutionRecord) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行记录表格:序号/脚本/状态/开始时间/耗时/查看日志 // Logan.Li
|
||||||
|
export default function ScriptExecutionsTable({ executions, onRefresh, onViewLogs }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button variant="outline" onClick={onRefresh}>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">#</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">脚本</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">开始时间</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">耗时</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{executions.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">
|
||||||
|
暂无执行记录
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
executions.map((job) => (
|
||||||
|
<tr key={job.id} className="hover:bg-muted/50">
|
||||||
|
<td className="px-4 py-3 text-sm text-muted-foreground">{job.id}</td>
|
||||||
|
<td className="px-4 py-3 text-sm">{job.task_name}</td>
|
||||||
|
<td className="px-4 py-3">{getStatusBadge(job.status)}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-muted-foreground">{formatDate(job.started_at)}</td>
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
{job.duration ? `${(job.duration / 1000).toFixed(1)}s` : '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<Button variant="ghost" size="sm" title="查看日志" onClick={() => onViewLogs(job)}>
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
|
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import { Plus, Search, RotateCcw, Trash2, Loader2 } from 'lucide-react';
|
||||||
Play, FileCode, Clock, CheckCircle, XCircle, RotateCcw, Eye,
|
|
||||||
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
@@ -16,12 +12,15 @@ import { toast } from 'sonner';
|
|||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { scriptApi, taskExecutionApi } from '@/services/api';
|
import { scriptApi, taskExecutionApi } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
type Script, type ExecutionRecord, mapScript, scriptTypes, getTypeBadge,
|
type Script, type ExecutionRecord, mapScript, scriptTypes, genCode,
|
||||||
genCode, getStatusBadge, formatDate,
|
|
||||||
} from './script-helpers';
|
} from './script-helpers';
|
||||||
import ScriptFormDialog, { type ScriptFormData } from './ScriptFormDialog';
|
import type { ScriptFormData } from './ScriptFormDialog';
|
||||||
|
import ScriptFormDialog from './ScriptFormDialog';
|
||||||
import ScriptDetailDialog from './ScriptDetailDialog';
|
import ScriptDetailDialog from './ScriptDetailDialog';
|
||||||
import ScriptLogsDialog from './ScriptLogsDialog';
|
import ScriptLogsDialog from './ScriptLogsDialog';
|
||||||
|
import ScriptStatsCards from './ScriptStatsCards';
|
||||||
|
import ScriptCard from './ScriptCard';
|
||||||
|
import ScriptExecutionsTable from './ScriptExecutionsTable';
|
||||||
|
|
||||||
export function ScriptManager() {
|
export function ScriptManager() {
|
||||||
const _confirm = useConfirm();
|
const _confirm = useConfirm();
|
||||||
@@ -225,12 +224,10 @@ export function ScriptManager() {
|
|||||||
setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
|
setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allSelected = filteredScripts.length > 0 && selectedIds.length === filteredScripts.length;
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const toggleSelectAll = () => {
|
||||||
setSelectedIds(prev =>
|
setSelectedIds(allSelected ? [] : filteredScripts.map(sc => sc.id));
|
||||||
prev.length === filteredScripts.length && filteredScripts.length > 0
|
|
||||||
? []
|
|
||||||
: filteredScripts.map(sc => sc.id)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBatchDelete = async () => {
|
const handleBatchDelete = async () => {
|
||||||
@@ -252,8 +249,6 @@ export function ScriptManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allSelected = filteredScripts.length > 0 && selectedIds.length === filteredScripts.length;
|
|
||||||
|
|
||||||
const runningCount = executions.filter(e => e.status === 'running').length;
|
const runningCount = executions.filter(e => e.status === 'running').length;
|
||||||
const todaySuccess = executions.filter(e =>
|
const todaySuccess = executions.filter(e =>
|
||||||
e.status === 'success' && new Date(e.created_at).toDateString() === new Date().toDateString()
|
e.status === 'success' && new Date(e.created_at).toDateString() === new Date().toDateString()
|
||||||
@@ -265,60 +260,12 @@ export function ScriptManager() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<ScriptStatsCards
|
||||||
<Card>
|
total={scripts.length}
|
||||||
<CardContent className="p-4">
|
running={runningCount}
|
||||||
<div className="flex items-center justify-between">
|
todaySuccess={todaySuccess}
|
||||||
<div>
|
todayFailed={todayFailed}
|
||||||
<p className="text-sm text-muted-foreground">脚本总数</p>
|
/>
|
||||||
<p className="text-2xl font-bold">{scripts.length}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-purple-500/10 rounded-lg">
|
|
||||||
<FileCode className="w-5 h-5 text-purple-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">执行中</p>
|
|
||||||
<p className="text-2xl font-bold text-blue-600">{runningCount}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-blue-500/10 rounded-lg">
|
|
||||||
<Loader2 className="w-5 h-5 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">今日成功</p>
|
|
||||||
<p className="text-2xl font-bold text-green-600">{todaySuccess}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-green-500/10 rounded-lg">
|
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground">今日失败</p>
|
|
||||||
<p className="text-2xl font-bold text-red-600">{todayFailed}</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-red-500/10 rounded-lg">
|
|
||||||
<XCircle className="w-5 h-5 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
|
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
|
||||||
<TabsList className="grid w-full grid-cols-2 lg:w-[360px]">
|
<TabsList className="grid w-full grid-cols-2 lg:w-[360px]">
|
||||||
@@ -406,112 +353,30 @@ export function ScriptManager() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredScripts.map((sc) => (
|
filteredScripts.map((sc) => (
|
||||||
<Card key={sc.id} className="hover:shadow-md transition-shadow">
|
<ScriptCard
|
||||||
<CardHeader className="pb-3">
|
key={sc.id}
|
||||||
<div className="flex items-start justify-between gap-2">
|
script={sc}
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
selected={selectedIds.includes(sc.id)}
|
||||||
<Checkbox
|
executing={executing}
|
||||||
checked={selectedIds.includes(sc.id)}
|
onToggleSelect={() => toggleSelect(sc.id)}
|
||||||
onCheckedChange={() => toggleSelect(sc.id)}
|
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
|
||||||
className="border-[var(--dg-border)]"
|
onExecute={() => handleExecute(sc)}
|
||||||
/>
|
onEdit={() => openEdit(sc)}
|
||||||
<FileCode className="w-5 h-5 text-purple-500 shrink-0" />
|
onClone={() => handleClone(sc)}
|
||||||
<CardTitle className="text-base truncate">{sc.name}</CardTitle>
|
onDelete={() => handleDelete(sc)}
|
||||||
</div>
|
/>
|
||||||
{getTypeBadge(sc.type)}
|
|
||||||
</div>
|
|
||||||
<CardDescription className="line-clamp-2">{sc.description || '暂无描述'}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<div className="bg-muted/40 rounded-md p-2 mb-3 font-mono text-xs text-muted-foreground overflow-hidden">
|
|
||||||
<pre className="whitespace-pre-wrap line-clamp-3">{sc.content}</pre>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-3">
|
|
||||||
<span>执行 {sc.run_count} 次 · v{sc.version}</span>
|
|
||||||
<span>超时 {sc.timeout}s</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button variant="ghost" size="sm" title="查看内容"
|
|
||||||
onClick={() => { setSelectedScript(sc); setDetailOpen(true); }}>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" title="执行"
|
|
||||||
disabled={executing}
|
|
||||||
onClick={() => handleExecute(sc)}>
|
|
||||||
{executing
|
|
||||||
? <Loader2 className="w-4 h-4 animate-spin text-blue-600" />
|
|
||||||
: <Play className="w-4 h-4 text-green-600" />}
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" title="编辑" onClick={() => openEdit(sc)}>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" title="复制" onClick={() => handleClone(sc)}>
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" title="删除" onClick={() => handleDelete(sc)}>
|
|
||||||
<Trash2 className="w-4 h-4 text-red-600" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* ============ 执行记录 ============ */}
|
{/* ============ 执行记录 ============ */}
|
||||||
<TabsContent value="jobs" className="space-y-4">
|
<TabsContent value="jobs">
|
||||||
<div className="flex justify-end">
|
<ScriptExecutionsTable
|
||||||
<Button variant="outline" onClick={fetchExecutions}>
|
executions={executions}
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
onRefresh={fetchExecutions}
|
||||||
刷新
|
onViewLogs={(job) => { setSelectedExec(job); setLogsOpen(true); }}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-muted/50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">#</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">脚本</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">开始时间</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">耗时</th>
|
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y">
|
|
||||||
{executions.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">
|
|
||||||
暂无执行记录
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
executions.map((job) => (
|
|
||||||
<tr key={job.id} className="hover:bg-muted/50">
|
|
||||||
<td className="px-4 py-3 text-sm text-muted-foreground">{job.id}</td>
|
|
||||||
<td className="px-4 py-3 text-sm">{job.task_name}</td>
|
|
||||||
<td className="px-4 py-3">{getStatusBadge(job.status)}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-muted-foreground">{formatDate(job.started_at)}</td>
|
|
||||||
<td className="px-4 py-3 text-sm">
|
|
||||||
{job.duration ? `${(job.duration / 1000).toFixed(1)}s` : '-'}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<Button variant="ghost" size="sm" title="查看日志"
|
|
||||||
onClick={() => { setSelectedExec(job); setLogsOpen(true); }}>
|
|
||||||
<Terminal className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// ScriptStatsCards — 脚本库统计卡片 // Logan.Li
|
||||||
|
import { FileCode, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
total: number;
|
||||||
|
running: number;
|
||||||
|
todaySuccess: number;
|
||||||
|
todayFailed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 四个统计卡片:脚本总数 / 执行中 / 今日成功 / 今日失败 // Logan.Li
|
||||||
|
export default function ScriptStatsCards({ total, running, todaySuccess, todayFailed }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">脚本总数</p>
|
||||||
|
<p className="text-2xl font-bold">{total}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-purple-500/10 rounded-lg">
|
||||||
|
<FileCode className="w-5 h-5 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">执行中</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">{running}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-blue-500/10 rounded-lg">
|
||||||
|
<Loader2 className="w-5 h-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">今日成功</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600">{todaySuccess}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-500/10 rounded-lg">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">今日失败</p>
|
||||||
|
<p className="text-2xl font-bold text-red-600">{todayFailed}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-red-500/10 rounded-lg">
|
||||||
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user