fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由

This commit is contained in:
qyyfw committed 2026-07-11 11:40:39 +08:00
1 parent d95b847fee
commit e8e5ffa1a9
65 files changed
+35624 -339

No files matched your search

+599
View File
@@ -0,0 +1,599 @@
import React, { useState, useEffect } from 'react';
import { adminAPI } from '@/api';
interface AutomationTask {
id: string;
name: string;
description: string;
trigger_type: string;
trigger_type_label: string;
cron_expression: string;
action_type: string;
action_type_label: string;
action_config: Record<string, any>;
event_condition: Record<string, any>;
is_enabled: boolean;
status: string;
max_retries: number;
timeout_seconds: number;
last_run_at: string;
next_run_at: string;
total_runs: number;
success_runs: number;
created_at: string;
updated_at: string;
}
interface ExecLog {
id: string;
task_id: string;
task_name: string;
status: string;
trigger_type: string;
started_at: string;
finished_at: string;
duration_ms: number;
result_summary: string;
error_message: string;
created_at: string;
}
interface AutomationStats {
total_tasks: number;
enabled_tasks: number;
running_tasks: number;
by_trigger: Record<string, number>;
recent_executions: number;
recent_success: number;
}
const TRIGGER_CONFIG: Record<string, { label: string; color: string; bg: string }> = {
cron: { label: '定时触发', color: '#6C63FF', bg: '#EDE9FE' },
webhook: { label: 'Webhook', color: '#059669', bg: '#D1FAE5' },
event: { label: '事件触发', color: '#D97706', bg: '#FEF3C7' },
};
const STATUS_CONFIG: Record<string, { label: string; color: string; bg: string }> = {
idle: { label: '空闲', color: '#6B7280', bg: '#F3F4F6' },
running: { label: '运行中', color: '#3B82F6', bg: '#DBEAFE' },
success: { label: '成功', color: '#10B981', bg: '#D1FAE5' },
failed: { label: '失败', color: '#EF4444', bg: '#FEE2E2' },
};
const PAGE_SIZE = 15;
const Automation: React.FC = () => {
// Data state
const [tasks, setTasks] = useState<AutomationTask[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<AutomationStats | null>(null);
// Filter state
const [filterTrigger, setFilterTrigger] = useState('');
const [keyword, setKeyword] = useState('');
// Modal state
const [modalVisible, setModalVisible] = useState(false);
const [editingTask, setEditingTask] = useState<AutomationTask | null>(null);
const [formData, setFormData] = useState({
name: '',
description: '',
trigger_type: 'cron',
cron_expression: '',
event_condition_str: '',
action_type: 'ai_pipeline',
action_config_str: '{"pipeline_name": "default"}',
is_enabled: true,
max_retries: 3,
timeout_seconds: 300,
});
const [submitting, setSubmitting] = useState(false);
// Execution history
const [historyVisible, setHistoryVisible] = useState(false);
const [historyTaskId, setHistoryTaskId] = useState('');
const [historyTaskName, setHistoryTaskName] = useState('');
const [historyLogs, setHistoryLogs] = useState<ExecLog[]>([]);
const [historyPage, setHistoryPage] = useState(1);
const [historyTotal, setHistoryTotal] = useState(0);
const [executingId, setExecutingId] = useState<string | null>(null);
// Toast
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
};
// Load stats
const loadStats = async () => {
try {
const result = await adminAPI.getAdminAutomationStats() as any;
setStats(result.data || null);
} catch (e) { /* ignore */ }
};
// Load tasks
const loadTasks = async () => {
setLoading(true);
try {
const params: any = { page, page_size: PAGE_SIZE };
if (filterTrigger) params.trigger_type = filterTrigger;
if (keyword) params.keyword = keyword;
const result = await adminAPI.getAdminAutomationTasks(params) as any;
const data = result.data || result;
setTasks(data.items || []);
setTotal(data.total || 0);
} catch (e: any) {
showToast('加载失败: ' + (e.message || e), 'error');
} finally {
setLoading(false);
}
};
useEffect(() => { loadStats(); }, []);
useEffect(() => { loadTasks(); }, [page, filterTrigger, keyword]);
// Open create modal
const openCreateModal = () => {
setEditingTask(null);
setFormData({
name: '',
description: '',
trigger_type: 'cron',
cron_expression: '*/5 * * * *',
event_condition_str: '',
action_type: 'ai_pipeline',
action_config_str: '{"pipeline_name": "default"}',
is_enabled: true,
max_retries: 3,
timeout_seconds: 300,
});
setModalVisible(true);
};
// Open edit modal
const openEditModal = async (task: AutomationTask) => {
try {
const result = await adminAPI.getAdminAutomationTask(task.id) as any;
const detail = result.data;
setEditingTask(task);
setFormData({
name: detail.name || '',
description: detail.description || '',
trigger_type: detail.trigger_type || 'cron',
cron_expression: detail.cron_expression || '',
event_condition_str: detail.event_condition ? JSON.stringify(detail.event_condition, null, 2) : '',
action_type: detail.action_type || 'ai_pipeline',
action_config_str: detail.action_config ? JSON.stringify(detail.action_config, null, 2) : '{}',
is_enabled: detail.is_enabled !== false,
max_retries: detail.max_retries || 3,
timeout_seconds: detail.timeout_seconds || 300,
});
setModalVisible(true);
} catch (e: any) {
showToast('获取任务详情失败', 'error');
}
};
// Submit
const handleSubmit = async () => {
if (!formData.name.trim()) { showToast('请输入任务名称', 'error'); return; }
if (formData.trigger_type === 'cron' && !formData.cron_expression.trim()) {
showToast('请输入Cron表达式', 'error'); return;
}
let actionConfig: any = {};
if (formData.action_config_str.trim()) {
try { actionConfig = JSON.parse(formData.action_config_str); }
catch { showToast('操作配置JSON格式不正确', 'error'); return; }
}
let eventCondition: any = undefined;
if (formData.event_condition_str.trim()) {
try { eventCondition = JSON.parse(formData.event_condition_str); }
catch { showToast('事件条件JSON格式不正确', 'error'); return; }
}
setSubmitting(true);
try {
const data: any = {
name: formData.name.trim(),
description: formData.description.trim(),
trigger_type: formData.trigger_type,
cron_expression: formData.cron_expression.trim(),
action_type: formData.action_type,
action_config: actionConfig,
is_enabled: formData.is_enabled,
max_retries: formData.max_retries,
timeout_seconds: formData.timeout_seconds,
};
if (eventCondition) data.event_condition = eventCondition;
if (editingTask) {
await adminAPI.updateAdminAutomationTask(editingTask.id, data);
showToast('更新成功');
} else {
await adminAPI.createAdminAutomationTask(data);
showToast('创建成功');
}
setModalVisible(false);
loadTasks();
loadStats();
} catch (e: any) {
showToast('操作失败: ' + (e.message || e), 'error');
} finally {
setSubmitting(false);
}
};
// Delete
const handleDelete = async (task: AutomationTask) => {
if (!window.confirm(`确定要删除任务 "${task.name}" 吗?`)) return;
try {
await adminAPI.deleteAdminAutomationTask(task.id);
showToast('删除成功');
loadTasks();
loadStats();
} catch (e: any) {
showToast('删除失败: ' + (e.message || e), 'error');
}
};
// Toggle
const handleToggle = async (task: AutomationTask) => {
try {
await adminAPI.toggleAdminAutomationTask(task.id);
showToast(task.is_enabled ? '已禁用' : '已启用');
loadTasks();
loadStats();
} catch (e: any) {
showToast('操作失败: ' + (e.message || e), 'error');
}
};
// Execute now
const handleExecuteNow = async (task: AutomationTask) => {
setExecutingId(task.id);
try {
const result = await adminAPI.executeAdminAutomationTask(task.id) as any;
showToast(result.message || '执行完成');
loadTasks();
loadStats();
} catch (e: any) {
showToast('执行失败: ' + (e.message || e), 'error');
} finally {
setExecutingId(null);
}
};
// Show history
const showHistory = async (task: AutomationTask, pageNum: number = 1) => {
setHistoryTaskId(task.id);
setHistoryTaskName(task.name);
setHistoryPage(pageNum);
setHistoryVisible(true);
try {
const result = await adminAPI.getAdminAutomationTaskHistory(task.id, { page: pageNum, page_size: 20 }) as any;
const data = result.data || result;
setHistoryLogs(data.items || []);
setHistoryTotal(data.total || 0);
} catch (e) { /* ignore */ }
};
// Format time
const formatTime = (ts: string) => {
if (!ts) return '-';
try { return new Date(ts).toLocaleString('zh-CN'); }
catch { return ts; }
};
// Format duration
const formatDuration = (ms: number) => {
if (!ms) return '-';
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(1)}s`;
};
const labelStyle: React.CSSProperties = { display: 'block', marginBottom: '4px', fontSize: '13px', color: '#374151', fontWeight: 500 };
const inputStyle: React.CSSProperties = { width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #D1D5DB', fontSize: '14px', boxSizing: 'border-box' };
const btnStyle = (bg: string, disabled = false): React.CSSProperties => ({
padding: '8px 20px', borderRadius: '6px', border: 'none', background: bg,
cursor: disabled ? 'not-allowed' : 'pointer', fontSize: '14px', color: '#fff',
opacity: disabled ? 0.7 : 1,
});
return (
<div className="admin-page">
<div className="admin-page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "24px", flexWrap: "wrap", gap: "12px" }}>
<h2>自动化任务调度</h2>
<button className="admin-btn admin-btn-primary" onClick={openCreateModal}>+ 创建任务</button>
</div>
{/* Stats Cards */}
{stats && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '16px', marginBottom: '24px' }}>
<div className="stat-card">
<div className="stat-value">{stats.total_tasks}</div>
<div className="stat-label">总任务数</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#10B981' }}>{stats.enabled_tasks}</div>
<div className="stat-label">已启用</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#3B82F6' }}>{stats.running_tasks}</div>
<div className="stat-label">运行中</div>
</div>
<div className="stat-card">
<div className="stat-value">{stats.recent_executions}</div>
<div className="stat-label">24小时执行</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: '#10B981' }}>{stats.recent_success}</div>
<div className="stat-label">24小时成功</div>
</div>
</div>
)}
{/* Filter Bar */}
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', alignItems: 'center' }}>
<select value={filterTrigger} onChange={(e) => { setFilterTrigger(e.target.value); setPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #D1D5DB', fontSize: '14px' }}>
<option value="">全部类型</option>
{Object.entries(TRIGGER_CONFIG).map(([k, v]) => (<option key={k} value={k}>{v.label}</option>))}
</select>
<input type="text" placeholder="搜索任务名称..."
value={keyword} onChange={(e) => { setKeyword(e.target.value); setPage(1); }}
style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #D1D5DB', fontSize: '14px', flex: 1, maxWidth: '400px' }} />
</div>
{/* Table */}
<div style={{ background: '#fff', borderRadius: '12px', border: '1px solid #E5E7EB', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#F9FAFB', borderBottom: '1px solid #E5E7EB' }}>
<th style={thStyle}>任务名称</th>
<th style={thStyle}>触发方式</th>
<th style={thStyle}>操作类型</th>
<th style={thStyle}>状态</th>
<th style={thStyle}>上次执行</th>
<th style={thStyle}>下次执行</th>
<th style={thStyle}>成功率</th>
<th style={{ ...thStyle, textAlign: 'right', width: '320px' }}>操作</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={8} style={emptyStyle}>加载中...</td></tr>
) : tasks.length === 0 ? (
<tr><td colSpan={8} style={emptyStyle}>暂无自动化任务</td></tr>
) : (
tasks.map(task => (
<tr key={task.id} style={{ borderBottom: '1px solid #F3F4F6' }}>
<td style={tdStyle}>
<div style={{ fontWeight: 500 }}>{task.name}</div>
{task.description && <div style={{ fontSize: '12px', color: '#9CA3AF', marginTop: '2px' }}>{task.description}</div>}
</td>
<td style={tdStyle}>
<span style={badgeStyle(TRIGGER_CONFIG[task.trigger_type] || TRIGGER_CONFIG.cron)}>
{(TRIGGER_CONFIG[task.trigger_type] || {}).label || task.trigger_type}
</span>
{task.cron_expression && <div style={{ fontSize: '12px', fontFamily: 'monospace', marginTop: '2px', color: '#6B7280' }}>{task.cron_expression}</div>}
</td>
<td style={tdStyle}>{task.action_type_label}</td>
<td style={{ ...tdStyle, textAlign: 'center' }}>
<span style={badgeStyle(STATUS_CONFIG[task.status] || STATUS_CONFIG.idle)}>
{(STATUS_CONFIG[task.status] || {}).label || task.status}
</span>
</td>
<td style={tdStyle}>{formatTime(task.last_run_at)}</td>
<td style={tdStyle}>{formatTime(task.next_run_at)}</td>
<td style={tdStyle}>
{task.total_runs > 0
? <span style={{ color: task.success_runs / task.total_runs >= 0.9 ? '#10B981' : '#D97706' }}>
{Math.round(task.success_runs / task.total_runs * 100)}%
<span style={{ fontSize: '11px', color: '#9CA3AF' }}> ({task.success_runs}/{task.total_runs})</span>
</span>
: <span style={{ color: '#9CA3AF' }}>-</span>}
</td>
<td style={{ ...tdStyle, textAlign: 'right' }}>
<div style={{ display: 'flex', gap: '6px', justifyContent: 'flex-end' }}>
<button onClick={() => handleExecuteNow(task)} disabled={executingId === task.id || task.status === 'running'}
style={actionBtnStyle('#6C63FF', executingId === task.id)}>
{executingId === task.id ? '执行中...' : '执行'}
</button>
<button onClick={() => showHistory(task)}
style={actionBtnStyle('#374151')}>历史</button>
<button onClick={() => handleToggle(task)}
style={actionBtnStyle(task.is_enabled ? '#EF4444' : '#10B981')}>
{task.is_enabled ? '禁用' : '启用'}
</button>
<button onClick={() => openEditModal(task)}
style={actionBtnStyle('#374151')}>编辑</button>
<button onClick={() => handleDelete(task)}
style={actionBtnStyle('#EF4444', false, true)}>删除</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{total > PAGE_SIZE && (
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '20px' }}>
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid #D1D5DB', background: '#fff', cursor: page <= 1 ? 'not-allowed' : 'pointer', opacity: page <= 1 ? 0.5 : 1 }}>
上一页
</button>
<span style={{ padding: '6px 12px', color: '#6B7280', fontSize: '14px' }}>第 {page} / {Math.ceil(total / PAGE_SIZE)} 页 (共 {total} 条)</span>
<button disabled={page >= Math.ceil(total / PAGE_SIZE)} onClick={() => setPage(p => p + 1)}
style={{ padding: '6px 16px', borderRadius: '6px', border: '1px solid #D1D5DB', background: '#fff', cursor: page >= Math.ceil(total / PAGE_SIZE) ? 'not-allowed' : 'pointer', opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}>
下一页
</button>
</div>
)}
{/* Create/Edit Modal */}
{modalVisible && (
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#fff', borderRadius: '12px', padding: '24px', width: '560px', maxHeight: '90vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
<h3 style={{ margin: '0 0 20px', fontSize: '18px' }}>{editingTask ? '编辑任务' : '创建自动化任务'}</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
<div><label style={labelStyle}>任务名称 *</label>
<input type="text" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} style={inputStyle} placeholder="如: 每日报表生成" /></div>
<div><label style={labelStyle}>描述</label>
<input type="text" value={formData.description} onChange={e => setFormData({ ...formData, description: e.target.value })} style={inputStyle} placeholder="可选描述" /></div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div><label style={labelStyle}>触发方式</label>
<select value={formData.trigger_type} onChange={e => setFormData({ ...formData, trigger_type: e.target.value })} style={inputStyle}>
<option value="cron">定时触发 (Cron)</option>
<option value="webhook">Webhook回调</option>
<option value="event">事件触发</option>
</select></div>
<div><label style={labelStyle}>操作类型</label>
<select value={formData.action_type} onChange={e => setFormData({ ...formData, action_type: e.target.value })} style={inputStyle}>
<option value="ai_pipeline">AI流水线</option>
<option value="webhook">Webhook回调</option>
<option value="notification">发送通知</option>
<option value="skill_execute">技能执行</option>
</select></div>
</div>
{formData.trigger_type === 'cron' && (
<div><label style={labelStyle}>Cron 表达式 *</label>
<input type="text" value={formData.cron_expression} onChange={e => setFormData({ ...formData, cron_expression: e.target.value })}
style={{ ...inputStyle, fontFamily: 'monospace' }} placeholder="*/5 * * * * (每5分钟)" />
<div style={{ fontSize: '11px', color: '#9CA3AF', marginTop: '4px' }}>
格式: 分 时 日 月 周 | 示例: 0 9 * * * (每天9点)
</div></div>
)}
{formData.trigger_type !== 'cron' && (
<div><label style={labelStyle}>事件条件 (JSON)</label>
<textarea value={formData.event_condition_str} onChange={e => setFormData({ ...formData, event_condition_str: e.target.value })}
rows={3} style={{ ...inputStyle, fontFamily: 'monospace', resize: 'vertical' }}
placeholder={formData.trigger_type === 'webhook' ? '{"method": "POST", "secret": "xxx"}' : '{"event_type": "user.created"}'} /></div>
)}
<div><label style={labelStyle}>操作配置 (JSON)</label>
<textarea value={formData.action_config_str} onChange={e => setFormData({ ...formData, action_config_str: e.target.value })}
rows={3} style={{ ...inputStyle, fontFamily: 'monospace', resize: 'vertical' }}
placeholder='{"pipeline_name": "default"}' /></div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px' }}>
<div><label style={labelStyle}>重试次数</label>
<input type="number" value={formData.max_retries} onChange={e => setFormData({ ...formData, max_retries: parseInt(e.target.value) || 0 })}
min={0} max={10} style={inputStyle} /></div>
<div><label style={labelStyle}>超时(秒)</label>
<input type="number" value={formData.timeout_seconds} onChange={e => setFormData({ ...formData, timeout_seconds: parseInt(e.target.value) || 30 })}
min={10} max={3600} style={inputStyle} /></div>
<div style={{ display: 'flex', alignItems: 'flex-end', paddingBottom: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '14px', cursor: 'pointer' }}>
<input type="checkbox" checked={formData.is_enabled} onChange={e => setFormData({ ...formData, is_enabled: e.target.checked })} /> 启用</label></div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
<button onClick={() => setModalVisible(false)} style={{ padding: '8px 20px', borderRadius: '6px', border: '1px solid #D1D5DB', background: '#fff', cursor: 'pointer', fontSize: '14px', color: '#374151' }}>取消</button>
<button onClick={handleSubmit} disabled={submitting} style={btnStyle('#6C63FF', submitting)}>
{submitting ? '保存中...' : (editingTask ? '保存修改' : '创建任务')}
</button>
</div>
</div>
</div>
)}
{/* History Modal */}
{historyVisible && (
<div className="modal-overlay" style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#fff', borderRadius: '12px', padding: '24px', width: '700px', maxHeight: '85vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ margin: 0, fontSize: '18px' }}>执行历史: {historyTaskName}</h3>
<button onClick={() => setHistoryVisible(false)} style={{ background: 'none', border: 'none', fontSize: '20px', cursor: 'pointer', color: '#6B7280' }}>✕</button>
</div>
{historyLogs.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: '#9CA3AF' }}>暂无执行记录</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#F9FAFB' }}>
<th style={thStyle}>时间</th>
<th style={thStyle}>触发方式</th>
<th style={thStyle}>状态</th>
<th style={thStyle}>耗时</th>
<th style={thStyle}>结果</th>
</tr>
</thead>
<tbody>
{historyLogs.map(log => (
<tr key={log.id} style={{ borderBottom: '1px solid #F3F4F6' }}>
<td style={tdStyle}>{formatTime(log.started_at)}</td>
<td style={tdStyle}>{log.trigger_type}</td>
<td style={tdStyle}>
<span style={badgeStyle(STATUS_CONFIG[log.status] || STATUS_CONFIG.idle)}>
{(STATUS_CONFIG[log.status] || {}).label || log.status}
</span>
</td>
<td style={tdStyle}>{formatDuration(log.duration_ms)}</td>
<td style={{ ...tdStyle, maxWidth: '250px' }}>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{log.result_summary || log.error_message || '-'}
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
{/* History pagination */}
{historyTotal > 20 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '16px' }}>
<button disabled={historyPage <= 1} onClick={() => showHistory({ id: historyTaskId, name: historyTaskName } as AutomationTask, historyPage - 1)}
style={{ padding: '4px 12px', borderRadius: '4px', border: '1px solid #D1D5DB', background: '#fff', cursor: 'pointer' }}>上一页</button>
<span style={{ padding: '4px 8px', color: '#6B7280', fontSize: '13px' }}>{historyPage} / {Math.ceil(historyTotal / 20)}</span>
<button disabled={historyPage >= Math.ceil(historyTotal / 20)} onClick={() => showHistory({ id: historyTaskId, name: historyTaskName } as AutomationTask, historyPage + 1)}
style={{ padding: '4px 12px', borderRadius: '4px', border: '1px solid #D1D5DB', background: '#fff', cursor: 'pointer' }}>下一页</button>
</div>
)}
</div>
</div>
)}
{/* Toast */}
{toast && (
<div style={{ position: 'fixed', top: '20px', right: '20px', zIndex: 2000, padding: '12px 24px', borderRadius: '8px', color: '#fff',
background: toast.type === 'success' ? '#10B981' : '#EF4444', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', fontSize: '14px' }}>
{toast.msg}
</div>
)}
</div>
);
};
// Inline styles
const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600 };
const tdStyle: React.CSSProperties = { padding: '12px 16px', fontSize: '13px' };
const emptyStyle: React.CSSProperties = { padding: '40px', textAlign: 'center', color: '#9CA3AF' };
const badgeStyle = (cfg: { color: string; bg: string }) => ({
padding: '2px 10px', borderRadius: '12px', fontSize: '12px',
color: cfg.color, background: cfg.bg,
} as React.CSSProperties);
const actionBtnStyle = (color: string, disabled = false, outline = false): React.CSSProperties => ({
padding: '5px 12px', borderRadius: '6px', border: `1px solid ${outline ? '#FEE2E2' : '#D1D5DB'}`,
background: outline ? '#FEF2F2' : '#fff', cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: '12px', color, opacity: disabled ? 0.5 : 1,
});
export default Automation;