diff --git a/backend/app/api/v1/endpoints/ticket.py b/backend/app/api/v1/endpoints/ticket.py index 90b7f43..57caa29 100644 --- a/backend/app/api/v1/endpoints/ticket.py +++ b/backend/app/api/v1/endpoints/ticket.py @@ -69,6 +69,7 @@ class TicketStatus(str, Enum): PENDING = "pending" # 待审批 REVIEWING = "reviewing" # 审批中(多级审批) APPROVED = "approved" # 已通过 + SCHEDULED = "scheduled" # 定时执行中 REJECTED = "rejected" # 已拒绝 EXECUTING = "executing" # 执行中 SUCCESS = "success" # 执行成功 @@ -80,7 +81,8 @@ class TicketStatus(str, Enum): TICKET_STATE_TRANSITIONS: Dict[str, List[str]] = { TicketStatus.PENDING.value: [TicketStatus.REVIEWING.value, TicketStatus.APPROVED.value, TicketStatus.REJECTED.value], TicketStatus.REVIEWING.value: [TicketStatus.REVIEWING.value, TicketStatus.APPROVED.value, TicketStatus.REJECTED.value], - TicketStatus.APPROVED.value: [TicketStatus.EXECUTING.value, TicketStatus.REJECTED.value], + TicketStatus.APPROVED.value: [TicketStatus.EXECUTING.value, TicketStatus.SCHEDULED.value, TicketStatus.REJECTED.value], + TicketStatus.SCHEDULED.value: [TicketStatus.EXECUTING.value, TicketStatus.APPROVED.value], TicketStatus.EXECUTING.value: [TicketStatus.SUCCESS.value, TicketStatus.FAILED.value], TicketStatus.SUCCESS.value: [TicketStatus.ROLLBACK.value], TicketStatus.FAILED.value: [TicketStatus.ROLLBACK.value, TicketStatus.APPROVED.value], @@ -404,6 +406,7 @@ async def create_ticket( applicant_id=current_user.id, status=initial_status, audit_result=json.dumps(v2_report.to_dict(), ensure_ascii=False) if v2_report and v2_report.findings else None, + scheduled_at=data.scheduled_at, ) db.add(ticket) await db.flush() # flush to get ticket.id @@ -896,7 +899,25 @@ async def execute_ticket( # 5. 权限检查 await check_instance_permission(current_user, instance, "ticket:execute", db) - # 6. 状态转换: approved -> executing + # 6. 定时执行:如果 scheduled_at 在未来,改为 scheduled 状态 + if ticket.scheduled_at: + now = datetime.now(timezone.utc) + scheduled_aware = ticket.scheduled_at.replace(tzinfo=timezone.utc) if ticket.scheduled_at.tzinfo is None else ticket.scheduled_at + if scheduled_aware > now: + if not _validate_status_transition(ticket.status, TicketStatus.SCHEDULED.value): + raise HTTPException(status_code=400, detail=f"非法的状态转换: {ticket.status} -> scheduled") + ticket.status = TicketStatus.SCHEDULED.value + await db.commit() + await db.refresh(ticket) + + # 启动后台定时执行 + from app.services.ticket_scheduler import schedule_ticket_execution + await schedule_ticket_execution(ticket.id, scheduled_aware) + + ticket_data = await _enrich_ticket(ticket, db) + return ticket_data + + # 7. 状态转换: approved -> executing if not _validate_status_transition(ticket.status, TicketStatus.EXECUTING.value): raise HTTPException( status_code=400, @@ -907,7 +928,7 @@ async def execute_ticket( await db.commit() await db.refresh(ticket) - # 7. 使用多数据库执行引擎 + # 8. 使用多数据库执行引擎 from app.services.ticket_executor import TicketExecutor executor = TicketExecutor() @@ -1071,3 +1092,69 @@ async def _send_execution_notification( channels=channels, ticket_data=notification_data, ) + + +# ===== 定时任务管理 ===== + +@router.get("/scheduled/list") +async def list_scheduled_tickets( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role("admin", "dba")), +): + """列出所有定时执行中的工单。""" + result = await db.execute( + select(Ticket) + .where(Ticket.status == TicketStatus.SCHEDULED.value) + .order_by(Ticket.scheduled_at.asc()) + ) + tickets = result.scalars().all() + data = [] + for t in tickets: + inst_result = await db.execute(select(Instance).where(Instance.id == t.instance_id)) + inst = inst_result.scalar_one_or_none() + data.append({ + "id": t.id, + "ticket_no": t.ticket_no, + "title": t.title, + "db_type": t.db_type, + "instance_name": inst.name if inst else "未知", + "instance_db_type": inst.db_type if inst else "", + "target_database": t.target_database, + "sql_content": t.sql_content[:200] + "..." if len(t.sql_content) > 200 else t.sql_content, + "status": t.status, + "scheduled_at": t.scheduled_at.isoformat() if t.scheduled_at else None, + "priority": t.priority, + "create_time": t.create_time.isoformat() if t.create_time else None, + }) + return {"items": data, "total": len(data)} + + +@router.post("/{ticket_id}/cancel-schedule") +async def cancel_scheduled_ticket( + ticket_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_role("admin", "dba")), +): + """取消定时执行,将工单状态改回 approved。""" + ticket = await _get_ticket(db, ticket_id) + if ticket.status != TicketStatus.SCHEDULED.value: + raise HTTPException(status_code=400, detail="只能取消定时执行中的工单") + + if not _validate_status_transition(ticket.status, TicketStatus.APPROVED.value): + raise HTTPException(status_code=400, detail="非法的状态转换") + + # 取消后台任务 + from app.services.ticket_scheduler import cancel_ticket_execution + cancel_ticket_execution(ticket.id) + + ticket.status = TicketStatus.APPROVED.value + await db.commit() + await db.refresh(ticket) + + await AuditLogService.log( + db, user_id=current_user.id, action="CANCEL_SCHEDULED_TICKET", + target_type="ticket", target_id=ticket.id, + detail=f"取消定时执行: {ticket.scheduled_at}", + ) + + return {"message": "已取消定时执行", "ticket_id": ticket_id, "status": ticket.status} diff --git a/backend/app/models/ticket.py b/backend/app/models/ticket.py index a3ef296..e13266d 100644 --- a/backend/app/models/ticket.py +++ b/backend/app/models/ticket.py @@ -24,4 +24,5 @@ class Ticket(Base): create_time = Column(DateTime, server_default=func.now()) review_time = Column(DateTime, nullable=True) execute_time = Column(DateTime, nullable=True) + scheduled_at = Column(DateTime, nullable=True, comment="定时执行时间,NULL=立即执行") updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) diff --git a/backend/app/schemas/ticket.py b/backend/app/schemas/ticket.py index a435f84..66f95e7 100644 --- a/backend/app/schemas/ticket.py +++ b/backend/app/schemas/ticket.py @@ -12,6 +12,7 @@ class TicketCreate(BaseModel): rollback_sql: Optional[str] = None reason: str = Field(..., min_length=5) priority: Literal["high", "medium", "low"] = "medium" + scheduled_at: Optional[datetime] = None class TicketReview(BaseModel): @@ -42,6 +43,7 @@ class TicketResponse(BaseModel): create_time: datetime review_time: Optional[datetime] = None execute_time: Optional[datetime] = None + scheduled_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/services/ticket_scheduler.py b/backend/app/services/ticket_scheduler.py new file mode 100644 index 0000000..5be07a7 --- /dev/null +++ b/backend/app/services/ticket_scheduler.py @@ -0,0 +1,147 @@ +"""工单定时执行调度器 — 基于 asyncio 的轻量级定时任务。""" +import asyncio +import logging +from datetime import datetime, timezone +from typing import Dict + +logger = logging.getLogger(__name__) + +# 内存中保存活跃的定时任务句柄 +_active_tasks: Dict[int, asyncio.Task] = {} + + +async def schedule_ticket_execution(ticket_id: int, scheduled_at: datetime) -> None: + """注册一个定时执行任务。 + + 使用 asyncio.create_task 创建后台等待任务,到达 scheduled_at 后自动执行工单。 + + Args: + ticket_id: 工单 ID + scheduled_at: 定时执行时间 (UTC) + """ + now = datetime.now(timezone.utc) + delay = (scheduled_at - now).total_seconds() + + if delay <= 0: + # 已到时间,立即执行 + logger.info("工单 %d 定时时间已到,立即执行", ticket_id) + task = asyncio.create_task(_execute_scheduled_ticket(ticket_id)) + else: + logger.info("工单 %d 定时执行: %s (%.1f 秒后)", ticket_id, scheduled_at, delay) + task = asyncio.create_task(_wait_and_execute(ticket_id, delay)) + + _active_tasks[ticket_id] = task + + +async def _wait_and_execute(ticket_id: int, delay: float) -> None: + """等待 delay 秒后执行工单。""" + try: + await asyncio.sleep(delay) + await _execute_scheduled_ticket(ticket_id) + except asyncio.CancelledError: + logger.info("工单 %d 定时执行已取消", ticket_id) + except Exception: + logger.exception("工单 %d 定时执行异常", ticket_id) + finally: + _active_tasks.pop(ticket_id, None) + + +async def _execute_scheduled_ticket(ticket_id: int) -> None: + """执行定时工单 — 创建独立数据库会话执行。""" + from app.core.database import async_session + from app.models.ticket import Ticket as TicketModel + from app.models.instance import Instance + from app.services.ticket_executor import TicketExecutor + from app.services.audit_log import AuditLogService + from app.core.security import decrypt_password + from sqlalchemy import select + import json + + logger.info("开始执行定时工单 %d", ticket_id) + + async with async_session() as db: + try: + # 查询工单 + result = await db.execute(select(TicketModel).where(TicketModel.id == ticket_id)) + ticket = result.scalar_one_or_none() + if not ticket: + logger.error("定时工单 %d 不存在", ticket_id) + return + + # 状态检查 + if ticket.status != "scheduled": + logger.warning("工单 %d 状态已变更 (%s),跳过执行", ticket_id, ticket.status) + return + + # 更新状态为 executing + ticket.status = "executing" + await db.commit() + await db.refresh(ticket) + + # 查询实例 + inst_result = await db.execute(select(Instance).where(Instance.id == ticket.instance_id)) + instance = inst_result.scalar_one_or_none() + if not instance: + ticket.status = "failed" + ticket.execute_result = json.dumps({"status": "failed", "errors": ["数据源实例不存在"]}, ensure_ascii=False) + ticket.execute_time = datetime.now(timezone.utc) + await db.commit() + return + + # 解密密码 + instance.password = decrypt_password(instance.password) + + # 执行 + executor = TicketExecutor() + report = await executor.execute(ticket, instance) + + # 更新结果 + ticket.status = "success" if report.status == "success" else "failed" + ticket.execute_result = report.to_json() + ticket.execute_time = datetime.now(timezone.utc) + await db.commit() + + logger.info("定时工单 %d 执行完成: %s", ticket_id, report.status) + + except Exception as e: + logger.exception("定时工单 %d 执行异常", ticket_id) + try: + result = await db.execute(select(TicketModel).where(TicketModel.id == ticket_id)) + ticket = result.scalar_one_or_none() + if ticket: + ticket.status = "failed" + ticket.execute_result = json.dumps({"status": "failed", "errors": [f"{type(e).__name__}: {e}"]}, ensure_ascii=False) + ticket.execute_time = datetime.now(timezone.utc) + await db.commit() + except Exception: + logger.exception("更新定时工单 %d 失败状态也失败", ticket_id) + finally: + _active_tasks.pop(ticket_id, None) + + +def cancel_ticket_execution(ticket_id: int) -> bool: + """取消定时执行任务。 + + Args: + ticket_id: 工单 ID + + Returns: + 是否成功取消 + """ + task = _active_tasks.get(ticket_id) + if task and not task.done(): + task.cancel() + _active_tasks.pop(ticket_id, None) + logger.info("已取消工单 %d 的定时执行", ticket_id) + return True + return False + + +def get_active_scheduled_count() -> int: + """返回当前活跃的定时任务数量。""" + return len(_active_tasks) + + +def get_active_scheduled_ids() -> list[int]: + """返回当前活跃的定时工单 ID 列表。""" + return list(_active_tasks.keys()) diff --git a/frontend/src/pages/AutoTaskPage.tsx b/frontend/src/pages/AutoTaskPage.tsx index 61e9ad7..5e1ef7e 100644 --- a/frontend/src/pages/AutoTaskPage.tsx +++ b/frontend/src/pages/AutoTaskPage.tsx @@ -1,293 +1,188 @@ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { - Clock, Plus, Play, Pause, Trash2, AlertTriangle, - Calendar, Zap, Database, ChevronDown, ChevronUp, X, Save, Eye + Clock, XCircle, CheckCircle, AlertTriangle, + Database, Zap, Loader2, Calendar, Ban, } from 'lucide-react'; -import { Card, CardContent } from '@/components/ui/card'; -import { Switch } from '@/components/ui/switch'; +import api from '@/api/request'; -type TaskFrequency = 'once' | 'hourly' | 'daily' | 'weekly' | 'monthly'; -type TaskStatus = 'running' | 'paused' | 'completed' | 'failed'; +interface ScheduledTicket { + id: number; + ticket_no: string; + title: string; + db_type: string; + instance_name: string; + instance_db_type: string; + target_database: string; + sql_content: string; + status: string; + scheduled_at: string | null; + priority: string; + create_time: string | null; +} -type AutoTask = { - id: string; - name: string; - description: string; - enabled: boolean; - frequency: TaskFrequency; - cron: string; - nextRun: string; - lastRun: string; - lastStatus: 'success' | 'failed' | 'none'; - dbType: string; - instance: string; - sql: string; - status: TaskStatus; - notifyOnFail: boolean; - createdBy: string; +const DB_TYPE_ICON: Record = { + mysql: '🐬', postgres: '🐘', redis: '🔴', mongo: '🍃', }; - -const frequencyLabels: Record = { - once: '一次性', - hourly: '每小时', - daily: '每天', - weekly: '每周', - monthly: '每月', +const DB_TYPE_LABEL: Record = { + mysql: 'MySQL', postgres: 'PostgreSQL', redis: 'Redis', mongo: 'MongoDB', }; -const initialTasks: AutoTask[] = [ - { id: 'task-1', name: '每日数据归档', description: '将 30 天前的日志数据归档到历史表', enabled: true, frequency: 'daily', cron: '0 2 * * *', nextRun: '2024-01-16 02:00', lastRun: '2024-01-15 02:00', lastStatus: 'success', dbType: 'mysql', instance: '生产主库', sql: 'INSERT INTO logs_archive SELECT * FROM system_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);\nDELETE FROM system_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);', status: 'running', notifyOnFail: true, createdBy: 'admin' }, - { id: 'task-2', name: '每周统计报表', description: '生成周活跃用户统计报表', enabled: true, frequency: 'weekly', cron: '0 6 * * 1', nextRun: '2024-01-22 06:00', lastRun: '2024-01-08 06:00', lastStatus: 'success', dbType: 'mysql', instance: '生产主库', sql: 'INSERT INTO report_weekly_active (week_start, week_end, active_users, new_users)\nSELECT \n DATE_SUB(NOW(), INTERVAL 7 DAY) as week_start,\n NOW() as week_end,\n COUNT(DISTINCT user_id) as active_users,\n COUNT(DISTINCT CASE WHEN created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) THEN user_id END) as new_users\nFROM user_activity\nWHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY);', status: 'running', notifyOnFail: true, createdBy: 'admin' }, - { id: 'task-3', name: '索引健康检查', description: '检查慢查询并建议优化索引', enabled: true, frequency: 'daily', cron: '0 3 * * *', nextRun: '2024-01-16 03:00', lastRun: '2024-01-15 03:00', lastStatus: 'failed', dbType: 'mysql', instance: '生产主库', sql: 'SELECT \n table_schema,\n table_name,\n index_name,\n cardinality,\n table_rows,\n ROUND(cardinality / table_rows * 100, 2) as selectivity\nFROM information_schema.statistics\nWHERE table_schema NOT IN ("information_schema", "mysql", "performance_schema")\n AND index_name != "PRIMARY"\n AND cardinality / table_rows < 0.1\nORDER BY table_rows DESC\nLIMIT 100;', status: 'paused', notifyOnFail: true, createdBy: 'zhangsan' }, - { id: 'task-4', name: 'Redis 键过期清理', description: '清理 Redis 中已过期的会话键', enabled: false, frequency: 'hourly', cron: '0 * * * *', nextRun: '-', lastRun: '2024-01-14 12:00', lastStatus: 'success', dbType: 'redis', instance: 'Redis集群-01', sql: 'EVAL "local keys = redis.call(\'keys\', \'session:*\')\nfor i=1,#keys,5000 do\n redis.call(\'del\', unpack(keys, i, math.min(i+4999, #keys)))\nend\nreturn #keys" 0', status: 'paused', notifyOnFail: false, createdBy: 'lisi' }, - { id: 'task-5', name: '月度数据备份', description: '每月全量备份核心数据表', enabled: true, frequency: 'monthly', cron: '0 1 1 * *', nextRun: '2024-02-01 01:00', lastRun: '2024-01-01 01:00', lastStatus: 'success', dbType: 'mysql', instance: '生产主库', sql: '-- 调用备份脚本\nBACKUP DATABASE user_db, order_db TO S3://backups/monthly/;', status: 'running', notifyOnFail: true, createdBy: 'admin' }, -]; - export default function AutoTaskPage() { - const [tasks, setTasks] = useState(initialTasks); - const [showCreate, setShowCreate] = useState(false); - const [expandedId, setExpandedId] = useState(null); - const [previewTask, setPreviewTask] = useState(null); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [cancelling, setCancelling] = useState(null); - // 表单状态 - const [formName, setFormName] = useState(''); - const [formDesc, setFormDesc] = useState(''); - const [formFreq, setFormFreq] = useState('daily'); - const [formCron, setFormCron] = useState('0 2 * * *'); - const [formDbType, setFormDbType] = useState('mysql'); - const [formInstance, setFormInstance] = useState('生产主库'); - const [formSql, setFormSql] = useState(''); - const [formNotify, setFormNotify] = useState(true); + const fetchTasks = useCallback(async () => { + setLoading(true); + setError(''); + try { + const res = await api.get('/tickets/scheduled/list') as { items: ScheduledTicket[]; total: number }; + setTasks(res.items || []); + } catch { + setError('加载定时任务失败'); + } finally { + setLoading(false); + } + }, []); - const toggleTask = (id: string) => { - setTasks(prev => prev.map(t => t.id === id ? { ...t, enabled: !t.enabled, status: !t.enabled ? 'running' : 'paused' } : t)); + useEffect(() => { fetchTasks(); }, [fetchTasks]); + + // 自动刷新(每 30 秒) + useEffect(() => { + const timer = setInterval(fetchTasks, 30000); + return () => clearInterval(timer); + }, [fetchTasks]); + + const handleCancel = useCallback(async (id: number) => { + setCancelling(id); + try { + await api.post(`/tickets/${id}/cancel-schedule`); + setTasks(prev => prev.filter(t => t.id !== id)); + } catch { + setError('取消定时任务失败'); + } finally { + setCancelling(null); + } + }, []); + + const formatTime = (iso: string | null) => { + if (!iso) return '-'; + return new Date(iso).toLocaleString('zh-CN', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + }); }; - const deleteTask = (id: string) => { - setTasks(prev => prev.filter(t => t.id !== id)); - }; - - const openCreate = () => { - setFormName(''); - setFormDesc(''); - setFormFreq('daily'); - setFormCron('0 2 * * *'); - setFormDbType('mysql'); - setFormInstance('生产主库'); - setFormSql(''); - setFormNotify(true); - setShowCreate(true); - }; - - const saveTask = () => { - if (!formName.trim() || !formSql.trim()) return; - const newTask: AutoTask = { - id: `task-${Date.now()}`, - name: formName, - description: formDesc, - enabled: true, - frequency: formFreq, - cron: formCron, - nextRun: '即将计算...', - lastRun: '-', - lastStatus: 'none', - dbType: formDbType, - instance: formInstance, - sql: formSql, - status: 'running', - notifyOnFail: formNotify, - createdBy: 'admin', - }; - setTasks(prev => [...prev, newTask]); - setShowCreate(false); - }; - - const statusColors: Record = { - running: 'bg-green-500/15 text-green-400', - paused: 'bg-yellow-500/15 text-yellow-400', - completed: 'bg-blue-500/15 text-blue-400', - failed: 'bg-red-500/15 text-red-400', + const getCountdown = (iso: string | null) => { + if (!iso) return ''; + const diff = new Date(iso).getTime() - Date.now(); + if (diff <= 0) return '即将执行'; + const h = Math.floor(diff / 3600000); + const m = Math.floor((diff % 3600000) / 60000); + const s = Math.floor((diff % 60000) / 1000); + if (h > 0) return `${h}h ${m}m 后执行`; + if (m > 0) return `${m}m ${s}s 后执行`; + return `${s}s 后执行`; }; return (
+ {/* Header */}
-
-

