Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor

This commit is contained in:
2025-09-14 23:24:53 +08:00
13 changed files with 2833 additions and 1369 deletions

View File

@@ -124,22 +124,37 @@ class GlobalConfig(ConfigBase):
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
Data_LastStatisticsUpload = ConfigItem( Data_LastStatisticsUpload = ConfigItem(
"Data", "LastStatisticsUpload", "2000-01-01 00:00:00", DateTimeValidator() "Data",
"LastStatisticsUpload",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
) )
Data_LastStageUpdated = ConfigItem( Data_LastStageUpdated = ConfigItem(
"Data", "LastStageUpdated", "2000-01-01 00:00:00", DateTimeValidator() "Data",
"LastStageUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
) )
Data_StageTimeStamp = ConfigItem( Data_StageTimeStamp = ConfigItem(
"Data", "StageTimeStamp", "2000-01-01 00:00:00", DateTimeValidator() "Data",
"StageTimeStamp",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
) )
Data_Stage = ConfigItem("Data", "Stage", "{ }", JSONValidator()) Data_Stage = ConfigItem("Data", "Stage", "{ }", JSONValidator())
Data_LastNoticeUpdated = ConfigItem( Data_LastNoticeUpdated = ConfigItem(
"Data", "LastNoticeUpdated", "2000-01-01 00:00:00", DateTimeValidator() "Data",
"LastNoticeUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
) )
Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator()) Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator()) Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator())
Data_LastWebConfigUpdated = ConfigItem( Data_LastWebConfigUpdated = ConfigItem(
"Data", "LastWebConfigUpdated", "2000-01-01 00:00:00", DateTimeValidator() "Data",
"LastWebConfigUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
) )
Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator()) Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator())
@@ -265,11 +280,15 @@ class MaaUserConfig(ConfigBase):
"Info", "SklandToken", "", EncryptValidator() "Info", "SklandToken", "", EncryptValidator()
) )
self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01") self.Data_LastProxyDate = ConfigItem(
self.Data_LastAnnihilationDate = ConfigItem( "Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
"Data", "LastAnnihilationDate", "2000-01-01" )
self.Data_LastAnnihilationDate = ConfigItem(
"Data", "LastAnnihilationDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastSklandDate = ConfigItem(
"Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
) )
self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01")
self.Data_ProxyTimes = ConfigItem( self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 9999) "Data", "ProxyTimes", 0, RangeValidator(0, 9999)
) )

View File

@@ -180,9 +180,7 @@ class _TaskManager:
task["status"] = "运行" task["status"] = "运行"
await Config.send_json( await Config.send_json(
WebSocketMessage( WebSocketMessage(
id=str(task_id), id=str(task_id), type="Update", data={"task_list": task_list}
type="Update",
data={"task_list": task_list},
).model_dump() ).model_dump()
) )
logger.info(f"任务开始: {script_id}") logger.info(f"任务开始: {script_id}")
@@ -270,5 +268,19 @@ class _TaskManager:
).model_dump() ).model_dump()
) )
if mode == "自动代理" and task_id in Config.QueueConfig:
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Signal",
data={
"power": Config.QueueConfig[task_id].get(
"Info", "AfterAccomplish"
)
},
).model_dump()
)
TaskManager = _TaskManager() TaskManager = _TaskManager()

View File

