refactor: 重构调度中心的任务总览部分
This commit is contained in:
527
frontend/src/components/TaskTree.vue
Normal file
527
frontend/src/components/TaskTree.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<div class="task-tree-container">
|
||||
<div v-if="taskData.length === 0" class="empty-state">
|
||||
<a-empty description="暂无任务数据" />
|
||||
</div>
|
||||
<div v-else class="task-tree">
|
||||
<div
|
||||
v-for="script in taskData"
|
||||
:key="script.script_id"
|
||||
class="script-card"
|
||||
>
|
||||
<!-- 脚本级别 -->
|
||||
<div
|
||||
class="script-header"
|
||||
@click="toggleScript(script.script_id)"
|
||||
>
|
||||
<div class="script-content">
|
||||
<div class="script-info">
|
||||
<CaretDownOutlined
|
||||
v-if="expandedScripts.has(script.script_id)"
|
||||
class="expand-icon"
|
||||
/>
|
||||
<CaretRightOutlined
|
||||
v-else
|
||||
class="expand-icon"
|
||||
/>
|
||||
<span class="script-name">{{ script.name }}</span>
|
||||
<span class="user-count" v-if="script.user_list && script.user_list.length > 0">
|
||||
({{ script.user_list.length }}个用户)
|
||||
</span>
|
||||
</div>
|
||||
<a-tag
|
||||
:color="getStatusColor(script.status)"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ script.status }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div
|
||||
v-show="expandedScripts.has(script.script_id)"
|
||||
class="user-list"
|
||||
>
|
||||
<div v-if="!script.user_list || script.user_list.length === 0" class="no-users">
|
||||
<div class="no-users-content">
|
||||
<span class="no-users-text">暂无用户</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(user, index) in script.user_list"
|
||||
:key="user.user_id"
|
||||
class="user-item"
|
||||
:class="{ 'last-item': index === script.user_list.length - 1 }"
|
||||
>
|
||||
<div class="user-content">
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
<a-tag
|
||||
:color="getStatusColor(user.status)"
|
||||
size="small"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ user.status }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
interface User {
|
||||
user_id: string
|
||||
status: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Script {
|
||||
script_id: string
|
||||
status: string
|
||||
name: string
|
||||
user_list: User[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
taskData: Script[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 展开的脚本集合
|
||||
const expandedScripts = ref<Set<string>>(new Set())
|
||||
|
||||
// 切换脚本展开状态
|
||||
const toggleScript = (scriptId: string) => {
|
||||
if (expandedScripts.value.has(scriptId)) {
|
||||
expandedScripts.value.delete(scriptId)
|
||||
} else {
|
||||
expandedScripts.value.add(scriptId)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const statusColorMap: Record<string, string> = {
|
||||
'等待': 'orange',
|
||||
'运行中': 'blue',
|
||||
'已完成': 'green',
|
||||
'失败': 'red',
|
||||
'暂停': 'gray',
|
||||
'取消': 'default'
|
||||
}
|
||||
return statusColorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 初始化时展开所有脚本
|
||||
const initExpandedScripts = () => {
|
||||
console.log('初始化展开脚本,数据:', props.taskData)
|
||||
props.taskData.forEach(script => {
|
||||
console.log('添加展开脚本:', script.script_id, script.name)
|
||||
expandedScripts.value.add(script.script_id)
|
||||
})
|
||||
console.log('展开的脚本集合:', Array.from(expandedScripts.value))
|
||||
}
|
||||
|
||||
// 监听数据变化,自动展开新的脚本
|
||||
const updateExpandedScripts = () => {
|
||||
console.log('更新展开脚本,当前数据:', props.taskData)
|
||||
props.taskData.forEach(script => {
|
||||
if (!expandedScripts.value.has(script.script_id)) {
|
||||
console.log('添加新脚本到展开列表:', script.script_id, script.name)
|
||||
expandedScripts.value.add(script.script_id)
|
||||
}
|
||||
})
|
||||
console.log('更新后展开的脚本集合:', Array.from(expandedScripts.value))
|
||||
}
|
||||
|
||||
// 监听 taskData 变化
|
||||
watch(() => props.taskData, (newData) => {
|
||||
console.log('TaskData 发生变化:', newData)
|
||||
if (newData && newData.length > 0) {
|
||||
updateExpandedScripts()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
// 暴露方法供父组件调用
|
||||
defineExpose({
|
||||
expandAll: () => {
|
||||
props.taskData.forEach(script => {
|
||||
expandedScripts.value.add(script.script_id)
|
||||
})
|
||||
},
|
||||
collapseAll: () => {
|
||||
expandedScripts.value.clear()
|
||||
},
|
||||
updateExpandedScripts
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-tree-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.task-tree {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.script-card {
|
||||
background: var(--ant-color-bg-container);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.script-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--ant-color-primary-border);
|
||||
}
|
||||
|
||||
.script-header {
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, var(--ant-color-fill-quaternary) 0%, var(--ant-color-fill-tertiary) 100%);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.script-header:hover {
|
||||
background: linear-gradient(135deg, var(--ant-color-fill-tertiary) 0%, var(--ant-color-fill-secondary) 100%);
|
||||
}
|
||||
|
||||
.script-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.script-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-primary);
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-icon:hover {
|
||||
color: var(--ant-color-primary-hover);
|
||||
}
|
||||
|
||||
.script-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ant-color-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.user-count {
|
||||
font-size: 12px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
background: var(--ant-color-bg-layout);
|
||||
}
|
||||
|
||||
.no-users {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.no-users-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--ant-color-border);
|
||||
}
|
||||
|
||||
.no-users-text {
|
||||
font-size: 13px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
.user-item.last-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-content:hover {
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text);
|
||||
word-break: break-word;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
[data-theme="dark"] .script-card,
|
||||
.dark .script-card {
|
||||
background: #1f1f1f;
|
||||
border-color: #424242;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .script-card:hover,
|
||||
.dark .script-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .script-header,
|
||||
.dark .script-header {
|
||||
background: linear-gradient(135deg, #262626 0%, #303030 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .script-header:hover,
|
||||
.dark .script-header:hover {
|
||||
background: linear-gradient(135deg, #303030 0%, #383838 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .script-name,
|
||||
.dark .script-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-count,
|
||||
.dark .user-count {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-list,
|
||||
.dark .user-list {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .no-users-content,
|
||||
.dark .no-users-content {
|
||||
background: #262626;
|
||||
border-color: #424242;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .no-users-text,
|
||||
.dark .no-users-text {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-item,
|
||||
.dark .user-item {
|
||||
border-bottom-color: #424242;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-content:hover,
|
||||
.dark .user-content:hover {
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .user-name,
|
||||
.dark .user-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .expand-icon,
|
||||
.dark .expand-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .expand-icon:hover,
|
||||
.dark .expand-icon:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 浅色模式适配 */
|
||||
[data-theme="light"] .script-card,
|
||||
.light .script-card,
|
||||
.script-card {
|
||||
background: #ffffff;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
[data-theme="light"] .script-card:hover,
|
||||
.light .script-card:hover,
|
||||
.script-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
[data-theme="light"] .script-header,
|
||||
.light .script-header,
|
||||
.script-header {
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
|
||||
}
|
||||
|
||||
[data-theme="light"] .script-header:hover,
|
||||
.light .script-header:hover,
|
||||
.script-header:hover {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
|
||||
}
|
||||
|
||||
[data-theme="light"] .script-name,
|
||||
.light .script-name,
|
||||
.script-name {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
[data-theme="light"] .user-count,
|
||||
.light .user-count,
|
||||
.user-count {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
[data-theme="light"] .user-list,
|
||||
.light .user-list,
|
||||
.user-list {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
[data-theme="light"] .no-users-content,
|
||||
.light .no-users-content,
|
||||
.no-users-content {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
[data-theme="light"] .no-users-text,
|
||||
.light .no-users-text,
|
||||
.no-users-text {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
[data-theme="light"] .user-item,
|
||||
.light .user-item,
|
||||
.user-item {
|
||||
border-bottom-color: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-theme="light"] .user-content:hover,
|
||||
.light .user-content:hover,
|
||||
.user-content:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
[data-theme="light"] .user-name,
|
||||
.light .user-name,
|
||||
.user-name {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
[data-theme="light"] .expand-icon,
|
||||
.light .expand-icon,
|
||||
.expand-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
[data-theme="light"] .expand-icon:hover,
|
||||
.light .expand-icon:hover,
|
||||
.expand-icon:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.task-tree {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.script-header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.script-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-count {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.user-content {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-users {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.user-list {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.script-card {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="quick-nav-page">
|
||||
<!-- 手动导航 -->
|
||||
<div class="debug-section">
|
||||
<h4>🎯 手动导航</h4>
|
||||
<div class="manual-nav">
|
||||
<input
|
||||
v-model="manualPath"
|
||||
@keyup.enter="navigateToManualPath"
|
||||
placeholder="输入路径 (例: /home, /scripts)"
|
||||
class="path-input"
|
||||
/>
|
||||
<button @click="navigateToManualPath" class="nav-go-btn">跳转</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<div class="debug-section">
|
||||
<h4>🚀 快捷导航</h4>
|
||||
@@ -48,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -79,6 +93,22 @@ const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
// 手动导航路径
|
||||
const manualPath = ref('')
|
||||
|
||||
// 手动导航
|
||||
const navigateToManualPath = () => {
|
||||
if (manualPath.value.trim()) {
|
||||
let path = manualPath.value.trim()
|
||||
// 确保路径以 / 开头
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path
|
||||
}
|
||||
router.push(path)
|
||||
manualPath.value = '' // 清空输入框
|
||||
}
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
const clearStorage = () => {
|
||||
if (confirm('确定要清除所有本地存储数据吗?')) {
|
||||
@@ -196,4 +226,48 @@ const toggleConsole = () => {
|
||||
.desc {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.manual-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.path-input {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.path-input:focus {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.path-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.nav-go-btn {
|
||||
padding: 4px 12px;
|
||||
background: #2196f3;
|
||||
border: 1px solid #1976d2;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-go-btn:hover {
|
||||
background: #1976d2;
|
||||
border-color: #1565c0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 操作
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user