fix: 移除任务列表骨架屏

This commit is contained in:
DLmaster361
2025-09-19 10:20:54 +08:00
parent 01ee70f3fb
commit c0af9d7de0
7 changed files with 180 additions and 56 deletions

View File

@@ -1,5 +1,5 @@
<template>
<a-card title="任务列表" class="queue-item-card" :loading="loading">
<a-card title="任务列表" class="queue-item-card">
<template #extra>
<a-space>
<a-button type="primary" @click="addQueueItem" :loading="loading">
@@ -54,6 +54,28 @@
</template>
</template>
</a-table>
<!-- 队列项编辑弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="editingQueueItem ? '编辑任务' : '添加任务'"
@ok="saveQueueItem"
@cancel="cancelEdit"
:confirm-loading="saving"
width="600px"
>
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="关联脚本" name="script">
<a-select
v-model:value="form.script"
placeholder="请选择关联脚本"
allow-clear
:options="scriptOptions"
class="script-select"
/>
</a-form-item>
</a-form>
</a-modal>
</a-card>
</template>
@@ -66,7 +88,10 @@ import { Service } from '@/api'
// Props
interface Props {
queueId: string
queueItems: any[]
queueItems: Array<{
id: string
script: string | null
}>
}
const props = defineProps<Props>()
@@ -79,8 +104,14 @@ const emit = defineEmits<{
// 响应式数据
const loading = ref(false)
// 选项数据
const scriptOptions = ref<Array<{ label: string; value: string | null }>>([])
// 脚本选项类型定义
interface ScriptOption {
label: string
value: string | null
}
// 脚本选项
const scriptOptions = ref<ScriptOption[]>([])
// 表格列配置
const queueColumns = [
@@ -99,7 +130,7 @@ const queueColumns = [
{
title: '操作',
key: 'actions',
width: 100,
width: 180,
align: 'center',
},
]
@@ -139,6 +170,7 @@ const loadOptions = async () => {
// 更新队列项脚本
const updateQueueItemScript = async (record: any) => {
const oldScript = record.script
try {
loading.value = true
@@ -154,11 +186,15 @@ 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 {
@@ -178,6 +214,7 @@ const addQueueItem = async () => {
if (createResponse.code === 200 && createResponse.queueItemId) {
message.success('任务添加成功')
// 只在添加成功后刷新,避免不必要的闪烁
emit('refresh')
} else {
message.error('任务添加失败: ' + (createResponse.message || '未知错误'))

View File

@@ -451,9 +451,14 @@ const refreshQueueItems = async () => {
})
}
// 使用nextTick确保数据更新不会导致渲染问题
await nextTick()
currentQueueItems.value = [...queueItems]
// 只在数据真正发生变化时才更新,避免不必要的界面闪烁
const oldItemsString = JSON.stringify(currentQueueItems.value)
const newItemsString = JSON.stringify(queueItems)
if (oldItemsString !== newItemsString) {
currentQueueItems.value = [...queueItems]
}
console.log('刷新后的队列项数据:', queueItems) // 调试日志
} catch (error) {
console.error('刷新队列项列表失败:', error)
@@ -626,6 +631,19 @@ const saveQueueData = async () => {
}
}
// 监听队列项变化,但避免频繁刷新导致的界面闪烁
watch(
() => currentQueueItems.value,
(newItems, oldItems) => {
// 深度比较避免不必要的更新
if (JSON.stringify(newItems) !== JSON.stringify(oldItems)) {
// 只有当数据真正改变时才触发更新
console.log('队列项数据变化,触发更新')
}
},
{ deep: true }
)
// 自动保存功能
watch(
() => [

View File

@@ -109,15 +109,15 @@ const clearLogs = () => {
.log-content {
flex: 1;
padding: 12px;
background: var(--ant-color-bg-layout);
padding: 16px;
background: var(--ant-color-bg-container);
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,7 +125,7 @@ const clearLogs = () => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
min-height: 200px;
color: var(--ant-color-text-tertiary);
}
@@ -145,8 +145,8 @@ const clearLogs = () => {
.log-line {
margin-bottom: 2px;
padding: 2px 4px;
border-radius: 2px;
padding: 4px 8px;
border-radius: 4px;
word-wrap: break-word;
}
@@ -198,8 +198,9 @@ const clearLogs = () => {
}
.log-content {
background: var(--ant-color-bg-layout, #141414);
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);
}
.empty-state-mini {
@@ -250,3 +251,11 @@ const clearLogs = () => {
}
}
</style>
<style scoped>
@media (max-width: 768px) {
.log-content {
padding: 12px;
}
}
</style>

View File

@@ -74,6 +74,11 @@ 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 {
@@ -81,13 +86,13 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
min-height: 200px;
color: var(--ant-color-text-tertiary);
}
.empty-image-mini {
width: 48px;
height: 48px;
width: 64px;
height: 64px;
opacity: 0.5;
margin-bottom: 8px;
filter: var(--ant-color-scheme-dark, brightness(0.8));
@@ -109,11 +114,13 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
border-radius: 6px;
transition: all 0.2s ease;
background-color: var(--ant-color-bg-container);
border-color: var(--ant-color-border);
border: 1px solid var(--ant-color-border);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.queue-card:hover {
box-shadow: 0 2px 8px var(--ant-color-shadow);
border-color: var(--ant-color-primary);
}
.running-card {
@@ -144,6 +151,12 @@ 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);
}
@@ -159,19 +172,17 @@ const getStatusColor = (status: string) => getQueueStatusColor(status)
.queue-card {
background-color: var(--ant-color-bg-container, #1f1f1f);
border-color: var(--ant-color-border, #424242);
}
.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);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.item-name {
color: var(--ant-color-text, #ffffff);
}
}
@media (max-width: 768px) {
.queue-content {
padding: 8px;
}
}
</style>

View File

@@ -131,33 +131,70 @@ const filterTaskOption = (input: string, option: any) => {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--ant-color-bg-layout);
padding: 20px;
background: var(--ant-color-bg-container);
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);
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.control-row {
background: var(--ant-color-bg-layout, #141414);
border: 1px solid var(--ant-color-border, #424242);
}
.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;
}
.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%;
}
}
</style>

View File

@@ -225,22 +225,21 @@ onUnmounted(() => {
<style scoped>
/* 页面容器 */
.scheduler-page {
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
color: var(--ant-color-text);
background: var(--ant-color-bg-layout);
padding: 24px;
}
/* 页面头部样式 */
.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: 24px;
padding: 0 4px;
}
.header-left {
@@ -248,7 +247,7 @@ onUnmounted(() => {
}
.page-title {
margin: 0;
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
@@ -277,6 +276,9 @@ onUnmounted(() => {
flex-direction: column;
overflow: hidden;
background-color: var(--ant-color-bg-container);
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.scheduler-tabs :deep(.ant-tabs) {
@@ -301,10 +303,12 @@ onUnmounted(() => {
.scheduler-tabs :deep(.ant-tabs-tab) {
background-color: var(--ant-color-bg-layout);
border-color: var(--ant-color-border);
border-radius: 6px 6px 0 0;
}
.scheduler-tabs :deep(.ant-tabs-tab-active) {
background-color: var(--ant-color-bg-container);
border-bottom: 1px solid var(--ant-color-bg-container);
}
.tab-title {
@@ -333,6 +337,7 @@ onUnmounted(() => {
.status-row {
flex: 1;
overflow: hidden;
margin-top: 16px;
}
.status-row :deep(.ant-col) {
@@ -366,12 +371,8 @@ onUnmounted(() => {
/* 暗色模式适配 */
@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);
color: var(--ant-color-text, #ffffff);
}
.page-title {
@@ -384,6 +385,7 @@ onUnmounted(() => {
.scheduler-tabs {
background-color: var(--ant-color-bg-container, #1f1f1f);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.scheduler-tabs :deep(.ant-tabs) {
@@ -407,6 +409,7 @@ onUnmounted(() => {
.scheduler-tabs :deep(.ant-tabs-tab-active) {
background-color: var(--ant-color-bg-container, #1f1f1f);
color: var(--ant-color-text, #ffffff);
border-bottom: 1px solid var(--ant-color-bg-container, #1f1f1f);
}
.tab-title {
@@ -446,16 +449,25 @@ onUnmounted(() => {
}
@media (max-width: 768px) {
.scheduler-page {
padding: 16px;
}
.scheduler-header {
flex-direction: column;
align-items: stretch;
gap: 16px;
margin-bottom: 16px;
}
.header-actions {
justify-content: center;
}
.scheduler-tabs {
padding: 12px;
}
.status-row {
flex-direction: column;
}

View File

@@ -13,10 +13,10 @@ export const TAB_STATUS_COLOR: Record<SchedulerStatus, string> = {
// 队列状态 -> 颜色
export const getQueueStatusColor = (status: string): string => {
if (/成功|完成|已完成/.test(status)) return 'green'
if (/失败|错误|异常/.test(status)) return 'red'
if (/等待|排队|挂起/.test(status)) return 'orange'
if (/进行|执行|运行/.test(status)) return 'blue'
if (/成功|完成|已完成/.test(status)) return 'success'
if (/失败|错误|异常/.test(status)) return 'error'
if (/等待|排队|挂起/.test(status)) return 'warning'
if (/进行|执行|运行/.test(status)) return 'processing'
return 'default'
}
@@ -37,7 +37,7 @@ export const POWER_ACTION_TEXT: Record<PowerIn.signal, string> = {
[PowerIn.signal.SHUTDOWN_FORCE]: '强制关机',
}
export const getPowerActionText = (action: PowerIn.signal) => POWER_ACTION_TEXT[action] || '无动<EFBFBD><EFBFBD>'
export const getPowerActionText = (action: PowerIn.signal) => POWER_ACTION_TEXT[action] || '无动'
// 日志相关
export const LOG_MAX_LENGTH = 2000 // 最多保留日志条数
@@ -77,4 +77,4 @@ export interface TaskMessage {
needInput: boolean
messageId?: string
taskId?: string
}
}