Merge branch 'feature/refactor' of ssh://ssh.github.com:443/AUTO-MAS-Project/AUTO-MAS into feature/refactor

This commit is contained in:
MoeSnowyFox
2025-09-27 03:34:17 +08:00
27 changed files with 1127 additions and 685 deletions

View File

@@ -882,6 +882,13 @@ class AppConfig(GlobalConfig):
"RootPath": general_config["Script"]["RootPath"], "RootPath": general_config["Script"]["RootPath"],
} }
general_config["Script"]["ConfigPathMode"] = (
"File"
if "所有文件"
in general_config["Script"]["ConfigPathMode"]
else "Folder"
)
uid, sc = await self.add_script("General") uid, sc = await self.add_script("General")
script_dict[GeneralConfig.name] = str(uid) script_dict[GeneralConfig.name] = str(uid)
await sc.load(general_config) await sc.load(general_config)
@@ -1061,6 +1068,8 @@ class AppConfig(GlobalConfig):
await queue.QueueItem.remove(key) await queue.QueueItem.remove(key)
await self.ScriptConfig.remove(uid) await self.ScriptConfig.remove(uid)
if (Path.cwd() / f"data/{uid}").exists():
shutil.rmtree(Path.cwd() / f"data/{uid}")
async def reorder_script(self, index_list: list[str]) -> None: async def reorder_script(self, index_list: list[str]) -> None:
"""重新排序脚本""" """重新排序脚本"""
@@ -1288,6 +1297,8 @@ class AppConfig(GlobalConfig):
if isinstance(script_config, (MaaConfig | GeneralConfig)): if isinstance(script_config, (MaaConfig | GeneralConfig)):
await script_config.UserData.remove(uid) await script_config.UserData.remove(uid)
await self.ScriptConfig.save() await self.ScriptConfig.save()
if (Path.cwd() / f"data/{script_id}/{user_id}").exists():
shutil.rmtree(Path.cwd() / f"data/{script_id}/{user_id}")
async def reorder_user(self, script_id: str, index_list: list[str]) -> None: async def reorder_user(self, script_id: str, index_list: list[str]) -> None:
"""重新排序用户""" """重新排序用户"""
@@ -2027,11 +2038,25 @@ class AppConfig(GlobalConfig):
data = { data = {
"recruit_statistics": defaultdict(int), "recruit_statistics": defaultdict(int),
"drop_statistics": defaultdict(dict), "drop_statistics": defaultdict(dict),
"sanity": 0,
"sanity_full_at": "",
"maa_result": maa_result, "maa_result": maa_result,
} }
if_six_star = False if_six_star = False
# 提取理智相关信息
for log_line in logs:
# 提取当前理智值:理智: 5/180
sanity_match = re.search(r"理智:\s*(\d+)/\d+", log_line)
if sanity_match:
data["sanity"] = int(sanity_match.group(1))
# 提取理智回满时间:理智将在 2025-09-26 18:57 回满。(17h 29m 后)
sanity_full_match = re.search(r"(理智将在\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s*回满。\(\d+h\s+\d+m\s+后\))", log_line)
if sanity_full_match:
data["sanity_full_at"] = sanity_full_match.group(1)
# 公招统计(仅统计招募到的) # 公招统计(仅统计招募到的)
confirmed_recruit = False confirmed_recruit = False
current_star_level = None current_star_level = None
@@ -2140,6 +2165,7 @@ class AppConfig(GlobalConfig):
log_path.parent.mkdir(parents=True, exist_ok=True) log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("w", encoding="utf-8") as f: with log_path.open("w", encoding="utf-8") as f:
f.writelines(logs) f.writelines(logs)
# 保存统计数据
with log_path.with_suffix(".json").open("w", encoding="utf-8") as f: with log_path.with_suffix(".json").open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4) json.dump(data, f, ensure_ascii=False, indent=4)
@@ -2216,6 +2242,10 @@ class AppConfig(GlobalConfig):
data[key][stage][item] = 0 data[key][stage][item] = 0
data[key][stage][item] += count data[key][stage][item] += count
# 处理理智相关字段 - 使用最后一个文件的值
elif key in ["sanity", "sanity_full_at"]:
data[key] = single_data[key]
# 录入运行结果 # 录入运行结果
elif key in ["maa_result", "general_result"]: elif key in ["maa_result", "general_result"]:

View File

@@ -112,7 +112,7 @@ class _MainTimer:
"""静默模式通过模拟老板键来隐藏模拟器窗口""" """静默模式通过模拟老板键来隐藏模拟器窗口"""
if ( if (
len(Config.if_ignore_silence) > 0 len(Config.if_ignore_silence) == 0
and Config.get("Function", "IfSilence") and Config.get("Function", "IfSilence")
and Config.get("Function", "BossKey") != "" and Config.get("Function", "BossKey") != ""
): ):

View File

@@ -1122,7 +1122,7 @@ class MaaManager:
await asyncio.sleep(self.wait_time) await asyncio.sleep(self.wait_time)
if "-" in self.ADB_address: if "-" in self.ADB_address:
ADB_ip = f"{self.ADB_address.split("-")[0]}-" ADB_ip = f"{self.ADB_address.split('-')[0]}-"
ADB_port = int(self.ADB_address.split("-")[1]) ADB_port = int(self.ADB_address.split("-")[1])
elif ":" in self.ADB_address: elif ":" in self.ADB_address:
@@ -1933,6 +1933,8 @@ class MaaManager:
message_text = ( message_text = (
f"开始时间: {message['start_time']}\n" f"开始时间: {message['start_time']}\n"
f"结束时间: {message['end_time']}\n" f"结束时间: {message['end_time']}\n"
f"理智剩余: {message.get('sanity', '未知')}\n"
f"回复时间: {message.get('sanity_full_at', '未知')}\n"
f"MAA执行结果: {message['maa_result']}\n\n" f"MAA执行结果: {message['maa_result']}\n\n"
f"{recruit_text}\n" f"{recruit_text}\n"
f"{drop_text}" f"{drop_text}"

View File

@@ -781,97 +781,44 @@ ipcMain.handle('check-git-update', async () => {
GIT_ASKPASS: '', GIT_ASKPASS: '',
} }
log.info('开始检查Git仓库更新...') log.info('开始检查Git仓库更新跳过fetch避免直接访问GitHub...')
// 执行 git fetch 获取最新的远程信息 // 执行fetch直接检查本地状态
await new Promise<void>((resolve, reject) => { // 这样避免了直接访问GitHub而是在后续的pull操作中使用镜像站
const fetchProc = spawn(gitPath, ['fetch', 'origin'], {
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
})
fetchProc.stdout?.on('data', data => { // 获取当前HEAD的commit hash
log.info('git fetch output:', data.toString()) const currentCommit = await new Promise<string>((resolve, reject) => {
}) const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
fetchProc.stderr?.on('data', data => {
log.info('git fetch stderr:', data.toString())
})
fetchProc.on('close', code => {
if (code === 0) {
resolve()
} else {
reject(new Error(`git fetch失败退出码: ${code}`))
}
})
fetchProc.on('error', reject)
})
// 检查本地分支是否落后于远程分支
const hasUpdate = await new Promise<boolean>((resolve, reject) => {
const statusProc = spawn(gitPath, ['status', '-uno', '--porcelain=v1'], {
stdio: 'pipe', stdio: 'pipe',
env: gitEnv, env: gitEnv,
cwd: appRoot, cwd: appRoot,
}) })
let output = '' let output = ''
statusProc.stdout?.on('data', data => { revParseProc.stdout?.on('data', data => {
output += data.toString() output += data.toString()
}) })
statusProc.stderr?.on('data', data => { revParseProc.on('close', code => {
log.info('git status stderr:', data.toString())
})
statusProc.on('close', code => {
if (code === 0) { if (code === 0) {
// 检查是否有 "Your branch is behind" 的信息 resolve(output.trim())
// 使用 git rev-list 来比较本地和远程分支
const revListProc = spawn(
gitPath,
['rev-list', '--count', 'HEAD..origin/feature/refactor'],
{
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
}
)
let revOutput = ''
revListProc.stdout?.on('data', data => {
revOutput += data.toString()
})
revListProc.on('close', revCode => {
if (revCode === 0) {
const commitsBehind = parseInt(revOutput.trim())
const hasUpdates = commitsBehind > 0
log.info(`本地分支落后远程分支 ${commitsBehind} 个提交hasUpdate: ${hasUpdates}`)
resolve(hasUpdates)
} else {
log.warn('无法比较本地和远程分支,假设有更新')
resolve(true) // 如果无法确定,假设有更新
}
})
revListProc.on('error', () => {
log.warn('git rev-list执行失败假设有更新')
resolve(true)
})
} else { } else {
reject(new Error(`git status失败,退出码: ${code}`)) reject(new Error(`git rev-parse失败,退出码: ${code}`))
} }
}) })
statusProc.on('error', reject) revParseProc.on('error', reject)
}) })
log.info(`Git更新检查完成hasUpdate: ${hasUpdate}`) log.info(`当前本地commit: ${currentCommit}`)
return { hasUpdate }
// 由于我们跳过了fetch步骤避免直接访问GitHub
// 我们无法准确知道远程是否有更新
// 因此返回true让后续的pull操作通过镜像站来检查和获取更新
// 如果没有更新pull操作会很快完成且不会有实际变化
log.info('跳过远程检查返回hasUpdate=true以触发镜像站更新流程')
return { hasUpdate: true, skipReason: 'avoided_github_access' }
} catch (error) { } catch (error) {
log.error('检查Git更新失败:', error) log.error('检查Git更新失败:', error)
// 如果检查失败返回true以触发更新流程确保代码是最新的 // 如果检查失败返回true以触发更新流程确保代码是最新的

View File

@@ -207,7 +207,7 @@ export async function cloneBackend(
// ==== 下面是关键逻辑 ==== // ==== 下面是关键逻辑 ====
if (isGitRepository(backendPath)) { if (isGitRepository(backendPath)) {
// 已是 git 仓库,直接 pull // 已是 git 仓库,先更新远程URL为镜像站然后 pull
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('download-progress', { mainWindow.webContents.send('download-progress', {
type: 'backend', type: 'backend',
@@ -216,6 +216,24 @@ export async function cloneBackend(
message: '正在更新后端代码...', message: '正在更新后端代码...',
}) })
} }
// 更新远程URL为镜像站URL避免直接访问GitHub
console.log(`更新远程URL为镜像站: ${repoUrl}`)
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
stdio: 'pipe',
env: gitEnv,
cwd: backendPath
})
proc.stdout?.on('data', d => console.log('git remote set-url:', d.toString()))
proc.stderr?.on('data', d => console.log('git remote set-url err:', d.toString()))
proc.on('close', code =>
code === 0 ? resolve() : reject(new Error(`git remote set-url失败退出码: ${code}`))
)
proc.on('error', reject)
})
// 执行pull操作
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath }) const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath })
proc.stdout?.on('data', d => console.log('git pull:', d.toString())) proc.stdout?.on('data', d => console.log('git pull:', d.toString()))

