Merge branch 'feature/refactor' of ssh://ssh.github.com:443/AUTO-MAS-Project/AUTO-MAS into feature/refactor
This commit is contained in:
@@ -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"]:
|
||||||
|
|
||||||
|
|||||||
@@ -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") != ""
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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以触发更新流程,确保代码是最新的
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
307
frontend/src/components/GlobalPowerCountdown.vue
Normal file
307
frontend/src/components/GlobalPowerCountdown.vue
Normal 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>
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
6
frontend/src/types/electron.d.ts
vendored
6
frontend/src/types/electron.d.ts
vendored
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user