Files
AUTO-MAS-test/frontend/src/views/scheduler/index.vue

526 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 主要内容 -->
<div class="scheduler-main">
<!-- 页面头部 -->
<div class="scheduler-header">
<div class="header-left">
<h1 class="page-title">调度中心</h1>
</div>
<div class="header-actions">
<a-space size="middle">
<span class="power-label">任务完成后电源操作</span>
<a-select
v-model:value="powerAction"
style="width: 140px"
:disabled="!canChangePowerAction"
@change="onPowerActionChange"
size="large"
>
<a-select-option
v-for="(text, signal) in POWER_ACTION_TEXT"
:key="signal"
:value="signal"
>
{{ text }}
</a-select-option>
</a-select>
</a-space>
</div>
</div>
<!-- 调度台标签页 -->
<div class="scheduler-tabs">
<a-tabs
v-model:activeKey="activeSchedulerTab"
type="editable-card"
@edit="onSchedulerTabEdit"
:hide-add="false"
>
<a-tab-pane
v-for="tab in schedulerTabs"
:key="tab.key"
:closable="tab.closable && tab.status !== '运行'"
:data-tab-key="tab.key"
>
<template #tab>
<span class="tab-title">{{ tab.title }}</span>
<a-tag :color="TAB_STATUS_COLOR[tab.status]" size="small" class="tab-status">
{{ tab.status }}
</a-tag>
<a-tooltip v-if="tab.status === '运行'" title="运行中的调度台无法删除" placement="top">
<LockOutlined class="tab-lock-icon" />
</a-tooltip>
</template>
<!-- 任务控制与状态内容 -->
<div class="task-unified-card" :class="`status-${tab.status}`">
<!-- 任务控制栏 -->
<SchedulerTaskControl
v-model:selectedTaskId="tab.selectedTaskId"
v-model:selectedMode="tab.selectedMode"
:taskOptions="taskOptions"
:taskOptionsLoading="taskOptionsLoading"
:status="tab.status"
:disabled="tab.status === '运行'"
@start="startTask(tab)"
@stop="stopTask(tab)"
/>
<!-- 状态展示区域 -->
<div class="status-container">
<div class="overview-panel-container">
<TaskOverviewPanel
:ref="el => setOverviewRef(el, tab.key)"
/>
</div>
<div class="log-panel-container">
<SchedulerLogPanel
:log-content="tab.lastLogContent"
:tab-key="tab.key"
:is-log-at-bottom="tab.isLogAtBottom"
@scroll="onLogScroll(tab)"
@set-ref="setLogRef"
/>
</div>
</div>
</div>
</a-tab-pane>
<!-- 空状态 -->
<template #empty>
<div class="empty-tab-content">
<a-empty description="暂无调度台" />
</div>
</template>
</a-tabs>
</div>
<!-- 消息对话框 -->
<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 v-if="powerCountdownVisible" class="power-countdown-overlay">
<div class="power-countdown-container">
<div class="countdown-content">
<div class="warning-icon"></div>
<h2 class="countdown-title">{{ powerCountdownData.title || `${getPowerActionText(powerAction)}倒计时` }}</h2>
<p class="countdown-message">
{{ powerCountdownData.message || `程序将在倒计时结束后执行 ${getPowerActionText(powerAction)} 操作` }}
</p>
<div class="countdown-timer" v-if="powerCountdownData.countdown !== undefined">
<span class="countdown-number">{{ powerCountdownData.countdown }}</span>
<span class="countdown-unit"></span>
</div>
<div class="countdown-timer" v-else>
<span class="countdown-text">等待后端倒计时...</span>
</div>
<a-progress
v-if="powerCountdownData.countdown !== undefined"
:percent="Math.max(0, Math.min(100, (60 - powerCountdownData.countdown) / 60 * 100))"
:show-info="false"
:stroke-color="(powerCountdownData.countdown || 0) <= 10 ? '#ff4d4f' : '#1890ff'"
:stroke-width="8"
class="countdown-progress"
/>
<div class="countdown-actions">
<a-button
type="primary"
size="large"
@click="cancelPowerAction"
class="cancel-button"
>
取消操作
</a-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { LockOutlined } from '@ant-design/icons-vue'
import {
getPowerActionText,
POWER_ACTION_TEXT,
type SchedulerTab,
TAB_STATUS_COLOR,
} from './schedulerConstants'
import { useSchedulerLogic } from './useSchedulerLogic'
import SchedulerTaskControl from './SchedulerTaskControl.vue'
import SchedulerLogPanel from './SchedulerLogPanel.vue'
import TaskOverviewPanel from './TaskOverviewPanel.vue'
// 使用业务逻辑层
const {
// 状态
schedulerTabs,
activeSchedulerTab,
taskOptionsLoading,
taskOptions,
powerAction,
powerCountdownVisible,
powerCountdownData,
messageModalVisible,
currentMessage,
messageResponse,
// 计算属性
canChangePowerAction,
// Tab 管理
addSchedulerTab,
removeSchedulerTab,
// 任务操作
startTask,
stopTask,
// 日志操作
onLogScroll,
setLogRef,
// 电源操作
onPowerActionChange,
cancelPowerAction,
// 消息操作
sendMessageResponse,
cancelMessage,
// 初始化与清理
initialize,
loadTaskOptions,
cleanup,
// 新增:任务总览面板引用管理
setOverviewRef,
} = useSchedulerLogic()
// Tab 操作
const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
if (action === 'add') {
addSchedulerTab()
} else if (action === 'remove' && typeof targetKey === 'string') {
removeSchedulerTab(targetKey)
}
}
// 生命周期
onMounted(() => {
initialize() // 初始化TaskManager订阅
loadTaskOptions()
})
onUnmounted(() => {
cleanup()
})
</script>
<style scoped>
/* 页面容器 */
.scheduler-main {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-layout);
}
/* 页面头部样式 */
.scheduler-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 16px;
padding: 0 4px;
}
.header-left {
flex: 1;
}
.page-title {
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
}
.power-label {
font-size: 14px;
color: var(--ant-color-text-secondary);
margin-right: 8px;
}
/* 标签页样式 */
.scheduler-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
border-radius: 8px;
padding: 12px;
}
.scheduler-tabs :deep(.ant-tabs) {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--ant-color-bg-container);
}
/* 任务卡片统一容器 */
.task-unified-card {
background-color: transparent;
box-shadow: none;
height: calc(100vh - 230px); /* 动态计算高度 */
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 根据状态变化的样式 */
.task-unified-card.status-新建 {
background-color: transparent;
}
.task-unified-card.status-运行 {
background-color: transparent;
}
.task-unified-card.status-结束 {
background-color: transparent;
}
/* 状态容器 */
.status-container {
display: flex;
flex: 1;
overflow: hidden;
gap: 16px;
padding: 0;
margin: 0;
}
/* 任务总览面板容器 */
.overview-panel-container {
flex: 0 0 33.333333%; /* 占据1/3宽度 */
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 日志面板容器 */
.log-panel-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 电源操作倒计时全屏弹窗样式 */
.power-countdown-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-out;
}
.power-countdown-container {
background: var(--ant-color-bg-container);
border-radius: 16px;
padding: 48px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 500px;
width: 90%;
animation: slideIn 0.3s ease-out;
}
.countdown-content .warning-icon {
font-size: 64px;
margin-bottom: 24px;
display: block;
animation: pulse 2s infinite;
}
.countdown-title {
font-size: 28px;
font-weight: 600;
color: var(--ant-color-text);
margin: 0 0 16px 0;
}
.countdown-message {
font-size: 16px;
color: var(--ant-color-text-secondary);
margin: 0 0 32px 0;
line-height: 1.5;
}
.countdown-timer {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 32px;
}
.countdown-number {
font-size: 72px;
font-weight: 700;
color: var(--ant-color-primary);
line-height: 1;
margin-right: 8px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.countdown-unit {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-text {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-progress {
margin-bottom: 32px;
}
.countdown-actions {
display: flex;
justify-content: center;
}
.cancel-button {
padding: 12px 32px;
height: auto;
font-size: 16px;
font-weight: 500;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* 响应式 - 移动端适配 */
@media (max-width: 768px) {
.scheduler-main {
padding: 8px;
}
.scheduler-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.header-actions {
width: 100%;
justify-content: space-between;
}
.power-label {
display: none;
}
.status-container {
flex-direction: column;
}
.overview-panel-container,
.log-panel-container {
flex: 1;
width: 100%;
}
/* 移动端倒计时弹窗适配 */
.power-countdown-container {
padding: 32px 24px;
margin: 16px;
}
.countdown-title {
font-size: 24px;
}
.countdown-number {
font-size: 56px;
}
.countdown-unit {
font-size: 20px;
}
.countdown-content .warning-icon {
font-size: 48px;
margin-bottom: 16px;
}
}
</style>