feat: 添加用户认证功能,包含登录、管理控制台和节点提交页面

This commit is contained in:
ioococ
2025-11-16 19:16:35 +08:00
parent 13bd80471b
commit 7e03f12817
6 changed files with 822 additions and 17 deletions

View File

@@ -1,33 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import { ref, onMounted } from 'vue'
import { isAuthenticated } from '@/utils/request/api'
const year = new Date().getFullYear() const year = new Date().getFullYear()
const isLoggedIn = ref(false)
onMounted(() => {
isLoggedIn.value = isAuthenticated()
// Re-check auth status on storage changes
window.addEventListener('storage', () => {
isLoggedIn.value = isAuthenticated()
})
})
</script> </script>
<template> <template>
<div class="layout"> <div class="layout">
<header class="navbar bg-base-100 shadow-md"> <header class="navbar bg-base-100 shadow-md">
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><RouterLink to="/">主页</RouterLink></li>
<li><RouterLink to="/monitor">节点监控</RouterLink></li>
<li><RouterLink to="/submit">提交节点</RouterLink></li>
</ul>
</div>
<div class="flex-1"> <div class="flex-1">
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1> <h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
</div> </div>
</div> </div>
<div class="navbar-end hidden lg:flex"> <div class="navbar-end lg:flex">
<ul class="menu menu-horizontal px-1 gap-2"> <ul class="menu menu-horizontal px-1 gap-2">
<li><RouterLink to="/" class="btn btn-ghost">主页</RouterLink></li> <li><RouterLink to="/" class="btn btn-ghost">主页</RouterLink></li>
<li><RouterLink to="/monitor" class="btn btn-ghost">节点监控</RouterLink></li> <li><RouterLink to="/monitor" class="btn btn-ghost">节点监控</RouterLink></li>
<li><RouterLink to="/submit" class="btn btn-ghost">提交节点</RouterLink></li> <li><RouterLink to="/submit" class="btn btn-ghost">提交节点</RouterLink></li>
<li v-if="isLoggedIn"><RouterLink to="/dashboard" class="btn btn-ghost">管理控制台</RouterLink></li>
<li v-else><RouterLink to="/login" class="btn btn-ghost">登录</RouterLink></li>
</ul> </ul>
</div> </div>
</header> </header>

View File

@@ -19,6 +19,16 @@ const router = createRouter({
name: 'submit', name: 'submit',
component: () => import('../views/SubmitView.vue'), component: () => import('../views/SubmitView.vue'),
}, },
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
},
], ],
}) })

88
src/utils/request/api.ts Normal file
View File

@@ -0,0 +1,88 @@
import Api from './index'
export interface HealthResponse {
status?: string
uptime?: number
[key: string]: any
}
export function getHealth() {
const path = import.meta.env.VITE_HEALTH_PATH || '/health'
return Api.get<HealthResponse>(path)
}
export function getApiDocsUrl() {
return import.meta.env.VITE_API_DOCS_URL || `${import.meta.env.VITE_API_BASE_URL}/api`
}
export interface LoginResponse {
token: string
[key: string]: any
}
export async function login(username: string, password: string) {
const res = (await Api.post<LoginResponse>('/auth/login', { username, password })) as unknown as LoginResponse
const token = (res as any)?.data.token
if (token) {
localStorage.setItem('auth_token', token)
}
return res
}
export function logout() {
localStorage.removeItem('auth_token')
}
export function isAuthenticated() {
return !!localStorage.getItem('auth_token')
}
export function getMe() {
return (Api.get('/auth/me') as Promise<any>)
}
// Admin list (requires admin role)
export function listAdmins() {
return Api.get('/admins') as Promise<any[]>
}
export function deleteAdmin(id: string | number) {
return Api.delete(`/admins/${id}`)
}
export function createApiKey(data: any) {
return Api.post('/api-keys', data)
}
export function listApiKeys() {
return Api.get('/api-keys') as Promise<any[]>
}
export function toggleApiKeyStatus(id: string | number, status: string) {
return Api.patch(`/api-keys/${id}/status`, { status })
}
export function deleteApiKey(id: string | number) {
return Api.delete(`/api-keys/${id}`)
}
export function createNode(data: any) {
return Api.post('/nodes', data)
}
export function listNodes() {
return Api.get('/nodes') as Promise<any[]>
}
export function deleteNode(id: string | number) {
return Api.delete(`/nodes/${id}`)
}
export function updateNodeStatus(id: string | number, status: string) {
return Api.put(`/nodes/${id}/status`, { status })
}
// Public peers (optional API key)
export function getPeers() {
return Api.get('/peers') as Promise<any[]>
}