View File

@@ -8,6 +8,7 @@ import AppLayout from './components/AppLayout.vue'
import TitleBar from './components/TitleBar.vue' import TitleBar from './components/TitleBar.vue'
import UpdateModal from './components/UpdateModal.vue' import UpdateModal from './components/UpdateModal.vue'
import DevDebugPanel from './components/DevDebugPanel.vue' import DevDebugPanel from './components/DevDebugPanel.vue'
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN' import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { logger } from '@/utils/logger' import { logger } from '@/utils/logger'
@@ -49,6 +50,9 @@ onMounted(() => {
<!-- 开发环境调试面板 --> <!-- 开发环境调试面板 -->
<DevDebugPanel /> <DevDebugPanel />
<!-- 全局电源倒计时弹窗 -->
<GlobalPowerCountdown />
</ConfigProvider> </ConfigProvider>
</template> </template>

View File

@@ -0,0 +1,307 @@
<template>
<!-- 电源操作倒计时全屏弹窗 -->
<div v-if="visible" class="power-countdown-overlay">
<div class="power-countdown-container">
<div class="countdown-content">
<div class="warning-icon"></div>
<h2 class="countdown-title">{{ title }}</h2>
<p class="countdown-message">{{ message }}</p>
<div class="countdown-timer" v-if="countdown !== undefined">
<span class="countdown-number">{{ countdown }}</span>
<span class="countdown-unit"></span>
</div>
<div class="countdown-timer" v-else>
<span class="countdown-text">等待后端倒计时...</span>
</div>
<a-progress
v-if="countdown !== undefined"
:percent="Math.max(0, Math.min(100, (60 - countdown) / 60 * 100))"
:show-info="false"
:stroke-color="(countdown || 0) <= 10 ? '#ff4d4f' : '#1890ff'"
:stroke-width="8"
class="countdown-progress"
/>
<div class="countdown-actions">
<a-button
type="primary"
size="large"
@click="handleCancel"
class="cancel-button"
>
取消操作
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Service } from '@/api'
import { ExternalWSHandlers } from '@/composables/useWebSocket'
// 响应式状态
const visible = ref(false)
const title = ref('')
const message = ref('')
const countdown = ref<number | undefined>(undefined)
// 倒计时定时器
let countdownTimer: ReturnType<typeof setInterval> | null = null
// 启动倒计时
const startCountdown = (data: any) => {
console.log('[GlobalPowerCountdown] 启动倒计时:', data)
// 清除之前的计时器
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
// 显示倒计时弹窗
visible.value = true
// 设置倒计时数据从60秒开始
title.value = data.title || '电源操作倒计时'
message.value = data.message || '程序将在倒计时结束后执行电源操作'
countdown.value = 60
// 启动每秒倒计时
countdownTimer = setInterval(() => {
if (countdown.value !== undefined && countdown.value > 0) {
countdown.value--
console.log('[GlobalPowerCountdown] 倒计时:', countdown.value)
// 倒计时结束
if (countdown.value <= 0) {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
visible.value = false
console.log('[GlobalPowerCountdown] 倒计时结束,弹窗关闭')
}
}
}, 1000)
}
// 取消电源操作
const handleCancel = async () => {
console.log('[GlobalPowerCountdown] 用户取消电源操作')
// 清除倒计时器
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
// 关闭倒计时弹窗
visible.value = false
// 调用取消电源操作的API
try {
await Service.cancelPowerTaskApiDispatchCancelPowerPost()
console.log('[GlobalPowerCountdown] 电源操作已取消')
} catch (error) {
console.error('[GlobalPowerCountdown] 取消电源操作失败:', error)
}
}
// 处理Main消息的函数
const handleMainMessage = (message: any) => {
if (!message || typeof message !== 'object') return
const { type, data } = message
if (type === 'Message' && data && data.type === 'Countdown') {
console.log('[GlobalPowerCountdown] 收到倒计时消息:', data)
startCountdown(data)
}
}
// 清理函数
const cleanup = () => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
// 生命周期
onMounted(() => {
// 替换全局Main消息处理器添加倒计时处理
const originalMainHandler = ExternalWSHandlers.mainMessage
ExternalWSHandlers.mainMessage = (message: any) => {
// 先调用原有的处理逻辑
if (typeof originalMainHandler === 'function') {
try {
originalMainHandler(message)
} catch (e) {
console.warn('[GlobalPowerCountdown] 原有Main消息处理器出错:', e)
}
}
// 然后处理倒计时消息
handleMainMessage(message)
}
console.log('[GlobalPowerCountdown] 全局电源倒计时组件已挂载')
})
onUnmounted(() => {
cleanup()
console.log('[GlobalPowerCountdown] 全局电源倒计时组件已卸载')
})
</script>
<style scoped>
/* 电源操作倒计时全屏弹窗样式 */
.power-countdown-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 10000; /* 确保在所有其他内容之上 */
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-out;
}
.power-countdown-container {
background: var(--ant-color-bg-container);
border-radius: 16px;
padding: 48px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 500px;
width: 90%;
animation: slideIn 0.3s ease-out;
}
.countdown-content .warning-icon {
font-size: 64px;
margin-bottom: 24px;
display: block;
animation: pulse 2s infinite;
}
.countdown-title {
font-size: 28px;
font-weight: 600;
color: var(--ant-color-text);
margin: 0 0 16px 0;
}
.countdown-message {
font-size: 16px;
color: var(--ant-color-text-secondary);
margin: 0 0 32px 0;
line-height: 1.5;
}
.countdown-timer {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 32px;
}
.countdown-number {
font-size: 72px;
font-weight: 700;
color: var(--ant-color-primary);
line-height: 1;
margin-right: 8px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.countdown-unit {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-text {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-progress {
margin-bottom: 32px;
}
.countdown-actions {
display: flex;
justify-content: center;
}
.cancel-button {
padding: 12px 32px;
height: auto;
font-size: 16px;
font-weight: 500;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* 响应式 - 移动端适配 */
@media (max-width: 768px) {
.power-countdown-container {
padding: 32px 24px;
margin: 16px;
}
.countdown-title {
font-size: 24px;
}
.countdown-number {
font-size: 56px;
}
.countdown-unit {
font-size: 20px;
}
.countdown-content .warning-icon {
font-size: 48px;
margin-bottom: 16px;
}
}
</style>

View File

@@ -60,7 +60,7 @@
<template #icon> <template #icon>
<ExportOutlined /> <ExportOutlined />
</template> </template>
导出日志txt格式 导出日志log格式
</a-button> </a-button>
<!-- <a-button @click="scrollToBottom" :disabled="!logs">--> <!-- <a-button @click="scrollToBottom" :disabled="!logs">-->
<!-- <template #icon><DownOutlined /></template>--> <!-- <template #icon><DownOutlined /></template>-->
@@ -285,7 +285,7 @@ const exportLogs = async () => {
} else { } else {
fileName = `logs_${new Date().toISOString().slice(0, 10)}` fileName = `logs_${new Date().toISOString().slice(0, 10)}`
} }
a.download = `${fileName}.txt` a.download = `${fileName}.log`
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()

View File

@@ -41,7 +41,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import { BorderOutlined, CloseOutlined, CopyOutlined, MinusOutlined } from '@ant-design/icons-vue' import { BorderOutlined, CloseOutlined, MinusOutlined } from '@ant-design/icons-vue'
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import type { UpdateCheckOut } from '@/api' import type { UpdateCheckOut } from '@/api'
import { Service, type VersionOut } from '@/api' import { Service, type VersionOut } from '@/api'

View File

@@ -9,6 +9,12 @@ export interface ElectronAPI {
windowMaximize: () => Promise<void> windowMaximize: () => Promise<void>
windowClose: () => Promise<void> windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean> windowIsMaximized: () => Promise<boolean>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 初始化相关API // 初始化相关API
checkEnvironment: () => Promise<any> checkEnvironment: () => Promise<any>

View File

@@ -16,7 +16,12 @@ export interface ElectronAPI {
// 重启为管理员 // 重启为管理员
restartAsAdmin: () => Promise<void> restartAsAdmin: () => Promise<void>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 环境检查 // 环境检查
checkEnvironment: () => Promise<{ checkEnvironment: () => Promise<{
pythonExists: boolean pythonExists: boolean

View File

@@ -647,7 +647,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue' import { onMounted, reactive, ref, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@@ -894,6 +894,9 @@ const updatePathsBasedOnRoot = (newRootPath: string) => {
const pageLoading = ref(false) const pageLoading = ref(false)
const scriptId = route.params.id as string const scriptId = route.params.id as string
// 在初始化(从接口加载数据)期间阻止某些 watcher 生效
const isInitializing = ref(false)
const formData = reactive({ const formData = reactive({
name: '', name: '',
type: 'General' as ScriptType, type: 'General' as ScriptType,
@@ -945,26 +948,36 @@ const rules = {
type: [{ required: true, message: '请选择脚本类型', trigger: 'change' }], type: [{ required: true, message: '请选择脚本类型', trigger: 'change' }],
} }
// 监听配置文件类型变化,重置路径为根目录 // 延迟注册 ConfigPathMode watcher在加载脚本并完成初始化后再注册
watch( let stopConfigPathModeWatcher: (() => void) | null = null
() => generalConfig.Script.ConfigPathMode,
(newMode, oldMode) => { const setupConfigPathModeWatcher = () => {
if (newMode !== oldMode && generalConfig.Script.ConfigPath && generalConfig.Script.ConfigPath !== '.') { // 如果已存在 watcher先停止
// 当配置文件类型改变时,重置为根目录路径 if (stopConfigPathModeWatcher) {
const rootPath = generalConfig.Info.RootPath stopConfigPathModeWatcher()
if (rootPath && rootPath !== '.') { stopConfigPathModeWatcher = null
generalConfig.Script.ConfigPath = rootPath }
const typeText = newMode === 'Folder' ? '文件夹' : '文件'
message.info(`配置文件类型已切换为${typeText},路径已重置为根目录`) stopConfigPathModeWatcher = watch(
} else { () => generalConfig.Script.ConfigPathMode,
// 如果没有设置根目录,则清空路径 (newMode, oldMode) => {
generalConfig.Script.ConfigPath = '.' if (newMode !== oldMode && generalConfig.Script.ConfigPath && generalConfig.Script.ConfigPath !== '.') {
const typeText = newMode === 'Folder' ? '文件夹' : '文件' // 当配置文件类型改变时,重置为根目录路径
message.info(`配置文件类型已切换为${typeText},请重新选择路径`) const rootPath = generalConfig.Info.RootPath
if (rootPath && rootPath !== '.') {
generalConfig.Script.ConfigPath = rootPath
const typeText = newMode === 'Folder' ? '文件夹' : '文件'
message.info(`配置文件类型已切换为${typeText},路径已重置为根目录`)
} else {
// 如果没有设置根目录,则清空路径
generalConfig.Script.ConfigPath = '.'
const typeText = newMode === 'Folder' ? '文件夹' : '文件'
message.info(`配置文件类型已切换为${typeText},请重新选择路径`)
}
} }
} }
} )
) }
// 监听根目录变化,自动调整其他路径以保持相对关系 // 监听根目录变化,自动调整其他路径以保持相对关系
watch( watch(
@@ -987,9 +1000,13 @@ watch(
onMounted(async () => { onMounted(async () => {
await loadScript() await loadScript()
// 在脚本加载完成并完成初始化后,再注册 ConfigPathMode 的 watcher避免初始化阶段触发重置逻辑
setupConfigPathModeWatcher()
}) })
const loadScript = async () => { const loadScript = async () => {
// 标记正在初始化,阻止某些 watcher 在赋值时触发
isInitializing.value = true
pageLoading.value = true pageLoading.value = true
try { try {
// 检查是否有通过路由状态传递的数据(新建脚本时) // 检查是否有通过路由状态传递的数据(新建脚本时)
@@ -1030,6 +1047,10 @@ const loadScript = async () => {
router.push('/scripts') router.push('/scripts')
} finally { } finally {
pageLoading.value = false pageLoading.value = false
// 初始化完成,等待一次 nextTick 以确保所有由赋值触发的 watcher
// 在 isInitializing 为 true 时被调度并能正确跳过,然后再清除初始化标志
await nextTick()
isInitializing.value = false
} }
} }

View File

@@ -50,6 +50,33 @@
</a-space> </a-space>
</div> </div>
<!-- 通用配置遮罩层 -->
<teleport to="body">
<div v-if="showGeneralConfigMask" class="maa-config-mask">
<div class="mask-content">
<div class="mask-icon">
<SettingOutlined :style="{ fontSize: '48px', color: '#1890ff' }" />
</div>
<h2 class="mask-title">正在进行通用配置</h2>
<p class="mask-description">
当前正在进行该用户的通用配置请在配置界面完成相关设置
<br />
配置完成后请点击"保存配置"按钮来结束配置会话
</p>
<div class="mask-actions">
<a-button
v-if="generalWebsocketId"
type="primary"
size="large"
@click="handleSaveGeneralConfig"
>
保存配置
</a-button>
</div>
</div>
</div>
</teleport>
<div class="user-edit-content"> <div class="user-edit-content">
<a-card class="config-card"> <a-card class="config-card">
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="config-form"> <a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" class="config-form">
@@ -355,6 +382,8 @@ import type { FormInstance, Rule } from 'ant-design-vue/es/form'
import { useUserApi } from '@/composables/useUserApi' import { useUserApi } from '@/composables/useUserApi'
import { useScriptApi } from '@/composables/useScriptApi' import { useScriptApi } from '@/composables/useScriptApi'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { Service } from '@/api'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -376,6 +405,8 @@ const scriptName = ref('')
// 通用配置相关 // 通用配置相关
const generalConfigLoading = ref(false) const generalConfigLoading = ref(false)
const generalWebsocketId = ref<string | null>(null) const generalWebsocketId = ref<string | null>(null)
const showGeneralConfigMask = ref(false)
let generalConfigTimeout: number | null = null
// 通用脚本默认用户数据 // 通用脚本默认用户数据
const getDefaultGeneralUserData = () => ({ const getDefaultGeneralUserData = () => ({
@@ -556,31 +587,106 @@ const handleGeneralConfig = async () => {
try { try {
generalConfigLoading.value = true generalConfigLoading.value = true
// 先立即显示遮罩以避免后端延迟导致无法感知
showGeneralConfigMask.value = true
// 如果已有连接,先断开并清理
if (generalWebsocketId.value) { if (generalWebsocketId.value) {
unsubscribe(generalWebsocketId.value) unsubscribe(generalWebsocketId.value)
generalWebsocketId.value = null generalWebsocketId.value = null
showGeneralConfigMask.value = false
if (generalConfigTimeout) {
window.clearTimeout(generalConfigTimeout)
generalConfigTimeout = null
}
} }
const subId = userId // 调用后端启动任务接口,传入 userId 作为 taskId 与设置模式
const response = await Service.addTaskApiDispatchStartPost({
subscribe(subId, { taskId: userId,
onError: error => { mode: TaskCreateIn.mode.SettingScriptMode,
console.error(`用户 ${formData.userName} 通用配置错误:`, error)
message.error(`通用配置连接失败: ${error}`)
generalWebsocketId.value = null
}
}) })
generalWebsocketId.value = subId console.debug('通用配置 start 接口返回:', response)
message.success(`已开始配置用户 ${formData.userName} 的通用设置`) if (response && response.websocketId) {
const wsId = response.websocketId
console.debug('订阅 websocketId:', wsId)
// 订阅 websocket
subscribe(wsId, {
onMessage: (wsMessage: any) => {
if (wsMessage.type === 'error') {
console.error(`用户 ${formData.userName} 通用配置错误:`, wsMessage.data)
message.error(`通用配置连接失败: ${wsMessage.data}`)
unsubscribe(wsId)
generalWebsocketId.value = null
showGeneralConfigMask.value = false
return
}
if (wsMessage.data && wsMessage.data.Accomplish) {
message.success(`用户 ${formData.userName} 的配置已完成`)
unsubscribe(wsId)
generalWebsocketId.value = null
showGeneralConfigMask.value = false
}
},
})
generalWebsocketId.value = wsId
showGeneralConfigMask.value = true
message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
// 设置 30 分钟超时自动断开
generalConfigTimeout = window.setTimeout(() => {
if (generalWebsocketId.value) {
const id = generalWebsocketId.value
unsubscribe(id)
generalWebsocketId.value = null
showGeneralConfigMask.value = false
message.info(`用户 ${formData.userName} 的配置会话已超时断开`)
}
generalConfigTimeout = null
}, 30 * 60 * 1000)
} else {
message.error(response?.message || '启动通用配置失败')
}
} catch (error) { } catch (error) {
console.error('通用配置失败:', error) console.error('启动通用配置失败:', error)
message.error('通用配置失败') message.error('启动通用配置失败')
} finally { } finally {
generalConfigLoading.value = false generalConfigLoading.value = false
} }
} }
const handleSaveGeneralConfig = async () => {
try {
const websocketId = generalWebsocketId.value
if (!websocketId) {
message.error('未找到活动的配置会话')
return
}
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
if (response && response.code === 200) {
unsubscribe(websocketId)
generalWebsocketId.value = null
showGeneralConfigMask.value = false
if (generalConfigTimeout) {
window.clearTimeout(generalConfigTimeout)
generalConfigTimeout = null
}
message.success('用户的通用配置已保存')
} else {
message.error(response.message || '保存配置失败')
}
} catch (error) {
console.error('保存通用配置失败:', error)
message.error('保存通用配置失败')
}
}
// 文件选择方法 // 文件选择方法
const selectScriptBeforeTask = async () => { const selectScriptBeforeTask = async () => {
try { try {
@@ -622,6 +728,11 @@ const handleCancel = () => {
if (generalWebsocketId.value) { if (generalWebsocketId.value) {
unsubscribe(generalWebsocketId.value) unsubscribe(generalWebsocketId.value)
generalWebsocketId.value = null generalWebsocketId.value = null
showGeneralConfigMask.value = false
if (generalConfigTimeout) {
window.clearTimeout(generalConfigTimeout)
generalConfigTimeout = null
}
} }
router.push('/scripts') router.push('/scripts')
} }
@@ -829,4 +940,55 @@ onMounted(() => {
max-width: 100%; max-width: 100%;
} }
} }
/* 通用/MAA 配置遮罩样式(用于全局覆盖) */
.maa-config-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.mask-content {
background: var(--ant-color-bg-elevated);
border-radius: 8px;
padding: 24px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
border: 1px solid var(--ant-color-border);
}
.mask-icon {
margin-bottom: 16px;
}
.mask-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px;
color: var(--ant-color-text);
}
.mask-description {
font-size: 14px;
color: var(--ant-color-text-secondary);
margin: 0 0 24px;
line-height: 1.5;
}
.mask-actions {
display: flex;
justify-content: center;
}
</style> </style>

View File

@@ -26,7 +26,7 @@
</div> </div>
<!-- 详细筛选条件 --> <!-- 详细筛选条件 -->
<a-row :gutter="16" align="middle"> <a-row :gutter="16" :align="'middle'">
<a-col :span="6"> <a-col :span="6">
<a-form-item label="合并模式" style="margin-bottom: 0"> <a-form-item label="合并模式" style="margin-bottom: 0">
<a-select v-model:value="searchForm.mode" style="width: 100%"> <a-select v-model:value="searchForm.mode" style="width: 100%">
@@ -277,43 +277,54 @@
<a-card size="small" title="详细日志" class="log-card"> <a-card size="small" title="详细日志" class="log-card">
<template #extra> <template #extra>
<a-space> <a-space>
<a-tooltip title="打开日志文件"> <a-tooltip title="打开日志文件" :getPopupContainer="tooltipContainer">
<a-button <a-button
size="small" size="small"
type="text" type="text"
:disabled="!currentJsonFile" :disabled="!currentJsonFile"
@click="handleOpenLogFile" @click="handleOpenLogFile"
:class="{ 'no-hover-shift': true }"
:style="buttonFixedStyle"
> >
<template #icon> <template #icon>
<FileOutlined /> <FileOutlined />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip title="打开日志文件所在目录"> <a-tooltip title="打开日志文件所在目录" :getPopupContainer="tooltipContainer">
<a-button <a-button
size="small" size="small"
type="text" type="text"
:disabled="!currentJsonFile" :disabled="!currentJsonFile"
@click="handleOpenLogDirectory" @click="handleOpenLogDirectory"
:class="{ 'no-hover-shift': true }"
:style="buttonFixedStyle"
> >
<template #icon> <template #icon>
<FolderOpenOutlined /> <FolderOpenOutlined />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip :getPopupContainer="tooltipContainer">
<a-select
v-model:value="logFontSize"
size="small"
class="log-font-size-select"
style="width: 72px"
:options="logFontSizeOptions.map(v => ({ value: v, label: v + 'px' }))"
/>
</a-tooltip>
</a-space> </a-space>
</template> </template>
<a-spin :spinning="detailLoading"> <a-spin :spinning="detailLoading">
<div v-if="currentDetail?.log_content" class="log-content"> <div v-if="currentDetail?.log_content" class="log-content" :style="{ fontSize: logFontSize + 'px' }">
<pre>{{ currentDetail.log_content }}</pre> <pre>{{ currentDetail.log_content }}</pre>
</div> </div>
<div v-else class="no-log"> <div v-else class="no-log">
<a-empty <a-empty
description="未选择日志,请从左边记录条目中选择" description="未选择日志,请从左边记录条目中选择"
:image="NodataImage" :image="NodataImage"
:image-style="{ :image-style="{ height: '60px' }"
height: '60px',
}"
/> />
</div> </div>
</a-spin> </a-spin>
@@ -341,7 +352,7 @@ import {
FileOutlined, 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 } from '@/api' import { HistorySearchIn, type HistoryData } from '@/api' // 调整枚举需要值导入
import dayjs from 'dayjs' import dayjs from 'dayjs'
import NodataImage from '@/assets/NoData.png' import NodataImage from '@/assets/NoData.png'
@@ -358,62 +369,20 @@ const selectedRecordIndex = ref(-1)
const currentDetail = ref<HistoryData | null>(null) const currentDetail = ref<HistoryData | null>(null)
const currentJsonFile = ref('') const currentJsonFile = ref('')
// 快捷时间选择预设 // 快捷时间选择预设(改用枚举值)
const timePresets = [ const timePresets = [
{ { key: 'today', label: '今天', startDate: () => dayjs().format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
key: 'today', { key: 'yesterday', label: '昨天', startDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'), endDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
label: '今天', { key: 'week', label: '最近一周', startDate: () => dayjs().subtract(7, 'day').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
startDate: () => dayjs().format('YYYY-MM-DD'), { key: 'month', label: '最近一个月', startDate: () => dayjs().subtract(1, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
endDate: () => dayjs().format('YYYY-MM-DD'), { key: 'twoMonths', label: '最近两个月', startDate: () => dayjs().subtract(2, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
mode: '按日合并' as HistorySearchIn.mode, { key: 'threeMonths', label: '最近三个月', startDate: () => dayjs().subtract(3, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.MONTHLY },
}, { key: 'halfYear', label: '最近半年', startDate: () => dayjs().subtract(6, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.MONTHLY },
{
key: 'yesterday',
label: '昨天',
startDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
endDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'),
mode: '按日合并' as HistorySearchIn.mode,
},
{
key: 'week',
label: '最近一周',
startDate: () => dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
endDate: () => dayjs().format('YYYY-MM-DD'),
mode: '按日合并' as HistorySearchIn.mode,
},
{
key: 'month',
label: '最近一个月',
startDate: () => dayjs().subtract(1, 'month').format('YYYY-MM-DD'),
endDate: () => dayjs().format('YYYY-MM-DD'),
mode: '按周合并' as HistorySearchIn.mode,
},
{
key: 'twoMonths',
label: '最近两个月',
startDate: () => dayjs().subtract(2, 'month').format('YYYY-MM-DD'),
endDate: () => dayjs().format('YYYY-MM-DD'),
mode: '按周合并' as HistorySearchIn.mode,
},
{
key: 'threeMonths',
label: '最近三个月',
startDate: () => dayjs().subtract(3, 'month').format('YYYY-MM-DD'),
endDate: () => dayjs().format('YYYY-MM-DD'),
mode: '按月合并' as HistorySearchIn.mode,
},
{
key: 'halfYear',
label: '最近半年',
startDate: () => dayjs().subtract(6, 'month').format('YYYY-MM-DD'),
endDate: () => dayjs().format('YYYY-MM-DD'),
mode: '按月合并' as HistorySearchIn.mode,
},
] ]
// 搜索表单 // 搜索表单(默认按日合并)
const searchForm = reactive({ const searchForm = reactive({
mode: '按日合并' as HistorySearchIn.mode, mode: HistorySearchIn.mode.DAILY as HistorySearchIn.mode,
startDate: dayjs().subtract(7, 'day').format('YYYY-MM-DD'), startDate: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'), endDate: dayjs().format('YYYY-MM-DD'),
}) })
@@ -426,37 +395,6 @@ interface HistoryDateGroup {
const historyData = ref<HistoryDateGroup[]>([]) const historyData = ref<HistoryDateGroup[]>([])
// 计算总览数据
const totalOverview = computed(() => {
let totalRecruit = 0
let totalDrop = 0
historyData.value.forEach(dateGroup => {
Object.values(dateGroup.users).forEach(userData => {
// 统计公招数据
if (userData.recruit_statistics) {
Object.values(userData.recruit_statistics).forEach((count: any) => {
totalRecruit += count
})
}
// 统计掉落数据
if (userData.drop_statistics) {
Object.values(userData.drop_statistics).forEach((stageDrops: any) => {
Object.values(stageDrops).forEach((count: any) => {
totalDrop += count
})
})
}
})
})
return {
totalRecruit,
totalDrop,
}
})
// 当前显示的统计数据(根据是否选中记录条目来决定显示用户总计还是单条记录的数据) // 当前显示的统计数据(根据是否选中记录条目来决定显示用户总计还是单条记录的数据)
const currentStatistics = computed(() => { const currentStatistics = computed(() => {
if (selectedRecordIndex.value >= 0 && currentDetail.value) { if (selectedRecordIndex.value >= 0 && currentDetail.value) {
@@ -523,7 +461,7 @@ const handleSearch = async () => {
// 重置搜索条件 // 重置搜索条件
const handleReset = () => { const handleReset = () => {
searchForm.mode = '按日合并' searchForm.mode = HistorySearchIn.mode.DAILY
searchForm.startDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD') searchForm.startDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD')
searchForm.endDate = dayjs().format('YYYY-MM-DD') searchForm.endDate = dayjs().format('YYYY-MM-DD')
historyData.value = [] historyData.value = []
@@ -546,12 +484,12 @@ const handleDateChange = () => {
currentPreset.value = '' currentPreset.value = ''
} }
// <EFBFBD><EFBFBD><EFBFBD>择用户处理 // 择用户处理(修正乱码注释)
const handleSelectUser = async (date: string, username: string, userData: HistoryData) => { const handleSelectUser = async (date: string, username: string, userData: HistoryData) => {
selectedUser.value = `${date}-${username}` selectedUser.value = `${date}-${username}`
selectedUserData.value = userData selectedUserData.value = userData
selectedRecordIndex.value = -1 // 重置记录选择 selectedRecordIndex.value = -1
currentDetail.value = null // 清空日志内容 currentDetail.value = null
currentJsonFile.value = '' currentJsonFile.value = ''
} }
@@ -655,15 +593,14 @@ const handleOpenLogDirectory = async () => {
} }
} }
// 获取日期状态颜色 // 日志字体大小(恢复)
const getDateStatusColor = (users: Record<string, HistoryData>) => { const logFontSize = ref(14)
const hasError = Object.values(users).some( const logFontSizeOptions = [12, 13, 14, 16, 18, 20]
user =>
user.index?.some(item => item.status === '异常') || // Tooltip 容器:避免挂载到 body 造成全局滚动条闪烁与布局抖动
(user.error_info && Object.keys(user.error_info).length > 0) const tooltipContainer = (triggerNode: HTMLElement) => triggerNode?.parentElement || document.body
) // 固定 button 尺寸,避免 hover/tooltip 状态导致宽度高度微调
return hasError ? 'error' : 'success' const buttonFixedStyle = { width: '28px', height: '28px', padding: 0 }
}
</script> </script>
<style scoped> <style scoped>
@@ -681,11 +618,6 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
gap: 16px; gap: 16px;
} }
.title-icon {
font-size: 32px;
color: var(--ant-color-primary);
}
.header-title h1 { .header-title h1 {
margin: 0; margin: 0;
font-size: 32px; font-size: 32px;
@@ -701,8 +633,9 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
margin-bottom: 24px; margin-bottom: 24px;
} }
.history-content { .history-content { /* 避免 tooltip 在局部弹出时引起外层出现滚动条 */
height: calc(80vh - 200px); height: calc(80vh - 200px);
overflow: hidden;
} }
.empty-state { .empty-state {
@@ -726,21 +659,6 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
gap: 16px; gap: 16px;
} }
.overview-section {
flex-shrink: 0;
}
.overview-card {
border: 1px solid var(--ant-color-border);
border-radius: 8px;
}
.overview-stats {
display: flex;
justify-content: space-around;
gap: 16px;
}
.date-list { .date-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@@ -785,7 +703,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
} }
.user-item:hover { .user-item:hover {
background: var(--ant-color-bg-container-disabled); background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
border-color: var(--ant-color-border); border-color: var(--ant-color-border);
} }
@@ -805,17 +723,11 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
font-size: 13px; font-size: 13px;
} }
.user-status {
display: flex;
gap: 4px;
}
/* 右侧详情区域 */ /* 右侧详情区域 */
.detail-area { .detail-area {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0;
} }
.no-selection { .no-selection {
@@ -826,6 +738,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: 8px; border-radius: 8px;
background: var(--ant-color-bg-container); background: var(--ant-color-bg-container);
min-height: 400px;
} }
.detail-content { .detail-content {
@@ -833,16 +746,18 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
display: flex; display: flex;
gap: 16px; gap: 16px;
min-height: 0; min-height: 0;
min-width: 0; /* 确保子项 flex:1 时可以收缩 */
overflow: hidden; /* 避免被长行撑出 */
} }
/* 记录条目区域 */ /* 记录条目区域 */
.records-area { .records-area {
width: 400px; width: 400px;
flex-shrink: 0; flex-shrink: 1; /* 新增: 允许一定程度收缩 */
min-width: 260px; /* 给一个合理下限 */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
min-width: 0;
} }
.records-section { .records-section {
@@ -883,7 +798,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
} }
.record-item:hover { .record-item:hover {
background: var(--ant-color-bg-container-disabled); background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
} }
.record-item.active { .record-item.active {
@@ -968,19 +883,11 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
min-height: 120px; min-height: 120px;
} }
.error-section {
margin-top: 16px;
}
.error-card {
border: 1px solid var(--ant-color-error-border);
border-radius: 8px;
}
/* 日志区域 */ /* 日志区域 */
.log-area { .log-area {
flex: 1; flex: 1;
min-width: 300px; /* 允许在父级 flex 宽度不足时压缩,避免整体被撑出视口 */
min-width: 0; /* 修改: 原来是 300px导致在内容渲染后无法收缩 */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -1004,30 +911,34 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
flex: 1; flex: 1;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
background: var(--ant-color-bg-layout); /* 新增: 防止超长无空格字符串把容器撑宽 */
border: 1px solid var(--ant-color-border); overflow-x: auto; /* 横向单独滚动,而不是撑出布局 */
border-radius: 6px; word-break: break-all;
padding: 12px; overflow-wrap: anywhere;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 12px; line-height: 1.5;
line-height: 1.4;
} }
.log-content pre { .log-content pre {
margin: 0; margin: 0;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all;
overflow-wrap: anywhere;
max-width: 100%;
font-size: inherit;
line-height: inherit;
} }
.no-log { /* 恢复字体选择器样式 */
flex: 1; .log-font-size-select :deep(.ant-select-selector) {
display: flex; padding: 0 4px;
align-items: center; text-align: center;
justify-content: center;
min-height: 500px;
} }
/* 按钮样式 */ /* 按钮样式 */
/* 移除未使用 .title-icon */
/* 移除 unused overview-section / overview-card / overview-stats / user-status / error-section / error-card */
.default { .default {
border-color: var(--ant-color-border); border-color: var(--ant-color-border);
color: var(--ant-color-text); color: var(--ant-color-text);
@@ -1038,6 +949,22 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
color: var(--ant-color-primary); color: var(--ant-color-primary);
} }
/* 防止按钮在获得焦点/激活时出现位移(如出现 outline 或行高变化导致的抖动) */
.no-hover-shift {
line-height: 1; /* 固定行高 */
}
.no-hover-shift :deep(.ant-btn-icon) {
display: flex;
align-items: center;
justify-content: center;
}
/* 约束 tooltip 在本容器内时的最大宽度,减少撑开 */
:deep(.ant-tooltip) {
max-width: 260px;
word-break: break-word;
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.history-layout { .history-layout {
@@ -1055,52 +982,22 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
.log-area { .log-area {
width: 100%; width: 100%;
max-height: 400px; min-width: 0;
} }
} }
/* 带tooltip的错误tag样式 */ /* 针对极窄窗口再降级为纵向布局,提前触发布局切换,避免出现水平滚动 */
.error-tag-with-tooltip { @media (max-width: 1000px) {
cursor: help; .history-layout {
position: relative; flex-direction: column;
} }
.records-area {
.error-tag-with-tooltip:hover { width: 100%;
opacity: 0.8; min-width: 0;
} }
.log-area {
/* 统计数据标题样式 */ width: 100%;
.stat-subtitle { min-width: 0;
font-size: 12px; }
color: var(--ant-color-text-secondary);
font-weight: normal;
margin-left: 8px;
}
/* 滚动条样式 */
.date-list::-webkit-scrollbar,
.log-content::-webkit-scrollbar,
.records-list::-webkit-scrollbar {
width: 6px;
}
.date-list::-webkit-scrollbar-track,
.log-content::-webkit-scrollbar-track,
.records-list::-webkit-scrollbar-track {
background: var(--ant-color-bg-container);
border-radius: 3px;
}
.date-list::-webkit-scrollbar-thumb,
.log-content::-webkit-scrollbar-thumb,
.records-list::-webkit-scrollbar-thumb {
background: var(--ant-color-border);
border-radius: 3px;
}
.date-list::-webkit-scrollbar-thumb:hover,
.log-content::-webkit-scrollbar-thumb:hover,
.records-list::-webkit-scrollbar-thumb:hover {
background: var(--ant-color-border-secondary);
} }
</style> </style>

View File

@@ -191,55 +191,45 @@ async function checkEnvironment() {
console.log('- main.py存在:', criticalFiles.mainPyExists) console.log('- main.py存在:', criticalFiles.mainPyExists)
console.log('- 所有关键文件存在:', allExeFilesExist) console.log('- 所有关键文件存在:', allExeFilesExist)
// 新的自动模式判断逻辑:只要所有关键exe文件都存在且不是第一次启动就进入自动模式 // 页面模式判断逻辑:
console.log('自动模式判断条件:') // 1. 第一次启动 -> 直接进入手动模式
console.log('- 不是第一次启动:', !isFirst) // 2. 非第一次启动 + 文件完整 -> 自动模式
// 3. 非第一次启动 + 文件缺失 -> 环境不完整页面
console.log('页面模式判断条件:')
console.log('- 是否第一次启动:', isFirst)
console.log('- 所有关键文件存在:', allExeFilesExist) console.log('- 所有关键文件存在:', allExeFilesExist)
// 只要不是第一次启动且所有关键exe文件都存在就进入动模式 // 第一次启动时,无论文件是否存在都直接进入动模式
if (!isFirst && allExeFilesExist) { if (isFirst) {
console.log('第一次启动,直接进入手动模式')
autoMode.value = false
showEnvironmentIncomplete.value = false
} else if (allExeFilesExist) {
// 不是第一次启动且所有关键exe文件都存在进入自动模式
console.log('进入自动模式,开始自动启动流程') console.log('进入自动模式,开始自动启动流程')
autoMode.value = true autoMode.value = true
showEnvironmentIncomplete.value = false
} else { } else {
console.log('进入手动模式') // 不是第一次启动但关键文件缺失,显示环境不完整页面
if (isFirst) { console.log('环境损坏,显示环境不完整页面')
console.log('原因: 第一次启动') console.log(' - python.exe缺失:', !criticalFiles.pythonExists)
// 第一次启动直接进入手动模式 console.log(' - git.exe缺失:', !criticalFiles.gitExists)
autoMode.value = false console.log(' - main.py缺失:', !criticalFiles.mainPyExists)
showEnvironmentIncomplete.value = false
} else if (!allExeFilesExist) {
console.log('原因: 关键exe文件缺失')
console.log(' - python.exe缺失:', !criticalFiles.pythonExists)
console.log(' - git.exe缺失:', !criticalFiles.gitExists)
console.log(' - main.py缺失:', !criticalFiles.mainPyExists)
// 检查是否应该显示环境不完整页面(仅在自动模式下) const missing = []
// 如果不是第一次启动且关键文件缺失,说明之前是自动模式但现在环境有问题 if (!criticalFiles.pythonExists) missing.push('Python 环境')
if (!isFirst) { if (!criticalFiles.gitExists) missing.push('Git 工具')
const missing = [] if (!criticalFiles.mainPyExists) missing.push('后端代码')
if (!criticalFiles.pythonExists) missing.push('Python 环境')
if (!criticalFiles.gitExists) missing.push('Git 工具')
if (!criticalFiles.mainPyExists) missing.push('后端代码')
missingComponents.value = missing missingComponents.value = missing
showEnvironmentIncomplete.value = true showEnvironmentIncomplete.value = true
autoMode.value = false autoMode.value = false
} else { }
// 第一次启动时,即使文件缺失也直接进入手动模式
autoMode.value = false
showEnvironmentIncomplete.value = false
}
} else {
// 其他情况直接进入手动模式
autoMode.value = false
showEnvironmentIncomplete.value = false
}
// 如果关键文件缺失,重置初始化状态 // 如果关键文件缺失,重置初始化状态
if (!allExeFilesExist && config.init) { if (!allExeFilesExist && config.init) {
console.log('检测到关键exe文件缺失重置初始化状态') console.log('检测到关键exe文件缺失重置初始化状态')
await saveConfig({ init: false }) await saveConfig({ init: false })
}
} }
} catch (error) { } catch (error) {
const errorMsg = `环境检查失败: ${error instanceof Error ? error.message : String(error)}` const errorMsg = `环境检查失败: ${error instanceof Error ? error.message : String(error)}`
@@ -284,14 +274,6 @@ onMounted(async () => {
console.log('测试配置系统...') console.log('测试配置系统...')
const testConfig = await getConfig() const testConfig = await getConfig()
console.log('当前配置:', testConfig) console.log('当前配置:', testConfig)
// 测试保存配置
await saveConfig({ isFirstLaunch: false })
console.log('测试配置保存成功')
// 重新读取配置验证
const updatedConfig = await getConfig()
console.log('更新后的配置:', updatedConfig)
} catch (error) { } catch (error) {
console.error('配置系统测试失败:', error) console.error('配置系统测试失败:', error)
} }

View File

@@ -1,5 +1,31 @@
<template> <template>
<div class="user-edit-container"> <div class="user-edit-container">
<!-- MAA配置遮罩层 -->
<teleport to="body">
<div v-if="showMAAConfigMask" class="maa-config-mask">
<div class="mask-content">
<div class="mask-icon">
<SettingOutlined :style="{ fontSize: '48px', color: '#1890ff' }" />
</div>
<h2 class="mask-title">正在进行MAA配置</h2>
<p class="mask-description">
当前正在配置该用户的 MAA请在 MAA 配置界面完成相关设置
<br />
配置完成后请点击"保存配置"按钮来结束配置会话
</p>
<div class="mask-actions">
<a-button
v-if="maaWebsocketId"
type="primary"
size="large"
@click="handleSaveMAAConfig"
>
保存配置
</a-button>
</div>
</div>
</div>
</teleport>
<!-- 头部组件 --> <!-- 头部组件 -->
<MAAUserEditHeader <MAAUserEditHeader
:script-id="scriptId" :script-id="scriptId"
@@ -104,13 +130,14 @@
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { SaveOutlined } from '@ant-design/icons-vue' import { SaveOutlined, SettingOutlined } from '@ant-design/icons-vue'
import type { FormInstance, Rule } from 'ant-design-vue/es/form' import type { FormInstance, Rule } from 'ant-design-vue/es/form'
import { useUserApi } from '@/composables/useUserApi' import { useUserApi } from '@/composables/useUserApi'
import { useScriptApi } from '@/composables/useScriptApi' import { useScriptApi } from '@/composables/useScriptApi'
import { usePlanApi } from '@/composables/usePlanApi' import { usePlanApi } from '@/composables/usePlanApi'
import { useWebSocket } from '@/composables/useWebSocket' import { useWebSocket } from '@/composables/useWebSocket'
import { Service } from '@/api' import { Service } from '@/api'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import { GetStageIn } from '@/api/models/GetStageIn' import { GetStageIn } from '@/api/models/GetStageIn'
import { getTodayWeekdayEast12 } from '@/utils/dateUtils' import { getTodayWeekdayEast12 } from '@/utils/dateUtils'
@@ -143,6 +170,8 @@ const scriptName = ref('')
// MAA配置相关 // MAA配置相关
const maaConfigLoading = ref(false) const maaConfigLoading = ref(false)
const maaWebsocketId = ref<string | null>(null) const maaWebsocketId = ref<string | null>(null)
const showMAAConfigMask = ref(false)
let maaConfigTimeout: number | null = null
// 基建配置文件相关 // 基建配置文件相关
const infrastructureConfigPath = ref('') const infrastructureConfigPath = ref('')
@@ -741,28 +770,96 @@ const handleMAAConfig = async () => {
if (maaWebsocketId.value) { if (maaWebsocketId.value) {
unsubscribe(maaWebsocketId.value) unsubscribe(maaWebsocketId.value)
maaWebsocketId.value = null maaWebsocketId.value = null
showMAAConfigMask.value = false
if (maaConfigTimeout) {
window.clearTimeout(maaConfigTimeout)
maaConfigTimeout = null
}
} }
// 直接订阅(旧 connect 参数移除) // 调用后端启动任务接口,传入 userId 作为 taskId 与设置模式
const subId = userId const response = await Service.addTaskApiDispatchStartPost({
subscribe(subId, { taskId: userId,
onError: error => { mode: TaskCreateIn.mode.SettingScriptMode,
console.error(`用户 ${formData.userName} MAA配置错误:`, error)
message.error(`MAA配置连接失败: ${error}`)
maaWebsocketId.value = null
},
}) })
maaWebsocketId.value = subId if (response && response.websocketId) {
message.success(`已开始配置用户 ${formData.userName} 的MAA设置`) const wsId = response.websocketId
// 订阅 websocket
subscribe(wsId, {
onMessage: (wsMessage: any) => {
if (wsMessage.type === 'error') {
console.error(`用户 ${formData.Info?.Name || formData.userName} MAA配置错误:`, wsMessage.data)
message.error(`MAA配置连接失败: ${wsMessage.data}`)
unsubscribe(wsId)
maaWebsocketId.value = null
showMAAConfigMask.value = false
return
}
if (wsMessage.data && wsMessage.data.Accomplish) {
message.success(`用户 ${formData.Info?.Name || formData.userName} 的配置已完成`)
unsubscribe(wsId)
maaWebsocketId.value = null
showMAAConfigMask.value = false
}
},
})
maaWebsocketId.value = wsId
showMAAConfigMask.value = true
message.success(`已开始配置用户 ${formData.Info?.Name || formData.userName} 的MAA设置`)
// 设置 30 分钟超时自动断开
maaConfigTimeout = window.setTimeout(() => {
if (maaWebsocketId.value) {
const id = maaWebsocketId.value
unsubscribe(id)
maaWebsocketId.value = null
showMAAConfigMask.value = false
message.info(`用户 ${formData.Info?.Name || formData.userName} 的配置会话已超时断开`)
}
maaConfigTimeout = null
}, 30 * 60 * 1000)
} else {
message.error(response?.message || '启动MAA配置失败')
}
} catch (error) { } catch (error) {
console.error('MAA配置失败:', error) console.error('启动MAA配置失败:', error)
message.error('MAA配置失败') message.error('启动MAA配置失败')
} finally { } finally {
maaConfigLoading.value = false maaConfigLoading.value = false
} }
} }
const handleSaveMAAConfig = async () => {
try {
const websocketId = maaWebsocketId.value
if (!websocketId) {
message.error('未找到活动的配置会话')
return
}
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
if (response && response.code === 200) {
unsubscribe(websocketId)
maaWebsocketId.value = null
showMAAConfigMask.value = false
if (maaConfigTimeout) {
window.clearTimeout(maaConfigTimeout)
maaConfigTimeout = null
}
message.success('用户的配置已保存')
} else {
message.error(response.message || '保存配置失败')
}
} catch (error) {
console.error('保存MAA配置失败:', error)
message.error('保存MAA配置失败')
}
}
// 验证关卡名称格式 // 验证关卡名称格式
const validateStageName = (stageName: string): boolean => { const validateStageName = (stageName: string): boolean => {
if (!stageName || !stageName.trim()) { if (!stageName || !stageName.trim()) {
@@ -1020,4 +1117,55 @@ onMounted(() => {
max-width: 100%; max-width: 100%;
} }
} }
/* MAA 配置遮罩样式(与 Scripts.vue 一致,用于全局覆盖) */
.maa-config-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.mask-content {
background: var(--ant-color-bg-elevated);
border-radius: 8px;
padding: 24px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
border: 1px solid var(--ant-color-border);
}
.mask-icon {
margin-bottom: 16px;
}
.mask-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px;
color: var(--ant-color-text);
}
.mask-description {
font-size: 14px;
color: var(--ant-color-text-secondary);
margin: 0 0 24px;
line-height: 1.5;
}
.mask-actions {
display: flex;
justify-content: center;
}
</style> </style>

View File

@@ -112,43 +112,7 @@
</div> </div>
</a-modal> </a-modal>
<!-- 电源操作倒计时全屏弹窗 --> <!-- 电源操作倒计时弹窗已移至全局组件 GlobalPowerCountdown.vue -->
<div v-if="powerCountdownVisible" class="power-countdown-overlay">
<div class="power-countdown-container">
<div class="countdown-content">
<div class="warning-icon"></div>
<h2 class="countdown-title">{{ powerCountdownData.title || `${getPowerActionText(powerAction)}倒计时` }}</h2>
<p class="countdown-message">
{{ powerCountdownData.message || `程序将在倒计时结束后执行 ${getPowerActionText(powerAction)} 操作` }}
</p>
<div class="countdown-timer" v-if="powerCountdownData.countdown !== undefined">
<span class="countdown-number">{{ powerCountdownData.countdown }}</span>
<span class="countdown-unit"></span>
</div>
<div class="countdown-timer" v-else>
<span class="countdown-text">等待后端倒计时...</span>
</div>
<a-progress
v-if="powerCountdownData.countdown !== undefined"
:percent="Math.max(0, Math.min(100, (60 - powerCountdownData.countdown) / 60 * 100))"
:show-info="false"
:stroke-color="(powerCountdownData.countdown || 0) <= 10 ? '#ff4d4f' : '#1890ff'"
:stroke-width="8"
class="countdown-progress"
/>
<div class="countdown-actions">
<a-button
type="primary"
size="large"
@click="cancelPowerAction"
class="cancel-button"
>
取消操作
</a-button>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -346,127 +310,7 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
/* 电源操作倒计时全屏弹窗样式 */ /* 电源操作倒计时弹窗样式已移至 GlobalPowerCountdown.vue */
.power-countdown-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-out;
}
.power-countdown-container {
background: var(--ant-color-bg-container);
border-radius: 16px;
padding: 48px;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 500px;
width: 90%;
animation: slideIn 0.3s ease-out;
}
.countdown-content .warning-icon {
font-size: 64px;
margin-bottom: 24px;
display: block;
animation: pulse 2s infinite;
}
.countdown-title {
font-size: 28px;
font-weight: 600;
color: var(--ant-color-text);
margin: 0 0 16px 0;
}
.countdown-message {
font-size: 16px;
color: var(--ant-color-text-secondary);
margin: 0 0 32px 0;
line-height: 1.5;
}
.countdown-timer {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 32px;
}
.countdown-number {
font-size: 72px;
font-weight: 700;
color: var(--ant-color-primary);
line-height: 1;
margin-right: 8px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.countdown-unit {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-text {
font-size: 24px;
color: var(--ant-color-text-secondary);
font-weight: 500;
}
.countdown-progress {
margin-bottom: 32px;
}
.countdown-actions {
display: flex;
justify-content: center;
}
.cancel-button {
padding: 12px 32px;
height: auto;
font-size: 16px;
font-weight: 500;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* 响应式 - 移动端适配 */ /* 响应式 - 移动端适配 */
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -499,27 +343,6 @@ onUnmounted(() => {
width: 100%; width: 100%;
} }
/* 移动端倒计时弹窗适配 */ /* 移动端倒计时弹窗适配已移至 GlobalPowerCountdown.vue */
.power-countdown-container {
padding: 32px 24px;
margin: 16px;
}
.countdown-title {
font-size: 24px;
}
.countdown-number {
font-size: 56px;
}
.countdown-unit {
font-size: 20px;
}
.countdown-content .warning-icon {
font-size: 48px;
margin-bottom: 16px;
}
} }
</style> </style>

View File

@@ -99,7 +99,11 @@ export function handleMainMessage(wsMessage: any) {
if (!wsMessage || typeof wsMessage !== 'object') return if (!wsMessage || typeof wsMessage !== 'object') return
const { type, data } = wsMessage const { type, data } = wsMessage
try { try {
if (type === 'Message' && data && data.type === 'Countdown') { if (type === 'Signal' && data && data.RequestClose) {
// 处理后端请求前端关闭的信号
console.log('收到后端关闭请求,开始执行应用自杀...')
handleRequestClose()
} else if (type === 'Message' && data && data.type === 'Countdown') {
// 存储倒计时消息,供 UI 回放 // 存储倒计时消息,供 UI 回放
storePendingCountdown(data) storePendingCountdown(data)
if (uiHooks.onCountdown) { if (uiHooks.onCountdown) {
@@ -118,6 +122,40 @@ export function handleMainMessage(wsMessage: any) {
} }
} }
// 处理后端请求关闭的函数
async function handleRequestClose() {
try {
console.log('开始执行前端自杀流程...')
// 使用更激进的强制退出方法
if (window.electronAPI?.forceExit) {
console.log('执行强制退出...')
await window.electronAPI.forceExit()
} else if (window.electronAPI?.windowClose) {
// 备用方法:先尝试正常关闭
console.log('执行窗口关闭...')
await window.electronAPI.windowClose()
setTimeout(async () => {
if (window.electronAPI?.appQuit) {
await window.electronAPI.appQuit()
}
}, 500)
} else {
// 最后的备用方法
console.log('使用页面重载作为最后手段...')
window.location.reload()
}
} catch (error) {
console.error('执行自杀流程失败:', error)
// 如果所有方法都失败,尝试页面重载
try {
window.location.reload()
} catch (e) {
console.error('页面重载也失败:', e)
}
}
}
// UI 在挂载时调用,消费并回放 pending 数据 // UI 在挂载时调用,消费并回放 pending 数据
export function consumePendingTabIds(): string[] { export function consumePendingTabIds(): string[] {
return popPendingTabs() return popPendingTabs()

View File

@@ -8,7 +8,6 @@ import schedulerHandlers from './schedulerHandlers'
import type { ComboBoxItem } from '@/api/models/ComboBoxItem' import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
import type { QueueItem, Script } from './schedulerConstants' import type { QueueItem, Script } from './schedulerConstants'
import { import {
getPowerActionText,
type SchedulerTab, type SchedulerTab,
type TaskMessage, type TaskMessage,
type SchedulerStatus, type SchedulerStatus,
@@ -94,13 +93,15 @@ export function useSchedulerLogic() {
// 电源操作 - 从本地存储加载或使用默认值 // 电源操作 - 从本地存储加载或使用默认值
const powerAction = ref<PowerIn.signal>(loadPowerActionFromStorage()) const powerAction = ref<PowerIn.signal>(loadPowerActionFromStorage())
// 注意:电源倒计时弹窗已移至全局组件 GlobalPowerCountdown.vue
// 这里保留引用以避免破坏现有代码,但实际功能由全局组件处理
const powerCountdownVisible = ref(false) const powerCountdownVisible = ref(false)
const powerCountdownData = ref<{ const powerCountdownData = ref<{
title?: string title?: string
message?: string message?: string
countdown?: number countdown?: number
}>({}) }>({})
// 前端自己的60秒倒计时 // 前端自己的60秒倒计时 - 已移至全局组件
let powerCountdownTimer: ReturnType<typeof setInterval> | null = null let powerCountdownTimer: ReturnType<typeof setInterval> | null = null
// 消息弹窗 // 消息弹窗
@@ -508,10 +509,10 @@ export function useSchedulerLogic() {
} }
const handleMessageDialog = (tab: SchedulerTab, data: any) => { const handleMessageDialog = (tab: SchedulerTab, data: any) => {
// 处理倒计时消息 // 处理倒计时消息 - 已移至全局组件处理
if (data.type === 'Countdown') { if (data.type === 'Countdown') {
console.log('[Scheduler] 收到倒计时消息,启动60秒倒计时:', data) console.log('[Scheduler] 收到倒计时消息,由全局组件处理:', data)
startPowerCountdown(data) // 不再在调度中心处理倒计时,由 GlobalPowerCountdown 组件处理
return return
} }
@@ -654,68 +655,16 @@ export function useSchedulerLogic() {
console.log('[Scheduler] 电源操作显示已更新为:', newPowerAction) console.log('[Scheduler] 电源操作显示已更新为:', newPowerAction)
} }
// 启动60秒倒计时 // 启动60秒倒计时 - 已移至全局组件,这里保留空函数避免破坏现有代码
const startPowerCountdown = (data: any) => { // 移除自动执行电源操作,由后端完全控制
// 清除之前的计时器
if (powerCountdownTimer) {
clearInterval(powerCountdownTimer)
powerCountdownTimer = null
}
// 显示倒计时弹窗
powerCountdownVisible.value = true
// 设置倒计时数据从60秒开始
powerCountdownData.value = {
title: data.title || `${getPowerActionText(powerAction.value)}倒计时`,
message: data.message || `程序将在倒计时结束后执行 ${getPowerActionText(powerAction.value)} 操作`,
countdown: 60
}
// 启动每秒倒计时
powerCountdownTimer = setInterval(() => {
if (powerCountdownData.value.countdown && powerCountdownData.value.countdown > 0) {
powerCountdownData.value.countdown--
console.log('[Scheduler] 倒计时:', powerCountdownData.value.countdown)
// 倒计时结束
if (powerCountdownData.value.countdown <= 0) {
if (powerCountdownTimer) {
clearInterval(powerCountdownTimer)
powerCountdownTimer = null
}
powerCountdownVisible.value = false
console.log('[Scheduler] 倒计时结束,弹窗关闭')
}
}
}, 1000)
}
// 移除自动执行电源操作,由后端完全控制
// const executePowerAction = async () => { // const executePowerAction = async () => {
// // 不再自己执行电源操作,完全由后端控制 // // 不再自己执行电源操作,完全由后端控制
// } // }
const cancelPowerAction = async () => { const cancelPowerAction = async () => {
// 清除倒计时器 console.log('[Scheduler] cancelPowerAction 已移至全局组件,调度中心不再处理')
if (powerCountdownTimer) { // 电源操作取消功能已移至 GlobalPowerCountdown 组件
clearInterval(powerCountdownTimer) // 这里保留空函数以避免破坏现有的调用代码
powerCountdownTimer = null
}
// 关闭倒计时弹窗
powerCountdownVisible.value = false
// 调用取消电源操作的API
try {
await Service.cancelPowerTaskApiDispatchCancelPowerPost()
message.success('已取消电源操作')
} catch (error) {
console.error('取消电源操作失败:', error)
message.error('取消电源操作失败')
}
// 注意:这里不重置 powerAction保留用户选择
} }
// 移除自动检查任务完成的逻辑,完全由后端控制 // 移除自动检查任务完成的逻辑,完全由后端控制
@@ -788,8 +737,8 @@ export function useSchedulerLogic() {
}, },
onCountdown: (data) => { onCountdown: (data) => {
try { try {
// 直接启动前端倒计时 // 倒计时已移至全局组件处理,这里不再处理
startPowerCountdown(data) console.log('[Scheduler] 倒计时消息由全局组件处理:', data)
} catch (e) { } catch (e) {
console.warn('[Scheduler] registerSchedulerUI onCountdown error:', e) console.warn('[Scheduler] registerSchedulerUI onCountdown error:', e)
} }
@@ -814,7 +763,8 @@ export function useSchedulerLogic() {
const pendingCountdown = schedulerHandlers.consumePendingCountdown() const pendingCountdown = schedulerHandlers.consumePendingCountdown()
if (pendingCountdown) { if (pendingCountdown) {
try { try {
startPowerCountdown(pendingCountdown) // 倒计时已移至全局组件处理,这里不再处理
console.log('[Scheduler] 待处理倒计时消息由全局组件处理:', pendingCountdown)
} catch (e) { } catch (e) {
console.warn('[Scheduler] replay pending countdown error:', e) console.warn('[Scheduler] replay pending countdown error:', e)
} }
@@ -843,10 +793,17 @@ export function useSchedulerLogic() {
const { type, data } = wsMessage const { type, data } = wsMessage
console.log('[Scheduler] 收到Main消息:', { type, data }) console.log('[Scheduler] 收到Main消息:', { type, data })
// 首先调用 schedulerHandlers 的处理函数,确保 RequestClose 等信号被正确处理
try {
schedulerHandlers.handleMainMessage(wsMessage)
} catch (e) {
console.warn('[Scheduler] schedulerHandlers.handleMainMessage error:', e)
}
if (type === 'Message' && data && data.type === 'Countdown') { if (type === 'Message' && data && data.type === 'Countdown') {
// 收到倒计时消息,启动前端60秒倒计时 // 收到倒计时消息,由全局组件处理
console.log('[Scheduler] 收到倒计时消息,启动前端60秒倒计时:', data) console.log('[Scheduler] 收到倒计时消息,由全局组件处理:', data)
startPowerCountdown(data) // 不再在调度中心处理倒计时
} else if (type === 'Update' && data && data.PowerSign !== undefined) { } else if (type === 'Update' && data && data.PowerSign !== undefined) {
// 收到电源操作更新消息,更新显示 // 收到电源操作更新消息,更新显示
console.log('[Scheduler] 收到电源操作更新消息:', data.PowerSign) console.log('[Scheduler] 收到电源操作更新消息:', data.PowerSign)
@@ -856,7 +813,7 @@ export function useSchedulerLogic() {
// 清理函数 // 清理函数
const cleanup = () => { const cleanup = () => {
// 清理倒计时器 // 清理倒计时器 - 已移至全局组件,这里保留以避免错误
if (powerCountdownTimer) { if (powerCountdownTimer) {
clearInterval(powerCountdownTimer) clearInterval(powerCountdownTimer)
powerCountdownTimer = null powerCountdownTimer = null

View File

@@ -1,19 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
const { const props = defineProps<{
goToLogs,
openDevTools,
mirrorConfigStatus,
refreshingConfig,
refreshMirrorConfig,
goToMirrorTest
} = defineProps<{
goToLogs: () => void goToLogs: () => void
openDevTools: () => void openDevTools: () => void
mirrorConfigStatus: { isUsingCloudConfig: boolean; version: string; lastUpdated: string; source: 'cloud' | 'fallback' }
refreshingConfig: boolean refreshingConfig: boolean
refreshMirrorConfig: () => Promise<void> refreshMirrorConfig: () => Promise<void>
goToMirrorTest: () => void goToMirrorTest: () => void
}>() }>()
const { goToLogs, openDevTools, refreshingConfig, refreshMirrorConfig, goToMirrorTest } = props
</script> </script>
<template> <template>
<div class="tab-content"> <div class="tab-content">

View File

@@ -2,19 +2,23 @@
import { QuestionCircleOutlined } from '@ant-design/icons-vue' import { QuestionCircleOutlined } from '@ant-design/icons-vue'
import type { SettingsData } from '@/types/settings' import type { SettingsData } from '@/types/settings'
const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, testingNotify } = defineProps<{ const props = defineProps<{
settings: SettingsData settings: SettingsData
sendTaskResultTimeOptions: { label: string; value: string }[] sendTaskResultTimeOptions: { label: string; value: string }[]
handleSettingChange: (category: keyof SettingsData, key: string, value: any) => Promise<void> handleSettingChange: (category: keyof SettingsData, key: string, value: any) => Promise<void>
testNotify: () => Promise<void> testNotify: () => Promise<void>
testingNotify: boolean testingNotify: boolean
}>() }>()
const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, testingNotify } = props
</script> </script>
<template> <template>
<div class="tab-content"> <div class="tab-content">
<div class="form-section"> <div class="form-section">
<div class="section-header"> <div class="section-header">
<h3>通知内容</h3> <h3>通知内容</h3>
<a-button type="primary" :loading="testingNotify" @click="testNotify" size="small" class="section-update-button primary-style">发送测试通知</a-button>
</div> </div>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="8"> <a-col :span="8">
@@ -319,17 +323,73 @@ const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, te
</a-row> </a-row>
</div> </div>
<div class="form-section"> <!-- 测试按钮已移至通知内容标题右侧 -->
<div class="section-header">
<h3>通知测试</h3>
</div>
<a-row :gutter="24">
<a-col :span="24">
<a-space>
<a-button type="primary" :loading="testingNotify" @click="testNotify">发送测试通知</a-button>
</a-space>
</a-col>
</a-row>
</div>
</div> </div>
</template> </template>
<style scoped>
/* Header layout */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Doc link and header action parity */
.section-header .section-update-button {
/* Apply doc-link visual tokens to the local update button only.
Do NOT touch global .section-doc-link so the real doc button remains unchanged. */
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.18s ease;
display: inline-flex;
align-items: center;
gap: 4px;
line-height: 1;
}
.section-header .section-update-button:hover {
color: var(--ant-color-primary-hover) !important;
background-color: var(--ant-color-primary-bg);
border-color: var(--ant-color-primary-hover);
}
/* Primary gradient style for the update button */
.section-header .section-update-button.primary-style {
/* Keep gradient but match doc-link height/rounded corners for parity */
height: 32px;
padding: 4px 8px; /* same vertical padding as doc-link */
font-size: 14px; /* same as doc-link for visual parity */
font-weight: 500;
border-radius: 4px; /* same radius as doc-link */
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.18);
transition: transform 0.16s ease, box-shadow 0.16s ease;
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover)) !important;
border: 1px solid var(--ant-color-primary) !important; /* subtle border to match doc-link rhythm */
color: #fff !important;
}
.section-header .section-update-button.primary-style:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(22, 119, 255, 0.22);
}
@media (max-width: 640px) {
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.section-header .section-update-button {
margin-top: 4px;
}
}
</style>

View File

@@ -13,8 +13,8 @@ const { version, backendUpdateInfo } = defineProps<{
<div class="section-header"> <div class="section-header">
<h3>项目链接</h3> <h3>项目链接</h3>
</div> </div>
<a-row :gutter="24"> <div class="link-grid">
<a-col :span="8"> <div class="link-item">
<div class="link-card"> <div class="link-card">
<div class="link-icon"><HomeOutlined /></div> <div class="link-icon"><HomeOutlined /></div>
<div class="link-content"> <div class="link-content">
@@ -23,18 +23,18 @@ const { version, backendUpdateInfo } = defineProps<{
<a href="https://auto-mas.top" target="_blank" class="link-button">访问官网</a> <a href="https://auto-mas.top" target="_blank" class="link-button">访问官网</a>
</div> </div>
</div> </div>
</a-col> </div>
<a-col :span="8"> <div class="link-item">
<div class="link-card"> <div class="link-card">
<div class="link-icon"><GithubOutlined /></div> <div class="link-icon"><GithubOutlined /></div>
<div class="link-content"> <div class="link-content">
<h4>GitHub仓库</h4> <h4>GitHub仓库</h4>
<p>查看源代码提交issue和贡献</p> <p>查看源代码提交issue和捐赠</p>
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" target="_blank" class="link-button">访问仓库</a> <a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" target="_blank" class="link-button">访问仓库</a>
</div> </div>
</div> </div>
</a-col> </div>
<a-col :span="8"> <div class="link-item">
<div class="link-card"> <div class="link-card">
<div class="link-icon"><QqOutlined /></div> <div class="link-icon"><QqOutlined /></div>
<div class="link-content"> <div class="link-content">
@@ -43,8 +43,8 @@ const { version, backendUpdateInfo } = defineProps<{
<a href="https://qm.qq.com/q/bd9fISNoME" target="_blank" class="link-button">加入群聊</a> <a href="https://qm.qq.com/q/bd9fISNoME" target="_blank" class="link-button">加入群聊</a>
</div> </div>
</div> </div>
</a-col> </div>
</a-row> </div>
</div> </div>
<div class="form-section"> <div class="form-section">
@@ -74,3 +74,30 @@ const { version, backendUpdateInfo } = defineProps<{
</div> </div>
</div> </div>
</template> </template>
<style scoped>
/* Responsive grid for link cards: ensures cards expand to fill available width */
.link-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
align-items: stretch;
width: 100%;
}
.link-item {
display: flex;
}
/* Make sure link-card fills its grid cell */
.link-card {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.link-content {
flex: 1 1 auto;
}
</style>

View File

@@ -34,11 +34,17 @@ const version = (import.meta as any).env?.VITE_APP_VERSION || '获取版本失
const backendUpdateInfo = ref<VersionOut | null>(null) const backendUpdateInfo = ref<VersionOut | null>(null)
// 镜像配置状态 // 镜像配置状态
const mirrorConfigStatus = ref({ type MirrorConfigStatus = {
isUsingCloudConfig: boolean
version?: string
lastUpdated?: string
source: 'cloud' | 'fallback'
}
const mirrorConfigStatus = ref<MirrorConfigStatus>({
isUsingCloudConfig: false, isUsingCloudConfig: false,
version: '', version: undefined,
lastUpdated: '', lastUpdated: undefined,
source: 'fallback' as 'cloud' | 'fallback', source: 'fallback',
}) })
const refreshingConfig = ref(false) const refreshingConfig = ref(false)
@@ -324,9 +330,12 @@ onMounted(() => {
<style scoped> <style scoped>
/* 统一样式,使用 :deep 作用到子组件内部 */ /* 统一样式,使用 :deep 作用到子组件内部 */
.settings-container { .settings-container {
max-width: 1200px; /* Allow the settings page to expand with the window width */
margin: 0 auto; width: 100%;
max-width: none;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.settings-header { .settings-header {
margin-bottom: 24px; margin-bottom: 24px;
@@ -340,6 +349,7 @@ onMounted(() => {
.settings-content { .settings-content {
background: var(--ant-color-bg-container); background: var(--ant-color-bg-container);
border-radius: 12px; border-radius: 12px;
width: 100%;
} }
.settings-tabs { .settings-tabs {
margin: 0; margin: 0;
@@ -356,6 +366,7 @@ onMounted(() => {
} }
:deep(.tab-content) { :deep(.tab-content) {
padding: 24px; padding: 24px;
width: 100%;
} }
:deep(.form-section) { :deep(.form-section) {
margin-bottom: 32px; margin-bottom: 32px;
@@ -536,6 +547,7 @@ onMounted(() => {
color: #fff !important; color: #fff !important;
text-decoration: none; text-decoration: none;
} }
/* link-grid styles moved into TabOthers.vue (scoped) */
:deep(.info-item) { :deep(.info-item) {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -126,9 +126,9 @@
</div> </div>
<div class="content"> <div class="content">
<p><strong>脚本实例名称:</strong>{{ script_name }}</p> <p><strong>实例名称:</strong>{{ script_name }}</p>
<p><strong>任务开始时间:</strong>{{ start_time }}</p> <p><strong>开始时间:</strong>{{ start_time }}</p>
<p><strong>任务结束时间:</strong>{{ end_time }}</p> <p><strong>结束时间:</strong>{{ end_time }}</p>
<p><strong>已完成数:</strong>{{ completed_count }}</p> <p><strong>已完成数:</strong>{{ completed_count }}</p>
{% if uncompleted_count %} {% if uncompleted_count %}
<p><strong>未完成数:</strong>{{ uncompleted_count }}</p> <p><strong>未完成数:</strong>{{ uncompleted_count }}</p>
@@ -150,8 +150,8 @@
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a> <a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a>
</div> </div>
<div> <div>
<p><strong>文档站仓库</strong></p> <p><strong>文档站:</strong></p>
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a> <a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -166,9 +166,11 @@
</div> </div>
<div class="content"> <div class="content">
<p><strong>用户代理信息:</strong>{{ user_info }}</p> <p><strong>用户信息:</strong>{{ user_info }}</p>
<p><strong>任务开始时间:</strong>{{ start_time }}</p> <p><strong>开始时间:</strong>{{ start_time }}</p>
<p><strong>任务结束时间:</strong>{{ end_time }}</p> <p><strong>结束时间:</strong>{{ end_time }}</p>
<p><strong>理智剩余:</strong>{{ sanity }}</p>
<p><strong>回复时间:</strong>{{ sanity_full_at }}</p>
<p><strong>MAA执行结果</strong> <p><strong>MAA执行结果</strong>
{% if maa_result == '代理任务全部完成' %} {% if maa_result == '代理任务全部完成' %}
<span class="greenhighlight">{{ maa_result }}</span> <span class="greenhighlight">{{ maa_result }}</span>
@@ -223,8 +225,8 @@
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a> <a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a>
</div> </div>
<div> <div>
<p><strong>文档站仓库</strong></p> <p><strong>文档站:</strong></p>
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a> <a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -126,9 +126,9 @@
</div> </div>
<div class="content"> <div class="content">
<p><strong>脚本实例名称:</strong>{{ script_name }}</p> <p><strong>实例名称:</strong>{{ script_name }}</p>
<p><strong>任务开始时间:</strong>{{ start_time }}</p> <p><strong>开始时间:</strong>{{ start_time }}</p>
<p><strong>任务结束时间:</strong>{{ end_time }}</p> <p><strong>结束时间:</strong>{{ end_time }}</p>
<p><strong>已完成数:</strong>{{ completed_count }}</p> <p><strong>已完成数:</strong>{{ completed_count }}</p>
{% if uncompleted_count %} {% if uncompleted_count %}
<p><strong>未完成数:</strong>{{ uncompleted_count }}</p> <p><strong>未完成数:</strong>{{ uncompleted_count }}</p>
@@ -150,8 +150,8 @@
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a> <a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a>
</div> </div>
<div> <div>
<p><strong>文档站仓库</strong></p> <p><strong>文档站:</strong></p>
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a> <a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -166,10 +166,10 @@
</div> </div>
<div class="content"> <div class="content">
<p><strong>用户代理信息:</strong>{{ sub_info }}</p> <p><strong>代理信息:</strong>{{ sub_info }}</p>
<p><strong>任务开始时间:</strong>{{ start_time }}</p> <p><strong>开始时间:</strong>{{ start_time }}</p>
<p><strong>任务结束时间:</strong>{{ end_time }}</p> <p><strong>结束时间:</strong>{{ end_time }}</p>
<p><strong>脚本执行结果:</strong> <p><strong>执行结果:</strong>
{% if sub_result == '代理成功' %} {% if sub_result == '代理成功' %}
<span class="greenhighlight">{{ sub_result }}</span> <span class="greenhighlight">{{ sub_result }}</span>
{% elif sub_result == '代理失败' %} {% elif sub_result == '代理失败' %}
@@ -190,8 +190,8 @@
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a> <a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a>
</div> </div>
<div> <div>
<p><strong>文档站仓库</strong></p> <p><strong>文档站:</strong></p>
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a> <a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
</div> </div>
</div> </div>
</div> </div>