mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-14 05:37:23 +08:00
Feat/web (Patchset 4) (#460)
support basic functions in frontend 1. create/del network 2. inspect network running status
This commit is contained in:
131
easytier-web/frontend/src/App.vue
Normal file
131
easytier-web/frontend/src/App.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { onMounted } from 'vue';
|
||||
import Login from './components/Login.vue'
|
||||
import { Button } from 'primevue';
|
||||
import ApiClient from './modules/api';
|
||||
import DeviceList from './components/DeviceList.vue';
|
||||
|
||||
const api = new ApiClient('http://10.147.223.128:11211/api/v1/'); // Replace with actual API URL
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
|
||||
<template>
|
||||
<div id="root" class="">
|
||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<button data-drawer-target="logo-sidebar" data-drawer-toggle="logo-sidebar" aria-controls="logo-sidebar"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<i class="pi pi-list" style="font-size: 1.3rem"></i>
|
||||
</button>
|
||||
<a href="https://flowbite.com" class="flex ms-2 md:me-24">
|
||||
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8 me-3" alt="FlowBite Logo" />
|
||||
<span
|
||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center ms-3">
|
||||
<div>
|
||||
<button type="button"
|
||||
class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||
aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
|
||||
alt="user photo">
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
Neil Sims
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||
neil.sims@flowbite.com
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Dashboard</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Earnings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside id="logo-sidebar"
|
||||
class="fixed top-0 left-0 z-40 w-64 h-screen pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||
<i class="pi pi-chart-pie" style="font-size: 1.2rem"></i>
|
||||
<span class="mb-0.5">DashBoard</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||
<i class="pi pi-server" style="font-size: 1.2rem"></i>
|
||||
<span class="mb-0.5">Devices</span>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||
<DeviceList :api="api"></DeviceList>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||
<Login :api="api"></Login>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
</style>
|
||||
1
easytier-web/frontend/src/assets/vue.svg
Normal file
1
easytier-web/frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
243
easytier-web/frontend/src/components/DeviceList.vue
Normal file
243
easytier-web/frontend/src/components/DeviceList.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import ApiClient, { ValidateConfigResponse } from '../modules/api';
|
||||
import { Config, Status, NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { Button, Column, DataTable, Drawer, Toolbar, IftaLabel, Select, Dialog, ConfirmPopup, useConfirm } from 'primevue';
|
||||
|
||||
function toHexString(uint64: bigint, padding = 9): string {
|
||||
let hexString = uint64.toString(16);
|
||||
while (hexString.length < padding) {
|
||||
hexString = '0' + hexString;
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
|
||||
// 将两个 uint64 转换为 16 进制字符串
|
||||
const part1Hex = toHexString(BigInt(part1), 8);
|
||||
const part2Hex = toHexString(BigInt(part2), 8);
|
||||
const part3Hex = toHexString(BigInt(part3), 8);
|
||||
const part4Hex = toHexString(BigInt(part4), 8);
|
||||
|
||||
// 构造 UUID 格式字符串
|
||||
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
interface UUID {
|
||||
part1: number;
|
||||
part2: number;
|
||||
part3: number;
|
||||
part4: number;
|
||||
}
|
||||
|
||||
function UuidToStr(uuid: UUID): string {
|
||||
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
interface DeviceList {
|
||||
hostname: string;
|
||||
public_ip: string;
|
||||
running_network_count: number;
|
||||
report_time: string;
|
||||
easytier_version: string;
|
||||
running_network_instances?: Array<string>;
|
||||
machine_id: string;
|
||||
}
|
||||
|
||||
const selectedDevice = ref<DeviceList | null>(null);
|
||||
const deviceList = ref<Array<DeviceList>>([]);
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = selectedDevice.value?.running_network_instances || [];
|
||||
let options = insts.map((instance: string) => {
|
||||
return { uuid: instance };
|
||||
});
|
||||
console.log("options", options);
|
||||
return options;
|
||||
});
|
||||
const selectedInstanceId = ref<any | null>(null);
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const loadDevices = async () => {
|
||||
const resp = await api?.list_machines();
|
||||
console.log(resp);
|
||||
let devices: Array<DeviceList> = [];
|
||||
for (const device of (resp || [])) {
|
||||
devices.push({
|
||||
hostname: device.info?.hostname,
|
||||
public_ip: device.client_url,
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
|
||||
running_network_count: device.info?.running_network_instances.length,
|
||||
report_time: device.info?.report_time,
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: UuidToStr(device.info?.machine_id),
|
||||
});
|
||||
}
|
||||
deviceList.value = devices;
|
||||
console.log(deviceList.value);
|
||||
};
|
||||
|
||||
interface SelectedDevice {
|
||||
machine_id: string;
|
||||
instance_id: string;
|
||||
}
|
||||
|
||||
const checkDeviceSelected = (): SelectedDevice => {
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
let inst_id = selectedInstanceId.value?.uuid;
|
||||
if (machine_id && inst_id) {
|
||||
return { machine_id, instance_id: inst_id };
|
||||
} else {
|
||||
throw new Error("No device selected");
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeviceInfo = async () => {
|
||||
let selectedDevice = checkDeviceSelected();
|
||||
if (!selectedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await api?.get_network_info(selectedDevice.machine_id, selectedDevice.instance_id);
|
||||
let device_info = ret[selectedDevice.instance_id]
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: selectedDevice.instance_id,
|
||||
running: device_info.running,
|
||||
error_msg: device_info.error_msg,
|
||||
detail: device_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setInterval(loadDevices, 1000);
|
||||
setInterval(loadDeviceInfo, 1000);
|
||||
});
|
||||
|
||||
const visibleRight = ref(false);
|
||||
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
if (!machine_id) {
|
||||
throw new Error("No machine selected");
|
||||
}
|
||||
|
||||
if (!newNetworkConfig.value) {
|
||||
throw new Error("No network config");
|
||||
}
|
||||
|
||||
let ret = await api?.validate_config(machine_id, newNetworkConfig.value);
|
||||
console.log("verifyNetworkConfig", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const createNewNetwork = async () => {
|
||||
let config = await verifyNetworkConfig();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
if (!machine_id) {
|
||||
throw new Error("No machine selected");
|
||||
}
|
||||
|
||||
let ret = await api?.run_network(machine_id, config?.toml_config);
|
||||
console.log("createNewNetwork", ret);
|
||||
showCreateNetworkDialog.value = false;
|
||||
await loadDevices();
|
||||
}
|
||||
|
||||
const confirm = useConfirm();
|
||||
const confirmDeleteNetwork = (event: any) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: 'Do you want to delete this network?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
const ret = checkDeviceSelected();
|
||||
await api?.delete_network(ret?.machine_id, ret?.instance_id);
|
||||
await loadDevices();
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<template>
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal header="Create New Network" :style="{ width: '55rem' }">
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
|
||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||
:sortOrder="-1">
|
||||
<template #header>
|
||||
<div class="text-xl font-bold">Device List</div>
|
||||
</template>
|
||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
||||
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
||||
<Column field="report_time" header="Report Time" sortable style="width: 150px"></Column>
|
||||
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||
<Column class="w-24 !text-end">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-search" @click="selectedDevice = data; visibleRight = true" severity="secondary"
|
||||
rounded></Button>
|
||||
</template>
|
||||
</Column>
|
||||
<template #footer>
|
||||
<div class="flex justify-start">
|
||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<Drawer v-model:visible="visibleRight" header="Device Management" position="right" class="w-1/2 min-w-96">
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<IftaLabel>
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid"
|
||||
inputId="dd-inst-id" placeholder="Select Instance" />
|
||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||
</IftaLabel>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="gap-x-3 flex">
|
||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||
iconPos="right" />
|
||||
<Button @click="showCreateNetworkDialog = true" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo">
|
||||
|
||||
</Status>
|
||||
</Drawer>
|
||||
</template>
|
||||
93
easytier-web/frontend/src/components/Login.vue
Normal file
93
easytier-web/frontend/src/components/Login.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">{{ isRegistering ? 'Register' : 'Login' }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="username" class="block text-sm font-medium">Username</label>
|
||||
<InputText id="username" v-model="username" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="password" v-model="password" required toggleMask />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Login" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="button" class="w-full" @click="isRegistering = true"
|
||||
severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="register-username" class="block text-sm font-medium">Username</label>
|
||||
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||
<InputText id="captcha" v-model="captcha" required class="w-full" />
|
||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Back to Login" type="button" class="w-full" @click="isRegistering = false"
|
||||
severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button } from 'primevue';
|
||||
import ApiClient from '../modules/api';
|
||||
import { Credential } from '../modules/api';
|
||||
|
||||
const props = defineProps({
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const registerUsername = ref('');
|
||||
const registerPassword = ref('');
|
||||
const captcha = ref('');
|
||||
const captchaSrc = computed(() => api?.captcha_url());
|
||||
const isRegistering = ref(false);
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
console.log('Username:', username.value);
|
||||
console.log('Password:', password.value);
|
||||
// Add your login logic here
|
||||
const credential: Credential = { username: username.value, password: password.value, };
|
||||
const ret = await api?.login(credential);
|
||||
alert(ret?.message);
|
||||
};
|
||||
|
||||
const onRegister = () => {
|
||||
console.log('Register Username:', registerUsername.value);
|
||||
console.log('Register Password:', registerPassword.value);
|
||||
console.log('Captcha:', captcha.value);
|
||||
// Add your register logic here
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
24
easytier-web/frontend/src/main.ts
Normal file
24
easytier-web/frontend/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import 'easytier-frontend-lib/style.css'
|
||||
import App from './App.vue'
|
||||
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
|
||||
createApp(App).use(PrimeVue,
|
||||
{
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: {
|
||||
name: 'primevue',
|
||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
||||
128
easytier-web/frontend/src/modules/api.ts
Normal file
128
easytier-web/frontend/src/modules/api.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
message: string;
|
||||
user: any; // 同上
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credential: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl,
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.log('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||
console.error('Response Error:', error.response.data);
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但是没有收到响应
|
||||
console.error('Request Error:', error.request);
|
||||
} else {
|
||||
// 发生了一些问题导致请求未发出
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, message: 'Invalid username or password', };
|
||||
} else {
|
||||
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
const response = await this.client.get('/sessions');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async list_machines(): Promise<Array<any>> {
|
||||
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||
return response.machines;
|
||||
}
|
||||
|
||||
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
|
||||
return response.info.map;
|
||||
}
|
||||
|
||||
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||
config: config,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async run_network(machine_id: string, config: string): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
|
||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
||||
}
|
||||
|
||||
public captcha_url() {
|
||||
return this.client.defaults.baseURL + 'auth/captcha';
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
33
easytier-web/frontend/src/style.css
Normal file
33
easytier-web/frontend/src/style.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@layer tailwind-base, primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
|
||||
@layer tailwind-utilities {
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
.p-password {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-password>input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user