feat(queue): 初步实现调度队列功能
- 添加队列列表获取、队列数据加载和保存功能 - 实现队列名称编辑、状态切换和删除功能 - 添加定时项和队列项的数据刷新和展示 - 优化队列界面样式,包括空状态、队列头部、配置区域等
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ant-design-vue": "4.x",
|
||||
"dayjs": "^1.11.13",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "4"
|
||||
},
|
||||
|
||||
485
frontend/src/components/queue/QueueItemManager.vue
Normal file
485
frontend/src/components/queue/QueueItemManager.vue
Normal file
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<a-card title="队列项" class="queue-item-card" :loading="loading">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addQueueItem" :loading="loading">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加队列项
|
||||
</a-button>
|
||||
<a-button @click="refreshData" :loading="loading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="queue-items-grid">
|
||||
<div
|
||||
v-for="item in queueItems"
|
||||
:key="item.id"
|
||||
class="queue-item-card-item"
|
||||
>
|
||||
<div class="item-header">
|
||||
<div class="item-name">{{ item.name || `项目 ${item.id}` }}</div>
|
||||
<a-dropdown>
|
||||
<a-button size="small" type="text">
|
||||
<MoreOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="editQueueItem(item)">
|
||||
<EditOutlined />
|
||||
编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteQueueItem(item.id)" danger>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-info">
|
||||
<div class="info-row">
|
||||
<span class="label">状态:</span>
|
||||
<a-tag :color="getStatusColor(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="info-row" v-if="item.script">
|
||||
<span class="label">脚本:</span>
|
||||
<span class="value">{{ item.script }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="item.plan">
|
||||
<span class="label">计划:</span>
|
||||
<span class="value">{{ item.plan }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="item.description">
|
||||
<span class="label">描述:</span>
|
||||
<span class="value">{{ item.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!queueItems.length && !loading" class="empty-state">
|
||||
<a-empty description="暂无队列项数据" />
|
||||
</div>
|
||||
|
||||
<!-- 队列项编辑弹窗 -->
|
||||
<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="name">
|
||||
<a-input v-model:value="form.name" placeholder="请输入项目名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="关联脚本" name="script">
|
||||
<a-select
|
||||
v-model:value="form.script"
|
||||
placeholder="请选择关联脚本"
|
||||
allow-clear
|
||||
:options="scriptOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联计划" name="plan">
|
||||
<a-select
|
||||
v-model:value="form.plan"
|
||||
placeholder="请选择关联计划"
|
||||
allow-clear
|
||||
:options="planOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="form.status" placeholder="请选择状态">
|
||||
<a-select-option value="active">激活</a-select-option>
|
||||
<a-select-option value="inactive">未激活</a-select-option>
|
||||
<a-select-option value="pending">等待中</a-select-option>
|
||||
<a-select-option value="running">运行中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="failed">失败</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="form.description"
|
||||
placeholder="请输入队列项描述(可选)"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
MoreOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Service } from '@/api'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
queueId: string
|
||||
queueItems: any[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const editingQueueItem = ref<any>(null)
|
||||
|
||||
// 选项数据
|
||||
const scriptOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const planOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
// 表单引用和数据
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
name: '',
|
||||
script: '',
|
||||
plan: '',
|
||||
status: 'active',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 计算属性 - 使用props传入的数据
|
||||
const queueItems = ref(props.queueItems)
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.queueItems, (newQueueItems) => {
|
||||
queueItems.value = newQueueItems
|
||||
}, { deep: true })
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '激活',
|
||||
inactive: '未激活',
|
||||
pending: '等待中',
|
||||
running: '运行中',
|
||||
completed: '已完成',
|
||||
failed: '失败'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'green',
|
||||
inactive: 'default',
|
||||
pending: 'orange',
|
||||
running: 'blue',
|
||||
completed: 'cyan',
|
||||
failed: 'red'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
// 加载脚本和计划选项
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
// 加载脚本选项
|
||||
const scriptsResponse = await Service.getScriptsApiScriptsGetPost({})
|
||||
if (scriptsResponse.code === 200) {
|
||||
scriptOptions.value = scriptsResponse.index.map((item: any) => ({
|
||||
label: Object.values(item)[0] as string,
|
||||
value: Object.keys(item)[0]
|
||||
}))
|
||||
}
|
||||
|
||||
// 加载计划选项
|
||||
const plansResponse = await Service.getPlanApiPlanGetPost({})
|
||||
if (plansResponse.code === 200) {
|
||||
planOptions.value = plansResponse.index.map((item: any) => ({
|
||||
label: Object.values(item)[0] as string,
|
||||
value: Object.keys(item)[0]
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 添加队列项
|
||||
const addQueueItem = () => {
|
||||
editingQueueItem.value = null
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
script: '',
|
||||
plan: '',
|
||||
status: 'active',
|
||||
description: ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑队列项
|
||||
const editQueueItem = (item: any) => {
|
||||
editingQueueItem.value = item
|
||||
Object.assign(form, {
|
||||
name: item.name || '',
|
||||
script: item.script || '',
|
||||
plan: item.plan || '',
|
||||
status: item.status || 'active',
|
||||
description: item.description || ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 保存队列项
|
||||
const saveQueueItem = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
|
||||
if (editingQueueItem.value) {
|
||||
// 更新队列项 - 根据API文档格式
|
||||
const response = await Service.updateItemApiQueueItemUpdatePost({
|
||||
queueId: props.queueId,
|
||||
queueItemId: editingQueueItem.value.id,
|
||||
data: {
|
||||
Info: {
|
||||
name: form.name,
|
||||
script: form.script,
|
||||
plan: form.plan,
|
||||
status: form.status,
|
||||
description: form.description
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success('队列项更新成功')
|
||||
} else {
|
||||
message.error('队列项更新失败: ' + (response.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 添加队列项 - 先创建,再更新
|
||||
// 1. 先创建队列项,只传queueId
|
||||
const createResponse = await Service.addItemApiQueueItemAddPost({
|
||||
queueId: props.queueId
|
||||
})
|
||||
|
||||
// 2. 用返回的queueItemId更新队列项数据
|
||||
if (createResponse.code === 200 && createResponse.queueItemId) {
|
||||
const updateResponse = await Service.updateItemApiQueueItemUpdatePost({
|
||||
queueId: props.queueId,
|
||||
queueItemId: createResponse.queueItemId,
|
||||
data: {
|
||||
Info: {
|
||||
name: form.name,
|
||||
script: form.script,
|
||||
plan: form.plan,
|
||||
status: form.status,
|
||||
description: form.description
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (updateResponse.code === 200) {
|
||||
message.success('队列项添加成功')
|
||||
} else {
|
||||
message.error('队列项添加失败: ' + (updateResponse.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
message.error('创建队列项失败: ' + (createResponse.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error('保存队列项失败:', error)
|
||||
message.error('保存队列项失败: ' + (error?.message || '网络错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
modalVisible.value = false
|
||||
editingQueueItem.value = null
|
||||
}
|
||||
|
||||
// 删除队列项
|
||||
const deleteQueueItem = async (itemId: string) => {
|
||||
try {
|
||||
const response = await Service.deleteItemApiQueueItemDeletePost({
|
||||
queueId: props.queueId,
|
||||
queueItemId: itemId
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success('队列项删除成功')
|
||||
// 确保删除后刷新数据
|
||||
emit('refresh')
|
||||
} else {
|
||||
message.error('删除队列项失败: ' + (response.message || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除队列项失败:', error)
|
||||
message.error('删除队列项失败: ' + (error?.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.queue-item-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.queue-item-card :deep(.ant-card-head-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 队列项网格样式 */
|
||||
.queue-items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.queue-item-card-item {
|
||||
background: var(--ant-color-bg-container);
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.queue-item-card-item:hover {
|
||||
border-color: var(--ant-color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ant-color-text);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--ant-color-text-secondary);
|
||||
min-width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--ant-color-text);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.queue-items-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.queue-items-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.queue-item-card-item {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
:deep(.ant-tag) {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
382
frontend/src/components/queue/TimeSetManager.vue
Normal file
382
frontend/src/components/queue/TimeSetManager.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<a-card title="定时项" class="time-set-card" :loading="loading">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addTimeSet" :loading="loading"
|
||||
:disabled="!props.queueId || props.queueId.trim() === ''">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加定时项
|
||||
</a-button>
|
||||
<a-button @click="refreshData" :loading="loading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table :columns="timeColumns" :data-source="timeSets" :pagination="false" size="middle" :scroll="{ x: 800 }">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'enabled'">
|
||||
<a-switch v-model:checked="record.enabled" @change="updateTimeSetStatus(record)" size="small" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'time'">
|
||||
<div class="time-display">
|
||||
{{ record.time || '--:--' }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="editTimeSet(record)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除这个定时项吗?" @confirm="deleteTimeSet(record.id)" ok-text="确定" cancel-text="取消">
|
||||
<a-button size="small" danger>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div v-if="!timeSets.length && !loading" class="empty-state">
|
||||
<a-empty :description="!props.queueId ? '请先选择一个队列' : '暂无定时项数据'" />
|
||||
</div>
|
||||
|
||||
<!-- 定时项编辑弹窗 -->
|
||||
<a-modal v-model:open="modalVisible" :title="editingTimeSet ? '编辑定时项' : '添加定时项'" @ok="saveTimeSet"
|
||||
@cancel="cancelEdit" :confirm-loading="saving">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-form-item label="执行时间" name="time">
|
||||
<a-time-picker v-model:value="form.time" format="HH:mm" placeholder="请选择执行时间" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态" name="enabled">
|
||||
<a-switch v-model:checked="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Service } from '@/api'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 时间处理工具函数
|
||||
const parseTimeString = (timeStr: string) => {
|
||||
if (!timeStr) return undefined
|
||||
try {
|
||||
const [hours, minutes] = timeStr.split(':').map(Number)
|
||||
if (isNaN(hours) || isNaN(minutes)) return undefined
|
||||
return dayjs().hour(hours).minute(minutes).second(0).millisecond(0)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const formatTimeValue = (timeValue: any) => {
|
||||
if (!timeValue) return '00:00'
|
||||
try {
|
||||
if (dayjs.isDayjs(timeValue)) {
|
||||
return timeValue.format('HH:mm')
|
||||
}
|
||||
return dayjs(timeValue).format('HH:mm')
|
||||
} catch {
|
||||
return '00:00'
|
||||
}
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
queueId: string
|
||||
timeSets: any[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const editingTimeSet = ref<any>(null)
|
||||
|
||||
// 表单引用和数据
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
time: undefined as any,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
time: [{ required: true, message: '请选择执行时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
const timeColumns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
customRender: ({ index }: { index: number }) => index + 1
|
||||
},
|
||||
{
|
||||
title: '执行时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性 - 使用props传入的数据
|
||||
const timeSets = ref([...props.timeSets])
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.timeSets, (newTimeSets) => {
|
||||
timeSets.value = [...newTimeSets]
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 添加定时项
|
||||
const addTimeSet = () => {
|
||||
editingTimeSet.value = null
|
||||
Object.assign(form, {
|
||||
time: undefined,
|
||||
enabled: true,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑定时项
|
||||
const editTimeSet = (timeSet: any) => {
|
||||
editingTimeSet.value = timeSet
|
||||
|
||||
// 安全地处理时间值
|
||||
const timeValue = parseTimeString(timeSet.time)
|
||||
|
||||
Object.assign(form, {
|
||||
time: timeValue,
|
||||
enabled: timeSet.enabled,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 保存定时项
|
||||
const saveTimeSet = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
|
||||
// 验证queueId是否存在
|
||||
if (!props.queueId || props.queueId.trim() === '') {
|
||||
message.error('队列ID为空,无法添加定时项')
|
||||
saving.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 处理时间格式 - 使用工具函数
|
||||
console.log('form.time:', form.time, 'type:', typeof form.time, 'isDayjs:', dayjs.isDayjs(form.time))
|
||||
|
||||
const timeString = formatTimeValue(form.time)
|
||||
console.log('timeString:', timeString)
|
||||
|
||||
if (editingTimeSet.value) {
|
||||
// 更新定时项
|
||||
const response = await Service.updateTimeSetApiQueueTimeUpdatePost({
|
||||
queueId: props.queueId,
|
||||
timeSetId: editingTimeSet.value.id,
|
||||
data: {
|
||||
Info: {
|
||||
Enabled: form.enabled,
|
||||
Time: timeString
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success('定时项更新成功')
|
||||
} else {
|
||||
message.error('定时项更新失败: ' + (response.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 添加定时项 - 先创建,再更新
|
||||
const createResponse = await Service.addTimeSetApiQueueTimeAddPost({
|
||||
queueId: props.queueId
|
||||
})
|
||||
|
||||
if (createResponse.code === 200 && createResponse.timeSetId) {
|
||||
const updateResponse = await Service.updateTimeSetApiQueueTimeUpdatePost({
|
||||
queueId: props.queueId,
|
||||
timeSetId: createResponse.timeSetId,
|
||||
data: {
|
||||
Info: {
|
||||
Enabled: form.enabled,
|
||||
Time: timeString,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (updateResponse.code === 200) {
|
||||
message.success('定时项添加成功')
|
||||
} else {
|
||||
message.error('定时项添加失败: ' + (updateResponse.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
message.error('创建定时项失败: ' + (createResponse.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
emit('refresh')
|
||||
} catch (error) {
|
||||
console.error('保存定时项失败:', error)
|
||||
message.error('保存定时项失败: ' + (error?.message || '网络错误'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
modalVisible.value = false
|
||||
editingTimeSet.value = null
|
||||
}
|
||||
|
||||
// 更新定时项状态
|
||||
const updateTimeSetStatus = async (timeSet: any) => {
|
||||
try {
|
||||
const response = await Service.updateTimeSetApiQueueTimeUpdatePost({
|
||||
queueId: props.queueId,
|
||||
timeSetId: timeSet.id,
|
||||
data: {
|
||||
Info: {
|
||||
Enabled: timeSet.enabled
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success('状态更新成功')
|
||||
} else {
|
||||
message.error('状态更新失败: ' + (response.message || '未知错误'))
|
||||
// 回滚状态
|
||||
timeSet.enabled = !timeSet.enabled
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新状态失败:', error)
|
||||
message.error('更新状态失败: ' + (error?.message || '网络错误'))
|
||||
// 回滚状态
|
||||
timeSet.enabled = !timeSet.enabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 删除定时项
|
||||
const deleteTimeSet = async (timeSetId: string) => {
|
||||
try {
|
||||
const response = await Service.deleteTimeSetApiQueueTimeDeletePost({
|
||||
queueId: props.queueId,
|
||||
timeSetId
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
message.success('定时项删除成功')
|
||||
// 确保删除后刷新数据
|
||||
emit('refresh')
|
||||
} else {
|
||||
message.error('删除定时项失败: ' + (response.message || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除定时项失败:', error)
|
||||
message.error('删除定时项失败: ' + (error?.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.time-set-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.time-set-card :deep(.ant-card-head-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 时间选择器样式 */
|
||||
:deep(.ant-picker) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 开关样式 */
|
||||
:deep(.ant-switch) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 时间显示样式 */
|
||||
.time-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--ant-color-text);
|
||||
padding: 4px 8px;
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -29,22 +29,27 @@
|
||||
<!-- 活动信息展示 -->
|
||||
<div v-if="currentActivity && !loading" class="activity-info">
|
||||
<div class="activity-header">
|
||||
<div class="activity-name">
|
||||
<!-- <CalendarOutlined class="activity-icon" />-->
|
||||
<span class="activity-title">{{ currentActivity.StageName }}</span>
|
||||
<a-tag color="blue" class="activity-tip">{{ currentActivity.Tip }}</a-tag>
|
||||
</div>
|
||||
<div class="activity-time">
|
||||
<div class="time-item">
|
||||
<ClockCircleOutlined class="time-icon" />
|
||||
<span class="time-label">剩余时间:</span>
|
||||
<span class="time-value remaining">{{ getTimeRemaining(currentActivity.UtcExpireTime, currentActivity.TimeZone) }}</span>
|
||||
<div class="activity-left">
|
||||
<div class="activity-name">
|
||||
<span class="activity-title">{{ currentActivity.StageName }}</span>
|
||||
<a-tag color="blue" class="activity-tip">{{ currentActivity.Tip }}</a-tag>
|
||||
</div>
|
||||
<div class="time-item">
|
||||
<div class="activity-end-time">
|
||||
<ClockCircleOutlined class="time-icon" />
|
||||
<span class="time-label">结束时间:</span>
|
||||
<span class="time-value">{{ formatTime(currentActivity.UtcExpireTime, currentActivity.TimeZone) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-right">
|
||||
<a-statistic-countdown
|
||||
title="当期活动剩余时间"
|
||||
:value="getCountdownValue(currentActivity.UtcExpireTime)"
|
||||
format="D 天 H 时 m 分"
|
||||
:value-style="getCountdownStyle(currentActivity.UtcExpireTime)"
|
||||
@finish="onCountdownFinish"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,31 +141,52 @@ const formatTime = (timeString: string, timeZone: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算活动剩余时间
|
||||
const getTimeRemaining = (expireTime: string, timeZone: number) => {
|
||||
// 获取倒计时的目标时间戳
|
||||
const getCountdownValue = (expireTime: string) => {
|
||||
try {
|
||||
return new Date(expireTime).getTime()
|
||||
} catch {
|
||||
return Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取倒计时样式 - 如果剩余时间小于2天则显示红色
|
||||
const getCountdownStyle = (expireTime: string) => {
|
||||
try {
|
||||
const expire = new Date(expireTime)
|
||||
const now = new Date()
|
||||
const remaining = expire.getTime() - now.getTime()
|
||||
const twoDaysInMs = 2 * 24 * 60 * 60 * 1000
|
||||
|
||||
if (remaining <= 0) return '已结束'
|
||||
if (remaining <= twoDaysInMs) {
|
||||
return {
|
||||
color: '#ff4d4f',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '18px'
|
||||
}
|
||||
}
|
||||
|
||||
const days = Math.floor(remaining / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天${hours}小时`
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时`
|
||||
} else {
|
||||
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60))
|
||||
return `${minutes}分钟`
|
||||
return {
|
||||
color: 'var(--ant-color-text)',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px'
|
||||
}
|
||||
} catch {
|
||||
return '未知'
|
||||
return {
|
||||
color: 'var(--ant-color-text)',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 倒计时结束回调
|
||||
const onCountdownFinish = () => {
|
||||
message.warning('活动已结束')
|
||||
// 重新获取数据
|
||||
fetchActivityData()
|
||||
}
|
||||
|
||||
const getMaterialImage = (dropName: string) => {
|
||||
try {
|
||||
return new URL(`../assets/materials/${dropName}.png`, import.meta.url).href
|
||||
@@ -243,15 +269,35 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.activity-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-right {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.activity-end-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,755 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>调度队列</h1>
|
||||
<p>这里是调度队列内容的占位符</p>
|
||||
<div v-if="loading" class="loading-box">
|
||||
<a-spin tip="加载中,请稍候..." size="large" />
|
||||
</div>
|
||||
|
||||
<div v-else class="queue-content">
|
||||
<!-- 队列头部 -->
|
||||
<div class="queue-header">
|
||||
<div class="header-title">
|
||||
<h1>调度队列</h1>
|
||||
</div>
|
||||
<a-space size="middle">
|
||||
<a-button type="primary" size="large" @click="handleAddQueue">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新建队列
|
||||
</a-button>
|
||||
<a-button size="large" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!queueList.length || !currentQueueData" class="empty-state">
|
||||
<div class="empty-content empty-content-fancy" @click="handleAddQueue" style="cursor: pointer">
|
||||
<div class="empty-icon">
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
<h2>你还没有创建过队列</h2>
|
||||
<h1>点击此处来新建队列</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 队列内容 -->
|
||||
<div class="queue-main-content" v-else-if="currentQueueData">
|
||||
<!-- 队列选择器 -->
|
||||
<div class="queue-selector">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeQueueId"
|
||||
type="editable-card"
|
||||
@edit="onTabEdit"
|
||||
@change="onQueueChange"
|
||||
class="queue-tabs"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="queue in queueList"
|
||||
:key="queue.id"
|
||||
:tab="queue.name"
|
||||
:closable="queueList.length > 1"
|
||||
/>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 队列配置区域 -->
|
||||
<div class="queue-config-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<div class="queue-name-editor">
|
||||
<a-input
|
||||
v-model:value="currentQueueName"
|
||||
placeholder="请输入队列名称"
|
||||
size="large"
|
||||
class="queue-name-input"
|
||||
@blur="onQueueNameBlur"
|
||||
@pressEnter="onQueueNameBlur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-controls">
|
||||
<a-space>
|
||||
<span class="status-label">状态:</span>
|
||||
<a-switch
|
||||
v-model:checked="currentQueueEnabled"
|
||||
@change="onQueueStatusChange"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 定时项组件 -->
|
||||
<TimeSetManager
|
||||
v-if="activeQueueId && currentQueueData"
|
||||
:queue-id="activeQueueId"
|
||||
:time-sets="currentTimeSets"
|
||||
@refresh="refreshTimeSets"
|
||||
/>
|
||||
|
||||
<!-- 队列项组件 -->
|
||||
<QueueItemManager
|
||||
v-if="activeQueueId && currentQueueData"
|
||||
:queue-id="activeQueueId"
|
||||
:queue-items="currentQueueItems"
|
||||
@refresh="refreshQueueItems"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮保存按钮 -->
|
||||
<a-float-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
class="float-button"
|
||||
:style="{ right: '24px' }"
|
||||
>
|
||||
<template #icon>
|
||||
<SaveOutlined />
|
||||
</template>
|
||||
</a-float-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { Service } from '@/api'
|
||||
import TimeSetManager from '@/components/queue/TimeSetManager.vue'
|
||||
import QueueItemManager from '@/components/queue/QueueItemManager.vue'
|
||||
|
||||
// 队列列表和当前选中的队列
|
||||
const queueList = ref<Array<{ id: string; name: string }>>([])
|
||||
const activeQueueId = ref<string>('')
|
||||
const currentQueueData = ref<Record<string, any> | null>(null)
|
||||
|
||||
// 当前队列的名称和状态
|
||||
const currentQueueName = ref<string>('')
|
||||
const currentQueueEnabled = ref<boolean>(true)
|
||||
|
||||
// 当前队列的定时项和队列项
|
||||
const currentTimeSets = ref<any[]>([])
|
||||
const currentQueueItems = ref<any[]>([])
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
// 获取队列列表
|
||||
const fetchQueues = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await Service.getQueuesApiQueueGetPost({})
|
||||
if (response.code === 200) {
|
||||
// 处理队列数据
|
||||
console.log('API Response:', response) // 调试日志
|
||||
|
||||
if (response.index && response.index.length > 0) {
|
||||
queueList.value = response.index.map((item: any, index: number) => {
|
||||
try {
|
||||
// API响应格式: {"uid": "xxx", "type": "QueueConfig"}
|
||||
const queueId = item.uid
|
||||
const queueName = response.data[queueId]?.Info?.Name || `队列 ${index + 1}`
|
||||
console.log('Queue ID:', queueId, 'Name:', queueName, 'Type:', typeof queueId) // 调试日志
|
||||
return {
|
||||
id: queueId,
|
||||
name: queueName
|
||||
}
|
||||
} catch (itemError) {
|
||||
console.warn('解析队列项失败:', itemError, item)
|
||||
return {
|
||||
id: `queue_${index}`,
|
||||
name: `队列 ${index + 1}`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有队列且没有选中的队列,默认选中第一个
|
||||
if (queueList.value.length > 0 && !activeQueueId.value) {
|
||||
activeQueueId.value = queueList.value[0].id
|
||||
console.log('Selected queue ID:', activeQueueId.value) // 调试日志
|
||||
// 使用nextTick确保DOM更新后再加载数据
|
||||
nextTick(() => {
|
||||
loadQueueData(activeQueueId.value).catch(error => {
|
||||
console.error('加载队列数据失败:', error)
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('No queues found in response') // 调试日志
|
||||
queueList.value = []
|
||||
currentQueueData.value = null
|
||||
}
|
||||
} else {
|
||||
console.error('API响应错误:', response)
|
||||
queueList.value = []
|
||||
currentQueueData.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取队列列表失败:', error)
|
||||
queueList.value = []
|
||||
currentQueueData.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载队列数据
|
||||
const loadQueueData = async (queueId: string) => {
|
||||
if (!queueId) return
|
||||
|
||||
try {
|
||||
const response = await Service.getQueuesApiQueueGetPost({})
|
||||
currentQueueData.value = response.data
|
||||
|
||||
// 根据API响应数据更新队列信息
|
||||
if (response.data && response.data[queueId]) {
|
||||
const queueData = response.data[queueId]
|
||||
|
||||
// 更新队列名称和状态
|
||||
const currentQueue = queueList.value.find(queue => queue.id === queueId)
|
||||
if (currentQueue) {
|
||||
currentQueueName.value = currentQueue.name
|
||||
}
|
||||
currentQueueEnabled.value = queueData.enabled ?? true
|
||||
|
||||
// 使用nextTick确保DOM更新后再加载数据
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// 加载定时项和队列项数据 - 添加错误处理
|
||||
try {
|
||||
await refreshTimeSets()
|
||||
} catch (timeError) {
|
||||
console.error('刷新定时项失败:', timeError)
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshQueueItems()
|
||||
} catch (itemError) {
|
||||
console.error('刷新队列项失败:', itemError)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载队列数据失败:', error)
|
||||
// 不显示错误消息,避免干扰用户体验
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新定时项数据
|
||||
const refreshTimeSets = async () => {
|
||||
if (!activeQueueId.value) {
|
||||
currentTimeSets.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 重新从API获取最新的队列数据
|
||||
const response = await Service.getQueuesApiQueueGetPost({})
|
||||
if (response.code !== 200) {
|
||||
console.error('获取队列数据失败:', response)
|
||||
currentTimeSets.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 更新缓存的队列数据
|
||||
currentQueueData.value = response.data
|
||||
|
||||
// 从最新的队列数据中获取定时项信息
|
||||
if (response.data && response.data[activeQueueId.value]) {
|
||||
const queueData = response.data[activeQueueId.value]
|
||||
const timeSets: any[] = []
|
||||
|
||||
// 检查是否有TimeSet配置
|
||||
if (queueData?.SubConfigsInfo?.TimeSet) {
|
||||
const timeSetConfig = queueData.SubConfigsInfo.TimeSet
|
||||
|
||||
// 遍历instances数组获取所有定时项ID
|
||||
if (Array.isArray(timeSetConfig.instances)) {
|
||||
timeSetConfig.instances.forEach((instance: any) => {
|
||||
try {
|
||||
const timeSetId = instance?.uid
|
||||
if (!timeSetId) return
|
||||
|
||||
const timeSetData = timeSetConfig[timeSetId]
|
||||
if (timeSetData?.Info) {
|
||||
// 解析时间字符串 "HH:mm" - 修复字段名
|
||||
const originalTimeString = timeSetData.Info.Set || timeSetData.Info.Time || '00:00'
|
||||
const [hours = 0, minutes = 0] = originalTimeString.split(':').map(Number)
|
||||
|
||||
// 创建标准化的时间字符串
|
||||
const validHours = Math.max(0, Math.min(23, hours))
|
||||
const validMinutes = Math.max(0, Math.min(59, minutes))
|
||||
const timeString = `${validHours.toString().padStart(2, '0')}:${validMinutes.toString().padStart(2, '0')}`
|
||||
|
||||
timeSets.push({
|
||||
id: timeSetId,
|
||||
time: timeString,
|
||||
enabled: Boolean(timeSetData.Info.Enabled),
|
||||
description: timeSetData.Info.Description || ''
|
||||
})
|
||||
}
|
||||
} catch (itemError) {
|
||||
console.warn('解析单个定时项失败:', itemError, instance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用nextTick确保数据更新不会导致渲染问题
|
||||
await nextTick()
|
||||
currentTimeSets.value = [...timeSets]
|
||||
console.log('刷新后的定时项数据:', timeSets) // 调试日志
|
||||
} else {
|
||||
currentTimeSets.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新定时项列表失败:', error)
|
||||
currentTimeSets.value = []
|
||||
// 不显示错误消息,避免干扰用户
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新队列项数据
|
||||
const refreshQueueItems = async () => {
|
||||
if (!activeQueueId.value) {
|
||||
currentQueueItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 重新从API获取最新的队列数据
|
||||
const response = await Service.getQueuesApiQueueGetPost({})
|
||||
if (response.code !== 200) {
|
||||
console.error('获取队列数据失败:', response)
|
||||
currentQueueItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 更新缓存的队列数据
|
||||
currentQueueData.value = response.data
|
||||
|
||||
// 从最新的队列数据中获取队列项信息
|
||||
if (response.data && response.data[activeQueueId.value]) {
|
||||
const queueData = response.data[activeQueueId.value]
|
||||
const queueItems: any[] = []
|
||||
|
||||
// 检查是否有QueueItem配置
|
||||
if (queueData?.SubConfigsInfo?.QueueItem) {
|
||||
const queueItemConfig = queueData.SubConfigsInfo.QueueItem
|
||||
|
||||
// 遍历instances数组获取所有队列项ID
|
||||
if (Array.isArray(queueItemConfig.instances)) {
|
||||
queueItemConfig.instances.forEach((instance: any) => {
|
||||
try {
|
||||
const queueItemId = instance?.uid
|
||||
if (!queueItemId) return
|
||||
|
||||
const queueItemData = queueItemConfig[queueItemId]
|
||||
if (queueItemData?.Info) {
|
||||
queueItems.push({
|
||||
id: queueItemId,
|
||||
name: queueItemData.Info.name || `项目 ${queueItemId}`,
|
||||
script: queueItemData.Info.script || '',
|
||||
plan: queueItemData.Info.plan || '',
|
||||
status: queueItemData.Info.status || 'inactive',
|
||||
description: queueItemData.Info.description || ''
|
||||
})
|
||||
}
|
||||
} catch (itemError) {
|
||||
console.warn('解析单个队列项失败:', itemError, instance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用nextTick确保数据更新不会导致渲染问题
|
||||
await nextTick()
|
||||
currentQueueItems.value = [...queueItems]
|
||||
console.log('刷新后的队列项数据:', queueItems) // 调试日志
|
||||
} else {
|
||||
currentQueueItems.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新队列项列表失败:', error)
|
||||
currentQueueItems.value = []
|
||||
// 不显示错误消息,避免干扰用户
|
||||
}
|
||||
}
|
||||
|
||||
// 队列名称编辑失焦处理
|
||||
const onQueueNameBlur = () => {
|
||||
if (activeQueueId.value) {
|
||||
const currentQueue = queueList.value.find(queue => queue.id === activeQueueId.value)
|
||||
if (currentQueue) {
|
||||
currentQueue.name = currentQueueName.value || `队列 ${queueList.value.indexOf(currentQueue) + 1}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 队列状态切换处理
|
||||
const onQueueStatusChange = () => {
|
||||
// 状态切换时只更新本地状态,不自动保存
|
||||
}
|
||||
|
||||
// 标签页编辑处理
|
||||
const onTabEdit = async (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
|
||||
try {
|
||||
if (action === 'add') {
|
||||
await handleAddQueue()
|
||||
} else if (action === 'remove' && typeof targetKey === 'string') {
|
||||
await handleRemoveQueue(targetKey)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标签页操作失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加队列
|
||||
const handleAddQueue = async () => {
|
||||
try {
|
||||
const response = await Service.addQueueApiQueueAddPost()
|
||||
|
||||
if (response.code === 200 && response.queueId) {
|
||||
const defaultName = `队列 ${queueList.value.length + 1}`
|
||||
const newQueue = {
|
||||
id: response.queueId,
|
||||
name: defaultName,
|
||||
}
|
||||
queueList.value.push(newQueue)
|
||||
activeQueueId.value = newQueue.id
|
||||
|
||||
// 设置默认名称到输入框中
|
||||
currentQueueName.value = defaultName
|
||||
currentQueueEnabled.value = true
|
||||
|
||||
await loadQueueData(newQueue.id)
|
||||
message.success('队列创建成功')
|
||||
} else {
|
||||
message.error('队列创建失败: ' + (response.message || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加队列失败:', error)
|
||||
message.error('添加队列失败: ' + (error?.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除队列
|
||||
const handleRemoveQueue = async (queueId: string) => {
|
||||
try {
|
||||
const response = await Service.deleteQueueApiQueueDeletePost({ queueId })
|
||||
|
||||
if (response.code === 200) {
|
||||
const index = queueList.value.findIndex(queue => queue.id === queueId)
|
||||
if (index > -1) {
|
||||
queueList.value.splice(index, 1)
|
||||
if (activeQueueId.value === queueId) {
|
||||
activeQueueId.value = queueList.value[0]?.id || ''
|
||||
if (activeQueueId.value) {
|
||||
await loadQueueData(activeQueueId.value)
|
||||
} else {
|
||||
currentQueueData.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
message.success('队列删除成功')
|
||||
} else {
|
||||
message.error('删除队列失败: ' + (response.message || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除队列失败:', error)
|
||||
message.error('删除队列失败: ' + (error?.message || '网络错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 队列切换
|
||||
const onQueueChange = async (queueId: string) => {
|
||||
if (!queueId) return
|
||||
|
||||
try {
|
||||
// 清空当前数据,避免渲染问题
|
||||
currentTimeSets.value = []
|
||||
currentQueueItems.value = []
|
||||
|
||||
await loadQueueData(queueId)
|
||||
} catch (error) {
|
||||
console.error('队列切换失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动保存处理
|
||||
const handleSave = async () => {
|
||||
if (!activeQueueId.value) {
|
||||
message.warning('请先选择一个队列')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await saveQueueData()
|
||||
message.success('保存成功')
|
||||
} catch (error) {
|
||||
message.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存队列数据
|
||||
const saveQueueData = async () => {
|
||||
if (!activeQueueId.value) return
|
||||
|
||||
try {
|
||||
// 构建符合API要求的数据结构
|
||||
const queueData: Record<string, any> = {
|
||||
name: currentQueueName.value,
|
||||
enabled: currentQueueEnabled.value,
|
||||
// 这里可以添加其他需要保存的队列配置
|
||||
}
|
||||
|
||||
const response = await Service.updateQueueApiQueueUpdatePost({
|
||||
queueId: activeQueueId.value,
|
||||
data: queueData
|
||||
})
|
||||
|
||||
if (response.code !== 200) {
|
||||
throw new Error(response.message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存队列数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新队列列表
|
||||
const handleRefresh = async () => {
|
||||
loading.value = true
|
||||
await fetchQueues()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchQueues()
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
background: var(--ant-color-bg-container);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.empty-content h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--ant-color-text);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.empty-content p {
|
||||
font-size: 14px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
/* 队列内容区域 */
|
||||
.queue-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--ant-color-bg-container);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 队列头部样式 */
|
||||
.queue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
background: var(--ant-color-bg-container);
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
margin: 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;
|
||||
}
|
||||
|
||||
/* 队列选择器 */
|
||||
.queue-selector {
|
||||
padding: 0 32px;
|
||||
background: var(--ant-color-bg-container);
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
/* 队列主内容 */
|
||||
.queue-main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 队列配置区域 */
|
||||
.queue-config-section {
|
||||
flex: 1;
|
||||
padding: 24px 32px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--ant-color-bg-container);
|
||||
}
|
||||
|
||||
.section-title h3 {
|
||||
margin: 0;
|
||||
color: var(--ant-color-text);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 队列名称编辑器样式 */
|
||||
.queue-name-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.queue-name-input {
|
||||
max-width: 300px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.queue-name-input :deep(.ant-input) {
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--ant-color-text);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.queue-name-input :deep(.ant-input:hover) {
|
||||
border-color: var(--ant-color-border);
|
||||
}
|
||||
|
||||
.queue-name-input :deep(.ant-input:focus) {
|
||||
border-color: var(--ant-color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.float-button {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.empty-content-fancy {
|
||||
transition: box-shadow 0.3s, transform 0.2s;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 32px;
|
||||
border-radius: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.empty-content-fancy h2 {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: 1px;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.empty-content-fancy h1 {
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
margin: 0 0 12px 0;
|
||||
display: inline-block;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.queue-config-section {
|
||||
border-color: var(--ant-color-border-secondary);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-color: var(--ant-color-border-secondary);
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1682
frontend/yarn.lock
1682
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user