refactor: ScriptManager split into 5 files (730 to 546 lines)
- script-helpers.tsx (97 lines): Script/ExecutionRecord types, mapScript, scriptTypes, getTypeBadge, genCode, getStatusBadge, formatDate - ScriptFormDialog.tsx (113 lines): new/edit script dialog - ScriptDetailDialog.tsx (49 lines): view script content dialog - ScriptLogsDialog.tsx (39 lines): execution logs dialog - ScriptManager.tsx (546 lines): main component, imports sub-components Verified: tsc --noEmit zero errors, npm run build passes, browser tested all 3 dialogs + tabs + stats cards, console zero errors
This commit is contained in:
1 parent
23fe742cc2
commit
745dde358e
5 files changed
+331
-217
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// Script 查看内容弹窗 // Logan.Li
|
||||
import { FileCode, Edit } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { Script } from './script-helpers';
|
||||
import { getTypeBadge } from './script-helpers';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
script: Script | null;
|
||||
onEdit: (sc: Script) => void;
|
||||
}
|
||||
|
||||
const ScriptDetailDialog: React.FC<Props> = ({ open, onOpenChange, script, onEdit }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileCode className="w-5 h-5 text-purple-500" />
|
||||
{script?.name}
|
||||
{script && getTypeBadge(script.type)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{script?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{script?.content || '(空)'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||
{script && (
|
||||
<Button onClick={() => onEdit(script)}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptDetailDialog;
|
||||
@@ -0,0 +1,113 @@
|
||||
// Script 新建/编辑弹窗 // Logan.Li
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { scriptTypes } from './script-helpers';
|
||||
|
||||
export interface ScriptFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
type: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingId: number | null;
|
||||
editingCode: string;
|
||||
formData: ScriptFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<ScriptFormData>>;
|
||||
onTypeChange: (t: string) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
const ScriptFormDialog: React.FC<Props> = ({
|
||||
open, onOpenChange, editingId, editingCode, formData, setFormData, onTypeChange, onSave,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId !== null ? '编辑脚本' : '新建脚本'}</DialogTitle>
|
||||
<DialogDescription>编写运维脚本,支持 Shell / Python / PowerShell</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 overflow-y-auto max-h-[60vh] pr-1">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="如:系统巡检脚本"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>类型</Label>
|
||||
<Select value={formData.type} onValueChange={onTypeChange}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{scriptTypes.map(t => (
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="脚本用途说明"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>超时(秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.timeout}
|
||||
onChange={(e) => setFormData({ ...formData, timeout: Number(e.target.value) })}
|
||||
placeholder="300"
|
||||
/>
|
||||
</div>
|
||||
{editingId !== null && (
|
||||
<div>
|
||||
<Label>编码</Label>
|
||||
<Input value={editingCode} disabled className="font-mono text-xs" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>脚本内容</Label>
|
||||
<Textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
className="min-h-[360px] font-mono text-sm"
|
||||
placeholder="在此编写脚本内容..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button onClick={onSave}>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{editingId !== null ? '保存' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptFormDialog;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Script 执行日志弹窗 // Logan.Li
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { ExecutionRecord } from './script-helpers';
|
||||
import { getStatusBadge } from './script-helpers';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
exec: ExecutionRecord | null;
|
||||
}
|
||||
|
||||
const ScriptLogsDialog: React.FC<Props> = ({ open, onOpenChange, exec }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>执行日志</DialogTitle>
|
||||
<DialogDescription>
|
||||
{exec?.task_name} · {getStatusBadge(exec?.status || '')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[480px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{exec?.output || '暂无日志输出...'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptLogsDialog;
|
||||
@@ -1,3 +1,4 @@
|
||||
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Play, FileCode, Clock, CheckCircle, XCircle, RotateCcw, Eye,
|
||||
@@ -5,92 +6,22 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { scriptApi, taskExecutionApi } from '@/services/api';
|
||||
|
||||
// ---- 类型定义 ----
|
||||
|
||||
interface Script {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string; // shell | python | powershell | yaml
|
||||
content: string;
|
||||
timeout: number;
|
||||
version: number;
|
||||
run_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ExecutionRecord {
|
||||
id: number;
|
||||
task_type: string;
|
||||
task_id: number;
|
||||
task_name: string;
|
||||
status: string;
|
||||
output: string;
|
||||
duration: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 将后端返回的脚本原始对象映射为前端结构
|
||||
function mapScript(raw: any): Script {
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
code: raw.code || '',
|
||||
description: raw.description || '',
|
||||
type: raw.type || 'shell',
|
||||
content: raw.content || '',
|
||||
timeout: raw.timeout || 300,
|
||||
version: raw.version || 1,
|
||||
run_count: raw.run_count || 0,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
const scriptTypes = [
|
||||
{ value: 'shell', label: 'Shell', sample: '#!/bin/bash\n# 系统巡检脚本\necho "===== 系统信息 ====="\nuname -a\necho ""\necho "===== 磁盘使用 ====="\ndf -h\necho ""\necho "===== 内存使用 ====="\nfree -h\n' },
|
||||
{ value: 'python', label: 'Python', sample: '#!/usr/bin/env python3\n# 主机信息采集\nimport platform, socket\nprint("主机名:", socket.gethostname())\nprint("系统:", platform.platform())\nprint("Python:", platform.python_version())\n' },
|
||||
{ value: 'powershell', label: 'PowerShell', sample: '# Windows 系统检查\nWrite-Host "===== 系统信息 ====="\nGet-ComputerInfo | Select-Object CsName, WindowsVersion\nWrite-Host "===== 磁盘使用 ====="\nGet-PSDrive -PSProvider FileSystem | Format-Table\n' },
|
||||
{ value: 'yaml', label: 'YAML', sample: '# YAML 配置/任务定义\nname: example-task\nsteps:\n - name: step1\n action: echo\n message: hello\n' },
|
||||
];
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
shell: 'border-green-500/30 text-green-500 bg-green-500/10',
|
||||
bash: 'border-green-500/30 text-green-500 bg-green-500/10',
|
||||
python: 'border-blue-500/30 text-blue-500 bg-blue-500/10',
|
||||
powershell: 'border-cyan-500/30 text-cyan-500 bg-cyan-500/10',
|
||||
yaml: 'border-purple-500/30 text-purple-500 bg-purple-500/10',
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => (
|
||||
<Badge variant="outline" className={typeColors[type] || typeColors.shell}>
|
||||
{(type || 'shell').toUpperCase()}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
// 生成唯一脚本编码
|
||||
const genCode = () => `sc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
import {
|
||||
type Script, type ExecutionRecord, mapScript, scriptTypes, getTypeBadge,
|
||||
genCode, getStatusBadge, formatDate,
|
||||
} from './script-helpers';
|
||||
import ScriptFormDialog, { type ScriptFormData } from './ScriptFormDialog';
|
||||
import ScriptDetailDialog from './ScriptDetailDialog';
|
||||
import ScriptLogsDialog from './ScriptLogsDialog';
|
||||
|
||||
export function ScriptManager() {
|
||||
const _confirm = useConfirm();
|
||||
@@ -112,7 +43,7 @@ export function ScriptManager() {
|
||||
const [selectedExec, setSelectedExec] = useState<ExecutionRecord | null>(null);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
const [formData, setFormData] = useState<ScriptFormData>({
|
||||
name: '', description: '', content: '', type: 'shell', timeout: 300,
|
||||
});
|
||||
|
||||
@@ -157,6 +88,7 @@ export function ScriptManager() {
|
||||
|
||||
const openEdit = (sc: Script) => {
|
||||
setEditingId(sc.id);
|
||||
setSelectedScript(sc);
|
||||
setFormData({
|
||||
name: sc.name,
|
||||
description: sc.description,
|
||||
@@ -165,6 +97,7 @@ export function ScriptManager() {
|
||||
timeout: sc.timeout,
|
||||
});
|
||||
setEditorOpen(true);
|
||||
setDetailOpen(false);
|
||||
};
|
||||
|
||||
const handleTypeChange = (t: string) => {
|
||||
@@ -195,7 +128,6 @@ export function ScriptManager() {
|
||||
};
|
||||
try {
|
||||
if (editingId !== null) {
|
||||
// 更新时后端忽略 code 字段(按主键更新)
|
||||
await scriptApi.updateScript(editingId, payload);
|
||||
toast.success('脚本更新成功');
|
||||
} else {
|
||||
@@ -278,27 +210,6 @@ export function ScriptManager() {
|
||||
|
||||
// ---- 辅助 ----
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="gap-1"><Clock className="w-3 h-3" />等待中</Badge>;
|
||||
case 'running':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Loader2 className="w-3 h-3 animate-spin" />执行中</Badge>;
|
||||
case 'success':
|
||||
return <Badge variant="default" className="bg-green-600 gap-1"><CheckCircle className="w-3 h-3" />成功</Badge>;
|
||||
case 'failed':
|
||||
return <Badge variant="destructive" className="gap-1"><XCircle className="w-3 h-3" />失败</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">{status || '未知'}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (s: string | null) => {
|
||||
if (!s) return '-';
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleString('zh-CN', { hour12: false });
|
||||
};
|
||||
|
||||
const filteredScripts = scripts.filter(sc => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchSearch = sc.name.toLowerCase().includes(q) ||
|
||||
@@ -605,126 +516,31 @@ export function ScriptManager() {
|
||||
</Tabs>
|
||||
|
||||
{/* ===== 新建/编辑脚本对话框 ===== */}
|
||||
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId !== null ? '编辑脚本' : '新建脚本'}</DialogTitle>
|
||||
<DialogDescription>编写运维脚本,支持 Shell / Python / PowerShell</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 overflow-y-auto max-h-[60vh] pr-1">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="如:系统巡检脚本"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>类型</Label>
|
||||
<Select value={formData.type} onValueChange={handleTypeChange}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{scriptTypes.map(t => (
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="脚本用途说明"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>超时(秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.timeout}
|
||||
onChange={(e) => setFormData({ ...formData, timeout: Number(e.target.value) })}
|
||||
placeholder="300"
|
||||
/>
|
||||
</div>
|
||||
{editingId !== null && (
|
||||
<div>
|
||||
<Label>编码</Label>
|
||||
<Input value={selectedScript?.code || ''} disabled className="font-mono text-xs" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label>脚本内容</Label>
|
||||
<Textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
className="min-h-[360px] font-mono text-sm"
|
||||
placeholder="在此编写脚本内容..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditorOpen(false)}>取消</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{editingId !== null ? '保存' : '创建'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ScriptFormDialog
|
||||
open={editorOpen}
|
||||
onOpenChange={setEditorOpen}
|
||||
editingId={editingId}
|
||||
editingCode={selectedScript?.code || ''}
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onTypeChange={handleTypeChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
{/* ===== 查看脚本内容 ===== */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileCode className="w-5 h-5 text-purple-500" />
|
||||
{selectedScript?.name}
|
||||
{selectedScript && getTypeBadge(selectedScript.type)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{selectedScript?.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{selectedScript?.content || '(空)'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||
{selectedScript && (
|
||||
<Button onClick={() => { setDetailOpen(false); openEdit(selectedScript); }}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ScriptDetailDialog
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
script={selectedScript}
|
||||
onEdit={openEdit}
|
||||
/>
|
||||
|
||||
{/* ===== 执行日志 ===== */}
|
||||
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>执行日志</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedExec?.task_name} · {getStatusBadge(selectedExec?.status || '')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[480px] w-full rounded-md border bg-black">
|
||||
<pre className="p-4 font-mono text-sm text-green-400 whitespace-pre-wrap">
|
||||
{selectedExec?.output || '暂无日志输出...'}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setLogsOpen(false)}>关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ScriptLogsDialog
|
||||
open={logsOpen}
|
||||
onOpenChange={setLogsOpen}
|
||||
exec={selectedExec}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// ScriptManager 共享类型与工具函数 // Logan.Li
|
||||
import React from 'react';
|
||||
import { Clock, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
// ---- 类型定义 ----
|
||||
|
||||
export interface Script {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string; // shell | python | powershell | yaml
|
||||
content: string;
|
||||
timeout: number;
|
||||
version: number;
|
||||
run_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ExecutionRecord {
|
||||
id: number;
|
||||
task_type: string;
|
||||
task_id: number;
|
||||
task_name: string;
|
||||
status: string;
|
||||
output: string;
|
||||
duration: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 将后端返回的脚本原始对象映射为前端结构
|
||||
export function mapScript(raw: any): Script {
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
code: raw.code || '',
|
||||
description: raw.description || '',
|
||||
type: raw.type || 'shell',
|
||||
content: raw.content || '',
|
||||
timeout: raw.timeout || 300,
|
||||
version: raw.version || 1,
|
||||
run_count: raw.run_count || 0,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export const scriptTypes = [
|
||||
{ value: 'shell', label: 'Shell', sample: '#!/bin/bash\n# 系统巡检脚本\necho "===== 系统信息 ====="\nuname -a\necho ""\necho "===== 磁盘使用 ====="\ndf -h\necho ""\necho "===== 内存使用 ====="\nfree -h\n' },
|
||||
{ value: 'python', label: 'Python', sample: '#!/usr/bin/env python3\n# 主机信息采集\nimport platform, socket\nprint("主机名:", socket.gethostname())\nprint("系统:", platform.platform())\nprint("Python:", platform.python_version())\n' },
|
||||
{ value: 'powershell', label: 'PowerShell', sample: '# Windows 系统检查\nWrite-Host "===== 系统信息 ====="\nGet-ComputerInfo | Select-Object CsName, WindowsVersion\nWrite-Host "===== 磁盘使用 ====="\nGet-PSDrive -PSProvider FileSystem | Format-Table\n' },
|
||||
{ value: 'yaml', label: 'YAML', sample: '# YAML 配置/任务定义\nname: example-task\nsteps:\n - name: step1\n action: echo\n message: hello\n' },
|
||||
];
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
shell: 'border-green-500/30 text-green-500 bg-green-500/10',
|
||||
bash: 'border-green-500/30 text-green-500 bg-green-500/10',
|
||||
python: 'border-blue-500/30 text-blue-500 bg-blue-500/10',
|
||||
powershell: 'border-cyan-500/30 text-cyan-500 bg-cyan-500/10',
|
||||
yaml: 'border-purple-500/30 text-purple-500 bg-purple-500/10',
|
||||
};
|
||||
|
||||
export const getTypeBadge = (type: string) => (
|
||||
<Badge variant="outline" className={typeColors[type] || typeColors.shell}>
|
||||
{(type || 'shell').toUpperCase()}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
// 生成唯一脚本编码
|
||||
export const genCode = () => `sc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
// 执行状态 Badge
|
||||
export function getStatusBadge(status: string) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="gap-1"><Clock className="w-3 h-3" />等待中</Badge>;
|
||||
case 'running':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Loader2 className="w-3 h-3 animate-spin" />执行中</Badge>;
|
||||
case 'success':
|
||||
return <Badge variant="default" className="bg-green-600 gap-1"><CheckCircle className="w-3 h-3" />成功</Badge>;
|
||||
case 'failed':
|
||||
return <Badge variant="destructive" className="gap-1"><XCircle className="w-3 h-3" />失败</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">{status || '未知'}</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export const formatDate = (s: string | null) => {
|
||||
if (!s) return '-';
|
||||
const d = new Date(s);
|
||||
return Number.isNaN(d.getTime()) ? '-' : d.toLocaleString('zh-CN', { hour12: false });
|
||||
};
|
||||
Reference in new issue
Block a user