@@ -30,7 +30,7 @@ from typing import List, Any, Dict, Union, Optional
from app.utils import dpapi_encrypt, dpapi_decrypt from app.utils import dpapi_encrypt, dpapi_decrypt
from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS, DEFAULT_DATETIME
class ConfigValidator: class ConfigValidator:
@@ -114,24 +114,30 @@ class UUIDValidator(ConfigValidator):
class DateTimeValidator(ConfigValidator): class DateTimeValidator(ConfigValidator):
"""日期时间验证器"""
def __init__(self, date_format: str) -> None:
if not date_format:
raise ValueError("日期时间格式不能为空")
self.date_format = date_format
def validate(self, value: Any) -> bool: def validate(self, value: Any) -> bool:
if not isinstance(value, str): if not isinstance(value, str):
return False return False
try: try:
datetime.strptime(value, "%Y-%m-%d %H:%M:%S") datetime.strptime(value, self.date_format)
return True return True
except ValueError: except ValueError:
return False return False
def correct(self, value: Any) -> str: def correct(self, value: Any) -> str:
if not isinstance(value, str): if not isinstance(value, str):
return "2000-01-01 00:00:00" return DEFAULT_DATETIME.strftime(self.date_format)
try: try:
datetime.strptime(value, "%Y-%m-%d %H:%M:%S") datetime.strptime(value, self.date_format)
return value return value
except ValueError: except ValueError:
return "2000-01-01 00:00:00" return DEFAULT_DATETIME.strftime(self.date_format)
class JSONValidator(ConfigValidator): class JSONValidator(ConfigValidator):
@@ -302,6 +308,8 @@ class ConfigItem:
要设置的值, 可以是任何合法类型 要设置的值, 可以是任何合法类型
""" """
print(self.group, self.name, value, self.value)
if ( if (
dpapi_decrypt(self.value) dpapi_decrypt(self.value)
if isinstance(self.validator, EncryptValidator) if isinstance(self.validator, EncryptValidator)

View File

@@ -1339,7 +1339,7 @@ class MaaManager:
shutil.copy( shutil.copy(
( (
Path.cwd() Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json" / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile/gui.json"
), ),
self.maa_set_path, self.maa_set_path,
) )
@@ -1597,7 +1597,7 @@ class MaaManager:
if ( if (
Path.cwd() Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json" / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
).exists(): ).exists():
data["Configurations"]["Default"][ data["Configurations"]["Default"][
@@ -1618,7 +1618,7 @@ class MaaManager:
"Infrast.CustomInfrastFile" "Infrast.CustomInfrastFile"
] = str( ] = str(
Path.cwd() Path.cwd()
/ f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json" / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/Infrastructure/infrastructure.json"
) # 自定义基建配置文件地址 ) # 自定义基建配置文件地址
else: else:
logger.warning( logger.warning(

View File

@@ -20,6 +20,8 @@
# Contact: DLmaster_361@163.com # Contact: DLmaster_361@163.com
from datetime import datetime
RESOURCE_STAGE_INFO = [ RESOURCE_STAGE_INFO = [
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]}, {"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]}, {"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
@@ -270,3 +272,6 @@ MIRROR_ERROR_INFO = {
1: "未知错误类型", 1: "未知错误类型",
} }
"""MirrorChyan错误代码映射表""" """MirrorChyan错误代码映射表"""
DEFAULT_DATETIME = datetime.strptime("2000-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
"""默认日期时间"""

View File

@@ -0,0 +1,388 @@
# AUTO-MAS 后端任务调度逻辑与WebSocket消息格式说明
## 1. 任务调度架构概览
AUTO-MAS 后端采用基于 AsyncIO 的异步任务调度系统,主要由以下核心组件构成:
### 1.1 核心组件
- **TaskManager**: 任务调度器,负责任务的创建、运行、停止和清理
- **Broadcast**: 消息广播系统,负责在不同组件间传递消息
- **WebSocket**: 与前端的实时通信通道
- **Config**: 配置管理系统,包含脚本配置、队列配置等
### 1.2 任务类型
系统支持三种主要任务模式:
1. **设置脚本** - 直接执行单个脚本配置
2. **自动代理** - 按队列顺序自动执行多个脚本
3. **人工排查** - 手动排查和执行任务
## 2. 任务调度流程
### 2.1 任务创建流程
```
前端请求 → API接口 → TaskManager.add_task() → 任务验证 → 创建异步任务 → 返回任务ID
```
**具体步骤:**
1. **任务验证**: 根据模式和UID验证任务配置是否存在
2. **重复检查**: 确保相同任务未在运行中
3. **任务创建**: 使用`asyncio.create_task()`创建异步任务
4. **回调设置**: 添加任务完成回调用于清理工作
### 2.2 任务执行流程
#### 设置脚本模式
```
获取脚本配置 → 确定脚本类型(MAA/General) → 创建对应Manager → 执行任务
```
#### 自动代理模式
```
获取队列配置 → 构建任务列表 → 逐个执行脚本 → 更新状态 → 发送完成信号
```
### 2.3 任务状态管理
- **等待**: 任务已加入队列但未开始执行
- **运行**: 任务正在执行中
- **跳过**: 任务因重复或其他原因被跳过
- **完成**: 任务执行完毕
## 3. WebSocket 消息系统
### 3.1 消息基础结构
所有WebSocket消息都遵循统一的JSON格式
```json
{
"id": "消息ID或任务ID",
"type": "消息类型",
"data": {
"具体数据": "根据类型而定"
}
}
```
### 3.2 消息类型详解
#### 3.2.1 Update 类型 - 数据更新
**用途**: 通知前端更新界面数据
**常见数据格式:**
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"user_list": [
{
"name": "用户名",
"status": "运行状态",
"config": "配置信息"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"task_list": [
{
"script_id": "脚本ID",
"status": "等待/运行/完成/跳过",
"name": "脚本名称"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"log": "任务执行日志内容"
}
}
```
#### 3.2.2 Info 类型 - 信息显示
**用途**: 向前端发送需要显示的信息,包括普通信息、警告和错误
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Error": "错误信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Warning": "警告信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Info": "普通信息内容"
}
}
```
#### 3.2.3 Message 类型 - 对话框请求
**用途**: 请求前端弹出对话框显示重要信息
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Message",
"data": {
"title": "对话框标题",
"content": "对话框内容",
"type": "info/warning/error"
}
}
```
#### 3.2.4 Signal 类型 - 程序信号
**用途**: 发送程序控制信号和状态通知
**常见信号:**
**任务完成信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"Accomplish": "任务完成后调度台显示的日志内容"
}
}
```
**电源操作信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"power": "NoAction/KillSelf/Sleep/Hibernate/Shutdown/ShutdownForce",
}
}
```
**心跳信号:**
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Ping": "无描述"
}
}
```
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Pong": "无描述"
}
}
```
## 4. 任务管理器详细说明
### 4.1 TaskManager 核心方法
#### add_task(mode: str, uid: str)
- **功能**: 添加新任务到调度队列
- **参数**:
- `mode`: 任务模式 ("设置脚本", "自动代理", "人工排查")
- `uid`: 任务唯一标识符
- **返回**: 任务UUID
#### stop_task(task_id: str)
- **功能**: 停止指定任务
- **参数**:
- `task_id`: 任务ID支持 "ALL" 停止所有任务
#### run_task(mode: str, task_id: UUID, actual_id: Optional[UUID])
- **功能**: 执行具体任务逻辑
- **流程**: 根据模式选择相应的执行策略
### 4.2 任务执行器
#### GeneralManager
- **用途**: 处理通用脚本任务
- **特点**: 支持自定义脚本路径和参数
- **配置**: 基于 GeneralConfig 和 GeneralUserConfig
#### MaaManager
- **用途**: 处理MAA (明日方舟助手) 专用任务
- **特点**: 支持模拟器控制、ADB连接、游戏自动化
- **配置**: 基于 MaaConfig 和 MaaUserConfig
## 5. 消息广播系统
### 5.1 Broadcast 机制
- **设计模式**: 发布-订阅模式
- **功能**: 实现组件间解耦的消息传递
- **特点**: 支持多个订阅者同时接收消息
### 5.2 消息流向
```
任务执行器 → Broadcast → WebSocket → 前端界面
其他订阅者
```
## 6. 配置管理系统
### 6.1 配置类型
- **ScriptConfig**: 脚本配置包含MAA和General两种类型
- **QueueConfig**: 队列配置,定义自动代理任务的执行顺序
- **GlobalConfig**: 全局系统配置
### 6.2 配置操作
- **锁机制**: 防止配置在使用时被修改
- **实时更新**: 支持动态加载配置变更
- **类型验证**: 确保配置数据的正确性
## 7. API 接口说明
### 7.1 任务控制接口
**创建任务**
- **端点**: `POST /api/dispatch/start`
- **请求体**:
```json
{
"mode": "自动代理|人工排查|设置脚本",
"taskId": "目标任务ID"
}
```
- **响应**:
```json
{
"code": 200,
"status": "success",
"message": "操作成功",
"websocketId": "新任务ID"
}
```
**停止任务**
- **端点**: `POST /api/dispatch/stop`
- **请求体**:
```json
{
"taskId": "要停止的任务ID"
}
```
**电源操作**
- **端点**: `POST /api/dispatch/power`
- **请求体**:
```json
{
"signal": "NoAction|Shutdown|ShutdownForce|Hibernate|Sleep|KillSelf"
}
```
### 7.2 WebSocket 连接
**端点**: `WS /api/core/ws`
**连接特性**:
- 同时只允许一个WebSocket连接
- 自动心跳检测 (15秒超时)
- 连接断开时自动清理资源
## 8. 错误处理机制
### 8.1 异常类型
- **ValueError**: 配置验证失败
- **RuntimeError**: 任务状态冲突
- **TimeoutError**: 操作超时
- **ConnectionError**: 连接相关错误
### 8.2 错误响应格式
```json
{
"id": "相关任务ID",
"type": "Info",
"data": {
"Error": "具体错误描述"
}
}
```
## 9. 性能和监控
### 9.1 日志系统
- **分层日志**: 按模块划分日志记录器
- **实时监控**: 支持日志实时推送到前端
- **文件轮转**: 自动管理日志文件大小
### 9.2 资源管理
- **进程管理**: 自动清理子进程
- **内存监控**: 防止内存泄漏
- **连接池**: 复用数据库和网络连接
## 10. 安全考虑
### 10.1 输入验证
- **参数校验**: 使用 Pydantic 模型验证
- **路径安全**: 防止路径遍历攻击
- **命令注入**: 严格控制执行的命令参数
### 10.2 权限控制
- **单一连接**: 限制WebSocket连接数量
- **操作限制**: 防止重复或冲突操作
- **资源保护**: 防止资源滥用
---
*此文档基于 AUTO-MAS v5.0.0 版本编写详细的API文档和配置说明请参考相关配置文件和源代码注释。*

View File

@@ -62,9 +62,9 @@ const mainMenuItems = [
{ key: '/plans', label: '计划管理', icon: icon(CalendarOutlined) }, { key: '/plans', label: '计划管理', icon: icon(CalendarOutlined) },
{ key: '/queue', label: '调度队列', icon: icon(UnorderedListOutlined) }, { key: '/queue', label: '调度队列', icon: icon(UnorderedListOutlined) },
{ key: '/scheduler', label: '调度中心', icon: icon(ControlOutlined) }, { key: '/scheduler', label: '调度中心', icon: icon(ControlOutlined) },
{ key: '/history', label: '历史记录', icon: icon(HistoryOutlined) },
] ]
const bottomMenuItems = [ const bottomMenuItems = [
{ key: '/history', label: '历史记录', icon: icon(HistoryOutlined) },
{ key: '/settings', label: '设置', icon: icon(SettingOutlined) }, { key: '/settings', label: '设置', icon: icon(SettingOutlined) },
] ]

View File

@@ -37,24 +37,24 @@
type="primary" type="primary"
ghost ghost
size="middle" size="middle"
@click="handleMAAConfig(script)" @click="handleStartMAAConfig(script)"
> >
<template #icon> <template #icon>
<SettingOutlined /> <SettingOutlined />
</template> </template>
置MAA全局配置 置MAA
</a-button> </a-button>
<a-button <a-button
v-if="script.type === 'MAA' && props.activeConnections.has(script.id)" v-if="script.type === 'MAA' && props.activeConnections.has(script.id)"
type="primary" type="primary"
danger
size="middle" size="middle"
@click="handleDisconnectMAA(script)" style="background: #52c41a; border-color: #52c41a;"
@click="handleSaveMAAConfig(script)"
> >
<template #icon> <template #icon>
<StopOutlined /> <SaveOutlined />
</template> </template>
断开配置连接 保存配置
</a-button> </a-button>
<a-button type="default" size="middle" @click="handleEdit(script)"> <a-button type="default" size="middle" @click="handleEdit(script)">
<template #icon> <template #icon>
@@ -291,8 +291,8 @@ import type { Script, User } from '../types/script'
import { import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
SaveOutlined,
SettingOutlined, SettingOutlined,
StopOutlined,
UserAddOutlined, UserAddOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
@@ -312,9 +312,9 @@ interface Emits {
(e: 'deleteUser', user: User): void (e: 'deleteUser', user: User): void
(e: 'maaConfig', script: Script): void (e: 'startMaaConfig', script: Script): void
(e: 'disconnectMaa', script: Script): void (e: 'saveMaaConfig', script: Script): void
(e: 'toggleUserStatus', user: User): void (e: 'toggleUserStatus', user: User): void
} }
@@ -350,12 +350,12 @@ const handleDeleteUser = (user: User) => {
emit('deleteUser', user) emit('deleteUser', user)
} }
const handleMAAConfig = (script: Script) => { const handleStartMAAConfig = (script: Script) => {
emit('maaConfig', script) emit('startMaaConfig', script)
} }
const handleDisconnectMAA = (script: Script) => { const handleSaveMAAConfig = (script: Script) => {
emit('disconnectMaa', script) emit('saveMaaConfig', script)
} }
const handleToggleUserStatus = (user: User) => { const handleToggleUserStatus = (user: User) => {

View File

@@ -17,18 +17,32 @@
</div> </div>
<a-space size="middle"> <a-space size="middle">
<!-- MAA配置按钮组 -->
<a-button <a-button
v-if="formData.Info.Mode !== '简洁'" v-if="formData.Info.Mode !== '简洁' && !maaWebsocketId"
type="primary" type="primary"
ghost ghost
size="large" size="large"
@click="handleMAAConfig" @click="handleStartMAAConfig"
:loading="maaConfigLoading" :loading="maaConfigLoading"
> >
<template #icon> <template #icon>
<SettingOutlined /> <SettingOutlined />
</template> </template>
MAA配置 配置MAA
</a-button>
<a-button
v-if="formData.Info.Mode !== '简洁' && maaWebsocketId"
type="primary"
size="large"
@click="handleSaveMAAConfig"
:loading="maaConfigLoading"
style="background: #52c41a; border-color: #52c41a;"
>
<template #icon>
<SaveOutlined />
</template>
保存配置
</a-button> </a-button>
<a-button size="large" @click="handleCancel" class="cancel-button"> <a-button size="large" @click="handleCancel" class="cancel-button">
<template #icon> <template #icon>
@@ -351,8 +365,15 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">{{ displayMedicineNumb }}</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示输入框 -->
<a-input-number <a-input-number
v-model:value="formData.Info.MedicineNumb" v-else
v-model:value="displayMedicineNumb"
:min="0" :min="0"
:max="9999" :max="9999"
placeholder="0" placeholder="0"
@@ -374,8 +395,23 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{
displaySeriesNumb === '0'
? 'AUTO'
: displaySeriesNumb === '-1'
? '不切换'
: displaySeriesNumb
}}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.SeriesNumb" v-else
v-model:value="displaySeriesNumb"
:options="[ :options="[
{ label: 'AUTO', value: '0' }, { label: 'AUTO', value: '0' },
{ label: '1', value: '1' }, { label: '1', value: '1' },
@@ -402,8 +438,17 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{ displayStage === '-' ? '当前/上次' : displayStage || '不选择' }}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.Stage" v-else
v-model:value="displayStage"
:disabled="loading" :disabled="loading"
size="large" size="large"
placeholder="选择或输入自定义关卡" placeholder="选择或输入自定义关卡"
@@ -428,16 +473,25 @@
</a-button> </a-button>
</a-space> </a-space>
</template> </template>
<a-select-option v-for="option in stageOptions" :key="option.value" :value="option.value"> <a-select-option
v-for="option in stageOptions"
:key="option.value"
:value="option.value"
>
<template v-if="option.label.includes('|')"> <template v-if="option.label.includes('|')">
<span>{{ option.label.split('|')[0] }}</span> <span>{{ option.label.split('|')[0] }}</span>
<a-tag color="green" size="small" style="margin-left: 8px;"> <a-tag color="green" size="small" style="margin-left: 8px">
{{ option.label.split('|')[1] }} {{ option.label.split('|')[1] }}
</a-tag> </a-tag>
</template> </template>
<template v-else> <template v-else>
{{ option.label }} {{ option.label }}
<a-tag v-if="isCustomStage(option.value)" color="blue" size="small" style="margin-left: 8px;"> <a-tag
v-if="isCustomStage(option.value)"
color="blue"
size="small"
style="margin-left: 8px"
>
自定义 自定义
</a-tag> </a-tag>
</template> </template>
@@ -459,8 +513,17 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{ displayStage1 === '-' ? '当前/上次' : displayStage1 || '不选择' }}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.Stage_1" v-else
v-model:value="displayStage1"
:disabled="loading" :disabled="loading"
size="large" size="large"
placeholder="选择或输入自定义关卡" placeholder="选择或输入自定义关卡"
@@ -485,16 +548,25 @@
</a-button> </a-button>
</a-space> </a-space>
</template> </template>
<a-select-option v-for="option in stageOptions" :key="option.value" :value="option.value"> <a-select-option
v-for="option in stageOptions"
:key="option.value"
:value="option.value"
>
<template v-if="option.label.includes('|')"> <template v-if="option.label.includes('|')">
<span>{{ option.label.split('|')[0] }}</span> <span>{{ option.label.split('|')[0] }}</span>
<a-tag color="green" size="small" style="margin-left: 8px;"> <a-tag color="green" size="small" style="margin-left: 8px">
{{ option.label.split('|')[1] }} {{ option.label.split('|')[1] }}
</a-tag> </a-tag>
</template> </template>
<template v-else> <template v-else>
{{ option.label }} {{ option.label }}
<a-tag v-if="isCustomStage(option.value)" color="blue" size="small" style="margin-left: 8px;"> <a-tag
v-if="isCustomStage(option.value)"
color="blue"
size="small"
style="margin-left: 8px"
>
自定义 自定义
</a-tag> </a-tag>
</template> </template>
@@ -514,8 +586,17 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{ displayStage2 === '-' ? '当前/上次' : displayStage2 || '不选择' }}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.Stage_2" v-else
v-model:value="displayStage2"
:disabled="loading" :disabled="loading"
size="large" size="large"
placeholder="选择或输入自定义关卡" placeholder="选择或输入自定义关卡"
@@ -540,16 +621,25 @@
</a-button> </a-button>
</a-space> </a-space>
</template> </template>
<a-select-option v-for="option in stageOptions" :key="option.value" :value="option.value"> <a-select-option
v-for="option in stageOptions"
:key="option.value"
:value="option.value"
>
<template v-if="option.label.includes('|')"> <template v-if="option.label.includes('|')">
<span>{{ option.label.split('|')[0] }}</span> <span>{{ option.label.split('|')[0] }}</span>
<a-tag color="green" size="small" style="margin-left: 8px;"> <a-tag color="green" size="small" style="margin-left: 8px">
{{ option.label.split('|')[1] }} {{ option.label.split('|')[1] }}
</a-tag> </a-tag>
</template> </template>
<template v-else> <template v-else>
{{ option.label }} {{ option.label }}
<a-tag v-if="isCustomStage(option.value)" color="blue" size="small" style="margin-left: 8px;"> <a-tag
v-if="isCustomStage(option.value)"
color="blue"
size="small"
style="margin-left: 8px"
>
自定义 自定义
</a-tag> </a-tag>
</template> </template>
@@ -569,8 +659,17 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{ displayStage3 === '-' ? '当前/上次' : displayStage3 || '不选择' }}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.Stage_3" v-else
v-model:value="displayStage3"
:disabled="loading" :disabled="loading"
size="large" size="large"
placeholder="选择或输入自定义关卡" placeholder="选择或输入自定义关卡"
@@ -595,16 +694,25 @@
</a-button> </a-button>
</a-space> </a-space>
</template> </template>
<a-select-option v-for="option in stageOptions" :key="option.value" :value="option.value"> <a-select-option
v-for="option in stageOptions"
:key="option.value"
:value="option.value"
>
<template v-if="option.label.includes('|')"> <template v-if="option.label.includes('|')">
<span>{{ option.label.split('|')[0] }}</span> <span>{{ option.label.split('|')[0] }}</span>
<a-tag color="green" size="small" style="margin-left: 8px;"> <a-tag color="green" size="small" style="margin-left: 8px">
{{ option.label.split('|')[1] }} {{ option.label.split('|')[1] }}
</a-tag> </a-tag>
</template> </template>
<template v-else> <template v-else>
{{ option.label }} {{ option.label }}
<a-tag v-if="isCustomStage(option.value)" color="blue" size="small" style="margin-left: 8px;"> <a-tag
v-if="isCustomStage(option.value)"
color="blue"
size="small"
style="margin-left: 8px"
>
自定义 自定义
</a-tag> </a-tag>
</template> </template>
@@ -615,15 +723,24 @@
<a-col :span="6"> <a-col :span="6">
<a-form-item name="mode"> <a-form-item name="mode">
<template #label> <template #label>
<a-tooltip title="剩余理智关卡,选择「当前/上次」时视为不使用剩余理智关卡"> <a-tooltip title="剩余理智关卡,选择「不选择」时视为不使用剩余理智关卡">
<span class="form-label"> <span class="form-label">
剩余理智关卡 剩余理智关卡
<QuestionCircleOutlined class="help-icon" /> <QuestionCircleOutlined class="help-icon" />
</span> </span>
</a-tooltip> </a-tooltip>
</template> </template>
<!-- 计划模式显示只读文本 -->
<div v-if="isPlanMode" class="plan-mode-display">
<div class="plan-value">
{{ displayStageRemain === '-' ? '不选择' : displayStageRemain || '不选择' }}
</div>
<div class="plan-source">来自计划表</div>
</div>
<!-- 固定模式显示选择框 -->
<a-select <a-select
v-model:value="formData.Info.Stage_Remain" v-else
v-model:value="displayStageRemain"
:disabled="loading" :disabled="loading"
size="large" size="large"
placeholder="选择或输入自定义关卡" placeholder="选择或输入自定义关卡"
@@ -648,16 +765,25 @@
</a-button> </a-button>
</a-space> </a-space>
</template> </template>
<a-select-option v-for="option in stageOptions" :key="option.value" :value="option.value"> <a-select-option
v-for="option in stageRemainOptions"
:key="option.value"
:value="option.value"
>
<template v-if="option.label.includes('|')"> <template v-if="option.label.includes('|')">
<span>{{ option.label.split('|')[0] }}</span> <span>{{ option.label.split('|')[0] }}</span>
<a-tag color="green" size="small" style="margin-left: 8px;"> <a-tag color="green" size="small" style="margin-left: 8px">
{{ option.label.split('|')[1] }} {{ option.label.split('|')[1] }}
</a-tag> </a-tag>
</template> </template>
<template v-else> <template v-else>
{{ option.label }} {{ option.label }}
<a-tag v-if="isCustomStage(option.value)" color="blue" size="small" style="margin-left: 8px;"> <a-tag
v-if="isCustomStage(option.value)"
color="blue"
size="small"
style="margin-left: 8px"
>
自定义 自定义
</a-tag> </a-tag>
</template> </template>
@@ -883,28 +1009,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' import { computed, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { import {
ArrowLeftOutlined, ArrowLeftOutlined,
PlusOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
SaveOutlined, SaveOutlined,
SettingOutlined, SettingOutlined,
PlusOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import type { FormInstance, Rule } from 'ant-design-vue/es/form' import type { FormInstance, Rule } from 'ant-design-vue/es/form'
import { useUserApi } from '@/composables/useUserApi' import { useUserApi } from '@/composables/useUserApi'
import { useScriptApi } from '@/composables/useScriptApi' import { useScriptApi } from '@/composables/useScriptApi'
import { usePlanApi } from '@/composables/usePlanApi'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { Service } from '@/api' import { Service } from '@/api'
import { GetStageIn } from '@/api/models/GetStageIn' import { GetStageIn } from '@/api/models/GetStageIn'
import { defineComponent } from 'vue' import { TaskCreateIn } from '@/api/models/TaskCreateIn'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi() const { addUser, updateUser, getUsers, loading: userLoading } = useUserApi()
const { getScript } = useScriptApi() const { getScript } = useScriptApi()
const { getPlans } = usePlanApi()
const { subscribe, unsubscribe } = useWebSocket() const { subscribe, unsubscribe } = useWebSocket()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@@ -939,13 +1067,23 @@ const serverOptions = [
// 关卡选项 // 关卡选项
const stageOptions = ref<any[]>([{ label: '不选择', value: '' }]) const stageOptions = ref<any[]>([{ label: '不选择', value: '' }])
// 剩余理智关卡专用选项(将"当前/上次"改为"不选择"
const stageRemainOptions = computed(() => {
return stageOptions.value.map(option => {
if (option.value === '-') {
return { ...option, label: option.label.replace('当前/上次', '不选择') }
}
return option
})
})
// 判断值是否为自定义关卡 // 判断值是否为自定义关卡
const isCustomStage = (value: string) => { const isCustomStage = (value: string) => {
if (!value || value === '' || value === '-') return false if (!value || value === '' || value === '-') return false
// 检查是否在从API加载的关卡列表中 // 检查是否在从API加载的关卡列表中
const predefinedStage = stageOptions.value.find(option => const predefinedStage = stageOptions.value.find(
option.value === value && !option.isCustom option => option.value === value && !option.isCustom
) )
return !predefinedStage return !predefinedStage
@@ -954,6 +1092,150 @@ const isCustomStage = (value: string) => {
// 关卡配置模式选项 // 关卡配置模式选项
const stageModeOptions = ref<any[]>([{ label: '固定', value: 'Fixed' }]) const stageModeOptions = ref<any[]>([{ label: '固定', value: 'Fixed' }])
// 计划模式状态
const isPlanMode = computed(() => {
return formData.Info.StageMode !== 'Fixed'
})
const planModeConfig = ref<any>(null)
// 计算属性用于显示正确的值(来自计划表或用户配置)
const displayMedicineNumb = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.MedicineNumb !== undefined) {
return planModeConfig.value.MedicineNumb
}
return formData.Info.MedicineNumb
},
set: value => {
if (!isPlanMode.value) {
formData.Info.MedicineNumb = value
}
},
})
const displaySeriesNumb = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.SeriesNumb !== undefined) {
return planModeConfig.value.SeriesNumb
}
return formData.Info.SeriesNumb
},
set: value => {
if (!isPlanMode.value) {
formData.Info.SeriesNumb = value
}
},
})
const displayStage = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.Stage !== undefined) {
return planModeConfig.value.Stage
}
return formData.Info.Stage
},
set: value => {
if (!isPlanMode.value) {
formData.Info.Stage = value
}
},
})
const displayStage1 = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.Stage_1 !== undefined) {
return planModeConfig.value.Stage_1
}
return formData.Info.Stage_1
},
set: value => {
if (!isPlanMode.value) {
formData.Info.Stage_1 = value
}
},
})
const displayStage2 = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.Stage_2 !== undefined) {
return planModeConfig.value.Stage_2
}
return formData.Info.Stage_2
},
set: value => {
if (!isPlanMode.value) {
formData.Info.Stage_2 = value
}
},
})
const displayStage3 = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.Stage_3 !== undefined) {
return planModeConfig.value.Stage_3
}
return formData.Info.Stage_3
},
set: value => {
if (!isPlanMode.value) {
formData.Info.Stage_3 = value
}
},
})
const displayStageRemain = computed({
get: () => {
if (isPlanMode.value && planModeConfig.value?.Stage_Remain !== undefined) {
return planModeConfig.value.Stage_Remain
}
return formData.Info.Stage_Remain
},
set: value => {
if (!isPlanMode.value) {
formData.Info.Stage_Remain = value
}
},
})
// 获取计划当前配置
const getPlanCurrentConfig = (planData: any) => {
if (!planData) return null
const mode = planData.Info?.Mode || 'ALL'
if (mode === 'ALL') {
return planData.ALL || null
} else if (mode === 'Weekly') {
// 获取+12时区的当前时间
const now = new Date()
const utc12Time = new Date(now.getTime() + 12 * 60 * 60 * 1000)
// 如果是凌晨4点前算作前一天
if (utc12Time.getHours() < 4) {
utc12Time.setDate(utc12Time.getDate() - 1)
}
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const today = weekdays[utc12Time.getDay()]
console.log('计划表周模式调试:', {
原始时间: now.toISOString(),
UTC12时间: utc12Time.toISOString(),
星期: today,
计划数据: planData,
})
// 优先使用今天的配置如果没有或为空则使用ALL配置
const todayConfig = planData[today]
if (todayConfig && Object.keys(todayConfig).length > 0) {
return todayConfig
}
return planData.ALL || null
}
return null
}
// MAA脚本默认用户数据 // MAA脚本默认用户数据
const getDefaultMAAUserData = () => ({ const getDefaultMAAUserData = () => ({
Info: { Info: {
@@ -1006,9 +1288,9 @@ const getDefaultMAAUserData = () => ({
Data: { Data: {
CustomInfrastPlanIndex: '', CustomInfrastPlanIndex: '',
IfPassCheck: false, IfPassCheck: false,
LastAnnihilationDate: '', LastAnnihilationDate: '2000-01-01',
LastProxyDate: '', LastProxyDate: '2000-01-01',
LastSklandDate: '', LastSklandDate: '2000-01-01',
ProxyTimes: 0, ProxyTimes: 0,
}, },
}) })
@@ -1130,7 +1412,7 @@ const loadUserData = async () => {
stageOptions.value.push({ stageOptions.value.push({
label: stageValue, label: stageValue,
value: stageValue, value: stageValue,
isCustom: true isCustom: true,
}) })
} }
} }
@@ -1160,17 +1442,13 @@ const loadUserData = async () => {
const loadStageOptions = async () => { const loadStageOptions = async () => {
try { try {
const response = await Service.getStageComboxApiInfoComboxStagePost({ const response = await Service.getStageComboxApiInfoComboxStagePost({
type: GetStageIn.type.TODAY type: GetStageIn.type.TODAY,
}) })
if (response && response.code === 200 && response.data) { if (response && response.code === 200 && response.data) {
stageOptions.value = [...response.data].map(option => ({ stageOptions.value = [...response.data].map(option => ({
...option, ...option,
isCustom: false // 明确标记从API加载的关卡为非自定义 isCustom: false, // 明确标记从API加载的关卡为非自定义
})).sort((a, b) => { }))
if (a.value === '-') return -1
if (b.value === '-') return 1
return 0
})
} }
} catch (error) { } catch (error) {
console.error('加载关卡选项失败:', error) console.error('加载关卡选项失败:', error)
@@ -1194,7 +1472,7 @@ const VNodes = defineComponent({
props: { vnodes: { type: Object, required: true } }, props: { vnodes: { type: Object, required: true } },
setup(props) { setup(props) {
return () => props.vnodes as any return () => props.vnodes as any
} },
}) })
// 选择基建配置文件 // 选择基建配置文件
@@ -1307,36 +1585,95 @@ const handleSubmit = async () => {
} }
} }
const handleMAAConfig = async () => { const handleStartMAAConfig = async () => {
if (!isEdit.value) { if (!isEdit.value) {
message.warning('请先保存用户后再进行MAA配置') message.warning('请先保存用户后再进行MAA配置')
return return
} }
try { try {
maaConfigLoading.value = true // 检查是否已有连接
// 如果已有连接,先断开
if (maaWebsocketId.value) { if (maaWebsocketId.value) {
unsubscribe(maaWebsocketId.value) message.warning('该用户MAA配置已在进行中请先保存配置')
maaWebsocketId.value = null return
} }
// 直接订阅(旧 connect 参数移除) maaConfigLoading.value = true
const subId = userId
subscribe(subId, { // 调用启动配置任务API
const response = await Service.addTaskApiDispatchStartPost({
taskId: userId,
mode: TaskCreateIn.mode.SettingScriptMode
})
if (response.code === 200) {
// 订阅WebSocket消息
subscribe(response.websocketId, {
onError: error => { onError: error => {
console.error(`用户 ${formData.userName} MAA配置错误:`, error) console.error(`用户 ${formData.userName} MAA配置错误:`, error)
message.error(`MAA配置连接失败: ${error}`) message.error(`MAA配置连接失败: ${error}`)
maaWebsocketId.value = null maaWebsocketId.value = null
},
onResult: (data: any) => {
// 处理配置完成消息(兼容任何结构)
if (data.Accomplish) {
message.success(`用户 ${formData.userName} MAA配置已完成`)
maaWebsocketId.value = null
}
} }
}) })
maaWebsocketId.value = subId // 记录连接和websocketId
message.success(`已开始配置用户 ${formData.userName} 的MAA设置`) maaWebsocketId.value = response.websocketId
message.success(`已启动用户 ${formData.userName} 的MAA配置`)
// 设置自动断开连接的定时器30分钟后
setTimeout(
() => {
if (maaWebsocketId.value) {
unsubscribe(maaWebsocketId.value)
maaWebsocketId.value = null
message.info(`用户 ${formData.userName} MAA配置会话已超时断开`)
}
},
30 * 60 * 1000
) // 30分钟
} else {
message.error(response.message || '启动MAA配置失败')
}
} catch (error) { } catch (error) {
console.error('MAA配置失败:', error) console.error('启动MAA配置失败:', error)
message.error('MAA配置失败') message.error('启动MAA配置失败')
} finally {
maaConfigLoading.value = false
}
}
const handleSaveMAAConfig = async () => {
try {
if (!maaWebsocketId.value) {
message.error('未找到活动的配置会话')
return
}
maaConfigLoading.value = true
// 调用停止配置任务API
const response = await Service.stopTaskApiDispatchStopPost({
taskId: maaWebsocketId.value
})
if (response.code === 200) {
// 取消订阅
unsubscribe(maaWebsocketId.value)
maaWebsocketId.value = null
message.success(`用户 ${formData.userName} 的MAA配置已保存`)
} else {
message.error(response.message || '保存配置失败')
}
} catch (error) {
console.error('保存MAA配置失败:', error)
message.error('保存MAA配置失败')
} finally { } finally {
maaConfigLoading.value = false maaConfigLoading.value = false
} }
@@ -1386,7 +1723,7 @@ const addStageToOptions = (stageName: string) => {
stageOptions.value.push({ stageOptions.value.push({
label: trimmedName, label: trimmedName,
value: trimmedName, value: trimmedName,
isCustom: true isCustom: true,
}) })
message.success(`自定义关卡 "${trimmedName}" 添加成功`) message.success(`自定义关卡 "${trimmedName}" 添加成功`)
@@ -1401,7 +1738,9 @@ const addCustomStage = () => {
} }
if (addStageToOptions(customStageName.value)) { if (addStageToOptions(customStageName.value)) {
if (!isPlanMode.value) {
formData.Info.Stage = customStageName.value.trim() formData.Info.Stage = customStageName.value.trim()
}
customStageName.value = '' customStageName.value = ''
nextTick(() => { nextTick(() => {
stageInputRef.value?.focus() stageInputRef.value?.focus()
@@ -1417,7 +1756,9 @@ const addCustomStage1 = () => {
} }
if (addStageToOptions(customStage1Name.value)) { if (addStageToOptions(customStage1Name.value)) {
if (!isPlanMode.value) {
formData.Info.Stage_1 = customStage1Name.value.trim() formData.Info.Stage_1 = customStage1Name.value.trim()
}
customStage1Name.value = '' customStage1Name.value = ''
nextTick(() => { nextTick(() => {
stage1InputRef.value?.focus() stage1InputRef.value?.focus()
@@ -1433,7 +1774,9 @@ const addCustomStage2 = () => {
} }
if (addStageToOptions(customStage2Name.value)) { if (addStageToOptions(customStage2Name.value)) {
if (!isPlanMode.value) {
formData.Info.Stage_2 = customStage2Name.value.trim() formData.Info.Stage_2 = customStage2Name.value.trim()
}
customStage2Name.value = '' customStage2Name.value = ''
nextTick(() => { nextTick(() => {
stage2InputRef.value?.focus() stage2InputRef.value?.focus()
@@ -1449,7 +1792,9 @@ const addCustomStage3 = () => {
} }
if (addStageToOptions(customStage3Name.value)) { if (addStageToOptions(customStage3Name.value)) {
if (!isPlanMode.value) {
formData.Info.Stage_3 = customStage3Name.value.trim() formData.Info.Stage_3 = customStage3Name.value.trim()
}
customStage3Name.value = '' customStage3Name.value = ''
nextTick(() => { nextTick(() => {
stage3InputRef.value?.focus() stage3InputRef.value?.focus()
@@ -1465,7 +1810,9 @@ const addCustomStageRemain = () => {
} }
if (addStageToOptions(customStageRemainName.value)) { if (addStageToOptions(customStageRemainName.value)) {
if (!isPlanMode.value) {
formData.Info.Stage_Remain = customStageRemainName.value.trim() formData.Info.Stage_Remain = customStageRemainName.value.trim()
}
customStageRemainName.value = '' customStageRemainName.value = ''
nextTick(() => { nextTick(() => {
stageRemainInputRef.value?.focus() stageRemainInputRef.value?.focus()
@@ -1492,6 +1839,48 @@ onMounted(() => {
loadScriptInfo() loadScriptInfo()
loadStageModeOptions() loadStageModeOptions()
loadStageOptions() loadStageOptions()
// 设置StageMode变化监听器
watch(
() => formData.Info.StageMode,
async newStageMode => {
if (newStageMode === 'Fixed') {
// 切换到固定模式,清除计划配置
planModeConfig.value = null
} else if (newStageMode && newStageMode !== '') {
// 切换到计划模式,加载计划配置
try {
const response = await getPlans(newStageMode)
if (response && response.code === 200 && response.data[newStageMode]) {
const planData = response.data[newStageMode]
const currentConfig = getPlanCurrentConfig(planData)
planModeConfig.value = currentConfig
console.log('计划配置加载成功:', {
planId: newStageMode,
currentConfig,
planModeConfigValue: planModeConfig.value,
})
// 从stageModeOptions中查找对应的计划名称
const planOption = stageModeOptions.value.find(option => option.value === newStageMode)
const planName = planOption ? planOption.label : newStageMode
message.success(`已切换到计划模式:${planName}`)
} else {
message.warning('计划配置加载失败,请检查计划是否存在')
planModeConfig.value = null
}
} catch (error) {
console.error('加载计划配置失败:', error)
message.error('加载计划配置时发生错误')
planModeConfig.value = null
}
}
},
{ immediate: false }
)
}) })
</script> </script>
@@ -1749,4 +2138,41 @@ onMounted(() => {
width: 60px; width: 60px;
height: 60px; height: 60px;
} }
/* 计划模式提示样式 */
.plan-mode-hint {
margin-top: 4px;
font-size: 12px;
color: var(--ant-color-primary);
font-weight: 500;
}
/* 计划模式显示样式 */
.plan-mode-display {
min-height: 40px;
padding: 8px 12px;
border: 1px solid var(--ant-color-border);
border-radius: 6px;
background: var(--ant-color-fill-alter);
display: flex;
align-items: center;
justify-content: space-between;
}
.plan-value {
font-size: 14px;
color: var(--ant-color-text);
font-weight: 500;
flex: 1;
}
.plan-source {
font-size: 12px;
color: var(--ant-color-primary);
font-weight: 500;
padding: 2px 8px;
background: var(--ant-color-primary-bg);
border-radius: 12px;
border: 1px solid var(--ant-color-primary-border);
}
</style> </style>

View File

@@ -135,7 +135,7 @@
:placeholder="getPlaceholder(record.taskName)" class="config-input-number" :placeholder="getPlaceholder(record.taskName)" class="config-input-number"
:controls="false" :disabled="isColumnDisabled(column.key)" /> :controls="false" :disabled="isColumnDisabled(column.key)" />
</template> </template>
<template v-else-if="['关卡选择', '备选关卡-1', '备选关卡-2', '备选关卡-3', '剩余理智'].includes(record.taskName)"> <template v-else-if="['关卡选择', '备选关卡-1', '备选关卡-2', '备选关卡-3', '剩余理智关卡'].includes(record.taskName)">
<a-select <a-select
v-model:value="record[column.key]" v-model:value="record[column.key]"
size="small" size="small"
@@ -436,14 +436,14 @@ const tableData = ref([
{ {
key: 'SeriesNumb', key: 'SeriesNumb',
taskName: '连战次数', taskName: '连战次数',
ALL: '-1', ALL: '0',
Monday: '-1', Monday: '0',
Tuesday: '-1', Tuesday: '0',
Wednesday: '-1', Wednesday: '0',
Thursday: '-1', Thursday: '0',
Friday: '-1', Friday: '0',
Saturday: '-1', Saturday: '0',
Sunday: '-1', Sunday: '0',
}, },
{ {
key: 'Stage', key: 'Stage',
@@ -495,7 +495,7 @@ const tableData = ref([
}, },
{ {
key: 'Stage_Remain', key: 'Stage_Remain',
taskName: '剩余理智', taskName: '剩余理智关卡',
ALL: '-', ALL: '-',
Monday: '-', Monday: '-',
Tuesday: '-', Tuesday: '-',
@@ -535,10 +535,10 @@ const STAGE_DAILY_INFO = [
{ value: '1-7', text: '1-7', days: [1, 2, 3, 4, 5, 6, 7] }, { value: '1-7', text: '1-7', days: [1, 2, 3, 4, 5, 6, 7] },
{ value: 'R8-11', text: 'R8-11', days: [1, 2, 3, 4, 5, 6, 7] }, { value: 'R8-11', text: 'R8-11', days: [1, 2, 3, 4, 5, 6, 7] },
{ value: '12-17-HARD', text: '12-17-HARD', days: [1, 2, 3, 4, 5, 6, 7] }, { value: '12-17-HARD', text: '12-17-HARD', days: [1, 2, 3, 4, 5, 6, 7] },
{ value: 'LS-6', text: '经验-6/5', days: [1, 2, 3, 4, 5, 6, 7] },
{ value: 'CE-6', text: '龙门币-6/5', days: [2, 4, 6, 7] }, { value: 'CE-6', text: '龙门币-6/5', days: [2, 4, 6, 7] },
{ value: 'AP-5', text: '红票-5', days: [1, 4, 6, 7] }, { value: 'AP-5', text: '红票-5', days: [1, 4, 6, 7] },
{ value: 'CA-5', text: '技能-5', days: [2, 3, 5, 7] }, { value: 'CA-5', text: '技能-5', days: [2, 3, 5, 7] },
{ value: 'LS-6', text: '经验-6/5', days: [1, 2, 3, 4, 5, 6, 7] },
{ value: 'SK-5', text: '碳-5', days: [1, 3, 5, 6] }, { value: 'SK-5', text: '碳-5', days: [1, 3, 5, 6] },
{ value: 'PR-A-1', text: '奶/盾芯片', days: [1, 4, 5, 7] }, { value: 'PR-A-1', text: '奶/盾芯片', days: [1, 4, 5, 7] },
{ value: 'PR-A-2', text: '奶/盾芯片组', days: [1, 4, 5, 7] }, { value: 'PR-A-2', text: '奶/盾芯片组', days: [1, 4, 5, 7] },
@@ -590,20 +590,20 @@ const getSelectOptions = (columnKey: string, taskName: string, currentValue?: st
switch (taskName) { switch (taskName) {
case '连战次数': case '连战次数':
return [ return [
{ label: '不选择', value: '0' }, { label: 'AUTO', value: '0' },
{ label: '1', value: '1' }, { label: '1', value: '1' },
{ label: '2', value: '2' }, { label: '2', value: '2' },
{ label: '3', value: '3' }, { label: '3', value: '3' },
{ label: '4', value: '4' }, { label: '4', value: '4' },
{ label: '5', value: '5' }, { label: '5', value: '5' },
{ label: '6', value: '6' }, { label: '6', value: '6' },
{ label: 'AUTO', value: '-1' }, { label: '不切换', value: '-1' },
] ]
case '关卡选择': case '关卡选择':
case '备选关卡-1': case '备选关卡-1':
case '备选关卡-2': case '备选关卡-2':
case '备选关卡-3': case '备选关卡-3':
case '剩余理智': { case '剩余理智关卡': {
const dayNumber = getDayNumber(columnKey) const dayNumber = getDayNumber(columnKey)
// 基础关卡选项 // 基础关卡选项
@@ -611,14 +611,14 @@ const getSelectOptions = (columnKey: string, taskName: string, currentValue?: st
if (dayNumber === 0) { if (dayNumber === 0) {
// 如果是全局列,显示所有选项 // 如果是全局列,显示所有选项
baseOptions = STAGE_DAILY_INFO.map(stage => ({ baseOptions = STAGE_DAILY_INFO.map(stage => ({
label: stage.text, label: taskName === '剩余理智关卡' && stage.value === '-' ? '不选择' : stage.text,
value: stage.value, value: stage.value,
isCustom: false isCustom: false
})) }))
} else { } else {
// 根据星期过滤可用的关卡 // 根据星期过滤可用的关卡
baseOptions = STAGE_DAILY_INFO.filter(stage => stage.days.includes(dayNumber)).map(stage => ({ baseOptions = STAGE_DAILY_INFO.filter(stage => stage.days.includes(dayNumber)).map(stage => ({
label: stage.text, label: taskName === '剩余理智关卡' && stage.value === '-' ? '不选择' : stage.text,
value: stage.value, value: stage.value,
isCustom: false isCustom: false
})) }))
@@ -670,8 +670,8 @@ const getPlaceholder = (taskName: string) => {
case '备选关卡-2': case '备选关卡-2':
case '备选关卡-3': case '备选关卡-3':
return '1-7' return '1-7'
case '剩余理智': case '剩余理智关卡':
return '-' return '不选择'
default: default:
return '请选择' return '请选择'
} }
@@ -1561,52 +1561,78 @@ const disableAllStages = (stageValue: string) => {
/* 任务名称单元格背景色 */ /* 任务名称单元格背景色 */
.config-table :deep(.task-row-MedicineNumb td:first-child) { .config-table :deep(.task-row-MedicineNumb td:first-child) {
background: rgba(59, 130, 246, 0.1); background: #EBF4FF !important; /* 不透明的蓝色背景 */
color: #3B82F6; color: #3B82F6;
font-weight: 500; font-weight: 500;
} }
.config-table :deep(.ant-table-tbody > tr.task-row-MedicineNumb:hover > td:first-child) { .config-table :deep(.ant-table-tbody > tr.task-row-MedicineNumb:hover > td:first-child) {
background: rgba(59, 130, 246, 0.2); background: #DBEAFE !important; /* 悬停时稍深的蓝色 */
} }
.config-table :deep(.task-row-SeriesNumb td:first-child) { .config-table :deep(.task-row-SeriesNumb td:first-child) {
background: rgba(34, 197, 94, 0.1); background: #ECFDF5 !important; /* 不透明的绿色背景 */
color: #22C55E; color: #22C55E;
font-weight: 500; font-weight: 500;
} }
.config-table :deep(.ant-table-tbody > tr.task-row-SeriesNumb:hover > td:first-child) { .config-table :deep(.ant-table-tbody > tr.task-row-SeriesNumb:hover > td:first-child) {
background: rgba(34, 197, 94, 0.2); background: #D1FAE5 !important; /* 悬停时稍深的绿色 */
} }
.config-table :deep(.task-row-Stage td:first-child) { .config-table :deep(.task-row-Stage td:first-child) {
background: rgba(249, 115, 22, 0.1); background: #FFF7ED !important; /* 不透明的橙色背景 */
color: #F97316; color: #F97316;
font-weight: 500; font-weight: 500;
} }
.config-table :deep(.ant-table-tbody > tr.task-row-Stage:hover > td:first-child) { .config-table :deep(.ant-table-tbody > tr.task-row-Stage:hover > td:first-child) {
background: rgba(249, 115, 22, 0.2); background: #FED7AA !important; /* 悬停时稍深的橙色 */
} }
.config-table :deep(.task-row-Stage_1 td:first-child), .config-table :deep(.task-row-Stage_1 td:first-child),
.config-table :deep(.task-row-Stage_2 td:first-child), .config-table :deep(.task-row-Stage_2 td:first-child),
.config-table :deep(.task-row-Stage_3 td:first-child) { .config-table :deep(.task-row-Stage_3 td:first-child) {
background: rgba(168, 85, 247, 0.1); background: #FAF5FF !important; /* 不透明的紫色背景 */
color: #A855F7; color: #A855F7;
font-weight: 500; font-weight: 500;
} }
.config-table :deep(.ant-table-tbody > tr.task-row-Stage_1:hover > td:first-child), .config-table :deep(.ant-table-tbody > tr.task-row-Stage_1:hover > td:first-child),
.config-table :deep(.ant-table-tbody > tr.task-row-Stage_2:hover > td:first-child), .config-table :deep(.ant-table-tbody > tr.task-row-Stage_2:hover > td:first-child),
.config-table :deep(.ant-table-tbody > tr.task-row-Stage_3:hover > td:first-child) { .config-table :deep(.ant-table-tbody > tr.task-row-Stage_3:hover > td:first-child) {
background: rgba(168, 85, 247, 0.2); background: #F3E8FF !important; /* 悬停时稍深的紫色 */
} }
.config-table :deep(.task-row-Stage_Remain td:first-child) { .config-table :deep(.task-row-Stage_Remain td:first-child) {
background: rgba(14, 165, 233, 0.1); background: #F0F9FF !important; /* 不透明的天蓝色背景 */
color: #0EA5E9; color: #0EA5E9;
font-weight: 500; font-weight: 500;
} }
.config-table :deep(.ant-table-tbody > tr.task-row-Stage_Remain:hover > td:first-child) { .config-table :deep(.ant-table-tbody > tr.task-row-Stage_Remain:hover > td:first-child) {
background: rgba(14, 165, 233, 0.2); background: #E0F2FE !important; /* 悬停时稍深的天蓝色 */
}
/* 确保固定列在滚动时背景不透明 */
.config-table :deep(.ant-table-fixed-left) {
background: var(--ant-color-bg-container) !important;
}
.config-table :deep(.ant-table-thead > tr > th.ant-table-fixed-left) {
background: var(--ant-color-bg-container) !important;
border-right: 1px solid var(--ant-color-border);
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
}
/* 专门处理"配置项"表头单元格 */
.config-table :deep(.ant-table-thead > tr > th:first-child) {
background: var(--ant-color-bg-container) !important;
border-right: 1px solid var(--ant-color-border);
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 2;
}
.config-table :deep(.ant-table-tbody > tr > td.ant-table-fixed-left) {
background: inherit !important;
border-right: 1px solid var(--ant-color-border-secondary);
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
} }
/* 禁用列标题样式 */ /* 禁用列标题样式 */

File diff suppressed because it is too large Load Diff

View File

@@ -40,8 +40,8 @@
@add-user="handleAddUser" @add-user="handleAddUser"
@edit-user="handleEditUser" @edit-user="handleEditUser"
@delete-user="handleDeleteUser" @delete-user="handleDeleteUser"
@maa-config="handleMAAConfig" @start-maa-config="handleStartMAAConfig"
@disconnect-maa="handleDisconnectMAA" @save-maa-config="handleSaveMAAConfig"
@toggle-user-status="handleToggleUserStatus" @toggle-user-status="handleToggleUserStatus"
/> />
@@ -232,6 +232,8 @@ import { useScriptApi } from '@/composables/useScriptApi'
import { useUserApi } from '@/composables/useUserApi' import { useUserApi } from '@/composables/useUserApi'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi' import { useTemplateApi, type WebConfigTemplate } from '@/composables/useTemplateApi'
import { Service } from '@/api/services/Service'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
const router = useRouter() const router = useRouter()
@@ -494,51 +496,89 @@ const handleDeleteUser = async (user: User) => {
} }
} }
const handleMAAConfig = async (script: Script) => { const handleStartMAAConfig = async (script: Script) => {
try { try {
// 检查是否已有连接 // 检查是否已有连接
const existingConnection = activeConnections.value.get(script.id) const existingConnection = activeConnections.value.get(script.id)
if (existingConnection) { if (existingConnection) {
message.warning('该脚本已在配置中,请先断开连接') message.warning('该脚本已在配置中,请先保存配置')
return return
} }
// 新订阅 // 调用启动配置任务API
subscribe(script.id, { const response = await Service.addTaskApiDispatchStartPost({
taskId: script.id,
mode: TaskCreateIn.mode.SettingScriptMode
})
if (response.code === 200) {
// 订阅WebSocket消息
subscribe(response.websocketId, {
onError: error => { onError: error => {
console.error(`脚本 ${script.name} 连接错误:`, error) console.error(`脚本 ${script.name} 连接错误:`, error)
message.error(`MAA配置连接失败: ${error}`) message.error(`MAA配置连接失败: ${error}`)
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
}, },
onResult: (data: any) => {
// 处理配置完成消息(兼容任何结构)
if (data.Accomplish) {
message.success(`${script.name} 配置已完成`)
activeConnections.value.delete(script.id)
}
}
}) })
// 记录连接 // 记录连接和websocketId
activeConnections.value.set(script.id, script.id) activeConnections.value.set(script.id, response.websocketId)
message.success(`开始配置 ${script.name}`) message.success(`启动 ${script.name} 的MAA配置`)
// 可选:设置自动断开连接的定时器(比如30分钟后 // 设置自动断开连接的定时器30分钟后
setTimeout( setTimeout(
() => { () => {
if (activeConnections.value.has(script.id)) { if (activeConnections.value.has(script.id)) {
unsubscribe(script.id) const wsId = activeConnections.value.get(script.id)
if (wsId) {
unsubscribe(wsId)
}
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
message.info(`${script.name} 配置会话已超时断开`) message.info(`${script.name} 配置会话已超时断开`)
} }
}, },
30 * 60 * 1000 30 * 60 * 1000
) // 30分钟 ) // 30分钟
} else {
message.error(response.message || '启动MAA配置失败')
}
} catch (error) { } catch (error) {
console.error('MAA配置失败:', error) console.error('启动MAA配置失败:', error)
message.error('MAA配置失败') message.error('启动MAA配置失败')
} }
} }
const handleDisconnectMAA = (script: Script) => { const handleSaveMAAConfig = async (script: Script) => {
const connectionId = activeConnections.value.get(script.id) try {
if (connectionId) { const websocketId = activeConnections.value.get(script.id)
unsubscribe(script.id) if (!websocketId) {
message.error('未找到活动的配置会话')
return
}
// 调用停止配置任务API
const response = await Service.stopTaskApiDispatchStopPost({
taskId: websocketId
})
if (response.code === 200) {
// 取消订阅
unsubscribe(websocketId)
activeConnections.value.delete(script.id) activeConnections.value.delete(script.id)
message.success(`已断开 ${script.name} 的配置连接`) message.success(`${script.name} 的配置已保存`)
} else {
message.error(response.message || '保存配置失败')
}
} catch (error) {
console.error('保存MAA配置失败:', error)
message.error('保存MAA配置失败')
} }
} }

View File

@@ -11,8 +11,6 @@ import { useUpdateChecker } from '@/composables/useUpdateChecker'
import { Service, type VersionOut } from '@/api' import { Service, type VersionOut } from '@/api'
import UpdateModal from '@/components/UpdateModal.vue' import UpdateModal from '@/components/UpdateModal.vue'
import { mirrorManager } from '@/utils/mirrorManager' import { mirrorManager } from '@/utils/mirrorManager'
import { request } from '@/api/core/request'
import { OpenAPI } from '@/api'
// 引入拆分后的 Tab 组件 // 引入拆分后的 Tab 组件
import TabBasic from './TabBasic.vue' import TabBasic from './TabBasic.vue'
@@ -243,7 +241,7 @@ const testingNotify = ref(false)
const testNotify = async () => { const testNotify = async () => {
testingNotify.value = true testingNotify.value = true
try { try {
const res: any = await request<any>(OpenAPI, { method: 'POST', url: '/api/setting/test_notify' }) const res = await Service.testNotifyApiSettingTestNotifyPost()
if (res?.code && res.code !== 200) message.warning(res?.message || '测试通知发送结果未知') if (res?.code && res.code !== 200) message.warning(res?.message || '测试通知发送结果未知')
else message.success('测试通知已发送') else message.success('测试通知已发送')
} catch (e) { } catch (e) {