自动任务

-

配置定时执行的 SQL 任务,支持 Cron 表达式

+
+ +

定时执行任务

+ + 审批通过并设置定时执行的工单,到时间后自动执行 SQL +
-
-
- {[ - { label: '任务总数', value: tasks.length, icon: Clock, color: 'text-blue-400', bg: 'bg-blue-400/10' }, - { label: '运行中', value: tasks.filter(t => t.status === 'running').length, icon: Play, color: 'text-green-400', bg: 'bg-green-400/10' }, - { label: '已暂停', value: tasks.filter(t => t.status === 'paused').length, icon: Pause, color: 'text-yellow-400', bg: 'bg-yellow-400/10' }, - { label: '上次失败', value: tasks.filter(t => t.lastStatus === 'failed').length, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-400/10' }, - ].map((s, i) => ( - - -
-
{s.value}
{s.label}
-
-
- ))} -
+ {/* Error */} + {error && ( +
+ {error} +
+ )} + {/* Empty */} + {!loading && tasks.length === 0 && ( +
+ +

暂无定时执行中的工单

+

+ 在「新建数据变更工单」中勾选「定时执行」可创建定时任务 +

+
+ )} + + {/* Task Cards */}
{tasks.map(task => { - const isExpanded = expandedId === task.id; + const countdown = getCountdown(task.scheduled_at); return ( - - -
-
-
-

{task.name}

- - {task.status === 'running' ? '运行中' : task.status === 'paused' ? '已暂停' : task.status === 'failed' ? '失败' : '完成'} - - - {frequencyLabels[task.frequency]} - - - {task.cron} - -
-

{task.description}

-
- {task.instance} ({task.dbType.toUpperCase()}) - 下次执行: {task.nextRun} - - 上次执行: - {task.lastRun} - - 创建者: {task.createdBy} -
- {isExpanded && ( -
-
SQL 语句
-
{task.sql}
-
- )} +
+ {/* Row 1: Title + Status + Countdown */} +
+ {DB_TYPE_ICON[task.db_type] || '💾'} +
+
+ {task.title} + + 定时执行中 +
-
- - - - +
+ {task.ticket_no} + · + + {task.instance_name} + · + {DB_TYPE_LABEL[task.db_type] || task.db_type} + · + {task.target_database}
- - + {/* Countdown */} +
+
{countdown}
+
+ + {formatTime(task.scheduled_at)} +
+
+
+ + {/* Row 2: SQL Preview */} +
+
+                  {task.sql_content}
+                
+
+ + {/* Row 3: Actions */} +
+ + 创建于 {formatTime(task.create_time)} + + +
+
); })}
- - {/* 预览弹窗 */} - {previewTask && ( -
setPreviewTask(null)}> -
e.stopPropagation()}> -
-

{previewTask.name}

- -
-
-
-
执行频率: {frequencyLabels[previewTask.frequency]} ({previewTask.cron})
-
数据库: {previewTask.instance} ({previewTask.dbType.toUpperCase()})
-
下次执行: {previewTask.nextRun}
-
上次状态: {previewTask.lastStatus === 'success' ? '成功' : previewTask.lastStatus === 'failed' ? '失败' : '未执行'}
-
-
-
SQL 语句
-
{previewTask.sql}
-
-
-
- -
-
-
- )} - - {/* 创建弹窗 */} - {showCreate && ( -
setShowCreate(false)}> -
e.stopPropagation()}> -
-

新建自动任务

- -
-
-
- - setFormName(e.target.value)} className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500" placeholder="例如:每日数据归档" /> -
-
- -