feat(queue): 完成调度队列页面并添加脚本下拉框功能

- 重新设计了队列页面布局,使用表格代替原来的卡片布局
- 添加了脚本下拉框功能,支持选择已有的脚本
- 简化了队列项的展示内容,只显示脚本名称
- 优化了队列项的编辑和删除操作
- 新增了获取脚本下拉框信息的 API 接口
This commit is contained in:
2025-08-12 22:26:15 +08:00
parent bc6ae5562e
commit 9fb25a2d33
18 changed files with 412 additions and 265 deletions

View File

@@ -7,7 +7,16 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { ComboBoxItem } from './models/ComboBoxItem';
export type { ComboBoxOut } from './models/ComboBoxOut';
export type { DispatchIn } from './models/DispatchIn';
export type { GlobalConfig } from './models/GlobalConfig';
export type { GlobalConfig_Function } from './models/GlobalConfig_Function';
export type { GlobalConfig_Notify } from './models/GlobalConfig_Notify';
export type { GlobalConfig_Start } from './models/GlobalConfig_Start';
export type { GlobalConfig_UI } from './models/GlobalConfig_UI';
export type { GlobalConfig_Update } from './models/GlobalConfig_Update';
export type { GlobalConfig_Voice } from './models/GlobalConfig_Voice';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { InfoOut } from './models/InfoOut';
export type { OutBase } from './models/OutBase';

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ComboBoxItem = {
/**
* 展示值
*/
label: string;
/**
* 实际值
*/
value: string;
};

View File

@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ComboBoxItem } from './ComboBoxItem';
export type ComboBoxOut = {
/**
* 状态码
*/
code?: number;
/**
* 操作状态
*/
status?: string;
/**
* 操作消息
*/
message?: string;
/**
* 下拉框选项
*/
data: Array<ComboBoxItem>;
};

View File

@@ -0,0 +1,37 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GlobalConfig_Function } from './GlobalConfig_Function';
import type { GlobalConfig_Notify } from './GlobalConfig_Notify';
import type { GlobalConfig_Start } from './GlobalConfig_Start';
import type { GlobalConfig_UI } from './GlobalConfig_UI';
import type { GlobalConfig_Update } from './GlobalConfig_Update';
import type { GlobalConfig_Voice } from './GlobalConfig_Voice';
export type GlobalConfig = {
/**
* 功能相关配置
*/
Function?: (GlobalConfig_Function | null);
/**
* 语音相关配置
*/
Voice?: (GlobalConfig_Voice | null);
/**
* 启动相关配置
*/
Start?: (GlobalConfig_Start | null);
/**
* 界面相关配置
*/
UI?: (GlobalConfig_UI | null);
/**
* 通知相关配置
*/
Notify?: (GlobalConfig_Notify | null);
/**
* 更新相关配置
*/
Update?: (GlobalConfig_Update | null);
};

View File

@@ -0,0 +1,31 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Function = {
/**
* 历史记录保留时间, 0表示永久保存
*/
HistoryRetentionTime?: (7 | 15 | 30 | 60 | 90 | 180 | 365 | 0 | null);
/**
* 允许休眠
*/
IfAllowSleep?: (boolean | null);
/**
* 静默模式
*/
IfSilence?: (boolean | null);
/**
* 模拟器老板键
*/
BossKey?: (string | null);
/**
* 同意哔哩哔哩用户协议
*/
IfAgreeBilibili?: (boolean | null);
/**
* 跳过Mumu模拟器启动广告
*/
IfSkipMumuSplashAds?: (boolean | null);
};

View File

