diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx index c747b11e..75a1cd95 100644 --- a/src/layouts/defaultProps.tsx +++ b/src/layouts/defaultProps.tsx @@ -66,6 +66,12 @@ export default { icon: , component: '@/pages/log/index', }, + { + path: '/scenario', + name: intl.get('场景模式'), + icon: , + component: '@/pages/scenario/index', + }, { path: '/diff', name: intl.get('对比工具'), diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 9a21ef3a..67bcc370 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -521,5 +521,75 @@ "远程仓库缓存": "Remote repository cache", "SSH 文件缓存": "SSH file cache", "清除依赖缓存": "Clean dependency cache", - "清除成功": "Clean successful" + "清除成功": "Clean successful", + "场景模式": "Scenario Mode", + "新建场景": "New Scenario", + "编辑场景": "Edit Scenario", + "场景日志": "Scenario Logs", + "确定要删除场景": "Are you sure to delete scenario", + "场景已触发": "Scenario triggered", + "使用此 URL 接收外部触发": "Use this URL to receive external triggers", + "获取 Webhook URL 失败": "Failed to get Webhook URL", + "获取 Webhook": "Get Webhook", + "手动触发": "Manual Trigger", + "查看日志": "View Logs", + "变量监听": "Variable Monitor", + "任务状态": "Task Status", + "时间触发": "Time Trigger", + "系统事件": "System Event", + "描述": "Description", + "触发类型": "Trigger Type", + "执行次数": "Execution Count", + "成功/失败": "Success/Failure", + "最后触发": "Last Triggered", + "监听路径": "Watch Path", + "留空自动生成": "Leave blank to auto-generate", + "Cron 表达式": "Cron Expression", + "事件类型": "Event Type", + "磁盘空间": "Disk Space", + "内存使用": "Memory Usage", + "阈值": "Threshold", + "检查间隔": "Check Interval", + "任务 ID": "Task ID", + "条件配置": "Condition Configuration", + "条件逻辑": "Condition Logic", + "多个条件之间的关系": "Relationship between multiple conditions", + "全部满足": "All satisfied", + "任一满足": "Any satisfied", + "字段名": "Field Name", + "操作符": "Operator", + "包含": "Contains", + "不包含": "Not Contains", + "值": "Value", + "添加条件": "Add Condition", + "动作配置": "Action Configuration", + "动作类型": "Action Type", + "运行任务": "Run Task", + "设置变量": "Set Variable", + "执行命令": "Execute Command", + "发送通知": "Send Notification", + "变量名": "Variable Name", + "变量值": "Variable Value", + "命令": "Command", + "消息": "Message", + "添加动作": "Add Action", + "高级设置": "Advanced Settings", + "延迟执行": "Delay Execution", + "秒": "seconds", + "失败熔断阈值": "Failure Threshold", + "连续失败多少次后自动禁用": "Auto-disable after N consecutive failures", + "最大重试次数": "Max Retry Count", + "重试延迟": "Retry Delay", + "退避倍数": "Backoff Multiplier", + "每次重试延迟的乘数": "Multiplier for retry delay", + "启用": "Enable", + "时间": "Time", + "条件匹配": "Condition Matched", + "执行时间": "Execution Time", + "重试次数": "Retry Count", + "错误信息": "Error Message", + "是": "Yes", + "否": "No", + "共": "Total", + "项": "items" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index c19d6984..52808dc5 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -521,5 +521,75 @@ "远程仓库缓存": "远程仓库缓存", "SSH 文件缓存": "SSH 文件缓存", "清除依赖缓存": "清除依赖缓存", - "清除成功": "清除成功" + "清除成功": "清除成功", + "场景模式": "场景模式", + "新建场景": "新建场景", + "编辑场景": "编辑场景", + "场景日志": "场景日志", + "确定要删除场景": "确定要删除场景", + "场景已触发": "场景已触发", + "使用此 URL 接收外部触发": "使用此 URL 接收外部触发", + "获取 Webhook URL 失败": "获取 Webhook URL 失败", + "获取 Webhook": "获取 Webhook", + "手动触发": "手动触发", + "查看日志": "查看日志", + "变量监听": "变量监听", + "任务状态": "任务状态", + "时间触发": "时间触发", + "系统事件": "系统事件", + "描述": "描述", + "触发类型": "触发类型", + "执行次数": "执行次数", + "成功/失败": "成功/失败", + "最后触发": "最后触发", + "监听路径": "监听路径", + "留空自动生成": "留空自动生成", + "Cron 表达式": "Cron 表达式", + "事件类型": "事件类型", + "磁盘空间": "磁盘空间", + "内存使用": "内存使用", + "阈值": "阈值", + "检查间隔": "检查间隔", + "任务 ID": "任务 ID", + "条件配置": "条件配置", + "条件逻辑": "条件逻辑", + "多个条件之间的关系": "多个条件之间的关系", + "全部满足": "全部满足", + "任一满足": "任一满足", + "字段名": "字段名", + "操作符": "操作符", + "包含": "包含", + "不包含": "不包含", + "值": "值", + "添加条件": "添加条件", + "动作配置": "动作配置", + "动作类型": "动作类型", + "运行任务": "运行任务", + "设置变量": "设置变量", + "执行命令": "执行命令", + "发送通知": "发送通知", + "变量名": "变量名", + "变量值": "变量值", + "命令": "命令", + "消息": "消息", + "添加动作": "添加动作", + "高级设置": "高级设置", + "延迟执行": "延迟执行", + "秒": "秒", + "失败熔断阈值": "失败熔断阈值", + "连续失败多少次后自动禁用": "连续失败多少次后自动禁用", + "最大重试次数": "最大重试次数", + "重试延迟": "重试延迟", + "退避倍数": "退避倍数", + "每次重试延迟的乘数": "每次重试延迟的乘数", + "启用": "启用", + "时间": "时间", + "条件匹配": "条件匹配", + "执行时间": "执行时间", + "重试次数": "重试次数", + "错误信息": "错误信息", + "是": "是", + "否": "否", + "共": "共", + "项": "项" } diff --git a/src/pages/scenario/index.tsx b/src/pages/scenario/index.tsx new file mode 100644 index 00000000..44f8af86 --- /dev/null +++ b/src/pages/scenario/index.tsx @@ -0,0 +1,318 @@ +import React, { useEffect, useState } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import { + Button, + Table, + Space, + Modal, + message, + Tag, + Switch, + Tooltip, + Dropdown, + MenuProps, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + PlayCircleOutlined, + EllipsisOutlined, + FileTextOutlined, + LinkOutlined, +} from '@ant-design/icons'; +import { request } from '@/utils/http'; +import intl from 'react-intl-universal'; +import ScenarioModal from './modal'; +import ScenarioLogModal from './logModal'; +import dayjs from 'dayjs'; + +const Scenario = () => { + const [loading, setLoading] = useState(false); + const [scenarios, setScenarios] = useState([]); + const [selectedScenario, setSelectedScenario] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + const [isLogModalVisible, setIsLogModalVisible] = useState(false); + + const fetchScenarios = async () => { + setLoading(true); + try { + const { code, data } = await request.get('/api/scenarios'); + if (code === 200) { + setScenarios(data || []); + } + } catch (error) { + console.error('Failed to fetch scenarios:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchScenarios(); + }, []); + + const handleCreate = () => { + setSelectedScenario(null); + setIsModalVisible(true); + }; + + const handleEdit = (record: any) => { + setSelectedScenario(record); + setIsModalVisible(true); + }; + + const handleDelete = (record: any) => { + Modal.confirm({ + title: intl.get('确认删除'), + content: `${intl.get('确定要删除场景')} "${record.name}" ${intl.get('吗')}?`, + onOk: async () => { + try { + const { code } = await request.delete('/api/scenarios', { + data: [record.id], + }); + if (code === 200) { + message.success(intl.get('删除成功')); + fetchScenarios(); + } + } catch (error) { + console.error('Failed to delete scenario:', error); + } + }, + }); + }; + + const handleToggleEnabled = async (record: any) => { + try { + const { code } = await request.put('/api/scenarios', { + id: record.id, + isEnabled: record.isEnabled === 1 ? 0 : 1, + }); + if (code === 200) { + message.success(intl.get('更新成功')); + fetchScenarios(); + } + } catch (error) { + console.error('Failed to toggle scenario:', error); + } + }; + + const handleTrigger = async (record: any) => { + try { + const { code } = await request.post(`/api/scenarios/${record.id}/trigger`, {}); + if (code === 200) { + message.success(intl.get('场景已触发')); + } + } catch (error) { + console.error('Failed to trigger scenario:', error); + } + }; + + const handleViewLogs = (record: any) => { + setSelectedScenario(record); + setIsLogModalVisible(true); + }; + + const handleGetWebhook = async (record: any) => { + try { + const { code, data } = await request.get(`/api/scenarios/${record.id}/webhook`); + if (code === 200) { + Modal.info({ + title: 'Webhook URL', + content: ( +
+

{intl.get('使用此 URL 接收外部触发')}:

+ {data.webhookUrl} +
+ ), + width: 600, + }); + } + } catch (error) { + message.error(intl.get('获取 Webhook URL 失败')); + } + }; + + const getTriggerTypeName = (type: string) => { + const types: any = { + variable: intl.get('变量监听'), + webhook: 'Webhook', + task_status: intl.get('任务状态'), + time: intl.get('时间触发'), + system_event: intl.get('系统事件'), + }; + return types[type] || type; + }; + + const getActionsMenu = (record: any): MenuProps => ({ + items: [ + { + key: 'trigger', + icon: , + label: intl.get('手动触发'), + onClick: () => handleTrigger(record), + }, + { + key: 'logs', + icon: , + label: intl.get('查看日志'), + onClick: () => handleViewLogs(record), + }, + ...(record.triggerType === 'webhook' + ? [ + { + key: 'webhook', + icon: , + label: intl.get('获取 Webhook'), + onClick: () => handleGetWebhook(record), + }, + ] + : []), + { + type: 'divider' as const, + }, + { + key: 'edit', + icon: , + label: intl.get('编辑'), + onClick: () => handleEdit(record), + }, + { + key: 'delete', + icon: , + label: intl.get('删除'), + danger: true, + onClick: () => handleDelete(record), + }, + ], + }); + + const columns = [ + { + title: intl.get('名称'), + dataIndex: 'name', + key: 'name', + width: 180, + ellipsis: true, + }, + { + title: intl.get('描述'), + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + { + title: intl.get('触发类型'), + dataIndex: 'triggerType', + key: 'triggerType', + width: 120, + render: (type: string) => ( + {getTriggerTypeName(type)} + ), + }, + { + title: intl.get('状态'), + dataIndex: 'isEnabled', + key: 'isEnabled', + width: 80, + render: (isEnabled: number, record: any) => ( + handleToggleEnabled(record)} + /> + ), + }, + { + title: intl.get('执行次数'), + dataIndex: 'executionCount', + key: 'executionCount', + width: 100, + render: (count: number) => count || 0, + }, + { + title: intl.get('成功/失败'), + key: 'stats', + width: 100, + render: (_: any, record: any) => ( + + {record.successCount || 0} + {record.failureCount || 0} + + ), + }, + { + title: intl.get('最后触发'), + dataIndex: 'lastTriggeredAt', + key: 'lastTriggeredAt', + width: 160, + render: (date: string) => + date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-', + }, + { + title: intl.get('操作'), + key: 'action', + width: 100, + fixed: 'right', + render: (_: any, record: any) => ( + + , + ], + }} + > + `${intl.get('共')} ${total} ${intl.get('项')}`, + }} + /> + + { + setIsModalVisible(false); + setSelectedScenario(null); + }} + onSuccess={() => { + setIsModalVisible(false); + setSelectedScenario(null); + fetchScenarios(); + }} + /> + + { + setIsLogModalVisible(false); + setSelectedScenario(null); + }} + /> + + ); +}; + +export default Scenario; diff --git a/src/pages/scenario/logModal.tsx b/src/pages/scenario/logModal.tsx new file mode 100644 index 00000000..3285afe0 --- /dev/null +++ b/src/pages/scenario/logModal.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Table, Tag, Typography } from 'antd'; +import { request } from '@/utils/http'; +import intl from 'react-intl-universal'; +import dayjs from 'dayjs'; + +const { Text } = Typography; + +interface ScenarioLogModalProps { + visible: boolean; + scenario: any; + onCancel: () => void; +} + +const ScenarioLogModal: React.FC = ({ + visible, + scenario, + onCancel, +}) => { + const [loading, setLoading] = useState(false); + const [logs, setLogs] = useState([]); + + useEffect(() => { + if (visible && scenario) { + fetchLogs(); + } + }, [visible, scenario]); + + const fetchLogs = async () => { + setLoading(true); + try { + const { code, data } = await request.get('/api/scenarios/logs', { + params: { + scenarioId: scenario.id, + limit: 100, + }, + }); + if (code === 200) { + setLogs(data || []); + } + } catch (error) { + console.error('Failed to fetch logs:', error); + } finally { + setLoading(false); + } + }; + + const columns = [ + { + title: intl.get('时间'), + dataIndex: 'createdAt', + key: 'createdAt', + width: 180, + render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: intl.get('状态'), + dataIndex: 'executionStatus', + key: 'executionStatus', + width: 100, + render: (status: string) => { + const colors: any = { + success: 'success', + failure: 'error', + partial: 'warning', + }; + return {status}; + }, + }, + { + title: intl.get('条件匹配'), + dataIndex: 'conditionsMatched', + key: 'conditionsMatched', + width: 100, + render: (matched: boolean) => ( + + {matched ? intl.get('是') : intl.get('否')} + + ), + }, + { + title: intl.get('执行时间'), + dataIndex: 'executionTime', + key: 'executionTime', + width: 100, + render: (time: number) => (time ? `${time}ms` : '-'), + }, + { + title: intl.get('重试次数'), + dataIndex: 'retriesAttempted', + key: 'retriesAttempted', + width: 100, + render: (retries: number) => retries || 0, + }, + { + title: intl.get('错误信息'), + dataIndex: 'errorMessage', + key: 'errorMessage', + ellipsis: true, + render: (error: string) => ( + {error || '-'} + ), + }, + ]; + + return ( + +
+ + ); +}; + +export default ScenarioLogModal; diff --git a/src/pages/scenario/modal.tsx b/src/pages/scenario/modal.tsx new file mode 100644 index 00000000..4e32b3c8 --- /dev/null +++ b/src/pages/scenario/modal.tsx @@ -0,0 +1,443 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, + Form, + Input, + Select, + InputNumber, + Switch, + Button, + Space, + Card, + message, + Divider, +} from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import { request } from '@/utils/http'; +import intl from 'react-intl-universal'; + +const { TextArea } = Input; +const { Option } = Select; + +interface ScenarioModalProps { + visible: boolean; + scenario: any; + onCancel: () => void; + onSuccess: () => void; +} + +const ScenarioModal: React.FC = ({ + visible, + scenario, + onCancel, + onSuccess, +}) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [triggerType, setTriggerType] = useState('time'); + + useEffect(() => { + if (visible) { + if (scenario) { + form.setFieldsValue({ + ...scenario, + conditions: scenario.conditions || [], + actions: scenario.actions || [], + }); + setTriggerType(scenario.triggerType); + } else { + form.resetFields(); + form.setFieldsValue({ + isEnabled: 1, + conditionLogic: 'AND', + failureThreshold: 3, + delayExecution: 0, + conditions: [], + actions: [], + }); + setTriggerType('time'); + } + } + }, [visible, scenario, form]); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + const endpoint = scenario ? '/api/scenarios' : '/api/scenarios'; + const method = scenario ? 'put' : 'post'; + const payload = scenario ? { ...values, id: scenario.id } : values; + + const { code } = await request[method](endpoint, payload); + if (code === 200) { + message.success( + scenario ? intl.get('更新成功') : intl.get('创建成功'), + ); + onSuccess(); + } + } catch (error) { + console.error('Failed to save scenario:', error); + } finally { + setLoading(false); + } + }; + + const renderTriggerConfig = () => { + switch (triggerType) { + case 'variable': + return ( + + + + ); + case 'webhook': + return ( + + + + ); + case 'time': + return ( + + + + ); + case 'system_event': + return ( + <> + + + + + + + + + + + ); + case 'task_status': + return ( + + + + ); + default: + return null; + } + }; + + return ( + +
+ + + + + +