🎨基本功能完善

This commit is contained in:
coward 2024-05-24 16:08:29 +08:00
parent 6a37c8d902
commit 66982f3a7b
22 changed files with 1256 additions and 32 deletions

View File

@ -47,6 +47,7 @@
"url": "https://github.com/xiaoxian521"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.4.7",
@ -64,6 +65,7 @@
"path": "^0.12.7",
"pinia": "^2.1.7",
"pinyin-pro": "^3.20.4",
"plus-pro-components": "^0.1.4",
"qs": "^6.12.1",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.2",

19
pnpm-lock.yaml generated
View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.1
version: 2.3.1(vue@3.4.27)
'@pureadmin/descriptions':
specifier: ^1.2.1
version: 1.2.1(echarts@5.5.0)(element-plus@2.7.1)(typescript@5.4.5)
@ -56,6 +59,9 @@ dependencies:
pinyin-pro:
specifier: ^3.20.4
version: 3.20.4
plus-pro-components:
specifier: ^0.1.4
version: 0.1.4(element-plus@2.7.1)(vue@3.4.27)
qs:
specifier: ^6.12.1
version: 6.12.1
@ -4623,6 +4629,19 @@ packages:
dev: false
optional: true
/plus-pro-components@0.1.4(element-plus@2.7.1)(vue@3.4.27):
resolution: {integrity: sha512-lglcrqYQqYM20GgxN+Hg9te/DfNHeERTBSP3EKzWAw6mSl3TU2LYVJIvPgzxAFtoRcg8f9Ybv9vVgtFHSH3JVg==}
peerDependencies:
element-plus: ^2.3.4
vue: ^3.2.0
dependencies:
'@element-plus/icons-vue': 2.3.1(vue@3.4.27)
element-plus: 2.7.1(vue@3.4.27)
lodash-es: 4.17.21
sortablejs: 1.15.2
vue: 3.4.27(typescript@5.4.5)
dev: false
/popmotion@11.0.5:
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
dependencies:

29
src/api/clients.ts Normal file
View File

@ -0,0 +1,29 @@
import { http } from "@/utils/http";
import { baseUri } from "@/api/utils";
// 获取服务端信息
export const getClients = (params?: object) => {
return http.request<any>("get", baseUri("/client/list"), { params });
};
// 下载客户端配置文件
export const downloadClient = (id: string) => {
return http.request<any>("post", baseUri("/client/download/" + id), null, {
responseType: "arraybuffer"
});
};
// 获取客户端配置二维码
export const clientQrCode = (id: string) => {
return http.request<any>("post", baseUri("/client/generate-qrcode/" + id));
};
// 新增/更新客户端信息
export const saveClient = (data?: object) => {
return http.request<any>("post", baseUri("/client/save"), { data });
};
// 删除客户端
export const deleteClient = (id: string) => {
return http.request<any>("delete", baseUri("/client/" + id));
};

27
src/api/server.ts Normal file
View File

@ -0,0 +1,27 @@
import { http } from "@/utils/http";
import { baseUri } from "@/api/utils";
// 获取服务端信息
export const getServer = () => {
return http.request<any>("get", baseUri("/server"));
};
// 更新服务端信息
export const updateServer = (data?: object) => {
return http.request<any>("post", baseUri("/server"), { data });
};
// 获取全局配置
export const getGlobalConfig = () => {
return http.request<any>("get", baseUri("/setting/server"));
};
// 更新全局配置
export const updateGlobalSetting = (data?: object) => {
return http.request<any>("post", baseUri("/setting/server-global"), { data });
};
// 获取当前主机的公网IP
export const getPublicIP = () => {
return http.request<any>("get", baseUri("/setting/public-ip"));
};

View File

@ -1,4 +1,4 @@
// 这里存放本地图标,在 src/layout/list.vue 文件中加载,避免在首启动加载
// 这里存放本地图标,在 src/layout/server.vue 文件中加载,避免在首启动加载
import { addIcon } from "@iconify/vue/dist/offline";
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标