433
src/views/DashboardView.vue Normal file
View File

@@ -0,0 +1,433 @@
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import {
getMe,
listAdmins,
deleteAdmin,
listApiKeys,
createApiKey,
toggleApiKeyStatus,
deleteApiKey,
listNodes,
deleteNode,
updateNodeStatus,
logout
} from '@/utils/request/api'
const router = useRouter()
// User info
const currentUser = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)
// Active tab
const activeTab = ref<'nodes' | 'apiKeys' | 'users'>('nodes')
// Nodes
const nodeList = ref<any[]>([])
const loadingNodes = ref(false)
// API Keys
const apiKeys = ref<any[]>([])
const loadingApiKeys = ref(false)
const showApiKeyForm = ref(false)
const apiKeyForm = ref({ name: '', rateLimit: 100 })
// Admins
const userList = ref<any[]>([])
const loadingAdmins = ref(false)
const isAdmin = computed(() => currentUser.value?.username === 'admin')
async function fetchUserInfo() {
try {
const { data } = await getMe()
currentUser.value = data
} catch (e: any) {
console.error(e);
error.value = '未授权,请先登录'
logout()
setTimeout(() => router.push('/login'), 1500)
} finally {
loading.value = false
}
}
// Nodes management
async function fetchNodes() {
loadingNodes.value = true
try {
const {data} = await listNodes()
nodeList.value = data
} catch (e: any) {
console.error('获取节点失败:', e)
} finally {
loadingNodes.value = false
}
}
async function handleDeleteNode(id: string | number) {
if (!confirm('确定删除此节点?')) return
try {
await deleteNode(id)
await fetchNodes()
} catch (e: any) {
alert('删除失败: ' + (e.message || e))
}
}
async function handleToggleNodeStatus(node: any) {
try {
const newStatus = node.status === 'active' ? 'inactive' : 'active'
await updateNodeStatus(node.id, newStatus)
await fetchNodes()
} catch (e: any) {
alert('更新状态失败: ' + (e.message || e))
}
}
// API Keys management
async function fetchApiKeys() {
loadingApiKeys.value = true
try {
const {data} = await listApiKeys()
apiKeys.value = data
} catch (e: any) {
console.error('获取 API Keys 失败:', e)
} finally {
loadingApiKeys.value = false
}
}
async function handleCreateApiKey() {
if (!apiKeyForm.value.name) return
try {
await createApiKey(apiKeyForm.value)
apiKeyForm.value = { name: '', rateLimit: 100 }
showApiKeyForm.value = false
await fetchApiKeys()
} catch (e: any) {
alert('创建 API Key 失败: ' + (e.message || e))
}
}
async function handleToggleApiKeyStatus(key: any) {
try {
const newStatus = key.is_active === 'active' ? 'inactive' : 'active'
await toggleApiKeyStatus(key.id, newStatus)
await fetchApiKeys()
} catch (e: any) {
alert('更新状态失败: ' + (e.message || e))
}
}
async function handleDeleteApiKey(id: string | number) {
if (!confirm('确定删除此 API Key')) return
try {
await deleteApiKey(id)
await fetchApiKeys()
} catch (e: any) {
alert('删除失败: ' + (e.message || e))
}
}
// Admins management
async function fetchAdmins() {
if (!isAdmin.value) return
loadingAdmins.value = true
try {
const { data } = await listAdmins()
userList.value = data
} catch (e: any) {
console.error('获取管理员列表失败:', e)
} finally {
loadingAdmins.value = false
}
}
async function handleDeleteAdmin(id: string | number) {
if (!confirm('确定删除此管理员?')) return
try {
await deleteAdmin(id)
await fetchAdmins()
} catch (e: any) {
alert('删除失败: ' + (e.message || e))
}
}
function handleLogout() {
logout()
router.push('/login')
}
// Tab switching
function switchTab(tab: 'nodes' | 'apiKeys' | 'users') {
activeTab.value = tab
if (tab === 'nodes' && nodeList.value.length === 0) fetchNodes()
if (tab === 'apiKeys' && apiKeys.value.length === 0) fetchApiKeys()
if (tab === 'users' && userList.value.length === 0) fetchAdmins()
}
onMounted(async () => {
await fetchUserInfo()
if (!error.value) await fetchNodes()
})
</script>
<template>
<div class="max-w-7xl mx-auto">
<div v-if="loading" class="text-center py-8">加载中...</div>
<div v-else-if="error" class="alert alert-error">{{ error }}</div>
<div v-else>
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold">管理控制台</h1>
<p class="text-base-content/70 mt-1">
欢迎{{ currentUser?.username }}
<span v-if="currentUser?.role" class="badge badge-sm ml-2">{{ currentUser.role }}</span>
</p>
</div>
<button class="btn btn-ghost btn-sm" @click="handleLogout">退出登录</button>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-bordered mb-4">
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'nodes' }"
@click="switchTab('nodes')">
节点管理
</a>
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'apiKeys' }"
@click="switchTab('apiKeys')">
API Keys
</a>
<a v-if="isAdmin" role="tab" class="tab" :class="{ 'tab-active': activeTab === 'users' }"
@click="switchTab('users')">
管理员
</a>
</div>
<!-- Nodes Tab -->
<div v-show="activeTab === 'nodes'" class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">节点列表</h2>
</div>
<!-- Nodes Table -->
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>地址</th>
<th>协议</th>
<th>最大连接</th>
<th>中继</th>
<th>区域</th>
<th>运营商</th>
<th>状态</th>
<th>响应时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingNodes">
<td colspan="11" class="text-center">加载中...</td>
</tr>
<tr v-else-if="nodeList.length === 0">
<td colspan="11" class="text-center text-base-content/60">暂无节点</td>
</tr>
<tr v-else v-for="node in nodeList" :key="node.id">
<td>{{ node.id }}</td>
<td>
<div class="font-medium">{{ node.name }}</div>
<div v-if="node.description" class="text-xs text-base-content/60">{{ node.description }}</div>
</td>
<td>
<div class="font-mono text-sm">{{ node.host }}:{{ node.port }}</div>
<div v-if="node.network_name" class="text-xs text-base-content/60">网络: {{ node.network_name }}</div>
</td>
<td>
<span class="badge badge-sm">{{ node.protocol?.toUpperCase() }}</span>
</td>
<td>{{ node.max_connections }}</td>
<td>
<span v-if="node.allow_relay" class="badge badge-success badge-sm">允许</span>
<span v-else class="badge badge-ghost badge-sm">禁止</span>
</td>
<td>{{ node.region || '-' }}</td>
<td>{{ node.ISP || '-' }}</td>
<td>
<span
class="badge badge-sm"
:class="{
'badge-success': node.status === 'Online',
'badge-error': node.status === 'Offline',
'badge-warning': node.status === 'Checking',
'badge-ghost': !node.status
}"
>
{{ node.status || 'Unknown' }}
</span>
<div v-if="node.last_status_update" class="text-xs text-base-content/60 mt-1">
{{ new Date(node.last_status_update).toLocaleString() }}
</div>
</td>
<td>
<span v-if="node.response_time">{{ node.response_time }}ms</span>
<span v-else class="text-base-content/60">-</span>
</td>
<td>
<div class="flex gap-1">
<button
class="btn btn-xs btn-ghost"
@click="handleToggleNodeStatus(node)"
:title="node.status === 'Online' ? '设为离线' : '设为在线'"
>
切换
</button>
<button
class="btn btn-xs btn-error"
@click="handleDeleteNode(node.id)"
>
删除
</button>
</div>
<div v-if="node.qq_number || node.mail" class="text-xs text-base-content/60 mt-1">
<div v-if="node.qq_number">QQ: {{ node.qq_number }}</div>
<div v-if="node.mail">{{ node.mail }}</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- API Keys Tab -->
<div v-show="activeTab === 'apiKeys'" class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">API Key 列表</h2>
<button class="btn btn-primary btn-sm" @click="showApiKeyForm = !showApiKeyForm">
{{ showApiKeyForm ? '取消' : '+ 创建 API Key' }}
</button>
</div>
<!-- API Key Form -->
<div v-if="showApiKeyForm" class="card bg-base-200 shadow">
<div class="card-body">
<h3 class="card-title text-lg">创建新 API Key</h3>
<form @submit.prevent="handleCreateApiKey" class="grid gap-3">
<input v-model="apiKeyForm.name" type="text" placeholder="API Key 名称"
class="input input-bordered" required />
<input v-model.number="apiKeyForm.rateLimit" type="number" placeholder="速率限制(次/分钟)"
class="input input-bordered" min="1" />
<button type="submit" class="btn btn-primary">创建</button>
</form>
</div>
</div>
<!-- API Keys Table -->
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>ID</th>
<th>Key</th>
<th>描述</th>
<th>速率限制</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingApiKeys">
<td colspan="6" class="text-center">加载中...</td>
</tr>
<tr v-else-if="apiKeys.length === 0">
<td colspan="6" class="text-center text-base-content/60">暂无 API Key</td>
</tr>
<tr v-else v-for="key in apiKeys" :key="key.id">
<td>{{ key.id }}</td>
<td class="font-mono text-xs">{{ key.key }}</td>
<td>{{ key.description }}</td>
<td>{{ key.rate_limit}}</td>
<td>
<span class="badge" :class="key.status === 'active' ? 'badge-success' : 'badge-ghost'">
{{ key.is_active }}
</span>
</td>
<td>
<button class="btn btn-xs btn-ghost" @click="handleToggleApiKeyStatus(key)" >
切换状态
</button>
<button class="btn btn-xs btn-error ml-1" @click="handleDeleteApiKey(key.id)">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Admins Tab -->
<div v-if="isAdmin" v-show="activeTab === 'users'" class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">管理员列表</h2>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>Email</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingAdmins">
<td colspan="6" class="text-center">加载中...</td>
</tr>
<tr v-else-if="userList.length === 0">
<td colspan="6" class="text-center text-base-content/60">暂无管理员</td>
</tr>
<tr v-else v-for="user in userList" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ new Date(user.created_at).toLocaleDateString()}}</td>
<td>{{ new Date(user.updated_at).toLocaleDateString()}}</td>
<td>
<button v-if="user.id !== currentUser?.id" class="btn btn-xs btn-error" @click="handleDeleteAdmin(user.id)">
删除
</button>
<span v-else class="text-xs text-base-content/60">当前用户</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.tabs {
margin-bottom: 1.5rem;
}
</style>

