Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа
This commit is contained in:
@@ -29,4 +29,3 @@ module.exports = {
|
||||
OptionType: "readonly",
|
||||
},
|
||||
};
|
||||
цц
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import {
|
||||
AccountSaveDto,
|
||||
AccountInfo,
|
||||
AccountLoginDto,
|
||||
AccountLoginVo,
|
||||
AccountPageDto,
|
||||
AccountUpdateDto,
|
||||
AccountVo,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Поиск
|
||||
*/
|
||||
export function getAccountApi(data: IdDto): AxiosPromise<AccountVo> {
|
||||
return request({
|
||||
url: "/account/getAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранение
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function saveAccountApi(data: AccountSaveDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/saveAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос текущего пользователя
|
||||
*/
|
||||
export function getAccountInfoApi(): AxiosPromise<AccountInfo> {
|
||||
return request({
|
||||
url: "/account/getAccountInfo",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Пагинация
|
||||
* @param data
|
||||
*/
|
||||
export function pageAccountApi(
|
||||
data: AccountPageDto
|
||||
): AxiosPromise<PageVo<AccountVo>> {
|
||||
return request({
|
||||
url: "/account/pageAccount",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
export function deleteAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/deleteAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Изменение
|
||||
* @param data
|
||||
*/
|
||||
export function updateAccountApi(data: AccountUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/updateAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс трафика
|
||||
* @param data
|
||||
*/
|
||||
export function resetTrafficApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/resetTraffic",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Вход
|
||||
* @param data
|
||||
*/
|
||||
export function loginApi(data: AccountLoginDto): AxiosPromise<AccountLoginVo> {
|
||||
return request({
|
||||
url: "/auth/login",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Импорт
|
||||
*/
|
||||
export function importAccountApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/importAccount",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Экспорт
|
||||
*/
|
||||
export function exportAccountApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/exportAccount",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Снятие статуса отключения
|
||||
*/
|
||||
export function releaseKickAccountApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/releaseKickAccount",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка пароля по умолчанию
|
||||
* @param data
|
||||
*/
|
||||
export function verifyDefaultPassApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/account/verifyDefaultPass",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import { AdminInfo, AdminLoginDto, AdminLoginVo } from "./types";
|
||||
|
||||
export function loginApi(data: AdminLoginDto): AxiosPromise<AdminLoginVo> {
|
||||
return request({
|
||||
url: "/auth/login",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminInfoApi(): AxiosPromise<AdminInfo> {
|
||||
return request({
|
||||
url: "/admin/me",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyDefaultPassApi(): AxiosPromise<boolean> {
|
||||
return request({
|
||||
url: "/admin/verify-default-pass",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface AdminLoginDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
}
|
||||
|
||||
export interface AdminLoginVo {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export interface AdminInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import {
|
||||
DashboardSummaryVo,
|
||||
DashboardTimeseriesVo,
|
||||
DashboardTopPeerVo,
|
||||
SecurityRiskVo,
|
||||
} from "./types";
|
||||
|
||||
export function dashboardSummaryApi(): AxiosPromise<DashboardSummaryVo> {
|
||||
return request({
|
||||
url: "/dashboard/summary",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function dashboardTimeseriesApi(range = "24h"): AxiosPromise<DashboardTimeseriesVo> {
|
||||
return request({
|
||||
url: "/dashboard/timeseries",
|
||||
method: "get",
|
||||
params: { range },
|
||||
});
|
||||
}
|
||||
|
||||
export function dashboardTopPeersApi(range = "24h", limit = 10): AxiosPromise<DashboardTopPeerVo[]> {
|
||||
return request({
|
||||
url: "/dashboard/top-peers",
|
||||
method: "get",
|
||||
params: { range, limit },
|
||||
});
|
||||
}
|
||||
|
||||
export function dashboardSecurityApi(): AxiosPromise<SecurityRiskVo[]> {
|
||||
return request({
|
||||
url: "/dashboard/security",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
export interface SecurityRiskVo {
|
||||
key: string;
|
||||
severity: "info" | "warning" | "critical";
|
||||
actionRoute?: string;
|
||||
dismissible: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardSummaryVo {
|
||||
collectedAt: number;
|
||||
system: {
|
||||
cpuPercent: number;
|
||||
memUsedBytes: number;
|
||||
memTotalBytes: number;
|
||||
memPercent: number;
|
||||
diskUsedBytes: number;
|
||||
diskTotalBytes: number;
|
||||
diskPercent: number;
|
||||
};
|
||||
hysteria: {
|
||||
version: string;
|
||||
running: boolean;
|
||||
apiReachable: boolean;
|
||||
lastApiError?: string;
|
||||
};
|
||||
peers: {
|
||||
total: number;
|
||||
enabled: number;
|
||||
disabled: number;
|
||||
expired: number;
|
||||
onlinePeers: number;
|
||||
onlineDevices: number;
|
||||
};
|
||||
traffic: {
|
||||
downloadBytes: number;
|
||||
uploadBytes: number;
|
||||
totalBytes: number;
|
||||
todayDownloadBytes: number;
|
||||
todayUploadBytes: number;
|
||||
sinceResetDownloadBytes: number;
|
||||
sinceResetUploadBytes: number;
|
||||
};
|
||||
health: {
|
||||
collector: {
|
||||
status: "ok" | "stale" | "error";
|
||||
messageKey?: string;
|
||||
lastSuccessAt?: number;
|
||||
};
|
||||
hysteria: {
|
||||
status: "ok" | "stale" | "error";
|
||||
messageKey?: string;
|
||||
lastSuccessAt?: number;
|
||||
};
|
||||
};
|
||||
securityRisks: SecurityRiskVo[];
|
||||
}
|
||||
|
||||
export interface DashboardTimeseriesVo {
|
||||
range: string;
|
||||
traffic: Array<{ ts: number; download?: number; upload?: number }>;
|
||||
system: Array<{ ts: number; cpu?: number; mem?: number }>;
|
||||
collectedAt: number;
|
||||
}
|
||||
|
||||
export interface DashboardTopPeerVo {
|
||||
peerId: number;
|
||||
name: string;
|
||||
remark: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Hysteria2ServerConfig } from "@/api/config/types";
|
||||
import request from "@/utils/request";
|
||||
import {
|
||||
Hysteria2KickDto,
|
||||
Hysteria2SubscribeVo,
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
Hysteria2UrlVo,
|
||||
} from "@/api/hysteria2/types";
|
||||
@@ -19,16 +17,6 @@ export function hysteria2KickApi(
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2SubscribeUrlApi(
|
||||
dto: Hysteria2SubscribeUrlDto
|
||||
): AxiosPromise<Hysteria2SubscribeVo> {
|
||||
return request({
|
||||
url: "/hysteria2/hysteria2SubscribeUrl",
|
||||
method: "get",
|
||||
params: dto,
|
||||
});
|
||||
}
|
||||
|
||||
export function hysteria2UrlApi(
|
||||
dto: Hysteria2UrlDto
|
||||
): AxiosPromise<Hysteria2UrlVo> {
|
||||
|
||||
@@ -3,24 +3,12 @@ export interface Hysteria2KickDto {
|
||||
kickUtilTime: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeUrlDto {
|
||||
accountId: number;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2UrlDto {
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Hysteria2UrlVo {
|
||||
url: string;
|
||||
qrCode: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { AxiosPromise } from "axios";
|
||||
import request from "@/utils/request";
|
||||
import { Hysteria2MonitorVo, SystemMonitorVo } from "@/api/monitor/types";
|
||||
|
||||
export function monitorSystemApi(): AxiosPromise<SystemMonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorSystem",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function monitorHysteria2Api(): AxiosPromise<Hysteria2MonitorVo> {
|
||||
return request({
|
||||
url: "/monitor/monitorHysteria2",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export interface SystemMonitorVo {
|
||||
huiVersion: string;
|
||||
cpuPercent: number;
|
||||
diskPercent: number;
|
||||
memPercent: number;
|
||||
}
|
||||
|
||||
export interface Hysteria2MonitorVo {
|
||||
userTotal: number;
|
||||
deviceTotal: number;
|
||||
version: string;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from "@/utils/request";
|
||||
import { AxiosPromise } from "axios";
|
||||
import {
|
||||
PeerPageDto,
|
||||
PeerSaveDto,
|
||||
PeerUpdateDto,
|
||||
PeerVo,
|
||||
} from "./types";
|
||||
|
||||
export function getPeerApi(data: IdDto): AxiosPromise<PeerVo> {
|
||||
return request({
|
||||
url: `/peers/${data.id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function savePeerApi(data: PeerSaveDto): AxiosPromise {
|
||||
return request({
|
||||
url: "/peers",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function pagePeerApi(data: PeerPageDto): AxiosPromise<PageVo<PeerVo>> {
|
||||
return request({
|
||||
url: "/peers",
|
||||
method: "get",
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePeerApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: `/peers/${data.id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePeerApi(data: PeerUpdateDto): AxiosPromise {
|
||||
return request({
|
||||
url: `/peers/${data.id}`,
|
||||
method: "patch",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetPeerTrafficApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: `/peers/${data.id}/reset-traffic`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export function releaseKickPeerApi(data: IdDto): AxiosPromise {
|
||||
return request({
|
||||
url: `/peers/${data.id}/release-kick`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export function importPeerApi(data: FormData): AxiosPromise {
|
||||
return request({
|
||||
url: "/peers/import",
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function exportPeerApi(): AxiosPromise {
|
||||
return request({
|
||||
url: "/peers/export",
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface AccountPageDto extends BaseDto {
|
||||
export interface PeerPageDto extends BaseDto {
|
||||
username?: string;
|
||||
deleted?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface AccountUpdateDto extends IdDto {
|
||||
export interface PeerUpdateDto extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
@@ -15,7 +15,7 @@ export interface AccountUpdateDto extends IdDto {
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountSaveDto {
|
||||
export interface PeerSaveDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
@@ -26,12 +26,7 @@ export interface AccountSaveDto {
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
}
|
||||
|
||||
export interface AccountVo extends IdDto {
|
||||
export interface PeerVo extends IdDto {
|
||||
username: string;
|
||||
quota: number;
|
||||
download: number;
|
||||
@@ -42,27 +37,14 @@ export interface AccountVo extends IdDto {
|
||||
role: string;
|
||||
deleted: number;
|
||||
createTime: string;
|
||||
|
||||
online: boolean;
|
||||
device: number;
|
||||
|
||||
loginAt: number;
|
||||
conAt: number;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface AccountLoginVo {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface AccountForm extends IdDto {
|
||||
export interface PeerForm extends IdDto {
|
||||
username: string;
|
||||
pass: string;
|
||||
conPass: string;
|
||||
@@ -73,9 +55,8 @@ export interface AccountForm extends IdDto {
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export interface KickAccountForm {
|
||||
export interface KickPeerForm {
|
||||
ids: number[];
|
||||
kickUtilTime: number;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -4,15 +4,15 @@ import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { locale } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
function handleLanguageChange(lang: string) {
|
||||
locale.value = lang;
|
||||
appStore.changeLanguage(lang);
|
||||
if (lang == "en") {
|
||||
ElMessage.success("Switch Language Successful!");
|
||||
ElMessage.success(t("common.switchLanguageSuccess"));
|
||||
} else {
|
||||
ElMessage.success("Язык переключён");
|
||||
ElMessage.success(t("common.switchLanguageSuccess"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,6 +77,7 @@ export default {
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface Form {
|
||||
key: string;
|
||||
@@ -96,6 +97,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const mapObject = useVModel(props, "mapObject", emit);
|
||||
const { t } = useI18n();
|
||||
|
||||
const dataFormRef = ref(ElForm);
|
||||
|
||||
@@ -103,14 +105,14 @@ const dataFormRules = {
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
@@ -157,7 +159,7 @@ const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (mapObject.value[state.dataForm.key]) {
|
||||
ElMessage.error("key cannot be repeated");
|
||||
ElMessage.error(t("common.invalid"));
|
||||
return;
|
||||
}
|
||||
mapObject.value[state.dataForm.key] = state.dataForm.value;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const sizeOptions = ref([
|
||||
{ label: "Обычный", value: "default" },
|
||||
{ label: "Крупный", value: "large" },
|
||||
{ label: "Компактный", value: "small" },
|
||||
{ label: t("common.sizeDefault"), value: "default" },
|
||||
{ label: t("common.sizeLarge"), value: "large" },
|
||||
{ label: t("common.sizeSmall"), value: "small" },
|
||||
]);
|
||||
|
||||
function handleSizeChange(size: string) {
|
||||
appStore.changeSize(size);
|
||||
ElMessage.success("Размер интерфейса изменён");
|
||||
ElMessage.success(t("common.sizeChanged"));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
<el-select
|
||||
v-model="unit"
|
||||
:placeholder="$t('account.unit')"
|
||||
:placeholder="$t('peer.unit')"
|
||||
style="width: 100px"
|
||||
>
|
||||
<el-option
|
||||
@@ -74,3 +74,4 @@ watch(
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { useAdminStoreHook } from "@/store/modules/admin";
|
||||
import { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ export const hasRole: Directive = {
|
||||
|
||||
if (value) {
|
||||
const requiredRoles = value; // Коды ролей, требуемые DOM-привязкой
|
||||
const { roles } = useAccountStoreHook();
|
||||
const { roles } = useAdminStoreHook();
|
||||
const hasRole = roles.some((perm) => {
|
||||
return requiredRoles.includes(perm);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export default {
|
||||
// МаршрутЛокализация
|
||||
route: {
|
||||
account: "Account",
|
||||
accountList: "Account Manage",
|
||||
dashboard: "Dashboard",
|
||||
peer: "Peers",
|
||||
peerList: "Peer Management",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Hysteria Manage",
|
||||
config: "System",
|
||||
@@ -12,8 +13,6 @@ export default {
|
||||
log: "Log",
|
||||
logSystem: "System Log",
|
||||
logHysteria: "Hysteria Log",
|
||||
info: "Info",
|
||||
infoAccount: "Account Info",
|
||||
},
|
||||
// Локализация страницы входа
|
||||
login: {
|
||||
@@ -42,8 +41,6 @@ export default {
|
||||
confirm: "Confirm",
|
||||
cancel: "Cancel",
|
||||
copySuccess: "Copy successful",
|
||||
subscribe: "Subscribe",
|
||||
subscribeQrCode: "Subscribe QR Code",
|
||||
nodeUrl: "Node URL",
|
||||
nodeQrCode: "Node QR Code",
|
||||
resetTraffic: "Reset traffic",
|
||||
@@ -60,8 +57,27 @@ export default {
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
securityRisk: "Security Risks",
|
||||
defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Click here</a> to change`,
|
||||
defaultPassTip: `Please change the default login password as soon as possible, It is recommended to set a strong password to protect your account security. <a href="/#/peers/list?focus=change-pass" style="color: #00BFFF">Click here</a> to change`,
|
||||
noHttpsTip: `Your website is not using HTTPS, making data transmission insecure, Please enable HTTPS as soon as possible to protect user information. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Click here</a> to enable`,
|
||||
required: "Required",
|
||||
warning: "Warning",
|
||||
fileFormatUnsupported: "File format not supported",
|
||||
fileTooLarge: "The file is too big, less than 2 MB",
|
||||
weekLater: "A week later",
|
||||
monthLater: "A month later",
|
||||
yearLater: "A year later",
|
||||
hourLater: "A hour later",
|
||||
dayLater: "A day later",
|
||||
deleteConfirm: "Are you sure to delete the user \u300c{username}\u300d?",
|
||||
resetTrafficConfirm: "Are you sure to reset traffic?",
|
||||
invalid: "Invalid value",
|
||||
switchLanguageSuccess: "Language switched successfully",
|
||||
sizeChanged: "Interface size changed",
|
||||
sizeDefault: "Default",
|
||||
sizeLarge: "Large",
|
||||
sizeSmall: "Small",
|
||||
logoutConfirm: "Are you sure you want to log out?",
|
||||
sessionExpired: "Current session has expired, please log in again",
|
||||
},
|
||||
info: {
|
||||
expireTime: "y-M-d H:m:s",
|
||||
@@ -72,7 +88,7 @@ export default {
|
||||
greeting5:
|
||||
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
||||
},
|
||||
account: {
|
||||
peer: {
|
||||
remark: "Remark",
|
||||
username: "Username",
|
||||
pass: "Pass",
|
||||
@@ -157,7 +173,6 @@ export default {
|
||||
config: {
|
||||
enable: "Enable/Disable",
|
||||
remark: "Remark",
|
||||
clashExtension: "Clash subscription extension",
|
||||
listen:
|
||||
"When the IP address is omitted, the server will listen on all interfaces, both IPv4 and IPv6. To listen on IPv4 only, you can use 0.0.0.0:443. To listen on IPv6 only, you can use [::]:443.",
|
||||
tlsType: "TLS type",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
route: {
|
||||
account: "Аккаунты",
|
||||
accountList: "Управление аккаунтами",
|
||||
dashboard: "Дашборд",
|
||||
peer: "Пиры",
|
||||
peerList: "Управление пирами",
|
||||
hysteria: "Hysteria",
|
||||
hysteriaList: "Управление Hysteria",
|
||||
config: "Система",
|
||||
@@ -11,8 +12,6 @@ export default {
|
||||
log: "Логи",
|
||||
logSystem: "Системные логи",
|
||||
logHysteria: "Логи Hysteria",
|
||||
info: "Информация",
|
||||
infoAccount: "Профиль",
|
||||
},
|
||||
login: {
|
||||
title: "HY2XS admin",
|
||||
@@ -39,8 +38,6 @@ export default {
|
||||
confirm: "Подтвердить",
|
||||
cancel: "Отмена",
|
||||
copySuccess: "Скопировано",
|
||||
subscribe: "Ссылка подписки",
|
||||
subscribeQrCode: "QR подписки",
|
||||
nodeUrl: "URL узла",
|
||||
nodeQrCode: "QR узла",
|
||||
resetTraffic: "Сбросить трафик",
|
||||
@@ -57,8 +54,27 @@ export default {
|
||||
yes: "Да",
|
||||
no: "Нет",
|
||||
securityRisk: "Риски безопасности",
|
||||
defaultPassTip: `Смените пароль по умолчанию как можно скорее. <a href="/#/account/list?focus=change-pass" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
defaultPassTip: `Смените пароль по умолчанию как можно скорее. <a href="/#/peers/list?focus=change-pass" style="color: #00BFFF">Перейти к смене</a>`,
|
||||
noHttpsTip: `Панель работает без HTTPS. Включите HTTPS для защиты данных. <a href="/#/config/list?focus=huiHttps" style="color: #00BFFF">Открыть настройки</a>`,
|
||||
required: "Обязательное поле",
|
||||
warning: "Внимание",
|
||||
fileFormatUnsupported: "Формат файла не поддерживается",
|
||||
fileTooLarge: "Файл слишком большой, не более 2 МБ",
|
||||
weekLater: "Через неделю",
|
||||
monthLater: "Через месяц",
|
||||
yearLater: "Через год",
|
||||
hourLater: "Через час",
|
||||
dayLater: "Через день",
|
||||
deleteConfirm: "Вы уверены, что хотите удалить пользователя «{username}»?",
|
||||
resetTrafficConfirm: "Сбросить трафик для пользователя?",
|
||||
invalid: "Некорректное значение",
|
||||
switchLanguageSuccess: "Язык переключён",
|
||||
sizeChanged: "Размер интерфейса изменён",
|
||||
sizeDefault: "Обычный",
|
||||
sizeLarge: "Крупный",
|
||||
sizeSmall: "Компактный",
|
||||
logoutConfirm: "Выйти из системы?",
|
||||
sessionExpired: "Текущая сессия истекла, войдите снова",
|
||||
},
|
||||
info: {
|
||||
expireTime: "г-М-д Ч:м:с",
|
||||
@@ -68,7 +84,7 @@ export default {
|
||||
greeting4: "Добрый вечер,",
|
||||
greeting5: "Доброй ночи,",
|
||||
},
|
||||
account: {
|
||||
peer: {
|
||||
remark: "Комментарий",
|
||||
username: "Логин",
|
||||
pass: "Пароль входа",
|
||||
@@ -152,7 +168,6 @@ export default {
|
||||
config: {
|
||||
enable: "Включить/отключить",
|
||||
remark: "Комментарий",
|
||||
clashExtension: "Расширение подписки Clash",
|
||||
listen: "Адрес и порт прослушивания. Для IPv4 используйте 0.0.0.0:443.",
|
||||
tlsType: "Тип TLS",
|
||||
tls: {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAppStore } from "@/store/modules/app";
|
||||
import { useTagsViewStore } from "@/store/modules/tagsView";
|
||||
import { useAccountStore } from "@/store/modules/account";
|
||||
import { useAdminStore } from "@/store/modules/admin";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const accountStore = useAccountStore();
|
||||
const adminStore = useAdminStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -30,12 +32,12 @@ const { isFullscreen, toggle } = useFullscreen();
|
||||
* Выход из системы.
|
||||
*/
|
||||
function logout() {
|
||||
ElMessageBox.confirm("Выйти из системы?", "Подтверждение", {
|
||||
confirmButtonText: "Выйти",
|
||||
cancelButtonText: "Отмена",
|
||||
ElMessageBox.confirm(t("common.logoutConfirm"), t("common.warning"), {
|
||||
confirmButtonText: t("navbar.logout"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
accountStore
|
||||
adminStore
|
||||
.logout()
|
||||
.then(() => {
|
||||
tagsViewStore.delAllViews();
|
||||
@@ -139,4 +141,3 @@ function logout() {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,14 @@ function resolvePath(routePath: string) {
|
||||
:icon-class="onlyOneChild.meta.icon"
|
||||
/>
|
||||
<template #title>
|
||||
{{ translateRouteTitleI18n(onlyOneChild.meta.title) }}
|
||||
<el-tooltip
|
||||
:content="translateRouteTitleI18n(onlyOneChild.meta.title)"
|
||||
placement="right"
|
||||
>
|
||||
<span class="menu-title">{{
|
||||
translateRouteTitleI18n(onlyOneChild.meta.title)
|
||||
}}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
@@ -105,9 +112,13 @@ function resolvePath(routePath: string) {
|
||||
v-if="item.meta && item.meta.icon"
|
||||
:icon-class="item.meta.icon"
|
||||
/>
|
||||
<span v-if="item.meta && item.meta.title">{{
|
||||
translateRouteTitleI18n(item.meta.title)
|
||||
}}</span>
|
||||
<el-tooltip
|
||||
v-if="item.meta && item.meta.title"
|
||||
:content="translateRouteTitleI18n(item.meta.title)"
|
||||
placement="right"
|
||||
>
|
||||
<span class="menu-title">{{ translateRouteTitleI18n(item.meta.title) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<sidebar-item
|
||||
@@ -120,4 +131,12 @@ function resolvePath(routePath: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import router from "@/router";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { useAdminStoreHook } from "@/store/modules/admin";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
|
||||
import NProgress from "nprogress";
|
||||
@@ -21,8 +21,8 @@ router.beforeEach(async (to, from, next) => {
|
||||
next({ path: "/" });
|
||||
NProgress.done();
|
||||
} else {
|
||||
const AccountStore = useAccountStoreHook();
|
||||
const hasRoles = AccountStore.roles && AccountStore.roles.length > 0;
|
||||
const adminStore = useAdminStoreHook();
|
||||
const hasRoles = adminStore.roles && adminStore.roles.length > 0;
|
||||
if (hasRoles) {
|
||||
// Если маршрут не найден, перейти на 404
|
||||
if (to.matched.length === 0) {
|
||||
@@ -32,7 +32,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { roles } = await AccountStore.getAccountInfo();
|
||||
const { roles } = await adminStore.getAdminInfo();
|
||||
const accessRoutes = permissionStore.generateRoutes(roles);
|
||||
accessRoutes.forEach((route) => {
|
||||
router.addRoute(route);
|
||||
@@ -40,7 +40,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
next({ ...to, replace: true });
|
||||
} catch (error) {
|
||||
// Удалить token и перейти на страницу входа
|
||||
await AccountStore.resetToken();
|
||||
await adminStore.resetToken();
|
||||
next(`/login?redirect=${to.path}`);
|
||||
NProgress.done();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
redirect: "/info/account",
|
||||
redirect: "/dashboard/index",
|
||||
children: [
|
||||
{
|
||||
path: "401",
|
||||
@@ -47,41 +47,41 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
|
||||
export const asyncRoutes: any[] = [
|
||||
{
|
||||
path: "/info",
|
||||
path: "/dashboard",
|
||||
component: "Layout",
|
||||
redirect: "/account",
|
||||
name: "Info",
|
||||
redirect: "/dashboard/index",
|
||||
name: "Dashboard",
|
||||
meta: {
|
||||
title: "info",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
title: "dashboard",
|
||||
icon: "dashboard",
|
||||
roles: ["admin"],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "account",
|
||||
component: "info/account/index",
|
||||
name: "AccountInfo",
|
||||
path: "index",
|
||||
component: "dashboard/index",
|
||||
name: "DashboardIndex",
|
||||
meta: {
|
||||
title: "infoAccount",
|
||||
icon: "user",
|
||||
roles: ["user", "admin"],
|
||||
title: "dashboard",
|
||||
icon: "dashboard",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/account",
|
||||
path: "/peers",
|
||||
component: "Layout",
|
||||
redirect: "/list",
|
||||
name: "Account",
|
||||
meta: { title: "account", icon: "users", roles: ["admin"] },
|
||||
name: "Peer",
|
||||
meta: { title: "peer", icon: "users", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "list",
|
||||
component: "account/list/index",
|
||||
name: "AccountList",
|
||||
component: "peer/list/index",
|
||||
name: "PeerList",
|
||||
meta: {
|
||||
title: "accountList",
|
||||
title: "peerList",
|
||||
icon: "users",
|
||||
roles: ["admin"],
|
||||
},
|
||||
@@ -132,25 +132,6 @@ export const asyncRoutes: any[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/monitor",
|
||||
component: "Layout",
|
||||
redirect: "/monitor",
|
||||
name: "Monitor",
|
||||
meta: { title: "monitor", icon: "report", roles: ["admin"] },
|
||||
children: [
|
||||
{
|
||||
path: "system",
|
||||
component: "monitor/system/index",
|
||||
name: "MonitorSystem",
|
||||
meta: {
|
||||
title: "monitorSystem",
|
||||
icon: "report",
|
||||
roles: ["admin"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/log",
|
||||
component: "Layout",
|
||||
@@ -201,5 +182,3 @@ export function resetRouter() {
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
+15
-26
@@ -1,31 +1,25 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { getAccountInfoApi, loginApi } from "@/api/account";
|
||||
import { getAdminInfoApi, loginApi } from "@/api/admin";
|
||||
import { resetRouter } from "@/router";
|
||||
import { store } from "@/store";
|
||||
|
||||
import { AccountInfo, AccountLoginDto } from "@/api/account/types";
|
||||
import { AdminInfo, AdminLoginDto } from "@/api/admin/types";
|
||||
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useAccountStore = defineStore("account", () => {
|
||||
// state
|
||||
export const useAdminStore = defineStore("admin", () => {
|
||||
const token = useStorage("accessToken", "");
|
||||
const id = ref(0);
|
||||
const username = ref("");
|
||||
const roles = ref<Array<string>>([]); // Коды ролей пользователя для проверки доступа к маршрутам
|
||||
const roles = ref<Array<string>>([]);
|
||||
|
||||
/**
|
||||
* Вход
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function login(accountLoginDto: AccountLoginDto) {
|
||||
function login(adminLoginDto: AdminLoginDto) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
loginApi(accountLoginDto)
|
||||
loginApi(adminLoginDto)
|
||||
.then((response) => {
|
||||
const { tokenType, accessToken } = response.data;
|
||||
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
|
||||
token.value = tokenType + " " + accessToken;
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -34,16 +28,15 @@ export const useAccountStore = defineStore("account", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Запрос текущего пользователя
|
||||
function getAccountInfo() {
|
||||
return new Promise<AccountInfo>((resolve, reject) => {
|
||||
getAccountInfoApi()
|
||||
function getAdminInfo() {
|
||||
return new Promise<AdminInfo>((resolve, reject) => {
|
||||
getAdminInfoApi()
|
||||
.then(({ data }) => {
|
||||
if (!data) {
|
||||
return reject("Verification failed, please Login again.");
|
||||
}
|
||||
if (!data.roles || data.roles.length <= 0) {
|
||||
reject("getAccountInfoApi: roles must be a non-null array!");
|
||||
reject("getAdminInfoApi: roles must be a non-null array!");
|
||||
}
|
||||
id.value = data.id;
|
||||
username.value = data.username;
|
||||
@@ -56,16 +49,14 @@ export const useAccountStore = defineStore("account", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Выход
|
||||
function logout() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
resetRouter();
|
||||
resetToken();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// Сброс
|
||||
function resetToken() {
|
||||
token.value = "";
|
||||
id.value = 0;
|
||||
@@ -79,15 +70,13 @@ export const useAccountStore = defineStore("account", () => {
|
||||
username,
|
||||
roles,
|
||||
login,
|
||||
getAccountInfo,
|
||||
getAdminInfo,
|
||||
logout,
|
||||
resetToken,
|
||||
};
|
||||
});
|
||||
|
||||
// Вне setup
|
||||
export function useAccountStoreHook() {
|
||||
return useAccountStore(store);
|
||||
export function useAdminStoreHook() {
|
||||
return useAdminStore(store);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,32 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 48px;
|
||||
line-height: normal;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
@@ -100,16 +124,6 @@
|
||||
overflow: hidden;
|
||||
|
||||
& > .el-sub-menu__title {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.sub-el-icon {
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
-6
@@ -3,11 +3,8 @@ export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
||||
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
@@ -269,11 +266,8 @@ declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly ElForm: UnwrapRef<typeof import('element-plus/es')['ElForm']>
|
||||
readonly ElInput: UnwrapRef<typeof import('element-plus/es')['ElInput']>
|
||||
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
|
||||
readonly ElMessageBox: UnwrapRef<typeof import('element-plus/es')['ElMessageBox']>
|
||||
readonly ElNotification: UnwrapRef<typeof import('element-plus/es')['ElNotification']>
|
||||
readonly ElSelect: UnwrapRef<typeof import('element-plus/es')['ElSelect']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
import { useAdminStoreHook } from "@/store/modules/admin";
|
||||
import i18n from "@/lang/index";
|
||||
|
||||
const dynamicBase = (window as any).__dynamic_base__ || "";
|
||||
const API_BASE = "/hui";
|
||||
const t = i18n.global.t;
|
||||
// Создание axios instance
|
||||
const service = axios.create({
|
||||
baseURL: `${dynamicBase}${API_BASE}`,
|
||||
@@ -13,9 +15,9 @@ const service = axios.create({
|
||||
// Request interceptor
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const accountStore = useAccountStoreHook();
|
||||
if (accountStore.token) {
|
||||
config.headers.Authorization = accountStore.token;
|
||||
const adminStore = useAdminStoreHook();
|
||||
if (adminStore.token) {
|
||||
config.headers.Authorization = adminStore.token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
@@ -44,8 +46,8 @@ service.interceptors.response.use(
|
||||
const { code, msg } = error.response.data;
|
||||
// Token истёк, нужен повторный вход
|
||||
if (code === "A0230") {
|
||||
ElMessageBox.confirm("Текущая сессия истекла, войдите снова", "Подтверждение", {
|
||||
confirmButtonText: "ОК",
|
||||
ElMessageBox.confirm(t("common.sessionExpired"), t("common.warning"), {
|
||||
confirmButtonText: t("common.confirm"),
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
localStorage.clear();
|
||||
@@ -61,5 +63,3 @@ service.interceptors.response.use(
|
||||
|
||||
// Export axios instance
|
||||
export default service;
|
||||
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const dataFormRef = ref(ElForm);
|
||||
const huiHttpsRef = ref(ElSelect);
|
||||
const huiHttpsRef = ref<any>(null);
|
||||
|
||||
const huiWebPortKey = "H_UI_WEB_PORT";
|
||||
const huiWebContext = "H_UI_WEB_CONTEXT";
|
||||
@@ -200,7 +200,7 @@ const dataFormRules = {
|
||||
huiWebPort: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -212,7 +212,7 @@ const dataFormRules = {
|
||||
huiWebContext: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -224,7 +224,7 @@ const dataFormRules = {
|
||||
hysteria2TrafficTime: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -321,11 +321,11 @@ const handleImport = async (params: UploadRequestOptions) => {
|
||||
};
|
||||
const beforeImport = (file: UploadRawFile) => {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
ElMessage.error("file format not supported");
|
||||
ElMessage.error(t("common.fileFormatUnsupported"));
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
ElMessage.error("the file is too big, less than 2 MB");
|
||||
ElMessage.error(t("common.fileTooLarge"));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -353,7 +353,7 @@ const handleExport = async () => {
|
||||
|
||||
const handleRestartServer = async () => {
|
||||
try {
|
||||
ElMessageBox.confirm("Are you sure to restart panel?", "Warning", {
|
||||
ElMessageBox.confirm(t("config.restartTip"), t("common.warning"), {
|
||||
confirmButtonText: t("common.confirm"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
type: "warning",
|
||||
@@ -385,5 +385,3 @@ onMounted(() => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-actions mb-2">
|
||||
<el-button size="small" @click="loadDashboard">Refresh</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="loadError"
|
||||
:title="loadError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
class="mb-2"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="isStale"
|
||||
title="Dashboard data is stale. Retrying automatically..."
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
v-for="risk in securityRisks"
|
||||
:key="risk.key"
|
||||
:title="risk.key"
|
||||
:type="risk.severity === 'critical' ? 'error' : risk.severity === 'warning' ? 'warning' : 'info'"
|
||||
:closable="risk.dismissible"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-row :gutter="10" class="mt-2">
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">CPU: {{ summary.system.cpuPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">RAM: {{ summary.system.memPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Disk: {{ summary.system.diskPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Peers: {{ summary.peers.total }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online peers: {{ summary.peers.onlinePeers }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online devices: {{ summary.peers.onlineDevices }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today download: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today upload: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mt-3">
|
||||
<template #header>Top peers (24h)</template>
|
||||
<el-table :data="topPeers" size="small">
|
||||
<el-table-column prop="name" label="Peer" />
|
||||
<el-table-column prop="download" label="Download">
|
||||
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upload" label="Upload">
|
||||
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total" label="Total">
|
||||
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntervalFn } from "@vueuse/core";
|
||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||
import { DashboardSummaryVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
|
||||
const summary = ref<DashboardSummaryVo>({
|
||||
collectedAt: 0,
|
||||
system: { cpuPercent: 0, memUsedBytes: 0, memTotalBytes: 0, memPercent: 0, diskUsedBytes: 0, diskTotalBytes: 0, diskPercent: 0 },
|
||||
hysteria: { version: "-", running: false, apiReachable: false },
|
||||
peers: { total: 0, enabled: 0, disabled: 0, expired: 0, onlinePeers: 0, onlineDevices: 0 },
|
||||
traffic: { downloadBytes: 0, uploadBytes: 0, totalBytes: 0, todayDownloadBytes: 0, todayUploadBytes: 0, sinceResetDownloadBytes: 0, sinceResetUploadBytes: 0 },
|
||||
health: {
|
||||
collector: { status: "stale" },
|
||||
hysteria: { status: "ok" },
|
||||
},
|
||||
securityRisks: [],
|
||||
});
|
||||
const topPeers = ref<DashboardTopPeerVo[]>([]);
|
||||
const securityRisks = ref<SecurityRiskVo[]>([]);
|
||||
const loadError = ref("");
|
||||
const loading = ref(false);
|
||||
const lastSuccessAt = ref(0);
|
||||
|
||||
const staleThresholdMs = 90_000;
|
||||
const pollIntervalMs = 30_000;
|
||||
|
||||
const isStale = computed(() => {
|
||||
if (!lastSuccessAt.value) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - lastSuccessAt.value > staleThresholdMs;
|
||||
});
|
||||
|
||||
const loadDashboard = async () => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const [summaryRes, topRes, secRes] = await Promise.all([
|
||||
dashboardSummaryApi(),
|
||||
dashboardTopPeersApi("24h", 10),
|
||||
dashboardSecurityApi(),
|
||||
]);
|
||||
summary.value = summaryRes.data;
|
||||
topPeers.value = topRes.data;
|
||||
securityRisks.value = secRes.data;
|
||||
lastSuccessAt.value = Date.now();
|
||||
loadError.value = "";
|
||||
} catch (error) {
|
||||
loadError.value = "Failed to refresh dashboard data";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
||||
() => {
|
||||
loadDashboard();
|
||||
},
|
||||
pollIntervalMs,
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
loadDashboard();
|
||||
startPolling();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDashboard();
|
||||
startPolling();
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -283,6 +283,9 @@ import {
|
||||
} from "@/api/config/types";
|
||||
import { PropType } from "vue";
|
||||
import { deepCopy } from "@/utils/copy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
outbounds: {
|
||||
@@ -308,11 +311,11 @@ const state = reactive({
|
||||
...defaultHysteria2ServerConfigOutbound,
|
||||
} as Hysteria2ServerConfigOutbound,
|
||||
dialog: {
|
||||
title: "Add Outbound",
|
||||
title: t("hysteria.addOutbound"),
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
outboundInfoDialog: {
|
||||
title: "Outbound Info",
|
||||
title: t("hysteria.outbounds"),
|
||||
visible: false,
|
||||
},
|
||||
outboundInfo: {} as Hysteria2ServerConfigOutbound,
|
||||
@@ -344,7 +347,7 @@ const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
if (outbounds.value.some((item) => item.name === state.dataForm.name)) {
|
||||
ElMessage.error("name cannot be repeated");
|
||||
ElMessage.error(t("common.invalid"));
|
||||
return;
|
||||
}
|
||||
if (state.dataForm.type === "socks5") {
|
||||
|
||||
@@ -63,19 +63,6 @@
|
||||
<el-input v-model="configForm.remark" clearable />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
:content="$t('hysteria.config.clashExtension')"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-form-item label="clashExtension" prop="clashExtension">
|
||||
<el-input
|
||||
v-model="configForm.clashExtension"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3 }"
|
||||
@keydown="(e:KeyboardEvent) => e.stopPropagation()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('hysteria.listen')" name="listen">
|
||||
<el-tooltip
|
||||
@@ -1011,27 +998,26 @@ import {
|
||||
UploadFile,
|
||||
UploadRequestOptions,
|
||||
} from "element-plus/lib/components";
|
||||
import { monitorHysteria2Api } from "@/api/monitor";
|
||||
import { dashboardSummaryApi } from "@/api/dashboard";
|
||||
import { UploadUserFile } from "element-plus";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const hysteria2Remark = "HYSTERIA2_CONFIG_REMARK";
|
||||
const clashExtension = "CLASH_EXTENSION";
|
||||
const dataFormRef = ref(ElForm);
|
||||
|
||||
const dataFormRules = {
|
||||
listen: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
"trafficStats.listen": [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
@@ -1057,7 +1043,6 @@ const masqueradeTypes = ref<string[]>(["file", "proxy", "string"]);
|
||||
const state = reactive({
|
||||
configForm: {
|
||||
remark: "",
|
||||
clashExtension: "",
|
||||
},
|
||||
dataForm: { ...defaultHysteria2ServerConfig } as Hysteria2ServerConfig,
|
||||
activeName: "extension",
|
||||
@@ -1189,14 +1174,12 @@ const handleExport = async () => {
|
||||
|
||||
const setConfig = () => {
|
||||
listConfigApi({
|
||||
keys: [hysteria2Remark, clashExtension],
|
||||
keys: [hysteria2Remark],
|
||||
}).then((response) => {
|
||||
const data = response.data;
|
||||
data.forEach((configVo) => {
|
||||
if (configVo.key === hysteria2Remark) {
|
||||
state.configForm.remark = configVo.value;
|
||||
} else if (configVo.key === clashExtension) {
|
||||
state.configForm.clashExtension = configVo.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1225,8 +1208,9 @@ const setConfig = () => {
|
||||
};
|
||||
|
||||
const setHysteria2Monitor = async () => {
|
||||
const { data } = await monitorHysteria2Api();
|
||||
Object.assign(state.hysteria2Monitor, data);
|
||||
const { data } = await dashboardSummaryApi();
|
||||
state.hysteria2Monitor.version = data.hysteria.version;
|
||||
state.hysteria2Monitor.running = data.hysteria.running;
|
||||
};
|
||||
|
||||
const uploadCertFile = async (params: UploadRequestOptions) => {
|
||||
@@ -1238,10 +1222,10 @@ const uploadCertFile = async (params: UploadRequestOptions) => {
|
||||
!params.file.name.endsWith(".crt") &&
|
||||
!params.file.name.endsWith(".key")
|
||||
) {
|
||||
ElMessage.error("file format not supported");
|
||||
ElMessage.error(t("common.fileFormatUnsupported"));
|
||||
}
|
||||
if (params.file.size > 1024 * 1024) {
|
||||
ElMessage.error("the file is too big");
|
||||
ElMessage.error(t("common.fileTooLarge"));
|
||||
}
|
||||
let formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<el-card shadow="never">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="12" :xs="24">
|
||||
<div class="flex h-full items-center">
|
||||
<img
|
||||
class="w-20 h-20 mr-5 rounded-full"
|
||||
src="/src/assets/logo.png"
|
||||
/>
|
||||
<div>
|
||||
<p>{{ greetings }}</p>
|
||||
<p class="text-sm text-gray">
|
||||
{{ $t("account.createTime") }}:
|
||||
{{ timestampToDateTime(account.createTime) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" :xs="24">
|
||||
<div class="flex h-full items-center" style="justify-content: right">
|
||||
<el-button type="primary" :icon="Share" @click="handleSubscribe">
|
||||
{{ $t("common.subscribe") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Share"
|
||||
@click="handleSubscribeQrCode"
|
||||
>
|
||||
{{ $t("common.subscribeQrCode") }}
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Share" @click="handleNodeUrl">
|
||||
{{ $t("common.nodeUrl") }}
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Share" @click="handleUrlQrCode">
|
||||
{{ $t("common.nodeQrCode") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="10" class="mt-3">
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("account.quota") }}
|
||||
</span>
|
||||
<el-tag type="success"
|
||||
>{{ formatStorageUnit(account.quota) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ formatBytes(account.quota) }}
|
||||
</div>
|
||||
<svg-icon icon-class="quota" size="2em" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("account.download") }}
|
||||
</span>
|
||||
<el-tag type="success"
|
||||
>{{ formatStorageUnit(account.download) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ formatBytes(account.download) }}
|
||||
</div>
|
||||
<svg-icon icon-class="download" size="2em" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("account.upload") }}
|
||||
</span>
|
||||
<el-tag type="success"
|
||||
>{{ formatStorageUnit(account.upload) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ formatBytes(account.upload) }}
|
||||
</div>
|
||||
<svg-icon icon-class="upload" size="2em" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("account.expireTime") }}
|
||||
</span>
|
||||
<el-tag type="success">{{ $t("info.expireTime") }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ timestampToDateTime(account.expireTime) }}
|
||||
</div>
|
||||
<svg-icon icon-class="expire-time" size="2em" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
:title="qrCodeDialog.title"
|
||||
v-model="qrCodeDialog.visible"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="qrCodeDialog.visible = false"
|
||||
>
|
||||
<el-form style="text-align: center">
|
||||
<el-image
|
||||
style="width: 300px; height: 300px"
|
||||
:src="qrCodeSrc"
|
||||
></el-image>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="qrCodeDialog.visible = false"
|
||||
>{{ $t("common.confirm") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "index",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAccountApi, verifyDefaultPassApi } from "@/api/account";
|
||||
import { AccountVo } from "@/api/account/types";
|
||||
import { useAccountStore } from "@/store/modules/account";
|
||||
import { timestampToDateTime } from "@/utils/time";
|
||||
import { formatBytes, formatStorageUnit } from "@/utils/byte";
|
||||
import { Share } from "@element-plus/icons-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
} from "@/api/hysteria2/types";
|
||||
import { hysteria2SubscribeUrlApi, hysteria2UrlApi } from "@/api/hysteria2";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
const { t } = useI18n();
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
const date: Date = new Date();
|
||||
|
||||
const greetings = computed(() => {
|
||||
const hours = date.getHours();
|
||||
if (hours >= 6 && hours < 8) {
|
||||
return t("info.greeting1");
|
||||
} else if (hours >= 8 && hours < 12) {
|
||||
return t("info.greeting2") + accountStore.username + "!";
|
||||
} else if (hours >= 12 && hours < 18) {
|
||||
return t("info.greeting3") + accountStore.username + "!";
|
||||
} else if (hours >= 18 && hours < 24) {
|
||||
return t("info.greeting4") + accountStore.username + "!";
|
||||
} else if (hours >= 0 && hours < 6) {
|
||||
return t("info.greeting5");
|
||||
}
|
||||
return "Hello HY2XS";
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
account: {} as AccountVo,
|
||||
qrCodeDialog: {
|
||||
title: "QR Code",
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
qrCodeSrc: "",
|
||||
});
|
||||
|
||||
const { qrCodeDialog, account, qrCodeSrc } = toRefs(state);
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
try {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: accountStore.id,
|
||||
protocol: window.location.protocol,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
copy(data.url);
|
||||
ElMessage.success(t("common.copySuccess"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribeQrCode = async () => {
|
||||
try {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: accountStore.id,
|
||||
protocol: window.location.protocol,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
state.qrCodeDialog.visible = true;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeUrl = async () => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: accountStore.id,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
copy(data.url);
|
||||
ElMessage.success(t("common.copySuccess"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlQrCode = async () => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: accountStore.id,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
state.qrCodeDialog.visible = true;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getAccountApi({ id: accountStore.id }).then((response) => {
|
||||
Object.assign(state.account, response.data);
|
||||
});
|
||||
if (accountStore.roles.indexOf("admin") != -1) {
|
||||
verifyDefaultPassApi().then((response) => {
|
||||
if (response.data) {
|
||||
ElNotification({
|
||||
title: t("common.securityRisk"),
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: t("common.defaultPassTip"),
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
});
|
||||
if (window.location.protocol !== "https:") {
|
||||
ElNotification({
|
||||
title: t("common.securityRisk"),
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: t("common.noHttpsTip"),
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-container {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.github-corner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.data-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-bg-color-overlay);
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: var(--el-box-shadow-dark);
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
fill: currentcolor !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flex.h-full.items-center {
|
||||
.el-button {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flex.h-full.items-center {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 634px) {
|
||||
.flex.h-full.items-center {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -75,16 +75,18 @@ export default {
|
||||
import router from "@/router";
|
||||
import LangSelect from "@/components/LangSelect/index.vue";
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Зависимость store
|
||||
import { useAccountStore } from "@/store/modules/account";
|
||||
import { useAdminStore } from "@/store/modules/admin";
|
||||
|
||||
// Зависимость API
|
||||
import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
|
||||
import { AccountLoginDto } from "@/api/account/types";
|
||||
import { AdminLoginDto } from "@/api/admin/types";
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const adminStore = useAdminStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
/**
|
||||
* Состояние загрузки кнопки
|
||||
@@ -107,7 +109,7 @@ const loginFormRef = ref(ElForm);
|
||||
/**
|
||||
* Форма входа
|
||||
*/
|
||||
const loginForm = ref<AccountLoginDto>({
|
||||
const loginForm = ref<AdminLoginDto>({
|
||||
username: "",
|
||||
pass: "",
|
||||
});
|
||||
@@ -116,7 +118,7 @@ const loginRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -128,7 +130,7 @@ const loginRules = {
|
||||
pass: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -155,7 +157,7 @@ const handleLogin = () => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const params = { ...loginForm.value };
|
||||
accountStore
|
||||
adminStore
|
||||
.login(params)
|
||||
.then(() => {
|
||||
const query: LocationQuery = route.query;
|
||||
@@ -238,5 +240,3 @@ const handleLogin = () => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<el-row :gutter="10" class="mt-3">
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.huiVersion") }}
|
||||
</span>
|
||||
<el-tag type="success">version</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ systemMonitor.huiVersion ? systemMonitor.huiVersion : "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.cpuPercent") }}
|
||||
</span>
|
||||
<el-tag type="success">%</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{
|
||||
systemMonitor.cpuPercent ? systemMonitor.cpuPercent + "%" : "-"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.memPercent") }}
|
||||
</span>
|
||||
<el-tag type="success">%</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{
|
||||
systemMonitor.memPercent ? systemMonitor.memPercent + "%" : "-"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.diskPercent") }}
|
||||
</span>
|
||||
<el-tag type="success">%</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{
|
||||
systemMonitor.diskPercent
|
||||
? systemMonitor.diskPercent + "%"
|
||||
: "-"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.hysteria2Version") }}
|
||||
</span>
|
||||
<el-tag type="success">version</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ hysteria2Monitor.version ? hysteria2Monitor.version : "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.hysteria2Running") }}
|
||||
</span>
|
||||
<el-tag type="success">running</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div
|
||||
class="text-lg text-right"
|
||||
:style="
|
||||
hysteria2Monitor.running === undefined
|
||||
? '-'
|
||||
: hysteria2Monitor.running
|
||||
? 'color: #2ecc71'
|
||||
: 'color: #e74c3c'
|
||||
"
|
||||
>
|
||||
{{
|
||||
hysteria2Monitor.running === undefined
|
||||
? "-"
|
||||
: hysteria2Monitor.running
|
||||
? $t("monitor.hysteria2RunningTrue")
|
||||
: $t("monitor.hysteria2RunningFalse")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.hysteria2UserTotal") }}
|
||||
</span>
|
||||
<el-tag type="success">account</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ hysteria2Monitor.userTotal }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--el-text-color-secondary)]">
|
||||
{{ $t("monitor.hysteria2DeviceTotal") }}
|
||||
</span>
|
||||
<el-tag type="success">device</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div class="text-lg text-right">
|
||||
{{ hysteria2Monitor.deviceTotal }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "index",
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { monitorHysteria2Api, monitorSystemApi } from "@/api/monitor";
|
||||
|
||||
const state = reactive({
|
||||
systemMonitor: {
|
||||
huiVersion: "",
|
||||
cpuPercent: 0,
|
||||
memPercent: 0,
|
||||
diskPercent: 0,
|
||||
},
|
||||
hysteria2Monitor: {
|
||||
userTotal: 0,
|
||||
deviceTotal: 0,
|
||||
version: undefined,
|
||||
running: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { systemMonitor, hysteria2Monitor } = toRefs(state);
|
||||
|
||||
const setMonitor = () => {
|
||||
monitorSystemApi().then((response) => {
|
||||
const { data } = response;
|
||||
Object.assign(state.systemMonitor, data);
|
||||
});
|
||||
monitorHysteria2Api().then((response) => {
|
||||
const { data } = response;
|
||||
Object.assign(state.hysteria2Monitor, data);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setMonitor();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-container {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
|
||||
.svg-icon {
|
||||
fill: currentcolor !important;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
+85
-163
@@ -2,19 +2,19 @@
|
||||
<div class="app-container">
|
||||
<div class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item :label="$t('account.remark')" prop="remark">
|
||||
<el-form-item :label="$t('peer.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="queryParams.remark"
|
||||
:placeholder="$t('account.remark')"
|
||||
:placeholder="$t('peer.remark')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.username')" prop="username">
|
||||
<el-form-item :label="$t('peer.username')" prop="username">
|
||||
<el-input
|
||||
v-model="queryParams.username"
|
||||
:placeholder="$t('account.username')"
|
||||
:placeholder="$t('peer.username')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
@@ -86,25 +86,19 @@
|
||||
/>
|
||||
<el-table-column
|
||||
key="remark"
|
||||
:label="$t('account.remark')"
|
||||
:label="$t('peer.remark')"
|
||||
align="center"
|
||||
prop="remark"
|
||||
/>
|
||||
<el-table-column
|
||||
key="username"
|
||||
:label="$t('account.username')"
|
||||
:label="$t('peer.username')"
|
||||
align="center"
|
||||
prop="username"
|
||||
/>
|
||||
<el-table-column
|
||||
key="role"
|
||||
:label="$t('account.role')"
|
||||
align="center"
|
||||
prop="role"
|
||||
/>
|
||||
<el-table-column
|
||||
key="quota"
|
||||
:label="$t('account.quota')"
|
||||
:label="$t('peer.quota')"
|
||||
align="center"
|
||||
prop="quota"
|
||||
>
|
||||
@@ -114,7 +108,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="download"
|
||||
:label="$t('account.download')"
|
||||
:label="$t('peer.download')"
|
||||
align="center"
|
||||
prop="download"
|
||||
>
|
||||
@@ -124,7 +118,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="upload"
|
||||
:label="$t('account.upload')"
|
||||
:label="$t('peer.upload')"
|
||||
align="center"
|
||||
prop="upload"
|
||||
>
|
||||
@@ -134,32 +128,32 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="online"
|
||||
:label="$t('account.onlineStatus')"
|
||||
:label="$t('peer.onlineStatus')"
|
||||
align="center"
|
||||
prop="online"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.online" type="success"
|
||||
>{{ $t("account.online") }}
|
||||
>{{ $t("peer.online") }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info">{{ $t("account.offline") }}</el-tag>
|
||||
<el-tag v-else type="info">{{ $t("peer.offline") }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="device"
|
||||
:label="$t('account.device')"
|
||||
:label="$t('peer.device')"
|
||||
align="center"
|
||||
prop="device"
|
||||
/>
|
||||
<el-table-column
|
||||
key="deviceNo"
|
||||
:label="$t('account.deviceNo')"
|
||||
:label="$t('peer.deviceNo')"
|
||||
align="center"
|
||||
prop="deviceNo"
|
||||
/>
|
||||
<el-table-column
|
||||
key="kickUtilTime"
|
||||
:label="$t('account.kickUtilTimeLast')"
|
||||
:label="$t('peer.kickUtilTimeLast')"
|
||||
align="center"
|
||||
prop="kickUtilTime"
|
||||
>
|
||||
@@ -169,7 +163,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="expireTime"
|
||||
:label="$t('account.expireTime')"
|
||||
:label="$t('peer.expireTime')"
|
||||
align="center"
|
||||
prop="expireTime"
|
||||
width="160"
|
||||
@@ -180,7 +174,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="loginAt"
|
||||
:label="$t('account.loginAt')"
|
||||
:label="$t('peer.loginAt')"
|
||||
align="center"
|
||||
prop="loginAt"
|
||||
width="160"
|
||||
@@ -193,7 +187,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
key="conAt"
|
||||
:label="$t('account.conAt')"
|
||||
:label="$t('peer.conAt')"
|
||||
align="center"
|
||||
prop="conAt"
|
||||
width="160"
|
||||
@@ -232,9 +226,6 @@
|
||||
width="300"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleSubscribe(scope.row)"
|
||||
>{{ $t("common.subscribe") }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleNodeUrl(scope.row)"
|
||||
>{{ $t("common.nodeUrl") }}
|
||||
</el-button>
|
||||
@@ -242,7 +233,7 @@
|
||||
{{ $t("common.nodeQrCode") }}
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
title="Are you sure to reset traffic?"
|
||||
:title="$t('common.resetTrafficConfirm')"
|
||||
@confirm="resetTraffic(scope.row)"
|
||||
>
|
||||
<template #reference>
|
||||
@@ -258,16 +249,16 @@
|
||||
>{{ $t("common.delete") }}
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleKick(scope.row)"
|
||||
>{{ $t("account.kick") }}
|
||||
>{{ $t("peer.kick") }}
|
||||
</el-button>
|
||||
<el-popconfirm
|
||||
:title="$t('account.releaseKickTip')"
|
||||
:title="$t('peer.releaseKickTip')"
|
||||
@confirm="confirmReleaseKick(scope.row)"
|
||||
v-if="calculateTimeDifference(scope.row.kickUtilTime) !== '-'"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="danger" link
|
||||
>{{ $t("account.releaseKick") }}
|
||||
>{{ $t("peer.releaseKick") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
@@ -301,51 +292,39 @@
|
||||
:model="dataForm"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item :label="$t('account.remark')" prop="remark">
|
||||
<el-form-item :label="$t('peer.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="dataForm.remark"
|
||||
:placeholder="$t('account.remark')"
|
||||
:placeholder="$t('peer.remark')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.username')" prop="username">
|
||||
<el-form-item :label="$t('peer.username')" prop="username">
|
||||
<el-input
|
||||
v-model="dataForm.username"
|
||||
:placeholder="$t('account.username')"
|
||||
:placeholder="$t('peer.username')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.pass')" prop="pass">
|
||||
<el-input
|
||||
v-model="dataForm.pass"
|
||||
:placeholder="$t('account.pass')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
ref="dataFormPassRef"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.conPass')" prop="conPass">
|
||||
<el-form-item :label="$t('peer.conPass')" prop="conPass">
|
||||
<el-input
|
||||
v-model="dataForm.conPass"
|
||||
:placeholder="$t('account.conPass')"
|
||||
:placeholder="$t('peer.conPass')"
|
||||
maxlength="50"
|
||||
clearable
|
||||
type="password"
|
||||
show-password
|
||||
ref="dataFormConPassRef"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.quota')" prop="quota">
|
||||
<el-form-item :label="$t('peer.quota')" prop="quota">
|
||||
<unit-select :setValue="setQuota" :valueTmp="quotaTmp" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.deviceNo')" prop="deviceNo">
|
||||
<el-form-item :label="$t('peer.deviceNo')" prop="deviceNo">
|
||||
<el-input-number
|
||||
v-model="dataForm.deviceNo"
|
||||
:placeholder="$t('account.deviceNo')"
|
||||
:placeholder="$t('peer.deviceNo')"
|
||||
:min="1"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
@@ -353,11 +332,11 @@
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('account.expireTime')" prop="expireTime">
|
||||
<el-form-item :label="$t('peer.expireTime')" prop="expireTime">
|
||||
<el-date-picker
|
||||
v-model="dataForm.expireTime"
|
||||
type="datetime"
|
||||
:placeholder="$t('account.expireTime')"
|
||||
:placeholder="$t('peer.expireTime')"
|
||||
value-format="x"
|
||||
:shortcuts="shortcuts"
|
||||
clearable
|
||||
@@ -388,11 +367,11 @@
|
||||
@close="closeDialogKick"
|
||||
>
|
||||
<el-form ref="kickFormRef" :model="kickForm" label-width="100px">
|
||||
<el-form-item :label="$t('account.kickUtilTime')" prop="kickUtilTime">
|
||||
<el-form-item :label="$t('peer.kickUtilTime')" prop="kickUtilTime">
|
||||
<el-date-picker
|
||||
v-model="kickForm.kickUtilTime"
|
||||
type="datetime"
|
||||
:placeholder="$t('account.kickUtilTime')"
|
||||
:placeholder="$t('peer.kickUtilTime')"
|
||||
value-format="x"
|
||||
:shortcuts="shortcutsKick"
|
||||
clearable
|
||||
@@ -443,23 +422,23 @@ export default {
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccountForm,
|
||||
AccountPageDto,
|
||||
AccountUpdateDto,
|
||||
AccountVo,
|
||||
KickAccountForm,
|
||||
} from "@/api/account/types";
|
||||
KickPeerForm,
|
||||
PeerForm,
|
||||
PeerPageDto,
|
||||
PeerUpdateDto,
|
||||
PeerVo,
|
||||
} from "@/api/peer/types";
|
||||
import {
|
||||
saveAccountApi,
|
||||
deleteAccountApi,
|
||||
getAccountApi,
|
||||
pageAccountApi,
|
||||
updateAccountApi,
|
||||
exportAccountApi,
|
||||
releaseKickAccountApi,
|
||||
importAccountApi,
|
||||
resetTrafficApi,
|
||||
} from "@/api/account";
|
||||
deletePeerApi,
|
||||
exportPeerApi,
|
||||
getPeerApi,
|
||||
importPeerApi,
|
||||
pagePeerApi,
|
||||
releaseKickPeerApi,
|
||||
resetPeerTrafficApi,
|
||||
savePeerApi,
|
||||
updatePeerApi,
|
||||
} from "@/api/peer";
|
||||
import { Search, Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
timestampToDateTime,
|
||||
@@ -474,7 +453,6 @@ import { formatBytes } from "@/utils/byte";
|
||||
|
||||
import {
|
||||
hysteria2KickApi,
|
||||
hysteria2SubscribeUrlApi,
|
||||
hysteria2UrlApi,
|
||||
} from "@/api/hysteria2";
|
||||
import {
|
||||
@@ -484,20 +462,13 @@ import {
|
||||
} from "element-plus/lib/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import {
|
||||
Hysteria2SubscribeUrlDto,
|
||||
Hysteria2UrlDto,
|
||||
} from "@/api/hysteria2/types";
|
||||
import { Hysteria2UrlDto } from "@/api/hysteria2/types";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const queryFormRef = ref(ElForm); // Форма поиска
|
||||
const dataFormRef = ref(ElForm); // Форма пользователя
|
||||
const kickFormRef = ref(ElForm); // Форма отключения пользователя
|
||||
const dataFormPassRef = ref(ElInput);
|
||||
const dataFormConPassRef = ref(ElInput);
|
||||
|
||||
const dataFormAddRules = {
|
||||
remark: [
|
||||
@@ -511,7 +482,7 @@ const dataFormAddRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -520,22 +491,10 @@ const dataFormAddRules = {
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
pass: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Pass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
conPass: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
{
|
||||
@@ -547,21 +506,21 @@ const dataFormAddRules = {
|
||||
expireTime: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
deviceNo: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
deleted: [
|
||||
{
|
||||
required: true,
|
||||
message: "Required",
|
||||
message: t("common.required"),
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
@@ -583,13 +542,6 @@ const dataFormUpdateRules = {
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
pass: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
message: "Pass format is incorrect",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
conPass: [
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||
@@ -601,34 +553,34 @@ const dataFormUpdateRules = {
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
text: "A week later",
|
||||
text: t("common.weekLater"),
|
||||
value: getWeekLater,
|
||||
},
|
||||
{
|
||||
text: "A month later",
|
||||
text: t("common.monthLater"),
|
||||
value: getMonthLater,
|
||||
},
|
||||
{
|
||||
text: "A year later",
|
||||
text: t("common.yearLater"),
|
||||
value: getYearLater,
|
||||
},
|
||||
];
|
||||
|
||||
const shortcutsKick = [
|
||||
{
|
||||
text: "A hour later",
|
||||
text: t("common.hourLater"),
|
||||
value: getHourLater,
|
||||
},
|
||||
{
|
||||
text: "A day later",
|
||||
text: t("common.dayLater"),
|
||||
value: getDayLater,
|
||||
},
|
||||
{
|
||||
text: "A week later",
|
||||
text: t("common.weekLater"),
|
||||
value: getWeekLater,
|
||||
},
|
||||
{
|
||||
text: "A month later",
|
||||
text: t("common.monthLater"),
|
||||
value: getMonthLater,
|
||||
},
|
||||
];
|
||||
@@ -636,7 +588,7 @@ const shortcutsKick = [
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
total: 0,
|
||||
records: [] as AccountVo[],
|
||||
records: [] as PeerVo[],
|
||||
dialog: {
|
||||
visible: false,
|
||||
} as DialogType,
|
||||
@@ -648,17 +600,17 @@ const state = reactive({
|
||||
expireTime: getMonthLater(),
|
||||
deviceNo: 6,
|
||||
deleted: 0,
|
||||
} as AccountForm,
|
||||
} as PeerForm,
|
||||
kickForm: {
|
||||
kickUtilTime: getHourLater(),
|
||||
} as KickAccountForm,
|
||||
} as KickPeerForm,
|
||||
queryParams: {
|
||||
remark: undefined,
|
||||
username: undefined,
|
||||
deleted: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
} as AccountPageDto,
|
||||
} as PeerPageDto,
|
||||
quotaTmp: 0,
|
||||
fileList: [] as UploadFile[],
|
||||
qrCodeDialog: {
|
||||
@@ -699,7 +651,7 @@ const resetDataForm = () => {
|
||||
const handleQuery = async () => {
|
||||
state.loading = true;
|
||||
try {
|
||||
const { data } = await pageAccountApi(state.queryParams);
|
||||
const { data } = await pagePeerApi(state.queryParams);
|
||||
state.records = data.records;
|
||||
state.total = data.total;
|
||||
} finally {
|
||||
@@ -730,7 +682,7 @@ const handleAdd = () => {
|
||||
**/
|
||||
const handleUpdate = async (row: { [key: string]: any }) => {
|
||||
const id = row.id;
|
||||
const { data } = await getAccountApi({ id: id });
|
||||
const { data } = await getPeerApi({ id: id });
|
||||
Object.assign(state.dataForm, data);
|
||||
quotaTmp.value = data.quota;
|
||||
dialog.value = {
|
||||
@@ -750,15 +702,15 @@ const submitForm = () => {
|
||||
dataFormRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
const accountId = state.dataForm.id;
|
||||
let accountUpdateDto: AccountUpdateDto = { ...state.dataForm };
|
||||
let accountUpdateDto: PeerUpdateDto = { ...state.dataForm };
|
||||
if (accountId) {
|
||||
updateAccountApi(accountUpdateDto).then(() => {
|
||||
updatePeerApi(accountUpdateDto).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
closeDialog();
|
||||
handleQuery();
|
||||
});
|
||||
} else {
|
||||
saveAccountApi(accountUpdateDto).then(() => {
|
||||
savePeerApi(accountUpdateDto).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
closeDialog();
|
||||
handleQuery();
|
||||
@@ -791,10 +743,8 @@ const handleDelete = (row: { [key: string]: any }) => {
|
||||
const id = row.id;
|
||||
const username = row.username;
|
||||
ElMessageBox.confirm(
|
||||
"Are you sure to delete the data item with the username「" +
|
||||
username +
|
||||
"」?",
|
||||
"Warning",
|
||||
t("common.deleteConfirm", { username }),
|
||||
t("common.warning"),
|
||||
{
|
||||
confirmButtonText: t("common.confirm"),
|
||||
cancelButtonText: t("common.cancel"),
|
||||
@@ -802,7 +752,7 @@ const handleDelete = (row: { [key: string]: any }) => {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteAccountApi({ id: id }).then(() => {
|
||||
deletePeerApi({ id: id }).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
handleQuery();
|
||||
});
|
||||
@@ -817,7 +767,7 @@ const handleDelete = (row: { [key: string]: any }) => {
|
||||
const handleKick = (row: { [key: string]: any }) => {
|
||||
state.kickForm.ids = [row.id];
|
||||
dialogKick.value = {
|
||||
title: t("account.kickTip"),
|
||||
title: t("peer.kickTip"),
|
||||
visible: true,
|
||||
};
|
||||
};
|
||||
@@ -827,8 +777,8 @@ const handleKick = (row: { [key: string]: any }) => {
|
||||
* @param row
|
||||
*/
|
||||
const confirmReleaseKick = (row: { [key: string]: any }) => {
|
||||
releaseKickAccountApi({ id: row.id }).then(() => {
|
||||
ElMessage.success(t("account.releaseSuccess"));
|
||||
releaseKickPeerApi({ id: row.id }).then(() => {
|
||||
ElMessage.success(t("peer.releaseSuccess"));
|
||||
handleQuery();
|
||||
});
|
||||
};
|
||||
@@ -862,20 +812,21 @@ const handleImport = (params: UploadRequestOptions) => {
|
||||
if (state.fileList.length > 0) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", params.file);
|
||||
importAccountApi(formData).then(() => {
|
||||
importPeerApi(formData).then(() => {
|
||||
ElMessage.success(t("common.success"));
|
||||
});
|
||||
state.fileList = [];
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const beforeImport = (file: UploadRawFile) => {
|
||||
if (!file.name.endsWith(".json")) {
|
||||
ElMessage.error("file format not supported");
|
||||
ElMessage.error(t("common.fileFormatUnsupported"));
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 2) {
|
||||
ElMessage.error("the file is too big, less than 2 MB");
|
||||
ElMessage.error(t("common.fileTooLarge"));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -884,7 +835,7 @@ const beforeImport = (file: UploadRawFile) => {
|
||||
* Экспорт
|
||||
*/
|
||||
const handleExport = () => {
|
||||
exportAccountApi().then((res) => {
|
||||
exportPeerApi().then((res) => {
|
||||
const blob = new Blob([res.data], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
@@ -901,20 +852,6 @@ const handleExport = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubscribe = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: row.id,
|
||||
protocol: window.location.protocol,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
copy(data.url);
|
||||
ElMessage.success(t("common.copySuccess"));
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeUrl = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
@@ -943,7 +880,7 @@ const handleQrCode = async (row: { [key: string]: any }) => {
|
||||
|
||||
const resetTraffic = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
await resetTrafficApi({ id: row.id });
|
||||
await resetPeerTrafficApi({ id: row.id });
|
||||
ElMessage.success(t("common.success"));
|
||||
await handleQuery();
|
||||
} catch (e) {
|
||||
@@ -954,21 +891,6 @@ const resetTraffic = async (row: { [key: string]: any }) => {
|
||||
onMounted(() => {
|
||||
// Инициализация списка пользователей
|
||||
handleQuery();
|
||||
if (route.query.focus === "change-pass") {
|
||||
nextTick(() => {
|
||||
handleUpdate({ id: 1 }).then(() => {
|
||||
setTimeout(() => {
|
||||
const inputPass = dataFormPassRef.value.$el.querySelector(
|
||||
".el-input__wrapper input"
|
||||
);
|
||||
if (inputPass) {
|
||||
inputPass.focus();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user