feat: 增加QQBot插件定时任务页面
This commit is contained in:
parent
4b7d732ec7
commit
35747e39eb
@ -45,6 +45,7 @@
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vue-js-cron/core": "catalog:",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"antdv-next": "catalog:",
|
||||
|
||||
@ -46,6 +46,12 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
|
||||
'QqBotPermissionDelete',
|
||||
'QqBotPermissionEdit',
|
||||
'QqBotPlugin',
|
||||
'QqBotPluginTask',
|
||||
'QqBotPluginTaskDisable',
|
||||
'QqBotPluginTaskEnable',
|
||||
'QqBotPluginTaskRun',
|
||||
'QqBotPluginTaskRunLog',
|
||||
'QqBotPluginTaskUpdateCron',
|
||||
'QqBotRule',
|
||||
'QqBotRuleCreate',
|
||||
'QqBotRuleDelete',
|
||||
|
||||
78
apps/web-antdv-next/src/api/qqbot/plugin-task.spec.ts
Normal file
78
apps/web-antdv-next/src/api/qqbot/plugin-task.spec.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
import {
|
||||
disableQqbotPluginTask,
|
||||
enableQqbotPluginTask,
|
||||
getQqbotPluginTaskPage,
|
||||
getQqbotPluginTaskRunPage,
|
||||
runQqbotPluginTaskOnce,
|
||||
updateQqbotPluginTaskCron,
|
||||
} from './plugin-task';
|
||||
|
||||
vi.mock('#/api/request', () => ({
|
||||
requestClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('qqbot plugin task API wrappers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('uses plugin-platform task endpoints', async () => {
|
||||
vi.mocked(requestClient.get).mockResolvedValue({ list: [], total: 0 });
|
||||
vi.mocked(requestClient.post).mockResolvedValue({});
|
||||
|
||||
await getQqbotPluginTaskPage({
|
||||
enabled: true,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
taskKey: 'bangdream.bestdori.sync-main-data',
|
||||
});
|
||||
await enableQqbotPluginTask('task-1');
|
||||
await disableQqbotPluginTask('task-1');
|
||||
await updateQqbotPluginTaskCron('task-1', '0 */6 * * *');
|
||||
await runQqbotPluginTaskOnce('task-1', { force: true });
|
||||
await getQqbotPluginTaskRunPage('task-1', { pageNo: 1, pageSize: 20 });
|
||||
|
||||
expect(requestClient.get).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/page',
|
||||
{
|
||||
params: {
|
||||
enabled: true,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
taskKey: 'bangdream.bestdori.sync-main-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/task-1/enable',
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/task-1/disable',
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/task-1/cron',
|
||||
{
|
||||
cronExpression: '0 */6 * * *',
|
||||
},
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/task-1/run',
|
||||
{
|
||||
input: { force: true },
|
||||
},
|
||||
);
|
||||
expect(requestClient.get).toHaveBeenCalledWith(
|
||||
'/qqbot/plugin-platform/tasks/task-1/runs',
|
||||
{
|
||||
params: { pageNo: 1, pageSize: 20 },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
114
apps/web-antdv-next/src/api/qqbot/plugin-task.ts
Normal file
114
apps/web-antdv-next/src/api/qqbot/plugin-task.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { QqbotApi } from './index';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace QqbotPluginTaskApi {
|
||||
export type RuntimeStatus =
|
||||
| 'disabled'
|
||||
| 'failed'
|
||||
| 'idle'
|
||||
| 'running'
|
||||
| 'scheduled';
|
||||
export type RunStatus = 'failed' | 'running' | 'skipped' | 'success';
|
||||
export type TriggerType = 'bootstrap' | 'manual' | 'schedule';
|
||||
|
||||
export interface Task {
|
||||
cronExpression: string;
|
||||
defaultCron: string;
|
||||
description?: null | string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
installationId: string;
|
||||
lastDurationMs?: null | number;
|
||||
lastError?: null | string;
|
||||
lastRunAt?: null | string;
|
||||
lastStatus?: null | RunStatus;
|
||||
nextRunAt?: null | string;
|
||||
pluginId: string;
|
||||
pluginKey?: string;
|
||||
pluginName?: string;
|
||||
runtimeStatus: RuntimeStatus;
|
||||
taskKey: string;
|
||||
taskName: string;
|
||||
}
|
||||
|
||||
export interface TaskRun {
|
||||
createTime?: string;
|
||||
durationMs?: null | number;
|
||||
errorMessage?: null | string;
|
||||
finishedAt?: null | string;
|
||||
id: string;
|
||||
jobId?: null | string;
|
||||
safeSummary?: null | Recordable<any>;
|
||||
startedAt?: null | string;
|
||||
status: RunStatus;
|
||||
taskId: string;
|
||||
taskKey: string;
|
||||
triggerType: TriggerType;
|
||||
}
|
||||
|
||||
export interface TaskQuery extends Recordable<any> {
|
||||
enabled?: boolean;
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
pluginId?: string;
|
||||
pluginKey?: string;
|
||||
status?: RuntimeStatus;
|
||||
taskKey?: string;
|
||||
}
|
||||
|
||||
export interface TaskRunQuery extends Recordable<any> {
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
status?: RunStatus;
|
||||
triggerType?: TriggerType;
|
||||
}
|
||||
}
|
||||
|
||||
export function getQqbotPluginTaskPage(params: QqbotPluginTaskApi.TaskQuery) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotPluginTaskApi.Task>>(
|
||||
'/qqbot/plugin-platform/tasks/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function enableQqbotPluginTask(id: string) {
|
||||
return requestClient.post<QqbotPluginTaskApi.Task>(
|
||||
`/qqbot/plugin-platform/tasks/${id}/enable`,
|
||||
);
|
||||
}
|
||||
|
||||
export function disableQqbotPluginTask(id: string) {
|
||||
return requestClient.post<QqbotPluginTaskApi.Task>(
|
||||
`/qqbot/plugin-platform/tasks/${id}/disable`,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateQqbotPluginTaskCron(id: string, cronExpression: string) {
|
||||
return requestClient.post<QqbotPluginTaskApi.Task>(
|
||||
`/qqbot/plugin-platform/tasks/${id}/cron`,
|
||||
{ cronExpression },
|
||||
);
|
||||
}
|
||||
|
||||
export function runQqbotPluginTaskOnce(
|
||||
id: string,
|
||||
input: Recordable<any> = {},
|
||||
) {
|
||||
return requestClient.post<{ jobId: string; taskId: string }>(
|
||||
`/qqbot/plugin-platform/tasks/${id}/run`,
|
||||
{ input },
|
||||
);
|
||||
}
|
||||
|
||||
export function getQqbotPluginTaskRunPage(
|
||||
id: string,
|
||||
params: QqbotPluginTaskApi.TaskRunQuery,
|
||||
) {
|
||||
return requestClient.get<QqbotApi.PageResult<QqbotPluginTaskApi.TaskRun>>(
|
||||
`/qqbot/plugin-platform/tasks/${id}/runs`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
@ -66,6 +66,15 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'QqBotPlugin',
|
||||
path: '/qqbot/plugin',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/plugin-task/list'),
|
||||
meta: {
|
||||
icon: 'lucide:calendar-clock',
|
||||
title: '插件定时任务',
|
||||
},
|
||||
name: 'QqBotPluginTask',
|
||||
path: '/qqbot/plugin-task',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/conversation/list'),
|
||||
meta: {
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
||||
import { useCron } from '@vue-js-cron/core';
|
||||
import { Alert, Input, RadioButton, RadioGroup, Space } from 'antdv-next';
|
||||
|
||||
const AAlert = Alert as any;
|
||||
const AInput = Input as any;
|
||||
const ARadioButton = RadioButton as any;
|
||||
const ARadioGroup = RadioGroup as any;
|
||||
const ASpace = Space as any;
|
||||
|
||||
const fieldPattern = /^[\d*/,-]+$/;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CronEditorAntdvNext',
|
||||
props: {
|
||||
value: {
|
||||
default: '0 */6 * * *',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
emits: ['update:value', 'validChange'],
|
||||
setup(props, { emit }) {
|
||||
const cronCore = useCron({
|
||||
format: 'crontab',
|
||||
initialValue: props.value,
|
||||
locale: 'cn',
|
||||
});
|
||||
const expression = ref(props.value);
|
||||
const error = ref('');
|
||||
|
||||
function validate(value: string) {
|
||||
const nextValue = `${value || ''}`.trim().replaceAll(/\s+/g, ' ');
|
||||
const fields = nextValue.split(' ').filter(Boolean);
|
||||
cronCore.cron.value = nextValue;
|
||||
if (fields.length !== 5) {
|
||||
error.value = '请输入 5 段 cron 表达式';
|
||||
emit('validChange', false);
|
||||
return;
|
||||
}
|
||||
if (!fields.every((field) => fieldPattern.test(field))) {
|
||||
error.value = 'cron 只能包含数字、星号、斜杠、逗号和横线';
|
||||
emit('validChange', false);
|
||||
return;
|
||||
}
|
||||
if (fields[0] === '*') {
|
||||
error.value = '不允许每分钟执行';
|
||||
emit('validChange', false);
|
||||
return;
|
||||
}
|
||||
error.value = cronCore.error.value || '';
|
||||
emit('validChange', !error.value);
|
||||
}
|
||||
|
||||
function updateValue(value: string) {
|
||||
expression.value = value;
|
||||
emit('update:value', value);
|
||||
validate(value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
expression.value = value;
|
||||
validate(value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return () => (
|
||||
<ASpace direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<ARadioGroup
|
||||
buttonStyle="solid"
|
||||
onChange={(event: any) => updateValue(event.target.value)}
|
||||
value={expression.value}
|
||||
>
|
||||
<ARadioButton value="0 */6 * * *">每 6 小时</ARadioButton>
|
||||
<ARadioButton value="0 3 * * *">每天 03:00</ARadioButton>
|
||||
<ARadioButton value="0 3 * * 1">每周一 03:00</ARadioButton>
|
||||
</ARadioGroup>
|
||||
<AInput
|
||||
onChange={(event: any) => updateValue(event.target.value)}
|
||||
value={expression.value}
|
||||
/>
|
||||
{error.value ? (
|
||||
<AAlert message={error.value} showIcon type="error" />
|
||||
) : null}
|
||||
</ASpace>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,79 @@
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { QqbotPluginTaskApi } from '#/api/qqbot/plugin-task';
|
||||
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
||||
import { message, Modal } from 'antdv-next';
|
||||
|
||||
import { updateQqbotPluginTaskCron } from '#/api/qqbot/plugin-task';
|
||||
|
||||
import CronEditorAntdvNext from './CronEditorAntdvNext';
|
||||
|
||||
const AModal = Modal as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotPluginTaskCronModal',
|
||||
props: {
|
||||
open: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
task: {
|
||||
default: undefined,
|
||||
type: Object as PropType<QqbotPluginTaskApi.Task | undefined>,
|
||||
},
|
||||
},
|
||||
emits: ['close', 'saved'],
|
||||
setup(props, { emit }) {
|
||||
const cronExpression = ref('0 */6 * * *');
|
||||
const valid = ref(true);
|
||||
const saving = ref(false);
|
||||
|
||||
watch(
|
||||
() => [props.open, props.task?.id],
|
||||
() => {
|
||||
cronExpression.value =
|
||||
props.task?.cronExpression ||
|
||||
props.task?.defaultCron ||
|
||||
'0 */6 * * *';
|
||||
valid.value = true;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function save() {
|
||||
if (!props.task || !valid.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateQqbotPluginTaskCron(props.task.id, cronExpression.value);
|
||||
message.success('Cron 已更新');
|
||||
emit('saved');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<AModal
|
||||
confirmLoading={saving.value}
|
||||
okButtonProps={{ disabled: !valid.value }}
|
||||
onCancel={() => emit('close')}
|
||||
onOk={() => void save()}
|
||||
open={props.open}
|
||||
title="修改 Cron"
|
||||
width={620}
|
||||
>
|
||||
<CronEditorAntdvNext
|
||||
onUpdate:value={(value: string) => {
|
||||
cronExpression.value = value;
|
||||
}}
|
||||
onValidChange={(value: boolean) => {
|
||||
valid.value = value;
|
||||
}}
|
||||
value={cronExpression.value}
|
||||
/>
|
||||
</AModal>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,98 @@
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { QqbotPluginTaskApi } from '#/api/qqbot/plugin-task';
|
||||
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
||||
import { Drawer, Tag } from 'antdv-next';
|
||||
|
||||
import { getQqbotPluginTaskRunPage } from '#/api/qqbot/plugin-task';
|
||||
|
||||
const ADrawer = Drawer as any;
|
||||
|
||||
const runStatusColor: Record<QqbotPluginTaskApi.RunStatus, string> = {
|
||||
failed: 'error',
|
||||
running: 'processing',
|
||||
skipped: 'default',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotPluginTaskRunDrawer',
|
||||
props: {
|
||||
open: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
task: {
|
||||
default: undefined,
|
||||
type: Object as PropType<QqbotPluginTaskApi.Task | undefined>,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const loading = ref(false);
|
||||
const runs = ref<QqbotPluginTaskApi.TaskRun[]>([]);
|
||||
|
||||
watch(
|
||||
() => [props.open, props.task?.id],
|
||||
() => {
|
||||
if (props.open && props.task?.id) void loadRuns();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function loadRuns() {
|
||||
if (!props.task?.id) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const page = await getQqbotPluginTaskRunPage(props.task.id, {
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
runs.value = page.list || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const renderRun = (item: QqbotPluginTaskApi.TaskRun) => (
|
||||
<div class="border-b border-solid border-gray-100 py-3" key={item.id}>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<Tag color={runStatusColor[item.status]}>{item.status}</Tag>
|
||||
<Tag>{item.triggerType}</Tag>
|
||||
<span>{item.startedAt || item.createTime || '-'}</span>
|
||||
<span>
|
||||
{item.durationMs === null || item.durationMs === undefined
|
||||
? '-'
|
||||
: `${item.durationMs} ms`}
|
||||
</span>
|
||||
</div>
|
||||
{item.safeSummary ? (
|
||||
<pre class="whitespace-pre-wrap rounded bg-gray-50 p-2 text-xs">
|
||||
{JSON.stringify(item.safeSummary, null, 2)}
|
||||
</pre>
|
||||
) : null}
|
||||
{item.errorMessage ? (
|
||||
<div class="mt-2 text-sm text-red-500">{item.errorMessage}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return () => (
|
||||
<ADrawer
|
||||
loading={loading.value}
|
||||
onClose={() => emit('close')}
|
||||
open={props.open}
|
||||
size="large"
|
||||
title={props.task?.taskName || '运行记录'}
|
||||
>
|
||||
{runs.value.length > 0 ? (
|
||||
<div>{runs.value.map((run) => renderRun(run))}</div>
|
||||
) : (
|
||||
<span>暂无运行记录</span>
|
||||
)}
|
||||
</ADrawer>
|
||||
);
|
||||
},
|
||||
});
|
||||
248
apps/web-antdv-next/src/views/qqbot/plugin-task/list.tsx
Normal file
248
apps/web-antdv-next/src/views/qqbot/plugin-task/list.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { QqbotPluginTaskApi } from '#/api/qqbot/plugin-task';
|
||||
import type { KtTableApi, KtTableRowAction } from '#/components/ktTable';
|
||||
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message, Tag } from 'antdv-next';
|
||||
|
||||
import {
|
||||
disableQqbotPluginTask,
|
||||
enableQqbotPluginTask,
|
||||
getQqbotPluginTaskPage,
|
||||
runQqbotPluginTaskOnce,
|
||||
} from '#/api/qqbot/plugin-task';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
|
||||
import TaskCronModal from './components/TaskCronModal';
|
||||
import TaskRunDrawer from './components/TaskRunDrawer';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
|
||||
const runtimeStatusColor: Record<QqbotPluginTaskApi.RuntimeStatus, string> = {
|
||||
disabled: 'default',
|
||||
failed: 'error',
|
||||
idle: 'default',
|
||||
running: 'processing',
|
||||
scheduled: 'success',
|
||||
};
|
||||
|
||||
const runStatusColor: Record<QqbotPluginTaskApi.RunStatus, string> = {
|
||||
failed: 'error',
|
||||
running: 'processing',
|
||||
skipped: 'default',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotPluginTaskList',
|
||||
setup() {
|
||||
const cronModalOpen = ref(false);
|
||||
const cronTask = ref<QqbotPluginTaskApi.Task>();
|
||||
const runDrawerOpen = ref(false);
|
||||
const runTask = ref<QqbotPluginTaskApi.Task>();
|
||||
|
||||
const columns: Array<TableColumnType<QqbotPluginTaskApi.Task>> = [
|
||||
{ dataIndex: 'pluginName', key: 'pluginName', title: '插件', width: 160 },
|
||||
{ dataIndex: 'taskKey', key: 'taskKey', title: '任务 Key', width: 260 },
|
||||
{ dataIndex: 'taskName', key: 'taskName', title: '任务名称', width: 180 },
|
||||
{
|
||||
dataIndex: 'cronExpression',
|
||||
key: 'cronExpression',
|
||||
title: 'Cron',
|
||||
width: 150,
|
||||
},
|
||||
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
|
||||
{
|
||||
dataIndex: 'runtimeStatus',
|
||||
key: 'runtimeStatus',
|
||||
title: '运行状态',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastStatus',
|
||||
key: 'lastStatus',
|
||||
title: '最近结果',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'nextRunAt',
|
||||
key: 'nextRunAt',
|
||||
title: '下次运行',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
const api: KtTableApi<QqbotPluginTaskApi.Task> = {
|
||||
list: async (params) => await getQqbotPluginTaskPage(params),
|
||||
};
|
||||
const rowActions: Array<KtTableRowAction<QqbotPluginTaskApi.Task>> = [
|
||||
{
|
||||
key: 'run',
|
||||
label: '运行一次',
|
||||
onClick: async (row, context) => {
|
||||
await runQqbotPluginTaskOnce(row.id, {});
|
||||
message.success('任务已提交');
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: ['QqBot:PluginTask:Run'],
|
||||
},
|
||||
{
|
||||
key: 'cron',
|
||||
label: '修改 Cron',
|
||||
onClick: (row) => {
|
||||
cronTask.value = row;
|
||||
cronModalOpen.value = true;
|
||||
},
|
||||
permissionCodes: ['QqBot:PluginTask:UpdateCron'],
|
||||
},
|
||||
{
|
||||
key: 'runs',
|
||||
label: '运行记录',
|
||||
onClick: (row) => {
|
||||
runTask.value = row;
|
||||
runDrawerOpen.value = true;
|
||||
},
|
||||
permissionCodes: ['QqBot:PluginTask:RunLog'],
|
||||
},
|
||||
{
|
||||
key: 'toggle',
|
||||
label: '启停',
|
||||
onClick: async (row, context) => {
|
||||
if (row.enabled) {
|
||||
await disableQqbotPluginTask(row.id);
|
||||
message.success('任务已停用');
|
||||
} else {
|
||||
await enableQqbotPluginTask(row.id);
|
||||
message.success('任务已启用');
|
||||
}
|
||||
await context.reload();
|
||||
},
|
||||
permissionCodes: [
|
||||
'QqBot:PluginTask:Enable',
|
||||
'QqBot:PluginTask:Disable',
|
||||
],
|
||||
},
|
||||
];
|
||||
const [registerTable, tableApi] = useKtTable<QqbotPluginTaskApi.Task>({
|
||||
api,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '插件 Key',
|
||||
},
|
||||
fieldName: 'pluginKey',
|
||||
label: '插件',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '任务 Key',
|
||||
},
|
||||
fieldName: 'taskKey',
|
||||
label: '任务',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '已调度', value: 'scheduled' },
|
||||
{ label: '运行中', value: 'running' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
{ label: '空闲', value: 'idle' },
|
||||
],
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '停用', value: false },
|
||||
],
|
||||
},
|
||||
fieldName: 'enabled',
|
||||
label: '启用',
|
||||
},
|
||||
],
|
||||
},
|
||||
rowActions,
|
||||
tableTitle: '插件定时任务',
|
||||
});
|
||||
|
||||
function closeCronModal() {
|
||||
cronModalOpen.value = false;
|
||||
cronTask.value = undefined;
|
||||
}
|
||||
|
||||
async function handleCronSaved() {
|
||||
closeCronModal();
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Page autoContentHeight>
|
||||
<AKtTable
|
||||
onRegister={registerTable}
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotPluginTaskApi.Task;
|
||||
if (column.key === 'pluginName') {
|
||||
return row.pluginName || row.pluginKey || row.pluginId || '-';
|
||||
}
|
||||
if (column.key === 'enabled') {
|
||||
return (
|
||||
<Tag color={row.enabled ? 'success' : 'default'}>
|
||||
{row.enabled ? '启用' : '停用'}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'runtimeStatus') {
|
||||
return (
|
||||
<Tag color={runtimeStatusColor[row.runtimeStatus]}>
|
||||
{row.runtimeStatus}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (column.key === 'lastStatus') {
|
||||
return row.lastStatus ? (
|
||||
<Tag color={runStatusColor[row.lastStatus]}>
|
||||
{row.lastStatus}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TaskCronModal
|
||||
onClose={closeCronModal}
|
||||
onSaved={() => void handleCronSaved()}
|
||||
open={cronModalOpen.value}
|
||||
task={cronTask.value}
|
||||
/>
|
||||
<TaskRunDrawer
|
||||
onClose={() => {
|
||||
runDrawerOpen.value = false;
|
||||
}}
|
||||
open={runDrawerOpen.value}
|
||||
task={runTask.value}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,126 @@
|
||||
/* @vitest-environment happy-dom */
|
||||
|
||||
/* eslint-disable vue/one-component-per-file */
|
||||
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { isSupportedAdminMenuName } from '#/api/core/menu';
|
||||
import routes from '#/router/routes/modules/qqbot';
|
||||
|
||||
import QqBotPluginTaskList from './list';
|
||||
|
||||
vi.mock('#/api/request', () => ({
|
||||
requestClient: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: defineComponent({
|
||||
name: 'MockPage',
|
||||
setup(_, { slots }) {
|
||||
return () => h('main', slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('antdv-next', () => ({
|
||||
Alert: defineComponent({
|
||||
name: 'MockAlert',
|
||||
setup() {
|
||||
return () => h('div');
|
||||
},
|
||||
}),
|
||||
Drawer: defineComponent({
|
||||
name: 'MockDrawer',
|
||||
setup(_, { slots }) {
|
||||
return () => h('aside', slots.default?.());
|
||||
},
|
||||
}),
|
||||
Input: defineComponent({
|
||||
name: 'MockInput',
|
||||
setup() {
|
||||
return () => h('input');
|
||||
},
|
||||
}),
|
||||
Modal: defineComponent({
|
||||
name: 'MockModal',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
RadioButton: defineComponent({
|
||||
name: 'MockRadioButton',
|
||||
setup(_, { slots }) {
|
||||
return () => h('button', slots.default?.());
|
||||
},
|
||||
}),
|
||||
RadioGroup: defineComponent({
|
||||
name: 'MockRadioGroup',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
Space: defineComponent({
|
||||
name: 'MockSpace',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
Tag: defineComponent({
|
||||
name: 'MockTag',
|
||||
setup(_, { slots }) {
|
||||
return () => h('span', slots.default?.());
|
||||
},
|
||||
}),
|
||||
message: {
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/components/ktTable', () => ({
|
||||
KtTable: defineComponent({
|
||||
name: 'KtTable',
|
||||
setup() {
|
||||
return () => h('section', { 'data-testid': 'plugin-task-table' });
|
||||
},
|
||||
}),
|
||||
useKtTable: vi.fn(() => [vi.fn(), { reload: vi.fn() }]),
|
||||
}));
|
||||
|
||||
vi.mock('#/api/qqbot/plugin-task', () => ({
|
||||
disableQqbotPluginTask: vi.fn(),
|
||||
enableQqbotPluginTask: vi.fn(),
|
||||
getQqbotPluginTaskPage: vi.fn(async () => ({ list: [], total: 0 })),
|
||||
getQqbotPluginTaskRunPage: vi.fn(async () => ({ list: [], total: 0 })),
|
||||
runQqbotPluginTaskOnce: vi.fn(),
|
||||
updateQqbotPluginTaskCron: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('qqbot plugin task page', () => {
|
||||
it('renders a single route root and task table shell', () => {
|
||||
const wrapper = mount(QqBotPluginTaskList);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.element.nodeType).toBe(Node.ELEMENT_NODE);
|
||||
expect(wrapper.find('[data-testid="plugin-task-table"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('registers a supported QQBot plugin task route', () => {
|
||||
const qqbotRoute = routes.find((route) => route.name === 'QqBot');
|
||||
expect(qqbotRoute?.children).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'QqBotPluginTask',
|
||||
path: '/qqbot/plugin-task',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(isSupportedAdminMenuName('QqBotPluginTask')).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -105,6 +105,9 @@ catalogs:
|
||||
'@vitejs/plugin-vue-jsx':
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4
|
||||
'@vue-js-cron/core':
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0
|
||||
'@vue/shared':
|
||||
specifier: ^3.5.27
|
||||
version: 3.5.27
|
||||
@ -581,6 +584,9 @@ importers:
|
||||
'@vben/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
'@vue-js-cron/core':
|
||||
specifier: 'catalog:'
|
||||
version: 6.3.0
|
||||
'@vueuse/core':
|
||||
specifier: 'catalog:'
|
||||
version: 14.2.0(vue@3.5.27(typescript@5.9.3))
|
||||
@ -4149,6 +4155,9 @@ packages:
|
||||
'@volar/typescript@2.4.28':
|
||||
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
||||
|
||||
'@vue-js-cron/core@6.3.0':
|
||||
resolution: {integrity: sha512-XhEJ77vcvbgg1TQP3eluHHrLM1ouM6S6SJEJMTV2vP0clKtKwiM0s79HcWIYzDvQWLGLtl3hFJlWdxqhp3LNqA==}
|
||||
|
||||
'@vue/babel-helper-vue-transform-on@1.5.0':
|
||||
resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==}
|
||||
|
||||
@ -6806,6 +6815,10 @@ packages:
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
@ -12623,6 +12636,10 @@ snapshots:
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.1.0
|
||||
|
||||
'@vue-js-cron/core@6.3.0':
|
||||
dependencies:
|
||||
mustache: 4.2.0
|
||||
|
||||
'@vue/babel-helper-vue-transform-on@1.5.0': {}
|
||||
|
||||
'@vue/babel-helper-vue-transform-on@2.0.1': {}
|
||||
@ -15712,6 +15729,8 @@ snapshots:
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mz@2.7.0:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
@ -51,6 +51,7 @@ catalog:
|
||||
'@types/qrcode': ^1.5.6
|
||||
'@types/qs': ^6.14.0
|
||||
'@types/sortablejs': ^1.15.9
|
||||
'@vue-js-cron/core': ^6.3.0
|
||||
'@typescript-eslint/eslint-plugin': ^8.54.0
|
||||
'@typescript-eslint/parser': ^8.54.0
|
||||
'@vee-validate/zod': ^4.15.1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user