diff --git a/src/api/workflow/timer/index.ts b/src/api/workflow/timer/index.ts new file mode 100644 index 00000000..a2fb4983 --- /dev/null +++ b/src/api/workflow/timer/index.ts @@ -0,0 +1,19 @@ +import type { PageResult, R } from '@/api/types'; +import request from '@/api/request'; +import type { EventWaitQuery, EventWaitSummary, EventWaitVO, TimerTriggerForm } from './types'; + +export function listEventWait(query: EventWaitQuery) { + return request>>({ url: '/workflow/eventWait/list', method: 'get', params: query }); +} + +export function summaryEventWait(query: EventWaitQuery) { + return request>({ url: '/workflow/eventWait/summary', method: 'get', params: query }); +} + +export function getEventWait(id: string | number) { + return request>({ url: `/workflow/eventWait/${id}`, method: 'get' }); +} + +export function triggerTimerStart(data: TimerTriggerForm) { + return request>({ url: '/workflow/eventWait/trigger', method: 'post', data }); +} diff --git a/src/api/workflow/timer/types.ts b/src/api/workflow/timer/types.ts new file mode 100644 index 00000000..8bf7db16 --- /dev/null +++ b/src/api/workflow/timer/types.ts @@ -0,0 +1,56 @@ +import type { PageQuery } from '@/api/types'; + +export interface EventWaitVO { + id: string | number; + definitionId?: string | number; + flowName?: string; + instanceId?: string | number; + businessId?: string; + nodeCode?: string; + eventType?: string; + eventTypeName?: string; + eventName?: string; + attachedTaskId?: string | number; + triggerType?: string; + triggerInterval?: string; + nextTriggerAt?: string; + timezone?: string; + repeatCount?: number; + maxRepeatCount?: number; + status?: string; + statusName?: string; + retryCount?: number; + maxRetryCount?: number; + retryAt?: string; + payload?: string; + lastError?: string; + createTime?: string; + updateTime?: string; + timerEnabled?: boolean; +} + +export interface EventWaitQuery extends PageQuery { + statuses?: string; + eventTypes?: string; + timerOnly?: boolean; + flowCode?: string; + businessId?: string; + nodeCode?: string; + eventName?: string; + nextTriggerAtFrom?: string; + nextTriggerAtTo?: string; +} + +export interface EventWaitSummary { + waiting: number; + retry: number; + running: number; + dead: number; + cancelled: number; +} + +export interface TimerTriggerForm { + waitId: string | number; + businessId?: string; + variables?: Record; +} diff --git a/src/pages/dynamicPage.tsx b/src/pages/dynamicPage.tsx index a6e2dd4e..2af3c45b 100644 --- a/src/pages/dynamicPage.tsx +++ b/src/pages/dynamicPage.tsx @@ -38,6 +38,7 @@ import WorkflowMyDocumentPage from '@/pages/workflow/task/myDocument'; import WorkflowTaskCopyPage from '@/pages/workflow/task/taskCopyList'; import WorkflowTaskFinishPage from '@/pages/workflow/task/taskFinish'; import WorkflowTaskWaitingPage from '@/pages/workflow/task/taskWaiting'; +import WorkflowTimerPage from '@/pages/workflow/timer'; import { usePermissionStore } from '@/stores/permissionStore'; import { flattenBackendRoutes } from '@/utils/menu'; @@ -78,6 +79,7 @@ const migratedPages: Record = { 'workflow/processDefinition/index': WorkflowProcessDefinitionPage, 'workflow/processInstance/index': WorkflowProcessInstancePage, 'workflow/spel/index': WorkflowSpelPage, + 'workflow/timer/index': WorkflowTimerPage, 'workflow/task/allTaskWaiting/index': WorkflowAllTaskWaitingPage, 'workflow/task/allTaskWaiting': WorkflowAllTaskWaitingPage, 'workflow/task/myDocument/index': WorkflowMyDocumentPage, diff --git a/src/pages/workflow/timer/index.tsx b/src/pages/workflow/timer/index.tsx new file mode 100644 index 00000000..401cb760 --- /dev/null +++ b/src/pages/workflow/timer/index.tsx @@ -0,0 +1,273 @@ +import { EyeOutlined, PlayCircleOutlined, ReloadOutlined } from '@ant-design/icons'; +import { PageContainer, ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; +import { Alert, Button, Descriptions, Drawer, Form, Input, message, Modal, Space, Tag, Typography } from 'antd'; +import { useRef, useState } from 'react'; +import type { EventWaitQuery, EventWaitSummary, EventWaitVO } from '@/api/workflow/timer/types'; +import { getEventWait, listEventWait, summaryEventWait, triggerTimerStart } from '@/api/workflow/timer'; +import { useTableScroll } from '@/hooks/useTableScroll'; +import { useUserStore } from '@/stores/userStore'; +import { hasPermi } from '@/utils/permission'; +import { toPageQuery, toTableData } from '@/utils/ruoyi'; + +type TimerSearch = Omit & { + statuses?: string[]; + eventTypes?: string[]; + nextTriggerAt?: [string, string]; +}; + +const eventTypeOptions = [ + { label: '定时启动', value: 'TIMER_START' }, + { label: '定时捕获', value: 'TIMER_CATCH' }, + { label: '定时边界', value: 'TIMER_BOUNDARY' }, + { label: '消息启动', value: 'MESSAGE_START' }, + { label: '消息捕获', value: 'MESSAGE_CATCH' }, + { label: '消息边界', value: 'MESSAGE_BOUNDARY' }, + { label: '错误捕获', value: 'ERROR_CATCH' }, + { label: '错误边界', value: 'ERROR_BOUNDARY' } +]; + +const statusOptions = [ + { label: '待触发', value: 'WAITING' }, + { label: '重试中', value: 'RETRY' }, + { label: '执行中', value: 'RUNNING' }, + { label: '已取消', value: 'CANCELLED' }, + { label: '已死', value: 'DEAD' } +]; + +const triggerTypeNames: Record = { + AFTER: '一次性延迟', + FIXED: '固定间隔', + POINT_IN_TIME: '每日时点' +}; + +const statusColors: Record = { + WAITING: 'blue', + RETRY: 'orange', + RUNNING: 'green', + DEAD: 'red', + CANCELLED: 'default' +}; + +function buildQuery(params: TimerSearch): EventWaitQuery { + const { statuses, eventTypes, nextTriggerAt, ...rest } = toPageQuery(params); + return { + ...rest, + statuses: statuses?.length ? statuses.join(',') : undefined, + eventTypes: eventTypes?.length ? eventTypes.join(',') : undefined, + nextTriggerAtFrom: nextTriggerAt?.[0], + nextTriggerAtTo: nextTriggerAt?.[1] + }; +} + +export default function WorkflowTimerPage() { + const actionRef = useRef(undefined); + const { tableScroll } = useTableScroll(1390); + const userInfo = useUserStore(state => state.userInfo); + const canTrigger = hasPermi(userInfo, ['workflow:eventWait:trigger']); + const [summary, setSummary] = useState(); + const [timerDisabled, setTimerDisabled] = useState(false); + const [detail, setDetail] = useState(); + const [triggerRow, setTriggerRow] = useState(); + const [businessId, setBusinessId] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const openDetail = async (row: EventWaitVO) => { + const res = await getEventWait(row.id); + setDetail(res.data || row); + }; + + const submitTrigger = async () => { + if (!triggerRow) return; + setSubmitting(true); + try { + const res = await triggerTimerStart({ waitId: triggerRow.id, businessId: businessId.trim() || undefined }); + message.success(`已发起一次实例 ${res.data}`); + setTriggerRow(undefined); + actionRef.current?.reload(); + } finally { + setSubmitting(false); + } + }; + + const columns: ProColumns[] = [ + { title: '流程编码', dataIndex: 'flowCode', hideInTable: true }, + { title: '节点编码', dataIndex: 'nodeCode', hideInTable: true }, + { title: '业务ID', dataIndex: 'businessId', hideInTable: true }, + { + title: '事件类型', + dataIndex: 'eventTypes', + hideInTable: true, + valueType: 'select', + fieldProps: { mode: 'multiple', options: eventTypeOptions, placeholder: '默认只查定时类' } + }, + { + title: '状态', + dataIndex: 'statuses', + hideInTable: true, + valueType: 'select', + fieldProps: { mode: 'multiple', options: statusOptions, placeholder: '不限' } + }, + { title: '下次触发', dataIndex: 'nextTriggerAt', hideInTable: true, valueType: 'dateTimeRange' }, + { title: '流程名称', dataIndex: 'flowName', search: false, width: 150, ellipsis: true }, + { title: '事件节点', dataIndex: 'nodeCode', search: false, width: 120, ellipsis: true }, + { title: '事件类型', dataIndex: 'eventTypeName', search: false, width: 110 }, + { + title: '触发策略', + dataIndex: 'triggerType', + search: false, + width: 120, + renderText: value => triggerTypeNames[value] || value || '-' + }, + { + title: '间隔/时点', + dataIndex: 'triggerInterval', + search: false, + width: 160, + renderText: (_, row) => row.triggerInterval || '-' + }, + { title: '下次触发时间', dataIndex: 'nextTriggerAt', search: false, width: 170 }, + { + title: '已触发', + dataIndex: 'repeatCount', + search: false, + width: 100, + renderText: (_, row) => `${row.repeatCount ?? 0}${row.maxRepeatCount ? ` / ${row.maxRepeatCount}` : ''}` + }, + { + title: '状态', + dataIndex: 'status', + search: false, + width: 100, + render: (_, row) => {row.statusName || row.status || '-'} + }, + { + title: '关联实例', + dataIndex: 'instanceId', + search: false, + width: 150, + renderText: (_, row) => row.businessId || row.instanceId || '定义级' + }, + { + title: '操作', + valueType: 'option', + width: 170, + fixed: 'right', + render: (_, row) => ( + + + {canTrigger && row.eventType === 'TIMER_START' && ( + + )} + + ) + } + ]; + + return ( + + {timerDisabled && ( + + )} + + actionRef={actionRef} + rowKey="id" + columns={columns} + scroll={tableScroll} + search={{ labelWidth: 90 }} + pagination={{ defaultPageSize: 10, showSizeChanger: true }} + request={async params => { + const query = buildQuery(params); + const res = await listEventWait(query); + setTimerDisabled(!!res.data?.rows?.length && res.data.rows[0].timerEnabled === false); + const totals = await summaryEventWait(query); + setSummary(totals.data); + return toTableData(res); + }} + toolbar={{ title: '定时任务' }} + toolBarRender={() => [ + + ]} + tableExtraRender={() => + summary && ( + + 待触发 {summary.waiting} · 重试中 {summary.retry} · 执行中 {summary.running} · 已取消 {summary.cancelled}{' '} + · 已死 {summary.dead} + + ) + } + /> + setDetail(undefined)}> + {detail && ( + + )} + + setTriggerRow(undefined)} + okText="确认触发" + > +
+ + {triggerRow?.flowName || '-'} / {triggerRow?.nodeCode || '-'} + + {triggerRow?.nextTriggerAt || '-'} + + setBusinessId(event.target.value)} + placeholder="留空自动生成" + allowClear + /> + +
+ 只额外发起一次实例,定时任务的下次触发时间保持不变。 +
+
+ ); +}