View File

@ -5,20 +5,20 @@ export default {
},
children: [
{
path: "/server/index",
name: "Server",
component: () => import("@/views/server/list.vue"),
path: "/server/config",
name: "Clients",
component: () => import("@/views/server/clients.vue"),
meta: {
title: "服务端列表",
title: "客户端",
showParent: true
}
},
{
path: "/server/config",
name: "ServerConfig",
component: () => import("@/views/server/config.vue"),
path: "/server/index",
name: "Server",
component: () => import("@/views/server/server.vue"),
meta: {
title: "服务端配置",
title: "服务端",
showParent: true
}
}

View File

@ -0,0 +1,83 @@
import { defineStore } from "pinia";
import type { globalSettingType } from "@/store/types";
import { store } from "@/store";
import {getGlobalConfig, updateGlobalSetting, updateServer} from "@/api/server";
import {state} from "vue-tsc/out/shared";
export const useGlobalSettingStore = defineStore({
id: "pure-globalSetting",
state: (): globalSettingType => ({
endpointAddress: "",
dnsServer: [],
MTU: 0,
persistentKeepalive: 0,
firewallMark: "",
table: "",
configFilePath: ""
}),
getters: {
getGlobalSetting(state) {
return state;
},
getEndpointAddress(state) {
return state.endpointAddress;
}
},
actions: {
SET_ENDPOINT_ADDRESS(endpointAddress: string) {
this.endpointAddress = endpointAddress;
},
SET_DNS_SERVER(dnsServer: []) {
this.dnsServer = dnsServer;
},
SET_MTU(MTU: number) {
this.MTU = MTU;
},
SET_PERSISTENT_KEEPALIVE(persistentKeepalive: string) {
this.persistentKeepalive = persistentKeepalive;
},
SET_FIREWALL_MARK(firewallMark: string) {
this.firewallMark = firewallMark;
},
SET_TABLE(table: number) {
this.table = table;
},
SET_CONFIG_FILE_PATH(configFilePath: number) {
this.configFilePath = configFilePath;
},
async getGlobalSettingApi() {
return new Promise<any>((resolve, reject) => {
getGlobalConfig()
.then(data => {
if (data.code === 200) {
this.SET_ENDPOINT_ADDRESS(data.data.endpointAddress);
this.SET_DNS_SERVER(data.data.dnsServer);
this.SET_MTU(data.data.MTU);
this.SET_PERSISTENT_KEEPALIVE(data.data.persistentKeepalive);
this.SET_FIREWALL_MARK(data.data.firewallMark);
this.SET_TABLE(data.data.table);
this.SET_CONFIG_FILE_PATH(data.data.configFilePath);
}
resolve(data);
})
.catch(error => {
reject(error);
});
});
},
async updateGlobalSettingApi(data?: object) {
return new Promise<any>((resolve, reject) => {
updateGlobalSetting(data)
.then(res => {
resolve(res);
})
.catch(error => {
reject(error);
});
});
}
}
});
export function useGlobalSettingStoreHook() {
return useGlobalSettingStore(store);
}

View File

@ -0,0 +1,86 @@
import { defineStore } from "pinia";
import type { serverType } from "@/store/types";
import { store } from "@/store";
import {getServer, updateServer} from "@/api/server";
import {storageLocal} from "@pureadmin/utils";
export const useServerStore = defineStore({
id: "pure-server",
state: (): serverType => ({
id: "",
ipScope: "",
listenPort: 0,
privateKey: "",
publicKey: "",
postUpScript: "",
preDownScript: "",
postDownScript: ""
}),
getters: {
getServerInfo(state) {
return state;
}
},
actions: {
SET_ID(id: string) {
this.id = id;
},
SET_IP_SCOPE(ipScope: string) {
this.ipScope = ipScope;
},
SET_LISTEN_PORT(listenPort: number) {
this.listenPort = listenPort;
},
SET_PRIVATE_KEY(privateKey: string) {
this.privateKey = privateKey;
},
SET_PUBLIC_KEY(publicKey: string) {
this.publicKey = publicKey;
},
SET_POST_UP_SCRIPT(postUpScript: number) {
this.postUpScript = postUpScript;
},
SET_PRE_UP_SCRIPT(preDownScript: number) {
this.preDownScript = preDownScript;
},
SET_POST_DOWN_SCRIPT(postDownScript: number) {
this.postDownScript = postDownScript;
},
async getServerApi() {
return new Promise<any>((resolve, reject) => {
getServer()
.then(data => {
if (data.code === 200) {
this.SET_ID(data.data.id);
this.SET_IP_SCOPE(data.data.ipScope);
this.SET_LISTEN_PORT(data.data.listenPort);
this.SET_PRIVATE_KEY(data.data.privateKey);
this.SET_PUBLIC_KEY(data.data.publicKey);
this.SET_POST_UP_SCRIPT(data.data.postUpScript);
this.SET_PRE_UP_SCRIPT(data.data.preDownScript);
this.SET_POST_DOWN_SCRIPT(data.data.postDownScript);
}
storageLocal().setItem("server-info",data.data);
resolve(data);
})
.catch(error => {
reject(error);
});
});
},
async updateServerApi(data?: object) {
return new Promise<any>((resolve, reject) => {
updateServer(data)
.then(res => {
resolve(res);
})
.catch(error => {
reject(error);
});
});
}
}
});
export function useServerStoreHook() {
return useServerStore(store);
}

View File

@ -39,7 +39,7 @@ export const useUserStore = defineStore({
this.status = status;
},
/** 登入 */
async loginByUsername(data: any) {
async loginByUsername(data?: any) {
return new Promise<any>((resolve, reject) => {
login(data)
.then(data => {

View File

@ -45,3 +45,24 @@ export type userType = {
isAdmin?: number;
status?: number;
};
export type serverType = {
id?: string;
ipScope?: string;
listenPort?: number;
privateKey?: string;
publicKey?: string;
postUpScript?: string;
preDownScript?: string;
postDownScript?: string;
};
export type globalSettingType = {
endpointAddress?: string;
dnsServer?: [];
MTU?: number;
persistentKeepalive?: number;
firewallMark?: string;
table?: string;
configFilePath?: string;
};

View File

@ -11,8 +11,11 @@ import type {
} from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { getToken, formatToken, TokenKey, multipleTabsKey } from "@/utils/auth";
import { message } from "@/utils/message";
import { storageLocal } from "@pureadmin/utils";
import Cookies from "js-cookie";
import router from "@/router";
// 相关配置请参考www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
@ -82,6 +85,16 @@ class PureHttp {
const now = new Date().getTime() / 1000;
const expired = parseInt(data.expireAt) - now <= 0;
if (expired) {
message("登陆已过期", { type: "error" });
router.replace({
path: "/login",
query: {
redirect: router.currentRoute.value.fullPath
}
});
Cookies.remove(TokenKey);
Cookies.remove(multipleTabsKey);
storageLocal().clear();
resolve(PureHttp.retryOriginalRequest(config));
} else {
config.headers["Authorization"] = data.token;

View File

@ -30,7 +30,7 @@ interface MessageParams {
onClose?: Function | null;
}
/** 用法非常简单,参考 src/views/components/message/list.vue 文件 */
/** 用法非常简单,参考 src/views/components/message/server.vue 文件 */
/**
* `Message`

View File

@ -0,0 +1,442 @@
<script setup lang="ts">
import {
deleteClient,
downloadClient,
getClients,
saveClient
} from "@/api/clients";
import { h, reactive, ref } from "vue";
import { addDialog } from "@/components/ReDialog/index";
import qrCodeForms from "./component/qrCode.vue";
import editClientForms from "./component/detail.vue";
import { type PlusColumn, PlusSearch } from "plus-pro-components";
import "plus-pro-components/es/components/search/style/css";
import { storageLocal } from "@pureadmin/utils";
import { ArrowDown } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
defineOptions({
// name name
name: "Clients"
});
const editClientFormRef = ref();
const clientSearchForm = ref({
name: "",
email: "",
ip: "",
createUser: "",
enabled: undefined,
current: 1,
size: 9
});
const clientSearchProps = {
gutter: 0
};
const clientSearchFormColumns: PlusColumn[] = [
{
label: "名称",
prop: "name",
valueType: "copy",
fieldProps: {
placeholder: "请输入"
}
},
{
label: "邮箱",
prop: "email",
valueType: "copy",
fieldProps: {
placeholder: "请输入"
}
},
{
label: "IP",
prop: "ip",
valueType: "copy",
fieldProps: {
placeholder: "请输入"
}
},
{
label: "创建人",
prop: "createUser",
valueType: "copy",
fieldProps: {
placeholder: "请输入"
}
},
{
label: "状态",
prop: "enabled",
valueType: "select",
options: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
],
fieldProps: {
placeholder: "请选择"
}
}
];
const clientsList = reactive({
data: [],
total: 0
});
const getClientsApi = (data?: object) => {
getClients(data).then(clients => {
if (clients.code === 200) {
clientsList.data = clients.data.records;
clientsList.total = clients.data.total;
}
});
};
//
const downloadClientConfig = (id: string, clientName: string) => {
downloadClient(id).then(response => {
if (response) {
const blob = new Blob([response], {
type: "text/plain"
});
const link = document.createElement("a"); // a
link.download = clientName + ".conf"; // a
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click(); //
URL.revokeObjectURL(link.href); // url
document.body.removeChild(link); //
}
});
};
//
const openQrCodeDialog = (clientName: string, id: string) => {
addDialog({
width: "20%",
title: clientName,
contentRenderer: () => h(qrCodeForms),
props: {
formInline: {
id: id
}
},
hideFooter: true
});
};
//
const openAddClientDialog = () => {
const serverInfo = storageLocal().getItem("server-info");
addDialog({
width: "40%",
title: "新增",
contentRenderer: () => h(editClientForms, { ref: editClientFormRef }),
props: {
formInline: {
id: "",
serverId: serverInfo.id,
name: "",
email: "",
subnetRange: "",
ipAllocation: "",
allowedIPS: "",
extraAllowedIPS: "",
endpoint: "",
useServerDNS: 0,
enableAfterCreation: 0,
keys: null,
enabled: 1
}
},
beforeSure: (done, { options }) => {
const FormRef = editClientFormRef.value.getDetailFormRef();
FormRef.validate(valid => {
if (!valid) return;
saveClient(options.props.formInline).then(res => {
if (res.code === 200) {
done();
getClientsApi(clientSearchForm.value);
}
});
});
}
});
};
//
const openEditClientDialog = (client?: any) => {
const serverInfo = storageLocal().getItem("server-info");
addDialog({
width: "40%",
title: client.name,
contentRenderer: () => h(editClientForms, { ref: editClientFormRef }),
props: {
formInline: {
id: client.id,
serverId: serverInfo.id,
name: client.name,
email: client.email,
subnetRange: client.subnetRange,
ipAllocation: client.ipAllocation,
allowedIPS: client.allowedIPS,
extraAllowedIPS: client.extraAllowedIPS,
endpoint: client.endpoint,
useServerDNS: client.useServerDNS,
enableAfterCreation: client.enableAfterCreation,
keys: client.keys,
enabled: Number(client.enabled)
}
},
beforeSure: (done, { options }) => {
const FormRef = editClientFormRef.value.getDetailFormRef();
FormRef.validate(valid => {
if (!valid) return;
saveClient(options.props.formInline).then(res => {
if (res.code === 200) {
done();
getClientsApi(clientSearchForm.value);
}
});
});
}
});
};
//
const openDeleteMessageBox = (clientName: string, clientId: string) => {
ElMessageBox.confirm("是否删除:" + clientName, "删除", {
distinguishCancelAndClose: true,
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
center: true
}).then(() => {
deleteClient(clientId).then(res => {
if (res.code === 200) {
getClientsApi(clientSearchForm.value);
}
});
});
};
//
const searchHandler = (value: object) => {
getClientsApi(value);
};
//
const resetSearchHandler = (value: object) => {
getClientsApi({
current: 1,
size: 9
});
clientSearchForm.value.current = 1;
clientSearchForm.value.size = 9;
};
//
const pageChange = (page: number, size: number) => {
clientSearchForm.value.size = size;
clientSearchForm.value.current = page;
getClientsApi(clientSearchForm.value);
};
//
const ellipsis = (str: string) => {
if (!str) return "";
if (str.length >= 10) {
return str.slice(0, 10) + "...";
}
return str;
};
getClientsApi(clientSearchForm.value);
</script>
<template>
<div>
<div class="search-header" style="padding-bottom: 5px">
<el-card>
<PlusSearch
v-model="clientSearchForm"
resetText="重置"
searchText="搜索"
:hasUnfold="false"
:columns="clientSearchFormColumns"
:rowProps="clientSearchProps"
label-width="80"
label-position="right"
@search="searchHandler"
@reset="resetSearchHandler"
/>
</el-card>
<div style="margin-top: 5px">
<el-card>
<el-button type="primary" @click="openAddClientDialog"
>新增客户端</el-button
>
</el-card>
</div>
</div>
<div class="content">
<el-card body-style="padding: inherit" shadow="hover">
<div class="flex flex-wrap gap-4">
<el-card
v-for="val in clientsList.data"
style="width: 540px"
shadow="hover"
>
<template #header>
<div class="card-header">
<el-tooltip :content="val.name" placement="top">
<el-tag size="large">
{{ ellipsis(val.name) }}
</el-tag>
</el-tooltip>
<el-dropdown
trigger="click"
type="primary"
split-button
style="float: right"
>
更多
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
type="warning"
@click="downloadClientConfig(val.id, val.name)"
>下载</el-button
>
</el-dropdown-item>
<el-dropdown-item>
<el-button
type="danger"
@click="openDeleteMessageBox(val.name, val.id)"
>删除</el-button
>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
type="success"
style="float: right; margin-right: 5px"
@click="openQrCodeDialog(val.name, val.id)"
>二维码</el-button
>
<el-button
type="primary"
style="float: right; margin-right: 5px"
@click="openEditClientDialog(val)"
>
详情
</el-button>
</div>
</template>
<el-form label-position="top">
<el-form-item prop="name" label="名称">
<el-input v-model="val.name" />
</el-form-item>
<el-form-item prop="email" label="邮箱">
<el-input v-model="val.email" />
</el-form-item>
<el-form-item prop="ipAllocation" label="客户端IP">
<el-select
v-model="val.ipAllocation"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="primary"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item prop="allowedIPS" label="允许的IP范围">
<el-select
v-model="val.allowedIPS"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="danger"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item label="创建人">
<el-tag effect="dark" type="primary">{{
val.createUser
}}</el-tag>
</el-form-item>
<el-form-item label="客户端状态">
<el-tag v-if="val.enabled" effect="dark" type="success"
>启用</el-tag
>
<el-tag v-else effect="dark" type="warning">禁用</el-tag>
</el-form-item>
<el-form-item class="timeItem">
<p>创建时间: {{ val.createdAt }}</p>
<p>更新时间: {{ val.updatedAt }}</p>
</el-form-item>
</el-form>
</el-card>
</div>
<div class="paginate" style="background-color: #ffffff; margin-top: 5px">
<el-card>
<el-pagination
small
background
layout="total,prev,pager,next"
:page-size="clientSearchForm.size"
:total="clientsList.total"
@change="pageChange"
/>
</el-card>
</div>
</el-card>
</div>
</div>
</template>
<style lang="scss">
.timeItem {
.el-form-item__content {
display: block;
}
}
.options-class {
display: none;
}
</style>
<style scoped>
.example-showcase .el-dropdown + .el-dropdown {
margin-left: 15px;
}
.example-showcase .el-dropdown-link {
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import { ref } from "vue";
import { FormInstance } from "element-plus";
import { storageLocal } from "@pureadmin/utils";
import { userKey } from "@/utils/auth";
import {clientFormRules} from "@/views/server/component/rules";
// props
export interface DetailFormProps {
formInline: {
id: string;
serverId: string;
name: string;
email: string;
subnetRange: string;
ipAllocation: [];
allowedIPS: [];
extraAllowedIPS: [];
endpoint: string;
useServerDNS: number;
enableAfterCreation: number;
keys: {
privateKey: string;
publicKey: string;
presharedKey: string;
};
enabled: number;
};
}
// props
// https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
const props = withDefaults(defineProps<DetailFormProps>(), {
formInline: () => ({
id: "",
serverId: "",
name: "",
email: "",
subnetRange: "",
ipAllocation: [],
allowedIPS: [],
extraAllowedIPS: [],
endpoint: "",
useServerDNS: 0,
enableAfterCreation: 0,
keys: {
privateKey: "",
publicKey: "",
presharedKey: ""
},
enabled: 1
})
});
// vue prop prop Vue
// reactive ref Ref value reactive
// newFormInline === props.formInline true props.formInline
// props.formInline
// https://cn.vuejs.org/guide/components/props.html#one-way-data-flow
const detailForm = ref(props.formInline);
const detailFormRef = ref<FormInstance>();
function getDetailFormRef() {
return detailFormRef.value;
}
defineExpose({ getDetailFormRef });
</script>
<template>
<el-form
ref="detailFormRef"
:model="detailForm"
:rules="clientFormRules"
label-width="20%"
label-position="right"
>
<el-form-item prop="name" label="名称">
<el-input v-model="detailForm.name" />
</el-form-item>
<el-form-item prop="email" label="邮箱">
<el-input v-model="detailForm.email" />
</el-form-item>
<el-form-item prop="subnetRange" label="子网范围">
<el-input v-model="detailForm.subnetRange" />
</el-form-item>
<el-form-item prop="ipAllocation" label="客户端IP">
<el-select
v-model="detailForm.ipAllocation"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="warning"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item prop="allowedIPS" label="允许访问的IP段">
<el-select
v-model="detailForm.allowedIPS"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="warning"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item prop="extraAllowedIPS" label="其他允许访问的IP段">
<el-select
v-model="detailForm.extraAllowedIPS"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="warning"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item prop="endpoint" label="链接端点">
<el-input v-model="detailForm.endpoint" />
</el-form-item>
<el-form-item prop="useServerDNS" label="是否使用服务端DNS">
<el-radio-group v-model="detailForm.useServerDNS">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="enableAfterCreation" label="确认后创建">
<el-radio-group v-model="detailForm.enableAfterCreation">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="enabled" label="状态">
<el-radio-group v-model="detailForm.enabled">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<style lang="scss">
.options-class {
display: none;
}
</style>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { ref } from "vue";
import { FormInstance } from "element-plus";
import { storageLocal } from "@pureadmin/utils";
import { userKey } from "@/utils/auth";
import {clientQrCode} from "@/api/clients";
// props
export interface DetailFormProps {
formInline: {
id: string;
qrCodeStr: string;
};
}
// props
// https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
const props = withDefaults(defineProps<DetailFormProps>(), {
formInline: () => ({
id: "",
qrCodeStr: ""
})
});
const qrCodeData = ref(props.formInline);
const getQrCode = (id: string) => {
clientQrCode(id).then(res => {
if (res.code === 200) {
qrCodeData.value.qrCodeStr = res.data.qrCode;
}
});
};
getQrCode(props.formInline.id);
</script>
<template>
<el-card body-style="padding: inherit" shadow="hover">
<el-image
:src="qrCodeData.qrCodeStr"
:preview-teleported="true"
style="width: 100%; height: 100%"
/>
</el-card>
</template>

View File

@ -0,0 +1,36 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 登录校验 */
const clientFormRules = reactive(<FormRules>{
name: [
{
required: true,
message: "名称不能为空",
trigger: "blur"
}
],
ipAllocation: [
{
required: true,
message: "客户端IP不能为空",
trigger: "blur"
}
],
allowedIPS: [
{
required: true,
message: "允许链接IP不能为空",
trigger: "blur"
}
],
enabled: [
{
required: true,
message: "状态不能为空",
trigger: "blur"
}
]
});
export { clientFormRules };

View File

@ -1,10 +0,0 @@
<script setup lang="ts">
defineOptions({
// name name
name: "ServerConfig"
});
</script>
<template>
<div>服务端配置页面</div>
</template>

View File

@ -1,10 +0,0 @@
<script setup lang="ts">
defineOptions({
// name name
name: "Server"
});
</script>
<template>
<div>服务端列表</div>
</template>

251
src/views/server/server.vue Normal file
View File

@ -0,0 +1,251 @@
<script setup lang="tsx">
import { reactive, ref } from "vue";
import type { FormInstance } from "element-plus";
import { useServerStoreHook } from "@/store/modules/server";
import { serverFormRules } from "@/views/server/utils/rules";
import { useGlobalSettingStoreHook } from "@/store/modules/globalSetting";
import { getPublicIP } from "@/api/server";
defineOptions({
// name name
name: "Server"
});
const serverFormRef = ref<FormInstance>();
let serverForm = reactive({
id: "",
ipScope: "",
listenPort: 0,
privateKey: "",
publicKey: "",
postUpScript: "",
preDownScript: "",
postDownScript: ""
});
const configFormRef = ref<FormInstance>();
let configForm = reactive({
endpointAddress: "",
dnsServer: [],
MTU: 0,
persistentKeepalive: 0,
firewallMark: "",
table: "",
configFilePath: ""
});
//
const getServerApi = () => {
useServerStoreHook()
.getServerApi()
.then(res => {
if (res.code === 200) {
const data = useServerStoreHook().getServerInfo;
serverForm.id = data.id || "";
serverForm.ipScope = data.ipScope || "";
serverForm.listenPort = data.listenPort || 0;
serverForm.privateKey = data.privateKey || "";
serverForm.publicKey = data.publicKey || "";
serverForm.postUpScript = data.postUpScript || "";
serverForm.preDownScript = data.preDownScript || "";
serverForm.postDownScript = data.postDownScript || "";
}
});
};
//
const updateServerApi = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid, fields) => {
if (!valid) return fields;
useServerStoreHook()
.updateServerApi(serverForm)
.then(res => {
if (res.code === 200) {
getServerApi();
}
});
});
};
//
const getGlobalSettingApi = () => {
useGlobalSettingStoreHook()
.getGlobalSettingApi()
.then(res => {
if (res.code === 200) {
const data = useGlobalSettingStoreHook().getGlobalSetting;
configForm.endpointAddress = data.endpointAddress || "";
configForm.dnsServer = data.dnsServer || [];
configForm.MTU = data.MTU || 0;
configForm.persistentKeepalive = data.persistentKeepalive || 0;
configForm.firewallMark = data.firewallMark || "";
configForm.table = data.table || "";
configForm.configFilePath = data.configFilePath || "";
}
});
};
//
const updateGlobalSettingApi = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate((valid, fields) => {
if (!valid) return fields;
useGlobalSettingStoreHook()
.updateGlobalSettingApi(configForm)
.then(res => {
if (res.code === 200) {
getGlobalSettingApi();
}
});
});
};
// IP
const getPublicIPApi = () => {
getPublicIP().then(res => {
if (res.code === 200) {
useGlobalSettingStoreHook().SET_ENDPOINT_ADDRESS(res.data.IP);
configForm.endpointAddress =
useGlobalSettingStoreHook().getEndpointAddress;
}
});
};
getServerApi();
getGlobalSettingApi();
</script>
<template>
<el-row :gutter="10">
<el-col :span="10" style="padding-left: 50px">
<el-card class="left-card" style="max-width: 800px">
<template #header>
<div class="card-header">
<span>服务端信息</span>
</div>
</template>
<div class="server-form">
<el-form
ref="serverFormRef"
:model="serverForm"
:rules="serverFormRules"
label-position="top"
>
<el-form-item prop="ipScope" label="子网IP段">
<el-input v-model="serverForm.ipScope" />
</el-form-item>
<el-form-item prop="listenPort" label="监听端口">
<el-input-number
v-model="serverForm.listenPort"
:min="49152"
:max="65535"
/>
</el-form-item>
<el-form-item prop="privateKey" label="私钥">
<el-input v-model="serverForm.privateKey" disabled />
</el-form-item>
<el-form-item prop="publicKey" label="公钥">
<el-input v-model="serverForm.publicKey" disabled />
</el-form-item>
<el-form-item prop="postUpScript" label="postUpScript">
<el-input v-model="serverForm.postUpScript" />
</el-form-item>
<el-form-item prop="postDownScript" label="postDownScript">
<el-input v-model="serverForm.preDownScript" />
</el-form-item>
<el-form-item prop="postDownScript" label="postDownScript">
<el-input v-model="serverForm.postDownScript" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="updateServerApi(serverFormRef)"
>确认</el-button
>
</el-form-item>
</el-form>
</div>
</el-card>
</el-col>
<el-col :span="10" style="padding-left: 50px">
<el-card style="max-width: 800px">
<template #header>
<div class="card-header">
<span>全局配置信息</span>
</div>
</template>
<el-form ref="configFormRef" :model="configForm" label-position="top">
<el-form-item prop="endpointAddress" label="endpointAddress">
<el-input v-model="configForm.endpointAddress" />
<el-button size="small" type="warning" class="getIp" @click="getPublicIPApi">获取IP</el-button>
</el-form-item>
<el-form-item prop="dnsServer" label="dnsServer">
<el-select
v-model="configForm.dnsServer"
:clearable="true"
:reserve-keyword="false"
suffix-icon=""
tag-type="success"
popper-class="options-class"
placeholder=""
multiple
filterable
allow-create
default-first-option
/>
</el-form-item>
<el-form-item prop="MTU" label="MTU">
<el-input v-model="configForm.MTU" />
</el-form-item>
<el-form-item prop="persistentKeepalive" label="persistentKeepalive">
<el-input v-model="configForm.persistentKeepalive" />
</el-form-item>
<el-form-item prop="firewallMark" label="firewallMark">
<el-input v-model="configForm.firewallMark" />
</el-form-item>
<el-form-item prop="table" label="table">
<el-input v-model="configForm.table" />
</el-form-item>
<el-form-item prop="configFilePath" label="configFilePath">
<el-input v-model="configForm.configFilePath" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="updateGlobalSettingApi(configFormRef)"
>确认</el-button
>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<style lang="scss">
.options-class {
display: none;
}
</style>
<style scoped lang="scss">
.el-row {
margin-bottom: 30px;
margin-left: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.el-col {
border-radius: 4px;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
}
.options-class {
display: none;
}
.getIp{
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,22 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 登录校验 */
const serverFormRules = reactive(<FormRules>{
ipScope: [
{
required: true,
message: "不能为空",
trigger: "blur"
}
],
listenPort: [
{
required: true,
message: "不能为空",
trigger: "blur"
}
]
});
export { serverFormRules };

View File

@ -290,6 +290,7 @@ userList(); // 用户列表接口
.user-list-table-header {
background-color: #fff;
padding: 20px;
}
.div-page {

View File

@ -1,7 +1,15 @@
<script setup lang="ts">
import {useServerStoreHook} from "@/store/modules/server";
defineOptions({
name: "Welcome"
});
const initServerInfo = () => {
useServerStoreHook().getServerApi();
};
initServerInfo();
</script>
<template>