refactor: 重构调度中心的任务总览部分

This commit is contained in:
2025-09-21 15:05:02 +08:00
parent 7c34b3ca94
commit 6897f35d1e
7 changed files with 688 additions and 406 deletions

View File

@@ -165,9 +165,7 @@ onUnmounted(() => {
border-bottom: 1px solid var(--ant-color-border, #424242);
}
.section-header h3 {
color: var(--ant-color-text-heading, #ffffff);
}
.log-text {
color: var(--ant-color-text, #ffffff);

View File

@@ -1,258 +0,0 @@
<template>
<div class="queue-panel">
<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-tree">
<a-tree
:tree-data="treeData"
:expanded-keys="expandedKeys"
@expand="handleExpand"
:show-line="showLine"
:selectable="false"
:show-icon="false"
class="queue-tree-view"
>
<template #title="{ title, status, type }">
<div class="tree-item-content">
<span class="item-name">{{ title }}</span>
<a-tag
v-if="status"
:color="getStatusColor(status)"
size="small"
class="status-tag"
>
{{ status }}
</a-tag>
</div>
</template>
</a-tree>
</div>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { getQueueStatusColor, type QueueItem } from './schedulerConstants';
interface Props {
title: string
items: QueueItem[]
type: 'task' | 'user'
emptyText?: string
}
const props = withDefaults(defineProps<Props>(), {
emptyText: '暂无数据',
})
// 树形数据结构
const treeData = computed(() => {
// 为任务队列构建树形结构
if (props.type === 'task') {
return props.items.map((item, index) => ({
key: `task-${index}`,
title: item.name,
status: item.status,
type: 'task'
}));
}
// 为用户队列构建树形结构
else {
// 按照名称前缀分组
const groups: Record<string, any[]> = {};
props.items.forEach((item, index) => {
const prefix = item.name.split('-')[0] || '默认分组';
if (!groups[prefix]) {
groups[prefix] = [];
}
groups[prefix].push({
key: `user-${index}`,
title: item.name,
status: item.status,
type: 'user'
});
});
// 构建树形结构
return Object.entries(groups).map(([groupName, groupItems]) => {
if (groupItems.length === 1) {
// 如果组内只有一个项目,直接返回该项目
return groupItems[0];
} else {
// 如果组内有多个项目,创建一个父节点
return {
key: `group-${groupName}`,
title: groupName,
type: 'group',
children: groupItems
};
}
});
}
});
const expandedKeys = ref<string[]>([]);
const showLine = computed(() => ({ showLeafIcon: false }));
const getStatusColor = (status: string) => getQueueStatusColor(status);
// 处理展开/收起事件
const handleExpand = (keys: string[]) => {
expandedKeys.value = keys;
};
// 监听items变化自动展开所有节点
watch(() => props.items, () => {
// 默认展开所有节点
expandedKeys.value = treeData.value
.filter(node => node.children)
.map(node => node.key as string);
}, { immediate: true });
</script>
<style scoped>
.queue-panel {
height: 100%;
display: flex;
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;
width: 100%;
}
.section-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--ant-color-text-heading);
}
.queue-content {
height: 100%;
overflow-y: auto;
}
.empty-state-mini {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.queue-tree {
height: 100%;
}
.queue-tree-view {
background: transparent;
}
.queue-tree-view :deep(.ant-tree-treenode) {
padding: 2px 0;
}
.queue-tree-view :deep(.ant-tree-node-content-wrapper) {
display: flex;
align-items: center;
padding: 6px 8px;
border-radius: 6px;
transition: all 0.2s;
}
.queue-tree-view :deep(.ant-tree-node-content-wrapper:hover) {
background-color: var(--ant-color-fill-secondary);
}
.tree-item-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.item-name {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--ant-color-text);
word-break: break-word;
padding-right: 8px;
}
.status-tag {
flex-shrink: 0;
}
/* 暗色模式适配 */
@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);
}
.queue-tree-view :deep(.ant-tree-node-content-wrapper:hover) {
background-color: var(--ant-color-fill);
}
.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>

View File

@@ -2,108 +2,71 @@
<div class="overview-panel">
<div class="section-header">
<h3>任务总览</h3>
<!-- <a-badge :count="totalTaskCount" :overflow-count="99" />-->
</div>
<div class="overview-content">
<div v-if="treeData.length === 0" class="empty-state-mini">
<a-empty description="暂无任务" />
</div>
<div v-else class="overview-tree">
<a-tree
:tree-data="treeData"
:expanded-keys="expandedKeys"
@expand="handleExpand"
:show-line="showLine"
:selectable="false"
:show-icon="false"
class="overview-tree-view"
>
<template #title="{ title, status, type }">
<div class="tree-item-content">
<span class="item-name">{{ title }}</span>
<a-tag
v-if="status"
:color="getStatusColor(status)"
size="small"
class="status-tag"
>
{{ status }}
</a-tag>
</div>
</template>
</a-tree>
</div>
<TaskTree :task-data="taskData" ref="taskTreeRef" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { getQueueStatusColor, type QueueItem } from './schedulerConstants';
import { ref, computed } from 'vue'
import TaskTree from '@/components/TaskTree.vue'
interface Props {
taskQueue: QueueItem[];
userQueue: QueueItem[];
interface User {
user_id: string
status: string
name: string
}
const props = defineProps<Props>();
interface Script {
script_id: string
status: string
name: string
user_list: User[]
}
// 树形数据结构
const treeData = computed(() => {
// 构建脚本节点
const scriptNodes = props.taskQueue.map((task, index) => {
// 查找相关的用户
const relatedUsers = props.userQueue.filter(user =>
user.name.startsWith(`${task.name}-`)
);
// 构建用户子节点
const userChildren = relatedUsers.map((user, userIndex) => ({
key: `user-${index}-${userIndex}`,
title: user.name.replace(`${task.name}-`, ''), // 移除前缀以避免重复
status: user.status,
type: 'user'
}));
// 构建脚本节点
const scriptNode: any = {
key: `script-${index}`,
title: task.name,
status: task.status,
type: 'script'
};
// 如果有相关用户,添加为子节点
if (userChildren.length > 0) {
scriptNode.children = userChildren;
interface WSMessage {
type: string
id: string
data: {
task_dict: Script[]
}
fullMessage?: any
}
// 任务数据
const taskData = ref<Script[]>([])
const taskTreeRef = ref()
// 计算总任务数量
const totalTaskCount = computed(() => {
return taskData.value.reduce((total, script) => {
return total + (script.user_list?.length || 0)
}, 0)
})
// 处理 WebSocket 消息
const handleWSMessage = (message: WSMessage) => {
console.log('TaskOverviewPanel 收到 WebSocket 消息:', message)
if (message.type === 'Update' && message.data?.task_dict) {
console.log('更新任务数据:', message.data.task_dict)
taskData.value = message.data.task_dict
console.log('设置后的 taskData:', taskData.value)
// 更新展开状态
if (taskTreeRef.value) {
taskTreeRef.value.updateExpandedScripts()
}
return scriptNode;
});
return scriptNodes;
});
}
}
const expandedKeys = ref<string[]>([]);
const showLine = computed(() => ({ showLeafIcon: false }));
const getStatusColor = (status: string) => getQueueStatusColor(status);
// 处理展开/收起事件
const handleExpand = (keys: string[]) => {
expandedKeys.value = keys;
};
// 监听任务队列变化,自动展开所有节点
watch(() => props.taskQueue, (newTaskQueue) => {
// 默认展开所有脚本节点
expandedKeys.value = newTaskQueue.map((_, index) => `script-${index}`);
}, { immediate: true });
// 监听用户队列变化,更新展开状态
watch(() => props.userQueue, () => {
// 重新计算展开状态,确保新增的节点能正确显示
expandedKeys.value = props.taskQueue.map((_, index) => `script-${index}`);
});
// 暴露方法供父组件调用
defineExpose({
handleWSMessage,
expandAll: () => taskTreeRef.value?.expandAll(),
collapseAll: () => taskTreeRef.value?.collapseAll()
})
</script>
<style scoped>
@@ -148,51 +111,7 @@ watch(() => props.userQueue, () => {
height: 100%;
}
.overview-tree {
height: 100%;
}
.overview-tree-view {
background: transparent;
}
.overview-tree-view :deep(.ant-tree-treenode) {
padding: 4px 0;
}
.overview-tree-view :deep(.ant-tree-node-content-wrapper) {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s;
height: auto;
}
.overview-tree-view :deep(.ant-tree-node-content-wrapper:hover) {
background-color: var(--ant-color-fill-secondary);
}
.tree-item-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.item-name {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--ant-color-text);
word-break: break-word;
padding-right: 8px;
}
.status-tag {
flex-shrink: 0;
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
@@ -205,17 +124,9 @@ watch(() => props.userQueue, () => {
border-bottom: 1px solid var(--ant-color-border, #424242);
}
.section-header h3 {
color: var(--ant-color-text-heading, #ffffff);
}
.overview-tree-view :deep(.ant-tree-node-content-wrapper:hover) {
background-color: var(--ant-color-fill);
}
.item-name {
color: var(--ant-color-text, #ffffff);
}
}
@media (max-width: 768px) {

View File

@@ -70,8 +70,7 @@
<div class="status-container">
<div class="overview-panel-container">
<TaskOverviewPanel
:task-queue="tab.taskQueue"
:user-queue="tab.userQueue"
:ref="el => setOverviewRef(el, tab.key)"
/>
</div>
<div class="log-panel-container">
@@ -192,6 +191,9 @@ const {
// 初始化与清理
loadTaskOptions,
cleanup,
// 新增:任务总览面板引用管理
setOverviewRef,
} = useSchedulerLogic()
// Tab 操作

View File

@@ -78,6 +78,7 @@ export function useSchedulerLogic() {
const activeSchedulerTab = ref(schedulerTabs.value[0]?.key || 'main')
const logRefs = ref(new Map<string, HTMLElement>())
const overviewRefs = ref(new Map<string, any>()) // 任务总览面板引用
let tabCounter =
schedulerTabs.value.length > 1
? Math.max(
@@ -185,6 +186,9 @@ export function useSchedulerLogic() {
// 清理日志引用
logRefs.value.delete(key)
// 清理任务总览面板引用
overviewRefs.value.delete(key)
schedulerTabs.value.splice(idx, 1)
@@ -312,6 +316,18 @@ export function useSchedulerLogic() {
}
const handleUpdateMessage = (tab: SchedulerTab, data: any) => {
// 直接将 WebSocket 消息传递给 TaskOverviewPanel
const overviewPanel = overviewRefs.value.get(tab.key)
if (overviewPanel && overviewPanel.handleWSMessage) {
const wsMessage = {
type: 'Update',
id: tab.websocketId,
data: data
}
console.log('传递 WebSocket 消息给 TaskOverviewPanel:', wsMessage)
overviewPanel.handleWSMessage(wsMessage)
}
// 处理task_dict初始化消息
if (data.task_dict && Array.isArray(data.task_dict)) {
// 初始化任务队列
@@ -451,6 +467,15 @@ export function useSchedulerLogic() {
}
}
const setOverviewRef = (el: any, key: string) => {
if (el) {
overviewRefs.value.set(key, el)
console.log('设置 TaskOverviewPanel 引用:', key, el)
} else {
overviewRefs.value.delete(key)
}
}
// 电源操作
const onPowerActionChange = (value: PowerIn.signal) => {
powerAction.value = value
@@ -602,5 +627,8 @@ export function useSchedulerLogic() {
// 初始化与清理
loadTaskOptions,
cleanup,
// 任务总览面板引用管理
setOverviewRef,
}
}