refactor: 拆分ScriptManager.tsx(546→411行)提取ScriptStatsCards+ScriptCard+ScriptExecutionsTable子组件

This commit is contained in:
attacker committed 2026-06-17 04:04:33 +08:00
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
import React, { useState, useEffect } from 'react';
import {
Play, FileCode, Clock, CheckCircle, XCircle, RotateCcw, Eye,
Plus, Search, Trash2, Edit, Copy, Terminal, Loader2,
} from 'lucide-react';
import { Plus, Search, RotateCcw, Trash2, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
@@ -16,12 +12,15 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { scriptApi, taskExecutionApi } from '@/services/api';
import {
type Script, type ExecutionRecord, mapScript, scriptTypes, getTypeBadge,
genCode, getStatusBadge, formatDate,
type Script, type ExecutionRecord, mapScript, scriptTypes, genCode,
} from './script-helpers';
import ScriptFormDialog, { type ScriptFormData } from './ScriptFormDialog';
import type { ScriptFormData } from './ScriptFormDialog';
import ScriptFormDialog from './ScriptFormDialog';
import ScriptDetailDialog from './ScriptDetailDialog';
import ScriptLogsDialog from './ScriptLogsDialog';
import ScriptStatsCards from './ScriptStatsCards';
import ScriptCard from './ScriptCard';
import ScriptExecutionsTable from './ScriptExecutionsTable';
export function ScriptManager() {
const _confirm = useConfirm();
@@ -225,12 +224,10 @@ export function ScriptManager() {
setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
};
const allSelected = filteredScripts.length > 0 && selectedIds.length === filteredScripts.length;
const toggleSelectAll = () => {
setSelectedIds(prev =>
prev.length === filteredScripts.length && filteredScripts.length > 0
? []
: filteredScripts.map(sc => sc.id)
);
setSelectedIds(allSelected ? [] : filteredScripts.map(sc => sc.id));
};
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 todaySuccess = executions.filter(e =>
e.status === 'success' && new Date(e.created_at).toDateString() === new Date().toDateString()
@@ -265,60 +260,12 @@ export function ScriptManager() {
return (
<div className="space-y-6">
{/* 统计卡片 */}
<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">{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>
<ScriptStatsCards
total={scripts.length}
running={runningCount}
todaySuccess={todaySuccess}
todayFailed={todayFailed}
/>
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); if (v === 'jobs') fetchExecutions(); }}>
<TabsList className="grid w-full grid-cols-2 lg:w-[360px]">
@@ -406,112 +353,30 @@ export function ScriptManager() {
</div>
) : (
filteredScripts.map((sc) => (
<Card key={sc.id} 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={selectedIds.includes(sc.id)}
onCheckedChange={() => toggleSelect(sc.id)}
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={() => { 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>
<ScriptCard
key={sc.id}
script={sc}
selected={selectedIds.includes(sc.id)}
executing={executing}
onToggleSelect={() => toggleSelect(sc.id)}
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
onExecute={() => handleExecute(sc)}
onEdit={() => openEdit(sc)}
onClone={() => handleClone(sc)}
onDelete={() => handleDelete(sc)}
/>
))
)}
</div>
</TabsContent>
{/* ============ 执行记录 ============ */}
<TabsContent value="jobs" className="space-y-4">
<div className="flex justify-end">
<Button variant="outline" onClick={fetchExecutions}>
<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={() => { setSelectedExec(job); setLogsOpen(true); }}>
<Terminal className="w-4 h-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
<TabsContent value="jobs">
<ScriptExecutionsTable
executions={executions}
onRefresh={fetchExecutions}
onViewLogs={(job) => { setSelectedExec(job); setLogsOpen(true); }}
/>
</TabsContent>
</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>
);
}