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

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

View File

@@ -882,6 +882,13 @@ class AppConfig(GlobalConfig):
"RootPath": general_config["Script"]["RootPath"],
}
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"]:

View File

@@ -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") != ""
):

View File

@@ -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}"

View File

@@ -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以触发更新流程确保代码是最新的

View File

@@ -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()))

View File

@@ -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>

View File

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

View File

@@ -60,7 +60,7 @@
<template #icon>
<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()

View File

@@ -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'

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>