diff --git a/apps/web-antdv-next/package.json b/apps/web-antdv-next/package.json index 310d1fb..00a37a8 100644 --- a/apps/web-antdv-next/package.json +++ b/apps/web-antdv-next/package.json @@ -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:", diff --git a/apps/web-antdv-next/src/api/core/menu.ts b/apps/web-antdv-next/src/api/core/menu.ts index 0b708e1..03329d9 100644 --- a/apps/web-antdv-next/src/api/core/menu.ts +++ b/apps/web-antdv-next/src/api/core/menu.ts @@ -46,6 +46,12 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([ 'QqBotPermissionDelete', 'QqBotPermissionEdit', 'QqBotPlugin', + 'QqBotPluginTask', + 'QqBotPluginTaskDisable', + 'QqBotPluginTaskEnable', + 'QqBotPluginTaskRun', + 'QqBotPluginTaskRunLog', + 'QqBotPluginTaskUpdateCron', 'QqBotRule', 'QqBotRuleCreate', 'QqBotRuleDelete', diff --git a/apps/web-antdv-next/src/api/qqbot/plugin-task.spec.ts b/apps/web-antdv-next/src/api/qqbot/plugin-task.spec.ts new file mode 100644 index 0000000..76a566c --- /dev/null +++ b/apps/web-antdv-next/src/api/qqbot/plugin-task.spec.ts @@ -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 }, + }, + ); + }); +}); diff --git a/apps/web-antdv-next/src/api/qqbot/plugin-task.ts b/apps/web-antdv-next/src/api/qqbot/plugin-task.ts new file mode 100644 index 0000000..6fde059 --- /dev/null +++ b/apps/web-antdv-next/src/api/qqbot/plugin-task.ts @@ -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; + startedAt?: null | string; + status: RunStatus; + taskId: string; + taskKey: string; + triggerType: TriggerType; + } + + export interface TaskQuery extends Recordable { + enabled?: boolean; + pageNo?: number; + pageSize?: number; + pluginId?: string; + pluginKey?: string; + status?: RuntimeStatus; + taskKey?: string; + } + + export interface TaskRunQuery extends Recordable { + pageNo?: number; + pageSize?: number; + status?: RunStatus; + triggerType?: TriggerType; + } +} + +export function getQqbotPluginTaskPage(params: QqbotPluginTaskApi.TaskQuery) { + return requestClient.get>( + '/qqbot/plugin-platform/tasks/page', + { params }, + ); +} + +export function enableQqbotPluginTask(id: string) { + return requestClient.post( + `/qqbot/plugin-platform/tasks/${id}/enable`, + ); +} + +export function disableQqbotPluginTask(id: string) { + return requestClient.post( + `/qqbot/plugin-platform/tasks/${id}/disable`, + ); +} + +export function updateQqbotPluginTaskCron(id: string, cronExpression: string) { + return requestClient.post( + `/qqbot/plugin-platform/tasks/${id}/cron`, + { cronExpression }, + ); +} + +export function runQqbotPluginTaskOnce( + id: string, + input: Recordable = {}, +) { + 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>( + `/qqbot/plugin-platform/tasks/${id}/runs`, + { params }, + ); +} diff --git a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts index b890af4..2e05613 100644 --- a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts +++ b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts @@ -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: { diff --git a/apps/web-antdv-next/src/views/qqbot/plugin-task/components/CronEditorAntdvNext.tsx b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/CronEditorAntdvNext.tsx new file mode 100644 index 0000000..826c1ae --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/CronEditorAntdvNext.tsx @@ -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 () => ( + + updateValue(event.target.value)} + value={expression.value} + > + 每 6 小时 + 每天 03:00 + 每周一 03:00 + + updateValue(event.target.value)} + value={expression.value} + /> + {error.value ? ( + + ) : null} + + ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskCronModal.tsx b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskCronModal.tsx new file mode 100644 index 0000000..af8a4e6 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskCronModal.tsx @@ -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, + }, + }, + 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 () => ( + emit('close')} + onOk={() => void save()} + open={props.open} + title="修改 Cron" + width={620} + > + { + cronExpression.value = value; + }} + onValidChange={(value: boolean) => { + valid.value = value; + }} + value={cronExpression.value} + /> + + ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskRunDrawer.tsx b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskRunDrawer.tsx new file mode 100644 index 0000000..a47804c --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin-task/components/TaskRunDrawer.tsx @@ -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 = { + 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, + }, + }, + emits: ['close'], + setup(props, { emit }) { + const loading = ref(false); + const runs = ref([]); + + 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) => ( +
+
+ {item.status} + {item.triggerType} + {item.startedAt || item.createTime || '-'} + + {item.durationMs === null || item.durationMs === undefined + ? '-' + : `${item.durationMs} ms`} + +
+ {item.safeSummary ? ( +
+            {JSON.stringify(item.safeSummary, null, 2)}
+          
+ ) : null} + {item.errorMessage ? ( +
{item.errorMessage}
+ ) : null} +
+ ); + + return () => ( + emit('close')} + open={props.open} + size="large" + title={props.task?.taskName || '运行记录'} + > + {runs.value.length > 0 ? ( +
{runs.value.map((run) => renderRun(run))}
+ ) : ( + 暂无运行记录 + )} +
+ ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/plugin-task/list.tsx b/apps/web-antdv-next/src/views/qqbot/plugin-task/list.tsx new file mode 100644 index 0000000..ef6d547 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin-task/list.tsx @@ -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 = { + disabled: 'default', + failed: 'error', + idle: 'default', + running: 'processing', + scheduled: 'success', +}; + +const runStatusColor: Record = { + failed: 'error', + running: 'processing', + skipped: 'default', + success: 'success', +}; + +export default defineComponent({ + name: 'QqBotPluginTaskList', + setup() { + const cronModalOpen = ref(false); + const cronTask = ref(); + const runDrawerOpen = ref(false); + const runTask = ref(); + + const columns: Array> = [ + { 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 = { + list: async (params) => await getQqbotPluginTaskPage(params), + }; + const rowActions: Array> = [ + { + 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({ + 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 () => ( + + { + const row = record as QqbotPluginTaskApi.Task; + if (column.key === 'pluginName') { + return row.pluginName || row.pluginKey || row.pluginId || '-'; + } + if (column.key === 'enabled') { + return ( + + {row.enabled ? '启用' : '停用'} + + ); + } + if (column.key === 'runtimeStatus') { + return ( + + {row.runtimeStatus} + + ); + } + if (column.key === 'lastStatus') { + return row.lastStatus ? ( + + {row.lastStatus} + + ) : ( + '-' + ); + } + return undefined; + }, + }} + /> + void handleCronSaved()} + open={cronModalOpen.value} + task={cronTask.value} + /> + { + runDrawerOpen.value = false; + }} + open={runDrawerOpen.value} + task={runTask.value} + /> + + ); + }, +}); diff --git a/apps/web-antdv-next/src/views/qqbot/plugin-task/plugin-task.spec.tsx b/apps/web-antdv-next/src/views/qqbot/plugin-task/plugin-task.spec.tsx new file mode 100644 index 0000000..16037d2 --- /dev/null +++ b/apps/web-antdv-next/src/views/qqbot/plugin-task/plugin-task.spec.tsx @@ -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); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 923da6b..40062db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1ec3221..2cd463d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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