70
src/views/LoginView.vue Normal file
View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { login } from '@/utils/request/api'
const router = useRouter()
const username = ref('admin')
const password = ref('admin123')
const loading = ref(false)
const error = ref<string | null>(null)
const success = ref(false)
async function onSubmit() {
if (!username.value || !password.value) {
error.value = '请输入用户名和密码'
return
}
loading.value = true
error.value = null
success.value = false
try {
await login(username.value, password.value)
success.value = true
// 登录成功后跳转到管理控制台
setTimeout(() => router.push('/dashboard'), 600)
} catch (e: any) {
error.value = e?.message || '登录失败,请重试'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-[calc(100vh-200px)] flex items-center justify-center">
<div class="max-w-md w-full mx-auto px-4">
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-2xl justify-center mb-4">登录</h2>
<form @submit.prevent="onSubmit" class="flex flex-col gap-4">
<label class="form-control w-full">
<div class="label">
<span class="label-text font-medium">用户账户</span>
</div>
<input v-model="username" type="text" class="input input-bordered w-full" autocomplete="username" />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text font-medium">登陆密码</span>
</div>
<input v-model="password" type="password" class="input input-bordered w-full" autocomplete="current-password" />
</label>
<div v-if="error" class="alert alert-error">
<span>{{ error }}</span>
</div>
<button class="btn btn-primary mt-2 w-full" type="submit" :disabled="loading">
<span>{{loading ? '登录中...' : '登录' }}</span>
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,10 +1,211 @@
<template> <template>
<div> <div class="flex justify-center px-4">
<h2>提交节点</h2> <div class="w-full max-w-3xl">
<p>这里是提交节点页面</p> <h2 class="text-3xl font-bold mb-2">提交节点</h2>
<p class="text-base-content/70 mb-6">填写以下信息提交新的 EasyTier 节点</p>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h3 class="card-title text-xl mb-4">节点信息</h3>
<form @submit.prevent="handleCreateNode" class="grid gap-4">
<!-- 基础信息 -->
<div class="grid md:grid-cols-2 gap-4">
<label class="form-control">
<div class="label">
<span class="label-text">节点名称 <span class="text-error">*</span></span>
</div>
<input v-model="nodeForm.name" type="text" placeholder="例如:北京联通节点" class="input input-bordered" required />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">主机地址 <span class="text-error">*</span></span>
</div>
<input v-model="nodeForm.host" type="text" placeholder="例如example.com 或 192.168.1.1" class="input input-bordered" required />
</label>
</div>
<div class="grid md:grid-cols-2 gap-4">
<label class="form-control">
<div class="label">
<span class="label-text">端口 <span class="text-error">*</span></span>
</div>
<input v-model.number="nodeForm.port" type="number" placeholder="例如11010" class="input input-bordered" min="1" max="65535" required />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">最大连接数 <span class="text-error">*</span></span>
</div>
<input v-model.number="nodeForm.max_connections" type="number" placeholder="例如10" class="input input-bordered" min="1" required />
</label>
</div>
<!-- 协议选择 -->
<div class="form-control">
<label class="label">
<span class="label-text">协议 <span class="text-error">*</span></span>
</label>
<div class="flex gap-4 flex-wrap">
<label class="label cursor-pointer gap-2">
<input v-model="nodeForm.protocol" type="radio" value="http" class="radio radio-primary" required />
<span>HTTP</span>
</label>
<label class="label cursor-pointer gap-2">
<input v-model="nodeForm.protocol" type="radio" value="https" class="radio radio-primary" />
<span>HTTPS</span>
</label>
<label class="label cursor-pointer gap-2">
<input v-model="nodeForm.protocol" type="radio" value="ws" class="radio radio-primary" />
<span>WS</span>
</label>
<label class="label cursor-pointer gap-2">
<input v-model="nodeForm.protocol" type="radio" value="wss" class="radio radio-primary" />
<span>WSS</span>
</label>
</div>
</div>
<!-- 允许中继 -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input v-model="nodeForm.allow_relay" type="checkbox" class="checkbox checkbox-primary" />
<span class="label-text">允许中继连接</span>
</label>
</div>
<!-- 网络配置 -->
<div class="divider">网络配置</div>
<div class="grid md:grid-cols-2 gap-4">
<label class="form-control">
<div class="label">
<span class="label-text">网络名称</span>
</div>
<input v-model="nodeForm.network_name" type="text" placeholder="默认网络" class="input input-bordered" />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">网络密钥</span>
</div>
<input v-model="nodeForm.network_secret" type="password" placeholder="留空表示无密钥" class="input input-bordered" />
</label>
</div>
<!-- 地理位置 -->
<div class="divider">地理位置</div>
<div class="grid md:grid-cols-2 gap-4">
<label class="form-control">
<div class="label">
<span class="label-text">区域位置</span>
</div>
<input v-model="nodeForm.region" type="text" placeholder="例如:华北、华东" class="input input-bordered" />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">运营商</span>
</div>
<input v-model="nodeForm.ISP" type="text" placeholder="例如:电信、联通、移动" class="input input-bordered" />
</label>
</div>
<!-- 联系方式 -->
<div class="divider">联系方式</div>
<div class="grid md:grid-cols-2 gap-4">
<label class="form-control">
<div class="label">
<span class="label-text">QQ 号码</span>
</div>
<input v-model="nodeForm.qq_number" type="text" placeholder="例如123456789" class="input input-bordered" />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">邮箱地址</span>
</div>
<input v-model="nodeForm.mail" type="email" placeholder="例如admin@example.com" class="input input-bordered" />
</label>
</div>
<!-- 描述 -->
<label class="form-control">
<div class="label">
<span class="label-text">节点描述</span>
</div>
<textarea v-model="nodeForm.description" placeholder="介绍一下这个节点..." class="textarea textarea-bordered h-24"></textarea>
</label>
<!-- 提交按钮 -->
<div class="card-actions justify-end mt-4">
<button type="button" class="btn btn-ghost" @click="resetForm">重置</button>
<button type="submit" class="btn btn-primary" :disabled="submitting">
<span v-if="submitting" class="loading loading-spinner loading-sm"></span>
<span>{{ submitting ? '提交中...' : '提交节点' }}</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 可根据需要添加逻辑 import { createNode } from '@/utils/request/api'
import { ref } from 'vue'
const nodeForm = ref({
name: '',
host: '',
port: 11010,
protocol: 'http',
allow_relay: true,
network_name: null as string | null,
network_secret: null as string | null,
max_connections: 10,
region: null as string | null,
ISP: null as string | null,
qq_number: null as string | null,
mail: null as string | null,
description: ''
})
const submitting = ref(false)
function resetForm() {
nodeForm.value = {
name: '',
host: '',
port: 11010,
protocol: 'http',
allow_relay: true,
network_name: null,
network_secret: null,
max_connections: 10,
region: null,
ISP: null,
qq_number: null,
mail: null,
description: ''
}
}
async function handleCreateNode() {
if (!nodeForm.value.name || !nodeForm.value.host) return
submitting.value = true
try {
await createNode(nodeForm.value)
alert('节点创建成功!')
resetForm()
} catch (e: any) {
alert('创建节点失败: ' + (e.message || e))
} finally {
submitting.value = false
}
}
</script> </script>