web-定时任务
This commit is contained in:
196
schoolNewsWeb/src/apis/crontab/index.ts
Normal file
196
schoolNewsWeb/src/apis/crontab/index.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* @description 定时任务相关API
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-25
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { api } from '@/apis/index';
|
||||||
|
import type { CrontabTask, CrontabLog, ResultDomain, PageParam } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务API服务
|
||||||
|
*/
|
||||||
|
export const crontabApi = {
|
||||||
|
baseUrl: '/crontab',
|
||||||
|
|
||||||
|
// ==================== 定时任务管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时任务
|
||||||
|
* @param task 任务对象
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async createTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新定时任务
|
||||||
|
* @param task 任务对象
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async updateTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.put<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除定时任务
|
||||||
|
* @param task 任务对象
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async deleteTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.delete<CrontabTask>(`${this.baseUrl}/task`, task);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询任务
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async getTaskById(taskId: string): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.get<CrontabTask>(`${this.baseUrl}/task/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询任务列表
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async getTaskList(filter?: Partial<CrontabTask>): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/list`, filter);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询任务列表
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async getTaskPage(filter?: Partial<CrontabTask>, pageParam?: PageParam): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/page`, {
|
||||||
|
filter,
|
||||||
|
pageParam: {
|
||||||
|
pageNumber: pageParam?.page || 1,
|
||||||
|
pageSize: pageParam?.size || 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时任务
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async startTask(taskId: string): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/start/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停定时任务
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async pauseTask(taskId: string): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/pause/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即执行一次任务
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabTask>>
|
||||||
|
*/
|
||||||
|
async executeTaskOnce(taskId: string): Promise<ResultDomain<CrontabTask>> {
|
||||||
|
const response = await api.post<CrontabTask>(`${this.baseUrl}/task/execute/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Cron表达式
|
||||||
|
* @param cronExpression Cron表达式
|
||||||
|
* @returns Promise<ResultDomain<string>>
|
||||||
|
*/
|
||||||
|
async validateCronExpression(cronExpression: string): Promise<ResultDomain<string>> {
|
||||||
|
const response = await api.get<string>(`${this.baseUrl}/task/validate`, {
|
||||||
|
cronExpression
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 定时任务日志 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据任务ID查询日志
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabLog>>
|
||||||
|
*/
|
||||||
|
async getLogsByTaskId(taskId: string): Promise<ResultDomain<CrontabLog>> {
|
||||||
|
const response = await api.get<CrontabLog>(`${this.baseUrl}/log/task/${taskId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询日志列表
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @returns Promise<ResultDomain<CrontabLog>>
|
||||||
|
*/
|
||||||
|
async getLogList(filter?: Partial<CrontabLog>): Promise<ResultDomain<CrontabLog>> {
|
||||||
|
const response = await api.post<CrontabLog>(`${this.baseUrl}/log/list`, filter);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询日志列表
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @returns Promise<ResultDomain<CrontabLog>>
|
||||||
|
*/
|
||||||
|
async getLogPage(filter?: Partial<CrontabLog>, pageParam?: PageParam): Promise<ResultDomain<CrontabLog>> {
|
||||||
|
const response = await api.post<CrontabLog>(`${this.baseUrl}/log/page`, {
|
||||||
|
filter,
|
||||||
|
pageParam: {
|
||||||
|
pageNumber: pageParam?.page || 1,
|
||||||
|
pageSize: pageParam?.size || 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询日志详情
|
||||||
|
* @param logId 日志ID
|
||||||
|
* @returns Promise<ResultDomain<CrontabLog>>
|
||||||
|
*/
|
||||||
|
async getLogById(logId: string): Promise<ResultDomain<CrontabLog>> {
|
||||||
|
const response = await api.get<CrontabLog>(`${this.baseUrl}/log/${logId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理指定天数之前的日志
|
||||||
|
* @param days 天数
|
||||||
|
* @returns Promise<ResultDomain<number>>
|
||||||
|
*/
|
||||||
|
async cleanLogs(days: number): Promise<ResultDomain<number>> {
|
||||||
|
const response = await api.delete<number>(`${this.baseUrl}/log/clean/${days}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除日志
|
||||||
|
* @param log 日志对象
|
||||||
|
* @returns Promise<ResultDomain<CrontabLog>>
|
||||||
|
*/
|
||||||
|
async deleteLog(log: CrontabLog): Promise<ResultDomain<CrontabLog>> {
|
||||||
|
const response = await api.delete<CrontabLog>(`${this.baseUrl}/log`, log);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
92
schoolNewsWeb/src/types/crontab/index.ts
Normal file
92
schoolNewsWeb/src/types/crontab/index.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* @description 定时任务相关类型定义
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-25
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseDTO } from '../base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务配置
|
||||||
|
*/
|
||||||
|
export interface CrontabTask extends BaseDTO {
|
||||||
|
/** 任务ID */
|
||||||
|
taskId?: string;
|
||||||
|
/** 任务名称 */
|
||||||
|
taskName?: string;
|
||||||
|
/** 任务分组 */
|
||||||
|
taskGroup?: string;
|
||||||
|
/** Bean名称 */
|
||||||
|
beanName?: string;
|
||||||
|
/** 方法名称 */
|
||||||
|
methodName?: string;
|
||||||
|
/** 方法参数 */
|
||||||
|
methodParams?: string;
|
||||||
|
/** Cron表达式 */
|
||||||
|
cronExpression?: string;
|
||||||
|
/** 任务状态(0:暂停 1:运行中) */
|
||||||
|
status?: number;
|
||||||
|
/** 任务描述 */
|
||||||
|
description?: string;
|
||||||
|
/** 是否允许并发执行(0:否 1:是) */
|
||||||
|
concurrent?: number;
|
||||||
|
/** 错过执行策略(1:立即执行 2:执行一次 3:放弃执行) */
|
||||||
|
misfirePolicy?: number;
|
||||||
|
/** 创建者 */
|
||||||
|
creator?: string;
|
||||||
|
/** 更新者 */
|
||||||
|
updater?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务执行日志
|
||||||
|
*/
|
||||||
|
export interface CrontabLog extends BaseDTO {
|
||||||
|
/** 任务ID */
|
||||||
|
taskId?: string;
|
||||||
|
/** 任务名称 */
|
||||||
|
taskName?: string;
|
||||||
|
/** 任务分组 */
|
||||||
|
taskGroup?: string;
|
||||||
|
/** Bean名称 */
|
||||||
|
beanName?: string;
|
||||||
|
/** 方法名称 */
|
||||||
|
methodName?: string;
|
||||||
|
/** 方法参数 */
|
||||||
|
methodParams?: string;
|
||||||
|
/** 执行状态(0:失败 1:成功) */
|
||||||
|
executeStatus?: number;
|
||||||
|
/** 执行结果信息 */
|
||||||
|
executeMessage?: string;
|
||||||
|
/** 异常信息 */
|
||||||
|
exceptionInfo?: string;
|
||||||
|
/** 开始时间 */
|
||||||
|
startTime?: string;
|
||||||
|
/** 结束时间 */
|
||||||
|
endTime?: string;
|
||||||
|
/** 执行时长(毫秒) */
|
||||||
|
executeDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻爬虫配置
|
||||||
|
*/
|
||||||
|
export interface NewsCrawlerConfig {
|
||||||
|
/** 配置ID */
|
||||||
|
id?: string;
|
||||||
|
/** 爬虫名称 */
|
||||||
|
name?: string;
|
||||||
|
/** 目标URL */
|
||||||
|
targetUrl?: string;
|
||||||
|
/** 爬取规则 */
|
||||||
|
crawlRule?: string;
|
||||||
|
/** 是否启用 */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Cron表达式 */
|
||||||
|
cronExpression?: string;
|
||||||
|
/** 最后执行时间 */
|
||||||
|
lastExecuteTime?: string;
|
||||||
|
/** 状态 */
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -45,6 +45,9 @@ export * from './ai';
|
|||||||
// 用户中心相关
|
// 用户中心相关
|
||||||
export * from './usercenter';
|
export * from './usercenter';
|
||||||
|
|
||||||
|
// 定时任务相关
|
||||||
|
export * from './crontab';
|
||||||
|
|
||||||
// 枚举类型
|
// 枚举类型
|
||||||
export * from './enums';
|
export * from './enums';
|
||||||
export * from './enums/achievement-enums';
|
export * from './enums/achievement-enums';
|
||||||
|
|||||||
@@ -0,0 +1,505 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-management">
|
||||||
|
<div class="header">
|
||||||
|
<h2>执行日志</h2>
|
||||||
|
<el-button type="danger" @click="handleCleanLogs">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
清理日志
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区域 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">任务名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskName"
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">任务分组</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskGroup"
|
||||||
|
placeholder="请输入任务分组"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">执行状态</span>
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.executeStatus"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="成功" :value="1" />
|
||||||
|
<el-option label="失败" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<el-table
|
||||||
|
:data="logList"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
>
|
||||||
|
<el-table-column prop="taskName" label="任务名称" min-width="150" />
|
||||||
|
<el-table-column prop="taskGroup" label="任务分组" width="120" />
|
||||||
|
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
|
||||||
|
<el-table-column prop="methodName" label="方法名称" width="120" />
|
||||||
|
<el-table-column label="执行状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.executeStatus === 1 ? '成功' : '失败' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="executeDuration" label="执行时长" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.executeDuration }}ms
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="startTime" label="开始时间" width="180" />
|
||||||
|
<el-table-column prop="endTime" label="结束时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleViewDetail(row)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageParam.page"
|
||||||
|
v-model:page-size="pageParam.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="执行日志详情"
|
||||||
|
width="700px"
|
||||||
|
>
|
||||||
|
<div class="detail-content" v-if="currentLog">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">任务名称:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.taskName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">任务分组:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.taskGroup }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">Bean名称:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.beanName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">方法名称:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.methodName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item" v-if="currentLog.methodParams">
|
||||||
|
<span class="detail-label">方法参数:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.methodParams }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">执行状态:</span>
|
||||||
|
<el-tag :type="currentLog.executeStatus === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ currentLog.executeStatus === 1 ? '成功' : '失败' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">执行时长:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.executeDuration }}ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">开始时间:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.startTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">结束时间:</span>
|
||||||
|
<span class="detail-value">{{ currentLog.endTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item" v-if="currentLog.executeMessage">
|
||||||
|
<span class="detail-label">执行结果:</span>
|
||||||
|
<div class="detail-message">{{ currentLog.executeMessage }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item" v-if="currentLog.exceptionInfo">
|
||||||
|
<span class="detail-label">异常信息:</span>
|
||||||
|
<div class="detail-exception">{{ currentLog.exceptionInfo }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 清理日志对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="cleanDialogVisible"
|
||||||
|
title="清理日志"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
<div class="clean-dialog-content">
|
||||||
|
<el-alert
|
||||||
|
title="清理操作不可恢复,请谨慎操作!"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<div class="clean-item">
|
||||||
|
<span class="clean-label">保留天数:</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="cleanDays"
|
||||||
|
:min="1"
|
||||||
|
:max="365"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<span class="clean-tip">天</span>
|
||||||
|
</div>
|
||||||
|
<div class="clean-desc">
|
||||||
|
将删除 {{ cleanDays }} 天前的所有执行日志
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="cleanDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
@click="handleConfirmClean"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
确定清理
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Delete, Search, Refresh } from '@element-plus/icons-vue';
|
||||||
|
import { crontabApi } from '@/apis/crontab';
|
||||||
|
import type { CrontabLog, PageParam } from '@/types';
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const logList = ref<CrontabLog[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const currentLog = ref<CrontabLog | null>(null);
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: '',
|
||||||
|
executeStatus: undefined as number | undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pageParam = reactive<PageParam>({
|
||||||
|
page: 1,
|
||||||
|
size: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const detailDialogVisible = ref(false);
|
||||||
|
const cleanDialogVisible = ref(false);
|
||||||
|
const cleanDays = ref(30);
|
||||||
|
|
||||||
|
// 加载日志列表
|
||||||
|
const loadLogList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const filter: Partial<CrontabLog> = {};
|
||||||
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||||
|
if (searchForm.taskGroup) filter.taskGroup = searchForm.taskGroup;
|
||||||
|
if (searchForm.executeStatus !== undefined) filter.executeStatus = searchForm.executeStatus;
|
||||||
|
|
||||||
|
const result = await crontabApi.getLogPage(filter, pageParam);
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
logList.value = result.dataList;
|
||||||
|
total.value = result.pageParam?.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载日志列表失败');
|
||||||
|
logList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载日志列表失败:', error);
|
||||||
|
ElMessage.error('加载日志列表失败');
|
||||||
|
logList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadLogList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.taskName = '';
|
||||||
|
searchForm.taskGroup = '';
|
||||||
|
searchForm.executeStatus = undefined;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadLogList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
pageParam.page = page;
|
||||||
|
loadLogList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageParam.size = size;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadLogList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewDetail = async (row: CrontabLog) => {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.getLogById(row.id!);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
currentLog.value = result.data;
|
||||||
|
detailDialogVisible.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '获取详情失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取日志详情失败:', error);
|
||||||
|
ElMessage.error('获取日志详情失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除日志
|
||||||
|
const handleDelete = async (row: CrontabLog) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要删除这条日志吗?',
|
||||||
|
'删除确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.deleteLog(row);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
loadLogList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除日志失败:', error);
|
||||||
|
ElMessage.error('删除日志失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理日志
|
||||||
|
const handleCleanLogs = () => {
|
||||||
|
cleanDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认清理
|
||||||
|
const handleConfirmClean = async () => {
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.cleanLogs(cleanDays.value);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(`已清理 ${result.data || 0} 条日志`);
|
||||||
|
cleanDialogVisible.value = false;
|
||||||
|
loadLogList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '清理失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理日志失败:', error);
|
||||||
|
ElMessage.error('清理日志失败');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadLogList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.log-management {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #141F38;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.search-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.search-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
min-width: 100px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message,
|
||||||
|
.detail-exception {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-exception {
|
||||||
|
background-color: #fef0f0;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-dialog-content {
|
||||||
|
.clean-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.clean-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clean-desc {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
706
schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue
Normal file
706
schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
<template>
|
||||||
|
<div class="news-crawler">
|
||||||
|
<div class="header">
|
||||||
|
<h2>新闻爬虫配置</h2>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增爬虫
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说明卡片 -->
|
||||||
|
<el-alert
|
||||||
|
title="新闻爬虫说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
<p>新闻爬虫配置允许系统自动从指定的新闻源抓取最新新闻内容。</p>
|
||||||
|
<p>配置完成后,系统会按照设定的Cron表达式定时执行爬取任务。</p>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区域 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">爬虫名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskName"
|
||||||
|
placeholder="请输入爬虫名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">状态</span>
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="运行中" :value="1" />
|
||||||
|
<el-option label="已暂停" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 爬虫配置列表 -->
|
||||||
|
<div class="crawler-list">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8" v-for="crawler in crawlerList" :key="crawler.taskId">
|
||||||
|
<el-card class="crawler-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<el-icon class="title-icon"><DocumentCopy /></el-icon>
|
||||||
|
<span>{{ crawler.taskName }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag
|
||||||
|
:type="crawler.status === 1 ? 'success' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ crawler.status === 1 ? '运行中' : '已暂停' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Bean名称:</span>
|
||||||
|
<span class="info-value">{{ crawler.beanName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方法名称:</span>
|
||||||
|
<span class="info-value">{{ crawler.methodName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">执行周期:</span>
|
||||||
|
<span class="info-value">{{ crawler.cronExpression }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item" v-if="crawler.description">
|
||||||
|
<span class="info-label">描述:</span>
|
||||||
|
<span class="info-value">{{ crawler.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="crawler.status === 0"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="handleStart(crawler)"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
启动
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handlePause(crawler)"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPause /></el-icon>
|
||||||
|
暂停
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleExecute(crawler)"
|
||||||
|
>
|
||||||
|
<el-icon><Promotion /></el-icon>
|
||||||
|
执行一次
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(crawler)"
|
||||||
|
>
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(crawler)"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<el-empty
|
||||||
|
v-if="!loading && crawlerList.length === 0"
|
||||||
|
description="暂无爬虫配置"
|
||||||
|
style="margin-top: 40px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageParam.page"
|
||||||
|
v-model:page-size="pageParam.size"
|
||||||
|
:page-sizes="[9, 18, 36]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑爬虫' : '新增爬虫'"
|
||||||
|
width="700px"
|
||||||
|
@close="resetForm"
|
||||||
|
>
|
||||||
|
<div class="form-content">
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">爬虫名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.taskName"
|
||||||
|
placeholder="请输入爬虫名称"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">Bean名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.beanName"
|
||||||
|
placeholder="请输入Spring Bean名称(如:newsCrawlerTask)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">方法名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.methodName"
|
||||||
|
placeholder="请输入要执行的方法名(如:crawlNews)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">方法参数</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.methodParams"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入方法参数(JSON格式,可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<span class="form-tip">
|
||||||
|
示例:{"source":"xinhua","category":"education"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">Cron表达式</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.cronExpression"
|
||||||
|
placeholder="请输入Cron表达式"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="validateCron">验证</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<span class="form-tip">
|
||||||
|
常用示例:<br/>
|
||||||
|
- 0 0 */6 * * ? (每6小时执行一次)<br/>
|
||||||
|
- 0 0 8,12,18 * * ? (每天8点、12点、18点执行)<br/>
|
||||||
|
- 0 0/30 * * * ? (每30分钟执行一次)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">爬虫描述</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入爬虫描述"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">是否允许并发</span>
|
||||||
|
<el-radio-group v-model="formData.concurrent">
|
||||||
|
<el-radio :label="1">允许</el-radio>
|
||||||
|
<el-radio :label="0">禁止</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<span class="form-tip">
|
||||||
|
建议禁止并发,避免重复抓取
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import {
|
||||||
|
Plus, Search, Refresh, DocumentCopy, VideoPlay,
|
||||||
|
VideoPause, Promotion, Edit, Delete
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { crontabApi } from '@/apis/crontab';
|
||||||
|
import type { CrontabTask, PageParam } from '@/types';
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const crawlerList = ref<CrontabTask[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
taskName: '',
|
||||||
|
status: undefined as number | undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pageParam = reactive<PageParam>({
|
||||||
|
page: 1,
|
||||||
|
size: 9
|
||||||
|
});
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive<Partial<CrontabTask>>({
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: 'NEWS_CRAWLER',
|
||||||
|
beanName: '',
|
||||||
|
methodName: '',
|
||||||
|
methodParams: '',
|
||||||
|
cronExpression: '',
|
||||||
|
status: 0,
|
||||||
|
concurrent: 0,
|
||||||
|
misfirePolicy: 3,
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载爬虫列表
|
||||||
|
const loadCrawlerList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const filter: Partial<CrontabTask> = {
|
||||||
|
taskGroup: 'NEWS_CRAWLER'
|
||||||
|
};
|
||||||
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||||
|
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||||
|
|
||||||
|
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
crawlerList.value = result.dataList;
|
||||||
|
total.value = result.pageParam?.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载爬虫列表失败');
|
||||||
|
crawlerList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载爬虫列表失败:', error);
|
||||||
|
ElMessage.error('加载爬虫列表失败');
|
||||||
|
crawlerList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadCrawlerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.taskName = '';
|
||||||
|
searchForm.status = undefined;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadCrawlerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
pageParam.page = page;
|
||||||
|
loadCrawlerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageParam.size = size;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadCrawlerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增爬虫
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false;
|
||||||
|
resetFormData();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑爬虫
|
||||||
|
const handleEdit = (row: CrontabTask) => {
|
||||||
|
isEdit.value = true;
|
||||||
|
Object.assign(formData, row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动爬虫
|
||||||
|
const handleStart = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.startTask(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('爬虫已启动');
|
||||||
|
loadCrawlerList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '启动失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动爬虫失败:', error);
|
||||||
|
ElMessage.error('启动爬虫失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暂停爬虫
|
||||||
|
const handlePause = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.pauseTask(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('爬虫已暂停');
|
||||||
|
loadCrawlerList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '暂停失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('暂停爬虫失败:', error);
|
||||||
|
ElMessage.error('暂停爬虫失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行一次
|
||||||
|
const handleExecute = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定立即执行爬虫"${row.taskName}"吗?`,
|
||||||
|
'确认执行',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.executeTaskOnce(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('爬虫执行成功,请稍后查看执行日志');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '执行失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('执行爬虫失败:', error);
|
||||||
|
ElMessage.error('执行爬虫失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除爬虫
|
||||||
|
const handleDelete = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除爬虫"${row.taskName}"吗?`,
|
||||||
|
'删除确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.deleteTask(row);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
loadCrawlerList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除爬虫失败:', error);
|
||||||
|
ElMessage.error('删除爬虫失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证Cron表达式
|
||||||
|
const validateCron = async () => {
|
||||||
|
if (!formData.cronExpression) {
|
||||||
|
ElMessage.warning('请输入Cron表达式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.validateCronExpression(formData.cronExpression);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('Cron表达式验证通过');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Cron表达式格式错误');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证Cron表达式失败:', error);
|
||||||
|
ElMessage.error('验证失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!formData.taskName) {
|
||||||
|
ElMessage.warning('请输入爬虫名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.beanName) {
|
||||||
|
ElMessage.warning('请输入Bean名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.methodName) {
|
||||||
|
ElMessage.warning('请输入方法名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.cronExpression) {
|
||||||
|
ElMessage.warning('请输入Cron表达式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
...formData,
|
||||||
|
taskGroup: 'NEWS_CRAWLER'
|
||||||
|
};
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
result = await crontabApi.updateTask(data as CrontabTask);
|
||||||
|
} else {
|
||||||
|
result = await crontabApi.createTask(data as CrontabTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
loadCrawlerList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
ElMessage.error('提交失败');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
resetFormData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单数据
|
||||||
|
const resetFormData = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: 'NEWS_CRAWLER',
|
||||||
|
beanName: '',
|
||||||
|
methodName: '',
|
||||||
|
methodParams: '',
|
||||||
|
cronExpression: '',
|
||||||
|
status: 0,
|
||||||
|
concurrent: 0,
|
||||||
|
misfirePolicy: 3,
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadCrawlerList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.news-crawler {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #141F38;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.search-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.search-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawler-list {
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
|
.crawler-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
min-width: 80px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.required::before {
|
||||||
|
content: '*';
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
<template>
|
||||||
|
<div class="task-management">
|
||||||
|
<div class="header">
|
||||||
|
<h2>定时任务管理</h2>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增任务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区域 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">任务名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskName"
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">任务分组</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskGroup"
|
||||||
|
placeholder="请输入任务分组"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">状态</span>
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="运行中" :value="1" />
|
||||||
|
<el-option label="已暂停" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<el-table
|
||||||
|
:data="taskList"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
>
|
||||||
|
<el-table-column prop="taskName" label="任务名称" min-width="150" />
|
||||||
|
<el-table-column prop="taskGroup" label="任务分组" width="120" />
|
||||||
|
<el-table-column prop="beanName" label="Bean名称" min-width="150" />
|
||||||
|
<el-table-column prop="methodName" label="方法名称" width="120" />
|
||||||
|
<el-table-column prop="cronExpression" label="Cron表达式" min-width="150" />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.status === 1 ? '运行中' : '已暂停' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="并发" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.concurrent === 1 ? 'success' : 'warning'" size="small">
|
||||||
|
{{ row.concurrent === 1 ? '允许' : '禁止' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="300" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 0"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="handleStart(row)"
|
||||||
|
>
|
||||||
|
启动
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handlePause(row)"
|
||||||
|
>
|
||||||
|
暂停
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleExecute(row)"
|
||||||
|
>
|
||||||
|
执行一次
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageParam.page"
|
||||||
|
v-model:page-size="pageParam.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑任务' : '新增任务'"
|
||||||
|
width="700px"
|
||||||
|
@close="resetForm"
|
||||||
|
>
|
||||||
|
<div class="form-content">
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">任务名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.taskName"
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">任务分组</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.taskGroup"
|
||||||
|
placeholder="请输入任务分组(如:SYSTEM、BUSINESS)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">Bean名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.beanName"
|
||||||
|
placeholder="请输入Spring Bean名称"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">方法名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.methodName"
|
||||||
|
placeholder="请输入要执行的方法名"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">方法参数</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.methodParams"
|
||||||
|
placeholder="请输入方法参数(JSON格式,可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label required">Cron表达式</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.cronExpression"
|
||||||
|
placeholder="请输入Cron表达式(如:0 0 2 * * ?)"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="validateCron">验证</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<span class="form-tip">
|
||||||
|
格式:秒 分 时 日 月 周 年(年可选)。
|
||||||
|
示例:0 0 2 * * ? 表示每天凌晨2点执行
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">是否允许并发</span>
|
||||||
|
<el-radio-group v-model="formData.concurrent">
|
||||||
|
<el-radio :label="1">允许</el-radio>
|
||||||
|
<el-radio :label="0">禁止</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">错过执行策略</span>
|
||||||
|
<el-select v-model="formData.misfirePolicy" placeholder="请选择策略">
|
||||||
|
<el-option label="立即执行" :value="1" />
|
||||||
|
<el-option label="执行一次" :value="2" />
|
||||||
|
<el-option label="放弃执行" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<span class="form-label">任务描述</span>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入任务描述"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:loading="submitting"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Search, Refresh } from '@element-plus/icons-vue';
|
||||||
|
import { crontabApi } from '@/apis/crontab';
|
||||||
|
import type { CrontabTask, PageParam } from '@/types';
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const taskList = ref<CrontabTask[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: '',
|
||||||
|
status: undefined as number | undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pageParam = reactive<PageParam>({
|
||||||
|
page: 1,
|
||||||
|
size: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
// 对话框状态
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive<Partial<CrontabTask>>({
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: 'DEFAULT',
|
||||||
|
beanName: '',
|
||||||
|
methodName: '',
|
||||||
|
methodParams: '',
|
||||||
|
cronExpression: '',
|
||||||
|
status: 0,
|
||||||
|
concurrent: 0,
|
||||||
|
misfirePolicy: 2,
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载任务列表
|
||||||
|
const loadTaskList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const filter: Partial<CrontabTask> = {};
|
||||||
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||||
|
if (searchForm.taskGroup) filter.taskGroup = searchForm.taskGroup;
|
||||||
|
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||||
|
|
||||||
|
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
taskList.value = result.dataList;
|
||||||
|
total.value = result.pageParam?.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载任务列表失败');
|
||||||
|
taskList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务列表失败:', error);
|
||||||
|
ElMessage.error('加载任务列表失败');
|
||||||
|
taskList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadTaskList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.taskName = '';
|
||||||
|
searchForm.taskGroup = '';
|
||||||
|
searchForm.status = undefined;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadTaskList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
pageParam.page = page;
|
||||||
|
loadTaskList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageParam.size = size;
|
||||||
|
pageParam.page = 1;
|
||||||
|
loadTaskList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增任务
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false;
|
||||||
|
resetFormData();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑任务
|
||||||
|
const handleEdit = (row: CrontabTask) => {
|
||||||
|
isEdit.value = true;
|
||||||
|
Object.assign(formData, row);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动任务
|
||||||
|
const handleStart = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.startTask(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('任务已启动');
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '启动失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动任务失败:', error);
|
||||||
|
ElMessage.error('启动任务失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暂停任务
|
||||||
|
const handlePause = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.pauseTask(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('任务已暂停');
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '暂停失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('暂停任务失败:', error);
|
||||||
|
ElMessage.error('暂停任务失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行一次
|
||||||
|
const handleExecute = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定立即执行任务"${row.taskName}"吗?`,
|
||||||
|
'确认执行',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.executeTaskOnce(row.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('任务执行成功');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '执行失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('执行任务失败:', error);
|
||||||
|
ElMessage.error('执行任务失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
const handleDelete = async (row: CrontabTask) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除任务"${row.taskName}"吗?`,
|
||||||
|
'删除确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.deleteTask(row);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除任务失败:', error);
|
||||||
|
ElMessage.error('删除任务失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证Cron表达式
|
||||||
|
const validateCron = async () => {
|
||||||
|
if (!formData.cronExpression) {
|
||||||
|
ElMessage.warning('请输入Cron表达式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.validateCronExpression(formData.cronExpression);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('Cron表达式验证通过');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || 'Cron表达式格式错误');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证Cron表达式失败:', error);
|
||||||
|
ElMessage.error('验证失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!formData.taskName) {
|
||||||
|
ElMessage.warning('请输入任务名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.taskGroup) {
|
||||||
|
ElMessage.warning('请输入任务分组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.beanName) {
|
||||||
|
ElMessage.warning('请输入Bean名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.methodName) {
|
||||||
|
ElMessage.warning('请输入方法名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.cronExpression) {
|
||||||
|
ElMessage.warning('请输入Cron表达式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const data = { ...formData };
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
result = await crontabApi.updateTask(data as CrontabTask);
|
||||||
|
} else {
|
||||||
|
result = await crontabApi.createTask(data as CrontabTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
ElMessage.error('提交失败');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
resetFormData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单数据
|
||||||
|
const resetFormData = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
taskName: '',
|
||||||
|
taskGroup: 'DEFAULT',
|
||||||
|
beanName: '',
|
||||||
|
methodName: '',
|
||||||
|
methodParams: '',
|
||||||
|
cronExpression: '',
|
||||||
|
status: 0,
|
||||||
|
concurrent: 0,
|
||||||
|
misfirePolicy: 2,
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadTaskList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.task-management {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #141F38;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.search-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.search-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.required::before {
|
||||||
|
content: '*';
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user