diff --git a/frontend/src/views/Scripts.vue b/frontend/src/views/Scripts.vue index 8971147..855baa6 100644 --- a/frontend/src/views/Scripts.vue +++ b/frontend/src/views/Scripts.vue @@ -6,8 +6,8 @@
-
-

脚本管理

+
+

脚本管理

@@ -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 { diff --git a/frontend/src/views/scheduler/SchedulerLogPanel.vue b/frontend/src/views/scheduler/SchedulerLogPanel.vue index d4ef2ba..db2e22b 100644 --- a/frontend/src/views/scheduler/SchedulerLogPanel.vue +++ b/frontend/src/views/scheduler/SchedulerLogPanel.vue @@ -1,30 +1,35 @@ @@ -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); } } - + +@media (max-width: 768px) { + .log-content { + padding: 12px; + } + + .section-card :deep(.ant-card-head) { + padding: 0 16px; + } +} + \ No newline at end of file diff --git a/frontend/src/views/scheduler/SchedulerQueuePanel.vue b/frontend/src/views/scheduler/SchedulerQueuePanel.vue index 819876d..9a44dd6 100644 --- a/frontend/src/views/scheduler/SchedulerQueuePanel.vue +++ b/frontend/src/views/scheduler/SchedulerQueuePanel.vue @@ -1,35 +1,38 @@ @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/views/scheduler/SchedulerTaskControl.vue b/frontend/src/views/scheduler/SchedulerTaskControl.vue index a464dc3..8713d71 100644 --- a/frontend/src/views/scheduler/SchedulerTaskControl.vue +++ b/frontend/src/views/scheduler/SchedulerTaskControl.vue @@ -1,40 +1,51 @@ @@ -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; } } - + \ No newline at end of file diff --git a/frontend/src/views/scheduler/index.vue b/frontend/src/views/scheduler/index.vue index 4295f51..4279004 100644 --- a/frontend/src/views/scheduler/index.vue +++ b/frontend/src/views/scheduler/index.vue @@ -1,26 +1,30 @@ @@ -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 @@
+ + +
@@ -224,23 +236,22 @@ onUnmounted(() => { + \ No newline at end of file diff --git a/frontend/src/views/scheduler/useSchedulerLogic.ts b/frontend/src/views/scheduler/useSchedulerLogic.ts index 4323dd4..7d04b58 100644 --- a/frontend/src/views/scheduler/useSchedulerLogic.ts +++ b/frontend/src/views/scheduler/useSchedulerLogic.ts @@ -13,9 +13,38 @@ import { type TaskMessage, } from './schedulerConstants' -export function useSchedulerLogic() { - // 核心状态 - const schedulerTabs = ref([ +// 本地存储键名 +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(loadTabsFromStorage()) + + const activeSchedulerTab = ref(schedulerTabs.value[0]?.key || 'main') const logRefs = ref(new Map()) - 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([]) - // 电源操作 - const powerAction = ref(PowerIn.signal.NO_ACTION) + // 电源操作 - 从本地存储加载或使用默认值 + const powerAction = ref(loadPowerActionFromStorage()) const powerCountdownVisible = ref(false) const powerCountdown = ref(10) let powerCountdownTimer: ReturnType | 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, } -} +} \ No newline at end of file