+ {/* Header */}
+
+
+
Ansible 自动化部署
+
模板 → 匹配主机 → 执行部署 → 历史记录
+
+
+
+ 工作流
+ 执行记录
+
+
+
+
+ {/* Step Indicator */}
+ {activeTab === 'workflow' && (
+
+ {stepsList.map((s, i) => (
+
+
+ {i < stepsList.length - 1 && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* ============ Workflow Content ============ */}
+ {activeTab === 'workflow' && (
+ <>
+ {/* Step 1: Template Selection */}
+ {step === 'template' && (
+
+
+
+
+ 选择 Playbook 模板
+
+
+
+
+
+
+ setSearchPB(e.target.value)}
+ className="pl-10 bg-slate-800 border-slate-700"
+ />
+
+
+
+
+
+ {loadingPB ? (
+
+ 加载中…
+
+ ) : (
+
+ {filteredPlaybooks.map(pb => {
+ const Icon = catIcons[pb.category] || FileText;
+ const selected = selectedPB?.id === pb.id;
+ return (
+
setSelectedPB(selected ? null : pb)}
+ className={`p-4 rounded-lg border cursor-pointer transition-all ${
+ selected
+ ? 'border-blue-500 bg-blue-500/10 ring-1 ring-blue-500/30'
+ : 'border-slate-700 bg-slate-800/50 hover:border-slate-600'
+ }`}
+ >
+
+
+
+
+
+
{pb.name}
+
{pb.description}
+
+ {pb.tags.slice(0, 3).map(t => (
+
+ {t}
+
+ ))}
+
+ {categories.find(c => c.value === pb.category)?.label || pb.category}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* Step 2: Host Matching */}
+ {step === 'hosts' && (
+
+
+
+
+ 匹配目标主机
+
+
+
+
+
+
+ setHostPattern(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && matchHosts(hostPattern, matchType)}
+ className="pl-10 bg-slate-800 border-slate-700 font-mono text-sm"
+ />
+
+
+
+
+
+ {/* Host table */}
+ {loadingHosts ? (
+
+ 匹配中…
+
+ ) : (
+ <>
+
+
+ 匹配 {hostsTotal} 台主机
+ {selectedHosts.size > 0 && (
+ (已选 {selectedHosts.size} 台)
+ )}
+
+ {filteredHosts.length > 0 && (
+
+ 0}
+ onCheckedChange={toggleAllHosts}
+ />
+ 全选
+
+ )}
+
+
+
+
+
+
+ 选
+ 主机名
+ IP
+ OS
+ 状态
+ 认证
+
+
+
+ {filteredHosts.length === 0 ? (
+
+
+ 输入匹配模式后点击"匹配"预览主机
+
+
+ ) : (
+ filteredHosts.map(h => (
+ toggleHost(h.id)}
+ >
+
+
+
+ {h.hostname}
+ {h.ip}
+ {h.os || '-'}
+
+
+ {h.status === 'running' ? '运行中' : h.status || '未知'}
+
+
+
+ {h.has_auth ? (
+ 已配置
+ ) : (
+ 未配置
+ )}
+
+
+ ))
+ )}
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ )}
+
+ {/* Step 3: Review & Confirm */}
+ {step === 'review' && selectedPB && (
+
+
+
+
+ 确认执行
+
+
+
+ {/* Playbook summary */}
+
+
Playbook
+
+
+
+
+
+
{selectedPB.name}
+
{selectedPB.description}
+
+
+
+
+ {/* Host list summary */}
+
+
+ 目标主机 ({selectedHosts.size} 台)
+
+
+ {Array.from(selectedHosts).map(id => {
+ const h = hosts.find(x => x.id === id);
+ return (
+
+
+ {h?.hostname || `ID:${id}`}
+ ({h?.ip})
+
+ );
+ })}
+
+
+
+ {/* Playbook content preview */}
+
+
+ 📄 查看 Playbook YAML 内容
+
+
+ {selectedPB.content || '(空)'}
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Step 4: Execution & Monitor */}
+ {step === 'execute' && (
+
+
+
+
+ 执行监控
+ {executing && }
+
+
+
+ {/* Status indicator */}
+
+
+ {execStatus === 'success' ? :
+ execStatus === 'failed' ? :
+ }
+
+ {execStatus === 'success' ? '执行成功' :
+ execStatus === 'failed' ? '执行失败' :
+ executing ? '执行中…' : '等待执行'}
+
+
+ {execDuration > 0 && (
+
+
+ {execDuration >= 1000
+ ? `${(execDuration / 1000).toFixed(1)}s`
+ : `${execDuration}ms`}
+
+ )}
+
+
+ {/* Output terminal */}
+
+ {execOutput || (executing ? '等待输出…' : '无输出')}
+
+
+
+
+ {execJobId && (
+
+ )}
+
+
+
+ )}
+ >
+ )}
+
+ {/* ============ History Tab ============ */}
+ {activeTab === 'history' && (
+
+
+
+
+
+ 执行记录
+
+
+
+
+
+ {loadingExecs ? (
+
+ 加载中…
+
+ ) : executions.length === 0 ? (
+ 暂无执行记录
+ ) : (
+
+
+
+ Playbook
+ 状态
+ 耗时
+ 执行人
+ 时间
+ 输出
+
+
+
+ {executions.map(e => (
+
+ {e.task_name}
+ {getStatusBadge(e.status)}
+
+ {e.duration >= 1000 ? `${(e.duration / 1000).toFixed(1)}s` : `${e.duration}ms`}
+
+ {e.triggered_by || '-'}
+ {formatDate(e.created_at)}
+
+ {e.output ? (
+
+
+ 查看输出
+
+
+ {e.output.slice(0, 2000)}
+
+
+ ) : (
+ -
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/automation/ansible-helpers.tsx b/frontend/src/components/automation/ansible-helpers.tsx
index 7100253..20df843 100644
--- a/frontend/src/components/automation/ansible-helpers.tsx
+++ b/frontend/src/components/automation/ansible-helpers.tsx
@@ -25,6 +25,7 @@ export interface ExecutionRecord {
status: string;
output: string;
duration: number;
+ triggered_by: string;
started_at: string | null;
finished_at: string | null;
created_at: string;
diff --git a/frontend/src/components/automation/script-helpers.tsx b/frontend/src/components/automation/script-helpers.tsx
index c3125bd..cd9dff2 100644
--- a/frontend/src/components/automation/script-helpers.tsx
+++ b/frontend/src/components/automation/script-helpers.tsx
@@ -52,8 +52,6 @@ export function mapScript(raw: any): Script {
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