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

View File

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

View File

@@ -1,13 +1,15 @@
<template> <template>
<div class="queue-panel"> <div class="queue-panel">
<a-card class="section-card" :bordered="false">
<template #title>
<div class="section-header"> <div class="section-header">
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
<a-badge :count="items.length" :overflow-count="99" /> <a-badge :count="items.length" :overflow-count="99" />
</div> </div>
</template>
<div class="queue-content"> <div class="queue-content">
<div v-if="items.length === 0" class="empty-state-mini"> <div v-if="items.length === 0" class="empty-state-mini">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image-mini" /> <a-empty :description="emptyText" />
<p class="empty-text-mini">{{ emptyText }}</p>
</div> </div>
<div v-else class="queue-cards"> <div v-else class="queue-cards">
<a-card <a-card
@@ -30,6 +32,7 @@
</a-card> </a-card>
</div> </div>
</div> </div>
</a-card>
</div> </div>
</template> </template>
@@ -57,68 +60,72 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
flex-direction: column; 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 { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; width: 100%;
} }
.section-header h3 { .section-header h3 {
margin: 0; margin: 0;
font-size: 16px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--ant-color-text-heading); color: var(--ant-color-text-heading);
} }
.queue-content { .queue-content {
flex: 1; height: 100%;
overflow-y: auto; overflow-y: auto;
} }
.empty-state-mini { .empty-state-mini {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 200px; height: 100%;
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);
} }
.queue-cards { .queue-cards {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 12px;
} }
.queue-card { .queue-card {
border-radius: 6px; border-radius: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: var(--ant-color-bg-container); background-color: var(--ant-color-bg-layout);
border-color: var(--ant-color-border); border: 1px solid var(--ant-color-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
} }
.queue-card:hover { .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 { .running-card {
border-color: var(--ant-color-primary); 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 { .card-title-row {
@@ -140,38 +147,50 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
/* 暗色模式适配 */ /* 暗色模式适配 */
@media (prefers-color-scheme: dark) { @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 { .section-header h3 {
color: var(--ant-color-text-heading, #ffffff); 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 { .queue-card {
background-color: var(--ant-color-bg-container, #1f1f1f); background-color: var(--ant-color-bg-layout, #141414);
border-color: var(--ant-color-border, #424242); border: 1px solid var(--ant-color-border, #424242);
} }
.queue-card:hover { .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 { .running-card {
border-color: var(--ant-color-primary, #1890ff); 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 { .item-name {
color: var(--ant-color-text, #ffffff); 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> </style>

View File

@@ -1,6 +1,8 @@
<template> <template>
<div class="task-control"> <div class="task-control">
<a-card class="control-card" :bordered="false">
<div class="control-row"> <div class="control-row">
<a-space size="middle">
<a-select <a-select
v-model:value="localSelectedTaskId" v-model:value="localSelectedTaskId"
placeholder="选择任务项" placeholder="选择任务项"
@@ -11,6 +13,7 @@
:filter-option="filterTaskOption" :filter-option="filterTaskOption"
:disabled="disabled" :disabled="disabled"
@change="onTaskChange" @change="onTaskChange"
size="large"
/> />
<a-select <a-select
v-model:value="localSelectedMode" v-model:value="localSelectedMode"
@@ -18,23 +21,31 @@
style="width: 120px" style="width: 120px"
:disabled="disabled" :disabled="disabled"
@change="onModeChange" @change="onModeChange"
size="large"
> >
<a-select-option v-for="option in modeOptions" :key="option.value" :value="option.value"> <a-select-option v-for="option in modeOptions" :key="option.value" :value="option.value">
{{ option.label }} {{ option.label }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-space>
<div class="control-spacer"></div> <div class="control-spacer"></div>
<a-space size="middle">
<a-button <a-button
v-if="status !== '运行'" v-if="status !== '运行'"
type="primary" type="primary"
@click="onStart" @click="onStart"
:icon="h(PlayCircleOutlined)" :icon="h(PlayCircleOutlined)"
:disabled="!canStart" :disabled="!canStart"
size="large"
> >
开始任务 开始任务
</a-button> </a-button>
<a-button v-else danger @click="onStop" :icon="h(StopOutlined)"> 中止任务</a-button> <a-button v-else danger @click="onStop" :icon="h(StopOutlined)" size="large">
中止任务
</a-button>
</a-space>
</div> </div>
</a-card>
</div> </div>
</template> </template>
@@ -127,35 +138,52 @@ const filterTaskOption = (input: string, option: any) => {
margin-bottom: 16px; 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 { .control-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 16px; background: var(--ant-color-bg-container);
background: var(--ant-color-bg-layout);
border: 1px solid var(--ant-color-border);
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; 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 { .control-spacer {
flex: 1; 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) { @media (max-width: 768px) {
.control-row { .control-row {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.control-card :deep(.ant-card-body) {
padding: 12px;
}
.control-spacer { .control-spacer {
display: none; display: none;
} }

View File

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

View File

@@ -13,9 +13,38 @@ import {
type TaskMessage, type TaskMessage,
} from './schedulerConstants' } from './schedulerConstants'
export function useSchedulerLogic() { // 本地存储键名
// 核心状态 const SCHEDULER_STORAGE_KEY = 'scheduler-tabs-state'
const schedulerTabs = ref<SchedulerTab[]>([ 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', key: 'main',
title: '主调度台', title: '主调度台',
@@ -30,18 +59,64 @@ export function useSchedulerLogic() {
isLogAtBottom: true, isLogAtBottom: true,
lastLogContent: '', 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>()) 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 taskOptionsLoading = ref(false)
const taskOptions = ref<ComboBoxItem[]>([]) const taskOptions = ref<ComboBoxItem[]>([])
// 电源操作 // 电源操作 - 从本地存储加载或使用默认值
const powerAction = ref<PowerIn.signal>(PowerIn.signal.NO_ACTION) const powerAction = ref<PowerIn.signal>(loadPowerActionFromStorage())
const powerCountdownVisible = ref(false) const powerCountdownVisible = ref(false)
const powerCountdown = ref(10) const powerCountdown = ref(10)
let powerCountdownTimer: ReturnType<typeof setInterval> | null = null let powerCountdownTimer: ReturnType<typeof setInterval> | null = null
@@ -63,6 +138,31 @@ export function useSchedulerLogic() {
return schedulerTabs.value.find(tab => tab.key === activeSchedulerTab.value) 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 管理 // Tab 管理
const addSchedulerTab = () => { const addSchedulerTab = () => {
tabCounter++ tabCounter++
@@ -82,6 +182,7 @@ export function useSchedulerLogic() {
} }
schedulerTabs.value.push(tab) schedulerTabs.value.push(tab)
activeSchedulerTab.value = tab.key activeSchedulerTab.value = tab.key
saveTabsToStorage(schedulerTabs.value)
} }
const removeSchedulerTab = (key: string) => { const removeSchedulerTab = (key: string) => {
@@ -121,6 +222,7 @@ export function useSchedulerLogic() {
logRefs.value.delete(key) logRefs.value.delete(key)
schedulerTabs.value.splice(idx, 1) schedulerTabs.value.splice(idx, 1)
saveTabsToStorage(schedulerTabs.value)
if (activeSchedulerTab.value === key) { if (activeSchedulerTab.value === key) {
const newActiveIndex = Math.max(0, idx - 1) const newActiveIndex = Math.max(0, idx - 1)
@@ -158,6 +260,7 @@ export function useSchedulerLogic() {
subscribeToTask(tab) subscribeToTask(tab)
message.success('任务启动成功') message.success('任务启动成功')
saveTabsToStorage(schedulerTabs.value)
} else { } else {
message.error(response.message || '启动任务失败') message.error(response.message || '启动任务失败')
} }
@@ -182,6 +285,7 @@ export function useSchedulerLogic() {
message.success('任务已停止') message.success('任务已停止')
checkAllTasksCompleted() checkAllTasksCompleted()
saveTabsToStorage(schedulerTabs.value)
} catch (error) { } catch (error) {
console.error('停止任务失败:', error) console.error('停止任务失败:', error)
message.error('停止任务失败') message.error('停止任务失败')
@@ -192,6 +296,7 @@ export function useSchedulerLogic() {
tab.status = '结束' tab.status = '结束'
tab.websocketId = null tab.websocketId = null
} }
saveTabsToStorage(schedulerTabs.value)
} }
} }
@@ -265,6 +370,7 @@ export function useSchedulerLogic() {
else addLog(tab, JSON.stringify(data.log), 'info') else addLog(tab, JSON.stringify(data.log), 'info')
} }
} }
saveTabsToStorage(schedulerTabs.value)
} }
const handleInfoMessage = (tab: SchedulerTab, data: any) => { const handleInfoMessage = (tab: SchedulerTab, data: any) => {
@@ -301,10 +407,12 @@ export function useSchedulerLogic() {
notification.success({ message: '任务完成', description: data.Accomplish }) notification.success({ message: '任务完成', description: data.Accomplish })
checkAllTasksCompleted() checkAllTasksCompleted()
saveTabsToStorage(schedulerTabs.value)
} }
if (data.power && data.power !== 'NoAction') { if (data.power && data.power !== 'NoAction') {
powerAction.value = data.power as PowerIn.signal powerAction.value = data.power as PowerIn.signal
savePowerActionToStorage(powerAction.value)
startPowerCountdown() startPowerCountdown()
} }
} }
@@ -355,6 +463,7 @@ export function useSchedulerLogic() {
// 电源操作 // 电源操作
const onPowerActionChange = (value: PowerIn.signal) => { const onPowerActionChange = (value: PowerIn.signal) => {
powerAction.value = value powerAction.value = value
savePowerActionToStorage(value)
} }
const startPowerCountdown = () => { const startPowerCountdown = () => {
@@ -457,6 +566,8 @@ export function useSchedulerLogic() {
ws.unsubscribe(tab.websocketId) ws.unsubscribe(tab.websocketId)
} }
}) })
saveTabsToStorage(schedulerTabs.value)
savePowerActionToStorage(powerAction.value)
} }
return { return {