@@ -0,0 +1,59 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Notify = {
/**
* 任务结果推送时机
*/
SendTaskResultTime?: ('不推送' | '任何时刻' | '仅失败时' | null);
/**
* 是否发送统计信息
*/
IfSendStatistic?: (boolean | null);
/**
* 是否发送公招六星通知
*/
IfSendSixStar?: (boolean | null);
/**
* 是否推送系统通知
*/
IfPushPlyer?: (boolean | null);
/**
* 是否发送邮件通知
*/
IfSendMail?: (boolean | null);
/**
* SMTP服务器地址
*/
SMTPServerAddress?: (string | null);
/**
* SMTP授权码
*/
AuthorizationCode?: (string | null);
/**
* 邮件发送地址
*/
FromAddress?: (string | null);
/**
* 邮件接收地址
*/
ToAddress?: (string | null);
/**
* 是否使用ServerChan推送
*/
IfServerChan?: (boolean | null);
/**
* ServerChan推送密钥
*/
ServerChanKey?: (string | null);
/**
* 是否使用企微Webhook推送
*/
IfCompanyWebHookBot?: (boolean | null);
/**
* 企微Webhook Bot URL
*/
CompanyWebHookBotUrl?: (string | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Start = {
/**
* 是否在系统启动时自动运行
*/
IfSelfStart?: (boolean | null);
/**
* 启动时是否直接最小化到托盘而不显示主窗口
*/
IfMinimizeDirectly?: (boolean | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_UI = {
/**
* 是否常态显示托盘图标
*/
IfShowTray?: (boolean | null);
/**
* 是否最小化到托盘
*/
IfToTray?: (boolean | null);
};

View File

@@ -0,0 +1,27 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Update = {
/**
* 是否自动更新
*/
IfAutoUpdate?: (boolean | null);
/**
* 更新类型, stable为稳定版, beta为测试版
*/
UpdateType?: ('stable' | 'beta' | null);
/**
* 更新源: GitHub源, Mirror酱源, 自建源
*/
Source?: ('GitHub' | 'MirrorChyan' | 'AutoSite' | null);
/**
* 网络代理地址
*/
ProxyAddress?: (string | null);
/**
* Mirror酱CDK
*/
MirrorChyanCDK?: (string | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Voice = {
/**
* 语音功能是否启用
*/
Enabled?: (boolean | null);
/**
* 语音类型, simple为简洁, noisy为聒噪
*/
Type?: ('simple' | 'noisy' | null);
};

View File

@@ -3,9 +3,15 @@
/* tslint:disable */
/* eslint-disable */
export type ScriptCreateIn = {
/**
* 脚本类型: MAA脚本, 通用脚本
*/
type: ScriptCreateIn.type;
};
export namespace ScriptCreateIn {
/**
* 脚本类型: MAA脚本, 通用脚本
*/
export enum type {
MAA = 'MAA',
GENERAL = 'General',

View File

@@ -2,6 +2,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GlobalConfig } from './GlobalConfig';
export type SettingGetOut = {
/**
* 状态码
@@ -18,6 +19,6 @@ export type SettingGetOut = {
/**
* 全局设置数据
*/
data: Record<string, Record<string, any>>;
data: GlobalConfig;
};

View File

@@ -2,10 +2,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GlobalConfig } from './GlobalConfig';
export type SettingUpdateIn = {
/**
* 全局设置更新数据
* 全局设置需要更新数据
*/
data: Record<string, Record<string, any>>;
data: GlobalConfig;
};

View File

@@ -17,9 +17,9 @@ export namespace TaskCreateIn {
* 任务模式
*/
export enum mode {
AutoMode = '自动代理',
ManualMode = '人工排查',
SettingScriptMode = '设置脚本',
AutoMode = '自动代理',
ManualMode = '人工排查',
SettingScriptMode = '设置脚本',
}
}

View File

@@ -2,6 +2,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ComboBoxOut } from '../models/ComboBoxOut';
import type { DispatchIn } from '../models/DispatchIn';
import type { InfoOut } from '../models/InfoOut';
import type { OutBase } from '../models/OutBase';
@@ -58,6 +59,17 @@ export class Service {
url: '/api/info/stage',
});
}
/**
* 获取脚本下拉框信息
* @returns ComboBoxOut Successful Response
* @throws ApiError
*/
public static getScriptComboxApiInfoComboxScriptPost(): CancelablePromise<ComboBoxOut> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/info/combox/script',
});
}
/**
* 获取通知信息
* @returns InfoOut Successful Response

View File

@@ -8,120 +8,50 @@
</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-table
:columns="queueColumns"
:data-source="queueItems"
:pagination="false"
size="middle"
:scroll="{ x: 600 }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'">
{{ index + 1 }}个脚本
</template>
<template v-else-if="column.key === 'script'">
{{ getScriptName(record.script) }}
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button size="small" @click="editQueueItem(record)">
<EditOutlined />
编辑
</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>
<a-popconfirm title="确定要删除这个队列项吗?" @confirm="deleteQueueItem(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="!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-modal v-model:open="modalVisible" :title="editingQueueItem ? '编辑队列项' : '添加队列项'" @ok="saveQueueItem"
@cancel="cancelEdit" :confirm-loading="saving" width="600px">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="关联脚本" name="script">
<a-select
v-model:value="form.script"
placeholder="请选择关联脚本"
allow-clear
:options="scriptOptions"
/>
</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-select v-model:value="form.script" placeholder="请选择关联脚本" allow-clear :options="scriptOptions" />
</a-form-item>
</a-form>
</a-modal>
@@ -129,7 +59,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import { ref, reactive, watch, onMounted, h } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
@@ -162,24 +92,45 @@ const editingQueueItem = ref<any>(null)
// 选项数据
const scriptOptions = ref<Array<{ label: string; value: string }>>([])
const planOptions = ref<Array<{ label: string; value: string }>>([])
// 获取脚本名称
const getScriptName = (scriptId: string) => {
if (!scriptId) return '未选择脚本'
const script = scriptOptions.value.find(script => script.value === scriptId)
return script?.label || '未知脚本'
}
// 表单引用和数据
const formRef = ref<FormInstance>()
const form = reactive({
name: '',
script: '',
plan: '',
status: 'active',
description: ''
script: ''
})
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
script: [{ required: true, message: '请选择关联脚本', trigger: 'change' }]
}
// 表格列配置
const queueColumns = [
{
title: '序号',
key: 'index',
width: 150,
},
{
title: '脚本名称',
key: 'script',
width: 200,
},
{
title: '操作',
key: 'actions',
width: 150,
fixed: 'right',
},
]
// 计算属性 - 使用props传入的数据
const queueItems = ref(props.queueItems)
@@ -188,85 +139,50 @@ 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]
}))
}
console.log('开始加载脚本选项...')
// 使用正确的API获取脚本下拉框选项
const scriptsResponse = await Service.getScriptComboxApiInfoComboxScriptPost()
console.log('脚本API响应:', scriptsResponse)
// 加载计划选项
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]
}))
if (scriptsResponse.code === 200) {
console.log('脚本API响应数据:', scriptsResponse.data)
// 数据已经是正确的格式,直接使用
scriptOptions.value = scriptsResponse.data || []
console.log('处理后的脚本选项:', scriptOptions.value)
} else {
console.error('脚本API响应错误:', scriptsResponse)
}
} catch (error) {
console.error('加载选项失败:', error)
console.error('加载脚本选项失败:', error)
}
}
// 刷新数据
const refreshData = () => {
emit('refresh')
}
// 添加队列项
const addQueueItem = () => {
const addQueueItem = async () => {
editingQueueItem.value = null
Object.assign(form, {
name: '',
script: '',
plan: '',
status: 'active',
description: ''
script: ''
})
// 确保在打开弹窗时加载脚本选项
await loadOptions()
modalVisible.value = true
}
// 编辑队列项
const editQueueItem = (item: any) => {
const editQueueItem = async (item: any) => {
editingQueueItem.value = item
Object.assign(form, {
name: item.name || '',
script: item.script || '',
plan: item.plan || '',
status: item.status || 'active',
description: item.description || ''
script: item.script || ''
})
// 确保在打开弹窗时加载脚本选项
await loadOptions()
modalVisible.value = true
}
@@ -275,23 +191,19 @@ 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
ScriptId: form.script
}
}
})
if (response.code === 200) {
message.success('队列项更新成功')
} else {
@@ -304,7 +216,7 @@ const saveQueueItem = async () => {
const createResponse = await Service.addItemApiQueueItemAddPost({
queueId: props.queueId
})
// 2. 用返回的queueItemId更新队列项数据
if (createResponse.code === 200 && createResponse.queueItemId) {
const updateResponse = await Service.updateItemApiQueueItemUpdatePost({
@@ -312,15 +224,11 @@ const saveQueueItem = async () => {
queueItemId: createResponse.queueItemId,
data: {
Info: {
name: form.name,
script: form.script,
plan: form.plan,
status: form.status,
description: form.description
ScriptId: form.script
}
}
})
if (updateResponse.code === 200) {
message.success('队列项添加成功')
} else {
@@ -332,7 +240,7 @@ const saveQueueItem = async () => {
return
}
}
modalVisible.value = false
emit('refresh')
} catch (error) {
@@ -352,11 +260,11 @@ const cancelEdit = () => {
// 删除队列项
const deleteQueueItem = async (itemId: string) => {
try {
const response = await Service.deleteItemApiQueueItemDeletePost({
const response = await Service.deleteItemApiQueueItemDeletePost({
queueId: props.queueId,
queueItemId: itemId
queueItemId: itemId
})
if (response.code === 200) {
message.success('队列项删除成功')
// 确保删除后刷新数据
@@ -386,72 +294,52 @@ onMounted(() => {
font-weight: 600;
}
/* 队列项网格样式 */
.queue-items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
/* 队列项列表样式 */
.queue-items-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.queue-item-card-item {
.queue-item-row {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--ant-color-bg-container);
border: 1px solid var(--ant-color-border);
border-radius: 8px;
padding: 16px;
border-radius: 6px;
transition: all 0.2s ease;
}
.queue-item-card-item:hover {
.queue-item-row:hover {
border-color: var(--ant-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.item-left {
flex: 0 0 120px;
}
.item-name {
font-size: 16px;
font-weight: 600;
.item-index {
font-weight: 500;
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;
.item-center {
flex: 1;
padding: 0 16px;
}
.value {
.script-name {
color: var(--ant-color-text);
flex: 1;
min-width: 0;
word-break: break-all;
font-size: 14px;
}
.item-right {
flex: 0 0 auto;
display: flex;
gap: 8px;
}
/* 空状态样式 */
@@ -471,7 +359,7 @@ onMounted(() => {
.queue-items-grid {
grid-template-columns: 1fr;
}
.queue-item-card-item {
padding: 12px;
}

View File

@@ -13,12 +13,6 @@
</template>
添加定时项
</a-button>
<a-button @click="refreshData" :loading="loading">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</template>
@@ -46,6 +40,7 @@
<a-space>
<a-button size="small" @click="editTimeSet(record)">
<EditOutlined />
编辑
</a-button>
<a-popconfirm
title="确定要删除这个定时项吗?"
@@ -55,6 +50,7 @@
>
<a-button size="small" danger>
<DeleteOutlined />
删除
</a-button>
</a-popconfirm>
</a-space>

View File

@@ -71,17 +71,17 @@
/>
</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 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>
<!-- 定时项组件 -->
@@ -353,11 +353,7 @@ const refreshQueueItems = async () => {
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 || ''
script: queueItemData.Info.ScriptId || ''
})
}
} catch (itemError) {