feat(api): 添加任务创建和执行功能

This commit is contained in:
2025-08-13 15:54:10 +08:00
parent 9fb25a2d33
commit 7d728cb3ae
339 changed files with 676 additions and 12 deletions

View File

@@ -1,12 +1,627 @@
<template>
<div class="page-container">
<h1>调度中心</h1>
<p>这里是调度中心内容的占位符</p>
<div class="scheduler-container">
<!-- 顶部操作栏 -->
<div class="header-actions">
<a-button type="primary" @click="showAddTaskModal" :icon="h(PlusOutlined)">
添加任务
</a-button>
</div>
<!-- 已创建的任务显示区域 -->
<div v-if="createdTask" class="created-task-section">
<a-card size="small" class="task-card">
<div class="task-info">
<div class="task-details">
<h4>{{ createdTask.taskName }}</h4>
<p class="task-meta">
<span>WebSocket ID: {{ createdTask.websocketId }}</span>
<span>执行模式: {{ createdTask.mode }}</span>
</p>
</div>
<a-button type="primary" @click="startCreatedTask" :icon="h(PlayCircleOutlined)">
开始执行
</a-button>
</div>
</a-card>
</div>
<!-- 任务执行区域 -->
<div class="execution-area">
<div v-if="runningTasks.length === 0" class="empty-state">
<a-empty description="暂无执行中的任务" />
</div>
<div v-else class="task-panels">
<a-collapse v-model:activeKey="activeTaskPanels" ghost>
<a-collapse-panel v-for="task in runningTasks" :key="task.websocketId"
:header="`任务: ${task.taskName} (${task.websocketId})`">
<template #extra>
<a-tag :color="getTaskStatusColor(task.status)">
{{ task.status }}
</a-tag>
<a-button type="text" size="small" danger @click.stop="stopTask(task.websocketId)" :icon="h(StopOutlined)">
停止
</a-button>
</template>
<div class="task-output">
<div class="output-header">
<span>输出日志</span>
<a-button type="text" size="small" @click="clearTaskOutput(task.websocketId)" :icon="h(ClearOutlined)">
清空
</a-button>
</div>
<div class="output-content" ref="outputRefs">
<div v-for="(log, index) in task.logs" :key="index" :class="['log-line', `log-${log.type}`]">
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
<!-- 添加任务弹窗 -->
<a-modal v-model:open="addTaskModalVisible" title="添加任务" @ok="addTask" @cancel="cancelAddTask"
:confirmLoading="addTaskLoading">
<a-form :model="taskForm" layout="vertical">
<a-form-item label="选择任务" required>
<a-select v-model:value="taskForm.taskId" placeholder="请选择要执行的任务" :loading="taskOptionsLoading"
:options="taskOptions" show-search :filter-option="filterTaskOption" />
</a-form-item>
<a-form-item label="执行模式" required>
<a-select v-model:value="taskForm.mode" placeholder="请选择执行模式">
<a-select-option value="自动代理">自动代理</a-select-option>
<a-select-option value="人工排查">人工排查</a-select-option>
<a-select-option value="设置脚本">设置脚本</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- 消息对话框 -->
<a-modal v-model:open="messageModalVisible" :title="currentMessage?.title || '系统消息'" @ok="sendMessageResponse"
@cancel="cancelMessage">
<div v-if="currentMessage">
<p>{{ currentMessage.content }}</p>
<a-input v-if="currentMessage.needInput" v-model:value="messageResponse" placeholder="请输入回复内容" />
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, h, nextTick } from 'vue'
import { message, notification } from 'ant-design-vue'
import {
PlusOutlined,
PlayCircleOutlined,
StopOutlined,
ClearOutlined
} from '@ant-design/icons-vue'
import { Service } from '@/api/services/Service'
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
// 响应式数据
const addTaskModalVisible = ref(false)
const messageModalVisible = ref(false)
const taskOptionsLoading = ref(false)
const addTaskLoading = ref(false)
const selectedTaskId = ref<string>('')
const activeTaskPanels = ref<string[]>([])
const outputRefs = ref<HTMLElement[]>([])
// 任务选项
const taskOptions = ref<ComboBoxItem[]>([])
// 任务表单
const taskForm = reactive({
taskId: '',
mode: '自动代理' as TaskCreateIn.mode
})
// 已创建的任务
interface CreatedTask {
websocketId: string
taskName: string
mode: string
originalTaskId: string
}
const createdTask = ref<CreatedTask | null>(null)
// 运行中的任务
interface RunningTask {
websocketId: string
taskName: string
status: string
websocket: WebSocket | null
logs: Array<{
time: string
message: string
type: 'info' | 'error' | 'warning' | 'success'
}>
}
const runningTasks = ref<RunningTask[]>([])
// 消息处理
interface TaskMessage {
title: string
content: string
needInput: boolean
messageId?: string
taskId?: string
}
const currentMessage = ref<TaskMessage | null>(null)
const messageResponse = ref('')
// 获取任务选项
const loadTaskOptions = async () => {
try {
taskOptionsLoading.value = true
const response = await Service.getTaskComboxApiInfoComboxTaskPost()
if (response.code === 200) {
taskOptions.value = response.data
} else {
message.error('获取任务列表失败')
}
} catch (error) {
console.error('获取任务列表失败:', error)
message.error('获取任务列表失败')
} finally {
taskOptionsLoading.value = false
}
}
// 显示添加任务弹窗
const showAddTaskModal = () => {
addTaskModalVisible.value = true
if (taskOptions.value.length === 0) {
loadTaskOptions()
}
}
// 添加任务
const addTask = async () => {
if (!taskForm.taskId || !taskForm.mode) {
message.error('请填写完整的任务信息')
return
}
try {
addTaskLoading.value = true
const response = await Service.addTaskApiDispatchStartPost({
taskId: taskForm.taskId,
mode: taskForm.mode
})
if (response.code === 200) {
// 查找任务名称
const selectedOption = taskOptions.value.find(option => option.value === taskForm.taskId)
const taskName = selectedOption?.label || '未知任务'
// 保存创建的任务信息
createdTask.value = {
websocketId: response.websocketId,
taskName,
mode: taskForm.mode,
originalTaskId: taskForm.taskId
}
message.success('任务创建成功')
addTaskModalVisible.value = false
// 重置表单
taskForm.taskId = ''
taskForm.mode = '自动代理'
} else {
message.error(response.message || '创建任务失败')
}
} catch (error) {
console.error('创建任务失败:', error)
message.error('创建任务失败')
} finally {
addTaskLoading.value = false
}
}
// 取消添加任务
const cancelAddTask = () => {
addTaskModalVisible.value = false
taskForm.taskId = ''
taskForm.mode = '自动代理'
}
// 开始已创建的任务
const startCreatedTask = () => {
if (!createdTask.value) {
message.error('没有可执行的任务')
return
}
// 创建任务对象
const task: RunningTask = {
websocketId: createdTask.value.websocketId,
taskName: createdTask.value.taskName,
status: '连接中',
websocket: null,
logs: []
}
// 添加到运行任务列表
runningTasks.value.push(task)
activeTaskPanels.value.push(task.websocketId)
// 连接WebSocket
connectWebSocket(task)
// 清空已创建的任务
createdTask.value = null
}
// 连接WebSocket
const connectWebSocket = (task: RunningTask) => {
const wsUrl = `ws://localhost:8000/api/dispatch/ws/${task.websocketId}`
try {
const ws = new WebSocket(wsUrl)
task.websocket = ws
ws.onopen = () => {
task.status = '运行中'
addTaskLog(task, '已连接到任务服务器', 'success')
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
handleWebSocketMessage(task, data)
} catch (error) {
console.error('解析WebSocket消息失败:', error)
addTaskLog(task, `收到无效消息: ${event.data}`, 'error')
}
}
ws.onclose = () => {
task.status = '已断开'
addTaskLog(task, '与服务器连接已断开', 'warning')
task.websocket = null
}
ws.onerror = (error) => {
task.status = '连接错误'
addTaskLog(task, '连接发生错误', 'error')
console.error('WebSocket错误:', error)
}
} catch (error) {
task.status = '连接失败'
addTaskLog(task, '无法连接到服务器', 'error')
console.error('WebSocket连接失败:', error)
}
}
// 处理WebSocket消息
const handleWebSocketMessage = (task: RunningTask, data: any) => {
switch (data.type) {
case 'Update':
// 界面更新信息
if (data.data) {
for (const [key, value] of Object.entries(data.data)) {
addTaskLog(task, `${key}: ${value}`, 'info')
}
}
break
case 'Message':
// 需要用户输入的消息
currentMessage.value = {
title: '任务消息',
content: data.message || '任务需要您的输入',
needInput: true,
messageId: data.messageId,
taskId: task.websocketId
}
messageModalVisible.value = true
break
case 'Info':
// 通知信息
const level = data.key || 'info'
const content = data.val || data.message || '未知通知'
addTaskLog(task, content, level as any)
// 显示系统通知
if (level === 'error') {
notification.error({ message: '任务错误', description: content })
} else if (level === 'warning') {
notification.warning({ message: '任务警告', description: content })
} else if (level === 'success') {
notification.success({ message: '任务成功', description: content })
} else {
notification.info({ message: '任务信息', description: content })
}
break
case 'Signal':
// 状态信号
if (data.data?.Accomplish !== undefined) {
task.status = data.data.Accomplish ? '已完成' : '已失败'
addTaskLog(task, `任务${task.status}`, data.data.Accomplish ? 'success' : 'error')
// 断开连接
if (task.websocket) {
task.websocket.close()
task.websocket = null
}
}
break
default:
addTaskLog(task, `收到未知消息类型: ${data.type}`, 'warning')
}
}
// 添加任务日志
const addTaskLog = (task: RunningTask, message: string, type: 'info' | 'error' | 'warning' | 'success' = 'info') => {
const now = new Date()
const time = now.toLocaleTimeString()
task.logs.push({
time,
message,
type
})
// 自动滚动到底部
nextTick(() => {
const outputElements = outputRefs.value
if (outputElements && outputElements.length > 0) {
const taskIndex = runningTasks.value.findIndex(t => t.websocketId === task.websocketId)
if (taskIndex >= 0 && outputElements[taskIndex]) {
outputElements[taskIndex].scrollTop = outputElements[taskIndex].scrollHeight
}
}
})
}
// 发送消息响应
const sendMessageResponse = () => {
if (!currentMessage.value || !currentMessage.value.taskId) {
return
}
const task = runningTasks.value.find(t => t.websocketId === currentMessage.value?.taskId)
if (task && task.websocket) {
const response = {
type: 'MessageResponse',
messageId: currentMessage.value.messageId,
response: messageResponse.value
}
task.websocket.send(JSON.stringify(response))
addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info')
}
messageModalVisible.value = false
messageResponse.value = ''
currentMessage.value = null
}
// 取消消息
const cancelMessage = () => {
messageModalVisible.value = false
messageResponse.value = ''
currentMessage.value = null
}
// 停止任务
const stopTask = (taskId: string) => {
const taskIndex = runningTasks.value.findIndex(t => t.websocketId === taskId)
if (taskIndex >= 0) {
const task = runningTasks.value[taskIndex]
// 关闭WebSocket连接
if (task.websocket) {
task.websocket.close()
task.websocket = null
}
// 从列表中移除
runningTasks.value.splice(taskIndex, 1)
// 从展开面板中移除
const panelIndex = activeTaskPanels.value.indexOf(taskId)
if (panelIndex >= 0) {
activeTaskPanels.value.splice(panelIndex, 1)
}
message.success('任务已停止')
}
}
// 清空任务输出
const clearTaskOutput = (taskId: string) => {
const task = runningTasks.value.find(t => t.websocketId === taskId)
if (task) {
task.logs = []
}
}
// 获取任务状态颜色
const getTaskStatusColor = (status: string) => {
switch (status) {
case '运行中': return 'processing'
case '已完成': return 'success'
case '已失败': return 'error'
case '连接中': return 'default'
case '已断开': return 'warning'
case '连接错误': return 'error'
case '连接失败': return 'error'
default: return 'default'
}
}
// 任务选项过滤
const filterTaskOption = (input: string, option: any) => {
return option.label.toLowerCase().includes(input.toLowerCase())
}
// 组件挂载时加载任务选项
onMounted(() => {
loadTaskOptions()
})
// 组件卸载时清理WebSocket连接
onUnmounted(() => {
runningTasks.value.forEach(task => {
if (task.websocket) {
task.websocket.close()
}
})
})
</script>
<style scoped>
.page-container {
padding: 20px;
.scheduler-container {
padding: 24px;
height: 100%;
display: flex;
flex-direction: column;
}
.header-actions {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.execution-area {
flex: 1;
overflow: hidden;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.task-panels {
height: 100%;
overflow-y: auto;
}
.task-output {
border: 1px solid var(--ant-color-border);
border-radius: 6px;
overflow: hidden;
}
.output-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: var(--ant-color-fill-quaternary);
border-bottom: 1px solid var(--ant-color-border);
font-size: 12px;
font-weight: 500;
}
.output-content {
height: 300px;
overflow-y: auto;
padding: 8px;
background-color: var(--ant-color-bg-container);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.log-line {
display: flex;
margin-bottom: 2px;
word-break: break-all;
}
.log-time {
color: var(--ant-color-text-tertiary);
margin-right: 8px;
flex-shrink: 0;
min-width: 80px;
}
.log-message {
flex: 1;
}
.log-info .log-message {
color: var(--ant-color-text);
}
.log-success .log-message {
color: var(--ant-color-success);
}
.log-warning .log-message {
color: var(--ant-color-warning);
}
.log-error .log-message {
color: var(--ant-color-error);
}
/* 已创建任务区域样式 */
.created-task-section {
margin-bottom: 24px;
}
.task-card {
border: 1px solid var(--ant-color-border);
border-radius: 8px;
}
.task-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-details h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--ant-color-text);
}
.task-meta {
margin: 0;
font-size: 12px;
color: var(--ant-color-text-secondary);
display: flex;
gap: 16px;
}
.task-meta span {
display: inline-block;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.output-content {
background-color: var(--ant-color-bg-elevated);
}
.task-card {
background-color: var(--ant-color-bg-elevated);
}
}
</style>