添加定时任务管理功能
This commit is contained in:
346
src/pages/System/Scheduled/components/TaskForm.tsx
Normal file
346
src/pages/System/Scheduled/components/TaskForm.tsx
Normal file
@ -0,0 +1,346 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
message,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Tooltip,
|
||||
Button,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { addTask, editTask, validateCron } from '../service';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface TaskFormProps {
|
||||
visible: boolean;
|
||||
editingTask?: any;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const TaskForm: React.FC<TaskFormProps> = ({
|
||||
visible,
|
||||
editingTask,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cronValidating, setCronValidating] = useState(false);
|
||||
|
||||
const isEdit = !!editingTask;
|
||||
|
||||
// 常用Cron表达式示例
|
||||
const cronExamples = [
|
||||
{ label: '每分钟执行', value: '0 * * * * ?' },
|
||||
{ label: '每5分钟执行', value: '0 */5 * * * ?' },
|
||||
{ label: '每30分钟执行', value: '0 */30 * * * ?' },
|
||||
{ label: '每小时执行', value: '0 0 * * * ?' },
|
||||
{ label: '每天凌晨2点执行', value: '0 0 2 * * ?' },
|
||||
{ label: '每周一凌晨执行', value: '0 0 0 ? * MON' },
|
||||
{ label: '每月1号凌晨执行', value: '0 0 0 1 * ?' },
|
||||
];
|
||||
|
||||
// 请求方法选项
|
||||
const requestMethods = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (isEdit) {
|
||||
// 编辑模式,填充表单数据
|
||||
form.setFieldsValue({
|
||||
...editingTask,
|
||||
requestParams: editingTask.requestParams ? JSON.stringify(JSON.parse(editingTask.requestParams), null, 2) : '',
|
||||
requestHeaders: editingTask.requestHeaders ? JSON.stringify(JSON.parse(editingTask.requestHeaders), null, 2) : '',
|
||||
});
|
||||
} else {
|
||||
// 新增模式,设置默认值
|
||||
form.setFieldsValue({
|
||||
status: 1,
|
||||
requestMethod: 'POST',
|
||||
requestParams: '{}',
|
||||
requestHeaders: '{"Content-Type": "application/json"}',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [visible, editingTask, isEdit, form]);
|
||||
|
||||
// 验证Cron表达式
|
||||
const handleCronValidate = async () => {
|
||||
const cronExpression = form.getFieldValue('cronExpression');
|
||||
if (!cronExpression) {
|
||||
message.warning('请先输入Cron表达式');
|
||||
return;
|
||||
}
|
||||
|
||||
setCronValidating(true);
|
||||
try {
|
||||
const { success, data } = await validateCron(cronExpression);
|
||||
if (success) {
|
||||
message.success('Cron表达式验证通过');
|
||||
if (data?.nextExecuteTimes) {
|
||||
Modal.info({
|
||||
title: '接下来5次执行时间',
|
||||
content: (
|
||||
<div>
|
||||
{data.nextExecuteTimes.map((time: string, index: number) => (
|
||||
<div key={index}>{time}</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message.error('Cron表达式格式错误');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('验证失败');
|
||||
} finally {
|
||||
setCronValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 使用示例Cron表达式
|
||||
const useCronExample = (cronExpression: string) => {
|
||||
form.setFieldsValue({ cronExpression });
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证JSON格式
|
||||
try {
|
||||
if (values.requestParams) {
|
||||
JSON.parse(values.requestParams);
|
||||
}
|
||||
if (values.requestHeaders) {
|
||||
JSON.parse(values.requestHeaders);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('请求参数或请求头必须是有效的JSON格式');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const { success } = isEdit
|
||||
? await editTask({ ...values, taskId: editingTask.taskId })
|
||||
: await addTask(values);
|
||||
|
||||
if (success) {
|
||||
message.success(isEdit ? '修改成功' : '新增成功');
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? '编辑定时任务' : '新增定时任务'}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务名称"
|
||||
name="taskName"
|
||||
rules={[
|
||||
{ required: true, message: '请输入任务名称' },
|
||||
{ max: 100, message: '任务名称不能超过100个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入任务名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="任务状态"
|
||||
name="status"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
checkedChildren="启用"
|
||||
unCheckedChildren="禁用"
|
||||
onChange={(checked) => form.setFieldsValue({ status: checked ? 1 : 0 })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="任务描述"
|
||||
name="taskDesc"
|
||||
rules={[{ max: 500, message: '任务描述不能超过500个字符' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="请输入任务描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Card title="Cron配置" size="small" style={{ marginBottom: 16 }}>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Cron表达式
|
||||
<Tooltip title="格式:秒 分 时 日 月 周,例如:0 0 2 * * ? 表示每天凌晨2点执行">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="cronExpression"
|
||||
rules={[
|
||||
{ required: true, message: '请输入Cron表达式' },
|
||||
{
|
||||
pattern: /^[0-9\*\-\/\,\?\s]+$/,
|
||||
message: 'Cron表达式格式不正确'
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入Cron表达式,如:0 0 2 * * ?"
|
||||
addonAfter={
|
||||
<Button
|
||||
size="small"
|
||||
loading={cronValidating}
|
||||
onClick={handleCronValidate}
|
||||
>
|
||||
验证
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>
|
||||
常用表达式:
|
||||
</div>
|
||||
<Space wrap>
|
||||
{cronExamples.map((example, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => useCronExample(example.value)}
|
||||
style={{ padding: '0 4px', height: 'auto' }}
|
||||
>
|
||||
{example.label}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="接口配置" size="small" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="目标服务"
|
||||
name="targetService"
|
||||
rules={[
|
||||
{ required: true, message: '请输入目标服务名称' },
|
||||
{ max: 100, message: '服务名称不能超过100个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:user-service" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="请求方法"
|
||||
name="requestMethod"
|
||||
rules={[{ required: true, message: '请选择请求方法' }]}
|
||||
>
|
||||
<Select placeholder="请选择请求方法">
|
||||
{requestMethods.map(method => (
|
||||
<Option key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
label="目标接口"
|
||||
name="targetApi"
|
||||
rules={[
|
||||
{ required: true, message: '请输入目标接口路径' },
|
||||
{ max: 200, message: '接口路径不能超过200个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:/api/user/sync" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
请求参数
|
||||
<Tooltip title="JSON格式的请求参数">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="requestParams"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder='JSON格式,如:{"type": "sync"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
请求头
|
||||
<Tooltip title="JSON格式的请求头信息">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="requestHeaders"
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder='JSON格式,如:{"Content-Type": "application/json"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskForm;
|
216
src/pages/System/Scheduled/components/TaskLogModal.tsx
Normal file
216
src/pages/System/Scheduled/components/TaskLogModal.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Modal, Tag, Tooltip } from 'antd';
|
||||
import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
|
||||
import { getTaskLogs } from '../service';
|
||||
|
||||
interface TaskLogModalProps {
|
||||
visible: boolean;
|
||||
taskId: string;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const TaskLogModal: React.FC<TaskLogModalProps> = ({
|
||||
visible,
|
||||
taskId,
|
||||
onCancel,
|
||||
}) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
// 执行状态映射
|
||||
const statusMap = {
|
||||
SUCCESS: { text: '成功', color: 'success' },
|
||||
FAILED: { text: '失败', color: 'error' },
|
||||
RUNNING: { text: '执行中', color: 'processing' },
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns: ProColumns<any>[] = [
|
||||
{
|
||||
title: '序号',
|
||||
valueType: 'index',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
width: 160,
|
||||
valueType: 'dateTime',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
width: 160,
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '执行耗时(ms)',
|
||||
dataIndex: 'duration',
|
||||
width: 120,
|
||||
hideInSearch: true,
|
||||
render: (text) => {
|
||||
if (!text) return '-';
|
||||
const duration = Number(text);
|
||||
let color = 'default';
|
||||
if (duration < 1000) color = 'success';
|
||||
else if (duration < 5000) color = 'warning';
|
||||
else color = 'error';
|
||||
|
||||
return <Tag color={color}>{duration}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '执行状态',
|
||||
dataIndex: 'executeStatus',
|
||||
width: 100,
|
||||
render: (text) => {
|
||||
const status = statusMap[text as keyof typeof statusMap];
|
||||
return (
|
||||
<Tag color={status?.color}>
|
||||
{status?.text || text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
valueEnum: {
|
||||
SUCCESS: { text: '成功', status: 'Success' },
|
||||
FAILED: { text: '失败', status: 'Error' },
|
||||
RUNNING: { text: '执行中', status: 'Processing' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '执行结果',
|
||||
dataIndex: 'executeResult',
|
||||
width: 200,
|
||||
hideInSearch: true,
|
||||
ellipsis: true,
|
||||
render: (text) => {
|
||||
if (!text) return '-';
|
||||
const maxLength = 50;
|
||||
const displayText = text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
||||
|
||||
return (
|
||||
<Tooltip title={<pre style={{ whiteSpace: 'pre-wrap', maxHeight: 300, overflow: 'auto' }}>{text}</pre>}>
|
||||
<span style={{ cursor: 'pointer' }}>{displayText}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
dataIndex: 'errorMessage',
|
||||
width: 200,
|
||||
hideInSearch: true,
|
||||
ellipsis: true,
|
||||
render: (text) => {
|
||||
if (!text) return '-';
|
||||
const maxLength = 50;
|
||||
const displayText = text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
||||
|
||||
return (
|
||||
<Tooltip title={<pre style={{ whiteSpace: 'pre-wrap', maxHeight: 300, overflow: 'auto', color: 'red' }}>{text}</pre>}>
|
||||
<span style={{ cursor: 'pointer', color: '#ff4d4f' }}>{displayText}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '请求URL',
|
||||
dataIndex: 'requestUrl',
|
||||
width: 200,
|
||||
hideInSearch: true,
|
||||
ellipsis: true,
|
||||
render: (text) => {
|
||||
if (!text) return '-';
|
||||
return (
|
||||
<Tooltip title={text}>
|
||||
<code style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{text}
|
||||
</code>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '响应状态码',
|
||||
dataIndex: 'responseStatus',
|
||||
width: 120,
|
||||
hideInSearch: true,
|
||||
render: (text) => {
|
||||
if (!text) return '-';
|
||||
const status = Number(text);
|
||||
let color = 'default';
|
||||
if (status >= 200 && status < 300) color = 'success';
|
||||
else if (status >= 400 && status < 500) color = 'warning';
|
||||
else if (status >= 500) color = 'error';
|
||||
|
||||
return <Tag color={color}>{text}</Tag>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 重新加载数据
|
||||
useEffect(() => {
|
||||
if (visible && taskId) {
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
}, [visible, taskId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="任务执行日志"
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
width={1200}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
rowKey="logId"
|
||||
search={{
|
||||
labelWidth: 80,
|
||||
defaultCollapsed: false,
|
||||
collapseRender: false,
|
||||
}}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...searchParams } = params;
|
||||
const response = await getTaskLogs({
|
||||
taskId,
|
||||
pageNo: current,
|
||||
pageSize,
|
||||
...searchParams,
|
||||
});
|
||||
|
||||
return {
|
||||
data: response?.data?.records || [],
|
||||
success: response?.success,
|
||||
total: response?.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
columns={columns}
|
||||
scroll={{ x: 1000 }}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
dateFormatter="string"
|
||||
headerTitle="执行日志列表"
|
||||
options={{
|
||||
density: false,
|
||||
fullScreen: false,
|
||||
reload: true,
|
||||
setting: false,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskLogModal;
|
197
src/pages/System/Scheduled/index.less
Normal file
197
src/pages/System/Scheduled/index.less
Normal file
@ -0,0 +1,197 @@
|
||||
.scheduled-task-page {
|
||||
.ant-pro-table {
|
||||
.ant-pro-table-list-toolbar {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态开关样式调整
|
||||
.ant-switch-small {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
// Cron表达式代码样式
|
||||
code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 表格操作列按钮间距
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
.ant-btn + .ant-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格内容样式
|
||||
.ant-table-tbody {
|
||||
.ant-tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-btn-link {
|
||||
padding: 0 4px;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框内表格样式
|
||||
.ant-modal-body {
|
||||
.ant-pro-table {
|
||||
.ant-pro-table-search {
|
||||
padding: 16px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 任务表单卡片样式
|
||||
.ant-card {
|
||||
&.ant-card-small {
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
min-height: 38px;
|
||||
|
||||
.ant-card-head-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单布局调整
|
||||
.ant-form-vertical {
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding-bottom: 4px;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
|
||||
.anticon {
|
||||
color: #1890ff;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cron示例按钮样式
|
||||
.cron-examples {
|
||||
.ant-btn {
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
padding: 2px 6px;
|
||||
height: auto;
|
||||
line-height: 1.4;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 日志弹窗内容样式
|
||||
.task-log-modal {
|
||||
.ant-table-cell {
|
||||
// 日志内容显示样式
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态标签样式
|
||||
.ant-tag {
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
|
||||
&.ant-tag-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.ant-tag-error {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.ant-tag-processing {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式样式
|
||||
@media (max-width: 768px) {
|
||||
.ant-pro-table {
|
||||
.ant-table-thead {
|
||||
th {
|
||||
font-size: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
td {
|
||||
font-size: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
margin: 0;
|
||||
max-width: 100vw;
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态样式
|
||||
.loading-overlay {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具提示样式增强
|
||||
.ant-tooltip {
|
||||
.ant-tooltip-inner {
|
||||
max-width: 400px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
405
src/pages/System/Scheduled/index.tsx
Normal file
405
src/pages/System/Scheduled/index.tsx
Normal file
@ -0,0 +1,405 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
message,
|
||||
Modal,
|
||||
Button,
|
||||
Switch,
|
||||
Tag,
|
||||
Space,
|
||||
Popconfirm,
|
||||
Tooltip,
|
||||
Input,
|
||||
InputNumber
|
||||
} from 'antd';
|
||||
import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
|
||||
import {
|
||||
PlusOutlined,
|
||||
PlayCircleOutlined,
|
||||
FileTextOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
getTaskList,
|
||||
deleteTask,
|
||||
enableTask,
|
||||
disableTask,
|
||||
executeTask,
|
||||
cleanLogs
|
||||
} from './service';
|
||||
import TaskForm from './components/TaskForm';
|
||||
import TaskLogModal from './components/TaskLogModal';
|
||||
import './index.less';
|
||||
|
||||
const ScheduledTask: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [taskFormVisible, setTaskFormVisible] = useState(false);
|
||||
const [logModalVisible, setLogModalVisible] = useState(false);
|
||||
const [cleanLogsVisible, setCleanLogsVisible] = useState(false);
|
||||
const [editingTask, setEditingTask] = useState<any>(null);
|
||||
const [selectedTaskId, setSelectedTaskId] = useState<string>('');
|
||||
const [cleanDays, setCleanDays] = useState<number>(30);
|
||||
|
||||
// 任务状态映射
|
||||
const statusMap = {
|
||||
1: { text: '启用', color: 'success' },
|
||||
0: { text: '禁用', color: 'default' },
|
||||
};
|
||||
|
||||
// 请求方法映射
|
||||
const methodMap = {
|
||||
'GET': { text: 'GET', color: 'blue' },
|
||||
'POST': { text: 'POST', color: 'green' },
|
||||
'PUT': { text: 'PUT', color: 'orange' },
|
||||
'DELETE': { text: 'DELETE', color: 'red' },
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns: ProColumns<any>[] = [
|
||||
{
|
||||
title: '序号',
|
||||
valueType: 'index',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '任务名称',
|
||||
dataIndex: 'taskName',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '任务描述',
|
||||
dataIndex: 'taskDesc',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: 'Cron表达式',
|
||||
dataIndex: 'cronExpression',
|
||||
width: 150,
|
||||
hideInSearch: true,
|
||||
render: (text) => (
|
||||
<Tooltip title={text}>
|
||||
<code style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{text}
|
||||
</code>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标服务',
|
||||
dataIndex: 'targetService',
|
||||
width: 120,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '目标接口',
|
||||
dataIndex: 'targetApi',
|
||||
width: 150,
|
||||
hideInSearch: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
dataIndex: 'requestMethod',
|
||||
width: 100,
|
||||
hideInSearch: true,
|
||||
render: (text) => {
|
||||
const method = methodMap[text as keyof typeof methodMap];
|
||||
return (
|
||||
<Tag color={method?.color || 'default'}>
|
||||
{method?.text || text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
hideInSearch: true,
|
||||
render: (text) => {
|
||||
const status = statusMap[text as keyof typeof statusMap];
|
||||
return (
|
||||
<Tag color={status?.color}>
|
||||
{status?.text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上次执行时间',
|
||||
dataIndex: 'lastExecuteTime',
|
||||
width: 160,
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '下次执行时间',
|
||||
dataIndex: 'nextExecuteTime',
|
||||
width: 160,
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 160,
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 250,
|
||||
fixed: 'right',
|
||||
render: (_, record) => [
|
||||
<Button
|
||||
key="edit"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Switch
|
||||
key="status"
|
||||
size="small"
|
||||
checked={record.status === 1}
|
||||
loading={record.statusLoading}
|
||||
onChange={(checked) => handleStatusChange(record, checked)}
|
||||
/>,
|
||||
<Button
|
||||
key="execute"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleExecute(record)}
|
||||
disabled={record.status === 0}
|
||||
>
|
||||
执行
|
||||
</Button>,
|
||||
<Button
|
||||
key="logs"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => handleViewLogs(record)}
|
||||
>
|
||||
日志
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定要删除这个任务吗?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 新增任务
|
||||
const handleAdd = () => {
|
||||
setEditingTask(null);
|
||||
setTaskFormVisible(true);
|
||||
};
|
||||
|
||||
// 编辑任务
|
||||
const handleEdit = (record: any) => {
|
||||
setEditingTask(record);
|
||||
setTaskFormVisible(true);
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
const { success } = await deleteTask(record.taskId);
|
||||
if (success) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 状态切换
|
||||
const handleStatusChange = async (record: any, checked: boolean) => {
|
||||
// 设置加载状态
|
||||
record.statusLoading = true;
|
||||
actionRef.current?.reload();
|
||||
|
||||
try {
|
||||
const { success } = checked
|
||||
? await enableTask(record.id)
|
||||
: await disableTask(record.id);
|
||||
|
||||
if (success) {
|
||||
message.success(checked ? '启用成功' : '禁用成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
record.statusLoading = false;
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(checked ? '启用失败' : '禁用失败');
|
||||
record.statusLoading = false;
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// 执行任务
|
||||
const handleExecute = async (record: any) => {
|
||||
try {
|
||||
const { success } = await executeTask(record.taskId);
|
||||
if (success) {
|
||||
message.success('任务执行请求已提交');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('执行失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 查看日志
|
||||
const handleViewLogs = (record: any) => {
|
||||
setSelectedTaskId(record.taskId);
|
||||
setLogModalVisible(true);
|
||||
};
|
||||
|
||||
// 清理日志
|
||||
const handleCleanLogs = async () => {
|
||||
try {
|
||||
const { success } = await cleanLogs(cleanDays);
|
||||
if (success) {
|
||||
message.success(`已清理${cleanDays}天前的日志`);
|
||||
setCleanLogsVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('清理失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 表单提交成功回调
|
||||
const handleFormSuccess = () => {
|
||||
setTaskFormVisible(false);
|
||||
setEditingTask(null);
|
||||
actionRef.current?.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="scheduled-task-page">
|
||||
<ProTable
|
||||
headerTitle="定时任务管理"
|
||||
actionRef={actionRef}
|
||||
rowKey="taskId"
|
||||
search={{
|
||||
labelWidth: 120,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="clean"
|
||||
onClick={() => setCleanLogsVisible(true)}
|
||||
>
|
||||
清理日志
|
||||
</Button>,
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
新增任务
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...searchParams } = params;
|
||||
const response = await getTaskList({
|
||||
pageNo: current,
|
||||
pageSize,
|
||||
...searchParams,
|
||||
});
|
||||
|
||||
return {
|
||||
data: response?.data?.records || [],
|
||||
success: response?.success,
|
||||
total: response?.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
columns={columns}
|
||||
scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 任务表单弹窗 */}
|
||||
<TaskForm
|
||||
visible={taskFormVisible}
|
||||
editingTask={editingTask}
|
||||
onCancel={() => {
|
||||
setTaskFormVisible(false);
|
||||
setEditingTask(null);
|
||||
}}
|
||||
onSuccess={handleFormSuccess}
|
||||
/>
|
||||
|
||||
{/* 任务日志弹窗 */}
|
||||
<TaskLogModal
|
||||
visible={logModalVisible}
|
||||
taskId={selectedTaskId}
|
||||
onCancel={() => {
|
||||
setLogModalVisible(false);
|
||||
setSelectedTaskId('');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 清理日志弹窗 */}
|
||||
<Modal
|
||||
title="清理执行日志"
|
||||
visible={cleanLogsVisible}
|
||||
onOk={handleCleanLogs}
|
||||
onCancel={() => setCleanLogsVisible(false)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>清理 </span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={365}
|
||||
value={cleanDays}
|
||||
onChange={(value) => setCleanDays(value || 30)}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span> 天前的执行日志</span>
|
||||
</div>
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
注意:此操作不可逆,请谨慎操作
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduledTask;
|
88
src/pages/System/Scheduled/service.ts
Normal file
88
src/pages/System/Scheduled/service.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 分页查询定时任务
|
||||
export async function getTaskList(params: any) {
|
||||
return request('/api/sys/scheduled/task/list', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 新增定时任务
|
||||
export async function addTask(params: any) {
|
||||
return request('/api/sys/scheduled/task/add', {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改定时任务
|
||||
export async function editTask(params: any) {
|
||||
return request('/api/sys/scheduled/task/edit', {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除定时任务
|
||||
export async function deleteTask(taskId: string) {
|
||||
return request('/api/sys/scheduled/task/delete', {
|
||||
method: 'POST',
|
||||
data: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
// 启用任务
|
||||
export async function enableTask(taskId: string) {
|
||||
return request('/api/sys/scheduled/task/enable', {
|
||||
method: 'POST',
|
||||
data: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
// 禁用任务
|
||||
export async function disableTask(taskId: string) {
|
||||
return request('/api/sys/scheduled/task/disable', {
|
||||
method: 'POST',
|
||||
data: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
// 立即执行任务
|
||||
export async function executeTask(taskId: string) {
|
||||
return request('/api/sys/scheduled/task/execute', {
|
||||
method: 'POST',
|
||||
data: { taskId },
|
||||
});
|
||||
}
|
||||
|
||||
// 验证Cron表达式
|
||||
export async function validateCron(cronExpression: string) {
|
||||
return request('/api/sys/scheduled/task/validateCron', {
|
||||
method: 'POST',
|
||||
data: { cronExpression },
|
||||
});
|
||||
}
|
||||
|
||||
// 查询任务执行日志
|
||||
export async function getTaskLogs(params: any) {
|
||||
return request('/api/sys/scheduled/task/logs', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 清理执行日志
|
||||
export async function cleanLogs(days: number) {
|
||||
return request('/api/sys/scheduled/task/cleanLogs', {
|
||||
method: 'POST',
|
||||
data: { days },
|
||||
});
|
||||
}
|
||||
|
||||
// 根据ID获取任务详情
|
||||
export async function getTaskById(taskId: string) {
|
||||
return request(`/api/sys/scheduled/task/${taskId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user