import { useState } from 'react' import { Loader2, Search, Check, Clock, Zap, Settings2, AlertCircle, CheckCircle2, Calendar } from 'lucide-react' import { api, type ExtDataConfig } from '@/lib/api' import { toast } from '@/components/Toast' export function ExtDataPullPanel({ config, onSaved }: { config: ExtDataConfig onSaved: () => void }) { const pull = config.pull const [url, setUrl] = useState(pull?.url ?? '') const [method, setMethod] = useState(pull?.method ?? 'GET') const [headerStr, setHeaderStr] = useState( pull?.headers ? JSON.stringify(pull.headers, null, 2) : '' ) const [body, setBody] = useState(pull?.body ?? '') const [responsePath, setResponsePath] = useState(pull?.response_path ?? '') const [fieldMapStr, setFieldMapStr] = useState( pull?.field_map ? JSON.stringify(pull.field_map, null, 2) : '' ) const [schedule, setSchedule] = useState(pull?.schedule_minutes ?? 1440) const [enabled, setEnabled] = useState(pull?.enabled ?? false) const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const [running, setRunning] = useState(false) const [runResult, setRunResult] = useState<{ rows: number; date: string } | null>(null) const [testResult, setTestResult] = useState<{ total_rows: number; preview: Record[]; has_symbol: boolean } | null>(null) const [error, setError] = useState('') // 解析 JSON 输入, 失败时设置 error 并返回 null const parseJson = (str: string, label: string): Record | undefined | null => { if (!str.trim()) return undefined try { return JSON.parse(str) } catch { setError(`${label} 不是有效 JSON`); return null } } // 构建保存 payload (复用当前编辑态), enabledOverride 用于开关自动保存 const buildPayload = (enabledOverride?: boolean) => { const headers = parseJson(headerStr, 'Headers') if (headers === null) return null const field_map = parseJson(fieldMapStr, '字段映射') if (field_map === null) return null return { url, method, headers, body: body || undefined, response_path: responsePath, field_map, schedule_minutes: schedule, enabled: enabledOverride ?? enabled, } } const handleSave = (silent = false) => { const payload = buildPayload() if (!payload) return setSaving(true); setError('') api.extDataPullConfig(config.id, payload) .then(() => { onSaved() if (!silent) toast('配置已保存', 'success') }) .catch(e => setError(e.message || '保存失败')) .finally(() => setSaving(false)) } const handleTest = () => { setTesting(true); setError(''); setTestResult(null) const payload = buildPayload() if (!payload) { setTesting(false); return } api.extDataPullConfig(config.id, payload) .then(() => api.extDataPullTest(config.id)) .then(r => { setTestResult(r); onSaved() }) .catch(e => setError(e.message || '测试失败')) .finally(() => setTesting(false)) } const handleRun = () => { setRunning(true); setError(''); setRunResult(null) api.extDataPullRun(config.id) .then(r => { setRunResult({ rows: r.rows, date: r.date }) onSaved() toast(`拉取成功 · ${r.rows} 行`, 'success') }) .catch(e => setError(e.message || '执行失败')) .finally(() => setRunning(false)) } // 开关 toggle: 自动保存全量配置 (切换 enabled), 后端 refresh 后立即首次拉取 const [toggling, setToggling] = useState(false) const handleToggle = (next: boolean) => { if (toggling) return if (next && !url.trim()) { toast('请先填写拉取 URL', 'error') return } const payload = buildPayload(next) if (!payload) return setToggling(true); setError(''); setEnabled(next) api.extDataPullConfig(config.id, payload) .then(() => { onSaved() toast(next ? '定时拉取已启用 · 立即执行首次拉取' : '定时拉取已关闭', 'success') }) .catch(e => { setEnabled(!next) // 回滚 setError(e.message || '切换失败') }) .finally(() => setToggling(false)) } // 格式化时间显示 const fmtTime = (iso: string | null | undefined) => { if (!iso) return null const d = new Date(iso) if (isNaN(d.getTime())) return null const mm = String(d.getMonth() + 1).padStart(2, '0') const dd = String(d.getDate()).padStart(2, '0') const hh = String(d.getHours()).padStart(2, '0') const mi = String(d.getMinutes()).padStart(2, '0') return `${mm}-${dd} ${hh}:${mi}` } return (
{/* ===== 分区 ①: 请求配置 ===== */}
请求配置
setUrl(e.target.value)} placeholder="https://api.example.com/data" className="flex-1 min-w-0 rounded-btn border border-border bg-elevated px-2.5 py-1.5 text-[11px] font-mono text-foreground placeholder:text-muted/50" />
Headers (JSON,可选)