fix: 初步完成调度中心样式优化
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user