refactor: 代码解耦合,完善WebUI功能,添加网页VNC

This commit is contained in:
foxhui
2025-12-20 04:40:05 +08:00
Unverified
parent 41e3e022a3
commit c8c7aec0e1
74 changed files with 4984 additions and 644 deletions
+128
View File
@@ -0,0 +1,128 @@
<script setup>
import { ref, onMounted, reactive, computed } from 'vue';
import { useSettingsStore } from '@/stores/settings';
import { message } from 'ant-design-vue';
import { SettingOutlined, AppstoreOutlined } from '@ant-design/icons-vue';
const settingsStore = useSettingsStore();
const drawerVisible = ref(false);
const currentAdapter = ref(null);
const currentConfig = reactive({});
// 挂载时获取数据
onMounted(async () => {
await Promise.all([
settingsStore.fetchAdaptersMeta(),
settingsStore.fetchAdapterConfig()
]);
});
// 适配器列表
const adapters = computed(() => settingsStore.adaptersMeta);
// 打开抽屉进行编辑
const handleEdit = (adapter) => {
currentAdapter.value = adapter;
// 加载现有配置或默认值
const existing = settingsStore.adapterConfig[adapter.id] || {};
// 重置当前配置表单
Object.keys(currentConfig).forEach(key => delete currentConfig[key]);
// 使用现有值或schema中的默认值初始化表单
if (adapter.configSchema) {
adapter.configSchema.forEach(field => {
if (existing[field.key] !== undefined) {
currentConfig[field.key] = existing[field.key];
} else {
currentConfig[field.key] = field.default;
}
});
}
drawerVisible.value = true;
};
// 保存配置
const handleSave = async () => {
if (!currentAdapter.value) return;
const configToSave = {
[currentAdapter.value.id]: { ...currentConfig }
};
const success = await settingsStore.saveAdapterConfig(configToSave);
if (success) {
drawerVisible.value = false;
}
};
</script>
<template>
<a-layout style="background: transparent;">
<a-card title="适配器管理" :bordered="false">
<template #extra>
<a-button type="link" @click="settingsStore.fetchAdaptersMeta">刷新列表</a-button>
</template>
<a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }" :data-source="adapters">
<template #renderItem="{ item }">
<a-list-item>
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '16px' }">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center;">
<AppstoreOutlined style="font-size: 20px; color: #1890ff; margin-right: 12px;" />
<span style="font-weight: 600; font-size: 15px;">{{ item.id }}</span>
</div>
<SettingOutlined style="font-size: 16px; color: #8c8c8c;" />
</div>
</a-card>
</a-list-item>
</template>
</a-list>
</a-card>
<!-- 配置抽屉 -->
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.name}`"
width="500" placement="right">
<div v-if="!currentAdapter.configSchema || currentAdapter.configSchema.length === 0">
<a-empty description="该适配器没有可配置项" />
</div>
<a-form layout="vertical" v-else>
<template v-for="field in currentAdapter.configSchema" :key="field.key">
<a-form-item :label="field.label" :required="field.required">
<!-- 字符串输入 -->
<a-input v-if="field.type === 'string'" v-model:value="currentConfig[field.key]"
:placeholder="field.placeholder" />
<!-- 数字输入 -->
<a-input-number v-if="field.type === 'number'" v-model:value="currentConfig[field.key]"
:min="field.min" :max="field.max" style="width: 100%;" />
<!-- 布尔开关 -->
<div v-if="field.type === 'boolean'">
<a-switch v-model:checked="currentConfig[field.key]" />
</div>
<!-- 下拉选择 -->
<a-select v-if="field.type === 'select'" v-model:value="currentConfig[field.key]"
:options="field.options" />
<div v-if="field.note" style="font-size: 12px; color: #8c8c8c; margin-top: 4px;">
{{ field.note }}
</div>
</a-form-item>
</template>
</a-form>
<template #footer>
<div style="text-align: right;">
<a-button style="margin-right: 8px" @click="drawerVisible = false">取消</a-button>
<a-button type="primary" @click="handleSave">保存配置</a-button>
</div>
</template>
</a-drawer>
</a-layout>
</template>
+171
View File
@@ -0,0 +1,171 @@
<script setup>
import { onMounted, reactive } from 'vue';
import { useSettingsStore } from '@/stores/settings';
const settingsStore = useSettingsStore();
// 表单数据
const formData = reactive({
path: '',
headless: false,
// 全局代理
proxyEnable: false,
proxyType: 'http',
proxyHost: '127.0.0.1',
proxyPort: 7890,
proxyAuth: false,
proxyUser: '',
proxyPasswd: ''
});
onMounted(async () => {
await settingsStore.fetchBrowserConfig();
const cfg = settingsStore.browserConfig || {};
formData.path = cfg.path || '';
formData.headless = cfg.headless || false;
if (cfg.proxy) {
formData.proxyEnable = cfg.proxy.enable || false;
formData.proxyType = cfg.proxy.type || 'http';
formData.proxyHost = cfg.proxy.host || '';
formData.proxyPort = cfg.proxy.port || 7890;
formData.proxyAuth = cfg.proxy.auth || false;
formData.proxyUser = cfg.proxy.username || '';
formData.proxyPasswd = cfg.proxy.password || '';
}
});
// 保存设置
const handleSave = async () => {
const config = {
path: formData.path,
headless: formData.headless,
proxy: {
enable: formData.proxyEnable,
type: formData.proxyType,
host: formData.proxyHost,
port: formData.proxyPort,
auth: formData.proxyAuth,
username: formData.proxyUser,
password: formData.proxyPasswd
}
};
await settingsStore.saveBrowserConfig(config);
};
</script>
<template>
<a-layout style="background: transparent;">
<a-card title="浏览器设置" :bordered="false" style="width: 100%;">
<a-row :gutter="[16, 16]">
<!-- 浏览器可执行文件路径 -->
<a-col :xs="24" :md="24">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">浏览器可执行文件路径</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
留空则使用 Camoufox 默认下载路径<br>
Windows示例: C:\camoufox\camoufox.exe<br>
Linux示例: /opt/camoufox/camoufox
</div>
<a-input v-model:value="formData.path" placeholder="留空使用默认路径" />
</div>
</a-col>
<!-- 无头模式 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">无头模式</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
启用后浏览器将在后台运行不显示窗口
</div>
<a-switch v-model:checked="formData.headless" />
<span style="margin-left: 8px;">
{{ formData.headless ? '已启用' : '未启用' }}
</span>
</div>
</a-col>
</a-row>
<!-- 全局代理设置折叠面板 -->
<div style="margin-top: 16px;">
<a-collapse>
<a-collapse-panel key="proxy" header="全局代理设置">
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 16px;">
如果实例没有独立配置代理将使用此全局代理配置
</div>
<!-- 是否启用代理 -->
<div style="margin-bottom: 16px;">
<a-switch v-model:checked="formData.proxyEnable" />
<span style="margin-left: 8px;">
{{ formData.proxyEnable ? '已启用全局代理' : '未启用全局代理' }}
</span>
</div>
<!-- 代理类型 -->
<div style="margin-bottom: 16px;" v-if="formData.proxyEnable">
<div style="font-weight: 600; margin-bottom: 8px;">代理类型</div>
<a-segmented v-model:value="formData.proxyType" block :options="[
{ label: 'HTTP', value: 'http' },
{ label: 'SOCKS5', value: 'socks5' }
]" />
</div>
<a-row :gutter="16" v-if="formData.proxyEnable">
<!-- 代理主机 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">代理主机</div>
<a-input v-model:value="formData.proxyHost" placeholder="例如: 127.0.0.1" />
</div>
</a-col>
<!-- 代理端口 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">代理端口</div>
<a-input-number v-model:value="formData.proxyPort" :min="1" :max="65535"
style="width: 100%" placeholder="例如: 7890" />
</div>
</a-col>
</a-row>
<!-- 是否需要验证 -->
<div style="margin-bottom: 16px;" v-if="formData.proxyEnable">
<div style="font-weight: 600; margin-bottom: 8px;">代理认证</div>
<a-switch v-model:checked="formData.proxyAuth" />
<span style="margin-left: 8px;">
{{ formData.proxyAuth ? '需要认证' : '无需认证' }}
</span>
</div>
<a-row :gutter="16" v-if="formData.proxyEnable && formData.proxyAuth">
<!-- 用户名 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">用户名</div>
<a-input v-model:value="formData.proxyUser" placeholder="请输入用户名" />
</div>
</a-col>
<!-- 密码 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">密码</div>
<a-input-password v-model:value="formData.proxyPasswd" placeholder="请输入密码" />
</div>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</div>
<!-- 保存按钮右下角 -->
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
<a-button type="primary" @click="handleSave">
保存设置
</a-button>
</div>
</a-card>
</a-layout>
</template>
+123
View File
@@ -0,0 +1,123 @@
<script setup>
import { onMounted, reactive } from 'vue';
import { useSettingsStore } from '@/stores/settings';
const settingsStore = useSettingsStore();
// 表单数据
const formData = reactive({
port: 5173,
authToken: '',
keepaliveMode: 'comment',
queueBuffer: 2,
imageLimit: 5
});
onMounted(async () => {
await settingsStore.fetchServerConfig();
Object.assign(formData, settingsStore.serverConfig);
});
// 保存设置
const handleSave = async () => {
await settingsStore.saveServerConfig(formData);
};
</script>
<template>
<a-layout style="background: transparent;">
<a-card title="服务器设置" :bordered="false" style="width: 100%;">
<!-- 4宫格表单布局 -->
<a-row :gutter="[16, 16]">
<!-- 监听端口 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">监听端口</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
设置服务器监听的端口号默认为 5173
</div>
<a-input-number v-model:value="formData.port" :min="1" :max="65535" placeholder="请输入端口号"
style="width: 100%" />
</div>
</a-col>
<!-- 鉴权 Token -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">鉴权 Token</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
用于 API 请求鉴权的密钥留空则不启用鉴权
</div>
<a-input-password v-model:value="formData.authToken" placeholder="请输入 Token" type="password" />
</div>
</a-col>
<!-- 心跳包类型 (Keepalive Mode) -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">心跳包类型</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
选择 SSE 流式响应的心跳包格式
</div>
<a-select v-model:value="formData.keepaliveMode" style="width: 100%" placeholder="请选择心跳包类型">
<a-select-option value="comment">Comment - 注释格式</a-select-option>
<a-select-option value="content">Content - 内容格式</a-select-option>
</a-select>
</div>
</a-col>
</a-row>
<!-- 保存按钮右下角 -->
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
<a-button type="primary" @click="handleSave">
保存设置
</a-button>
</div>
</a-card>
<!-- 队列设置 -->
<a-card title="队列设置" :bordered="false" style="width: 100%; margin-top: 10px;">
<a-row :gutter="[16, 16]">
<!-- 队列缓冲区大小 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">队列缓冲区大小</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
非流式请求的额外排队数设为 0 则不限制非流式请求数量<br>
实际队列上限 = Workers数量 + 缓冲区大小
</div>
<a-input-number v-model:value="formData.queueBuffer" :min="0" :max="100" placeholder="默认为 2"
style="width: 100%" />
</div>
</a-col>
<!-- 图片数量上限 -->
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 4px;">图片数量上限</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
单次请求最多支持的图片附件数量<br>
网页最多支持10个附件超出会被丢弃
</div>
<a-input-number v-model:value="formData.imageLimit" :min="1" :max="10" placeholder="默认为 5"
style="width: 100%" />
</div>
</a-col>
</a-row>
<!-- 保存按钮右下角 -->
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
<a-button type="primary" @click="handleSave">
保存设置
</a-button>
</div>
</a-card>
</a-layout>
</template>
<style scoped>
/* 确保在手机端也能正常显示 */
.ant-input-number {
width: 100%;
}
</style>
+490
View File
@@ -0,0 +1,490 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useSettingsStore } from '@/stores/settings';
const settingsStore = useSettingsStore();
const poolConfig = computed({
get: () => settingsStore.poolConfig,
set: (val) => settingsStore.poolConfig = val
});
const handleSavePool = async () => {
await settingsStore.savePoolConfig(poolConfig.value);
};
// 获取初始数据
onMounted(async () => {
await Promise.all([
settingsStore.fetchWorkerConfig(),
settingsStore.fetchPoolConfig(),
settingsStore.fetchAdaptersMeta()
]);
});
// 计算属性:适配器选项
const adapterOptions = computed(() => {
const options = settingsStore.adaptersMeta.map(a => ({
label: a.name,
value: a.id
}));
// 手动添加 Merge 选项(如果没有在元数据中返回)
if (!options.find(o => o.value === 'merge')) {
options.push({ label: 'Merge(聚合模式)', value: 'merge' });
}
return options;
});
// 实例列表表格列定义
const columns = [
{
title: '实例名称',
dataIndex: 'name',
key: 'name',
},
{
title: 'Worker 数量',
dataIndex: 'workerCount',
key: 'workerCount',
},
{
title: '代理',
dataIndex: 'proxy',
key: 'proxy',
},
{
title: '数据标记',
key: 'userDataMark',
dataIndex: 'userDataMark',
},
{
title: '操作',
key: 'action',
},
];
// 实例列表数据 (从 Store 获取)
const instanceData = computed({
get: () => settingsStore.workerConfig,
set: (val) => { settingsStore.workerConfig = val; }
});
// 抽屉状态
const drawerOpen = ref(false);
const editingInstance = ref(null);
// 编辑表单数据
const editForm = ref({
name: '',
userDataMark: '',
proxy: false,
proxyType: 'socks5',
proxyHost: '',
proxyPort: 1080,
proxyAuth: false,
proxyUsername: '',
proxyPassword: '',
workers: []
});
// 创建实例
const handleCreateInstance = () => {
editingInstance.value = null; // null表示创建新实例
const randomSuffix = Math.random().toString(36).substring(2, 7);
// 重置表单为默认值
editForm.value = {
name: `instance-${(instanceData.value || []).length + 1}-${randomSuffix}`,
userDataMark: '',
proxy: false,
proxyType: 'socks5',
proxyHost: '',
proxyPort: 1080,
proxyAuth: false,
proxyUsername: '',
proxyPassword: '',
workers: []
};
drawerOpen.value = true;
};
// 编辑实例
const handleEdit = (record) => {
editingInstance.value = record;
// 填充表单数据
editForm.value = {
name: record.name,
userDataMark: record.userDataMark || '',
proxy: record.proxy ? true : false,
proxyType: record.proxy?.type || 'socks5',
proxyHost: record.proxy?.host || '',
proxyPort: record.proxy?.port || 1080,
proxyAuth: record.proxy?.auth || false,
proxyUsername: record.proxy?.username || '',
proxyPassword: record.proxy?.password || '',
workers: record.workers ? [...record.workers] : []
};
// 兼容前端展示用的 proxy 布尔值
if (record.proxy === null || record.proxy === undefined) {
editForm.value.proxy = false;
}
drawerOpen.value = true;
};
// 删除实例
const handleDelete = async (record) => {
const newList = instanceData.value.filter(item => item.id !== record.id);
await settingsStore.saveWorkerConfig(newList);
};
// 保存编辑
const handleSaveEdit = async () => {
// 构建要保存的对象结构
const instanceToSave = {
id: editingInstance.value ? editingInstance.value.id : `inst_${Date.now()}`,
name: editForm.value.name,
userDataMark: editForm.value.userDataMark,
workers: editForm.value.workers,
// 如果启用了代理,则构建代理对象,否则为 null
proxy: editForm.value.proxy ? {
enable: true,
type: editForm.value.proxyType,
host: editForm.value.proxyHost,
port: editForm.value.proxyPort,
auth: editForm.value.proxyAuth,
username: editForm.value.proxyUsername,
password: editForm.value.proxyPassword
} : null
};
let newList = [...(instanceData.value || [])];
if (editingInstance.value === null) {
// 创建
newList.push(instanceToSave);
} else {
// 更新
const index = newList.findIndex(item => item.id === editingInstance.value.id);
if (index > -1) {
newList[index] = instanceToSave;
}
}
const success = await settingsStore.saveWorkerConfig(newList);
if (success) {
drawerOpen.value = false;
}
};
// 编辑中的Worker索引
const editingWorkerIndex = ref(-1);
const workerFormVisible = ref(false);
const workerForm = ref({
name: '',
type: 'lmarena',
mergeTypes: [],
mergeMonitor: ''
});
// 添加Worker
const handleAddWorker = () => {
editingWorkerIndex.value = -1;
const randomSuffix = Math.random().toString(36).substring(2, 7);
workerForm.value = {
name: `worker-${editForm.value.workers.length + 1}-${randomSuffix}`,
type: 'lmarena',
mergeTypes: [],
mergeMonitor: ''
};
workerFormVisible.value = true;
};
// 编辑Worker
const handleEditWorker = (index) => {
editingWorkerIndex.value = index;
const worker = editForm.value.workers[index];
workerForm.value = {
name: worker.name,
type: worker.type,
mergeTypes: worker.mergeTypes ? [...worker.mergeTypes] : [],
mergeMonitor: worker.mergeMonitor || ''
};
workerFormVisible.value = true;
};
// 保存Worker配置
const handleSaveWorker = () => {
if (editingWorkerIndex.value === -1) {
// 新增
editForm.value.workers.push({ ...workerForm.value });
} else {
// 编辑
editForm.value.workers[editingWorkerIndex.value] = { ...workerForm.value };
}
workerFormVisible.value = false;
};
// 删除Worker
const handleRemoveWorker = (index) => {
editForm.value.workers.splice(index, 1);
};
</script>
<template>
<a-layout style="background: transparent;">
<a-card title="负载均衡" :bordered="false" style="width: 100%; margin-bottom: 10px;">
<!-- 调度策略 -->
<div style="margin-bottom: 24px;">
<div style="font-weight: 600; margin-bottom: 8px;">调度策略</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
选择任务分配到工作实例的调度算法
</div>
<a-segmented v-model:value="poolConfig.strategy" block :options="[
{ label: '最少繁忙', value: 'least_busy' },
{ label: '轮询', value: 'round_robin' },
{ label: '随机', value: 'random' }
]" />
</div>
<!-- 故障转移 -->
<a-row :gutter="16">
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 8px;">故障转移</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
启用后任务失败时会自动切换到其他可用实例重试
</div>
<a-switch v-model:checked="poolConfig.failover.enabled" />
</div>
</a-col>
<a-col :xs="24" :md="12">
<div style="margin-bottom: 8px;">
<div style="font-weight: 600; margin-bottom: 8px;">重试次数</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 12px;">
故障转移时最大重试次数范围 1-10
</div>
<a-input-number v-model:value="poolConfig.failover.maxRetries" :min="1" :max="10"
:disabled="!poolConfig.failover.enabled" style="width: 100%" placeholder="请输入重试次数" />
</div>
</a-col>
</a-row>
<!-- 保存按钮 -->
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
<a-button type="primary" @click="handleSavePool">
保存设置
</a-button>
</div>
</a-card>
<a-card :bordered="false" style="width: 100%;">
<!-- 卡片标题和创建按钮 -->
<template #title>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>实例列表</span>
<a-button type="primary" @click="handleCreateInstance">
创建实例
</a-button>
</div>
</template>
<!-- 实例表格 -->
<a-table :columns="columns" :data-source="instanceData" :pagination="false">
<template #bodyCell="{ column, record }">
<!-- 实例名称 -->
<template v-if="column.key === 'name'">
<a>{{ record.name }}</a>
</template>
<!-- Worker 数量 -->
<template v-else-if="column.key === 'workerCount'">
{{ record.workers ? record.workers.length : 0 }}
</template>
<!-- 代理状态 -->
<template v-else-if="column.key === 'proxy'">
<a-tag :color="record.proxy ? 'green' : 'default'">
{{ record.proxy ? '已启用' : '未启用' }}
</a-tag>
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'action'">
<span>
<a @click="handleEdit(record)">编辑</a>
<a-divider type="vertical" />
<a style="color: #ff4d4f" @click="handleDelete(record)">删除</a>
</span>
</template>
</template>
</a-table>
</a-card>
<!-- 编辑/创建抽屉 -->
<a-drawer v-model:open="drawerOpen"
:title="editingInstance === null ? '创建实例' : `编辑实例 - ${editingInstance.name}`" placement="right" width="500">
<div style="margin-bottom: 24px;">
<!-- 实例名称 -->
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 4px;">实例名称</div>
<div style="font-size: 12px; color: #ff4d4f; margin-bottom: 8px;">
* 名称必须全局唯一不可重复
</div>
<a-input v-model:value="editForm.name" placeholder="请输入实例名称" />
</div>
<!-- 数据标记 -->
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 4px;">数据标记</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
用于区分实例数据存储的文件夹名称 (userDataMark)
</div>
<a-input v-model:value="editForm.userDataMark" placeholder="请输入数据标记,如: main-gemini" />
</div>
<!-- 代理设置折叠面板 -->
<div style="margin-bottom: 16px;">
<a-collapse>
<a-collapse-panel key="proxy" header="代理设置">
<!-- 是否启用代理 -->
<div style="margin-bottom: 16px;">
<a-switch v-model:checked="editForm.proxy" />
<span style="margin-left: 8px;">
{{ editForm.proxy ? '已启用代理' : '未启用代理' }}
</span>
</div>
<!-- 代理类型 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
<div style="font-weight: 600; margin-bottom: 8px;">代理类型</div>
<a-segmented v-model:value="editForm.proxyType" block :options="[
{ label: 'SOCKS5', value: 'socks5' },
{ label: 'HTTP', value: 'http' }
]" style="width: 100%" />
</div>
<!-- 服务器地址 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
<div style="font-weight: 600; margin-bottom: 8px;">服务器地址</div>
<a-input v-model:value="editForm.proxyHost" placeholder="例如: 127.0.0.1" />
</div>
<!-- 端口 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
<div style="font-weight: 600; margin-bottom: 8px;">端口</div>
<a-input-number v-model:value="editForm.proxyPort" :min="1" :max="65535"
style="width: 100%" placeholder="例如: 1080" />
</div>
<!-- 是否需要验证 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy">
<div style="font-weight: 600; margin-bottom: 8px;">身份验证</div>
<a-switch v-model:checked="editForm.proxyAuth" />
<span style="margin-left: 8px;">
{{ editForm.proxyAuth ? '需要验证' : '无需验证' }}
</span>
</div>
<!-- 用户名 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy && editForm.proxyAuth">
<div style="font-weight: 600; margin-bottom: 8px;">用户名</div>
<a-input v-model:value="editForm.proxyUsername" placeholder="请输入用户名" />
</div>
<!-- 密码 -->
<div style="margin-bottom: 16px;" v-if="editForm.proxy && editForm.proxyAuth">
<div style="font-weight: 600; margin-bottom: 8px;">密码</div>
<a-input-password v-model:value="editForm.proxyPassword" placeholder="请输入密码" />
</div>
</a-collapse-panel>
</a-collapse>
</div>
<!-- Worker 列表 -->
<div>
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-weight: 600;">Worker 列表</div>
<a-button size="small" type="primary" @click="handleAddWorker">
添加 Worker
</a-button>
</div>
<a-list bordered :data-source="editForm.workers" style="margin-top: 8px;">
<template #renderItem="{ item, index }">
<a-list-item>
<template #actions>
<a @click="handleEditWorker(index)">编辑</a>
<a style="color: #ff4d4f" @click="handleRemoveWorker(index)">删除</a>
</template>
<div>
<div style="font-weight: 600;">{{ item.name }}</div>
<div style="font-size: 12px; color: #8c8c8c;">
类型: {{ item.type }}
<span v-if="item.type === 'merge'">
| 聚合: {{ item.mergeTypes?.join(', ') || '' }}
<span v-if="item.mergeMonitor">
| 监控: {{ item.mergeMonitor }}
</span>
</span>
</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</div>
<!-- 抽屉底部保存按钮 -->
<template #footer>
<div style="text-align: right;">
<a-button style="margin-right: 8px" @click="drawerOpen = false">取消</a-button>
<a-button type="primary" @click="handleSaveEdit">保存</a-button>
</div>
</template>
</a-drawer>
<!-- Worker配置模态框 -->
<a-modal v-model:open="workerFormVisible" :title="editingWorkerIndex === -1 ? '添加 Worker' : '编辑 Worker'"
okText="确定" cancelText="取消" @ok="handleSaveWorker">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 4px;">Worker 名称</div>
<div style="font-size: 12px; color: #ff4d4f; margin-bottom: 8px;">
* 名称必须全局唯一不可重复
</div>
<a-input v-model:value="workerForm.name" placeholder="例如: default" />
</div>
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 8px;">适配器类型</div>
<a-select v-model:value="workerForm.type" style="width: 100%" :options="adapterOptions" />
</div>
<!-- Merge 模式额外配置 -->
<template v-if="workerForm.type === 'merge'">
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 4px;">聚合类型</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
选择要聚合的后端适配器可多选
</div>
<a-select v-model:value="workerForm.mergeTypes" mode="multiple" style="width: 100%"
placeholder="选择要聚合的适配器" :options="adapterOptions">
</a-select>
</div>
<div style="margin-bottom: 16px;">
<div style="font-weight: 600; margin-bottom: 4px;">空闲监控后端</div>
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 8px;">
空闲时挂机监控的后端可选
</div>
<a-select v-model:value="workerForm.mergeMonitor" style="width: 100%" placeholder="选择监控后端(可留空)"
allow-clear>
<a-select-option value=""></a-select-option>
<a-select-option v-for="type in workerForm.mergeTypes" :key="type" :value="type">
{{ type }}
</a-select-option>
</a-select>
</div>
</template>
</a-modal>
</a-layout>
</template>