diff --git a/frontend/src/components/queue/QueueItemManager.vue b/frontend/src/components/queue/QueueItemManager.vue index 796bcf0..152c63f 100644 --- a/frontend/src/components/queue/QueueItemManager.vue +++ b/frontend/src/components/queue/QueueItemManager.vue @@ -11,49 +11,69 @@ - - - @@ -61,15 +81,13 @@ import { onMounted, ref, watch } from 'vue' import { message } from 'ant-design-vue' import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue' +import draggable from 'vuedraggable' import { Service } from '@/api' // Props interface Props { queueId: string - queueItems: Array<{ - id: string - script: string | null - }> + queueItems: any[] } const props = defineProps() @@ -82,14 +100,8 @@ const emit = defineEmits<{ // 响应式数据 const loading = ref(false) -// 脚本选项类型定义 -interface ScriptOption { - label: string - value: string | null -} - -// 脚本选项 -const scriptOptions = ref([]) +// 选项数据 +const scriptOptions = ref>([]) // 表格列配置 const queueColumns = [ @@ -108,7 +120,7 @@ const queueColumns = [ { title: '操作', key: 'actions', - width: 180, + width: 100, align: 'center', }, ] @@ -148,10 +160,9 @@ const loadOptions = async () => { // 更新队列项脚本 const updateQueueItemScript = async (record: any) => { - const oldScript = record.script try { loading.value = true - + const response = await Service.updateItemApiQueueItemUpdatePost({ queueId: props.queueId, queueItemId: record.id, @@ -164,15 +175,11 @@ const updateQueueItemScript = async (record: any) => { if (response.code === 200) { message.success('脚本更新成功') - // 不触发刷新,避免界面闪烁 + emit('refresh') } else { - // 回滚本地变更 - record.script = oldScript message.error('脚本更新失败: ' + (response.message || '未知错误')) } } catch (error: any) { - // 发生异常时回滚 - record.script = oldScript console.error('更新脚本失败:', error) message.error('更新脚本失败: ' + (error?.message || '网络错误')) } finally { @@ -184,7 +191,7 @@ const updateQueueItemScript = async (record: any) => { const addQueueItem = async () => { try { loading.value = true - + // 直接创建队列项,默认ScriptId为null(未选择) const createResponse = await Service.addItemApiQueueItemAddPost({ queueId: props.queueId, @@ -192,7 +199,6 @@ const addQueueItem = async () => { if (createResponse.code === 200 && createResponse.queueItemId) { message.success('任务添加成功') - // 只在添加成功后刷新,避免不必要的闪烁 emit('refresh') } else { message.error('任务添加失败: ' + (createResponse.message || '未知错误')) @@ -226,6 +232,44 @@ const deleteQueueItem = async (itemId: string) => { } } +// 拖拽结束处理函数 +const onDragEnd = async (evt: any) => { + // 如果位置没有变化,直接返回 + if (evt.oldIndex === evt.newIndex) { + return + } + + try { + loading.value = true + + // 构造排序后的ID列表 + const sortedIds = queueItems.value.map(item => item.id) + + // 调用排序API + const response = await Service.reorderItemApiQueueItemOrderPost({ + queueId: props.queueId, + indexList: sortedIds, + }) + + if (response.code === 200) { + message.success('任务顺序已更新') + // 刷新数据以确保与服务器同步 + emit('refresh') + } else { + message.error('更新任务顺序失败: ' + (response.message || '未知错误')) + // 如果失败,刷新数据恢复原状态 + emit('refresh') + } + } catch (error: any) { + console.error('拖拽排序失败:', error) + message.error('更新任务顺序失败: ' + (error?.message || '网络错误')) + // 如果失败,刷新数据恢复原状态 + emit('refresh') + } finally { + loading.value = false + } +} + // 初始化 onMounted(() => { loadOptions() @@ -483,10 +527,150 @@ onMounted(() => { gap: 8px; } +/* 拖拽表格样式 */ +.draggable-table-container { + width: 100%; + border: 1px solid var(--ant-color-border); + border-radius: 6px; + overflow: hidden; +} + +.draggable-table-header { + display: flex; + background-color: var(--ant-color-fill-quaternary); + border-bottom: 1px solid var(--ant-color-border); +} + +.header-cell { + padding: 12px 16px; + font-weight: 600; + color: var(--ant-color-text); + text-align: center; + border-right: 1px solid var(--ant-color-border); +} + +.header-cell:last-child { + border-right: none; +} + +.index-cell { + width: 80px; + min-width: 80px; + max-width: 80px; +} + +.script-cell { + flex: 1; + min-width: 200px; +} + +.actions-cell { + width: 180px; + min-width: 180px; + max-width: 180px; +} + +.draggable-container { + min-height: 60px; +} + +.draggable-row { + display: flex; + align-items: center; + background: var(--ant-color-bg-container); + border-bottom: 1px solid var(--ant-color-border); + transition: all 0.2s ease; + cursor: move; +} + +.draggable-row:last-child { + border-bottom: none; +} + +.draggable-row:hover { + background-color: var(--ant-color-fill-quaternary); +} + +.draggable-row.row-dragging { + cursor: not-allowed; +} + +.row-cell { + padding: 12px 16px; + text-align: center; + border-right: 1px solid var(--ant-color-border); + display: flex; + align-items: center; + justify-content: center; +} + +.row-cell:last-child { + border-right: none; +} + +.row-cell.index-cell { + width: 80px; + min-width: 80px; + max-width: 80px; + font-weight: 500; + color: var(--ant-color-text-secondary); +} + +.row-cell.script-cell { + flex: 1; + min-width: 200px; +} + +.row-cell.actions-cell { + width: 180px; + min-width: 180px; + max-width: 180px; +} + +/* 拖拽状态样式 */ +.ghost { + opacity: 0.5; + background: var(--ant-color-primary-bg); + border: 2px dashed var(--ant-color-primary); +} + +.chosen { + background: var(--ant-color-primary-bg-hover); + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.drag { + transform: rotate(5deg); + opacity: 0.8; +} + /* 空状态样式 */ .empty-state { - text-align: center; - padding: 40px 0; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.empty-content { + display: flex; + justify-content: center; +} + +.empty-image { + max-width: 200px; + height: auto; + opacity: 0.9; + filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.1)); + transition: all 0.3s ease; + position: relative; + z-index: 1; +} + +.empty-image:hover { + transform: translateY(-4px); + filter: drop-shadow(0 12px 32px rgba(0, 0, 0, 0.15)); } /* 响应式设计 */ @@ -504,6 +688,30 @@ onMounted(() => { .queue-item-card-item { padding: 12px; } + + .draggable-row { + flex-direction: column; + align-items: stretch; + } + + .row-cell, + .header-cell { + border-right: none; + border-bottom: 1px solid var(--ant-color-border); + } + + .row-cell:last-child, + .header-cell:last-child { + border-bottom: none; + } + + .index-cell, + .script-cell, + .actions-cell { + width: 100% !important; + min-width: auto !important; + max-width: none !important; + } } /* 标签样式 */ @@ -604,4 +812,4 @@ onMounted(() => { .script-select :deep(.ant-select-item-option-content) { font-size: 13px !important; } - \ No newline at end of file + diff --git a/frontend/src/components/queue/TimeSetManager.vue b/frontend/src/components/queue/TimeSetManager.vue index 469ff48..5a435f0 100644 --- a/frontend/src/components/queue/TimeSetManager.vue +++ b/frontend/src/components/queue/TimeSetManager.vue @@ -1,5 +1,5 @@ @@ -77,6 +97,7 @@ import { ref, watch } from 'vue' import { message } from 'ant-design-vue' import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue' +import draggable from 'vuedraggable' import { Service } from '@/api' import dayjs from 'dayjs' @@ -159,7 +180,7 @@ const timeSets = ref([...props.timeSets]) const processTimeSets = (rawTimeSets: any[]) => { return rawTimeSets.map(item => ({ ...item, - timeValue: parseTimeString(item.time) + timeValue: parseTimeString(item.time), })) } @@ -221,7 +242,7 @@ const addTimeSet = async () => { const updateTimeSetTime = async (timeSet: any) => { try { const timeString = formatTimeValue(timeSet.timeValue) - + const response = await Service.updateTimeSetApiQueueTimeUpdatePost({ queueId: props.queueId, timeSetId: timeSet.id, @@ -297,6 +318,44 @@ const deleteTimeSet = async (timeSetId: string) => { message.error('删除定时项失败: ' + (error?.message || '网络错误')) } } + +// 拖拽结束处理函数 +const onDragEnd = async (evt: any) => { + // 如果位置没有变化,直接返回 + if (evt.oldIndex === evt.newIndex) { + return + } + + try { + loading.value = true + + // 构造排序后的ID列表 + const sortedIds = timeSets.value.map(item => item.id) + + // 调用排序API + const response = await Service.reorderTimeSetApiQueueTimeOrderPost({ + queueId: props.queueId, + indexList: sortedIds, + }) + + if (response.code === 200) { + message.success('定时顺序已更新') + // 刷新数据以确保与服务器同步 + emit('refresh') + } else { + message.error('更新定时顺序失败: ' + (response.message || '未知错误')) + // 如果失败,刷新数据恢复原状态 + emit('refresh') + } + } catch (error: any) { + console.error('拖拽排序失败:', error) + message.error('更新定时顺序失败: ' + (error?.message || '网络错误')) + // 如果失败,刷新数据恢复原状态 + emit('refresh') + } finally { + loading.value = false + } +} diff --git a/frontend/src/views/Queue.vue b/frontend/src/views/Queue.vue index 8165036..8dd5ad9 100644 --- a/frontend/src/views/Queue.vue +++ b/frontend/src/views/Queue.vue @@ -362,7 +362,7 @@ const refreshTimeSets = async () => { if (response.code !== 200) { console.error('获取定时项数据失败:', response) - currentTimeSets.value = [] + // 不清空数组,避免骨架屏闪现 return } @@ -401,19 +401,19 @@ const refreshTimeSets = async () => { // 使用nextTick确保数据更新不会导致渲染问题 await nextTick() - currentTimeSets.value = [...timeSets] + // 直接替换数组内容,而不是清空再赋值,避免骨架屏闪现 + currentTimeSets.value.splice(0, currentTimeSets.value.length, ...timeSets) console.log('刷新后的定时项数据:', timeSets) // 调试日志 } catch (error) { console.error('刷新定时项列表失败:', error) - currentTimeSets.value = [] - // 不显示错误消息,避免干扰用户 + // 不清空数组,避免骨架屏闪现 } } // 刷新队列项数据 const refreshQueueItems = async () => { if (!activeQueueId.value) { - currentQueueItems.value = [] + // 不清空数组,避免骨架屏闪现 return } @@ -425,7 +425,7 @@ const refreshQueueItems = async () => { if (response.code !== 200) { console.error('获取队列项数据失败:', response) - currentQueueItems.value = [] + // 不清空数组,避免骨架屏闪现 return } @@ -451,19 +451,14 @@ const refreshQueueItems = async () => { }) } - // 只在数据真正发生变化时才更新,避免不必要的界面闪烁 - const oldItemsString = JSON.stringify(currentQueueItems.value) - const newItemsString = JSON.stringify(queueItems) - - if (oldItemsString !== newItemsString) { - currentQueueItems.value = [...queueItems] - } - + // 使用nextTick确保数据更新不会导致渲染问题 + await nextTick() + // 直接替换数组内容,而不是清空再赋值,避免骨架屏闪现 + currentQueueItems.value.splice(0, currentQueueItems.value.length, ...queueItems) console.log('刷新后的队列项数据:', queueItems) // 调试日志 } catch (error) { console.error('刷新队列项列表失败:', error) - currentQueueItems.value = [] - // 不显示错误消息,避免干扰用户 + // 不清空数组,避免骨架屏闪现 } } @@ -631,19 +626,6 @@ const saveQueueData = async () => { } } -// 监听队列项变化,但避免频繁刷新导致的界面闪烁 -watch( - () => currentQueueItems.value, - (newItems, oldItems) => { - // 深度比较避免不必要的更新 - if (JSON.stringify(newItems) !== JSON.stringify(oldItems)) { - // 只有当数据真正改变时才触发更新 - console.log('队列项数据变化,触发更新') - } - }, - { deep: true } -) - // 自动保存功能 watch( () => [ diff --git a/frontend/src/views/scheduler/SchedulerLogPanel.vue b/frontend/src/views/scheduler/SchedulerLogPanel.vue index 1f8122f..d4ef2ba 100644 --- a/frontend/src/views/scheduler/SchedulerLogPanel.vue +++ b/frontend/src/views/scheduler/SchedulerLogPanel.vue @@ -109,15 +109,15 @@ const clearLogs = () => { .log-content { flex: 1; - padding: 16px; - background: var(--ant-color-bg-container); + padding: 12px; + background: var(--ant-color-bg-layout); border: 1px solid var(--ant-color-border); border-radius: 6px; overflow-y: auto; + max-height: 400px; font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.4; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .empty-state-mini { @@ -125,13 +125,13 @@ const clearLogs = () => { flex-direction: column; align-items: center; justify-content: center; - min-height: 200px; + height: 200px; color: var(--ant-color-text-tertiary); } .empty-image-mini { - width: 64px; - height: 64px; + max-width: 64px; + height: auto; opacity: 0.5; margin-bottom: 8px; filter: var(--ant-color-scheme-dark, brightness(0.8)); @@ -145,8 +145,8 @@ const clearLogs = () => { .log-line { margin-bottom: 2px; - padding: 4px 8px; - border-radius: 4px; + padding: 2px 4px; + border-radius: 2px; word-wrap: break-word; } @@ -198,9 +198,8 @@ const clearLogs = () => { } .log-content { - background: var(--ant-color-bg-container, #1f1f1f); + background: var(--ant-color-bg-layout, #141414); border: 1px solid var(--ant-color-border, #424242); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .empty-state-mini { @@ -251,11 +250,3 @@ const clearLogs = () => { } } - - diff --git a/frontend/src/views/scheduler/SchedulerQueuePanel.vue b/frontend/src/views/scheduler/SchedulerQueuePanel.vue index 0c72f82..819876d 100644 --- a/frontend/src/views/scheduler/SchedulerQueuePanel.vue +++ b/frontend/src/views/scheduler/SchedulerQueuePanel.vue @@ -74,11 +74,6 @@ const getStatusColor = (status: string) => getQueueStatusColor(status) .queue-content { flex: 1; overflow-y: auto; - padding: 12px; - background: var(--ant-color-bg-layout); - border: 1px solid var(--ant-color-border); - border-radius: 6px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .empty-state-mini { @@ -86,13 +81,13 @@ const getStatusColor = (status: string) => getQueueStatusColor(status) flex-direction: column; align-items: center; justify-content: center; - min-height: 200px; + height: 200px; color: var(--ant-color-text-tertiary); } .empty-image-mini { - width: 64px; - height: 64px; + max-width: 48px; + height: auto; opacity: 0.5; margin-bottom: 8px; filter: var(--ant-color-scheme-dark, brightness(0.8)); @@ -114,13 +109,11 @@ const getStatusColor = (status: string) => getQueueStatusColor(status) border-radius: 6px; transition: all 0.2s ease; background-color: var(--ant-color-bg-container); - border: 1px solid var(--ant-color-border); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + border-color: var(--ant-color-border); } .queue-card:hover { box-shadow: 0 2px 8px var(--ant-color-shadow); - border-color: var(--ant-color-primary); } .running-card { @@ -151,12 +144,6 @@ const getStatusColor = (status: string) => getQueueStatusColor(status) color: var(--ant-color-text-heading, #ffffff); } - .queue-content { - background: var(--ant-color-bg-layout, #141414); - border: 1px solid var(--ant-color-border, #424242); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - .empty-state-mini { color: var(--ant-color-text-tertiary, #8c8c8c); } @@ -172,17 +159,19 @@ const getStatusColor = (status: string) => getQueueStatusColor(status) .queue-card { background-color: var(--ant-color-bg-container, #1f1f1f); border-color: var(--ant-color-border, #424242); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + } + + .queue-card:hover { + box-shadow: 0 2px 8px 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); } .item-name { color: var(--ant-color-text, #ffffff); } } - -@media (max-width: 768px) { - .queue-content { - padding: 8px; - } -} - + \ No newline at end of file diff --git a/frontend/src/views/scheduler/SchedulerTaskControl.vue b/frontend/src/views/scheduler/SchedulerTaskControl.vue index 4665614..a464dc3 100644 --- a/frontend/src/views/scheduler/SchedulerTaskControl.vue +++ b/frontend/src/views/scheduler/SchedulerTaskControl.vue @@ -131,70 +131,33 @@ const filterTaskOption = (input: string, option: any) => { display: flex; align-items: center; gap: 12px; - padding: 20px; - background: var(--ant-color-bg-container); + padding: 16px; + background: var(--ant-color-bg-layout); border: 1px solid var(--ant-color-border); border-radius: 8px; transition: all 0.3s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -.control-row:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.control-label { - font-weight: 500; - color: var(--ant-color-text); - white-space: nowrap; -} - -.control-select { - min-width: 200px; +/* 暗色模式适配 */ +@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; } -.control-button { - min-width: 100px; -} - -/* 暗色模式适配 */ -@media (prefers-color-scheme: dark) { - .control-row { - 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.3); - } - - .control-row:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - } - - .control-label { - color: var(--ant-color-text, #ffffff); - } -} - @media (max-width: 768px) { .control-row { flex-direction: column; align-items: stretch; - padding: 16px; - } - - .control-select { - min-width: auto; } .control-spacer { display: none; } - - .control-button { - width: 100%; - } } diff --git a/frontend/src/views/scheduler/index.vue b/frontend/src/views/scheduler/index.vue index 1fd95bf..4295f51 100644 --- a/frontend/src/views/scheduler/index.vue +++ b/frontend/src/views/scheduler/index.vue @@ -225,21 +225,22 @@ onUnmounted(() => {