rebuild: split core into separate repo; move wg-go integration to internal/ctr/corebind
- go.mod: require git.zkcoi.com/zkcoi/meshray/core, replace => ../Meshray - internal/ctr/corebind: EnhancedBind + registry + adapter (wg-go integration isolated) - cmd/mr-wg migrated from old core/cmd/meshray-core - core/ removed; CHANGELOG updated; fix checkdb vet warning
This commit is contained in:
+183
-8
@@ -261,16 +261,191 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他页面占位 -->
|
||||
<div v-if="['devices', 'services', 'settings'].includes(currentPage)">
|
||||
<h2 class="text-2xl font-bold mb-6">
|
||||
{{ currentPage === 'devices' ? '设备管理' :
|
||||
currentPage === 'services' ? '服务管理' : '系统设置' }}
|
||||
</h2>
|
||||
<div class="card p-6 text-center text-gray-500">
|
||||
<p>页面开发中...</p>
|
||||
<!-- 设备管理页面 -->
|
||||
<div v-if="currentPage === 'devices'">
|
||||
<h2 class="text-2xl font-bold mb-6">设备管理</h2>
|
||||
<div class="card p-6">
|
||||
<el-button type="primary" @click="openCreateDevice">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建设备
|
||||
</el-button>
|
||||
<el-table :data="devices" style="width: 100%; margin-top: 20px;">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="network_id" label="网络ID" width="100"></el-table-column>
|
||||
<el-table-column prop="virtual_ip" label="虚拟IP"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'online' ? 'success' : 'info'">
|
||||
{{ row.status === 'online' ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="downloadDeviceConfig(row)">配置</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteDevice(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="devices.length === 0" class="text-center text-gray-400 py-8">
|
||||
暂无设备,点击「创建设备」开始
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务管理页面 -->
|
||||
<div v-if="currentPage === 'services'">
|
||||
<h2 class="text-2xl font-bold mb-6">服务管理</h2>
|
||||
<div class="card p-6">
|
||||
<el-button type="primary" @click="openCreateService">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建服务
|
||||
</el-button>
|
||||
<el-table :data="services" style="width: 100%; margin-top: 20px;">
|
||||
<el-table-column prop="id" label="ID" width="200"></el-table-column>
|
||||
<el-table-column prop="name" label="名称"></el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100"></el-table-column>
|
||||
<el-table-column prop="address" label="地址"></el-table-column>
|
||||
<el-table-column prop="port" label="端口" width="80"></el-table-column>
|
||||
<el-table-column label="操作" width="260">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editService(row)">编辑</el-button>
|
||||
<el-button size="small" @click="testService(row)">测试</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteService(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="services.length === 0" class="text-center text-gray-400 py-8">
|
||||
暂无服务
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置页面 -->
|
||||
<div v-if="currentPage === 'settings'">
|
||||
<h2 class="text-2xl font-bold mb-6">系统设置</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-bold mb-4">基础设置</h3>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="监听端口">
|
||||
<el-input-number v-model="settings.server_port" :min="1" :max="65535"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="日志级别">
|
||||
<el-select v-model="settings.log_level" style="width: 100%">
|
||||
<el-option label="Debug" value="debug"></el-option>
|
||||
<el-option label="Info" value="info"></el-option>
|
||||
<el-option label="Warn" value="warn"></el-option>
|
||||
<el-option label="Error" value="error"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="主题">
|
||||
<el-select v-model="settings.theme" style="width: 100%">
|
||||
<el-option label="浅色" value="light"></el-option>
|
||||
<el-option label="深色" value="dark"></el-option>
|
||||
<el-option label="跟随系统" value="auto"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="语言">
|
||||
<el-select v-model="settings.language" style="width: 100%">
|
||||
<el-option label="简体中文" value="zh-CN"></el-option>
|
||||
<el-option label="English" value="en-US"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSettings">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-bold mb-4">安全</h3>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="旧密码">
|
||||
<el-input v-model="passwordForm.old_password" type="password" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="passwordForm.new_password" type="password" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码">
|
||||
<el-input v-model="passwordForm.confirm" type="password" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="changePassword">修改密码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="card p-6 mt-6">
|
||||
<h3 class="text-lg font-bold mb-4">运行状态</h3>
|
||||
<p class="mb-2">WireGuard 模式:<el-tag>{{ wgModeDisplay || wgMode || '未知' }}</el-tag></p>
|
||||
<el-button type="warning" @click="restartCore">重启核心服务</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 创建设备对话框 -->
|
||||
<el-dialog v-model="showCreateDeviceDialog" title="创建设备" width="500px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="所属组网">
|
||||
<el-select v-model="deviceForm.network_id" placeholder="请选择组网" style="width: 100%">
|
||||
<el-option v-for="n in networks" :key="n.id" :label="n.name" :value="n.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备名称">
|
||||
<el-input v-model="deviceForm.name" placeholder="请输入设备名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="虚拟IP">
|
||||
<el-input v-model="deviceForm.virtual_ip" placeholder="留空则自动分配"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateDeviceDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="createDevice">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 设备配置对话框 -->
|
||||
<el-dialog v-model="showDeviceConfigDialog" title="设备 WireGuard 配置" width="600px">
|
||||
<el-input type="textarea" :rows="12" v-model="deviceConfigText" readonly></el-input>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="() => { navigator.clipboard && navigator.clipboard.writeText(deviceConfigText); ElementPlus.ElMessage.success('已复制到剪贴板') }">复制</el-button>
|
||||
<el-button @click="showDeviceConfigDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 服务对话框 -->
|
||||
<el-dialog v-model="showServiceDialog" :title="serviceDialogTitle" width="500px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="服务名称">
|
||||
<el-input v-model="serviceForm.name" placeholder="请输入服务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="serviceForm.type" style="width: 100%">
|
||||
<el-option v-for="t in serviceTypes" :key="t.value" :label="t.label" :value="t.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="地址">
|
||||
<el-input v-model="serviceForm.address" placeholder="例如 stun.example.com"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="端口">
|
||||
<el-input-number v-model="serviceForm.port" :min="0" :max="65535"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="serviceForm.auth_username" placeholder="可选"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="serviceForm.auth_password" type="password" show-password placeholder="可选,留空不修改"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showServiceDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveService">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
+450
-133
@@ -12,130 +12,136 @@ const app = createApp({
|
||||
setup() {
|
||||
// 当前页面
|
||||
const currentPage = ref('dashboard');
|
||||
|
||||
|
||||
// 登录状态
|
||||
const isLoggedIn = ref(false);
|
||||
const loggingIn = ref(false);
|
||||
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
|
||||
// Token 存储
|
||||
let authToken = localStorage.getItem('meshray_token') || '';
|
||||
|
||||
|
||||
// 检查是否已登录
|
||||
if (authToken) {
|
||||
isLoggedIn.value = true;
|
||||
// 可以在这里验证 token 是否有效
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
|
||||
// API 基础 URL
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// 统一请求封装:自动注入 Bearer Token,并兼容 {data}|{error}|纯文本 三种响应
|
||||
const apiRequest = async (path, options = {}) => {
|
||||
const headers = {};
|
||||
if (options.body) headers['Content-Type'] = 'application/json';
|
||||
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(API_BASE + path, { ...options, headers });
|
||||
} catch (e) {
|
||||
return { ok: false, error: '网络请求失败:' + e.message };
|
||||
}
|
||||
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
let result = {};
|
||||
try {
|
||||
result = ct.includes('application/json') ? await resp.json() : { text: await resp.text() };
|
||||
} catch (e) { /* 忽略解析错误 */ }
|
||||
|
||||
return {
|
||||
ok: resp.ok,
|
||||
status: resp.status,
|
||||
data: result.data,
|
||||
error: result.error || result.message,
|
||||
text: result.text,
|
||||
raw: result
|
||||
};
|
||||
};
|
||||
|
||||
// ===================== 统计数据 / 组网 =====================
|
||||
const stats = reactive({
|
||||
networkCount: 0,
|
||||
deviceCount: 0,
|
||||
onlineDevices: 0,
|
||||
pendingApprovals: 0
|
||||
});
|
||||
|
||||
// 组网列表
|
||||
|
||||
const networks = ref([]);
|
||||
|
||||
// 对话框控制
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
|
||||
// 新组网表单
|
||||
const newNetwork = reactive({
|
||||
name: '',
|
||||
subnet_ipv4: '10.0.0.0/24',
|
||||
listen_port: 51820,
|
||||
mesh_mode: 'enhanced'
|
||||
});
|
||||
|
||||
// API 基础 URL
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// 加载统计数据
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/dashboard/stats`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.data) {
|
||||
stats.networkCount = result.data.network_count || 0;
|
||||
stats.deviceCount = result.data.device_count || 0;
|
||||
stats.onlineDevices = result.data.online_devices || 0;
|
||||
stats.pendingApprovals = result.data.pending_approvals || 0;
|
||||
}
|
||||
const r = await apiRequest('/dashboard/stats');
|
||||
if (r.ok && r.data) {
|
||||
stats.networkCount = r.data.total_networks || 0;
|
||||
stats.deviceCount = r.data.total_devices || 0;
|
||||
stats.onlineDevices = r.data.online_devices || 0;
|
||||
stats.pendingApprovals = r.data.pending_approvals || 0;
|
||||
} else if (!r.ok && r.status !== 401) {
|
||||
console.error('加载统计数据失败:', r.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载组网列表
|
||||
|
||||
const loadNetworks = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/networks`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
networks.value = result.data || [];
|
||||
const r = await apiRequest('/networks');
|
||||
if (r.ok) {
|
||||
networks.value = r.data || [];
|
||||
} else if (r.status !== 401) {
|
||||
console.error('加载组网列表失败:', r.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载组网列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建组网
|
||||
|
||||
const createNetwork = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/networks`, {
|
||||
const r = await apiRequest('/networks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newNetwork)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('组网创建成功');
|
||||
showCreateNetworkDialog.value = false;
|
||||
|
||||
// 重置表单
|
||||
newNetwork.name = '';
|
||||
newNetwork.subnet_ipv4 = '10.0.0.0/24';
|
||||
newNetwork.listen_port = 51820;
|
||||
|
||||
// 刷新列表
|
||||
await loadNetworks();
|
||||
await loadStats();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '创建失败');
|
||||
ElementPlus.ElMessage.error(r.error || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建组网失败:', error);
|
||||
ElementPlus.ElMessage.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 查看组网详情
|
||||
|
||||
const viewNetwork = (network) => {
|
||||
ElementPlus.ElMessageBox.alert(
|
||||
`组网名称:${network.name}\n` +
|
||||
`子网:${network.subnet_ipv4}\n` +
|
||||
`模式:${network.mesh_mode === 'enhanced' ? '增强' : '原生'}`,
|
||||
'组网详情',
|
||||
{
|
||||
confirmButtonText: '确定'
|
||||
}
|
||||
{ confirmButtonText: '确定' }
|
||||
);
|
||||
};
|
||||
|
||||
// 生成 MeshSeed
|
||||
|
||||
const generateMeshSeed = (network) => {
|
||||
ElementPlus.ElMessageBox.prompt(
|
||||
'请输入 MeshSeed 有效期(小时)',
|
||||
@@ -150,23 +156,12 @@ const app = createApp({
|
||||
try {
|
||||
const hours = parseInt(value);
|
||||
const expiresAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
|
||||
|
||||
const response = await fetch(`${API_BASE}/networks/${network.id}/mesh-seed`, {
|
||||
const r = await apiRequest(`/networks/${network.id}/mesh-seed`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
max_uses: 10,
|
||||
expires_at: expiresAt,
|
||||
ddns_enabled: false
|
||||
})
|
||||
body: JSON.stringify({ max_uses: 10, expires_at: expiresAt, ddns_enabled: false })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const seedUrl = result.data?.seed_url || result.data?.seed;
|
||||
|
||||
if (r.ok) {
|
||||
const seedUrl = (r.data && (r.data.seed_url || r.data.seed)) || '';
|
||||
if (seedUrl) {
|
||||
ElementPlus.ElMessageBox.alert(
|
||||
`MeshSeed 生成成功:<br><br>` +
|
||||
@@ -174,15 +169,13 @@ const app = createApp({
|
||||
`style="width: 100%; padding: 8px; margin-top: 10px; border: 1px solid #ddd;" ` +
|
||||
`onclick="this.select()">`,
|
||||
'MeshSeed',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '复制并关闭'
|
||||
}
|
||||
{ dangerouslyUseHTMLString: true, confirmButtonText: '复制并关闭' }
|
||||
);
|
||||
} else {
|
||||
ElementPlus.ElMessage.success('MeshSeed 生成成功');
|
||||
}
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '生成失败');
|
||||
ElementPlus.ElMessage.error(r.error || '生成失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成 MeshSeed 失败:', error);
|
||||
@@ -190,31 +183,21 @@ const app = createApp({
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 删除组网
|
||||
|
||||
const deleteNetwork = async (network) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定要删除组网 "${network.name}" 吗?此操作不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_BASE}/networks/${network.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const r = await apiRequest(`/networks/${network.id}`, { method: 'DELETE' });
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadNetworks();
|
||||
await loadStats();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
ElementPlus.ElMessage.error(result.error || '删除失败');
|
||||
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
@@ -223,50 +206,366 @@ const app = createApp({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
if (isLoggedIn.value) {
|
||||
loadStats();
|
||||
loadNetworks();
|
||||
|
||||
// ===================== 设备管理 =====================
|
||||
const devices = ref([]);
|
||||
const showCreateDeviceDialog = ref(false);
|
||||
const deviceForm = reactive({ network_id: null, name: '', virtual_ip: '' });
|
||||
const showDeviceConfigDialog = ref(false);
|
||||
const deviceConfigText = ref('');
|
||||
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
const r = await apiRequest('/devices');
|
||||
if (r.ok) {
|
||||
devices.value = r.data || [];
|
||||
} else if (r.status !== 401) {
|
||||
console.error('加载设备列表失败:', r.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateDevice = () => {
|
||||
deviceForm.network_id = networks.value.length ? networks.value[0].id : null;
|
||||
deviceForm.name = '';
|
||||
deviceForm.virtual_ip = '';
|
||||
showCreateDeviceDialog.value = true;
|
||||
};
|
||||
|
||||
const createDevice = async () => {
|
||||
if (!deviceForm.network_id) {
|
||||
ElementPlus.ElMessage.warning('请先选择所属组网');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await apiRequest(`/devices?network_id=${deviceForm.network_id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
network_id: deviceForm.network_id,
|
||||
name: deviceForm.name,
|
||||
virtual_ip: deviceForm.virtual_ip
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
const cfg = (r.data && r.data.wireguard_config) || (r.data && r.data.config_text) || '';
|
||||
if (cfg) {
|
||||
deviceConfigText.value = cfg;
|
||||
showDeviceConfigDialog.value = true;
|
||||
}
|
||||
showCreateDeviceDialog.value = false;
|
||||
ElementPlus.ElMessage.success('设备创建成功');
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建设备失败:', error);
|
||||
ElementPlus.ElMessage.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const downloadDeviceConfig = async (device) => {
|
||||
try {
|
||||
const r = await apiRequest(`/devices/${device.id}/config`);
|
||||
if (!r.ok) {
|
||||
ElementPlus.ElMessage.error(r.error || '获取配置失败');
|
||||
return;
|
||||
}
|
||||
const text = r.text || (r.raw && r.raw.text) || '';
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${device.name || 'device'}-wg.conf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('下载设备配置失败:', error);
|
||||
ElementPlus.ElMessage.error('下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDevice = async (device) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定要删除设备 "${device.name}" 吗?`,
|
||||
'警告',
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
const r = await apiRequest(`/devices/${device.id}`, { method: 'DELETE' });
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadDevices();
|
||||
await loadStats();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除设备失败:', error);
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== 服务管理 =====================
|
||||
const services = ref([]);
|
||||
const showServiceDialog = ref(false);
|
||||
const serviceDialogTitle = ref('创建服务');
|
||||
const serviceForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'turn',
|
||||
address: '',
|
||||
port: null,
|
||||
auth_username: '',
|
||||
auth_password: ''
|
||||
});
|
||||
|
||||
// 处理登录
|
||||
const serviceTypes = [
|
||||
{ value: 'tun', label: 'TUN 隧道' },
|
||||
{ value: 'turn', label: 'TURN 服务' },
|
||||
{ value: 'ddns', label: 'DDNS 服务' }
|
||||
];
|
||||
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
const r = await apiRequest('/services');
|
||||
if (r.ok) {
|
||||
services.value = r.data || [];
|
||||
} else if (r.status !== 401) {
|
||||
console.error('加载服务列表失败:', r.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateService = () => {
|
||||
serviceDialogTitle.value = '创建服务';
|
||||
serviceForm.id = '';
|
||||
serviceForm.name = '';
|
||||
serviceForm.type = 'turn';
|
||||
serviceForm.address = '';
|
||||
serviceForm.port = null;
|
||||
serviceForm.auth_username = '';
|
||||
serviceForm.auth_password = '';
|
||||
showServiceDialog.value = true;
|
||||
};
|
||||
|
||||
const editService = (svc) => {
|
||||
serviceDialogTitle.value = '编辑服务';
|
||||
serviceForm.id = svc.id;
|
||||
serviceForm.name = svc.name;
|
||||
serviceForm.type = svc.type;
|
||||
serviceForm.address = svc.address;
|
||||
serviceForm.port = svc.port;
|
||||
serviceForm.auth_username = svc.username || '';
|
||||
serviceForm.auth_password = '';
|
||||
showServiceDialog.value = true;
|
||||
};
|
||||
|
||||
const saveService = async () => {
|
||||
const payload = {
|
||||
name: serviceForm.name,
|
||||
type: serviceForm.type,
|
||||
address: serviceForm.address,
|
||||
port: serviceForm.port
|
||||
};
|
||||
if (serviceForm.auth_username) payload.username = serviceForm.auth_username;
|
||||
if (serviceForm.auth_password) payload.password = serviceForm.auth_password;
|
||||
|
||||
const isEdit = !!serviceForm.id;
|
||||
const r = isEdit
|
||||
? await apiRequest(`/services/${serviceForm.id}`, { method: 'PUT', body: JSON.stringify(payload) })
|
||||
: await apiRequest('/services', { method: 'POST', body: JSON.stringify(payload) });
|
||||
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success(isEdit ? '更新成功' : '创建成功');
|
||||
showServiceDialog.value = false;
|
||||
await loadServices();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteService = async (svc) => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
`确定要删除服务 "${svc.name}" 吗?`,
|
||||
'警告',
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
const r = await apiRequest(`/services/${svc.id}`, { method: 'DELETE' });
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('删除成功');
|
||||
await loadServices();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除服务失败:', error);
|
||||
ElementPlus.ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testService = async (svc) => {
|
||||
const r = await apiRequest(`/services/${svc.id}/test`, { method: 'POST' });
|
||||
if (r.ok) {
|
||||
const status = (r.data && r.data.status) || 'unknown';
|
||||
ElementPlus.ElMessage.info(`连通性状态:${status}`);
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '测试失败');
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== 系统设置 =====================
|
||||
const settings = reactive({
|
||||
server_port: 51820,
|
||||
log_level: 'info',
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
max_backups: 7,
|
||||
max_age: 30
|
||||
});
|
||||
const wgMode = ref('');
|
||||
const wgModeDisplay = ref('');
|
||||
const passwordForm = reactive({ old_password: '', new_password: '', confirm: '' });
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const r = await apiRequest('/settings');
|
||||
if (r.ok && r.data) {
|
||||
Object.assign(settings, r.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载系统设置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWGMode = async () => {
|
||||
try {
|
||||
const r = await apiRequest('/system-config/wg-mode');
|
||||
if (r.ok && r.data) {
|
||||
wgMode.value = r.data.actual_mode || r.data.wg_mode || '';
|
||||
wgModeDisplay.value = r.data.wg_mode_display || wgMode.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载 WG 模式失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
const r = await apiRequest('/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
server_port: settings.server_port,
|
||||
log_level: settings.log_level,
|
||||
theme: settings.theme,
|
||||
language: settings.language,
|
||||
max_backups: settings.max_backups,
|
||||
max_age: settings.max_age
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('设置已保存');
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!passwordForm.old_password || !passwordForm.new_password) {
|
||||
ElementPlus.ElMessage.warning('请输入旧密码和新密码');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.new_password !== passwordForm.confirm) {
|
||||
ElementPlus.ElMessage.warning('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
const r = await apiRequest('/system/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
old_password: passwordForm.old_password,
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
});
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('密码修改成功');
|
||||
passwordForm.old_password = '';
|
||||
passwordForm.new_password = '';
|
||||
passwordForm.confirm = '';
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '修改失败');
|
||||
}
|
||||
};
|
||||
|
||||
const restartCore = async () => {
|
||||
try {
|
||||
await ElementPlus.ElMessageBox.confirm(
|
||||
'确定要重启核心服务吗?重启过程约需数秒。',
|
||||
'重启核心',
|
||||
{ confirmButtonText: '重启', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
const r = await apiRequest('/system/restart-core', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ graceful: true })
|
||||
});
|
||||
if (r.ok) {
|
||||
ElementPlus.ElMessage.success('核心服务正在重启...');
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(r.error || '重启失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('重启核心失败:', error);
|
||||
ElementPlus.ElMessage.error('重启失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== 登录 / 登出 =====================
|
||||
const loadAll = async () => {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadNetworks(),
|
||||
loadDevices(),
|
||||
loadServices(),
|
||||
loadSettings(),
|
||||
loadWGMode()
|
||||
]);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (isLoggedIn.value) loadAll();
|
||||
});
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.username || !loginForm.password) {
|
||||
ElementPlus.ElMessage.warning('请输入用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
loggingIn.value = true;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
const r = await apiRequest('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.data) {
|
||||
// 保存 token
|
||||
authToken = result.data.token || result.data.access_token;
|
||||
if (r.ok && r.data) {
|
||||
authToken = r.data.token || r.data.access_token;
|
||||
localStorage.setItem('meshray_token', authToken);
|
||||
isLoggedIn.value = true;
|
||||
|
||||
ElementPlus.ElMessage.success('登录成功');
|
||||
|
||||
// 加载数据
|
||||
await loadStats();
|
||||
await loadNetworks();
|
||||
await loadAll();
|
||||
} else {
|
||||
ElementPlus.ElMessage.error(result.error || '登录失败');
|
||||
ElementPlus.ElMessage.error(r.error || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
@@ -275,31 +574,20 @@ const app = createApp({
|
||||
loggingIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理退出
|
||||
|
||||
const handleLogout = () => {
|
||||
ElementPlus.ElMessageBox.confirm(
|
||||
'确定要退出登录吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
// 清除 token
|
||||
ElementPlus.ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
localStorage.removeItem('meshray_token');
|
||||
authToken = '';
|
||||
isLoggedIn.value = false;
|
||||
|
||||
// 重置表单
|
||||
loginForm.username = '';
|
||||
loginForm.password = '';
|
||||
|
||||
ElementPlus.ElMessage.success('已退出登录');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
isLoggedIn,
|
||||
@@ -314,7 +602,36 @@ const app = createApp({
|
||||
createNetwork,
|
||||
viewNetwork,
|
||||
generateMeshSeed,
|
||||
deleteNetwork
|
||||
deleteNetwork,
|
||||
// devices
|
||||
devices,
|
||||
showCreateDeviceDialog,
|
||||
deviceForm,
|
||||
showDeviceConfigDialog,
|
||||
deviceConfigText,
|
||||
openCreateDevice,
|
||||
createDevice,
|
||||
downloadDeviceConfig,
|
||||
deleteDevice,
|
||||
// services
|
||||
services,
|
||||
showServiceDialog,
|
||||
serviceDialogTitle,
|
||||
serviceForm,
|
||||
serviceTypes,
|
||||
openCreateService,
|
||||
editService,
|
||||
saveService,
|
||||
deleteService,
|
||||
testService,
|
||||
// settings
|
||||
settings,
|
||||
wgMode,
|
||||
wgModeDisplay,
|
||||
passwordForm,
|
||||
saveSettings,
|
||||
changePassword,
|
||||
restartCore
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user