Merge branch 'feature/refactor' into z
This commit is contained in:
@@ -116,6 +116,10 @@ class GlobalConfig(ConfigBase):
|
|||||||
"Update", "MirrorChyanCDK", "", EncryptValidator()
|
"Update", "MirrorChyanCDK", "", EncryptValidator()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
|
||||||
|
Data_LastStatisticsUpload = ConfigItem(
|
||||||
|
"Data", "LastStatisticsUpload", "2000-01-01 00:00:00"
|
||||||
|
)
|
||||||
Data_LastStageUpdated = ConfigItem(
|
Data_LastStageUpdated = ConfigItem(
|
||||||
"Data", "LastStageUpdated", "2000-01-01 00:00:00"
|
"Data", "LastStageUpdated", "2000-01-01 00:00:00"
|
||||||
)
|
)
|
||||||
@@ -190,7 +194,7 @@ class MaaUserConfig(ConfigBase):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.Info_Name = ConfigItem("Info", "Name", "新用户")
|
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||||
self.Info_Id = ConfigItem("Info", "Id", "")
|
self.Info_Id = ConfigItem("Info", "Id", "")
|
||||||
self.Info_Mode = ConfigItem(
|
self.Info_Mode = ConfigItem(
|
||||||
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
|
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
|
||||||
@@ -451,7 +455,7 @@ class GeneralUserConfig(ConfigBase):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.Info_Name = ConfigItem("Info", "Name", "新用户")
|
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
|
||||||
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
|
||||||
self.Info_RemainedDay = ConfigItem(
|
self.Info_RemainedDay = ConfigItem(
|
||||||
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
|
||||||
@@ -460,13 +464,13 @@ class GeneralUserConfig(ConfigBase):
|
|||||||
"Info", "IfScriptBeforeTask", False, BoolValidator()
|
"Info", "IfScriptBeforeTask", False, BoolValidator()
|
||||||
)
|
)
|
||||||
self.Info_ScriptBeforeTask = ConfigItem(
|
self.Info_ScriptBeforeTask = ConfigItem(
|
||||||
"Info", "ScriptBeforeTask", "", FileValidator()
|
"Info", "ScriptBeforeTask", str(Path.cwd()), FileValidator()
|
||||||
)
|
)
|
||||||
self.Info_IfScriptAfterTask = ConfigItem(
|
self.Info_IfScriptAfterTask = ConfigItem(
|
||||||
"Info", "IfScriptAfterTask", False, BoolValidator()
|
"Info", "IfScriptAfterTask", False, BoolValidator()
|
||||||
)
|
)
|
||||||
self.Info_ScriptAfterTask = ConfigItem(
|
self.Info_ScriptAfterTask = ConfigItem(
|
||||||
"Info", "ScriptAfterTask", "", FileValidator()
|
"Info", "ScriptAfterTask", str(Path.cwd()), FileValidator()
|
||||||
)
|
)
|
||||||
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
self.Info_Notes = ConfigItem("Info", "Notes", "无")
|
||||||
|
|
||||||
@@ -571,7 +575,7 @@ TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
|
|||||||
|
|
||||||
class AppConfig(GlobalConfig):
|
class AppConfig(GlobalConfig):
|
||||||
|
|
||||||
VERSION = "5.0.0.1"
|
VERSION = [5, 0, 0, 1]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(if_save_multi_config=False)
|
super().__init__(if_save_multi_config=False)
|
||||||
@@ -579,7 +583,7 @@ class AppConfig(GlobalConfig):
|
|||||||
logger.info("")
|
logger.info("")
|
||||||
logger.info("===================================")
|
logger.info("===================================")
|
||||||
logger.info("AUTO_MAA 后端应用程序")
|
logger.info("AUTO_MAA 后端应用程序")
|
||||||
logger.info(f"版本号: v{self.VERSION}")
|
logger.info(f"版本号: {self.version()}")
|
||||||
logger.info(f"工作目录: {Path.cwd()}")
|
logger.info(f"工作目录: {Path.cwd()}")
|
||||||
logger.info("===================================")
|
logger.info("===================================")
|
||||||
|
|
||||||
@@ -605,6 +609,16 @@ class AppConfig(GlobalConfig):
|
|||||||
|
|
||||||
truststore.inject_into_ssl()
|
truststore.inject_into_ssl()
|
||||||
|
|
||||||
|
def version(self) -> str:
|
||||||
|
"""获取版本号字符串"""
|
||||||
|
|
||||||
|
if self.VERSION[3] == 0:
|
||||||
|
return f"v{'.'.join(str(_) for _ in self.VERSION[0:3])}"
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
f"v{'.'.join(str(_) for _ in self.VERSION[0:3])}-beta.{self.VERSION[3]}"
|
||||||
|
)
|
||||||
|
|
||||||
async def init_config(self) -> None:
|
async def init_config(self) -> None:
|
||||||
"""初始化配置管理"""
|
"""初始化配置管理"""
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,14 @@ class _TaskManager:
|
|||||||
partial(self.task_dict.pop, script_id)
|
partial(self.task_dict.pop, script_id)
|
||||||
)
|
)
|
||||||
await self.task_dict[script_id]
|
await self.task_dict[script_id]
|
||||||
|
task["status"] = "完成"
|
||||||
|
await Config.send_json(
|
||||||
|
WebSocketMessage(
|
||||||
|
id=str(task_id),
|
||||||
|
type="Update",
|
||||||
|
data={"task_list": task_list},
|
||||||
|
).model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
async def stop_task(self, task_id: str) -> None:
|
async def stop_task(self, task_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import asyncio
|
|||||||
import keyboard
|
import keyboard
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app.services import System
|
from app.services import Matomo, System
|
||||||
from app.utils import get_logger
|
from app.utils import get_logger
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -42,6 +42,33 @@ class _MainTimer:
|
|||||||
|
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def hour_task(self):
|
||||||
|
"""每小时定期任务"""
|
||||||
|
|
||||||
|
logger.info("每小时定期任务启动")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
if (
|
||||||
|
datetime.strptime(
|
||||||
|
Config.get("Data", "LastStatisticsUpload"), "%Y-%m-%d %H:%M:%S"
|
||||||
|
).date()
|
||||||
|
!= datetime.now().date()
|
||||||
|
):
|
||||||
|
await Matomo.send_event(
|
||||||
|
"App",
|
||||||
|
"Version",
|
||||||
|
Config.version(),
|
||||||
|
1 if "beta" in Config.version() else 0,
|
||||||
|
)
|
||||||
|
await Config.set(
|
||||||
|
"Data",
|
||||||
|
"LastStatisticsUpload",
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
|
||||||
async def set_silence(self):
|
async def set_silence(self):
|
||||||
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
|
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ import uuid
|
|||||||
import win32com.client
|
import win32com.client
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Any, Dict, Union
|
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
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidator:
|
class ConfigValidator:
|
||||||
@@ -97,8 +98,22 @@ class UidValidator(ConfigValidator):
|
|||||||
return value if self.validate(value) else None
|
return value if self.validate(value) else None
|
||||||
|
|
||||||
|
|
||||||
|
class UUIDValidator(ConfigValidator):
|
||||||
|
"""UUID验证器"""
|
||||||
|
|
||||||
|
def validate(self, value: Any) -> bool:
|
||||||
|
try:
|
||||||
|
uuid.UUID(value)
|
||||||
|
return True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def correct(self, value: Any) -> Any:
|
||||||
|
return value if self.validate(value) else str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
class EncryptValidator(ConfigValidator):
|
class EncryptValidator(ConfigValidator):
|
||||||
"""加数据验证器"""
|
"""加密数据验证器"""
|
||||||
|
|
||||||
def validate(self, value: Any) -> bool:
|
def validate(self, value: Any) -> bool:
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
@@ -163,6 +178,46 @@ class FolderValidator(ConfigValidator):
|
|||||||
return Path(value).resolve().as_posix()
|
return Path(value).resolve().as_posix()
|
||||||
|
|
||||||
|
|
||||||
|
class UserNameValidator(ConfigValidator):
|
||||||
|
"""用户名验证器"""
|
||||||
|
|
||||||
|
def validate(self, value: Any) -> bool:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not value or not value.strip():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if value != value.strip() or value != value.strip("."):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if any(char in ILLEGAL_CHARS for char in value):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if value.upper() in RESERVED_NAMES:
|
||||||
|
return False
|
||||||
|
if len(value) > 255:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def correct(self, value: Any) -> str:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
value = "默认用户名"
|
||||||
|
|
||||||
|
value = value.strip().strip(".")
|
||||||
|
|
||||||
|
value = "".join(char for char in value if char not in ILLEGAL_CHARS)
|
||||||
|
|
||||||
|
if value.upper() in RESERVED_NAMES or not value:
|
||||||
|
value = "默认用户名"
|
||||||
|
|
||||||
|
if len(value) > 255:
|
||||||
|
value = value[:255]
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class ConfigItem:
|
class ConfigItem:
|
||||||
"""配置项"""
|
"""配置项"""
|
||||||
|
|
||||||
@@ -171,7 +226,7 @@ class ConfigItem:
|
|||||||
group: str,
|
group: str,
|
||||||
name: str,
|
name: str,
|
||||||
default: Any,
|
default: Any,
|
||||||
validator: None | ConfigValidator = None,
|
validator: Optional[ConfigValidator] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
@@ -195,7 +250,10 @@ class ConfigItem:
|
|||||||
self.validator = validator or ConfigValidator()
|
self.validator = validator or ConfigValidator()
|
||||||
self.is_locked = False
|
self.is_locked = False
|
||||||
|
|
||||||
self.setValue(default)
|
if not self.validator.validate(self.value):
|
||||||
|
raise ValueError(
|
||||||
|
f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法"
|
||||||
|
)
|
||||||
|
|
||||||
def setValue(self, value: Any):
|
def setValue(self, value: Any):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ __version__ = "5.0.0"
|
|||||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||||
__license__ = "GPL-3.0 license"
|
__license__ = "GPL-3.0 license"
|
||||||
|
|
||||||
|
from .matomo import Matomo
|
||||||
from .notification import Notify
|
from .notification import Notify
|
||||||
from .system import System
|
from .system import System
|
||||||
|
|
||||||
|
|||||||
125
app/services/matomo.py
Normal file
125
app/services/matomo.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
|
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License,
|
||||||
|
# or (at your option) any later version.
|
||||||
|
|
||||||
|
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||||
|
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
# the GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import psutil
|
||||||
|
import platform
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from app.core import Config
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("信息上报")
|
||||||
|
|
||||||
|
|
||||||
|
class _MatomoHandler:
|
||||||
|
"""Matomo统计上报服务"""
|
||||||
|
|
||||||
|
base_url = "https://statistics.auto-mas.top/matomo.php"
|
||||||
|
site_id = "3"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
async def _get_session(self):
|
||||||
|
"""获取HTTP会话"""
|
||||||
|
|
||||||
|
if self.session is None or self.session.closed:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
self.session = aiohttp.ClientSession(timeout=timeout)
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""关闭HTTP会话"""
|
||||||
|
if self.session and not self.session.closed:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
def _build_base_params(self, custom_vars: Optional[Dict[str, Any]] = None):
|
||||||
|
"""构建基础参数"""
|
||||||
|
params = {
|
||||||
|
"idsite": self.site_id,
|
||||||
|
"rec": "1",
|
||||||
|
"action_name": "AUTO-MAS后端",
|
||||||
|
"_id": Config.get("Data", "UID")[:16],
|
||||||
|
"uid": Config.get("Data", "UID"),
|
||||||
|
"rand": str(uuid.uuid4().int)[:10],
|
||||||
|
"apiv": "1",
|
||||||
|
"h": time.strftime("%H"),
|
||||||
|
"m": time.strftime("%M"),
|
||||||
|
"s": time.strftime("%S"),
|
||||||
|
"ua": f"AUTO-MAS/{Config.version()} ({platform.system()} {platform.release()})",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加自定义变量
|
||||||
|
if custom_vars is not None:
|
||||||
|
cvar = {}
|
||||||
|
for i, (key, value) in enumerate(custom_vars.items(), 1):
|
||||||
|
if i <= 5:
|
||||||
|
cvar[str(i)] = [str(key), str(value)]
|
||||||
|
if cvar:
|
||||||
|
params["_cvar"] = json.dumps(cvar)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
async def send_event(
|
||||||
|
self,
|
||||||
|
category: str,
|
||||||
|
action: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
value: Optional[float] = None,
|
||||||
|
custom_vars: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""发送事件数据到Matomo
|
||||||
|
|
||||||
|
Args:
|
||||||
|
category: 事件类别,如 "Script", "Config", "User"
|
||||||
|
action: 事件动作,如 "Execute", "Update", "Login"
|
||||||
|
name: 事件名称,如具体的脚本名称
|
||||||
|
value: 事件值,如执行时长、文件大小等数值
|
||||||
|
custom_vars: 自定义变量字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = await self._get_session()
|
||||||
|
if session is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
params = self._build_base_params(custom_vars)
|
||||||
|
params.update({"e_c": category, "e_a": action, "e_n": name, "e_v": value})
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
async with session.get(self.base_url, params=params) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
logger.debug(f"Matomo事件上报成功: {category}/{action}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Matomo事件上报失败: {response.status}")
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("Matomo事件上报超时")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Matomo事件上报错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
Matomo = _MatomoHandler()
|
||||||
@@ -226,3 +226,32 @@ MATERIALS_MAP = {
|
|||||||
"PR-D": "近卫/特种芯片",
|
"PR-D": "近卫/特种芯片",
|
||||||
}
|
}
|
||||||
"""掉落物索引表"""
|
"""掉落物索引表"""
|
||||||
|
|
||||||
|
RESERVED_NAMES = {
|
||||||
|
"CON",
|
||||||
|
"PRN",
|
||||||
|
"AUX",
|
||||||
|
"NUL",
|
||||||
|
"COM1",
|
||||||
|
"COM2",
|
||||||
|
"COM3",
|
||||||
|
"COM4",
|
||||||
|
"COM5",
|
||||||
|
"COM6",
|
||||||
|
"COM7",
|
||||||
|
"COM8",
|
||||||
|
"COM9",
|
||||||
|
"LPT1",
|
||||||
|
"LPT2",
|
||||||
|
"LPT3",
|
||||||
|
"LPT4",
|
||||||
|
"LPT5",
|
||||||
|
"LPT6",
|
||||||
|
"LPT7",
|
||||||
|
"LPT8",
|
||||||
|
"LPT9",
|
||||||
|
}
|
||||||
|
"""Windows保留名称列表"""
|
||||||
|
|
||||||
|
ILLEGAL_CHARS = set('<>:"/\\|?*')
|
||||||
|
"""文件名非法字符集合"""
|
||||||
|
|||||||
@@ -490,6 +490,26 @@ ipcMain.handle('open-url', async (_event, url: string) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
ipcMain.handle('open-file', async (_event, filePath: string) => {
|
||||||
|
try {
|
||||||
|
await shell.openPath(filePath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开文件失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示文件所在目录并选中文件
|
||||||
|
ipcMain.handle('show-item-in-folder', async (_event, filePath: string) => {
|
||||||
|
try {
|
||||||
|
shell.showItemInFolder(filePath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('显示文件所在目录失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 环境检查
|
// 环境检查
|
||||||
ipcMain.handle('check-environment', async () => {
|
ipcMain.handle('check-environment', async () => {
|
||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
|
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
|
||||||
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),
|
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),
|
||||||
|
|
||||||
|
// 文件系统操作
|
||||||
|
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
||||||
|
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"electron-dev": "wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
|
"electron-dev": "wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
|
||||||
"build:main": "tsc -p tsconfig.electron.json",
|
"build:main": "tsc -p tsconfig.electron.json",
|
||||||
"build": "vite build && yarn build:main && electron-builder",
|
"build": "vite build && yarn build:main && electron-builder",
|
||||||
"web": "vite"
|
"web": "vite",
|
||||||
|
"release": "vite build && yarn build:main && electron-builder --win --publish always"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"asar": true,
|
"asar": true,
|
||||||
@@ -20,29 +21,37 @@
|
|||||||
"appId": "xyz.automaa.frontend",
|
"appId": "xyz.automaa.frontend",
|
||||||
"productName": "AUTO_MAA",
|
"productName": "AUTO_MAA",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist/**",
|
||||||
"dist-electron",
|
"dist-electron/**",
|
||||||
"public",
|
"public/**",
|
||||||
"!src/assets/*"
|
"!src/**",
|
||||||
|
"!**/*.map"
|
||||||
|
],
|
||||||
|
"publish": [
|
||||||
|
{
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "DLmaster_361",
|
||||||
|
"repo": "AUTO_MAA"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{ "from": "src/assets", "to": "assets", "filter": ["**/*"] }
|
||||||
"from": "src/assets",
|
|
||||||
"to": "assets",
|
|
||||||
"filter": []
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"requestedExecutionLevel": "requireAdministrator",
|
"requestedExecutionLevel": "requireAdministrator",
|
||||||
"target": "dir",
|
"target": [
|
||||||
|
{ "target": "nsis", "arch": ["x64"] }
|
||||||
|
],
|
||||||
"icon": "public/AUTO-MAS.ico",
|
"icon": "public/AUTO-MAS.ico",
|
||||||
"artifactName": "AUTO_MAA.exe"
|
"artifactName": "AUTO_MAA-Setup-${version}-${arch}.${ext}"
|
||||||
},
|
},
|
||||||
"mac": {
|
"nsis": {
|
||||||
"icon": "public/AUTO-MAS.ico"
|
"oneClick": false,
|
||||||
},
|
"perMachine": true,
|
||||||
"linux": {
|
"allowToChangeInstallationDirectory": true,
|
||||||
"icon": "public/AUTO-MAS.ico"
|
"createDesktopShortcut": true,
|
||||||
|
"shortcutName": "AUTO_MAA",
|
||||||
|
"differentialPackage": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,78 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-layout style="height: 100vh; overflow: hidden" class="app-layout-collapsed">
|
<a-layout style="height: 100vh; overflow: hidden">
|
||||||
<a-layout-sider
|
<a-layout-sider
|
||||||
v-model:collapsed="collapsed"
|
:width="SIDER_WIDTH"
|
||||||
collapsible
|
|
||||||
:trigger="null"
|
|
||||||
:width="180"
|
|
||||||
:collapsed-width="60"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
style="height: calc(100vh - 32px); position: fixed; left: 0; top: 32px; z-index: 100"
|
:style="{ height: 'calc(100vh - 32px)', position: 'fixed', left: '0', top: '32px', zIndex: 100, background: 'var(--app-sider-bg)', borderRight: '1px solid var(--app-sider-border-color)' }"
|
||||||
>
|
>
|
||||||
<div class="sider-content">
|
<div class="sider-content">
|
||||||
<!-- <!– 折叠按钮 –>-->
|
|
||||||
<!-- <div class="collapse-trigger" @click="toggleCollapse">-->
|
|
||||||
<!-- <MenuFoldOutlined v-if="!collapsed" />-->
|
|
||||||
<!-- <MenuUnfoldOutlined v-else />-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!-- 主菜单容器 -->
|
|
||||||
<div class="main-menu-container">
|
|
||||||
<a-menu
|
<a-menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
:inline-collapsed="collapsed"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
class="main-menu"
|
|
||||||
v-model:selectedKeys="selectedKeys"
|
v-model:selectedKeys="selectedKeys"
|
||||||
>
|
:items="mainMenuItems"
|
||||||
<template v-for="item in mainMenuItems" :key="item.path">
|
@click="onMenuClick"
|
||||||
<a-menu-item @click="goTo(item.path)" :data-title="item.label">
|
/>
|
||||||
<template #icon>
|
|
||||||
<component :is="item.icon" />
|
|
||||||
</template>
|
|
||||||
<span v-if="!collapsed" class="menu-text">{{ item.label }}</span>
|
|
||||||
</a-menu-item>
|
|
||||||
</template>
|
|
||||||
</a-menu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部菜单(带3px底部内边距) -->
|
|
||||||
<a-menu
|
<a-menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
:inline-collapsed="collapsed"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
class="bottom-menu"
|
class="bottom-menu"
|
||||||
v-model:selectedKeys="selectedKeys"
|
v-model:selectedKeys="selectedKeys"
|
||||||
>
|
:items="bottomMenuItems"
|
||||||
<template v-for="item in bottomMenuItems" :key="item.path">
|
@click="onMenuClick"
|
||||||
<a-menu-item @click="goTo(item.path)" :data-title="item.label">
|
/>
|
||||||
<template #icon>
|
|
||||||
<component :is="item.icon" />
|
|
||||||
</template>
|
|
||||||
<span v-if="!collapsed" class="menu-text">{{ item.label }}</span>
|
|
||||||
</a-menu-item>
|
|
||||||
</template>
|
|
||||||
</a-menu>
|
|
||||||
</div>
|
</div>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<a-layout :style="{ marginLeft: SIDER_WIDTH + 'px', height: 'calc(100vh - 32px)', transition: 'margin-left .2s' }">
|
||||||
<a-layout
|
<a-layout-content class="content-area">
|
||||||
:style="{
|
|
||||||
marginLeft: collapsed ? '60px' : '180px',
|
|
||||||
transition: 'margin-left 0.2s',
|
|
||||||
height: 'calc(100vh - 32px)',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<a-layout-content
|
|
||||||
class="content-area"
|
|
||||||
:style="{
|
|
||||||
padding: '24px',
|
|
||||||
background: isDark ? '#141414' : '#ffffff',
|
|
||||||
height: '100%',
|
|
||||||
overflow: 'auto',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
@@ -88,202 +41,92 @@ import {
|
|||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
MenuFoldOutlined,
|
|
||||||
MenuUnfoldOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { ref, computed } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useTheme } from '../composables/useTheme.ts'
|
import { useTheme } from '../composables/useTheme.ts'
|
||||||
|
import type { MenuProps } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const SIDER_WIDTH = 140
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
const collapsed = ref<boolean>(false)
|
// 工具:生成菜单项
|
||||||
|
const icon = (Comp: any) => () => h(Comp)
|
||||||
|
|
||||||
// 菜单数据
|
|
||||||
const mainMenuItems = [
|
const mainMenuItems = [
|
||||||
{ path: '/home', label: '主页', icon: HomeOutlined },
|
{ key: '/home', label: '主页', icon: icon(HomeOutlined) },
|
||||||
{ path: '/scripts', label: '脚本管理', icon: FileTextOutlined },
|
{ key: '/scripts', label: '脚本管理', icon: icon(FileTextOutlined) },
|
||||||
{ path: '/plans', label: '计划管理', icon: CalendarOutlined },
|
{ key: '/plans', label: '计划管理', icon: icon(CalendarOutlined) },
|
||||||
{ path: '/queue', label: '调度队列', icon: UnorderedListOutlined },
|
{ key: '/queue', label: '调度队列', icon: icon(UnorderedListOutlined) },
|
||||||
{ path: '/scheduler', label: '调度中心', icon: ControlOutlined },
|
{ key: '/scheduler', label: '调度中心', icon: icon(ControlOutlined) },
|
||||||
{ path: '/history', label: '历史记录', icon: HistoryOutlined },
|
{ key: '/history', label: '历史记录', icon: icon(HistoryOutlined) },
|
||||||
|
]
|
||||||
|
const bottomMenuItems = [
|
||||||
|
{ key: '/settings', label: '设置', icon: icon(SettingOutlined) },
|
||||||
]
|
]
|
||||||
|
|
||||||
const bottomMenuItems = [{ path: '/settings', label: '设置', icon: SettingOutlined }]
|
const allItems = [...mainMenuItems, ...bottomMenuItems]
|
||||||
|
|
||||||
// 自动同步选中项
|
// 选中项:根据当前路径前缀匹配
|
||||||
const selectedKeys = computed(() => {
|
const selectedKeys = computed(() => {
|
||||||
const path = route.path
|
const path = route.path
|
||||||
const allItems = [...mainMenuItems, ...bottomMenuItems]
|
const matched = allItems.find(i => path.startsWith(String(i.key)))
|
||||||
const matched = allItems.find(item => path.startsWith(item.path))
|
return [matched?.key || '/home']
|
||||||
return [matched?.path || '/home']
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const goTo = (path: string) => {
|
const onMenuClick: MenuProps['onClick'] = info => {
|
||||||
router.push(path)
|
const target = String(info.key)
|
||||||
}
|
if (route.path !== target) router.push(target)
|
||||||
|
|
||||||
const toggleCollapse = () => {
|
|
||||||
collapsed.value = !collapsed.value
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sider-content {
|
.sider-content { height:100%; display:flex; flex-direction:column; padding:4px 0 8px 0; }
|
||||||
height: 100%;
|
.sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
|
||||||
display: flex;
|
/* 菜单项外框居中(左右留空),内容左对齐 */
|
||||||
flex-direction: column;
|
.sider-content :deep(.ant-menu .ant-menu-item) {
|
||||||
padding-bottom: 4px; /* 关键:添加3px底部内边距 */
|
color: var(--app-menu-text-color);
|
||||||
}
|
margin: 2px auto; /* 水平居中 */
|
||||||
|
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
||||||
/* 折叠按钮 */
|
border-radius: 6px;
|
||||||
.collapse-trigger {
|
padding: 0 10px !important; /* 左右内边距 */
|
||||||
height: 42px;
|
line-height: 36px;
|
||||||
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start; /* 左对齐图标与文字 */
|
||||||
margin: 4px;
|
gap: 6px;
|
||||||
border-radius: 6px;
|
transition: background .16s ease, color .16s ease;
|
||||||
cursor: pointer;
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
|
||||||
|
color: var(--app-menu-icon-color);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: background-color 0.2s;
|
line-height: 1;
|
||||||
|
transition: color .16s ease;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
/* Hover */
|
||||||
.collapse-trigger:hover {
|
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background: var(--app-menu-item-hover-bg, var(--app-menu-item-hover-bg-hex));
|
||||||
|
color: var(--app-menu-item-hover-text-color);
|
||||||
}
|
}
|
||||||
|
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) { color: var(--app-menu-item-hover-text-color); }
|
||||||
:deep(.ant-layout-sider-light) .collapse-trigger:hover {
|
/* Selected */
|
||||||
background-color: rgba(0, 0, 0, 0.04);
|
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
|
||||||
}
|
background: var(--app-menu-item-selected-bg, var(--app-menu-item-selected-bg-hex));
|
||||||
|
color: var(--app-menu-text-color) !important;
|
||||||
:deep(.ant-layout-sider-dark) .collapse-trigger {
|
font-weight: 500;
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-layout-sider-light) .collapse-trigger {
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主菜单容器 */
|
|
||||||
.main-menu-container {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
/* 修复滚动条显示问题 */
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu-container::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu-container::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部菜单 */
|
|
||||||
.bottom-menu {
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-layout-sider-light .bottom-menu) {
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项文字 */
|
|
||||||
.menu-text {
|
|
||||||
margin-left: 36px;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主题颜色 */
|
|
||||||
:deep(.ant-layout-sider-dark) .logo-text,
|
|
||||||
:deep(.ant-layout-sider-dark) .menu-text {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-layout-sider-light) .logo-text,
|
|
||||||
:deep(.ant-layout-sider-light) .menu-text {
|
|
||||||
color: rgba(0, 0, 0, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单项统一样式 */
|
|
||||||
:deep(.ant-menu-item),
|
|
||||||
:deep(.ant-menu-item-selected) {
|
|
||||||
position: relative;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 34px;
|
|
||||||
margin: 0 6px;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图标绝对定位 */
|
|
||||||
:deep(.ant-menu-item .ant-menu-item-icon) {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏内容区滚动条 */
|
|
||||||
.content-area {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-area::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) { color: var(--app-menu-icon-color); }
|
||||||
|
.sider-content :deep(.ant-menu-light .ant-menu-item::after),
|
||||||
|
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) { display: none; }
|
||||||
|
.bottom-menu { margin-top:auto; }
|
||||||
|
.content-area { height:100%; overflow:auto; scrollbar-width:none; -ms-overflow-style:none; }
|
||||||
|
.content-area::-webkit-scrollbar { display:none; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- 全局样式 -->
|
<!-- 调整:外框(菜单项背景块)水平居中,文字与图标左对齐 -->
|
||||||
<style>
|
|
||||||
/* 收缩状态下,通过 data-title 显示 Tooltip */
|
|
||||||
.app-layout-collapsed .ant-menu-inline-collapsed .ant-menu-item:hover::before {
|
|
||||||
content: attr(data-title);
|
|
||||||
position: absolute;
|
|
||||||
left: 60px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: #1890ff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 12px;
|
|
||||||
z-index: 1000;
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 修复底部菜单在折叠状态下的tooltip位置 */
|
|
||||||
.app-layout-collapsed .ant-menu-inline-collapsed .bottom-menu .ant-menu-item:hover::before {
|
|
||||||
left: 60px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保底部菜单在收缩状态下也有3px间距 */
|
|
||||||
.app-layout-collapsed .ant-menu-inline-collapsed .bottom-menu {
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<!-- 左侧:Logo和软件名 -->
|
<!-- 左侧:Logo和软件名 -->
|
||||||
<div class="title-bar-left">
|
<div class="title-bar-left">
|
||||||
<div class="logo-section">
|
<div class="logo-section">
|
||||||
|
<!-- 新增虚化主题色圆形阴影 -->
|
||||||
|
<span class="logo-glow" aria-hidden="true"></span>
|
||||||
<img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" />
|
<img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" />
|
||||||
<span class="title-text">AUTO-MAS</span>
|
<span class="title-text">AUTO-MAS</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +96,7 @@ onMounted(async () => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar-dark {
|
.title-bar-dark {
|
||||||
@@ -112,17 +115,42 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
position: relative; /* 使阴影绝对定位基准 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增:主题色虚化圆形阴影 */
|
||||||
|
.logo-glow {
|
||||||
|
position: absolute;
|
||||||
|
left: 55px; /* 调整:更贴近图标 */
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
||||||
|
height: 100px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0,0,0,0) 70%);
|
||||||
|
filter: blur(24px); /* 降低模糊避免越界过多 */
|
||||||
|
opacity: 0.4;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.title-bar-dark .logo-glow {
|
||||||
|
opacity: 0.7;
|
||||||
|
filter: blur(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-logo {
|
.title-logo {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1; /* 确保在阴影上方 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar-dark .title-text {
|
.title-bar-dark .title-text {
|
||||||
|
|||||||
@@ -71,15 +71,32 @@ const updateCSSVariables = () => {
|
|||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
const primaryColor = themeColors[themeColor.value]
|
const primaryColor = themeColors[themeColor.value]
|
||||||
|
|
||||||
|
// 基础背景(用于估算混合)
|
||||||
|
const baseLightBg = '#ffffff'
|
||||||
|
const baseDarkBg = '#141414'
|
||||||
|
const baseMenuBg = isDark.value ? baseDarkBg : baseLightBg
|
||||||
|
|
||||||
|
// 改进:侧边栏背景使用 HSL 调整而不是简单线性混合,提高在不同主色下的可读性
|
||||||
|
const siderBg = deriveSiderBg(primaryColor, baseMenuBg, isDark.value)
|
||||||
|
const siderBorder = deriveSiderBorder(siderBg, primaryColor, isDark.value)
|
||||||
|
|
||||||
|
// 基础文字候选色
|
||||||
|
const candidateTextLight = 'rgba(255,255,255,0.88)'
|
||||||
|
const candidateTextDark = 'rgba(0,0,0,0.88)'
|
||||||
|
|
||||||
|
// 选菜单文字颜色:对 siderBg 计算对比度,优先满足 >=4.5
|
||||||
|
const menuTextColor = pickAccessibleColor(candidateTextDark, candidateTextLight, siderBg)
|
||||||
|
const iconColor = menuTextColor
|
||||||
|
|
||||||
|
// ===== AntD token 变量(保留) =====
|
||||||
if (isDark.value) {
|
if (isDark.value) {
|
||||||
// 深色模式变量
|
|
||||||
root.style.setProperty('--ant-color-primary', primaryColor)
|
root.style.setProperty('--ant-color-primary', primaryColor)
|
||||||
root.style.setProperty('--ant-color-primary-hover', lightenColor(primaryColor, 10))
|
root.style.setProperty('--ant-color-primary-hover', hslLighten(primaryColor, 6))
|
||||||
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
|
root.style.setProperty('--ant-color-primary-bg', addAlpha(primaryColor, 0.10))
|
||||||
root.style.setProperty('--ant-color-text', 'rgba(255, 255, 255, 0.88)')
|
root.style.setProperty('--ant-color-text', 'rgba(255, 255, 255, 0.88)')
|
||||||
root.style.setProperty('--ant-color-text-secondary', 'rgba(255, 255, 255, 0.65)')
|
root.style.setProperty('--ant-color-text-secondary', 'rgba(255, 255, 255, 0.65)')
|
||||||
root.style.setProperty('--ant-color-text-tertiary', 'rgba(255, 255, 255, 0.45)')
|
root.style.setProperty('--ant-color-text-tertiary', 'rgba(255, 255, 255, 0.45)')
|
||||||
root.style.setProperty('--ant-color-bg-container', '#141414')
|
root.style.setProperty('--ant-color-bg-container', baseDarkBg)
|
||||||
root.style.setProperty('--ant-color-bg-layout', '#000000')
|
root.style.setProperty('--ant-color-bg-layout', '#000000')
|
||||||
root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f')
|
root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f')
|
||||||
root.style.setProperty('--ant-color-border', '#424242')
|
root.style.setProperty('--ant-color-border', '#424242')
|
||||||
@@ -88,14 +105,13 @@ const updateCSSVariables = () => {
|
|||||||
root.style.setProperty('--ant-color-success', '#52c41a')
|
root.style.setProperty('--ant-color-success', '#52c41a')
|
||||||
root.style.setProperty('--ant-color-warning', '#faad14')
|
root.style.setProperty('--ant-color-warning', '#faad14')
|
||||||
} else {
|
} else {
|
||||||
// 浅色模式变量
|
|
||||||
root.style.setProperty('--ant-color-primary', primaryColor)
|
root.style.setProperty('--ant-color-primary', primaryColor)
|
||||||
root.style.setProperty('--ant-color-primary-hover', darkenColor(primaryColor, 10))
|
root.style.setProperty('--ant-color-primary-hover', hslDarken(primaryColor, 6))
|
||||||
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
|
root.style.setProperty('--ant-color-primary-bg', addAlpha(primaryColor, 0.10))
|
||||||
root.style.setProperty('--ant-color-text', 'rgba(0, 0, 0, 0.88)')
|
root.style.setProperty('--ant-color-text', 'rgba(0, 0, 0, 0.88)')
|
||||||
root.style.setProperty('--ant-color-text-secondary', 'rgba(0, 0, 0, 0.65)')
|
root.style.setProperty('--ant-color-text-secondary', 'rgba(0, 0, 0, 0.65)')
|
||||||
root.style.setProperty('--ant-color-text-tertiary', 'rgba(0, 0, 0, 0.45)')
|
root.style.setProperty('--ant-color-text-tertiary', 'rgba(0, 0, 0, 0.45)')
|
||||||
root.style.setProperty('--ant-color-bg-container', '#ffffff')
|
root.style.setProperty('--ant-color-bg-container', baseLightBg)
|
||||||
root.style.setProperty('--ant-color-bg-layout', '#f5f5f5')
|
root.style.setProperty('--ant-color-bg-layout', '#f5f5f5')
|
||||||
root.style.setProperty('--ant-color-bg-elevated', '#ffffff')
|
root.style.setProperty('--ant-color-bg-elevated', '#ffffff')
|
||||||
root.style.setProperty('--ant-color-border', '#d9d9d9')
|
root.style.setProperty('--ant-color-border', '#d9d9d9')
|
||||||
@@ -104,9 +120,70 @@ const updateCSSVariables = () => {
|
|||||||
root.style.setProperty('--ant-color-success', '#52c41a')
|
root.style.setProperty('--ant-color-success', '#52c41a')
|
||||||
root.style.setProperty('--ant-color-warning', '#faad14')
|
root.style.setProperty('--ant-color-warning', '#faad14')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 自定义菜单配色 =====
|
||||||
|
// 动态 Alpha:根据主色亮度调整透明度以保持区分度
|
||||||
|
const lumPrim = getLuminance(primaryColor)
|
||||||
|
const hoverAlphaBase = isDark.value ? 0.22 : 0.14
|
||||||
|
const selectedAlphaBase = isDark.value ? 0.38 : 0.26
|
||||||
|
const hoverAlpha = clamp01(hoverAlphaBase + (isDark.value ? (lumPrim > 0.65 ? -0.04 : 0) : (lumPrim < 0.30 ? 0.04 : 0)))
|
||||||
|
const selectedAlpha = clamp01(selectedAlphaBase + (isDark.value ? (lumPrim > 0.65 ? -0.05 : 0) : (lumPrim < 0.30 ? 0.05 : 0)))
|
||||||
|
|
||||||
|
// 估算最终选中背景(混合算实际颜色用于对比度计算)
|
||||||
|
const estimatedSelectedBg = blendColors(baseMenuBg, primaryColor, selectedAlpha)
|
||||||
|
const selectedTextColor = pickAccessibleColor('rgba(0,0,0,0.90)', 'rgba(255,255,255,0.92)', estimatedSelectedBg)
|
||||||
|
const hoverTextColor = menuTextColor
|
||||||
|
|
||||||
|
root.style.setProperty('--app-sider-bg', siderBg)
|
||||||
|
root.style.setProperty('--app-sider-border-color', siderBorder)
|
||||||
|
root.style.setProperty('--app-menu-text-color', menuTextColor)
|
||||||
|
root.style.setProperty('--app-menu-icon-color', iconColor)
|
||||||
|
root.style.setProperty('--app-menu-item-hover-text-color', hoverTextColor)
|
||||||
|
root.style.setProperty('--app-menu-item-selected-text-color', selectedTextColor)
|
||||||
|
|
||||||
|
// 背景同时提供 rgba 与 hex alpha(兼容处理)
|
||||||
|
const hoverRgba = hexToRgba(primaryColor, hoverAlpha)
|
||||||
|
const selectedRgba = hexToRgba(primaryColor, selectedAlpha)
|
||||||
|
root.style.setProperty('--app-menu-item-hover-bg', hoverRgba)
|
||||||
|
root.style.setProperty('--app-menu-item-hover-bg-hex', addAlpha(primaryColor, hoverAlpha))
|
||||||
|
root.style.setProperty('--app-menu-item-selected-bg', selectedRgba)
|
||||||
|
root.style.setProperty('--app-menu-item-selected-bg-hex', addAlpha(primaryColor, selectedAlpha))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 颜色工具函数
|
// ===== 新增缺失基础函数(从旧版本恢复) =====
|
||||||
|
const addAlpha = (hex: string, alpha: number) => {
|
||||||
|
const a = alpha > 1 ? alpha / 100 : alpha
|
||||||
|
const clamped = Math.min(1, Math.max(0, a))
|
||||||
|
const alphaHex = Math.round(clamped * 255).toString(16).padStart(2, '0')
|
||||||
|
return `${hex}${alphaHex}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const blendColors = (color1: string, color2: string, ratio: number) => {
|
||||||
|
const r1 = hexToRgb(color1)
|
||||||
|
const r2 = hexToRgb(color2)
|
||||||
|
if (!r1 || !r2) return color1
|
||||||
|
const r = Math.round(r1.r * (1 - ratio) + r2.r * ratio)
|
||||||
|
const g = Math.round(r1.g * (1 - ratio) + r2.g * ratio)
|
||||||
|
const b = Math.round(r1.b * (1 - ratio) + r2.b * ratio)
|
||||||
|
return rgbToHex(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLuminance = (hex: string) => {
|
||||||
|
const rgb = hexToRgb(hex)
|
||||||
|
if (!rgb) return 0
|
||||||
|
const transform = (v: number) => {
|
||||||
|
const srgb = v / 255
|
||||||
|
return srgb <= 0.03928 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4)
|
||||||
|
}
|
||||||
|
const r = transform(rgb.r)
|
||||||
|
const g = transform(rgb.g)
|
||||||
|
const b = transform(rgb.b)
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 新增/改进的颜色工具 =====
|
||||||
|
const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
|
||||||
|
|
||||||
const hexToRgb = (hex: string) => {
|
const hexToRgb = (hex: string) => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return result
|
return result
|
||||||
@@ -122,24 +199,105 @@ const rgbToHex = (r: number, g: number, b: number) => {
|
|||||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lightenColor = (hex: string, percent: number) => {
|
// 新增:hex -> rgba 字符串
|
||||||
|
const hexToRgba = (hex: string, alpha: number) => {
|
||||||
const rgb = hexToRgb(hex)
|
const rgb = hexToRgb(hex)
|
||||||
if (!rgb) return hex
|
if (!rgb) return 'rgba(0,0,0,0)'
|
||||||
|
const a = alpha > 1 ? alpha / 100 : alpha
|
||||||
const { r, g, b } = rgb
|
return `rgba(${rgb.r},${rgb.g},${rgb.b},${clamp01(a)})`
|
||||||
const amount = Math.round(2.55 * percent)
|
|
||||||
|
|
||||||
return rgbToHex(Math.min(255, r + amount), Math.min(255, g + amount), Math.min(255, b + amount))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const darkenColor = (hex: string, percent: number) => {
|
// HSL 转换(感知更平滑)
|
||||||
|
const rgbToHsl = (r: number, g: number, b: number) => {
|
||||||
|
r /= 255; g /= 255; b /= 255
|
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
||||||
|
let h = 0, s = 0
|
||||||
|
const l = (max + min) / 2
|
||||||
|
const d = max - min
|
||||||
|
if (d !== 0) {
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||||
|
switch (max) {
|
||||||
|
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
||||||
|
case g: h = (b - r) / d + 2; break
|
||||||
|
case b: h = (r - g) / d + 4; break
|
||||||
|
}
|
||||||
|
h /= 6
|
||||||
|
}
|
||||||
|
return { h: h * 360, s, l }
|
||||||
|
}
|
||||||
|
|
||||||
|
const hslToRgb = (h: number, s: number, l: number) => {
|
||||||
|
h /= 360
|
||||||
|
if (s === 0) {
|
||||||
|
const val = Math.round(l * 255)
|
||||||
|
return { r: val, g: val, b: val }
|
||||||
|
}
|
||||||
|
const hue2rgb = (p: number, q: number, t: number) => {
|
||||||
|
if (t < 0) t += 1
|
||||||
|
if (t > 1) t -= 1
|
||||||
|
if (t < 1/6) return p + (q - p) * 6 * t
|
||||||
|
if (t < 1/2) return q
|
||||||
|
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||||
|
const p = 2 * l - q
|
||||||
|
const r = hue2rgb(p, q, h + 1/3)
|
||||||
|
const g = hue2rgb(p, q, h)
|
||||||
|
const b = hue2rgb(p, q, h - 1/3)
|
||||||
|
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const hslAdjust = (hex: string, dl: number) => {
|
||||||
const rgb = hexToRgb(hex)
|
const rgb = hexToRgb(hex)
|
||||||
if (!rgb) return hex
|
if (!rgb) return hex
|
||||||
|
const { h, s, l } = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
||||||
|
const nl = clamp01(l + dl)
|
||||||
|
const nrgb = hslToRgb(h, s, nl)
|
||||||
|
return rgbToHex(nrgb.r, nrgb.g, nrgb.b)
|
||||||
|
}
|
||||||
|
|
||||||
const { r, g, b } = rgb
|
const hslLighten = (hex: string, percent: number) => hslAdjust(hex, percent/100)
|
||||||
const amount = Math.round(2.55 * percent)
|
const hslDarken = (hex: string, percent: number) => hslAdjust(hex, -percent/100)
|
||||||
|
|
||||||
return rgbToHex(Math.max(0, r - amount), Math.max(0, g - amount), Math.max(0, b - amount))
|
// 对比度 (WCAG)
|
||||||
|
const contrastRatio = (hex1: string, hex2: string) => {
|
||||||
|
const L1 = getLuminance(hex1)
|
||||||
|
const L2 = getLuminance(hex2)
|
||||||
|
const light = Math.max(L1, L2)
|
||||||
|
const dark = Math.min(L1, L2)
|
||||||
|
return (light + 0.05) / (dark + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgbaExtractHex = (color: string) => {
|
||||||
|
// 只支持 hex(#rrggbb) 直接返回;若 rgba 则忽略 alpha 并合成背景为黑假设
|
||||||
|
if (color.startsWith('#') && (color.length === 7)) return color
|
||||||
|
// 简化:返回黑或白占位
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickAccessibleColor = (c1: string, c2: string, bg: string, minRatio = 4.5) => {
|
||||||
|
const hexBg = rgbaExtractHex(bg)
|
||||||
|
const hex1 = rgbaExtractHex(c1 === 'rgba(255,255,255,0.88)' ? '#ffffff' : (c1.includes('255,255,255') ? '#ffffff' : '#000000'))
|
||||||
|
const hex2 = rgbaExtractHex(c2 === 'rgba(255,255,255,0.88)' ? '#ffffff' : (c2.includes('255,255,255') ? '#ffffff' : '#000000'))
|
||||||
|
const r1 = contrastRatio(hex1, hexBg)
|
||||||
|
const r2 = contrastRatio(hex2, hexBg)
|
||||||
|
// 优先满足 >= minRatio;都满足取更高;否则取更高
|
||||||
|
if (r1 >= minRatio && r2 >= minRatio) return r1 >= r2 ? c1 : c2
|
||||||
|
if (r1 >= minRatio) return c1
|
||||||
|
if (r2 >= minRatio) return c2
|
||||||
|
return r1 >= r2 ? c1 : c2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 改进侧栏背景:如果深色模式,降低亮度并略增饱和;浅色模式提高亮度轻度染色
|
||||||
|
const deriveSiderBg = (primary: string, base: string, dark: boolean) => {
|
||||||
|
const mixRatio = dark ? 0.22 : 0.18
|
||||||
|
const mixed = blendColors(base, primary, mixRatio)
|
||||||
|
return dark ? hslDarken(mixed, 8) : hslLighten(mixed, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deriveSiderBorder = (siderBg: string, primary: string, dark: boolean) => {
|
||||||
|
return dark ? blendColors(siderBg, primary, 0.30) : blendColors('#d9d9d9', primary, 0.25)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听系统主题变化
|
// 监听系统主题变化
|
||||||
|
|||||||
@@ -1,216 +1,577 @@
|
|||||||
import { ref, reactive, onUnmounted } from 'vue'
|
import { ref, type Ref } from 'vue'
|
||||||
import { message, notification } from 'ant-design-vue'
|
import { message, notification } from 'ant-design-vue'
|
||||||
|
|
||||||
// WebSocket连接状态
|
// WebSocket 调试开关
|
||||||
|
const WS_DEV = true
|
||||||
|
const WS_VERSION = 'v2.5-PERSISTENT-' + Date.now()
|
||||||
|
console.log(`🚀 WebSocket 模块已加载: ${WS_VERSION} - 永久连接模式`)
|
||||||
|
|
||||||
|
// 基础配置
|
||||||
|
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
||||||
|
const HEARTBEAT_INTERVAL = 15000
|
||||||
|
const HEARTBEAT_TIMEOUT = 5000
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
|
export type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
|
||||||
|
|
||||||
// WebSocket消息类型
|
|
||||||
export type WebSocketMessageType = 'Update' | 'Message' | 'Info' | 'Signal'
|
|
||||||
|
|
||||||
// WebSocket基础消息接口
|
|
||||||
export interface WebSocketBaseMessage {
|
export interface WebSocketBaseMessage {
|
||||||
type: WebSocketMessageType
|
id?: string
|
||||||
data: any
|
type: string
|
||||||
|
data?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度消息接口
|
|
||||||
export interface ProgressMessage {
|
export interface ProgressMessage {
|
||||||
taskId: string
|
percent?: number
|
||||||
status: 'running' | 'waiting' | 'finished' | 'failed'
|
status?: string
|
||||||
progress: number
|
msg?: string
|
||||||
msg: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 结果消息接口
|
|
||||||
export interface ResultMessage {
|
export interface ResultMessage {
|
||||||
taskId: string
|
success?: boolean
|
||||||
status: 'success' | 'failed'
|
result?: any
|
||||||
result: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误消息接口
|
|
||||||
export interface ErrorMessage {
|
export interface ErrorMessage {
|
||||||
msg: string
|
msg?: string
|
||||||
code: number
|
code?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知消息接口
|
|
||||||
export interface NotifyMessage {
|
export interface NotifyMessage {
|
||||||
title: string
|
title?: string
|
||||||
content: string
|
content?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket连接配置
|
export interface WebSocketSubscriber {
|
||||||
export interface WebSocketConfig {
|
id: string
|
||||||
taskId: string
|
|
||||||
onProgress?: (data: ProgressMessage) => void
|
onProgress?: (data: ProgressMessage) => void
|
||||||
onResult?: (data: ResultMessage) => void
|
onResult?: (data: ResultMessage) => void
|
||||||
onError?: (error: ErrorMessage) => void
|
onError?: (err: ErrorMessage) => void
|
||||||
onNotify?: (notify: NotifyMessage) => void
|
onNotify?: (n: NotifyMessage) => void
|
||||||
|
// 兼容旧版 API
|
||||||
|
onMessage?: (raw: WebSocketBaseMessage) => void
|
||||||
onStatusChange?: (status: WebSocketStatus) => void
|
onStatusChange?: (status: WebSocketStatus) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧版 connect(config) 接口
|
||||||
|
export interface WebSocketConfig {
|
||||||
|
taskId: string
|
||||||
|
mode?: string
|
||||||
showNotifications?: boolean
|
showNotifications?: boolean
|
||||||
|
onProgress?: (data: ProgressMessage) => void
|
||||||
|
onResult?: (data: ResultMessage) => void
|
||||||
|
onError?: (err: ErrorMessage | string) => void
|
||||||
|
onNotify?: (n: NotifyMessage) => void
|
||||||
|
onMessage?: (raw: WebSocketBaseMessage) => void
|
||||||
|
onStatusChange?: (status: WebSocketStatus) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWebSocket() {
|
// 日志工具
|
||||||
const connections = ref<Map<string, WebSocket>>(new Map())
|
const wsLog = (message: string, ...args: any[]) => {
|
||||||
const statuses = ref<Map<string, WebSocketStatus>>(new Map())
|
if (!WS_DEV) return
|
||||||
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0]
|
||||||
|
console.log(`[WS ${timestamp}] ${message}`, ...args)
|
||||||
// 心跳检测
|
|
||||||
const heartbeat = (ws: WebSocket) => {
|
|
||||||
const pingMessage = {
|
|
||||||
type: 'Ping',
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
ws.send(JSON.stringify(pingMessage))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建立WebSocket连接
|
const wsWarn = (message: string, ...args: any[]) => {
|
||||||
const connect = async (config: WebSocketConfig): Promise<string | null> => {
|
if (!WS_DEV) return
|
||||||
try {
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0]
|
||||||
const ws = new WebSocket(BASE_WS_URL)
|
console.warn(`[WS ${timestamp}] ${message}`, ...args)
|
||||||
const taskId = config.taskId
|
}
|
||||||
|
|
||||||
ws.onopen = () => {
|
const wsError = (message: string, ...args: any[]) => {
|
||||||
statuses.value.set(taskId, '已连接')
|
if (!WS_DEV) return
|
||||||
config.onStatusChange?.('已连接')
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0]
|
||||||
|
console.error(`[WS ${timestamp}] ${message}`, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局存储接口 - 移除销毁相关字段
|
||||||
|
interface GlobalWSStorage {
|
||||||
|
wsRef: WebSocket | null
|
||||||
|
status: Ref<WebSocketStatus>
|
||||||
|
subscribers: Ref<Map<string, WebSocketSubscriber>>
|
||||||
|
heartbeatTimer?: number
|
||||||
|
isConnecting: boolean
|
||||||
|
lastPingTime: number
|
||||||
|
connectionId: string
|
||||||
|
moduleLoadCount: number
|
||||||
|
createdAt: number
|
||||||
|
hasEverConnected: boolean
|
||||||
|
reconnectAttempts: number // 新增:重连尝试次数
|
||||||
|
}
|
||||||
|
|
||||||
|
const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')
|
||||||
|
|
||||||
|
// 初始化全局存储
|
||||||
|
const initGlobalStorage = (): GlobalWSStorage => {
|
||||||
|
return {
|
||||||
|
wsRef: null,
|
||||||
|
status: ref<WebSocketStatus>('已断开'),
|
||||||
|
subscribers: ref(new Map<string, WebSocketSubscriber>()),
|
||||||
|
heartbeatTimer: undefined,
|
||||||
|
isConnecting: false,
|
||||||
|
lastPingTime: 0,
|
||||||
|
connectionId: Math.random().toString(36).substr(2, 9),
|
||||||
|
moduleLoadCount: 0,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
hasEverConnected: false,
|
||||||
|
reconnectAttempts: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取全局存储
|
||||||
|
const getGlobalStorage = (): GlobalWSStorage => {
|
||||||
|
if (!(window as any)[WS_STORAGE_KEY]) {
|
||||||
|
wsLog('首次初始化全局 WebSocket 存储 - 永久连接模式')
|
||||||
|
;(window as any)[WS_STORAGE_KEY] = initGlobalStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = (window as any)[WS_STORAGE_KEY] as GlobalWSStorage
|
||||||
|
storage.moduleLoadCount++
|
||||||
|
|
||||||
|
const uptime = ((Date.now() - storage.createdAt) / 1000).toFixed(1)
|
||||||
|
wsLog(`模块加载第${storage.moduleLoadCount}次,存储运行时间: ${uptime}s,连接状态: ${storage.status.value}`)
|
||||||
|
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全局状态
|
||||||
|
const setGlobalStatus = (status: WebSocketStatus) => {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
const oldStatus = global.status.value
|
||||||
|
global.status.value = status
|
||||||
|
wsLog(`状态变更: ${oldStatus} -> ${status} [连接ID: ${global.connectionId}]`)
|
||||||
|
|
||||||
|
// 广播状态变化给所有订阅者(兼容 onStatusChange)
|
||||||
|
global.subscribers.value.forEach(sub => {
|
||||||
|
sub.onStatusChange?.(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止心跳
|
||||||
|
const stopGlobalHeartbeat = () => {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
if (global.heartbeatTimer) {
|
||||||
|
clearInterval(global.heartbeatTimer)
|
||||||
|
global.heartbeatTimer = undefined
|
||||||
|
wsLog('心跳检测已停止')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 启动心跳
|
// 启动心跳
|
||||||
const heartbeatInterval = setInterval(() => {
|
const startGlobalHeartbeat = (ws: WebSocket) => {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
stopGlobalHeartbeat()
|
||||||
|
|
||||||
|
wsLog('启动心跳检测,间隔15秒')
|
||||||
|
global.heartbeatTimer = window.setInterval(() => {
|
||||||
|
wsLog(`心跳检测 - WebSocket状态: ${ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`)
|
||||||
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
heartbeat(ws)
|
|
||||||
}
|
|
||||||
}, 30000)
|
|
||||||
|
|
||||||
// 清理定时器
|
|
||||||
ws.addEventListener('close', () => {
|
|
||||||
clearInterval(heartbeatInterval)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data) as WebSocketBaseMessage
|
const pingTime = Date.now()
|
||||||
|
global.lastPingTime = pingTime
|
||||||
|
const pingData = { Ping: pingTime, connectionId: global.connectionId }
|
||||||
|
|
||||||
switch (message.type) {
|
wsLog('发送心跳ping', pingData)
|
||||||
case 'Signal':
|
ws.send(JSON.stringify({
|
||||||
// 心跳信<E8B7B3><E4BFA1>,无需特殊处理
|
type: 'Signal',
|
||||||
break
|
data: pingData
|
||||||
case 'Progress':
|
}))
|
||||||
config.onProgress?.(message.data as ProgressMessage)
|
|
||||||
break
|
// 心跳超时检测 - 但不主动断开连接
|
||||||
case 'Result':
|
setTimeout(() => {
|
||||||
config.onResult?.(message.data as ResultMessage)
|
if (global.lastPingTime === pingTime && ws.readyState === WebSocket.OPEN) {
|
||||||
break
|
wsWarn(`心跳超时 - 发送时间: ${pingTime}, 当前lastPingTime: ${global.lastPingTime}, 连接状态: ${ws.readyState}`)
|
||||||
case 'Error':
|
wsWarn('心跳超时但保持连接,等待网络层或服务端处理')
|
||||||
const errorData = message.data as ErrorMessage
|
|
||||||
config.onError?.(errorData)
|
|
||||||
if (config.showNotifications) {
|
|
||||||
message.error(errorData.msg)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'Notify':
|
|
||||||
const notifyData = message.data as NotifyMessage
|
|
||||||
config.onNotify?.(notifyData)
|
|
||||||
if (config.showNotifications) {
|
|
||||||
notification.info({
|
|
||||||
message: notifyData.title,
|
|
||||||
description: notifyData.content
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
}, HEARTBEAT_TIMEOUT)
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('WebSocket消息解析错误:', e)
|
wsError('心跳发送失败', e)
|
||||||
|
if (ws.readyState !== WebSocket.OPEN) {
|
||||||
|
wsWarn('心跳发送失败,当前连接已不再是 OPEN 状态')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wsWarn(`心跳检测时连接状态异常: ${ws.readyState},但不主动断开连接`)
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息
|
||||||
|
const handleMessage = (raw: WebSocketBaseMessage) => {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
const msgType = String(raw.type)
|
||||||
|
const id = raw.id
|
||||||
|
|
||||||
|
// 处理心跳响应
|
||||||
|
if (msgType === 'Signal' && raw.data && raw.data.Pong) {
|
||||||
|
const pongTime = raw.data.Pong
|
||||||
|
const latency = Date.now() - pongTime
|
||||||
|
wsLog(`收到心跳pong响应,延迟: ${latency}ms`)
|
||||||
|
global.lastPingTime = 0 // 重置ping时间,表示收到了响应
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录其他类型的消息
|
||||||
|
if (msgType !== 'Signal') {
|
||||||
|
wsLog(`收到消息: type=${msgType}, id=${id || 'broadcast'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = (sub: WebSocketSubscriber) => {
|
||||||
|
if (msgType === 'Signal') return
|
||||||
|
|
||||||
|
// 兼容旧版:先调用通用 onMessage 回调
|
||||||
|
sub.onMessage?.(raw)
|
||||||
|
|
||||||
|
if (msgType === 'Progress') return sub.onProgress?.(raw.data as ProgressMessage)
|
||||||
|
if (msgType === 'Result') return sub.onResult?.(raw.data as ResultMessage)
|
||||||
|
if (msgType === 'Error') {
|
||||||
|
sub.onError?.(raw.data as ErrorMessage)
|
||||||
|
if (!sub.onError && raw.data && (raw.data as ErrorMessage).msg) {
|
||||||
|
message.error((raw.data as ErrorMessage).msg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (msgType === 'Notify') {
|
||||||
|
sub.onNotify?.(raw.data as NotifyMessage)
|
||||||
|
if (raw.data && (raw.data as NotifyMessage).title) {
|
||||||
|
notification.info({
|
||||||
|
message: (raw.data as NotifyMessage).title,
|
||||||
|
description: (raw.data as NotifyMessage).content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 其他类型可扩展
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
const sub = global.subscribers.value.get(id)
|
||||||
|
if (sub) {
|
||||||
|
dispatch(sub)
|
||||||
|
} else {
|
||||||
|
wsWarn(`未找到 ws_id=${id} 的订阅者, type=${msgType}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无 id 的消息广播给所有订阅者
|
||||||
|
global.subscribers.value.forEach((sub: WebSocketSubscriber) => dispatch(sub))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
// 延迟重连函数
|
||||||
statuses.value.set(taskId, '连接错误')
|
const scheduleReconnect = (global: GlobalWSStorage) => {
|
||||||
config.onStatusChange?.('连接错误')
|
const delay = Math.min(1000 * Math.pow(2, global.reconnectAttempts), 30000) // 最大30秒
|
||||||
config.onError?.({ msg: 'WebSocket连接错误', code: 500 })
|
wsLog(`计划在 ${delay}ms 后重连 (第${global.reconnectAttempts + 1}次尝试)`)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
global.reconnectAttempts++
|
||||||
|
createGlobalWebSocket()
|
||||||
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
// 创建 WebSocket 连接 - 移除销毁检查,确保永不放弃连接
|
||||||
statuses.value.set(taskId, '已断开')
|
const createGlobalWebSocket = (): WebSocket => {
|
||||||
config.onStatusChange?.('已断开')
|
const global = getGlobalStorage()
|
||||||
connections.value.delete(taskId)
|
|
||||||
|
// 检查现有连接状态
|
||||||
|
if (global.wsRef) {
|
||||||
|
wsLog(`检查现有连接状态: ${global.wsRef.readyState}`)
|
||||||
|
|
||||||
|
if (global.wsRef.readyState === WebSocket.OPEN) {
|
||||||
|
wsLog('检测到已有活跃连接,直接返回现有连接')
|
||||||
|
return global.wsRef
|
||||||
}
|
}
|
||||||
|
|
||||||
connections.value.set(taskId, ws)
|
if (global.wsRef.readyState === WebSocket.CONNECTING) {
|
||||||
statuses.value.set(taskId, '连接中')
|
wsLog('检测到正在连接的 WebSocket,返回现有连接实例')
|
||||||
config.onStatusChange?.('连接中')
|
return global.wsRef
|
||||||
|
|
||||||
return taskId
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = error instanceof Error ? error.message : '连接失败'
|
|
||||||
if (config.onError) {
|
|
||||||
config.onError({ msg: errorMsg, code: 500 })
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
|
wsLog('现有连接状态为 CLOSING 或 CLOSED,将创建新连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
wsLog(`开始创建新的 WebSocket 连接到: ${BASE_WS_URL}`)
|
||||||
|
const ws = new WebSocket(BASE_WS_URL)
|
||||||
|
|
||||||
|
// 记录连接创建
|
||||||
|
wsLog(`WebSocket 实例已创建 [连接ID: ${global.connectionId}]`)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
wsLog(`WebSocket 连接已建立 [连接ID: ${global.connectionId}]`)
|
||||||
|
global.isConnecting = false
|
||||||
|
global.hasEverConnected = true
|
||||||
|
global.reconnectAttempts = 0 // 重置重连计数
|
||||||
|
setGlobalStatus('已连接')
|
||||||
|
startGlobalHeartbeat(ws)
|
||||||
|
|
||||||
|
// 发送连接确认
|
||||||
|
try {
|
||||||
|
const connectData = { Connect: true, connectionId: global.connectionId }
|
||||||
|
wsLog('发送连接确认信号', connectData)
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'Signal',
|
||||||
|
data: connectData
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
wsError('发送连接确认失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送任务开始指令
|
ws.onmessage = (ev) => {
|
||||||
const startTask = (taskId: string, params: any) => {
|
try {
|
||||||
const ws = connections.value.get(taskId)
|
const raw = JSON.parse(ev.data) as WebSocketBaseMessage
|
||||||
|
handleMessage(raw)
|
||||||
|
} catch (e) {
|
||||||
|
wsError('解析 WebSocket 消息失败', e, '原始数据:', ev.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (event) => {
|
||||||
|
wsError(`WebSocket 连接错误 [连接ID: ${global.connectionId}]`, event)
|
||||||
|
wsError(`错误发生时连接状态: ${ws.readyState}`)
|
||||||
|
setGlobalStatus('连接错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
wsLog(`WebSocket 连接已关闭 [连接ID: ${global.connectionId}]`)
|
||||||
|
wsLog(`关闭码: ${event.code}, 关闭原因: "${event.reason}", 是否干净关闭: ${event.wasClean}`)
|
||||||
|
|
||||||
|
// 详细分析关闭原因
|
||||||
|
const closeReasons: { [key: number]: string } = {
|
||||||
|
1000: '正常关闭',
|
||||||
|
1001: '终端离开(如页面关闭)',
|
||||||
|
1002: '协议错误',
|
||||||
|
1003: '不支持的数据类型',
|
||||||
|
1005: '未收到状态码',
|
||||||
|
1006: '连接异常关闭',
|
||||||
|
1007: '数据格式错误',
|
||||||
|
1008: '策略违规',
|
||||||
|
1009: '消息过大',
|
||||||
|
1010: '扩展协商失败',
|
||||||
|
1011: '服务器意外错误',
|
||||||
|
1015: 'TLS握手失败'
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasonDesc = closeReasons[event.code] || '未知原因'
|
||||||
|
wsLog(`关闭详情: ${reasonDesc}`)
|
||||||
|
|
||||||
|
setGlobalStatus('已断开')
|
||||||
|
stopGlobalHeartbeat()
|
||||||
|
global.isConnecting = false
|
||||||
|
|
||||||
|
// 永不放弃:立即安排重连
|
||||||
|
wsLog('连接断开,安排自动重连以保持永久连接')
|
||||||
|
scheduleReconnect(global)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为新创建的 WebSocket 设置引用
|
||||||
|
global.wsRef = ws
|
||||||
|
wsLog(`WebSocket 引用已设置到全局存储`)
|
||||||
|
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接全局 WebSocket - 简化逻辑,移除销毁检查
|
||||||
|
const connectGlobalWebSocket = async (): Promise<boolean> => {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
|
||||||
|
// 详细检查连接状态
|
||||||
|
if (global.wsRef) {
|
||||||
|
wsLog(`检查现有连接: readyState=${global.wsRef.readyState}, isConnecting=${global.isConnecting}`)
|
||||||
|
|
||||||
|
if (global.wsRef.readyState === WebSocket.OPEN) {
|
||||||
|
wsLog('WebSocket 已连接,直接返回')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.wsRef.readyState === WebSocket.CONNECTING) {
|
||||||
|
wsLog('WebSocket 正在连接中')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.isConnecting) {
|
||||||
|
wsLog('全局连接标志显示正在连接中,等待连接完成')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
wsLog('开始建立 WebSocket 连接流程')
|
||||||
|
global.isConnecting = true
|
||||||
|
global.wsRef = createGlobalWebSocket()
|
||||||
|
setGlobalStatus('连接中')
|
||||||
|
wsLog('WebSocket 连接流程已启动')
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
wsError('创建 WebSocket 失败', e)
|
||||||
|
setGlobalStatus('连接错误')
|
||||||
|
global.isConnecting = false
|
||||||
|
|
||||||
|
// 即使创建失败也要安排重连
|
||||||
|
scheduleReconnect(global)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块初始化逻辑
|
||||||
|
wsLog('=== WebSocket 模块开始初始化 - 永久连接模式 ===')
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
|
||||||
|
if (global.moduleLoadCount > 1) {
|
||||||
|
wsLog(`检测到模块热更新重载 (第${global.moduleLoadCount}次)`)
|
||||||
|
wsLog(`当前连接状态: ${global.wsRef ? global.wsRef.readyState : 'null'}`)
|
||||||
|
wsLog('保持现有连接,不重新建立连接')
|
||||||
|
} else {
|
||||||
|
wsLog('首次加载模块,建立永久 WebSocket 连接')
|
||||||
|
connectGlobalWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面卸载时不关闭连接,保持永久连接
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
wsLog('页面即将卸载,但保持 WebSocket 连接')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主要 Hook 函数
|
||||||
|
export function useWebSocket() {
|
||||||
|
const global = getGlobalStorage()
|
||||||
|
|
||||||
|
const subscribe = (id: string, handlers: Omit<WebSocketSubscriber, 'id'>) => {
|
||||||
|
global.subscribers.value.set(id, { id, ...handlers })
|
||||||
|
wsLog(`添加订阅者: ${id},当前订阅者总数: ${global.subscribers.value.size}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = (id: string) => {
|
||||||
|
const existed = global.subscribers.value.delete(id)
|
||||||
|
wsLog(`移除订阅者: ${id},是否存在: ${existed},剩余订阅者: ${global.subscribers.value.size}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendRaw = (type: string, data?: any, id?: string) => {
|
||||||
|
const ws = global.wsRef
|
||||||
|
wsLog(`尝试发送消息: type=${type}, id=${id || 'broadcast'}`)
|
||||||
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
const message = {
|
try {
|
||||||
type: 'StartTask',
|
const messageData = { id, type, data }
|
||||||
data: {
|
ws.send(JSON.stringify(messageData))
|
||||||
taskId,
|
wsLog('消息发送成功')
|
||||||
params
|
} catch (e) {
|
||||||
|
wsError('发送消息失败', e)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
ws.send(JSON.stringify(message))
|
wsWarn(`WebSocket 未准备就绪: ${ws ? `状态=${ws.readyState}` : '连接为null'}`)
|
||||||
|
wsWarn('消息将在连接恢复后可用')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新配置
|
const startTaskRaw = (params: any) => {
|
||||||
const updateConfig = (configKey: string, value: any) => {
|
wsLog('发送启动任务请求', params)
|
||||||
// 发送给所<E7BB99><E68980><EFBFBD>活跃连接
|
sendRaw('StartTask', params)
|
||||||
connections.value.forEach((ws) => {
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
const message = {
|
|
||||||
type: 'UpdateConfig',
|
|
||||||
data: {
|
|
||||||
configKey,
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除 destroy 功能,确保连接永不断开
|
||||||
|
const forceReconnect = () => {
|
||||||
|
wsLog('手动触发重连')
|
||||||
|
if (global.wsRef) {
|
||||||
|
// 不关闭现有连接,直接尝试创建新连接
|
||||||
|
global.isConnecting = false
|
||||||
|
connectGlobalWebSocket()
|
||||||
}
|
}
|
||||||
ws.send(JSON.stringify(message))
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getConnectionInfo = () => {
|
||||||
|
const info = {
|
||||||
|
connectionId: global.connectionId,
|
||||||
|
status: global.status.value,
|
||||||
|
subscriberCount: global.subscribers.value.size,
|
||||||
|
moduleLoadCount: global.moduleLoadCount,
|
||||||
|
wsReadyState: global.wsRef ? global.wsRef.readyState : null,
|
||||||
|
isConnecting: global.isConnecting,
|
||||||
|
hasHeartbeat: !!global.heartbeatTimer,
|
||||||
|
hasEverConnected: global.hasEverConnected,
|
||||||
|
reconnectAttempts: global.reconnectAttempts,
|
||||||
|
wsDevEnabled: WS_DEV,
|
||||||
|
isPersistentMode: true // 标识为永久连接模式
|
||||||
|
}
|
||||||
|
wsLog('连接信息查询', info)
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧版 API:connect 重载
|
||||||
|
async function connect(): Promise<boolean>
|
||||||
|
async function connect(config: WebSocketConfig): Promise<string | null>
|
||||||
|
async function connect(config?: WebSocketConfig): Promise<boolean | string | null> {
|
||||||
|
if (!config) {
|
||||||
|
// 无参数调用:返回连接状态
|
||||||
|
return connectGlobalWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有参数调用:建立订阅,复用现有连接
|
||||||
|
const ok = await connectGlobalWebSocket()
|
||||||
|
if (!ok) {
|
||||||
|
// 即使连接失败也要建立订阅,等待连接恢复
|
||||||
|
wsLog('连接暂时不可用,但仍建立订阅等待连接恢复')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先移除旧订阅避免重复
|
||||||
|
if (global.subscribers.value.has(config.taskId)) {
|
||||||
|
unsubscribe(config.taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(config.taskId, {
|
||||||
|
onProgress: config.onProgress,
|
||||||
|
onResult: config.onResult,
|
||||||
|
onError: (e) => {
|
||||||
|
if (typeof config.onError === 'function') config.onError(e)
|
||||||
|
},
|
||||||
|
onNotify: (n) => {
|
||||||
|
config.onNotify?.(n)
|
||||||
|
if (config.showNotifications && n?.title) {
|
||||||
|
notification.info({ message: n.title, description: n.content })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMessage: config.onMessage,
|
||||||
|
onStatusChange: config.onStatusChange
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 立即推送当前状态
|
||||||
|
config.onStatusChange?.(global.status.value)
|
||||||
|
|
||||||
|
// 可根据 mode 发送一个初始信号(可选)
|
||||||
|
if (config.mode) {
|
||||||
|
sendRaw('Mode', { mode: config.mode }, config.taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭连接
|
return config.taskId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧版 API:disconnect / disconnectAll - 只取消订阅,不断开连接
|
||||||
const disconnect = (taskId: string) => {
|
const disconnect = (taskId: string) => {
|
||||||
const ws = connections.value.get(taskId)
|
if (!taskId) return
|
||||||
if (ws) {
|
unsubscribe(taskId)
|
||||||
ws.close()
|
wsLog(`兼容模式取消订阅: ${taskId}`)
|
||||||
connections.value.delete(taskId)
|
|
||||||
statuses.value.delete(taskId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所有连接
|
|
||||||
const disconnectAll = () => {
|
const disconnectAll = () => {
|
||||||
connections.value.forEach((ws, taskId) => {
|
const ids = Array.from(global.subscribers.value.keys())
|
||||||
disconnect(taskId)
|
ids.forEach((id: string) => unsubscribe(id))
|
||||||
})
|
wsLog('已取消所有订阅 (disconnectAll)')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时清理所有连接
|
|
||||||
onUnmounted(() => {
|
|
||||||
disconnectAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// 兼容 API
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
disconnectAll,
|
disconnectAll,
|
||||||
startTask,
|
// 原有 API & 工具
|
||||||
updateConfig,
|
subscribe,
|
||||||
statuses
|
unsubscribe,
|
||||||
|
sendRaw,
|
||||||
|
startTaskRaw,
|
||||||
|
forceReconnect,
|
||||||
|
getConnectionInfo,
|
||||||
|
status: global.status,
|
||||||
|
subscribers: global.subscribers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,18 +34,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('../views/ScriptEdit.vue'),
|
component: () => import('../views/ScriptEdit.vue'),
|
||||||
meta: { title: '编辑脚本' },
|
meta: { title: '编辑脚本' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/scripts/:scriptId/users/add',
|
|
||||||
name: 'UserAdd',
|
|
||||||
component: () => import('../views/UserEdit.vue'),
|
|
||||||
meta: { title: '添加用户' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/scripts/:scriptId/users/:userId/edit',
|
|
||||||
name: 'UserEdit',
|
|
||||||
component: () => import('../views/UserEdit.vue'),
|
|
||||||
meta: { title: '编辑用户' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/scripts/:scriptId/users/add/maa',
|
path: '/scripts/:scriptId/users/add/maa',
|
||||||
name: 'MAAUserAdd',
|
name: 'MAAUserAdd',
|
||||||
|
|||||||
4
frontend/src/types/electron.d.ts
vendored
4
frontend/src/types/electron.d.ts
vendored
@@ -40,6 +40,10 @@ export interface ElectronAPI {
|
|||||||
saveLogsToFile: (logs: string) => Promise<void>
|
saveLogsToFile: (logs: string) => Promise<void>
|
||||||
loadLogsFromFile: () => Promise<string | null>
|
loadLogsFromFile: () => Promise<string | null>
|
||||||
|
|
||||||
|
// 文件系统操作
|
||||||
|
openFile: (filePath: string) => Promise<void>
|
||||||
|
showItemInFolder: (filePath: string) => Promise<void>
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||||
removeDownloadProgressListener: () => void
|
removeDownloadProgressListener: () => void
|
||||||
|
|||||||
@@ -51,8 +51,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-edit-content">
|
<div class="user-edit-content">
|
||||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="user-form">
|
<a-card class="config-card">
|
||||||
<a-card title="基本信息" class="form-card">
|
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="config-form">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>基本信息</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item name="userName" required>
|
<a-form-item name="userName" required>
|
||||||
@@ -69,6 +74,7 @@
|
|||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -128,11 +134,16 @@
|
|||||||
placeholder="请输入备注信息"
|
placeholder="请输入备注信息"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="额外脚本" class="form-card">
|
<!-- 额外脚本 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>额外脚本</h3>
|
||||||
|
</div>
|
||||||
<a-form-item name="scriptBeforeTask">
|
<a-form-item name="scriptBeforeTask">
|
||||||
<template #label>
|
<template #label>
|
||||||
<a-tooltip title="在任务执行前运行自定义脚本">
|
<a-tooltip title="在任务执行前运行自定义脚本">
|
||||||
@@ -217,9 +228,13 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="通知配置" class="form-card">
|
<!-- 通知配置 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>通知配置</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24" align="middle">
|
<a-row :gutter="24" align="middle">
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<span style="font-weight: 500">启用通知</span>
|
<span style="font-weight: 500">启用通知</span>
|
||||||
@@ -302,11 +317,13 @@
|
|||||||
"
|
"
|
||||||
size="large"
|
size="large"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-float-button
|
<a-float-button
|
||||||
@@ -697,6 +714,89 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card :deep(.ant-card-body) {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid var(--ant-color-border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单标签 */
|
||||||
|
.form-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon {
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: help;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon:hover {
|
||||||
|
color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--ant-color-border);
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input:hover {
|
||||||
|
border-color: var(--ant-color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input:focus,
|
||||||
|
.modern-input.ant-input-focused {
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
|
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<a-select v-model:value="searchForm.mode" style="width: 100%">
|
<a-select v-model:value="searchForm.mode" style="width: 100%">
|
||||||
<a-select-option value="按日合并">按日合并</a-select-option>
|
<a-select-option value="按日合并">按日合并</a-select-option>
|
||||||
<a-select-option value="按周合并">按周合并</a-select-option>
|
<a-select-option value="按周合并">按周合并</a-select-option>
|
||||||
<a-select-option value="按年月并">按月合并</a-select-option>
|
<a-select-option value="按月合并">按月合并</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -90,32 +90,6 @@
|
|||||||
<div v-else class="history-layout">
|
<div v-else class="history-layout">
|
||||||
<!-- 左侧日期列表 -->
|
<!-- 左侧日期列表 -->
|
||||||
<div class="date-sidebar">
|
<div class="date-sidebar">
|
||||||
<!-- <!– 数据总览 –>-->
|
|
||||||
<!-- <div class="overview-section">-->
|
|
||||||
<!-- <a-card size="small" title="数据总览" class="overview-card">-->
|
|
||||||
<!-- <div class="overview-stats">-->
|
|
||||||
<!-- <a-statistic-->
|
|
||||||
<!-- title="总公招数"-->
|
|
||||||
<!-- :value="totalOverview.totalRecruit"-->
|
|
||||||
<!-- :value-style="{ color: '#1890ff', fontSize: '18px' }"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <template #prefix>-->
|
|
||||||
<!-- <UserOutlined />-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </a-statistic>-->
|
|
||||||
<!-- <a-statistic-->
|
|
||||||
<!-- title="总掉落数"-->
|
|
||||||
<!-- :value="totalOverview.totalDrop"-->
|
|
||||||
<!-- :value-style="{ color: '#52c41a', fontSize: '18px' }"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <template #prefix>-->
|
|
||||||
<!-- <GiftOutlined />-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </a-statistic>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </a-card>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!-- 日期折叠列表 -->
|
<!-- 日期折叠列表 -->
|
||||||
<div class="date-list">
|
<div class="date-list">
|
||||||
<a-collapse v-model:activeKey="activeKeys" ghost>
|
<a-collapse v-model:activeKey="activeKeys" ghost>
|
||||||
@@ -187,7 +161,21 @@
|
|||||||
<div class="record-info">
|
<div class="record-info">
|
||||||
<div class="record-header">
|
<div class="record-header">
|
||||||
<span class="record-time">{{ record.date }}</span>
|
<span class="record-time">{{ record.date }}</span>
|
||||||
|
<a-tooltip
|
||||||
|
v-if="record.status === '异常' && selectedUserData?.error_info && selectedUserData.error_info[record.date]"
|
||||||
|
:title="selectedUserData.error_info[record.date]"
|
||||||
|
placement="topLeft"
|
||||||
|
>
|
||||||
<a-tag
|
<a-tag
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
class="error-tag-with-tooltip"
|
||||||
|
>
|
||||||
|
{{ record.status }}
|
||||||
|
</a-tag>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tag
|
||||||
|
v-else
|
||||||
:color="record.status === '完成' ? 'success' : 'error'"
|
:color="record.status === '完成' ? 'success' : 'error'"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -287,7 +275,30 @@
|
|||||||
<a-card size="small" title="详细日志" class="log-card">
|
<a-card size="small" title="详细日志" class="log-card">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-space>
|
<a-space>
|
||||||
<FileTextOutlined />
|
<a-tooltip title="打开日志文件">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
:disabled="!currentJsonFile"
|
||||||
|
@click="handleOpenLogFile"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FileOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="打开日志文件所在目录">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
:disabled="!currentJsonFile"
|
||||||
|
@click="handleOpenLogDirectory"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FolderOpenOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<a-spin :spinning="detailLoading">
|
<a-spin :spinning="detailLoading">
|
||||||
@@ -316,13 +327,14 @@ import {
|
|||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
GiftOutlined,
|
GiftOutlined,
|
||||||
ExclamationCircleOutlined,
|
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
FileOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { Service } from '@/api/services/Service'
|
import { Service } from '@/api/services/Service'
|
||||||
import type { HistorySearchIn, HistoryData, HistoryDataGetIn } from '@/api/models'
|
import type { HistorySearchIn, HistoryData } from '@/api'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -565,6 +577,70 @@ const loadUserLog = async (jsonFile: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开日志文件
|
||||||
|
const handleOpenLogFile = async () => {
|
||||||
|
if (!currentJsonFile.value) {
|
||||||
|
message.warning('请先选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将 .json 扩展名替换为 .log
|
||||||
|
const logFilePath = currentJsonFile.value.replace(/\.json$/, '.log')
|
||||||
|
|
||||||
|
console.log('尝试打开日志文件:', logFilePath)
|
||||||
|
console.log('electronAPI 可用性:', !!window.electronAPI)
|
||||||
|
console.log('openFile 方法可用性:', !!(window.electronAPI && (window.electronAPI as any).openFile))
|
||||||
|
|
||||||
|
// 调用系统API打开文件
|
||||||
|
if (window.electronAPI && (window.electronAPI as any).openFile) {
|
||||||
|
await (window.electronAPI as any).openFile(logFilePath)
|
||||||
|
message.success('日志文件已打开')
|
||||||
|
} else {
|
||||||
|
const errorMsg = !window.electronAPI
|
||||||
|
? '当前环境不支持打开文件功能(electronAPI 不可用)'
|
||||||
|
: '当前环境不支持打开文件功能(openFile 方法不可用)'
|
||||||
|
console.error(errorMsg)
|
||||||
|
message.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开日志文件失败:', error)
|
||||||
|
message.error(`打开日志文件失败: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开日志文件所在目录
|
||||||
|
const handleOpenLogDirectory = async () => {
|
||||||
|
if (!currentJsonFile.value) {
|
||||||
|
message.warning('请先选择一条记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将 .json 扩展名替换为 .log
|
||||||
|
const logFilePath = currentJsonFile.value.replace(/\.json$/, '.log')
|
||||||
|
|
||||||
|
console.log('尝试打开日志文件目录:', logFilePath)
|
||||||
|
console.log('electronAPI 可用性:', !!window.electronAPI)
|
||||||
|
console.log('showItemInFolder 方法可用性:', !!(window.electronAPI && (window.electronAPI as any).showItemInFolder))
|
||||||
|
|
||||||
|
// 调用系统API打开目录并选中文件
|
||||||
|
if (window.electronAPI && (window.electronAPI as any).showItemInFolder) {
|
||||||
|
await (window.electronAPI as any).showItemInFolder(logFilePath)
|
||||||
|
message.success('日志文件目录已打开')
|
||||||
|
} else {
|
||||||
|
const errorMsg = !window.electronAPI
|
||||||
|
? '当前环境不支持打开目录功能(electronAPI 不可用)'
|
||||||
|
: '当前环境不支持打开目录功能(showItemInFolder 方法不可用)'
|
||||||
|
console.error(errorMsg)
|
||||||
|
message.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开日志文件目录失败:', error)
|
||||||
|
message.error(`打开日志文件目录失败: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取日期状态颜色
|
// 获取日期状态颜色
|
||||||
const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||||
const hasError = Object.values(users).some(
|
const hasError = Object.values(users).some(
|
||||||
@@ -612,7 +688,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
|||||||
|
|
||||||
/* 左侧日期栏 */
|
/* 左侧日期栏 */
|
||||||
.date-sidebar {
|
.date-sidebar {
|
||||||
width: 320px;
|
width: 200px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -947,6 +1023,16 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 带tooltip的错误tag样式 */
|
||||||
|
.error-tag-with-tooltip {
|
||||||
|
cursor: help;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tag-with-tooltip:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
/* 统计数据标题样式 */
|
/* 统计数据标题样式 */
|
||||||
.stat-subtitle {
|
.stat-subtitle {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -52,8 +52,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-edit-content">
|
<div class="user-edit-content">
|
||||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="user-form">
|
<a-card class="config-card">
|
||||||
<a-card title="基本信息" class="form-card">
|
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="config-form">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>基本信息</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item name="userName" required>
|
<a-form-item name="userName" required>
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -280,11 +286,16 @@
|
|||||||
placeholder="请输入备注信息"
|
placeholder="请输入备注信息"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="关卡配置" class="form-card">
|
<!-- 关卡配置 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>关卡配置</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item name="mode">
|
<a-form-item name="mode">
|
||||||
@@ -395,7 +406,28 @@
|
|||||||
v-model:value="formData.Info.Stage"
|
v-model:value="formData.Info.Stage"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
placeholder="选择或输入自定义关卡"
|
||||||
>
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<a-space style="padding: 4px 8px" size="small">
|
||||||
|
<a-input
|
||||||
|
ref="stageInputRef"
|
||||||
|
v-model:value="customStageName"
|
||||||
|
placeholder="输入自定义关卡,如: 11-8"
|
||||||
|
style="flex: 1"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="addCustomStage"
|
||||||
|
/>
|
||||||
|
<a-button type="text" size="small" @click="addCustomStage">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
添加关卡
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</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>
|
||||||
@@ -405,6 +437,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
<a-tag v-if="option.isCustom" color="blue" size="small" style="margin-left: 8px;">
|
||||||
|
自定义
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -428,7 +463,28 @@
|
|||||||
v-model:value="formData.Info.Stage_1"
|
v-model:value="formData.Info.Stage_1"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
placeholder="选择或输入自定义关卡"
|
||||||
>
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<a-space style="padding: 4px 8px" size="small">
|
||||||
|
<a-input
|
||||||
|
ref="stage1InputRef"
|
||||||
|
v-model:value="customStage1Name"
|
||||||
|
placeholder="输入自定义关卡,如: 11-8"
|
||||||
|
style="flex: 1"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="addCustomStage1"
|
||||||
|
/>
|
||||||
|
<a-button type="text" size="small" @click="addCustomStage1">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
添加关卡
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</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>
|
||||||
@@ -438,6 +494,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
<a-tag v-if="option.isCustom" color="blue" size="small" style="margin-left: 8px;">
|
||||||
|
自定义
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -459,7 +518,28 @@
|
|||||||
v-model:value="formData.Info.Stage_2"
|
v-model:value="formData.Info.Stage_2"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
placeholder="选择或输入自定义关卡"
|
||||||
>
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<a-space style="padding: 4px 8px" size="small">
|
||||||
|
<a-input
|
||||||
|
ref="stage2InputRef"
|
||||||
|
v-model:value="customStage2Name"
|
||||||
|
placeholder="输入自定义关卡,如: 11-8"
|
||||||
|
style="flex: 1"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="addCustomStage2"
|
||||||
|
/>
|
||||||
|
<a-button type="text" size="small" @click="addCustomStage2">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
添加关卡
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</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>
|
||||||
@@ -469,6 +549,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
<a-tag v-if="option.isCustom" color="blue" size="small" style="margin-left: 8px;">
|
||||||
|
自定义
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -490,7 +573,28 @@
|
|||||||
v-model:value="formData.Info.Stage_3"
|
v-model:value="formData.Info.Stage_3"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
placeholder="选择或输入自定义关卡"
|
||||||
>
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<a-space style="padding: 4px 8px" size="small">
|
||||||
|
<a-input
|
||||||
|
ref="stage3InputRef"
|
||||||
|
v-model:value="customStage3Name"
|
||||||
|
placeholder="输入自定义关卡,如: 11-8"
|
||||||
|
style="flex: 1"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="addCustomStage3"
|
||||||
|
/>
|
||||||
|
<a-button type="text" size="small" @click="addCustomStage3">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
添加关卡
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</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>
|
||||||
@@ -500,6 +604,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
<a-tag v-if="option.isCustom" color="blue" size="small" style="margin-left: 8px;">
|
||||||
|
自定义
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -519,7 +626,28 @@
|
|||||||
v-model:value="formData.Info.Stage_Remain"
|
v-model:value="formData.Info.Stage_Remain"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
size="large"
|
size="large"
|
||||||
|
placeholder="选择或输入自定义关卡"
|
||||||
>
|
>
|
||||||
|
<template #dropdownRender="{ menuNode: menu }">
|
||||||
|
<v-nodes :vnodes="menu" />
|
||||||
|
<a-divider style="margin: 4px 0" />
|
||||||
|
<a-space style="padding: 4px 8px" size="small">
|
||||||
|
<a-input
|
||||||
|
ref="stageRemainInputRef"
|
||||||
|
v-model:value="customStageRemainName"
|
||||||
|
placeholder="输入自定义关卡,如: 11-8"
|
||||||
|
style="flex: 1"
|
||||||
|
size="small"
|
||||||
|
@keyup.enter="addCustomStageRemain"
|
||||||
|
/>
|
||||||
|
<a-button type="text" size="small" @click="addCustomStageRemain">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
添加关卡
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</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>
|
||||||
@@ -529,15 +657,22 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
|
<a-tag v-if="option.isCustom" color="blue" size="small" style="margin-left: 8px;">
|
||||||
|
自定义
|
||||||
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="任务配置" class="form-card">
|
<!-- 任务配置 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>任务配置</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="24">
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-form-item name="ifWakeUp" label="开始唤醒">
|
<a-form-item name="ifWakeUp" label="开始唤醒">
|
||||||
@@ -594,9 +729,21 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="森空岛配置" class="form-card">
|
<!-- 森空岛配置 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>森空岛配置</h3>
|
||||||
|
<a
|
||||||
|
href="https://doc.auto-mas.top/docs/advanced-features.html#%E8%8E%B7%E5%8F%96%E9%B9%B0%E8%A7%92%E7%BD%91%E7%BB%9C%E9%80%9A%E8%A1%8C%E8%AF%81%E7%99%BB%E5%BD%95%E5%87%AD%E8%AF%81"
|
||||||
|
target="_blank"
|
||||||
|
class="section-doc-link"
|
||||||
|
title="查看森空岛签到配置文档"
|
||||||
|
>
|
||||||
|
文档
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24" align="middle">
|
<a-row :gutter="24" align="middle">
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<span style="font-weight: 500">森空岛签到</span>
|
<span style="font-weight: 500">森空岛签到</span>
|
||||||
@@ -617,21 +764,15 @@
|
|||||||
style="margin-top: 8px; width: 100%"
|
style="margin-top: 8px; width: 100%"
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
<div style="color: #999; font-size: 12px; margin-top: 4px">
|
|
||||||
请在森空岛官网获取您的专属Token并粘贴到此处,详细教程见
|
|
||||||
<a
|
|
||||||
href="https://doc.auto-mas.top/docs/advanced-features.html#%E8%8E%B7%E5%8F%96%E9%B9%B0%E8%A7%92%E7%BD%91%E7%BB%9C%E9%80%9A%E8%A1%8C%E8%AF%81%E7%99%BB%E5%BD%95%E5%87%AD%E8%AF%81"
|
|
||||||
target="_blank"
|
|
||||||
style="color: #409eff"
|
|
||||||
>获取鹰角网络通行证登录凭证</a
|
|
||||||
>
|
|
||||||
文档
|
|
||||||
</div>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<a-card title="通知配置" class="form-card">
|
<!-- 通知配置 -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>通知配置</h3>
|
||||||
|
</div>
|
||||||
<a-row :gutter="24" align="middle">
|
<a-row :gutter="24" align="middle">
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<span style="font-weight: 500">启用通知</span>
|
<span style="font-weight: 500">启用通知</span>
|
||||||
@@ -718,11 +859,13 @@
|
|||||||
"
|
"
|
||||||
size="large"
|
size="large"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
class="modern-input"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-float-button
|
<a-float-button
|
||||||
@@ -748,6 +891,7 @@ import {
|
|||||||
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'
|
||||||
@@ -790,6 +934,12 @@ const serverOptions = [
|
|||||||
{ label: '繁中服(txwy)', value: 'txwy' },
|
{ label: '繁中服(txwy)', value: 'txwy' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 关卡选项
|
||||||
|
const stageOptions = ref<any[]>([{ label: '不选择', value: '' }])
|
||||||
|
|
||||||
|
// 关卡配置模式选项
|
||||||
|
const stageModeOptions = ref<any[]>([{ label: '固定', value: 'Fixed' }])
|
||||||
|
|
||||||
// MAA脚本默认用户数据
|
// MAA脚本默认用户数据
|
||||||
const getDefaultMAAUserData = () => ({
|
const getDefaultMAAUserData = () => ({
|
||||||
Info: {
|
Info: {
|
||||||
@@ -976,6 +1126,93 @@ const loadUserData = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadStageOptions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await Service.getStageComboxApiInfoComboxStagePost({
|
||||||
|
type: 'Today',
|
||||||
|
})
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
const sorted = [...response.data].sort((a, b) => {
|
||||||
|
if (a.value === '-') return -1
|
||||||
|
if (b.value === '-') return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
stageOptions.value = sorted
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载关卡选项失败:', error)
|
||||||
|
// 保持默认选项
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStageModeOptions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await Service.getPlanComboxApiInfoComboxPlanPost()
|
||||||
|
if (response && response.code === 200 && response.data) {
|
||||||
|
stageModeOptions.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载关卡配置模式选项失败:', error)
|
||||||
|
// 保持默认的固定选项
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择基建配置文件
|
||||||
|
const selectInfrastructureConfig = async () => {
|
||||||
|
try {
|
||||||
|
const path = await window.electronAPI?.selectFile([
|
||||||
|
{ name: 'JSON 文件', extensions: ['json'] },
|
||||||
|
{ name: '所有文件', extensions: ['*'] },
|
||||||
|
])
|
||||||
|
|
||||||
|
if (path && path.length > 0) {
|
||||||
|
infrastructureConfigPath.value = path
|
||||||
|
formData.Info.InfrastPath = path[0]
|
||||||
|
message.success('文件选择成功')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件选择失败:', error)
|
||||||
|
message.error('文件选择失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入基建配置
|
||||||
|
const importInfrastructureConfig = async () => {
|
||||||
|
if (!infrastructureConfigPath.value) {
|
||||||
|
message.warning('请先选择配置文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEdit.value) {
|
||||||
|
message.warning('请先保存用户后再导入配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
infrastructureImporting.value = true
|
||||||
|
|
||||||
|
// 调用API导入基建配置
|
||||||
|
const result = await Service.importInfrastructureApiScriptsUserInfrastructurePost({
|
||||||
|
scriptId: scriptId,
|
||||||
|
userId: userId,
|
||||||
|
jsonFile: infrastructureConfigPath.value[0],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result && result.code === 200) {
|
||||||
|
message.success('基建配置导入成功')
|
||||||
|
// 清空文件路径
|
||||||
|
infrastructureConfigPath.value = ''
|
||||||
|
} else {
|
||||||
|
message.error(result?.msg || '基建配置导入失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('基建配置导入失败:', error)
|
||||||
|
message.error('基建配置导入失败')
|
||||||
|
} finally {
|
||||||
|
infrastructureImporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
@@ -1083,94 +1320,147 @@ const handleMAAConfig = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stageModeOptions = ref([{ label: '固定', value: 'Fixed' }])
|
// 自定义关卡相关
|
||||||
|
const customStageName = ref('')
|
||||||
|
const customStage1Name = ref('')
|
||||||
|
const customStage2Name = ref('')
|
||||||
|
const customStage3Name = ref('')
|
||||||
|
const customStageRemainName = ref('')
|
||||||
|
|
||||||
const loadStageModeOptions = async () => {
|
// 输入框引用
|
||||||
try {
|
const stageInputRef = ref()
|
||||||
const response = await Service.getPlanComboxApiInfoComboxPlanPost()
|
const stage1InputRef = ref()
|
||||||
if (response && response.code === 200 && response.data) {
|
const stage2InputRef = ref()
|
||||||
stageModeOptions.value = response.data
|
const stage3InputRef = ref()
|
||||||
}
|
const stageRemainInputRef = ref()
|
||||||
} catch (error) {
|
|
||||||
console.error('加载关卡配置模式选项失败:', error)
|
// VNodes 组件,用于渲染下拉菜单内容
|
||||||
// 保持默认的固定选项
|
const VNodes = {
|
||||||
}
|
props: {
|
||||||
|
vnodes: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return this.vnodes
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const stageOptions = ref([{ label: '不选择', value: '' }])
|
// 验证关卡名称格式
|
||||||
|
const validateStageName = (stageName: string): boolean => {
|
||||||
|
if (!stageName || !stageName.trim()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const loadStageOptions = async () => {
|
// 简单的关卡名称验证,可以根据实际需要调整
|
||||||
try {
|
const stagePattern = /^[a-zA-Z0-9\-_\u4e00-\u9fa5]+$/
|
||||||
const response = await Service.getStageComboxApiInfoComboxStagePost({
|
return stagePattern.test(stageName.trim())
|
||||||
type: 'Today',
|
}
|
||||||
|
|
||||||
|
// 添加自定义关卡到选项列表
|
||||||
|
const addStageToOptions = (stageName: string) => {
|
||||||
|
if (!stageName || !stageName.trim()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = stageName.trim()
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
const exists = stageOptions.value.find((option: any) => option.value === trimmedName)
|
||||||
|
if (exists) {
|
||||||
|
message.warning(`关卡 "${trimmedName}" 已存在`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到选项列表
|
||||||
|
stageOptions.value.push({
|
||||||
|
label: trimmedName,
|
||||||
|
value: trimmedName,
|
||||||
|
isCustom: true
|
||||||
})
|
})
|
||||||
if (response && response.code === 200 && response.data) {
|
|
||||||
const sorted = [...response.data].sort((a, b) => {
|
message.success(`自定义关卡 "${trimmedName}" 添加成功`)
|
||||||
if (a.value === '-') return -1
|
return true
|
||||||
if (b.value === '-') return 1
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
stageOptions.value = sorted
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载关卡选项失败:', error)
|
|
||||||
// 保持默认选项
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择基建配置文件
|
// 添加主关卡
|
||||||
const selectInfrastructureConfig = async () => {
|
const addCustomStage = () => {
|
||||||
try {
|
if (!validateStageName(customStageName.value)) {
|
||||||
const path = await window.electronAPI?.selectFile([
|
message.error('请输入有效的关卡名称')
|
||||||
{ name: 'JSON 文件', extensions: ['json'] },
|
|
||||||
{ name: '所有文件', extensions: ['*'] },
|
|
||||||
])
|
|
||||||
|
|
||||||
if (path && path.length > 0) {
|
|
||||||
infrastructureConfigPath.value = path
|
|
||||||
formData.Info.InfrastPath = path[0]
|
|
||||||
message.success('文件选择成功')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文件选择失败:', error)
|
|
||||||
message.error('文件选择失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入基建配置
|
|
||||||
const importInfrastructureConfig = async () => {
|
|
||||||
if (!infrastructureConfigPath.value) {
|
|
||||||
message.warning('请先选择配置文件')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEdit.value) {
|
if (addStageToOptions(customStageName.value)) {
|
||||||
message.warning('请先保存用户后再导入配置')
|
formData.Info.Stage = customStageName.value.trim()
|
||||||
|
customStageName.value = ''
|
||||||
|
nextTick(() => {
|
||||||
|
stageInputRef.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加备选关卡-1
|
||||||
|
const addCustomStage1 = () => {
|
||||||
|
if (!validateStageName(customStage1Name.value)) {
|
||||||
|
message.error('请输入有效的关卡名称')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (addStageToOptions(customStage1Name.value)) {
|
||||||
infrastructureImporting.value = true
|
formData.Info.Stage_1 = customStage1Name.value.trim()
|
||||||
|
customStage1Name.value = ''
|
||||||
// 调用API导入基建配置
|
nextTick(() => {
|
||||||
const result = await Service.importInfrastructureApiScriptsUserInfrastructurePost({
|
stage1InputRef.value?.focus()
|
||||||
scriptId: scriptId,
|
|
||||||
userId: userId,
|
|
||||||
jsonFile: infrastructureConfigPath.value[0],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result && result.code === 200) {
|
|
||||||
message.success('基建配置导入成功')
|
|
||||||
// 清空文件路径
|
|
||||||
infrastructureConfigPath.value = ''
|
|
||||||
} else {
|
|
||||||
message.error(result?.msg || '基建配置导入失败')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.error('基建配置导入失败:', error)
|
|
||||||
message.error('基建配置导入失败')
|
// 添加备选关卡-2
|
||||||
} finally {
|
const addCustomStage2 = () => {
|
||||||
infrastructureImporting.value = false
|
if (!validateStageName(customStage2Name.value)) {
|
||||||
|
message.error('请输入有效的关卡名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addStageToOptions(customStage2Name.value)) {
|
||||||
|
formData.Info.Stage_2 = customStage2Name.value.trim()
|
||||||
|
customStage2Name.value = ''
|
||||||
|
nextTick(() => {
|
||||||
|
stage2InputRef.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加备选关卡-3
|
||||||
|
const addCustomStage3 = () => {
|
||||||
|
if (!validateStageName(customStage3Name.value)) {
|
||||||
|
message.error('请输入有效的关卡名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addStageToOptions(customStage3Name.value)) {
|
||||||
|
formData.Info.Stage_3 = customStage3Name.value.trim()
|
||||||
|
customStage3Name.value = ''
|
||||||
|
nextTick(() => {
|
||||||
|
stage3InputRef.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加剩余理智关卡
|
||||||
|
const addCustomStageRemain = () => {
|
||||||
|
if (!validateStageName(customStageRemainName.value)) {
|
||||||
|
message.error('请输入有效的关卡名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addStageToOptions(customStageRemainName.value)) {
|
||||||
|
formData.Info.Stage_Remain = customStageRemainName.value.trim()
|
||||||
|
customStageRemainName.value = ''
|
||||||
|
nextTick(() => {
|
||||||
|
stageRemainInputRef.value?.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1183,6 +1473,7 @@ const handleCancel = () => {
|
|||||||
router.push('/scripts')
|
router.push('/scripts')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!scriptId) {
|
if (!scriptId) {
|
||||||
message.error('缺少脚本ID参数')
|
message.error('缺少脚本ID参数')
|
||||||
@@ -1241,6 +1532,115 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card :deep(.ant-card-body) {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* form-section 样式 - 来自 ScriptEdit.vue */
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid var(--ant-color-border-secondary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单标签 */
|
||||||
|
.form-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon {
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: help;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon:hover {
|
||||||
|
color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--ant-color-border);
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input:hover {
|
||||||
|
border-color: var(--ant-color-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-input:focus,
|
||||||
|
.modern-input.ant-input-focused {
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
|
box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* section标题右侧文档链接 */
|
||||||
|
.section-doc-link {
|
||||||
|
color: var(--ant-color-primary) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--ant-color-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-doc-link:hover {
|
||||||
|
color: var(--ant-color-primary-hover) !important;
|
||||||
|
background-color: var(--ant-color-primary-bg);
|
||||||
|
border-color: var(--ant-color-primary-hover);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@@ -202,15 +202,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted, h, nextTick, computed } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted, h, nextTick, computed } from 'vue'
|
||||||
import { message, notification } from 'ant-design-vue'
|
import { message, notification } from 'ant-design-vue'
|
||||||
import {
|
import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons-vue'
|
||||||
PlayCircleOutlined,
|
|
||||||
StopOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
|
||||||
import { Service } from '@/api/services/Service'
|
import { Service } from '@/api/services/Service'
|
||||||
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
|
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
|
||||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||||
|
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||||
|
|
||||||
// 调度台标签页相关
|
// 类型定义
|
||||||
|
interface RunningTask {
|
||||||
|
websocketId: string
|
||||||
|
taskName: string
|
||||||
|
status: string
|
||||||
|
logs: Array<{ time: string; message: string; type: 'info' | 'error' | 'warning' | 'success' }>
|
||||||
|
taskQueue: Array<{ name: string; status: string }>
|
||||||
|
userQueue: Array<{ name: string; status: string }>
|
||||||
|
}
|
||||||
interface SchedulerTab {
|
interface SchedulerTab {
|
||||||
key: string
|
key: string
|
||||||
title: string
|
title: string
|
||||||
@@ -219,715 +225,263 @@ interface SchedulerTab {
|
|||||||
activeTaskPanels: string[]
|
activeTaskPanels: string[]
|
||||||
completionAction: string
|
completionAction: string
|
||||||
}
|
}
|
||||||
|
interface TaskMessage { title: string; content: string; needInput: boolean; messageId?: string; taskId?: string }
|
||||||
|
|
||||||
const schedulerTabs = ref<SchedulerTab[]>([
|
// 状态
|
||||||
{
|
const schedulerTabs = ref<SchedulerTab[]>([{ key: 'main', title: '主调度台', closable: false, runningTasks: [], activeTaskPanels: [], completionAction: 'none' }])
|
||||||
key: 'main',
|
|
||||||
title: '主调度台',
|
|
||||||
closable: false,
|
|
||||||
runningTasks: [],
|
|
||||||
activeTaskPanels: [],
|
|
||||||
completionAction: 'none',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const activeSchedulerTab = ref('main')
|
const activeSchedulerTab = ref('main')
|
||||||
let tabCounter = 1
|
let tabCounter = 1
|
||||||
|
const currentTab = computed(() => schedulerTabs.value.find(t => t.key === activeSchedulerTab.value) || schedulerTabs.value[0])
|
||||||
|
|
||||||
// 当前活动的调度台
|
|
||||||
const currentTab = computed(() => {
|
|
||||||
return (
|
|
||||||
schedulerTabs.value.find(tab => tab.key === activeSchedulerTab.value) || schedulerTabs.value[0]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const addTaskModalVisible = ref(false)
|
const addTaskModalVisible = ref(false)
|
||||||
const messageModalVisible = ref(false)
|
const messageModalVisible = ref(false)
|
||||||
const taskOptionsLoading = ref(false)
|
const taskOptionsLoading = ref(false)
|
||||||
const addTaskLoading = ref(false)
|
const addTaskLoading = ref(false)
|
||||||
const outputRefs = ref<Map<string, HTMLElement>>(new Map())
|
|
||||||
|
|
||||||
// 任务选项
|
|
||||||
const taskOptions = ref<ComboBoxItem[]>([])
|
const taskOptions = ref<ComboBoxItem[]>([])
|
||||||
|
const outputRefs = ref(new Map<string, HTMLElement>())
|
||||||
// 任务表单(弹窗用)
|
|
||||||
const taskForm = reactive({
|
|
||||||
taskId: null,
|
|
||||||
mode: '自动代理' as TaskCreateIn['mode'],
|
|
||||||
})
|
|
||||||
|
|
||||||
// 快速任务表单(右上角用)
|
|
||||||
const quickTaskForm = reactive({
|
|
||||||
taskId: null,
|
|
||||||
mode: '自动代理' as TaskCreateIn['mode'],
|
|
||||||
})
|
|
||||||
|
|
||||||
// 运行中的任务
|
|
||||||
interface RunningTask {
|
|
||||||
websocketId: string
|
|
||||||
taskName: string
|
|
||||||
status: string
|
|
||||||
websocket: WebSocket | null
|
|
||||||
logs: Array<{
|
|
||||||
time: string
|
|
||||||
message: string
|
|
||||||
type: 'info' | 'error' | 'warning' | 'success'
|
|
||||||
}>
|
|
||||||
taskQueue: Array<{
|
|
||||||
name: string
|
|
||||||
status: string
|
|
||||||
}>
|
|
||||||
userQueue: Array<{
|
|
||||||
name: string
|
|
||||||
status: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息处理
|
|
||||||
interface TaskMessage {
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
needInput: boolean
|
|
||||||
messageId?: string
|
|
||||||
taskId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMessage = ref<TaskMessage | null>(null)
|
const currentMessage = ref<TaskMessage | null>(null)
|
||||||
const messageResponse = ref('')
|
const messageResponse = ref('')
|
||||||
|
|
||||||
// 调度台标签页操作
|
// 表单
|
||||||
const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
|
const taskForm = reactive<{ taskId: string | null; mode: TaskCreateIn.mode }>({ taskId: null, mode: TaskCreateIn.mode.AutoMode })
|
||||||
if (action === 'add') {
|
const quickTaskForm = reactive<{ taskId: string | null; mode: TaskCreateIn.mode }>({ taskId: null, mode: TaskCreateIn.mode.AutoMode })
|
||||||
addSchedulerTab()
|
|
||||||
} else if (action === 'remove' && typeof targetKey === 'string') {
|
|
||||||
removeSchedulerTab(targetKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// WebSocket API
|
||||||
|
const { connect: wsConnect, disconnect: wsDisconnect, sendRaw } = useWebSocket()
|
||||||
|
|
||||||
|
// Tab 事件
|
||||||
|
const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
|
||||||
|
if (action === 'add') addSchedulerTab()
|
||||||
|
else if (action === 'remove' && typeof targetKey === 'string') removeSchedulerTab(targetKey)
|
||||||
|
}
|
||||||
const addSchedulerTab = () => {
|
const addSchedulerTab = () => {
|
||||||
tabCounter++
|
tabCounter++
|
||||||
const newTab: SchedulerTab = {
|
const tab: SchedulerTab = { key: `tab-${tabCounter}`, title: `调度台${tabCounter}`, closable: true, runningTasks: [], activeTaskPanels: [], completionAction: 'none' }
|
||||||
key: `tab-${tabCounter}`,
|
schedulerTabs.value.push(tab)
|
||||||
title: `调度台${tabCounter}`,
|
activeSchedulerTab.value = tab.key
|
||||||
closable: true,
|
|
||||||
runningTasks: [],
|
|
||||||
activeTaskPanels: [],
|
|
||||||
completionAction: 'none',
|
|
||||||
}
|
}
|
||||||
schedulerTabs.value.push(newTab)
|
const removeSchedulerTab = (key: string) => {
|
||||||
activeSchedulerTab.value = newTab.key
|
const idx = schedulerTabs.value.findIndex(t => t.key === key)
|
||||||
|
if (idx === -1) return
|
||||||
|
schedulerTabs.value[idx].runningTasks.forEach(t => wsDisconnect(t.websocketId))
|
||||||
|
schedulerTabs.value.splice(idx, 1)
|
||||||
|
if (activeSchedulerTab.value === key) activeSchedulerTab.value = schedulerTabs.value[Math.max(0, idx - 1)]?.key || 'main'
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSchedulerTab = (targetKey: string) => {
|
// 引用
|
||||||
const targetIndex = schedulerTabs.value.findIndex(tab => tab.key === targetKey)
|
const setOutputRef = (el: HTMLElement | null, id: string) => { if (el) outputRefs.value.set(id, el); else outputRefs.value.delete(id) }
|
||||||
if (targetIndex === -1) return
|
|
||||||
|
|
||||||
// 停止该调度台的所有任务
|
// 拉取任务选项
|
||||||
const targetTab = schedulerTabs.value[targetIndex]
|
|
||||||
targetTab.runningTasks.forEach(task => {
|
|
||||||
if (task.websocket) {
|
|
||||||
task.websocket.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 移除调度台
|
|
||||||
schedulerTabs.value.splice(targetIndex, 1)
|
|
||||||
|
|
||||||
// 如果删除的是当前活动的调度台,切换到其他调度台
|
|
||||||
if (activeSchedulerTab.value === targetKey) {
|
|
||||||
activeSchedulerTab.value = schedulerTabs.value[Math.max(0, targetIndex - 1)].key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 设置输出容器引用
|
|
||||||
const setOutputRef = (el: HTMLElement | null, websocketId: string) => {
|
|
||||||
if (el) {
|
|
||||||
outputRefs.value.set(websocketId, el)
|
|
||||||
} else {
|
|
||||||
outputRefs.value.delete(websocketId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取任务选项
|
|
||||||
const loadTaskOptions = async () => {
|
const loadTaskOptions = async () => {
|
||||||
try {
|
try {
|
||||||
taskOptionsLoading.value = true
|
taskOptionsLoading.value = true
|
||||||
const response = await Service.getTaskComboxApiInfoComboxTaskPost()
|
const r = await Service.getTaskComboxApiInfoComboxTaskPost()
|
||||||
if (response.code === 200) {
|
if (r.code === 200) taskOptions.value = r.data
|
||||||
taskOptions.value = response.data
|
else message.error('获取任务列表失败')
|
||||||
} else {
|
} catch (e) {
|
||||||
message.error('获取任务列表失败')
|
console.error(e)
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取任务列表失败:', error)
|
|
||||||
message.error('获取任务列表失败')
|
message.error('获取任务列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
taskOptionsLoading.value = false
|
taskOptionsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示添加任务弹窗
|
// 添加任务(新 Tab)
|
||||||
const showAddTaskModal = () => {
|
|
||||||
addTaskModalVisible.value = true
|
|
||||||
if (taskOptions.value.length === 0) {
|
|
||||||
loadTaskOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加任务(弹窗方式,创建新调度台)
|
|
||||||
const addTask = async () => {
|
const addTask = async () => {
|
||||||
if (!taskForm.taskId || !taskForm.mode) {
|
if (!taskForm.taskId) return message.error('请填写完整的任务信息')
|
||||||
message.error('请填写完整的任务信息')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addTaskLoading.value = true
|
addTaskLoading.value = true
|
||||||
const response = await Service.addTaskApiDispatchStartPost({
|
const r = await Service.addTaskApiDispatchStartPost({ taskId: taskForm.taskId, mode: taskForm.mode })
|
||||||
taskId: taskForm.taskId,
|
if (r.code === 200) {
|
||||||
mode: taskForm.mode,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.code === 200) {
|
|
||||||
// 创建新的调度台
|
|
||||||
addSchedulerTab()
|
addSchedulerTab()
|
||||||
|
const opt = taskOptions.value.find(o => o.value === taskForm.taskId)
|
||||||
// 查找任务名称
|
const task: RunningTask = { websocketId: r.websocketId, taskName: opt?.label || '未知任务', status: '连接中', logs: [], taskQueue: [], userQueue: [] }
|
||||||
const selectedOption = taskOptions.value.find(option => option.value === taskForm.taskId)
|
|
||||||
const taskName = selectedOption?.label || '未知任务'
|
|
||||||
|
|
||||||
// 创建任务并添加到新调度台
|
|
||||||
const task: RunningTask = {
|
|
||||||
websocketId: response.websocketId,
|
|
||||||
taskName,
|
|
||||||
status: '连接中',
|
|
||||||
websocket: null,
|
|
||||||
logs: [],
|
|
||||||
taskQueue: [],
|
|
||||||
userQueue: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到当前活动的调度台
|
|
||||||
currentTab.value.runningTasks.push(task)
|
currentTab.value.runningTasks.push(task)
|
||||||
currentTab.value.activeTaskPanels.push(task.websocketId)
|
currentTab.value.activeTaskPanels.push(task.websocketId)
|
||||||
|
subscribeTask(task, taskForm.mode)
|
||||||
// 连接WebSocket
|
|
||||||
connectWebSocket(task)
|
|
||||||
|
|
||||||
message.success('任务创建成功')
|
message.success('任务创建成功')
|
||||||
addTaskModalVisible.value = false
|
addTaskModalVisible.value = false
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
taskForm.taskId = null
|
taskForm.taskId = null
|
||||||
taskForm.mode = '自动代理'
|
taskForm.mode = TaskCreateIn.mode.AutoMode
|
||||||
} else {
|
} else message.error(r.message || '创建任务失败')
|
||||||
message.error(response.message || '创建任务失败')
|
} catch (e) {
|
||||||
}
|
console.error(e)
|
||||||
} catch (error) {
|
|
||||||
console.error('创建任务失败:', error)
|
|
||||||
message.error('创建任务失败')
|
message.error('创建任务失败')
|
||||||
} finally {
|
} finally { addTaskLoading.value = false }
|
||||||
addTaskLoading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 快速开始任务(右上角方式,添加到当前调度台)
|
// 快速开始(当前 Tab)
|
||||||
const startQuickTask = async () => {
|
const startQuickTask = async () => {
|
||||||
if (!quickTaskForm.taskId || !quickTaskForm.mode) {
|
if (!quickTaskForm.taskId) return message.error('请选择任务和执行模式')
|
||||||
message.error('请选择任务和执行模式')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await Service.addTaskApiDispatchStartPost({
|
const r = await Service.addTaskApiDispatchStartPost({ taskId: quickTaskForm.taskId, mode: quickTaskForm.mode })
|
||||||
taskId: quickTaskForm.taskId,
|
if (r.code === 200) {
|
||||||
mode: quickTaskForm.mode,
|
const opt = taskOptions.value.find(o => o.value === quickTaskForm.taskId)
|
||||||
})
|
const name = opt?.label || '未知任务'
|
||||||
|
const idx = currentTab.value.runningTasks.findIndex(t => t.taskName === name)
|
||||||
if (response.code === 200) {
|
if (idx >= 0) {
|
||||||
// 查找任务名称
|
const existing = currentTab.value.runningTasks[idx]
|
||||||
const selectedOption = taskOptions.value.find(option => option.value === quickTaskForm.taskId)
|
wsDisconnect(existing.websocketId)
|
||||||
const taskName = selectedOption?.label || '未知任务'
|
const oldId = existing.websocketId
|
||||||
|
existing.websocketId = r.websocketId
|
||||||
// 检查是否已存在同名任务
|
existing.status = '连接中'
|
||||||
const existingTaskIndex = currentTab.value.runningTasks.findIndex(t => t.taskName === taskName)
|
existing.userQueue = []
|
||||||
|
existing.logs.push({ time: new Date().toLocaleTimeString(), message: '========== 新任务开始 ==========', type: 'info' })
|
||||||
if (existingTaskIndex >= 0) {
|
const pIdx = currentTab.value.activeTaskPanels.indexOf(oldId)
|
||||||
// 如果存在同名任务,复用现有任务卡片
|
if (pIdx >= 0) currentTab.value.activeTaskPanels.splice(pIdx, 1)
|
||||||
const existingTask = currentTab.value.runningTasks[existingTaskIndex]
|
currentTab.value.activeTaskPanels.push(existing.websocketId)
|
||||||
|
subscribeTask(existing, quickTaskForm.mode)
|
||||||
// 关闭旧的WebSocket连接(如果存在)
|
|
||||||
if (existingTask.websocket) {
|
|
||||||
existingTask.websocket.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存旧的 websocketId
|
|
||||||
const oldWebsocketId = existingTask.websocketId
|
|
||||||
|
|
||||||
// 更新任务信息
|
|
||||||
existingTask.websocketId = response.websocketId
|
|
||||||
existingTask.status = '连接中'
|
|
||||||
existingTask.websocket = null
|
|
||||||
existingTask.userQueue = []
|
|
||||||
|
|
||||||
// 添加分隔日志
|
|
||||||
existingTask.logs.push({
|
|
||||||
time: new Date().toLocaleTimeString(),
|
|
||||||
message: '========== 新任务开始 ==========',
|
|
||||||
type: 'info'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新 activeTaskPanels 数组:移除旧ID,添加新ID
|
|
||||||
const oldPanelIndex = currentTab.value.activeTaskPanels.indexOf(oldWebsocketId)
|
|
||||||
if (oldPanelIndex >= 0) {
|
|
||||||
currentTab.value.activeTaskPanels.splice(oldPanelIndex, 1)
|
|
||||||
}
|
|
||||||
currentTab.value.activeTaskPanels.push(existingTask.websocketId)
|
|
||||||
|
|
||||||
// 连接新的WebSocket
|
|
||||||
connectWebSocket(existingTask)
|
|
||||||
|
|
||||||
message.success('任务启动成功')
|
|
||||||
} else {
|
} else {
|
||||||
// 如果不存在同名任务,创建新任务
|
const task: RunningTask = { websocketId: r.websocketId, taskName: name, status: '连接中', logs: [], taskQueue: [], userQueue: [] }
|
||||||
const task: RunningTask = {
|
|
||||||
websocketId: response.websocketId,
|
|
||||||
taskName,
|
|
||||||
status: '连接中',
|
|
||||||
websocket: null,
|
|
||||||
logs: [],
|
|
||||||
taskQueue: [],
|
|
||||||
userQueue: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTab.value.runningTasks.push(task)
|
currentTab.value.runningTasks.push(task)
|
||||||
currentTab.value.activeTaskPanels.push(task.websocketId)
|
currentTab.value.activeTaskPanels.push(task.websocketId)
|
||||||
|
subscribeTask(task, quickTaskForm.mode)
|
||||||
// 连接WebSocket
|
|
||||||
connectWebSocket(task)
|
|
||||||
|
|
||||||
message.success('任务启动成功')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
quickTaskForm.taskId = null
|
quickTaskForm.taskId = null
|
||||||
quickTaskForm.mode = '自动代理'
|
quickTaskForm.mode = TaskCreateIn.mode.AutoMode
|
||||||
} else {
|
message.success('任务启动成功')
|
||||||
message.error(response.message || '启动任务失败')
|
} else message.error(r.message || '启动任务失败')
|
||||||
}
|
} catch (e) {
|
||||||
} catch (error) {
|
console.error(e)
|
||||||
console.error('启动任务失败:', error)
|
|
||||||
message.error('启动任务失败')
|
message.error('启动任务失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消添加任务
|
// 订阅任务
|
||||||
|
const subscribeTask = (task: RunningTask, mode: TaskCreateIn.mode) => {
|
||||||
|
wsConnect({
|
||||||
|
taskId: task.websocketId,
|
||||||
|
mode,
|
||||||
|
onMessage: raw => handleWebSocketMessage(task, raw),
|
||||||
|
onStatusChange: st => {
|
||||||
|
if (st === '已连接' && task.status === '连接中') task.status = '运行中'
|
||||||
|
if (st === '已断开' && task.status === '运行中') task.status = '已断开'
|
||||||
|
if (st === '连接错误') task.status = '连接错误'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消添加
|
||||||
const cancelAddTask = () => {
|
const cancelAddTask = () => {
|
||||||
addTaskModalVisible.value = false
|
addTaskModalVisible.value = false
|
||||||
taskForm.taskId = null
|
taskForm.taskId = null
|
||||||
taskForm.mode = '自动代理'
|
taskForm.mode = TaskCreateIn.mode.AutoMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接WebSocket
|
// 日志工具
|
||||||
const connectWebSocket = (task: RunningTask) => {
|
const addTaskLog = (task: RunningTask, msg: string, type: 'info' | 'error' | 'warning' | 'success' = 'info') => {
|
||||||
const wsUrl = `ws://localhost:36163/api/dispatch/ws/${task.websocketId}`
|
task.logs.push({ time: new Date().toLocaleTimeString(), message: msg, type })
|
||||||
|
|
||||||
try {
|
|
||||||
const ws = new WebSocket(wsUrl)
|
|
||||||
task.websocket = ws
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
// 更新任务状态
|
|
||||||
const taskIndex = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
|
||||||
if (taskIndex >= 0) {
|
|
||||||
currentTab.value.runningTasks[taskIndex].status = '运行中'
|
|
||||||
}
|
|
||||||
addTaskLog(task, '已连接到任务服务器', 'success')
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onmessage = event => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data)
|
|
||||||
handleWebSocketMessage(task, data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析WebSocket消息失败:', error)
|
|
||||||
addTaskLog(task, `收到无效消息: ${event.data}`, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
// 更新任务状态
|
|
||||||
const taskIndex = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
|
||||||
if (taskIndex >= 0) {
|
|
||||||
currentTab.value.runningTasks[taskIndex].status = '已断开'
|
|
||||||
currentTab.value.runningTasks[taskIndex].websocket = null
|
|
||||||
}
|
|
||||||
addTaskLog(task, '与服务器连接已断开', 'warning')
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onerror = error => {
|
|
||||||
// 更新任务状态
|
|
||||||
const taskIndex = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
|
||||||
if (taskIndex >= 0) {
|
|
||||||
currentTab.value.runningTasks[taskIndex].status = '连接错误'
|
|
||||||
}
|
|
||||||
addTaskLog(task, '连接发生错误', 'error')
|
|
||||||
console.error('WebSocket错误:', error)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
task.status = '连接失败'
|
|
||||||
addTaskLog(task, '无法连接到服务器', 'error')
|
|
||||||
console.error('WebSocket连接失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 处理WebSocket消息
|
|
||||||
const handleWebSocketMessage = (task: RunningTask, data: any) => {
|
|
||||||
// 添加详细的调试日志
|
|
||||||
console.log('收到WebSocket消息:', data)
|
|
||||||
console.log('消息类型:', data.type)
|
|
||||||
console.log('消息数据:', data.data)
|
|
||||||
|
|
||||||
// 找到任务在当前调度台中的索引,确保实时更新
|
|
||||||
const taskIndex = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
|
||||||
if (taskIndex === -1) {
|
|
||||||
console.log('未找到任务索引,websocketId:', task.websocketId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case 'Update':
|
|
||||||
console.log('处理Update消息')
|
|
||||||
// 界面更新信息 - 更新用户队列
|
|
||||||
if (data.data) {
|
|
||||||
console.log('data.data存在:', data.data)
|
|
||||||
// 更新用户队列 - 只显示 task_list 中的 name 字段
|
|
||||||
if (data.data.task_list && Array.isArray(data.data.task_list)) {
|
|
||||||
console.log('找到task_list,长度:', data.data.task_list.length)
|
|
||||||
console.log('task_list内容:', data.data.task_list)
|
|
||||||
|
|
||||||
// 直接更新响应式数组中的用户队列
|
|
||||||
const newUserQueue = data.data.task_list.map((item: any) => ({
|
|
||||||
name: item.name || '未知任务',
|
|
||||||
status: item.status || '未知',
|
|
||||||
}))
|
|
||||||
|
|
||||||
console.log('映射后的用户队列:', newUserQueue)
|
|
||||||
currentTab.value.runningTasks[taskIndex].userQueue = newUserQueue
|
|
||||||
|
|
||||||
// 强制触发响应式更新
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
console.log('nextTick后的用户队列:', currentTab.value.runningTasks[taskIndex].userQueue)
|
const el = outputRefs.value.get(task.websocketId)
|
||||||
|
if (el) el.scrollTop = el.scrollHeight
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加调试日志,确认数据更新
|
|
||||||
console.log('用户队列已更新:', currentTab.value.runningTasks[taskIndex].userQueue)
|
|
||||||
console.log('当前任务对象:', currentTab.value.runningTasks[taskIndex])
|
|
||||||
} else {
|
|
||||||
console.log('task_list不存在或不是数组')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他更新信息记录到日志,但不显示 task_list 的原始数据
|
// 颜色映射
|
||||||
for (const [key, value] of Object.entries(data.data)) {
|
const getTaskStatusColor = (s: string) => ({ '连接中': 'processing', '运行中': 'blue', '已完成': 'green', '已失败': 'red', '已断开': 'default', '连接错误': 'red' } as Record<string, string>)[s] || 'default'
|
||||||
if (key !== 'task_list') {
|
const getQueueItemStatusColor = (s: string) => /成功|完成|已完成/.test(s) ? 'green' : /失败|错误|异常/.test(s) ? 'red' : /等待|排队|挂起/.test(s) ? 'orange' : /进行|执行|运行/.test(s) ? 'blue' : 'default'
|
||||||
addTaskLog(currentTab.value.runningTasks[taskIndex], `${key}: ${value}`, 'info')
|
const filterTaskOption = (input: string, option: any) => (option?.label || '').toLowerCase().includes(input.toLowerCase())
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('data.data不存在')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'Message':
|
// 完成检测
|
||||||
// 需要用户输入的消息
|
|
||||||
currentMessage.value = {
|
|
||||||
title: '任务消息',
|
|
||||||
content: data.message || '任务需要您的输入',
|
|
||||||
needInput: true,
|
|
||||||
messageId: data.messageId,
|
|
||||||
taskId: task.websocketId,
|
|
||||||
}
|
|
||||||
messageModalVisible.value = true
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'Info':
|
|
||||||
// 通知信息
|
|
||||||
let level = 'info'
|
|
||||||
let content = '未知通知'
|
|
||||||
|
|
||||||
// 检查数据中是否有 Error 字段
|
|
||||||
if (data.data?.Error) {
|
|
||||||
// 如果是错误信息,设置为 error 级别
|
|
||||||
level = 'error'
|
|
||||||
content = data.data.Error // 错误信息内容
|
|
||||||
} else {
|
|
||||||
// 如果没有 Error 字段,继续处理 val 或 message 字段
|
|
||||||
content = data.data?.val || data.data?.message || '未知通知'
|
|
||||||
}
|
|
||||||
|
|
||||||
addTaskLog(task, content, level as any)
|
|
||||||
|
|
||||||
// 显示系统通知
|
|
||||||
if (level === 'error') {
|
|
||||||
notification.error({ message: '任务错误', description: content })
|
|
||||||
} else if (level === 'warning') {
|
|
||||||
notification.warning({ message: '任务警告', description: content })
|
|
||||||
} else if (level === 'success') {
|
|
||||||
notification.success({ message: '任务成功', description: content })
|
|
||||||
} else {
|
|
||||||
notification.info({ message: '任务信息', description: content })
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'Signal':
|
|
||||||
// 状态信号
|
|
||||||
if (data.data?.Accomplish !== undefined) {
|
|
||||||
const newStatus = data.data.Accomplish ? '已完成' : '已失败'
|
|
||||||
|
|
||||||
// 更新任务状态
|
|
||||||
currentTab.value.runningTasks[taskIndex].status = newStatus
|
|
||||||
addTaskLog(currentTab.value.runningTasks[taskIndex], `任务${newStatus}`, data.data.Accomplish ? 'success' : 'error')
|
|
||||||
|
|
||||||
// 检查是否所有任务都完成了
|
|
||||||
checkAllTasksCompleted()
|
|
||||||
|
|
||||||
// 断开连接
|
|
||||||
if (currentTab.value.runningTasks[taskIndex].websocket) {
|
|
||||||
currentTab.value.runningTasks[taskIndex].websocket.close()
|
|
||||||
currentTab.value.runningTasks[taskIndex].websocket = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
addTaskLog(task, `收到未知消息类型: ${data.type}`, 'warning')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 检查所有任务是否完成,执行完成后操作
|
|
||||||
const checkAllTasksCompleted = () => {
|
const checkAllTasksCompleted = () => {
|
||||||
const allCompleted = currentTab.value.runningTasks.every(
|
const all: RunningTask[] = []
|
||||||
task => task.status === '已完成' || task.status === '已失败' || task.status === '已断开'
|
schedulerTabs.value.forEach(t => all.push(...t.runningTasks))
|
||||||
)
|
if (!all.length) return
|
||||||
|
if (!all.every(t => ['已完成', '已失败', '已断开'].includes(t.status))) return
|
||||||
if (allCompleted && currentTab.value.runningTasks.length > 0) {
|
const action = currentTab.value.completionAction
|
||||||
executeCompletionAction(currentTab.value.completionAction)
|
if (!action || action === 'none') return
|
||||||
}
|
message.success(`所有任务结束,准备执行动作: ${action}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行完成后操作
|
// 消息弹窗控制
|
||||||
const executeCompletionAction = (action: string) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'none':
|
|
||||||
// 无动作
|
|
||||||
break
|
|
||||||
case 'exit':
|
|
||||||
// 退出软件
|
|
||||||
notification.info({ message: '所有任务已完成', description: '正在退出软件...' })
|
|
||||||
// 这里可以调用 Electron 的退出方法
|
|
||||||
window.close()
|
|
||||||
break
|
|
||||||
case 'sleep':
|
|
||||||
// 睡眠
|
|
||||||
notification.info({ message: '所有任务已完成', description: '正在进入睡眠模式...' })
|
|
||||||
// 这里需要调用系统睡眠 API
|
|
||||||
break
|
|
||||||
case 'hibernate':
|
|
||||||
// 休眠
|
|
||||||
notification.info({ message: '所有任务已完成', description: '正在进入休眠模式...' })
|
|
||||||
// 这里需要调用系统休眠 API
|
|
||||||
break
|
|
||||||
case 'shutdown':
|
|
||||||
// 关机
|
|
||||||
notification.info({ message: '所有任务已完成', description: '正在关机...' })
|
|
||||||
// 这里需要调用系统关机 API
|
|
||||||
break
|
|
||||||
case 'force-shutdown':
|
|
||||||
// 强制关机
|
|
||||||
notification.info({ message: '所有任务已完成', description: '正在强制关机...' })
|
|
||||||
// 这里需要调用系统强制关机 API
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加任务日志 - 使用 websocketId 而不是 task 对象
|
|
||||||
const addTaskLog = (
|
|
||||||
task: RunningTask,
|
|
||||||
message: string,
|
|
||||||
type: 'info' | 'error' | 'warning' | 'success' = 'info'
|
|
||||||
) => {
|
|
||||||
const now = new Date()
|
|
||||||
const time = now.toLocaleTimeString()
|
|
||||||
|
|
||||||
// 在所有调度台中查找正确的任务
|
|
||||||
let foundTask = false
|
|
||||||
for (const tab of schedulerTabs.value) {
|
|
||||||
const taskIndex = tab.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
|
||||||
if (taskIndex >= 0) {
|
|
||||||
// 直接修改 reactive 数组中的任务对象
|
|
||||||
tab.runningTasks[taskIndex].logs.push({
|
|
||||||
time,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新任务状态(如果有变化)
|
|
||||||
if (tab.runningTasks[taskIndex].status !== task.status) {
|
|
||||||
tab.runningTasks[taskIndex].status = task.status
|
|
||||||
}
|
|
||||||
|
|
||||||
foundTask = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundTask) {
|
|
||||||
console.warn('未找到对应的任务来添加日志,websocketId:', task.websocketId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动滚动到底部
|
|
||||||
nextTick(() => {
|
|
||||||
const outputElement = outputRefs.value.get(task.websocketId)
|
|
||||||
if (outputElement) {
|
|
||||||
outputElement.scrollTop = outputElement.scrollHeight
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
//发送消息响应
|
|
||||||
const sendMessageResponse = () => {
|
|
||||||
if (!currentMessage.value || !currentMessage.value.taskId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在所有调度台中查找任务
|
|
||||||
let task: RunningTask | undefined
|
|
||||||
for (const tab of schedulerTabs.value) {
|
|
||||||
task = tab.runningTasks.find(t => t.websocketId === currentMessage.value?.taskId)
|
|
||||||
if (task) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task && task.websocket) {
|
|
||||||
const response = {
|
|
||||||
type: 'MessageResponse',
|
|
||||||
messageId: currentMessage.value.messageId,
|
|
||||||
response: messageResponse.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
task.websocket.send(JSON.stringify(response))
|
|
||||||
addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info')
|
|
||||||
}
|
|
||||||
|
|
||||||
messageModalVisible.value = false
|
|
||||||
messageResponse.value = ''
|
|
||||||
currentMessage.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消消息
|
|
||||||
const cancelMessage = () => {
|
const cancelMessage = () => {
|
||||||
messageModalVisible.value = false
|
messageModalVisible.value = false
|
||||||
messageResponse.value = ''
|
messageResponse.value = ''
|
||||||
currentMessage.value = null
|
currentMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSocket 消息处理
|
||||||
|
const handleWebSocketMessage = (task: RunningTask, raw: WebSocketBaseMessage) => {
|
||||||
|
const type = raw.type
|
||||||
|
const payload: any = raw.data
|
||||||
|
const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === task.websocketId)
|
||||||
|
if (idx === -1) return
|
||||||
|
switch (type) {
|
||||||
|
case 'Update': {
|
||||||
|
if (payload?.task_list) {
|
||||||
|
currentTab.value.runningTasks[idx].userQueue = payload.task_list.map((i: any) => ({ name: i.name || '未知任务', status: i.status || '未知' }))
|
||||||
|
}
|
||||||
|
if (payload) Object.entries(payload).forEach(([k, v]) => { if (k !== 'task_list') addTaskLog(currentTab.value.runningTasks[idx], `${k}: ${v}`, 'info') })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Message': {
|
||||||
|
currentMessage.value = { title: '任务消息', content: payload?.message || payload?.val || '任务需要您的输入', needInput: true, messageId: payload?.messageId || (raw as any).messageId, taskId: task.websocketId }
|
||||||
|
messageModalVisible.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Info': {
|
||||||
|
const isErr = !!payload?.Error
|
||||||
|
const content = payload?.Error || payload?.val || payload?.message || '未知通知'
|
||||||
|
addTaskLog(task, content, isErr ? 'error' : 'info')
|
||||||
|
if (isErr) notification.error({ message: '任务错误', description: content })
|
||||||
|
else notification.info({ message: '任务信息', description: content })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Signal': {
|
||||||
|
if (payload?.Accomplish !== undefined) {
|
||||||
|
const done = !!payload.Accomplish
|
||||||
|
currentTab.value.runningTasks[idx].status = done ? '已完成' : '已失败'
|
||||||
|
addTaskLog(currentTab.value.runningTasks[idx], `任务${done ? '已完成' : '已失败'}`, done ? 'success' : 'error')
|
||||||
|
checkAllTasksCompleted()
|
||||||
|
wsDisconnect(task.websocketId)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
addTaskLog(task, `收到未知消息类型: ${type}`, 'warning')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回复消息
|
||||||
|
const sendMessageResponse = () => {
|
||||||
|
if (!currentMessage.value?.taskId) return
|
||||||
|
const task = schedulerTabs.value.flatMap(t => t.runningTasks).find(t => t.websocketId === currentMessage.value!.taskId)
|
||||||
|
if (task) {
|
||||||
|
sendRaw('MessageResponse', { messageId: currentMessage.value!.messageId, response: messageResponse.value }, task.websocketId)
|
||||||
|
addTaskLog(task, `用户回复: ${messageResponse.value}`, 'info')
|
||||||
|
}
|
||||||
|
messageModalVisible.value = false
|
||||||
|
messageResponse.value = ''
|
||||||
|
currentMessage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// 停止任务
|
// 停止任务
|
||||||
const stopTask = (taskId: string) => {
|
const stopTask = (id: string) => {
|
||||||
const taskIndex = currentTab.value.runningTasks.findIndex(t => t.websocketId === taskId)
|
const idx = currentTab.value.runningTasks.findIndex(t => t.websocketId === id)
|
||||||
if (taskIndex >= 0) {
|
if (idx >= 0) {
|
||||||
const task = currentTab.value.runningTasks[taskIndex]
|
const task = currentTab.value.runningTasks[idx]
|
||||||
|
wsDisconnect(task.websocketId)
|
||||||
// 关闭WebSocket连接
|
currentTab.value.runningTasks.splice(idx, 1)
|
||||||
if (task.websocket) {
|
const p = currentTab.value.activeTaskPanels.indexOf(id)
|
||||||
task.websocket.close()
|
if (p >= 0) currentTab.value.activeTaskPanels.splice(p, 1)
|
||||||
task.websocket = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从列表中移除
|
|
||||||
currentTab.value.runningTasks.splice(taskIndex, 1)
|
|
||||||
|
|
||||||
// 从展开面板中移除
|
|
||||||
const panelIndex = currentTab.value.activeTaskPanels.indexOf(taskId)
|
|
||||||
if (panelIndex >= 0) {
|
|
||||||
currentTab.value.activeTaskPanels.splice(panelIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success('任务已停止')
|
message.success('任务已停止')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空任务输出
|
// 清空日志(按钮已注释,可保留)
|
||||||
const clearTaskOutput = (taskId: string) => {
|
const clearTaskOutput = (id: string) => {
|
||||||
const task = currentTab.value.runningTasks.find(t => t.websocketId === taskId)
|
const t = currentTab.value.runningTasks.find(x => x.websocketId === id)
|
||||||
if (task) {
|
if (t) t.logs = []
|
||||||
task.logs = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务状态颜色
|
// 生命周期
|
||||||
const getTaskStatusColor = (status: string) => {
|
onMounted(() => { wsConnect(); loadTaskOptions() })
|
||||||
switch (status) {
|
onUnmounted(() => { schedulerTabs.value.forEach(tab => tab.runningTasks.forEach(t => wsDisconnect(t.websocketId))) })
|
||||||
case '运行中':
|
|
||||||
return 'processing'
|
|
||||||
case '已完成':
|
|
||||||
return 'success'
|
|
||||||
case '已失败':
|
|
||||||
return 'error'
|
|
||||||
case '连接中':
|
|
||||||
return 'default'
|
|
||||||
case '已断开':
|
|
||||||
return 'warning'
|
|
||||||
case '连接错误':
|
|
||||||
return 'error'
|
|
||||||
case '连接失败':
|
|
||||||
return 'error'
|
|
||||||
default:
|
|
||||||
return 'default'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取队列项状态颜色
|
|
||||||
const getQueueItemStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case '运行':
|
|
||||||
case '运行中':
|
|
||||||
return 'processing'
|
|
||||||
case '完成':
|
|
||||||
case '已完成':
|
|
||||||
return 'success'
|
|
||||||
case '失败':
|
|
||||||
case '已失败':
|
|
||||||
return 'error'
|
|
||||||
case '等待':
|
|
||||||
case '待执行':
|
|
||||||
return 'default'
|
|
||||||
case '暂停':
|
|
||||||
return 'warning'
|
|
||||||
default:
|
|
||||||
return 'default'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务选项过滤
|
|
||||||
const filterTaskOption = (input: string, option: any) => {
|
|
||||||
return option.label.toLowerCase().includes(input.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时加载任务选项
|
|
||||||
onMounted(() => {
|
|
||||||
loadTaskOptions()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时清理WebSocket连接
|
|
||||||
onUnmounted(() => {
|
|
||||||
schedulerTabs.value.forEach(tab => {
|
|
||||||
tab.runningTasks.forEach(task => {
|
|
||||||
if (task.websocket) {
|
|
||||||
task.websocket.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.scheduler-container {
|
.scheduler-container {
|
||||||
height: 100%;
|
height: calc(100vh - 64px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -941,59 +495,41 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
<<<<<<< Updated upstream
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
=======
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-bottom: 1px solid #d9d9d9;
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-actions {
|
.left-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
}
|
||||||
|
|
||||||
|
.completion-label {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-actions {
|
.right-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completion-label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.execution-area {
|
.execution-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
text-align: center;
|
||||||
align-items: center;
|
color: #999;
|
||||||
justify-content: center;
|
padding: 40px 0;
|
||||||
height: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-panels {
|
.task-panels {
|
||||||
height: 100%;
|
margin-top: 16px;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-detail-layout {
|
|
||||||
display: flex;
|
|
||||||
height: 400px;
|
|
||||||
gap: 1px;
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.realtime-logs-panel {
|
.realtime-logs-panel {
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
@@ -287,10 +287,6 @@ onMounted(() => {
|
|||||||
loadScripts()
|
loadScripts()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
// 清理所有WebSocket连接
|
|
||||||
disconnectAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadScripts = async () => {
|
const loadScripts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -454,20 +450,31 @@ const handleDeleteScript = async (script: Script) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddUser = (script: Script) => {
|
const handleAddUser = (script: Script) => {
|
||||||
// 跳转到添加用户页面
|
// 根据条件判断跳转到 MAA 还是通用用户添加页面
|
||||||
router.push(`/scripts/${script.id}/users/add`)
|
if (script.type === 'MAA') {
|
||||||
|
router.push(`/scripts/${script.id}/users/add/maa`) // 跳转到 MAA 用户添加页面
|
||||||
|
} else {
|
||||||
|
router.push(`/scripts/${script.id}/users/add/general`) // 跳转到通用用户添加页面
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditUser = (user: User) => {
|
const handleEditUser = (user: User) => {
|
||||||
// 从用户数据中找到对应的脚本
|
// 从用户数据中找到对应的脚本
|
||||||
const script = scripts.value.find(s => s.users.some(u => u.id === user.id))
|
const script = scripts.value.find(s => s.users.some(u => u.id === user.id))
|
||||||
if (script) {
|
if (script) {
|
||||||
// 跳转到编辑用户页面
|
// 判断是 MAA 用户还是通用用户
|
||||||
router.push(`/scripts/${script.id}/users/${user.id}/edit`)
|
if (user.Info.Server) {
|
||||||
|
// 跳转到 MAA 用户编辑页面
|
||||||
|
router.push(`/scripts/${script.id}/users/${user.id}/edit/maa`)
|
||||||
|
} else {
|
||||||
|
// 跳转到通用用户编辑页面
|
||||||
|
router.push(`/scripts/${script.id}/users/${user.id}/edit/general`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error('找不到对应的脚本')
|
message.error('找不到对应的脚本')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteUser = async (user: User) => {
|
const handleDeleteUser = async (user: User) => {
|
||||||
// 从用户数据中找到对应的脚本
|
// 从用户数据中找到对应的脚本
|
||||||
const script = scripts.value.find(s => s.users.some(u => u.id === user.id))
|
const script = scripts.value.find(s => s.users.some(u => u.id === user.id))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2478,6 +2478,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"electron-updater@npm:6.6.2":
|
||||||
|
version: 6.6.2
|
||||||
|
resolution: "electron-updater@npm:6.6.2"
|
||||||
|
dependencies:
|
||||||
|
builder-util-runtime: "npm:9.3.1"
|
||||||
|
fs-extra: "npm:^10.1.0"
|
||||||
|
js-yaml: "npm:^4.1.0"
|
||||||
|
lazy-val: "npm:^1.0.5"
|
||||||
|
lodash.escaperegexp: "npm:^4.1.2"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
|
semver: "npm:^7.6.3"
|
||||||
|
tiny-typed-emitter: "npm:^2.1.0"
|
||||||
|
checksum: 10c0/2b9ae5583b95f6772c4a2515ddba7ba52b65460ab81f09ae4f0b97c7e3d7b7e3d9426775eb9a53d3193bd4c3d5466bf30827c1a6ee75e4aca739c647f6ac46ff
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"electron@npm:^37.2.5":
|
"electron@npm:^37.2.5":
|
||||||
version: 37.4.0
|
version: 37.4.0
|
||||||
resolution: "electron@npm:37.4.0"
|
resolution: "electron@npm:37.4.0"
|
||||||
@@ -3075,6 +3091,7 @@ __metadata:
|
|||||||
electron: "npm:^37.2.5"
|
electron: "npm:^37.2.5"
|
||||||
electron-builder: "npm:^26.0.12"
|
electron-builder: "npm:^26.0.12"
|
||||||
electron-log: "npm:^5.4.3"
|
electron-log: "npm:^5.4.3"
|
||||||
|
electron-updater: "npm:6.6.2"
|
||||||
eslint: "npm:^9.32.0"
|
eslint: "npm:^9.32.0"
|
||||||
eslint-config-prettier: "npm:^10.1.8"
|
eslint-config-prettier: "npm:^10.1.8"
|
||||||
eslint-plugin-prettier: "npm:^5.5.3"
|
eslint-plugin-prettier: "npm:^5.5.3"
|
||||||
@@ -3872,6 +3889,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.escaperegexp@npm:^4.1.2":
|
||||||
|
version: 4.1.2
|
||||||
|
resolution: "lodash.escaperegexp@npm:4.1.2"
|
||||||
|
checksum: 10c0/484ad4067fa9119bb0f7c19a36ab143d0173a081314993fe977bd00cf2a3c6a487ce417a10f6bac598d968364f992153315f0dbe25c9e38e3eb7581dd333e087
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isequal@npm:^4.5.0":
|
||||||
|
version: 4.5.0
|
||||||
|
resolution: "lodash.isequal@npm:4.5.0"
|
||||||
|
checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.merge@npm:^4.6.2":
|
"lodash.merge@npm:^4.6.2":
|
||||||
version: 4.6.2
|
version: 4.6.2
|
||||||
resolution: "lodash.merge@npm:4.6.2"
|
resolution: "lodash.merge@npm:4.6.2"
|
||||||
@@ -5374,6 +5405,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tiny-typed-emitter@npm:^2.1.0":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "tiny-typed-emitter@npm:2.1.0"
|
||||||
|
checksum: 10c0/522bed4c579ee7ee16548540cb693a3d098b137496110f5a74bff970b54187e6b7343a359b703e33f77c5b4b90ec6cebc0d0ec3dbdf1bd418723c5c3ce36d8a2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14":
|
"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14":
|
||||||
version: 0.2.14
|
version: 0.2.14
|
||||||
resolution: "tinyglobby@npm:0.2.14"
|
resolution: "tinyglobby@npm:0.2.14"
|
||||||
|
|||||||
13
main.py
13
main.py
@@ -81,19 +81,26 @@ def main():
|
|||||||
await Config.init_config()
|
await Config.init_config()
|
||||||
await Config.get_stage(if_start=True)
|
await Config.get_stage(if_start=True)
|
||||||
await Config.clean_old_history()
|
await Config.clean_old_history()
|
||||||
main_timer = asyncio.create_task(MainTimer.second_task())
|
second_timer = asyncio.create_task(MainTimer.second_task())
|
||||||
|
hour_timer = asyncio.create_task(MainTimer.hour_task())
|
||||||
await System.set_Sleep()
|
await System.set_Sleep()
|
||||||
await System.set_SelfStart()
|
await System.set_SelfStart()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
await TaskManager.stop_task("ALL")
|
await TaskManager.stop_task("ALL")
|
||||||
main_timer.cancel()
|
second_timer.cancel()
|
||||||
|
hour_timer.cancel()
|
||||||
try:
|
try:
|
||||||
await main_timer
|
await second_timer
|
||||||
|
await hour_timer
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info("主业务定时器已关闭")
|
logger.info("主业务定时器已关闭")
|
||||||
|
|
||||||
|
from app.services import Matomo
|
||||||
|
|
||||||
|
await Matomo.close()
|
||||||
|
|
||||||
logger.info("AUTO_MAA 后端程序关闭")
|
logger.info("AUTO_MAA 后端程序关闭")
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ name = "AUTO_MAA"
|
|||||||
version = "4.0.0.1"
|
version = "4.0.0.1"
|
||||||
description = "AUTO_MAA~"
|
description = "AUTO_MAA~"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"loguru==0.7.3",
|
"loguru==0.7.3",
|
||||||
"fastapi==0.116.1",
|
"fastapi==0.116.1",
|
||||||
"pydantic==2.11.7",
|
"pydantic==2.11.7",
|
||||||
"uvicorn==0.35.0",
|
"uvicorn==0.35.0",
|
||||||
|
"websockets==15.0.1",
|
||||||
|
"aiofiles==24.1.0",
|
||||||
|
"aiohttp==3.12.15",
|
||||||
"plyer==2.1.0",
|
"plyer==2.1.0",
|
||||||
"psutil==7.0.0",
|
"psutil==7.0.0",
|
||||||
"jinja2==3.1.6",
|
"jinja2==3.1.6",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pydantic==2.11.7
|
|||||||
uvicorn==0.35.0
|
uvicorn==0.35.0
|
||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
|
aiohttp==3.12.15
|
||||||
plyer==2.1.0
|
plyer==2.1.0
|
||||||
psutil==7.0.0
|
psutil==7.0.0
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
|
|||||||
Reference in New Issue
Block a user