fix: 初步完成调度中心样式优化

This commit is contained in:
DLmaster361
2025-09-19 21:36:09 +08:00
parent a9769c6397
commit e62b9b3943
6 changed files with 445 additions and 407 deletions

View File

@@ -6,8 +6,8 @@
<!-- 主要内容 -->
<div class="scripts-header">
<div class="header-title">
<h1>脚本管理</h1>
<div class="header-left">
<h1 class="page-title">脚本管理</h1>
</div>
<a-space size="middle">
<a-button type="primary" size="large" @click="handleAddScript" class="link">
@@ -634,14 +634,24 @@ const handleToggleUserStatus = async (user: User) => {
.scripts-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-end;
margin-bottom: 24px;
padding: 0 4px;
}
.header-title h1 {
font-size: 28px;
font-weight: 600;
margin: 0;
.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;
}
.empty-state {

View File

@@ -1,30 +1,35 @@
<template>
<div class="log-panel">
<div class="section-header">
<h3>日志</h3>
<div class="log-controls">
<a-button size="small" @click="clearLogs" :disabled="logs.length === 0">
清空日志
</a-button>
<a-button size="small" @click="scrollToBottom" :disabled="logs.length === 0">
滚动到底部
</a-button>
<a-card class="section-card" :bordered="false">
<template #title>
<div class="section-header">
<h3>日志</h3>
<div class="log-controls">
<a-space size="small">
<a-button @click="clearLogs" :disabled="logs.length === 0" size="small">
清空日志
</a-button>
<a-button @click="scrollToBottom" :disabled="logs.length === 0" size="small">
滚动到底部
</a-button>
</a-space>
</div>
</div>
</template>
<div class="log-content" :ref="setLogRef" @scroll="onScroll">
<div v-if="logs.length === 0" class="empty-state-mini">
<a-empty description="暂无日志信息" />
</div>
<div
v-for="(log, index) in logs"
:key="`${tabKey}-${index}-${log.timestamp}`"
:class="['log-line', `log-${log.type}`]"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
<div class="log-content" :ref="setLogRef" @scroll="onScroll">
<div v-if="logs.length === 0" class="empty-state-mini">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image-mini" />
<p class="empty-text-mini">暂无日志信息</p>
</div>
<div
v-for="(log, index) in logs"
:key="`${tabKey}-${index}-${log.timestamp}`"
:class="['log-line', `log-${log.type}`]"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</a-card>
</div>
</template>
@@ -88,16 +93,34 @@ const clearLogs = () => {
flex-direction: column;
}
.section-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid var(--ant-color-border-secondary);
height: 100%;
}
.section-card :deep(.ant-card-head) {
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: 0 16px;
border-radius: 12px 12px 0 0;
}
.section-card :deep(.ant-card-body) {
padding: 0;
height: calc(100% - 52px);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
width: 100%;
}
.section-header h3 {
margin: 0;
font-size: 16px;
font-size: 18px;
font-weight: 600;
color: var(--ant-color-text-heading);
}
@@ -108,51 +131,33 @@ const clearLogs = () => {
}
.log-content {
flex: 1;
padding: 12px;
background: var(--ant-color-bg-layout);
border: 1px solid var(--ant-color-border);
border-radius: 6px;
height: 100%;
padding: 16px;
background: var(--ant-color-bg-container);
overflow-y: auto;
max-height: 400px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
font-size: 13px;
line-height: 1.5;
}
.empty-state-mini {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--ant-color-text-tertiary);
}
.empty-image-mini {
max-width: 64px;
height: auto;
opacity: 0.5;
margin-bottom: 8px;
filter: var(--ant-color-scheme-dark, brightness(0.8));
}
.empty-text-mini {
margin: 0;
font-size: 14px;
color: var(--ant-color-text-tertiary);
height: 100%;
min-height: 300px;
}
.log-line {
margin-bottom: 2px;
padding: 2px 4px;
border-radius: 2px;
margin-bottom: 4px;
padding: 4px 8px;
border-radius: 4px;
word-wrap: break-word;
}
.log-time {
color: var(--ant-color-text-secondary);
margin-right: 8px;
margin-right: 12px;
font-weight: 500;
}
@@ -166,7 +171,7 @@ const clearLogs = () => {
.log-error {
background-color: var(--ant-color-error-bg);
border-left: 3px solid var(--ant-color-error);
border-left: 4px solid var(--ant-color-error);
}
.log-error .log-message {
@@ -175,7 +180,7 @@ const clearLogs = () => {
.log-warning {
background-color: var(--ant-color-warning-bg);
border-left: 3px solid var(--ant-color-warning);
border-left: 4px solid var(--ant-color-warning);
}
.log-warning .log-message {
@@ -184,7 +189,7 @@ const clearLogs = () => {
.log-success {
background-color: var(--ant-color-success-bg);
border-left: 3px solid var(--ant-color-success);
border-left: 4px solid var(--ant-color-success);
}
.log-success .log-message {
@@ -193,25 +198,26 @@ const clearLogs = () => {
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.section-card {
background: var(--ant-color-bg-container, #1f1f1f);
border: 1px solid var(--ant-color-border, #424242);
}
.section-card :deep(.ant-card-head) {
background: var(--ant-color-bg-layout, #141414);
border-bottom: 1px solid var(--ant-color-border, #424242);
}
.section-card :deep(.ant-card-body) {
background: var(--ant-color-bg-container, #1f1f1f);
}
.section-header h3 {
color: var(--ant-color-text-heading, #ffffff);
}
.log-content {
background: var(--ant-color-bg-layout, #141414);
border: 1px solid var(--ant-color-border, #424242);
}
.empty-state-mini {
color: var(--ant-color-text-tertiary, #8c8c8c);
}
.empty-image-mini {
filter: brightness(0.8);
}
.empty-text-mini {
color: var(--ant-color-text-tertiary, #8c8c8c);
background: var(--ant-color-bg-container, #1f1f1f);
}
.log-time {
@@ -224,7 +230,7 @@ const clearLogs = () => {
.log-error {
background-color: rgba(255, 77, 79, 0.1);
border-left: 3px solid var(--ant-color-error, #ff4d4f);
border-left: 4px solid var(--ant-color-error, #ff4d4f);
}
.log-error .log-message {
@@ -233,7 +239,7 @@ const clearLogs = () => {
.log-warning {
background-color: rgba(250, 173, 20, 0.1);
border-left: 3px solid var(--ant-color-warning, #faad14);
border-left: 4px solid var(--ant-color-warning, #faad14);
}
.log-warning .log-message {
@@ -242,11 +248,21 @@ const clearLogs = () => {
.log-success {
background-color: rgba(82, 196, 26, 0.1);
border-left: 3px solid var(--ant-color-success, #52c41a);
border-left: 4px solid var(--ant-color-success, #52c41a);
}
.log-success .log-message {
color: var(--ant-color-success, #73d13d);
}
}
</style>
@media (max-width: 768px) {
.log-content {
padding: 12px;
}
.section-card :deep(.ant-card-head) {
padding: 0 16px;
}
}
</style>

View File

@@ -1,35 +1,38 @@
<template>
<div class="queue-panel">
<div class="section-header">
<h3>{{ title }}</h3>
<a-badge :count="items.length" :overflow-count="99" />
</div>
<div class="queue-content">
<div v-if="items.length === 0" class="empty-state-mini">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image-mini" />
<p class="empty-text-mini">{{ emptyText }}</p>
</div>
<div v-else class="queue-cards">
<a-card
v-for="(item, index) in items"
:key="`${type}-${index}`"
size="small"
class="queue-card"
:class="{ 'running-card': item.status === '运行' }"
>
<template #title>
<div class="card-title-row">
<a-tag :color="getStatusColor(item.status)" size="small">
{{ item.status }}
</a-tag>
<a-card class="section-card" :bordered="false">
<template #title>
<div class="section-header">
<h3>{{ title }}</h3>
<a-badge :count="items.length" :overflow-count="99" />
</div>
</template>
<div class="queue-content">
<div v-if="items.length === 0" class="empty-state-mini">
<a-empty :description="emptyText" />
</div>
<div v-else class="queue-cards">
<a-card
v-for="(item, index) in items"
:key="`${type}-${index}`"
size="small"
class="queue-card"
:class="{ 'running-card': item.status === '运行' }"
>
<template #title>
<div class="card-title-row">
<a-tag :color="getStatusColor(item.status)" size="small">
{{ item.status }}
</a-tag>
</div>
</template>
<div class="card-content">
<p class="item-name">{{ item.name }}</p>
</div>
</template>
<div class="card-content">
<p class="item-name">{{ item.name }}</p>
</div>
</a-card>
</a-card>
</div>
</div>
</div>
</a-card>
</div>
</template>
@@ -57,68 +60,72 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
flex-direction: column;
}
.section-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid var(--ant-color-border-secondary);
height: 100%;
}
.section-card :deep(.ant-card-head) {
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: 0 16px;
border-radius: 12px 12px 0 0;
}
.section-card :deep(.ant-card-body) {
padding: 16px;
height: calc(100% - 52px);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
width: 100%;
}
.section-header h3 {
margin: 0;
font-size: 16px;
font-size: 18px;
font-weight: 600;
color: var(--ant-color-text-heading);
}
.queue-content {
flex: 1;
height: 100%;
overflow-y: auto;
}
.empty-state-mini {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--ant-color-text-tertiary);
}
.empty-image-mini {
max-width: 48px;
height: auto;
opacity: 0.5;
margin-bottom: 8px;
filter: var(--ant-color-scheme-dark, brightness(0.8));
}
.empty-text-mini {
margin: 0;
font-size: 14px;
color: var(--ant-color-text-tertiary);
height: 100%;
}
.queue-cards {
display: flex;
flex-direction: column;
gap: 8px;
gap: 12px;
}
.queue-card {
border-radius: 6px;
border-radius: 8px;
transition: all 0.2s ease;
background-color: var(--ant-color-bg-container);
border-color: var(--ant-color-border);
background-color: var(--ant-color-bg-layout);
border: 1px solid var(--ant-color-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.queue-card:hover {
box-shadow: 0 2px 8px var(--ant-color-shadow);
box-shadow: 0 4px 12px var(--ant-color-shadow);
transform: translateY(-2px);
}
.running-card {
border-color: var(--ant-color-primary);
box-shadow: 0 0 0 1px var(--ant-color-primary-bg);
box-shadow: 0 0 0 2px var(--ant-color-primary-bg);
}
.card-title-row {
@@ -140,38 +147,50 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.section-card {
background: var(--ant-color-bg-container, #1f1f1f);
border: 1px solid var(--ant-color-border, #424242);
}
.section-card :deep(.ant-card-head) {
background: var(--ant-color-bg-layout, #141414);
border-bottom: 1px solid var(--ant-color-border, #424242);
}
.section-card :deep(.ant-card-body) {
background: var(--ant-color-bg-container, #1f1f1f);
}
.section-header h3 {
color: var(--ant-color-text-heading, #ffffff);
}
.empty-state-mini {
color: var(--ant-color-text-tertiary, #8c8c8c);
}
.empty-image-mini {
filter: brightness(0.8);
}
.empty-text-mini {
color: var(--ant-color-text-tertiary, #8c8c8c);
}
.queue-card {
background-color: var(--ant-color-bg-container, #1f1f1f);
border-color: var(--ant-color-border, #424242);
background-color: var(--ant-color-bg-layout, #141414);
border: 1px solid var(--ant-color-border, #424242);
}
.queue-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.running-card {
border-color: var(--ant-color-primary, #1890ff);
box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.2);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.item-name {
color: var(--ant-color-text, #ffffff);
}
}
@media (max-width: 768px) {
.section-card :deep(.ant-card-head) {
padding: 0 16px;
}
.section-card :deep(.ant-card-body) {
padding: 12px;
}
}
</style>

View File

@@ -1,40 +1,51 @@
<template>
<div class="task-control">
<div class="control-row">
<a-select
v-model:value="localSelectedTaskId"
placeholder="选择任务项"
style="width: 200px"
:loading="taskOptionsLoading"
:options="taskOptions"
show-search
:filter-option="filterTaskOption"
:disabled="disabled"
@change="onTaskChange"
/>
<a-select
v-model:value="localSelectedMode"
placeholder="选择模式"
style="width: 120px"
:disabled="disabled"
@change="onModeChange"
>
<a-select-option v-for="option in modeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<div class="control-spacer"></div>
<a-button
v-if="status !== '运行'"
type="primary"
@click="onStart"
:icon="h(PlayCircleOutlined)"
:disabled="!canStart"
>
开始任务
</a-button>
<a-button v-else danger @click="onStop" :icon="h(StopOutlined)"> 中止任务</a-button>
</div>
<a-card class="control-card" :bordered="false">
<div class="control-row">
<a-space size="middle">
<a-select
v-model:value="localSelectedTaskId"
placeholder="选择任务项"
style="width: 200px"
:loading="taskOptionsLoading"
:options="taskOptions"
show-search
:filter-option="filterTaskOption"
:disabled="disabled"
@change="onTaskChange"
size="large"
/>
<a-select
v-model:value="localSelectedMode"
placeholder="选择模式"
style="width: 120px"
:disabled="disabled"
@change="onModeChange"
size="large"
>
<a-select-option v-for="option in modeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
</a-space>
<div class="control-spacer"></div>
<a-space size="middle">
<a-button
v-if="status !== '运行'"
type="primary"
@click="onStart"
:icon="h(PlayCircleOutlined)"
:disabled="!canStart"
size="large"
>
开始任务
</a-button>
<a-button v-else danger @click="onStop" :icon="h(StopOutlined)" size="large">
中止任务
</a-button>
</a-space>
</div>
</a-card>
</div>
</template>
@@ -127,37 +138,54 @@ const filterTaskOption = (input: string, option: any) => {
margin-bottom: 16px;
}
.control-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid var(--ant-color-border-secondary);
}
.control-card :deep(.ant-card-body) {
padding: 16px;
}
.control-row {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--ant-color-bg-layout);
border: 1px solid var(--ant-color-border);
background: var(--ant-color-bg-container);
border-radius: 8px;
transition: all 0.3s ease;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.control-row {
background: var(--ant-color-bg-layout, #141414);
border: 1px solid var(--ant-color-border, #424242);
}
}
.control-spacer {
flex: 1;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.control-card {
background: var(--ant-color-bg-container, #1f1f1f);
border: 1px solid var(--ant-color-border, #424242);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.control-row {
background: var(--ant-color-bg-container, #1f1f1f);
}
}
@media (max-width: 768px) {
.control-row {
flex-direction: column;
align-items: stretch;
}
.control-card :deep(.ant-card-body) {
padding: 12px;
}
.control-spacer {
display: none;
}
}
</style>
</style>

View File

@@ -1,26 +1,30 @@
<template>
<div class="scheduler-page">
<!-- 主要内容 -->
<div class="scheduler-main">
<!-- 页面头部 -->
<div class="scheduler-header">
<div class="header-left">
<h1 class="page-title">调度中心</h1>
</div>
<div class="header-actions">
<span class="power-label">任务完成后电源操作</span>
<a-select
v-model:value="powerAction"
style="width: 140px"
:disabled="!canChangePowerAction"
@change="onPowerActionChange"
>
<a-select-option
v-for="(text, signal) in POWER_ACTION_TEXT"
:key="signal"
:value="signal"
<a-space size="middle">
<span class="power-label">任务完成后电源操作</span>
<a-select
v-model:value="powerAction"
style="width: 140px"
:disabled="!canChangePowerAction"
@change="onPowerActionChange"
size="large"
>
{{ text }}
</a-select-option>
</a-select>
<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>
@@ -30,6 +34,7 @@
v-model:activeKey="activeSchedulerTab"
type="editable-card"
@edit="onSchedulerTabEdit"
:hide-add="false"
>
<a-tab-pane
v-for="tab in schedulerTabs"
@@ -43,7 +48,7 @@
{{ tab.status }}
</a-tag>
<a-tooltip v-if="tab.status === '运行'" title="运行中的调度台无法删除" placement="top">
<span class="tab-lock-icon">🔒</span>
<LockOutlined class="tab-lock-icon" />
</a-tooltip>
</template>
@@ -89,7 +94,7 @@
:logs="tab.logs"
:tab-key="tab.key"
:is-log-at-bottom="tab.isLogAtBottom"
@scroll="onLogScroll(tab, $event)"
@scroll="onLogScroll(tab)"
@set-ref="setLogRef"
@clear-logs="clearTabLogs(tab)"
/>
@@ -97,6 +102,13 @@
</a-row>
</div>
</a-tab-pane>
<!-- 空状态 -->
<template #empty>
<div class="empty-tab-content">
<a-empty description="暂无调度台" />
</div>
</template>
</a-tabs>
</div>
@@ -224,23 +236,22 @@ onUnmounted(() => {
<style scoped>
/* 页面容器 */
.scheduler-page {
height: 100vh;
.scheduler-main {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
color: var(--ant-color-text);
padding: 16px;
background-color: var(--ant-color-bg-layout);
}
/* 页面头部样式 */
.scheduler-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px 24px;
flex-shrink: 0;
background-color: var(--ant-color-bg-layout);
align-items: flex-end;
margin-bottom: 16px;
padding: 0 4px;
}
.header-left {
@@ -248,7 +259,7 @@ onUnmounted(() => {
}
.page-title {
margin: 0;
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
@@ -268,6 +279,7 @@ onUnmounted(() => {
.power-label {
font-size: 14px;
color: var(--ant-color-text-secondary);
margin-right: 8px;
}
/* 标签页样式 */
@@ -277,6 +289,8 @@ onUnmounted(() => {
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
border-radius: 8px;
padding: 12px;
}
.scheduler-tabs :deep(.ant-tabs) {
@@ -286,185 +300,25 @@ onUnmounted(() => {
background-color: var(--ant-color-bg-container);
}
.scheduler-tabs :deep(.ant-tabs-content-holder) {
flex: 1;
overflow: hidden;
background-color: var(--ant-color-bg-container);
}
.scheduler-tabs :deep(.ant-tabs-tabpane) {
height: 100%;
overflow: hidden;
background-color: var(--ant-color-bg-container);
}
.scheduler-tabs :deep(.ant-tabs-tab) {
background-color: var(--ant-color-bg-layout);
border-color: var(--ant-color-border);
}
.scheduler-tabs :deep(.ant-tabs-tab-active) {
background-color: var(--ant-color-bg-container);
}
.tab-title {
margin-right: 8px;
color: var(--ant-color-text);
}
.tab-status {
margin-right: 4px;
}
.tab-lock-icon {
font-size: 12px;
opacity: 0.7;
}
/* 统一卡片样式 */
.task-unified-card {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
}
.status-row {
flex: 1;
overflow: hidden;
}
.status-row :deep(.ant-col) {
height: 100%;
}
/* 电源倒计时样式 */
.power-countdown {
display: flex;
align-items: flex-start;
gap: 16px;
}
.warning-icon {
color: var(--ant-color-warning);
font-size: 24px;
flex-shrink: 0;
}
.power-countdown p {
margin: 0 0 12px 0;
font-size: 16px;
line-height: 1.5;
color: var(--ant-color-text);
}
.power-countdown strong {
color: var(--ant-color-text-heading);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.scheduler-page {
background-color: var(--ant-color-bg-container, #1f1f1f);
color: var(--ant-color-text, #ffffff);
}
.scheduler-header {
background-color: var(--ant-color-bg-layout, #141414);
}
.page-title {
color: var(--ant-color-text, #ffffff);
}
.power-label {
color: var(--ant-color-text-secondary, #bfbfbf);
}
.scheduler-tabs {
background-color: var(--ant-color-bg-container, #1f1f1f);
}
.scheduler-tabs :deep(.ant-tabs) {
background-color: var(--ant-color-bg-container, #1f1f1f);
}
.scheduler-tabs :deep(.ant-tabs-content-holder) {
background-color: var(--ant-color-bg-container, #1f1f1f);
}
.scheduler-tabs :deep(.ant-tabs-tabpane) {
background-color: var(--ant-color-bg-container, #1f1f1f);
}
.scheduler-tabs :deep(.ant-tabs-tab) {
background-color: var(--ant-color-bg-layout, #141414);
border-color: var(--ant-color-border, #424242);
color: var(--ant-color-text, #ffffff);
}
.scheduler-tabs :deep(.ant-tabs-tab-active) {
background-color: var(--ant-color-bg-container, #1f1f1f);
color: var(--ant-color-text, #ffffff);
}
.tab-title {
color: var(--ant-color-text, #ffffff);
}
.task-unified-card {
background-color: var(--ant-color-bg-container, #1f1f1f);
}
.warning-icon {
color: var(--ant-color-warning, #faad14);
}
.power-countdown p {
color: var(--ant-color-text, #ffffff);
}
.power-countdown strong {
color: var(--ant-color-text-heading, #ffffff);
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.status-row :deep(.ant-col:nth-child(1)) {
span: 6;
}
.status-row :deep(.ant-col:nth-child(2)) {
span: 6;
}
.status-row :deep(.ant-col:nth-child(3)) {
span: 12;
}
}
/* 响应式 - 移动端适配 */
@media (max-width: 768px) {
.scheduler-main {
padding: 8px;
}
.scheduler-header {
flex-direction: column;
align-items: stretch;
gap: 16px;
align-items: flex-start;
gap: 12px;
}
.header-actions {
justify-content: center;
width: 100%;
justify-content: space-between;
}
.status-row {
flex-direction: column;
}
.status-row :deep(.ant-col) {
width: 100% !important;
flex: none;
height: auto;
margin-bottom: 16px;
.power-label {
display: none;
}
}
</style>
</style>

View File

@@ -13,9 +13,38 @@ import {
type TaskMessage,
} from './schedulerConstants'
export function useSchedulerLogic() {
// 核心状态
const schedulerTabs = ref<SchedulerTab[]>([
// 本地存储键名
const SCHEDULER_STORAGE_KEY = 'scheduler-tabs-state'
const SCHEDULER_POWER_ACTION_KEY = 'scheduler-power-action'
// 从本地存储加载调度台状态
const loadTabsFromStorage = (): SchedulerTab[] => {
try {
const stored = localStorage.getItem(SCHEDULER_STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored)
// 确保运行中的任务状态正确重置
return parsed.map((tab: any) => ({
...tab,
// 重置WebSocket相关状态
websocketId: null,
status: tab.status === '运行' ? '结束' : tab.status,
// 确保数组存在
taskQueue: Array.isArray(tab.taskQueue) ? tab.taskQueue : [],
userQueue: Array.isArray(tab.userQueue) ? tab.userQueue : [],
logs: Array.isArray(tab.logs) ? tab.logs : [],
// 确保其他属性存在
selectedTaskId: tab.selectedTaskId || null,
selectedMode: tab.selectedMode || TaskCreateIn.mode.AutoMode,
isLogAtBottom: typeof tab.isLogAtBottom === 'boolean' ? tab.isLogAtBottom : true,
lastLogContent: tab.lastLogContent || '',
}))
}
} catch (e) {
console.error('Failed to load scheduler tabs from storage:', e)
}
// 默认返回主调度台
return [
{
key: 'main',
title: '主调度台',
@@ -30,18 +59,64 @@ export function useSchedulerLogic() {
isLogAtBottom: true,
lastLogContent: '',
},
])
]
}
const activeSchedulerTab = ref('main')
// 从本地存储加载电源操作状态
const loadPowerActionFromStorage = (): PowerIn.signal => {
try {
const stored = localStorage.getItem(SCHEDULER_POWER_ACTION_KEY)
if (stored) {
return stored as PowerIn.signal
}
} catch (e) {
console.error('Failed to load power action from storage:', e)
}
return PowerIn.signal.NO_ACTION
}
// 保存调度台状态到本地存储
const saveTabsToStorage = (tabs: SchedulerTab[]) => {
try {
// 保存前清理运行时状态
const tabsToSave = tabs.map(tab => ({
...tab,
// 清理运行时属性
websocketId: null,
status: tab.status === '运行' ? '结束' : tab.status,
}))
localStorage.setItem(SCHEDULER_STORAGE_KEY, JSON.stringify(tabsToSave))
} catch (e) {
console.error('Failed to save scheduler tabs to storage:', e)
}
}
// 保存电源操作状态到本地存储
const savePowerActionToStorage = (powerAction: PowerIn.signal) => {
try {
localStorage.setItem(SCHEDULER_POWER_ACTION_KEY, powerAction)
} catch (e) {
console.error('Failed to save power action to storage:', e)
}
}
export function useSchedulerLogic() {
// 核心状态 - 从本地存储加载或使用默认值
const schedulerTabs = ref<SchedulerTab[]>(loadTabsFromStorage())
const activeSchedulerTab = ref(schedulerTabs.value[0]?.key || 'main')
const logRefs = ref(new Map<string, HTMLElement>())
let tabCounter = 1
let tabCounter = schedulerTabs.value.length > 1 ?
Math.max(...schedulerTabs.value
.filter(tab => tab.key.startsWith('tab-'))
.map(tab => parseInt(tab.key.replace('tab-', '')) || 0)) + 1 : 1
// 任务选项
const taskOptionsLoading = ref(false)
const taskOptions = ref<ComboBoxItem[]>([])
// 电源操作
const powerAction = ref<PowerIn.signal>(PowerIn.signal.NO_ACTION)
// 电源操作 - 从本地存储加载或使用默认值
const powerAction = ref<PowerIn.signal>(loadPowerActionFromStorage())
const powerCountdownVisible = ref(false)
const powerCountdown = ref(10)
let powerCountdownTimer: ReturnType<typeof setInterval> | null = null
@@ -63,6 +138,31 @@ export function useSchedulerLogic() {
return schedulerTabs.value.find(tab => tab.key === activeSchedulerTab.value)
})
// 监听调度台变化并保存到本地存储
const watchTabsChanges = () => {
const saveState = () => {
saveTabsToStorage(schedulerTabs.value)
}
// 监听各种可能导致状态变化的操作
const originalPush = schedulerTabs.value.push
schedulerTabs.value.push = function(...items: SchedulerTab[]) {
const result = originalPush.apply(this, items)
saveState()
return result
}
const originalSplice = schedulerTabs.value.splice
schedulerTabs.value.splice = function(start: number, deleteCount?: number, ...items: SchedulerTab[]) {
const result = originalSplice.apply(this, [start, deleteCount, ...items] as any)
saveState()
return result
}
}
// 初始化监听
watchTabsChanges()
// Tab 管理
const addSchedulerTab = () => {
tabCounter++
@@ -82,6 +182,7 @@ export function useSchedulerLogic() {
}
schedulerTabs.value.push(tab)
activeSchedulerTab.value = tab.key
saveTabsToStorage(schedulerTabs.value)
}
const removeSchedulerTab = (key: string) => {
@@ -121,6 +222,7 @@ export function useSchedulerLogic() {
logRefs.value.delete(key)
schedulerTabs.value.splice(idx, 1)
saveTabsToStorage(schedulerTabs.value)
if (activeSchedulerTab.value === key) {
const newActiveIndex = Math.max(0, idx - 1)
@@ -158,6 +260,7 @@ export function useSchedulerLogic() {
subscribeToTask(tab)
message.success('任务启动成功')
saveTabsToStorage(schedulerTabs.value)
} else {
message.error(response.message || '启动任务失败')
}
@@ -182,6 +285,7 @@ export function useSchedulerLogic() {
message.success('任务已停止')
checkAllTasksCompleted()
saveTabsToStorage(schedulerTabs.value)
} catch (error) {
console.error('停止任务失败:', error)
message.error('停止任务失败')
@@ -192,6 +296,7 @@ export function useSchedulerLogic() {
tab.status = '结束'
tab.websocketId = null
}
saveTabsToStorage(schedulerTabs.value)
}
}
@@ -265,6 +370,7 @@ export function useSchedulerLogic() {
else addLog(tab, JSON.stringify(data.log), 'info')
}
}
saveTabsToStorage(schedulerTabs.value)
}
const handleInfoMessage = (tab: SchedulerTab, data: any) => {
@@ -301,10 +407,12 @@ export function useSchedulerLogic() {
notification.success({ message: '任务完成', description: data.Accomplish })
checkAllTasksCompleted()
saveTabsToStorage(schedulerTabs.value)
}
if (data.power && data.power !== 'NoAction') {
powerAction.value = data.power as PowerIn.signal
savePowerActionToStorage(powerAction.value)
startPowerCountdown()
}
}
@@ -355,6 +463,7 @@ export function useSchedulerLogic() {
// 电源操作
const onPowerActionChange = (value: PowerIn.signal) => {
powerAction.value = value
savePowerActionToStorage(value)
}
const startPowerCountdown = () => {
@@ -457,6 +566,8 @@ export function useSchedulerLogic() {
ws.unsubscribe(tab.websocketId)
}
})
saveTabsToStorage(schedulerTabs.value)
savePowerActionToStorage(powerAction.value)
}
return {
@@ -502,4 +613,4 @@ export function useSchedulerLogic() {
loadTaskOptions,
cleanup,
}
}
}