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"],
|
||||
}
|
||||
|
||||
general_config["Script"]["ConfigPathMode"] = (
|
||||
"File"
|
||||
if "所有文件"
|
||||
in general_config["Script"]["ConfigPathMode"]
|
||||
else "Folder"
|
||||
)
|
||||
|
||||
uid, sc = await self.add_script("General")
|
||||
script_dict[GeneralConfig.name] = str(uid)
|
||||
await sc.load(general_config)
|
||||
@@ -1061,6 +1068,8 @@ class AppConfig(GlobalConfig):
|
||||
await queue.QueueItem.remove(key)
|
||||
|
||||
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:
|
||||
"""重新排序脚本"""
|
||||
@@ -1288,6 +1297,8 @@ class AppConfig(GlobalConfig):
|
||||
if isinstance(script_config, (MaaConfig | GeneralConfig)):
|
||||
await script_config.UserData.remove(uid)
|
||||
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:
|
||||
"""重新排序用户"""
|
||||
@@ -2027,11 +2038,25 @@ class AppConfig(GlobalConfig):
|
||||
data = {
|
||||
"recruit_statistics": defaultdict(int),
|
||||
"drop_statistics": defaultdict(dict),
|
||||
"sanity": 0,
|
||||
"sanity_full_at": "",
|
||||
"maa_result": maa_result,
|
||||
}
|
||||
|
||||
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
|
||||
current_star_level = None
|
||||
@@ -2140,6 +2165,7 @@ class AppConfig(GlobalConfig):
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log_path.open("w", encoding="utf-8") as f:
|
||||
f.writelines(logs)
|
||||
# 保存统计数据
|
||||
with log_path.with_suffix(".json").open("w", encoding="utf-8") as f:
|
||||
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] += count
|
||||
|
||||
# 处理理智相关字段 - 使用最后一个文件的值
|
||||
elif key in ["sanity", "sanity_full_at"]:
|
||||
data[key] = single_data[key]
|
||||
|
||||
# 录入运行结果
|
||||
elif key in ["maa_result", "general_result"]:
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class _MainTimer:
|
||||
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
|
||||
|
||||
if (
|
||||
len(Config.if_ignore_silence) > 0
|
||||
len(Config.if_ignore_silence) == 0
|
||||
and Config.get("Function", "IfSilence")
|
||||
and Config.get("Function", "BossKey") != ""
|
||||
):
|
||||
|
||||
@@ -1122,7 +1122,7 @@ class MaaManager:
|
||||
await asyncio.sleep(self.wait_time)
|
||||
|
||||
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])
|
||||
|
||||
elif ":" in self.ADB_address:
|
||||
@@ -1933,6 +1933,8 @@ class MaaManager:
|
||||
message_text = (
|
||||
f"开始时间: {message['start_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"{recruit_text}\n"
|
||||
f"{drop_text}"
|
||||
|
||||
@@ -781,97 +781,44 @@ ipcMain.handle('check-git-update', async () => {
|
||||
GIT_ASKPASS: '',
|
||||
}
|
||||
|
||||
log.info('开始检查Git仓库更新...')
|
||||
log.info('开始检查Git仓库更新(跳过fetch,避免直接访问GitHub)...')
|
||||
|
||||
// 执行 git fetch 获取最新的远程信息
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const fetchProc = spawn(gitPath, ['fetch', 'origin'], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot,
|
||||
})
|
||||
// 不执行fetch,直接检查本地状态
|
||||
// 这样避免了直接访问GitHub,而是在后续的pull操作中使用镜像站
|
||||
|
||||
fetchProc.stdout?.on('data', data => {
|
||||
log.info('git fetch output:', data.toString())
|
||||
})
|
||||
|
||||
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'], {
|
||||
// 获取当前HEAD的commit hash
|
||||
const currentCommit = await new Promise<string>((resolve, reject) => {
|
||||
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
|
||||
stdio: 'pipe',
|
||||
env: gitEnv,
|
||||
cwd: appRoot,
|
||||
})
|
||||
|
||||
let output = ''
|
||||
statusProc.stdout?.on('data', data => {
|
||||
revParseProc.stdout?.on('data', data => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
statusProc.stderr?.on('data', data => {
|
||||
log.info('git status stderr:', data.toString())
|
||||
})
|
||||
|
||||
statusProc.on('close', code => {
|
||||
revParseProc.on('close', code => {
|
||||
if (code === 0) {
|
||||
// 检查是否有 "Your branch is behind" 的信息
|
||||
// 使用 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)
|
||||
})
|
||||
resolve(output.trim())
|
||||
} 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}`)
|
||||
return { hasUpdate }
|
||||
log.info(`当前本地commit: ${currentCommit}`)
|
||||
|
||||
// 由于我们跳过了fetch步骤(避免直接访问GitHub),
|
||||
// 我们无法准确知道远程是否有更新
|
||||
// 因此返回true,让后续的pull操作通过镜像站来检查和获取更新
|
||||
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
||||
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
||||
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
||||
|
||||
} catch (error) {
|
||||
log.error('检查Git更新失败:', error)
|
||||
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function cloneBackend(
|
||||
|
||||
// ==== 下面是关键逻辑 ====
|
||||
if (isGitRepository(backendPath)) {
|
||||
// 已是 git 仓库,直接 pull
|
||||
// 已是 git 仓库,先更新远程URL为镜像站,然后 pull
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
type: 'backend',
|
||||
@@ -216,6 +216,24 @@ export async function cloneBackend(
|
||||
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) => {
|
||||
const proc = spawn(gitPath, ['pull'], { stdio: 'pipe', env: gitEnv, cwd: backendPath })
|
||||
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 UpdateModal from './components/UpdateModal.vue'
|
||||
import DevDebugPanel from './components/DevDebugPanel.vue'
|
||||
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
@@ -49,6 +50,9 @@ onMounted(() => {
|
||||
|
||||
<!-- 开发环境调试面板 -->
|
||||
<DevDebugPanel />
|
||||
|
||||
<!-- 全局电源倒计时弹窗 -->
|
||||
<GlobalPowerCountdown />
|
||||
</ConfigProvider>
|
||||
</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>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出日志(txt格式)
|
||||
导出日志(log格式)
|
||||
</a-button>
|
||||
<!-- <a-button @click="scrollToBottom" :disabled="!logs">-->
|
||||
<!-- <template #icon><DownOutlined /></template>-->
|
||||
@@ -285,7 +285,7 @@ const exportLogs = async () => {
|
||||
} else {
|
||||
fileName = `logs_${new Date().toISOString().slice(0, 10)}`
|
||||
}
|
||||
a.download = `${fileName}.txt`
|
||||
a.download = `${fileName}.log`
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 type { UpdateCheckOut } 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>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
|
||||
@@ -16,7 +16,12 @@ export interface ElectronAPI {
|
||||
|
||||
// 重启为管理员
|
||||
restartAsAdmin: () => Promise<void>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
// 环境检查
|
||||
checkEnvironment: () => Promise<{
|
||||
pythonExists: boolean
|
||||
|
||||
@@ -647,7 +647,7 @@
|
||||
</template>
|
||||
|
||||
<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 type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
@@ -894,6 +894,9 @@ const updatePathsBasedOnRoot = (newRootPath: string) => {
|
||||
const pageLoading = ref(false)
|
||||
const scriptId = route.params.id as string
|
||||
|
||||
// 在初始化(从接口加载数据)期间阻止某些 watcher 生效
|
||||
const isInitializing = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
type: 'General' as ScriptType,
|
||||
@@ -945,26 +948,36 @@ const rules = {
|
||||
type: [{ required: true, message: '请选择脚本类型', trigger: 'change' }],
|
||||
}
|
||||
|
||||
// 监听配置文件类型变化,重置路径为根目录
|
||||
watch(
|
||||
() => generalConfig.Script.ConfigPathMode,
|
||||
(newMode, oldMode) => {
|
||||
if (newMode !== oldMode && generalConfig.Script.ConfigPath && generalConfig.Script.ConfigPath !== '.') {
|
||||
// 当配置文件类型改变时,重置为根目录路径
|
||||
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},请重新选择路径`)
|
||||
// 延迟注册 ConfigPathMode watcher(在加载脚本并完成初始化后再注册)
|
||||
let stopConfigPathModeWatcher: (() => void) | null = null
|
||||
|
||||
const setupConfigPathModeWatcher = () => {
|
||||
// 如果已存在 watcher,先停止
|
||||
if (stopConfigPathModeWatcher) {
|
||||
stopConfigPathModeWatcher()
|
||||
stopConfigPathModeWatcher = null
|
||||
}
|
||||
|
||||
stopConfigPathModeWatcher = watch(
|
||||
() => generalConfig.Script.ConfigPathMode,
|
||||
(newMode, oldMode) => {
|
||||
if (newMode !== oldMode && generalConfig.Script.ConfigPath && generalConfig.Script.ConfigPath !== '.') {
|
||||
// 当配置文件类型改变时,重置为根目录路径
|
||||
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(
|
||||
@@ -987,9 +1000,13 @@ watch(
|
||||
|
||||
onMounted(async () => {
|
||||
await loadScript()
|
||||
// 在脚本加载完成并完成初始化后,再注册 ConfigPathMode 的 watcher,避免初始化阶段触发重置逻辑
|
||||
setupConfigPathModeWatcher()
|
||||
})
|
||||
|
||||
const loadScript = async () => {
|
||||
// 标记正在初始化,阻止某些 watcher 在赋值时触发
|
||||
isInitializing.value = true
|
||||
pageLoading.value = true
|
||||
try {
|
||||
// 检查是否有通过路由状态传递的数据(新建脚本时)
|
||||
@@ -1030,6 +1047,10 @@ const loadScript = async () => {
|
||||
router.push('/scripts')
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
// 初始化完成,等待一次 nextTick 以确保所有由赋值触发的 watcher
|
||||
// 在 isInitializing 为 true 时被调度并能正确跳过,然后再清除初始化标志
|
||||
await nextTick()
|
||||
isInitializing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,33 @@
|
||||
</a-space>
|
||||
</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">
|
||||
<a-card class="config-card">
|
||||
<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 { useScriptApi } from '@/composables/useScriptApi'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
import { Service } from '@/api'
|
||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -376,6 +405,8 @@ const scriptName = ref('')
|
||||
// 通用配置相关
|
||||
const generalConfigLoading = ref(false)
|
||||
const generalWebsocketId = ref<string | null>(null)
|
||||
const showGeneralConfigMask = ref(false)
|
||||
let generalConfigTimeout: number | null = null
|
||||
|
||||
// 通用脚本默认用户数据
|
||||
const getDefaultGeneralUserData = () => ({
|
||||
@@ -556,31 +587,106 @@ const handleGeneralConfig = async () => {
|
||||
try {
|
||||
generalConfigLoading.value = true
|
||||
|
||||
// 先立即显示遮罩以避免后端延迟导致无法感知
|
||||
showGeneralConfigMask.value = true
|
||||
|
||||
// 如果已有连接,先断开并清理
|
||||
if (generalWebsocketId.value) {
|
||||
unsubscribe(generalWebsocketId.value)
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
if (generalConfigTimeout) {
|
||||
window.clearTimeout(generalConfigTimeout)
|
||||
generalConfigTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
const subId = userId
|
||||
|
||||
subscribe(subId, {
|
||||
onError: error => {
|
||||
console.error(`用户 ${formData.userName} 通用配置错误:`, error)
|
||||
message.error(`通用配置连接失败: ${error}`)
|
||||
generalWebsocketId.value = null
|
||||
}
|
||||
// 调用后端启动任务接口,传入 userId 作为 taskId 与设置模式
|
||||
const response = await Service.addTaskApiDispatchStartPost({
|
||||
taskId: userId,
|
||||
mode: TaskCreateIn.mode.SettingScriptMode,
|
||||
})
|
||||
|
||||
generalWebsocketId.value = subId
|
||||
message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
|
||||
console.debug('通用配置 start 接口返回:', response)
|
||||
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) {
|
||||
console.error('通用配置失败:', error)
|
||||
message.error('通用配置失败')
|
||||
console.error('启动通用配置失败:', error)
|
||||
message.error('启动通用配置失败')
|
||||
} finally {
|
||||
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 () => {
|
||||
try {
|
||||
@@ -622,6 +728,11 @@ const handleCancel = () => {
|
||||
if (generalWebsocketId.value) {
|
||||
unsubscribe(generalWebsocketId.value)
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
if (generalConfigTimeout) {
|
||||
window.clearTimeout(generalConfigTimeout)
|
||||
generalConfigTimeout = null
|
||||
}
|
||||
}
|
||||
router.push('/scripts')
|
||||
}
|
||||
@@ -829,4 +940,55 @@ onMounted(() => {
|
||||
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>
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 详细筛选条件 -->
|
||||
<a-row :gutter="16" align="middle">
|
||||
<a-row :gutter="16" :align="'middle'">
|
||||
<a-col :span="6">
|
||||
<a-form-item label="合并模式" style="margin-bottom: 0">
|
||||
<a-select v-model:value="searchForm.mode" style="width: 100%">
|
||||
@@ -277,43 +277,54 @@
|
||||
<a-card size="small" title="详细日志" class="log-card">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-tooltip title="打开日志文件">
|
||||
<a-tooltip title="打开日志文件" :getPopupContainer="tooltipContainer">
|
||||
<a-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="!currentJsonFile"
|
||||
@click="handleOpenLogFile"
|
||||
:class="{ 'no-hover-shift': true }"
|
||||
:style="buttonFixedStyle"
|
||||
>
|
||||
<template #icon>
|
||||
<FileOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="打开日志文件所在目录">
|
||||
<a-tooltip title="打开日志文件所在目录" :getPopupContainer="tooltipContainer">
|
||||
<a-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="!currentJsonFile"
|
||||
@click="handleOpenLogDirectory"
|
||||
:class="{ 'no-hover-shift': true }"
|
||||
:style="buttonFixedStyle"
|
||||
>
|
||||
<template #icon>
|
||||
<FolderOpenOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</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>
|
||||
</template>
|
||||
<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>
|
||||
</div>
|
||||
<div v-else class="no-log">
|
||||
<a-empty
|
||||
description="未选择日志,请从左边记录条目中选择"
|
||||
:image="NodataImage"
|
||||
:image-style="{
|
||||
height: '60px',
|
||||
}"
|
||||
:image-style="{ height: '60px' }"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
@@ -341,7 +352,7 @@ import {
|
||||
FileOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Service } from '@/api/services/Service'
|
||||
import type { HistorySearchIn, HistoryData } from '@/api'
|
||||
import { HistorySearchIn, type HistoryData } from '@/api' // 调整:枚举需要值导入
|
||||
import dayjs from 'dayjs'
|
||||
import NodataImage from '@/assets/NoData.png'
|
||||
|
||||
@@ -358,62 +369,20 @@ const selectedRecordIndex = ref(-1)
|
||||
const currentDetail = ref<HistoryData | null>(null)
|
||||
const currentJsonFile = ref('')
|
||||
|
||||
// 快捷时间选择预设
|
||||
// 快捷时间选择预设(改用枚举值)
|
||||
const timePresets = [
|
||||
{
|
||||
key: 'today',
|
||||
label: '今天',
|
||||
startDate: () => dayjs().format('YYYY-MM-DD'),
|
||||
endDate: () => dayjs().format('YYYY-MM-DD'),
|
||||
mode: '按日合并' as HistorySearchIn.mode,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{ key: 'today', label: '今天', startDate: () => dayjs().format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
|
||||
{ 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 },
|
||||
{ key: 'week', label: '最近一周', startDate: () => dayjs().subtract(7, 'day').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
|
||||
{ key: 'month', label: '最近一个月', startDate: () => dayjs().subtract(1, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
|
||||
{ key: 'twoMonths', label: '最近两个月', startDate: () => dayjs().subtract(2, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
// 搜索表单(默认按日合并)
|
||||
const searchForm = reactive({
|
||||
mode: '按日合并' as HistorySearchIn.mode,
|
||||
mode: HistorySearchIn.mode.DAILY as HistorySearchIn.mode,
|
||||
startDate: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
})
|
||||
@@ -426,37 +395,6 @@ interface 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(() => {
|
||||
if (selectedRecordIndex.value >= 0 && currentDetail.value) {
|
||||
@@ -523,7 +461,7 @@ const handleSearch = async () => {
|
||||
|
||||
// 重置搜索条件
|
||||
const handleReset = () => {
|
||||
searchForm.mode = '按日合并'
|
||||
searchForm.mode = HistorySearchIn.mode.DAILY
|
||||
searchForm.startDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD')
|
||||
searchForm.endDate = dayjs().format('YYYY-MM-DD')
|
||||
historyData.value = []
|
||||
@@ -546,12 +484,12 @@ const handleDateChange = () => {
|
||||
currentPreset.value = ''
|
||||
}
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD>择用户处理
|
||||
// 选择用户处理(修正乱码注释)
|
||||
const handleSelectUser = async (date: string, username: string, userData: HistoryData) => {
|
||||
selectedUser.value = `${date}-${username}`
|
||||
selectedUserData.value = userData
|
||||
selectedRecordIndex.value = -1 // 重置记录选择
|
||||
currentDetail.value = null // 清空日志内容
|
||||
selectedRecordIndex.value = -1
|
||||
currentDetail.value = null
|
||||
currentJsonFile.value = ''
|
||||
}
|
||||
|
||||
@@ -655,15 +593,14 @@ const handleOpenLogDirectory = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取日期状态颜色
|
||||
const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
const hasError = Object.values(users).some(
|
||||
user =>
|
||||
user.index?.some(item => item.status === '异常') ||
|
||||
(user.error_info && Object.keys(user.error_info).length > 0)
|
||||
)
|
||||
return hasError ? 'error' : 'success'
|
||||
}
|
||||
// 日志字体大小(恢复)
|
||||
const logFontSize = ref(14)
|
||||
const logFontSizeOptions = [12, 13, 14, 16, 18, 20]
|
||||
|
||||
// Tooltip 容器:避免挂载到 body 造成全局滚动条闪烁与布局抖动
|
||||
const tooltipContainer = (triggerNode: HTMLElement) => triggerNode?.parentElement || document.body
|
||||
// 固定 button 尺寸,避免 hover/tooltip 状态导致宽度高度微调
|
||||
const buttonFixedStyle = { width: '28px', height: '28px', padding: 0 }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -681,11 +618,6 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 32px;
|
||||
color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
@@ -701,8 +633,9 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
.history-content { /* 避免 tooltip 在局部弹出时引起外层出现滚动条 */
|
||||
height: calc(80vh - 200px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -726,21 +659,6 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -785,7 +703,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: var(--ant-color-bg-container-disabled);
|
||||
background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
|
||||
border-color: var(--ant-color-border);
|
||||
}
|
||||
|
||||
@@ -805,17 +723,11 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 右侧详情区域 */
|
||||
.detail-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
@@ -826,6 +738,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--ant-color-bg-container);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
@@ -833,16 +746,18 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
min-width: 0; /* 确保子项 flex:1 时可以收缩 */
|
||||
overflow: hidden; /* 避免被长行撑出 */
|
||||
}
|
||||
|
||||
/* 记录条目区域 */
|
||||
.records-area {
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1; /* 新增: 允许一定程度收缩 */
|
||||
min-width: 260px; /* 给一个合理下限 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.records-section {
|
||||
@@ -883,7 +798,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
}
|
||||
|
||||
.record-item:hover {
|
||||
background: var(--ant-color-bg-container-disabled);
|
||||
background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
|
||||
}
|
||||
|
||||
.record-item.active {
|
||||
@@ -968,19 +883,11 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.error-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
border: 1px solid var(--ant-color-error-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 日志区域 */
|
||||
.log-area {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
/* 允许在父级 flex 宽度不足时压缩,避免整体被撑出视口 */
|
||||
min-width: 0; /* 修改: 原来是 300px,导致在内容渲染后无法收缩 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1004,30 +911,34 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
flex: 1;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
background: var(--ant-color-bg-layout);
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
/* 新增: 防止超长无空格字符串把容器撑宽 */
|
||||
overflow-x: auto; /* 横向单独滚动,而不是撑出布局 */
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.log-content pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
max-width: 100%;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.no-log {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 500px;
|
||||
/* 恢复字体选择器样式 */
|
||||
.log-font-size-select :deep(.ant-select-selector) {
|
||||
padding: 0 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
/* 移除未使用 .title-icon */
|
||||
/* 移除 unused overview-section / overview-card / overview-stats / user-status / error-section / error-card */
|
||||
.default {
|
||||
border-color: var(--ant-color-border);
|
||||
color: var(--ant-color-text);
|
||||
@@ -1038,6 +949,22 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
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) {
|
||||
.history-layout {
|
||||
@@ -1055,52 +982,22 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||
|
||||
.log-area {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 带tooltip的错误tag样式 */
|
||||
.error-tag-with-tooltip {
|
||||
cursor: help;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-tag-with-tooltip:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 统计数据标题样式 */
|
||||
.stat-subtitle {
|
||||
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);
|
||||
/* 针对极窄窗口再降级为纵向布局,提前触发布局切换,避免出现水平滚动 */
|
||||
@media (max-width: 1000px) {
|
||||
.history-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.records-area {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.log-area {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -191,55 +191,45 @@ async function checkEnvironment() {
|
||||
console.log('- main.py存在:', criticalFiles.mainPyExists)
|
||||
console.log('- 所有关键文件存在:', allExeFilesExist)
|
||||
|
||||
// 新的自动模式判断逻辑:只要所有关键exe文件都存在且不是第一次启动就进入自动模式
|
||||
console.log('自动模式判断条件:')
|
||||
console.log('- 不是第一次启动:', !isFirst)
|
||||
// 页面模式判断逻辑:
|
||||
// 1. 第一次启动 -> 直接进入手动模式
|
||||
// 2. 非第一次启动 + 文件完整 -> 自动模式
|
||||
// 3. 非第一次启动 + 文件缺失 -> 环境不完整页面
|
||||
console.log('页面模式判断条件:')
|
||||
console.log('- 是否第一次启动:', isFirst)
|
||||
console.log('- 所有关键文件存在:', allExeFilesExist)
|
||||
|
||||
// 只要不是第一次启动且所有关键exe文件都存在就进入自动模式
|
||||
if (!isFirst && allExeFilesExist) {
|
||||
// 第一次启动时,无论文件是否存在都直接进入手动模式
|
||||
if (isFirst) {
|
||||
console.log('第一次启动,直接进入手动模式')
|
||||
autoMode.value = false
|
||||
showEnvironmentIncomplete.value = false
|
||||
} else if (allExeFilesExist) {
|
||||
// 不是第一次启动且所有关键exe文件都存在,进入自动模式
|
||||
console.log('进入自动模式,开始自动启动流程')
|
||||
autoMode.value = true
|
||||
showEnvironmentIncomplete.value = false
|
||||
} else {
|
||||
console.log('进入手动模式')
|
||||
if (isFirst) {
|
||||
console.log('原因: 第一次启动')
|
||||
// 第一次启动直接进入手动模式
|
||||
autoMode.value = false
|
||||
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)
|
||||
// 不是第一次启动但关键文件缺失,显示环境不完整页面
|
||||
console.log('环境损坏,显示环境不完整页面')
|
||||
console.log(' - python.exe缺失:', !criticalFiles.pythonExists)
|
||||
console.log(' - git.exe缺失:', !criticalFiles.gitExists)
|
||||
console.log(' - main.py缺失:', !criticalFiles.mainPyExists)
|
||||
|
||||
// 检查是否应该显示环境不完整页面(仅在自动模式下)
|
||||
// 如果不是第一次启动且关键文件缺失,说明之前是自动模式但现在环境有问题
|
||||
if (!isFirst) {
|
||||
const missing = []
|
||||
if (!criticalFiles.pythonExists) missing.push('Python 环境')
|
||||
if (!criticalFiles.gitExists) missing.push('Git 工具')
|
||||
if (!criticalFiles.mainPyExists) missing.push('后端代码')
|
||||
const missing = []
|
||||
if (!criticalFiles.pythonExists) missing.push('Python 环境')
|
||||
if (!criticalFiles.gitExists) missing.push('Git 工具')
|
||||
if (!criticalFiles.mainPyExists) missing.push('后端代码')
|
||||
|
||||
missingComponents.value = missing
|
||||
showEnvironmentIncomplete.value = true
|
||||
autoMode.value = false
|
||||
} else {
|
||||
// 第一次启动时,即使文件缺失也直接进入手动模式
|
||||
autoMode.value = false
|
||||
showEnvironmentIncomplete.value = false
|
||||
}
|
||||
} else {
|
||||
// 其他情况直接进入手动模式
|
||||
autoMode.value = false
|
||||
showEnvironmentIncomplete.value = false
|
||||
}
|
||||
missingComponents.value = missing
|
||||
showEnvironmentIncomplete.value = true
|
||||
autoMode.value = false
|
||||
}
|
||||
|
||||
// 如果关键文件缺失,重置初始化状态
|
||||
if (!allExeFilesExist && config.init) {
|
||||
console.log('检测到关键exe文件缺失,重置初始化状态')
|
||||
await saveConfig({ init: false })
|
||||
}
|
||||
// 如果关键文件缺失,重置初始化状态
|
||||
if (!allExeFilesExist && config.init) {
|
||||
console.log('检测到关键exe文件缺失,重置初始化状态')
|
||||
await saveConfig({ init: false })
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `环境检查失败: ${error instanceof Error ? error.message : String(error)}`
|
||||
@@ -284,14 +274,6 @@ onMounted(async () => {
|
||||
console.log('测试配置系统...')
|
||||
const testConfig = await getConfig()
|
||||
console.log('当前配置:', testConfig)
|
||||
|
||||
// 测试保存配置
|
||||
await saveConfig({ isFirstLaunch: false })
|
||||
console.log('测试配置保存成功')
|
||||
|
||||
// 重新读取配置验证
|
||||
const updatedConfig = await getConfig()
|
||||
console.log('更新后的配置:', updatedConfig)
|
||||
} catch (error) {
|
||||
console.error('配置系统测试失败:', error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
<template>
|
||||
<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
|
||||
:script-id="scriptId"
|
||||
@@ -104,13 +130,14 @@
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
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 { useUserApi } from '@/composables/useUserApi'
|
||||
import { useScriptApi } from '@/composables/useScriptApi'
|
||||
import { usePlanApi } from '@/composables/usePlanApi'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
import { Service } from '@/api'
|
||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
import { GetStageIn } from '@/api/models/GetStageIn'
|
||||
import { getTodayWeekdayEast12 } from '@/utils/dateUtils'
|
||||
|
||||
@@ -143,6 +170,8 @@ const scriptName = ref('')
|
||||
// MAA配置相关
|
||||
const maaConfigLoading = ref(false)
|
||||
const maaWebsocketId = ref<string | null>(null)
|
||||
const showMAAConfigMask = ref(false)
|
||||
let maaConfigTimeout: number | null = null
|
||||
|
||||
// 基建配置文件相关
|
||||
const infrastructureConfigPath = ref('')
|
||||
@@ -741,28 +770,96 @@ const handleMAAConfig = async () => {
|
||||
if (maaWebsocketId.value) {
|
||||
unsubscribe(maaWebsocketId.value)
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
if (maaConfigTimeout) {
|
||||
window.clearTimeout(maaConfigTimeout)
|
||||
maaConfigTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
// 直接订阅(旧 connect 参数移除)
|
||||
const subId = userId
|
||||
subscribe(subId, {
|
||||
onError: error => {
|
||||
console.error(`用户 ${formData.userName} MAA配置错误:`, error)
|
||||
message.error(`MAA配置连接失败: ${error}`)
|
||||
maaWebsocketId.value = null
|
||||
},
|
||||
// 调用后端启动任务接口,传入 userId 作为 taskId 与设置模式
|
||||
const response = await Service.addTaskApiDispatchStartPost({
|
||||
taskId: userId,
|
||||
mode: TaskCreateIn.mode.SettingScriptMode,
|
||||
})
|
||||
|
||||
maaWebsocketId.value = subId
|
||||
message.success(`已开始配置用户 ${formData.userName} 的MAA设置`)
|
||||
if (response && response.websocketId) {
|
||||
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) {
|
||||
console.error('MAA配置失败:', error)
|
||||
message.error('MAA配置失败')
|
||||
console.error('启动MAA配置失败:', error)
|
||||
message.error('启动MAA配置失败')
|
||||
} finally {
|
||||
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 => {
|
||||
if (!stageName || !stageName.trim()) {
|
||||
@@ -1020,4 +1117,55 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
@@ -112,43 +112,7 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 电源操作倒计时全屏弹窗 -->
|
||||
<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>
|
||||
<!-- 电源操作倒计时弹窗已移至全局组件 GlobalPowerCountdown.vue -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -346,127 +310,7 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 电源操作倒计时全屏弹窗样式 */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
/* 电源操作倒计时弹窗样式已移至 GlobalPowerCountdown.vue */
|
||||
|
||||
/* 响应式 - 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
@@ -499,27 +343,6 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端倒计时弹窗适配 */
|
||||
.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;
|
||||
}
|
||||
/* 移动端倒计时弹窗适配已移至 GlobalPowerCountdown.vue */
|
||||
}
|
||||
</style>
|
||||
@@ -99,7 +99,11 @@ export function handleMainMessage(wsMessage: any) {
|
||||
if (!wsMessage || typeof wsMessage !== 'object') return
|
||||
const { type, data } = wsMessage
|
||||
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 回放
|
||||
storePendingCountdown(data)
|
||||
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 数据
|
||||
export function consumePendingTabIds(): string[] {
|
||||
return popPendingTabs()
|
||||
|
||||
@@ -8,7 +8,6 @@ import schedulerHandlers from './schedulerHandlers'
|
||||
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
|
||||
import type { QueueItem, Script } from './schedulerConstants'
|
||||
import {
|
||||
getPowerActionText,
|
||||
type SchedulerTab,
|
||||
type TaskMessage,
|
||||
type SchedulerStatus,
|
||||
@@ -94,13 +93,15 @@ export function useSchedulerLogic() {
|
||||
|
||||
// 电源操作 - 从本地存储加载或使用默认值
|
||||
const powerAction = ref<PowerIn.signal>(loadPowerActionFromStorage())
|
||||
// 注意:电源倒计时弹窗已移至全局组件 GlobalPowerCountdown.vue
|
||||
// 这里保留引用以避免破坏现有代码,但实际功能由全局组件处理
|
||||
const powerCountdownVisible = ref(false)
|
||||
const powerCountdownData = ref<{
|
||||
title?: string
|
||||
message?: string
|
||||
countdown?: number
|
||||
}>({})
|
||||
// 前端自己的60秒倒计时
|
||||
// 前端自己的60秒倒计时 - 已移至全局组件
|
||||
let powerCountdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// 消息弹窗
|
||||
@@ -508,10 +509,10 @@ export function useSchedulerLogic() {
|
||||
}
|
||||
|
||||
const handleMessageDialog = (tab: SchedulerTab, data: any) => {
|
||||
// 处理倒计时消息
|
||||
// 处理倒计时消息 - 已移至全局组件处理
|
||||
if (data.type === 'Countdown') {
|
||||
console.log('[Scheduler] 收到倒计时消息,启动60秒倒计时:', data)
|
||||
startPowerCountdown(data)
|
||||
console.log('[Scheduler] 收到倒计时消息,由全局组件处理:', data)
|
||||
// 不再在调度中心处理倒计时,由 GlobalPowerCountdown 组件处理
|
||||
return
|
||||
}
|
||||
|
||||
@@ -654,68 +655,16 @@ export function useSchedulerLogic() {
|
||||
console.log('[Scheduler] 电源操作显示已更新为:', newPowerAction)
|
||||
}
|
||||
|
||||
// 启动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)
|
||||
}
|
||||
|
||||
// 移除自动执行电源操作,由后端完全控制
|
||||
// 启动60秒倒计时 - 已移至全局组件,这里保留空函数避免破坏现有代码
|
||||
// 移除自动执行电源操作,由后端完全控制
|
||||
// const executePowerAction = async () => {
|
||||
// // 不再自己执行电源操作,完全由后端控制
|
||||
// }
|
||||
|
||||
const cancelPowerAction = async () => {
|
||||
// 清除倒计时器
|
||||
if (powerCountdownTimer) {
|
||||
clearInterval(powerCountdownTimer)
|
||||
powerCountdownTimer = null
|
||||
}
|
||||
|
||||
// 关闭倒计时弹窗
|
||||
powerCountdownVisible.value = false
|
||||
|
||||
// 调用取消电源操作的API
|
||||
try {
|
||||
await Service.cancelPowerTaskApiDispatchCancelPowerPost()
|
||||
message.success('已取消电源操作')
|
||||
} catch (error) {
|
||||
console.error('取消电源操作失败:', error)
|
||||
message.error('取消电源操作失败')
|
||||
}
|
||||
|
||||
// 注意:这里不重置 powerAction,保留用户选择
|
||||
console.log('[Scheduler] cancelPowerAction 已移至全局组件,调度中心不再处理')
|
||||
// 电源操作取消功能已移至 GlobalPowerCountdown 组件
|
||||
// 这里保留空函数以避免破坏现有的调用代码
|
||||
}
|
||||
|
||||
// 移除自动检查任务完成的逻辑,完全由后端控制
|
||||
@@ -788,8 +737,8 @@ export function useSchedulerLogic() {
|
||||
},
|
||||
onCountdown: (data) => {
|
||||
try {
|
||||
// 直接启动前端倒计时
|
||||
startPowerCountdown(data)
|
||||
// 倒计时已移至全局组件处理,这里不再处理
|
||||
console.log('[Scheduler] 倒计时消息由全局组件处理:', data)
|
||||
} catch (e) {
|
||||
console.warn('[Scheduler] registerSchedulerUI onCountdown error:', e)
|
||||
}
|
||||
@@ -814,7 +763,8 @@ export function useSchedulerLogic() {
|
||||
const pendingCountdown = schedulerHandlers.consumePendingCountdown()
|
||||
if (pendingCountdown) {
|
||||
try {
|
||||
startPowerCountdown(pendingCountdown)
|
||||
// 倒计时已移至全局组件处理,这里不再处理
|
||||
console.log('[Scheduler] 待处理倒计时消息由全局组件处理:', pendingCountdown)
|
||||
} catch (e) {
|
||||
console.warn('[Scheduler] replay pending countdown error:', e)
|
||||
}
|
||||
@@ -843,10 +793,17 @@ export function useSchedulerLogic() {
|
||||
const { type, data } = wsMessage
|
||||
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') {
|
||||
// 收到倒计时消息,启动前端60秒倒计时
|
||||
console.log('[Scheduler] 收到倒计时消息,启动前端60秒倒计时:', data)
|
||||
startPowerCountdown(data)
|
||||
// 收到倒计时消息,由全局组件处理
|
||||
console.log('[Scheduler] 收到倒计时消息,由全局组件处理:', data)
|
||||
// 不再在调度中心处理倒计时
|
||||
} else if (type === 'Update' && data && data.PowerSign !== undefined) {
|
||||
// 收到电源操作更新消息,更新显示
|
||||
console.log('[Scheduler] 收到电源操作更新消息:', data.PowerSign)
|
||||
@@ -856,7 +813,7 @@ export function useSchedulerLogic() {
|
||||
|
||||
// 清理函数
|
||||
const cleanup = () => {
|
||||
// 清理倒计时器
|
||||
// 清理倒计时器 - 已移至全局组件,这里保留以避免错误
|
||||
if (powerCountdownTimer) {
|
||||
clearInterval(powerCountdownTimer)
|
||||
powerCountdownTimer = null
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
goToLogs,
|
||||
openDevTools,
|
||||
mirrorConfigStatus,
|
||||
refreshingConfig,
|
||||
refreshMirrorConfig,
|
||||
goToMirrorTest
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
goToLogs: () => void
|
||||
openDevTools: () => void
|
||||
mirrorConfigStatus: { isUsingCloudConfig: boolean; version: string; lastUpdated: string; source: 'cloud' | 'fallback' }
|
||||
refreshingConfig: boolean
|
||||
refreshMirrorConfig: () => Promise<void>
|
||||
goToMirrorTest: () => void
|
||||
}>()
|
||||
|
||||
const { goToLogs, openDevTools, refreshingConfig, refreshMirrorConfig, goToMirrorTest } = props
|
||||
</script>
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons-vue'
|
||||
import type { SettingsData } from '@/types/settings'
|
||||
|
||||
const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, testingNotify } = defineProps<{
|
||||
const props = defineProps<{
|
||||
settings: SettingsData
|
||||
sendTaskResultTimeOptions: { label: string; value: string }[]
|
||||
handleSettingChange: (category: keyof SettingsData, key: string, value: any) => Promise<void>
|
||||
testNotify: () => Promise<void>
|
||||
testingNotify: boolean
|
||||
}>()
|
||||
|
||||
const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, testingNotify } = props
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>通知内容</h3>
|
||||
<a-button type="primary" :loading="testingNotify" @click="testNotify" size="small" class="section-update-button primary-style">发送测试通知</a-button>
|
||||
</div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
@@ -319,17 +323,73 @@ const { settings, sendTaskResultTimeOptions, handleSettingChange, testNotify, te
|
||||
</a-row>
|
||||
</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>
|
||||
</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">
|
||||
<h3>项目链接</h3>
|
||||
</div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<div class="link-grid">
|
||||
<div class="link-item">
|
||||
<div class="link-card">
|
||||
<div class="link-icon"><HomeOutlined /></div>
|
||||
<div class="link-content">
|
||||
@@ -23,18 +23,18 @@ const { version, backendUpdateInfo } = defineProps<{
|
||||
<a href="https://auto-mas.top" target="_blank" class="link-button">访问官网</a>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
</div>
|
||||
<div class="link-item">
|
||||
<div class="link-card">
|
||||
<div class="link-icon"><GithubOutlined /></div>
|
||||
<div class="link-content">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
</div>
|
||||
<div class="link-item">
|
||||
<div class="link-card">
|
||||
<div class="link-icon"><QqOutlined /></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
@@ -74,3 +74,30 @@ const { version, backendUpdateInfo } = defineProps<{
|
||||
</div>
|
||||
</div>
|
||||
</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 mirrorConfigStatus = ref({
|
||||
type MirrorConfigStatus = {
|
||||
isUsingCloudConfig: boolean
|
||||
version?: string
|
||||
lastUpdated?: string
|
||||
source: 'cloud' | 'fallback'
|
||||
}
|
||||
const mirrorConfigStatus = ref<MirrorConfigStatus>({
|
||||
isUsingCloudConfig: false,
|
||||
version: '',
|
||||
lastUpdated: '',
|
||||
source: 'fallback' as 'cloud' | 'fallback',
|
||||
version: undefined,
|
||||
lastUpdated: undefined,
|
||||
source: 'fallback',
|
||||
})
|
||||
const refreshingConfig = ref(false)
|
||||
|
||||
@@ -324,9 +330,12 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
/* 统一样式,使用 :deep 作用到子组件内部 */
|
||||
.settings-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
/* Allow the settings page to expand with the window width */
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settings-header {
|
||||
margin-bottom: 24px;
|
||||
@@ -340,6 +349,7 @@ onMounted(() => {
|
||||
.settings-content {
|
||||
background: var(--ant-color-bg-container);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.settings-tabs {
|
||||
margin: 0;
|
||||
@@ -356,6 +366,7 @@ onMounted(() => {
|
||||
}
|
||||
:deep(.tab-content) {
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.form-section) {
|
||||
margin-bottom: 32px;
|
||||
@@ -536,6 +547,7 @@ onMounted(() => {
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
/* link-grid styles moved into TabOthers.vue (scoped) */
|
||||
:deep(.info-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p><strong>脚本实例名称:</strong>{{ script_name }}</p>
|
||||
<p><strong>任务开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>任务结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>实例名称:</strong>{{ script_name }}</p>
|
||||
<p><strong>开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>已完成数:</strong>{{ completed_count }}</p>
|
||||
{% if uncompleted_count %}
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>文档站仓库:</strong></p>
|
||||
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a>
|
||||
<p><strong>文档站:</strong></p>
|
||||
<a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,9 +166,11 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p><strong>用户代理信息:</strong>{{ user_info }}</p>
|
||||
<p><strong>任务开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>任务结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>用户信息:</strong>{{ user_info }}</p>
|
||||
<p><strong>开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>理智剩余:</strong>{{ sanity }}</p>
|
||||
<p><strong>回复时间:</strong>{{ sanity_full_at }}</p>
|
||||
<p><strong>MAA执行结果:</strong>
|
||||
{% if maa_result == '代理任务全部完成' %}
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>文档站仓库:</strong></p>
|
||||
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a>
|
||||
<p><strong>文档站:</strong></p>
|
||||
<a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p><strong>脚本实例名称:</strong>{{ script_name }}</p>
|
||||
<p><strong>任务开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>任务结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>实例名称:</strong>{{ script_name }}</p>
|
||||
<p><strong>开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>已完成数:</strong>{{ completed_count }}</p>
|
||||
{% if uncompleted_count %}
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>文档站仓库:</strong></p>
|
||||
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a>
|
||||
<p><strong>文档站:</strong></p>
|
||||
<a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,10 +166,10 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p><strong>用户代理信息:</strong>{{ sub_info }}</p>
|
||||
<p><strong>任务开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>任务结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>脚本执行结果:</strong>
|
||||
<p><strong>代理信息:</strong>{{ sub_info }}</p>
|
||||
<p><strong>开始时间:</strong>{{ start_time }}</p>
|
||||
<p><strong>结束时间:</strong>{{ end_time }}</p>
|
||||
<p><strong>执行结果:</strong>
|
||||
{% if sub_result == '代理成功' %}
|
||||
<span class="greenhighlight">{{ sub_result }}</span>
|
||||
{% elif sub_result == '代理失败' %}
|
||||
@@ -190,8 +190,8 @@
|
||||
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS" class="button">AUTO-MAS GitHub</a>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>文档站仓库:</strong></p>
|
||||
<a href="https://github.com/AUTO-MAS-Project/AUTO-MAS-docs" class="button">AUTO-MAS 文档站 GitHub</a>
|
||||
<p><strong>文档站:</strong></p>
|
||||
<a href="https://doc.auto-mas.top/" class="button">AUTO-MAS 文档站</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user