Files
Neo-ZQYY/apps/admin-web/src/pages/TriggerJobs.tsx
Neo 6f8f12314f feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:03:48 +08:00

207 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 定时任务管理页面。
*
* 展示 biz.trigger_jobs 表中所有定时任务,支持手动执行。
*/
import React, { useEffect, useState, useCallback } from 'react';
import { Table, Tag, Button, message, Modal, Typography, Card, Space, Popconfirm, Tooltip } from 'antd';
import {
ReloadOutlined,
ClockCircleOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { fetchTriggerJobs, runTriggerJob, clearAllTasks, type TriggerJob } from '../api/triggerJobs';
const { Title, Text } = Typography;
const TRIGGER_LABEL: Record<string, string> = {
cron: '定时Cron',
interval: '间隔',
event: '事件触发',
};
const STATUS_COLOR: Record<string, string> = {
enabled: 'green',
disabled: 'default',
};
function formatTime(raw: string | null): string {
if (!raw) return '—';
const d = new Date(raw);
return Number.isNaN(d.getTime()) ? raw : d.toLocaleString('zh-CN');
}
function formatTriggerConfig(job: TriggerJob): string {
const cfg = job.trigger_config;
if (!cfg) return '—';
if (job.trigger_condition === 'cron') return cfg.cron_expression as string || '—';
if (job.trigger_condition === 'interval') {
const sec = cfg.interval_seconds as number;
if (sec >= 3600) return `${sec / 3600} 小时`;
if (sec >= 60) return `${sec / 60} 分钟`;
return `${sec}`;
}
if (job.trigger_condition === 'event') return `事件: ${cfg.event_name || '—'}`;
return JSON.stringify(cfg);
}
const TriggerJobs: React.FC = () => {
const [jobs, setJobs] = useState<TriggerJob[]>([]);
const [loading, setLoading] = useState(false);
const [runningId, setRunningId] = useState<number | null>(null);
const [clearing, setClearing] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const data = await fetchTriggerJobs();
setJobs(data);
} catch {
message.error('加载定时任务失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const handleRun = async (jobId: number) => {
setRunningId(jobId);
try {
const result = await runTriggerJob(jobId);
if (result.success) {
message.success(result.message);
} else {
message.error(result.message);
}
await load();
} catch {
message.error('执行失败');
} finally {
setRunningId(null);
}
};
const handleClearAllTasks = async () => {
setClearing(true);
try {
const result = await clearAllTasks();
if (result.success) {
Modal.success({
title: '清空完成',
content: result.message,
});
await load();
} else {
message.error(result.message);
}
} catch {
message.error('清空任务失败');
} finally {
setClearing(false);
}
};
const columns: ColumnsType<TriggerJob> = [
{
title: '任务名称', dataIndex: 'job_name', key: 'job_name', width: 180,
render: (name: string, record) => (
<Tooltip title={record.description || name}>
<Text strong>{record.description || name}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>{name}</Text>
</Tooltip>
),
},
{
title: '触发方式', dataIndex: 'trigger_condition', key: 'trigger_condition', width: 120,
render: (v: string) => <Tag>{TRIGGER_LABEL[v] || v}</Tag>,
},
{
title: '触发配置', key: 'trigger_config', width: 150,
render: (_: unknown, record) => <code style={{ fontSize: 12 }}>{formatTriggerConfig(record)}</code>,
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 80,
render: (v: string) => <Tag color={STATUS_COLOR[v] || 'default'}>{v === 'enabled' ? '启用' : '禁用'}</Tag>,
},
{
title: '上次执行', dataIndex: 'last_run_at', key: 'last_run_at', width: 170,
render: (v: string | null) => formatTime(v),
},
{
title: '下次执行', dataIndex: 'next_run_at', key: 'next_run_at', width: 170,
render: (v: string | null) => formatTime(v),
},
{
title: '最近错误', dataIndex: 'last_error', key: 'last_error', width: 200,
render: (v: string | null) => v
? <Tooltip title={v}><Text type="danger" ellipsis style={{ maxWidth: 180 }}><ExclamationCircleOutlined /> {v}</Text></Tooltip>
: <Text type="success"><CheckCircleOutlined /> </Text>,
},
{
title: '操作', key: 'action', width: 100, fixed: 'right',
render: (_: unknown, record) => (
<Popconfirm
title={`确认手动执行「${record.description || record.job_name}」?`}
onConfirm={() => handleRun(record.id)}
okText="执行"
cancelText="取消"
>
<Button
type="primary"
size="small"
icon={<PlayCircleOutlined />}
loading={runningId === record.id}
disabled={record.status !== 'enabled'}
>
</Button>
</Popconfirm>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}>
<ClockCircleOutlined style={{ marginRight: 8 }} />
</Title>
<Space>
<Popconfirm
title="确认清空所有助教任务?"
description="将删除 coach_tasks 和 coach_task_history 中的全部数据,此操作不可撤销。"
onConfirm={handleClearAllTasks}
okText="确认清空"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button danger loading={clearing}>🧹 </Button>
</Popconfirm>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
</Space>
</div>
<Card size="small">
<Table<TriggerJob>
rowKey="id"
columns={columns}
dataSource={jobs}
loading={loading}
pagination={false}
size="small"
scroll={{ x: 1200 }}
/>
</Card>
</div>
);
};
export default TriggerJobs;