feat(script): 添加脚本管理页面和相关功能

- 实现了脚本管理页面的基本布局和样式
- 添加了脚本列表加载、添加、编辑和删除功能- 集成了文件夹和文件选择对话框
- 优化了主题模式和颜色的动态切换
- 新增了多个脚本相关类型定义
This commit is contained in:
2025-08-04 14:42:31 +08:00
parent 94d038d563
commit 0b1ed48471
21 changed files with 3770 additions and 70 deletions

View File

@@ -38,8 +38,8 @@ const path = __importStar(require("path"));
let mainWindow = null;
function createWindow() {
mainWindow = new electron_1.BrowserWindow({
width: 1200,
height: 800,
width: 1600,
height: 900,
minWidth: 800,
minHeight: 600,
icon: path.join(__dirname, '../public/AUTO_MAA.ico'), // 设置应用图标
@@ -73,6 +73,35 @@ electron_1.ipcMain.handle('open-dev-tools', () => {
mainWindow.webContents.openDevTools({ mode: 'undocked' });
}
});
// 处理文件夹选择请求
electron_1.ipcMain.handle('select-folder', async () => {
if (!mainWindow)
return null;
const result = await electron_1.dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: '选择文件夹'
});
if (result.canceled) {
return null;
}
return result.filePaths[0];
});
// 处理文件选择请求
electron_1.ipcMain.handle('select-file', async (event, filters = []) => {
if (!mainWindow)
return null;
const result = await electron_1.dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
title: '选择文件',
filters: filters.length > 0 ? filters : [
{ name: '所有文件', extensions: ['*'] }
]
});
if (result.canceled) {
return null;
}
return result.filePaths[0];
});
electron_1.app.whenReady().then(createWindow);
electron_1.app.on('window-all-closed', () => {
if (process.platform !== 'darwin')

View File

@@ -6,5 +6,7 @@ window.addEventListener('DOMContentLoaded', () => {
});
// 暴露安全的 API 给渲染进程
electron_1.contextBridge.exposeInMainWorld('electronAPI', {
openDevTools: () => electron_1.ipcRenderer.invoke('open-dev-tools')
openDevTools: () => electron_1.ipcRenderer.invoke('open-dev-tools'),
selectFolder: () => electron_1.ipcRenderer.invoke('select-folder'),
selectFile: (filters) => electron_1.ipcRenderer.invoke('select-file', filters)
});

View File

@@ -1,12 +1,12 @@
import { app, BrowserWindow, ipcMain } from 'electron'
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
import * as path from 'path'
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
width: 1600,
height: 900,
minWidth: 800,
minHeight: 600,
icon: path.join(__dirname, '../public/AUTO_MAA.ico'), // 设置应用图标
@@ -44,6 +44,41 @@ ipcMain.handle('open-dev-tools', () => {
}
})
// 处理文件夹选择请求
ipcMain.handle('select-folder', async () => {
if (!mainWindow) return null
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: '选择文件夹'
})
if (result.canceled) {
return null
}
return result.filePaths[0]
})
// 处理文件选择请求
ipcMain.handle('select-file', async (event, filters = []) => {
if (!mainWindow) return null
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
title: '选择文件',
filters: filters.length > 0 ? filters : [
{ name: '所有文件', extensions: ['*'] }
]
})
if (result.canceled) {
return null
}
return result.filePaths[0]
})
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {

View File

@@ -6,5 +6,7 @@ window.addEventListener('DOMContentLoaded', () => {
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
openDevTools: () => ipcRenderer.invoke('open-dev-tools')
openDevTools: () => ipcRenderer.invoke('open-dev-tools'),
selectFolder: () => ipcRenderer.invoke('select-folder'),
selectFile: (filters?: any[]) => ipcRenderer.invoke('select-file', filters)
})

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
frontend/src/assets/MAA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -12,7 +12,7 @@
<div class="sider-content">
<!-- Logo区域 - 点击切换展开/折叠 -->
<div class="logo" @click="collapsed = !collapsed">
<img src="/AUTO_MAA.ico" alt="AUTO MAA" class="logo-image" />
<img src="/src/assets/AUTO_MAA.ico" alt="AUTO MAA" class="logo-image" />
<div v-if="!collapsed" class="logo-text">AUTO_MAA</div>
</div>

View File

@@ -0,0 +1,340 @@
<template>
<a-modal
v-model:open="visible"
:title="isEdit ? '编辑脚本' : '新建脚本'"
width="800px"
:confirm-loading="loading"
@ok="handleSave"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="脚本名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入脚本名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="脚本类型" name="type">
<a-select v-model:value="formData.type" :disabled="isEdit">
<a-select-option value="MAA">MAA</a-select-option>
<a-select-option value="General">General</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<!-- MAA脚本配置 -->
<template v-if="formData.type === 'MAA'">
<a-divider>基础配置</a-divider>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="脚本路径" name="path">
<a-input v-model:value="maaConfig.Info.Path" placeholder="请输入MAA脚本路径" />
</a-form-item>
</a-col>
</a-row>
<a-divider>运行配置</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="ADB搜索范围">
<a-input-number
v-model:value="maaConfig.Run.ADBSearchRange"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="剿灭时间限制(分钟)">
<a-input-number
v-model:value="maaConfig.Run.AnnihilationTimeLimit"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="代理次数限制">
<a-input-number
v-model:value="maaConfig.Run.ProxyTimesLimit"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="日常时间限制(分钟)">
<a-input-number
v-model:value="maaConfig.Run.RoutineTimeLimit"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="运行次数限制">
<a-input-number
v-model:value="maaConfig.Run.RunTimesLimit"
:min="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="任务转换方式">
<a-select v-model:value="maaConfig.Run.TaskTransitionMethod">
<a-select-option value="NoAction">无操作</a-select-option>
<a-select-option value="ExitEmulator">退出模拟器</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="剿灭周限制">
<a-switch v-model:checked="maaConfig.Run.AnnihilationWeeklyLimit" />
</a-form-item>
</a-col>
</a-row>
</template>
<!-- General脚本配置 -->
<template v-if="formData.type === 'General'">
<a-divider>基础配置</a-divider>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="根路径" name="rootPath">
<a-input v-model:value="generalConfig.Info.RootPath" placeholder="请输入根路径" />
</a-form-item>
</a-col>
</a-row>
<a-divider>游戏配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="游戏路径">
<a-input v-model:value="generalConfig.Game.Path" placeholder="请输入游戏路径" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="启动参数">
<a-input v-model:value="generalConfig.Game.Arguments" placeholder="请输入启动参数" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="游戏样式">
<a-select v-model:value="generalConfig.Game.Style">
<a-select-option value="Emulator">模拟器</a-select-option>
<a-select-option value="Game">游戏</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="等待时间(秒)">
<a-input-number
v-model:value="generalConfig.Game.WaitTime"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="启用游戏">
<a-switch v-model:checked="generalConfig.Game.Enabled" />
</a-form-item>
</a-col>
</a-row>
<a-divider>脚本配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="脚本路径">
<a-input v-model:value="generalConfig.Script.ScriptPath" placeholder="请输入脚本路径" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="配置路径">
<a-input v-model:value="generalConfig.Script.ConfigPath" placeholder="请输入配置路径" />
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { message } from 'ant-design-vue'
import type { FormInstance } from 'ant-design-vue'
import type { ScriptType, MAAScriptConfig, GeneralScriptConfig } from '@/types/script'
import { useScriptApi } from '@/composables/useScriptApi'
interface Props {
open: boolean
scriptData?: {
id?: string
type: ScriptType
config: MAAScriptConfig | GeneralScriptConfig
}
}
interface Emits {
(e: 'update:open', value: boolean): void
(e: 'success', data: any): void
}
const props = withDefaults(defineProps<Props>(), {
open: false
})
const emit = defineEmits<Emits>()
const { addScript, updateScript, loading } = useScriptApi()
const formRef = ref<FormInstance>()
const visible = computed({
get: () => props.open,
set: (value) => emit('update:open', value)
})
const isEdit = computed(() => !!props.scriptData?.id)
const formData = reactive({
name: '',
type: 'MAA' as ScriptType
})
// MAA配置
const maaConfig = reactive<MAAScriptConfig>({
Info: {
Name: '',
Path: '.'
},
Run: {
ADBSearchRange: 0,
AnnihilationTimeLimit: 40,
AnnihilationWeeklyLimit: true,
ProxyTimesLimit: 0,
RoutineTimeLimit: 10,
RunTimesLimit: 3,
TaskTransitionMethod: 'ExitEmulator'
},
SubConfigsInfo: {
UserData: {
instances: []
}
}
})
// General配置
const generalConfig = reactive<GeneralScriptConfig>({
Game: {
Arguments: '',
Enabled: false,
IfForceClose: false,
Path: '.',
Style: 'Emulator',
WaitTime: 0
},
Info: {
Name: '',
RootPath: '.'
},
Run: {
ProxyTimesLimit: 0,
RunTimeLimit: 10,
RunTimesLimit: 3
},
Script: {
Arguments: '',
ConfigPath: '.',
ConfigPathMode: '所有文件 (*)',
ErrorLog: '',
IfTrackProcess: false,
LogPath: '.',
LogPathFormat: '%Y-%m-%d',
LogTimeEnd: 1,
LogTimeStart: 1,
LogTimeFormat: '%Y-%m-%d %H:%M:%S',
ScriptPath: '.',
SuccessLog: '',
UpdateConfigMode: 'Never'
},
SubConfigsInfo: {
UserData: {
instances: []
}
}
})
const rules = {
name: [{ required: true, message: '请输入脚本名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择脚本类型', trigger: 'change' }]
}
// 监听props变化初始化表单数据
watch(() => props.scriptData, (data) => {
if (data) {
formData.name = data.type === 'MAA' ? (data.config as MAAScriptConfig).Info.Name : (data.config as GeneralScriptConfig).Info.Name
formData.type = data.type
if (data.type === 'MAA') {
Object.assign(maaConfig, data.config)
} else {
Object.assign(generalConfig, data.config)
}
}
}, { immediate: true })
const handleSave = async () => {
try {
await formRef.value?.validate()
if (isEdit.value) {
// 编辑模式
const config = formData.type === 'MAA' ? maaConfig : generalConfig
if (formData.type === 'MAA') {
maaConfig.Info.Name = formData.name
} else {
generalConfig.Info.Name = formData.name
}
const result = await updateScript(props.scriptData!.id!, config)
if (result) {
message.success('脚本更新成功')
emit('success', { id: props.scriptData!.id, type: formData.type, config })
visible.value = false
}
} else {
// 新建模式
const result = await addScript(formData.type)
if (result) {
message.success(result.message)
emit('success', { id: result.scriptId, type: formData.type, config: result.data })
visible.value = false
}
}
} catch (error) {
console.error('保存失败:', error)
}
}
const handleCancel = () => {
visible.value = false
}
</script>

View File

@@ -0,0 +1,567 @@
<template>
<a-table
:columns="columns"
:data-source="scripts"
:pagination="false"
:expandable="expandableConfig"
row-key="id"
class="modern-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="script-name-cell">
<div class="script-type-logo">
<img
v-if="record.type === 'MAA'"
src="@/assets/MAA.png"
alt="MAA"
class="script-logo"
/>
<img v-else src="@/assets/AUTO_MAA.png" alt="AUTO MAA" class="script-logo" />
</div>
<div class="script-info">
<div class="script-title">{{ record.name }}</div>
<a-tag :color="record.type === 'MAA' ? 'blue' : 'green'" class="script-type-tag">
{{ record.type }}
</a-tag>
</div>
</div>
</template>
<template v-if="column.key === 'userCount'">
<a-badge
:count="record.users && record.users.length ? record.users.length : 0"
:number-style="{
backgroundColor: record.users.length > 0 ? '#52c41a' : '#d9d9d9',
color: record.users.length > 0 ? '#fff' : '#666',
}"
class="user-count-badge"
/>
</template>
<template v-if="column.key === 'action'">
<a-space size="small">
<a-tooltip title="编辑脚本配置">
<a-button
type="primary"
size="small"
@click="handleEdit(record)"
class="action-button edit-button"
>
<template #icon>
<EditOutlined />
</template>
编辑
</a-button>
</a-tooltip>
<a-tooltip title="为此脚本添加用户">
<a-button
type="default"
size="small"
@click="handleAddUser(record)"
class="action-button add-user-button"
>
<template #icon>
<UserAddOutlined />
</template>
添加用户
</a-button>
</a-tooltip>
<a-popconfirm
title="确定要删除这个脚本吗"
description="删除后将无法恢复请谨慎操作"
@confirm="handleDelete(record)"
ok-text="确定"
cancel-text="取消"
>
<a-tooltip title="删除脚本">
<a-button danger size="small" class="action-button delete-button">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template>
</template>
<template #expandedRowRender="{ record }">
<div class="expanded-content">
<a-table
:columns="userColumns"
:data-source="record.users"
:pagination="false"
size="small"
row-key="id"
class="user-table"
>
<template #bodyCell="{ column, record: user }">
<template v-if="column.key === 'status'">
<div class="status-cell">
<PlayCircleOutlined v-if="user.Info.Status" class="status-icon active" />
<PauseCircleOutlined v-else class="status-icon inactive" />
<a-tag :color="user.Info.Status ? 'success' : 'error'">
{{ user.Info.Status ? '启用' : '禁用' }}
</a-tag>
</div>
</template>
<template v-if="column.key === 'lastRun'">
<div class="last-run-cell">
<div class="run-item">
<span class="run-label">剿灭:</span>
<span class="run-date">{{ user.Data.LastAnnihilationDate }}</span>
</div>
<div class="run-item">
<span class="run-label">代理:</span>
<span class="run-date">{{ user.Data.LastProxyDate }}</span>
</div>
</div>
</template>
<template v-if="column.key === 'tasks'">
<a-space wrap class="task-tags">
<a-tag v-if="user.Task.IfBase" color="blue" class="task-tag">基建</a-tag>
<a-tag v-if="user.Task.IfCombat" color="green" class="task-tag">作战</a-tag>
<a-tag v-if="user.Task.IfMall" color="orange" class="task-tag">商店</a-tag>
<a-tag v-if="user.Task.IfMission" color="purple" class="task-tag">任务</a-tag>
<a-tag v-if="user.Task.IfRecruiting" color="cyan" class="task-tag">招募</a-tag>
</a-space>
</template>
<template v-if="column.key === 'userAction'">
<a-space size="small">
<a-tooltip title="编辑用户配置">
<a-button
type="link"
size="small"
@click="handleEditUser(user)"
class="user-action-button"
>
<template #icon>
<EditOutlined />
</template>
编辑
</a-button>
</a-tooltip>
<a-popconfirm
title="确定要删除这个用户吗"
description="删除后将无法恢复"
@confirm="handleDeleteUser(user)"
ok-text="确定"
cancel-text="取消"
>
<a-tooltip title="删除用户">
<a-button type="link" danger size="small" class="user-action-button">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
</a-table>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TableColumnsType } from 'ant-design-vue'
import type { Script, User } from '@/types/script'
import {
DeleteOutlined,
EditOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
UserAddOutlined,
} from '@ant-design/icons-vue'
interface Props {
scripts: Script[]
}
interface Emits {
(e: 'edit', script: Script): void
(e: 'delete', script: Script): void
(e: 'addUser', script: Script): void
(e: 'editUser', user: User): void
(e: 'deleteUser', user: User): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const columns: TableColumnsType = [
{
title: '脚本名称',
dataIndex: 'name',
key: 'name',
width: 300,
},
// {
// title: '用户数量',
// dataIndex: 'userCount',
// key: 'userCount',
// width: 120,
// align: 'center',
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// width: 180,
// },
{
title: '操作',
key: 'action',
width: 250,
align: 'center',
},
]
const userColumns: TableColumnsType = [
{
title: '用户名',
dataIndex: ['Info', 'Name'],
key: 'name',
width: 150,
},
{
title: 'ID',
dataIndex: ['Info', 'Id'],
key: 'id',
width: 100,
},
{
title: '服务器',
dataIndex: ['Info', 'Server'],
key: 'server',
width: 100,
},
{
title: '状态',
key: 'status',
width: 80,
align: 'center',
},
{
title: '最后运行',
key: 'lastRun',
width: 200,
},
{
title: '启用任务',
key: 'tasks',
width: 300,
},
{
title: '备注',
dataIndex: ['Info', 'Notes'],
key: 'notes',
width: 150,
},
{
title: '操作',
key: 'userAction',
width: 120,
align: 'center',
},
]
const expandableConfig = computed(() => ({
expandedRowRender: true,
rowExpandable: (record: Script) => record.users.length > 0,
}))
const handleEdit = (script: Script) => {
emit('edit', script)
}
const handleDelete = (script: Script) => {
emit('delete', script)
}
const handleAddUser = (script: Script) => {
emit('addUser', script)
}
const handleEditUser = (user: User) => {
emit('editUser', user)
}
const handleDeleteUser = (user: User) => {
emit('deleteUser', user)
}
</script>
<style scoped>
/* 现代化表格样式 */
.modern-table :deep(.ant-table) {
background: var(--ant-color-bg-container);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.modern-table :deep(.ant-table-thead > tr > th) {
background: var(--ant-color-bg-container);
border-bottom: 2px solid var(--ant-color-border-secondary);
font-weight: 600;
font-size: 14px;
color: var(--ant-color-text);
padding: 16px 24px;
}
.modern-table :deep(.ant-table-tbody > tr > td) {
padding: 20px 24px;
border-bottom: 1px solid var(--ant-color-border-secondary);
transition: all 0.3s ease;
}
.modern-table :deep(.ant-table-tbody > tr:hover > td) {
background: var(--ant-color-bg-layout);
}
.modern-table :deep(.ant-table-expanded-row > td) {
padding: 0;
background: var(--ant-color-bg-layout);
}
/* 脚本名称单元格 */
.script-name-cell {
display: flex;
align-items: center;
gap: 12px;
}
.script-type-logo {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: var(--ant-color-bg-elevated);
border: 1px solid var(--ant-color-border-secondary);
overflow: hidden;
}
.script-logo {
width: 32px;
height: 32px;
object-fit: contain;
transition: all 0.3s ease;
}
.script-info {
flex: 1;
}
.script-title {
font-size: 16px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 4px;
}
.script-type-tag {
font-size: 12px;
font-weight: 500;
border-radius: 6px;
}
/* 用户数量徽章 */
.user-count-badge :deep(.ant-badge-count) {
font-weight: 600;
border-radius: 8px;
min-width: 24px;
height: 24px;
line-height: 24px;
font-size: 12px;
}
/* 操作按钮 */
.action-button {
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
}
.edit-button {
background: var(--ant-color-primary);
border-color: var(--ant-color-primary);
}
.edit-button:hover {
background: var(--ant-color-primary-hover);
border-color: var(--ant-color-primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.3);
}
.add-user-button {
border: 1px solid var(--ant-color-border);
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
}
.add-user-button:hover {
border-color: var(--ant-color-primary);
color: var(--ant-color-primary);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.delete-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 77, 79, 0.3);
}
/* 展开内容 */
.expanded-content {
padding: 24px;
background: var(--ant-color-bg-layout);
border-radius: 0 0 12px 12px;
}
.user-table :deep(.ant-table) {
background: var(--ant-color-bg-container);
border-radius: 8px;
border: 1px solid var(--ant-color-border-secondary);
}
.user-table :deep(.ant-table-thead > tr > th) {
background: var(--ant-color-bg-layout);
font-size: 13px;
padding: 12px 16px;
}
.user-table :deep(.ant-table-tbody > tr > td) {
padding: 16px;
font-size: 13px;
}
/* 状态单元格 */
.status-cell {
display: flex;
align-items: center;
gap: 8px;
}
.status-icon {
font-size: 16px;
}
.status-icon.active {
color: #52c41a;
}
.status-icon.inactive {
color: #ff4d4f;
}
/* 最后运行单元格 */
.last-run-cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.run-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
}
.run-label {
color: var(--ant-color-text-secondary);
font-weight: 500;
min-width: 32px;
}
.run-date {
color: var(--ant-color-text);
font-family: 'Consolas', 'Monaco', monospace;
}
/* 任务标签 */
.task-tags {
max-width: 300px;
}
.task-tag {
font-size: 11px;
font-weight: 500;
border-radius: 4px;
margin: 2px;
}
/* 用户操作按钮 */
.user-action-button {
font-size: 12px;
padding: 4px 8px;
height: auto;
border-radius: 6px;
}
.user-action-button:hover {
transform: translateY(-1px);
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.modern-table :deep(.ant-table) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.edit-button:hover {
box-shadow: 0 4px 8px rgba(24, 144, 255, 0.4);
}
.add-user-button:hover {
box-shadow: 0 4px 8px rgba(255, 255, 255, 0.1);
}
.delete-button:hover {
box-shadow: 0 4px 8px rgba(255, 77, 79, 0.4);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.modern-table :deep(.ant-table-tbody > tr > td) {
padding: 12px 16px;
}
.script-name-cell {
gap: 8px;
}
.script-type-icon {
width: 32px;
height: 32px;
font-size: 14px;
}
.script-title {
font-size: 14px;
}
.expanded-content {
padding: 16px;
}
.action-button {
font-size: 12px;
padding: 4px 8px;
}
}
</style>

View File

@@ -0,0 +1,208 @@
import { ref } from 'vue'
import type {
ScriptType,
AddScriptResponse,
MAAScriptConfig,
GeneralScriptConfig,
GetScriptsResponse,
ScriptDetail,
ScriptIndexItem
} from '@/types/script'
const API_BASE_URL = 'http://localhost:8000/api'
export function useScriptApi() {
const loading = ref(false)
const error = ref<string | null>(null)
// 添加脚本
const addScript = async (type: ScriptType): Promise<AddScriptResponse | null> => {
loading.value = true
error.value = null
try {
const response = await fetch(`${API_BASE_URL}/add/scripts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ type }),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data: AddScriptResponse = await response.json()
return data
} catch (err) {
error.value = err instanceof Error ? err.message : '添加脚本失败'
return null
} finally {
loading.value = false
}
}
// 获取所有脚本
const getScripts = async (): Promise<ScriptDetail[]> => {
loading.value = true
error.value = null
try {
const response = await fetch(`${API_BASE_URL}/get/scripts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}), // 传空对象获取全部脚本
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const apiResponse: GetScriptsResponse = await response.json()
// 转换API响应为前端需要的格式
const scripts: ScriptDetail[] = apiResponse.index.map((item: ScriptIndexItem) => {
const config = apiResponse.data[item.uid]
const scriptType: ScriptType = item.type === 'MaaConfig' ? 'MAA' : 'General'
// 从配置中获取脚本名称
let name = ''
if (scriptType === 'MAA') {
name = (config as MAAScriptConfig).Info.Name || '未命名MAA脚本'
} else {
name = (config as GeneralScriptConfig).Info.Name || '未命名General脚本'
}
return {
uid: item.uid,
type: scriptType,
name,
config,
createTime: new Date().toLocaleString() // 暂时使用当前时间后续可从API获取
}
})
return scripts
} catch (err) {
error.value = err instanceof Error ? err.message : '获取脚本列表失败'
return []
} finally {
loading.value = false
}
}
// 获取单个脚本
const getScript = async (scriptId: string): Promise<ScriptDetail | null> => {
loading.value = true
error.value = null
try {
const response = await fetch(`${API_BASE_URL}/get/scripts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ scriptId }), // 传scriptId获取单个脚本
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const apiResponse: GetScriptsResponse = await response.json()
// 检查是否有数据返回
if (apiResponse.index.length === 0) {
throw new Error('脚本不存在')
}
const item = apiResponse.index[0]
const config = apiResponse.data[item.uid]
const scriptType: ScriptType = item.type === 'MaaConfig' ? 'MAA' : 'General'
// 从配置中获取脚本名称
let name = ''
if (scriptType === 'MAA') {
name = (config as MAAScriptConfig).Info.Name || '未命名MAA脚本'
} else {
name = (config as GeneralScriptConfig).Info.Name || '未命名General脚本'
}
return {
uid: item.uid,
type: scriptType,
name,
config,
createTime: new Date().toLocaleString() // 暂时使用当前时间后续可从API获取
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取脚本详情失败'
return null
} finally {
loading.value = false
}
}
// 更新脚本(暂时模拟)
const updateScript = async (scriptId: string, config: MAAScriptConfig | GeneralScriptConfig) => {
loading.value = true
error.value = null
try {
const response = await fetch(`${API_BASE_URL}/update/scripts/${scriptId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : '更新脚本失败'
return null
} finally {
loading.value = false
}
}
// 删除脚本(暂时模拟)
const deleteScript = async (scriptId: string) => {
loading.value = true
error.value = null
try {
const response = await fetch(`${API_BASE_URL}/delete/scripts/${scriptId}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : '删除脚本失败'
return null
} finally {
loading.value = false
}
}
return {
loading,
error,
addScript,
getScripts,
getScript,
updateScript,
deleteScript,
}
}

View File

@@ -61,6 +61,91 @@ const updateTheme = () => {
} else {
document.documentElement.classList.remove('dark')
}
// 更新CSS变量
updateCSSVariables()
}
// 更新CSS变量
const updateCSSVariables = () => {
const root = document.documentElement
const primaryColor = themeColors[themeColor.value]
if (isDark.value) {
// 深色模式变量
root.style.setProperty('--ant-color-primary', primaryColor)
root.style.setProperty('--ant-color-primary-hover', lightenColor(primaryColor, 10))
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
root.style.setProperty('--ant-color-text', 'rgba(255, 255, 255, 0.88)')
root.style.setProperty('--ant-color-text-secondary', 'rgba(255, 255, 255, 0.65)')
root.style.setProperty('--ant-color-text-tertiary', 'rgba(255, 255, 255, 0.45)')
root.style.setProperty('--ant-color-bg-container', '#141414')
root.style.setProperty('--ant-color-bg-layout', '#000000')
root.style.setProperty('--ant-color-bg-elevated', '#1f1f1f')
root.style.setProperty('--ant-color-border', '#424242')
root.style.setProperty('--ant-color-border-secondary', '#303030')
root.style.setProperty('--ant-color-error', '#ff4d4f')
root.style.setProperty('--ant-color-success', '#52c41a')
root.style.setProperty('--ant-color-warning', '#faad14')
} else {
// 浅色模式变量
root.style.setProperty('--ant-color-primary', primaryColor)
root.style.setProperty('--ant-color-primary-hover', darkenColor(primaryColor, 10))
root.style.setProperty('--ant-color-primary-bg', `${primaryColor}1a`)
root.style.setProperty('--ant-color-text', 'rgba(0, 0, 0, 0.88)')
root.style.setProperty('--ant-color-text-secondary', 'rgba(0, 0, 0, 0.65)')
root.style.setProperty('--ant-color-text-tertiary', 'rgba(0, 0, 0, 0.45)')
root.style.setProperty('--ant-color-bg-container', '#ffffff')
root.style.setProperty('--ant-color-bg-layout', '#f5f5f5')
root.style.setProperty('--ant-color-bg-elevated', '#ffffff')
root.style.setProperty('--ant-color-border', '#d9d9d9')
root.style.setProperty('--ant-color-border-secondary', '#f0f0f0')
root.style.setProperty('--ant-color-error', '#ff4d4f')
root.style.setProperty('--ant-color-success', '#52c41a')
root.style.setProperty('--ant-color-warning', '#faad14')
}
}
// 颜色工具函数
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
const rgbToHex = (r: number, g: number, b: number) => {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
}
const lightenColor = (hex: string, percent: number) => {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const { r, g, b } = rgb
const amount = Math.round(2.55 * percent)
return rgbToHex(
Math.min(255, r + amount),
Math.min(255, g + amount),
Math.min(255, b + amount)
)
}
const darkenColor = (hex: string, percent: number) => {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const { r, g, b } = rgb
const amount = Math.round(2.55 * percent)
return rgbToHex(
Math.max(0, r - amount),
Math.max(0, g - amount),
Math.max(0, b - amount)
)
}
// 监听系统主题变化
@@ -71,8 +156,9 @@ mediaQuery.addEventListener('change', () => {
}
})
// 监听主题模式变化
// 监听主题模式和颜色变化
watch(themeMode, updateTheme, { immediate: true })
watch(themeColor, updateTheme)
// Ant Design 主题配置
const antdTheme = computed(() => ({

View File

@@ -18,6 +18,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/Scripts.vue'),
meta: { title: '脚本管理' },
},
{
path: '/scripts/:id/edit',
name: 'ScriptEdit',
component: () => import('../views/ScriptEdit.vue'),
meta: { title: '编辑脚本' },
},
{
path: '/plans',
name: 'Plans',
@@ -48,6 +54,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/Settings.vue'),
meta: { title: '设置' },
},
{
path: '/test',
name: 'Test',
component: () => import('../views/TestScript.vue'),
meta: { title: '测试' },
},
]
const router = createRouter({

View File

@@ -4,76 +4,91 @@
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 默认浅色模式 CSS 变量 */
--ant-color-primary: #1677ff;
--ant-color-primary-hover: #4096ff;
--ant-color-primary-bg: #1677ff1a;
--ant-color-text: rgba(0, 0, 0, 0.88);
--ant-color-text-secondary: rgba(0, 0, 0, 0.65);
--ant-color-text-tertiary: rgba(0, 0, 0, 0.45);
--ant-color-bg-container: #ffffff;
--ant-color-bg-layout: #f5f5f5;
--ant-color-bg-elevated: #ffffff;
--ant-color-border: #d9d9d9;
--ant-color-border-secondary: #f0f0f0;
--ant-color-error: #ff4d4f;
--ant-color-success: #52c41a;
--ant-color-warning: #faad14;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
/* 深色模式默认变量 */
:root.dark {
--ant-color-primary: #1677ff;
--ant-color-primary-hover: #4096ff;
--ant-color-primary-bg: #1677ff1a;
--ant-color-text: rgba(255, 255, 255, 0.88);
--ant-color-text-secondary: rgba(255, 255, 255, 0.65);
--ant-color-text-tertiary: rgba(255, 255, 255, 0.45);
--ant-color-bg-container: #141414;
--ant-color-bg-layout: #000000;
--ant-color-bg-elevated: #1f1f1f;
--ant-color-border: #424242;
--ant-color-border-secondary: #303030;
--ant-color-error: #ff4d4f;
--ant-color-success: #52c41a;
--ant-color-warning: #faad14;
}
a:hover {
color: #535bf2;
* {
box-sizing: border-box;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
background-color: var(--ant-color-bg-layout);
color: var(--ant-color-text);
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
width: 100%;
height: 100vh;
overflow: hidden;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
/* 链接样式 */
a {
color: var(--ant-color-primary);
text-decoration: none;
transition: color 0.3s ease;
}
a:hover {
color: var(--ant-color-primary-hover);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--ant-color-bg-layout);
}
::-webkit-scrollbar-thumb {
background: var(--ant-color-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--ant-color-border-secondary);
}

View File

@@ -0,0 +1,14 @@
// Electron API 类型定义
export interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise<string | null>
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export {}

View File

@@ -0,0 +1,171 @@
// 脚本类型定义
export type ScriptType = 'MAA' | 'General'
// MAA脚本配置
export interface MAAScriptConfig {
Info: {
Name: string
Path: string
}
Run: {
ADBSearchRange: number
AnnihilationTimeLimit: number
AnnihilationWeeklyLimit: boolean
ProxyTimesLimit: number
RoutineTimeLimit: number
RunTimesLimit: number
TaskTransitionMethod: string
}
SubConfigsInfo: {
UserData: {
instances: any[]
}
}
}
// General脚本配置
export interface GeneralScriptConfig {
Game: {
Arguments: string
Enabled: boolean
IfForceClose: boolean
Path: string
Style: string
WaitTime: number
}
Info: {
Name: string
RootPath: string
}
Run: {
ProxyTimesLimit: number
RunTimeLimit: number
RunTimesLimit: number
}
Script: {
Arguments: string
ConfigPath: string
ConfigPathMode: string
ErrorLog: string
IfTrackProcess: boolean
LogPath: string
LogPathFormat: string
LogTimeEnd: number
LogTimeStart: number
LogTimeFormat: string
ScriptPath: string
SuccessLog: string
UpdateConfigMode: string
}
SubConfigsInfo: {
UserData: {
instances: any[]
}
}
}
// 脚本基础信息
export interface Script {
id: string
type: ScriptType
name: string
config: MAAScriptConfig | GeneralScriptConfig
users: User[]
}
// 用户配置
export interface User {
id: string
name: string
Data: {
CustomInfrastPlanIndex: string
IfPassCheck: boolean
LastAnnihilationDate: string
LastProxyDate: string
LastSklandDate: string
ProxyTimes: number
}
Info: {
Annihilation: string
Id: string
IfSkland: boolean
InfrastMode: string
MedicineNumb: number
Mode: string
Name: string
Notes: string
Password: string
RemainedDay: number
Routine: boolean
SeriesNumb: string
Server: string
SklandToken: string
Stage: string
StageMode: string
Stage_1: string
Stage_2: string
Stage_3: string
Stage_Remain: string
Status: boolean
}
Notify: {
CompanyWebHookBotUrl: string
Enabled: boolean
IfCompanyWebHookBot: boolean
IfSendMail: boolean
IfSendSixStar: boolean
IfSendStatistic: boolean
IfServerChan: boolean
ServerChanChannel: string
ServerChanKey: string
ServerChanTag: string
ToAddress: string
}
Task: {
IfAutoRoguelike: boolean
IfBase: boolean
IfCombat: boolean
IfMall: boolean
IfMission: boolean
IfReclamation: boolean
IfRecruiting: boolean
IfWakeUp: boolean
}
QFluentWidgets: {
ThemeColor: string
ThemeMode: string
}
}
// API响应类型
export interface AddScriptResponse {
code: number
status: string
message: string
scriptId: string
data: MAAScriptConfig | GeneralScriptConfig
}
// 脚本索引项
export interface ScriptIndexItem {
uid: string
type: 'MaaConfig' | 'GeneralConfig'
}
// 获取脚本API响应
export interface GetScriptsResponse {
code: number
status: string
message: string
index: ScriptIndexItem[]
data: Record<string, MAAScriptConfig | GeneralScriptConfig>
}
// 脚本详情(用于前端展示)
export interface ScriptDetail {
uid: string
type: ScriptType
name: string
config: MAAScriptConfig | GeneralScriptConfig
createTime?: string
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,591 @@
<template>
<div class="page-container">
<h1>脚本管理</h1>
<p>这里是脚本管理内容的占位符</p>
<div class="scripts-container">
<div class="scripts-header">
<div class="header-title">
<FileTextOutlined class="title-icon" />
<h1>脚本管理</h1>
</div>
<a-space size="middle">
<a-button
type="primary"
size="large"
@click="handleAddScript"
class="add-button"
>
<template #icon>
<PlusOutlined />
</template>
添加脚本
</a-button>
<a-button
size="large"
@click="handleRefresh"
class="refresh-button"
>
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</div>
<div class="scripts-content">
<ScriptTable
:scripts="scripts"
@edit="handleEditScript"
@delete="handleDeleteScript"
@add-user="handleAddUser"
@edit-user="handleEditUser"
@delete-user="handleDeleteUser"
/>
</div>
<!-- 脚本类型选择弹窗 -->
<a-modal
v-model:open="typeSelectVisible"
title="选择脚本类型"
:confirm-loading="addLoading"
@ok="handleConfirmAddScript"
@cancel="typeSelectVisible = false"
class="type-select-modal"
width="500px"
>
<div class="type-selection">
<a-radio-group
v-model:value="selectedType"
class="type-radio-group"
>
<a-radio-button value="MAA" class="type-option">
<div class="type-content">
<div class="type-logo-container">
<img src="@/assets/MAA.png" alt="MAA" class="type-logo" />
</div>
<div class="type-info">
<div class="type-title">MAA脚本</div>
<div class="type-description">明日方舟自动化脚本支持日常任务作战等功能</div>
</div>
</div>
</a-radio-button>
<a-radio-button value="General" class="type-option">
<div class="type-content">
<div class="type-logo-container">
<img src="@/assets/AUTO_MAA.png" alt="AUTO MAA" class="type-logo" />
</div>
<div class="type-info">
<div class="type-title">General脚本</div>
<div class="type-description">通用自动化脚本支持自定义游戏和脚本配置</div>
</div>
</div>
</a-radio-button>
</a-radio-group>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { PlusOutlined, ReloadOutlined, FileTextOutlined } from '@ant-design/icons-vue'
import ScriptTable from '@/components/ScriptTable.vue'
import type { Script, User, ScriptType, MAAScriptConfig, GeneralScriptConfig, ScriptDetail } from '@/types/script'
import { useScriptApi } from '@/composables/useScriptApi'
const router = useRouter()
const { addScript, deleteScript, getScripts, loading } = useScriptApi()
const scripts = ref<Script[]>([])
const typeSelectVisible = ref(false)
const selectedType = ref<ScriptType>('MAA')
const addLoading = ref(false)
// 模拟数据
const mockScripts: Script[] = [
{
id: '1',
type: 'MAA',
name: 'MAA自动化脚本',
config: {
Info: {
Name: 'MAA自动化脚本',
Path: 'D:/MAA_For_AutoMAA'
},
Run: {
ADBSearchRange: 3,
AnnihilationTimeLimit: 40,
AnnihilationWeeklyLimit: true,
ProxyTimesLimit: 0,
RoutineTimeLimit: 10,
RunTimesLimit: 5,
TaskTransitionMethod: 'NoAction'
},
SubConfigsInfo: {
UserData: {
instances: []
}
}
} as MAAScriptConfig,
users: [
{
id: 'user1',
name: 'aoxuan',
Data: {
CustomInfrastPlanIndex: '0',
IfPassCheck: true,
LastAnnihilationDate: '2025-07-28',
LastProxyDate: '2025-08-03',
LastSklandDate: '2000-01-01',
ProxyTimes: 2
},
Info: {
Annihilation: 'Annihilation',
Id: '8668',
IfSkland: false,
InfrastMode: 'Normal',
MedicineNumb: 0,
Mode: '简洁',
Name: 'aoxuan',
Notes: '无',
Password: 'BVd/Y56Mts0gLywaz5kqT5lU',
RemainedDay: -1,
Routine: false,
SeriesNumb: '0',
Server: 'Official',
SklandToken: '',
Stage: 'AT-8',
StageMode: '固定',
Stage_1: '-',
Stage_2: '-',
Stage_3: '-',
Stage_Remain: '-',
Status: true
},
Notify: {
CompanyWebHookBotUrl: '',
Enabled: false,
IfCompanyWebHookBot: false,
IfSendMail: false,
IfSendSixStar: false,
IfSendStatistic: false,
IfServerChan: false,
ServerChanChannel: '',
ServerChanKey: '',
ServerChanTag: '',
ToAddress: ''
},
Task: {
IfAutoRoguelike: false,
IfBase: true,
IfCombat: true,
IfMall: true,
IfMission: true,
IfReclamation: false,
IfRecruiting: true,
IfWakeUp: true
},
QFluentWidgets: {
ThemeColor: '#ff009faa',
ThemeMode: 'Auto'
}
}
]
},
{
id: '2',
type: 'General',
name: '通用自动化脚本',
config: {
Game: {
Arguments: '',
Enabled: false,
IfForceClose: false,
Path: '.',
Style: 'Emulator',
WaitTime: 0
},
Info: {
Name: '通用自动化脚本',
RootPath: '.'
},
Run: {
ProxyTimesLimit: 0,
RunTimeLimit: 10,
RunTimesLimit: 3
},
Script: {
Arguments: '',
ConfigPath: '.',
ConfigPathMode: '所有文件 (*)',
ErrorLog: '',
IfTrackProcess: false,
LogPath: '.',
LogPathFormat: '%Y-%m-%d',
LogTimeEnd: 1,
LogTimeStart: 1,
LogTimeFormat: '%Y-%m-%d %H:%M:%S',
ScriptPath: '.',
SuccessLog: '',
UpdateConfigMode: 'Never'
},
SubConfigsInfo: {
UserData: {
instances: []
}
}
} as GeneralScriptConfig,
users: []
}
]
onMounted(() => {
loadScripts()
})
const loadScripts = async () => {
try {
const scriptDetails = await getScripts()
// 将 ScriptDetail 转换为 Script 格式(为了兼容现有的表格组件)
scripts.value = scriptDetails.map(detail => ({
id: detail.uid,
type: detail.type,
name: detail.name,
config: detail.config,
users: [], // 暂时为空后续可以从其他API获取用户数据
createTime: detail.createTime || new Date().toLocaleString()
}))
} catch (error) {
console.error('加载脚本列表失败:', error)
message.error('加载脚本列表失败')
}
}
const handleAddScript = () => {
selectedType.value = 'MAA'
typeSelectVisible.value = true
}
const handleConfirmAddScript = async () => {
addLoading.value = true
try {
const result = await addScript(selectedType.value)
if (result) {
message.success(result.message)
typeSelectVisible.value = false
// 跳转到编辑页面传递API返回的数据
router.push({
path: `/scripts/${result.scriptId}/edit`,
state: {
scriptData: {
id: result.scriptId,
type: selectedType.value,
config: result.data
}
}
})
}
} catch (error) {
console.error('添加脚本失败:', error)
} finally {
addLoading.value = false
}
}
const handleEditScript = (script: Script) => {
// 跳转到独立的编辑页面
router.push(`/scripts/${script.id}/edit`)
}
const handleDeleteScript = async (script: Script) => {
const result = await deleteScript(script.id)
if (result) {
message.success('脚本删除成功')
loadScripts()
}
}
const handleAddUser = (script: Script) => {
// TODO: 实现添加用户功能
message.info('添加用户功能待实现')
}
const handleEditUser = (user: User) => {
// TODO: 实现编辑用户功能
message.info('编辑用户功能待实现')
}
const handleDeleteUser = (user: User) => {
// TODO: 实现删除用户功能
message.info('删除用户功能待实现')
}
const handleRefresh = () => {
loadScripts()
message.success('刷新成功')
}
</script>
<style scoped>
.page-container {
padding: 20px;
.scripts-container {
padding: 32px;
height: 100%;
display: flex;
flex-direction: column;
background: var(--ant-color-bg-layout);
min-height: 100vh;
}
.scripts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding: 0 8px;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon {
font-size: 32px;
color: var(--ant-color-primary);
}
.header-title h1 {
margin: 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.add-button {
height: 48px;
padding: 0 24px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
}
.add-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
}
.refresh-button {
height: 48px;
padding: 0 24px;
font-size: 16px;
font-weight: 500;
border-radius: 12px;
border: 2px solid var(--ant-color-border);
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
transition: all 0.3s ease;
}
.refresh-button:hover {
border-color: var(--ant-color-primary);
color: var(--ant-color-primary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.scripts-content {
flex: 1;
background: var(--ant-color-bg-container);
border-radius: 16px;
padding: 32px;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.08),
0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid var(--ant-color-border-secondary);
transition: all 0.3s ease;
}
.scripts-content:hover {
box-shadow:
0 8px 30px rgba(0, 0, 0, 0.12),
0 2px 6px rgba(0, 0, 0, 0.15);
}
/* 脚本类型选择弹窗样式 */
.type-select-modal :deep(.ant-modal-content) {
border-radius: 16px;
overflow: hidden;
background: var(--ant-color-bg-container);
}
.type-select-modal :deep(.ant-modal-header) {
background: var(--ant-color-bg-container);
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: 24px 32px 20px;
}
.type-select-modal :deep(.ant-modal-title) {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
}
.type-select-modal :deep(.ant-modal-body) {
padding: 32px;
}
.type-selection {
margin: 16px 0;
}
.type-radio-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
}
.type-radio-group :deep(.ant-radio-button-wrapper) {
height: auto;
padding: 0;
border: 2px solid var(--ant-color-border);
border-radius: 12px;
background: var(--ant-color-bg-container);
transition: all 0.3s ease;
overflow: hidden;
}
.type-radio-group :deep(.ant-radio-button-wrapper:hover) {
border-color: var(--ant-color-primary);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.2);
}
.type-radio-group :deep(.ant-radio-button-wrapper-checked) {
border-color: var(--ant-color-primary);
background: var(--ant-color-primary-bg);
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.3);
}
.type-radio-group :deep(.ant-radio-button-wrapper::before) {
display: none;
}
.type-content {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
width: 100%;
}
.type-logo-container {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--ant-color-bg-elevated);
border: 1px solid var(--ant-color-border-secondary);
flex-shrink: 0;
overflow: hidden;
}
.type-logo {
width: 36px;
height: 36px;
object-fit: contain;
transition: all 0.3s ease;
}
.type-info {
flex: 1;
}
.type-title {
font-size: 18px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 4px;
}
.type-description {
font-size: 14px;
color: var(--ant-color-text-secondary);
line-height: 1.5;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.scripts-content {
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.3),
0 1px 3px rgba(0, 0, 0, 0.4);
}
.scripts-content:hover {
box-shadow:
0 8px 30px rgba(0, 0, 0, 0.4),
0 2px 6px rgba(0, 0, 0, 0.5);
}
.add-button {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.add-button:hover {
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.5);
}
.refresh-button:hover {
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.scripts-container {
padding: 16px;
}
.scripts-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-title h1 {
font-size: 24px;
}
.scripts-content {
padding: 16px;
}
.type-content {
padding: 16px;
}
.type-icon {
font-size: 24px;
}
.type-title {
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div style="padding: 20px;">
<h2>脚本API测试</h2>
<a-space>
<a-button type="primary" @click="testAddMAA" :loading="loading">
测试添加MAA脚本
</a-button>
<a-button type="primary" @click="testAddGeneral" :loading="loading">
测试添加General脚本
</a-button>
<a-button @click="goToScripts">
前往脚本管理页面
</a-button>
</a-space>
<div v-if="result" style="margin-top: 20px;">
<h3>API响应结果</h3>
<pre>{{ JSON.stringify(result, null, 2) }}</pre>
<a-space style="margin-top: 10px;">
<a-button type="primary" @click="goToEdit">
前往编辑页面
</a-button>
</a-space>
</div>
<div v-if="error" style="margin-top: 20px; color: red;">
<h3>错误信息</h3>
<p>{{ error }}</p>
</div>
<div style="margin-top: 30px;">
<h3>测试说明</h3>
<ul>
<li>点击"测试添加MAA脚本""测试添加General脚本"来测试API调用</li>
<li>成功后会显示API返回的数据包含scriptId和配置信息</li>
<li>点击"前往编辑页面"可以跳转到编辑页面查看配置</li>
<li>或者直接前往脚本管理页面测试完整流程</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useScriptApi } from '@/composables/useScriptApi'
const router = useRouter()
const { addScript, loading, error } = useScriptApi()
const result = ref<any>(null)
const lastScriptType = ref<'MAA' | 'General'>('MAA')
const testAddMAA = async () => {
result.value = null
lastScriptType.value = 'MAA'
const response = await addScript('MAA')
if (response) {
result.value = response
}
}
const testAddGeneral = async () => {
result.value = null
lastScriptType.value = 'General'
const response = await addScript('General')
if (response) {
result.value = response
}
}
const goToEdit = () => {
if (result.value) {
router.push({
path: `/scripts/${result.value.scriptId}/edit`,
state: {
scriptData: {
id: result.value.scriptId,
type: lastScriptType.value,
config: result.value.data
}
}
})
}
}
const goToScripts = () => {
router.push('/scripts')
}
</script>

View File

@@ -3,7 +3,9 @@
declare global {
interface Window {
electronAPI?: {
openDevTools: () => void
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
}
}
}