Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа

This commit is contained in:
2026-05-08 23:16:25 +05:00
parent de4a65a959
commit 1bca73814e
85 changed files with 4119 additions and 1911 deletions
-1
View File
@@ -29,4 +29,3 @@ module.exports = {
OptionType: "readonly",
},
};
цц
-157
View File
@@ -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",
});
}
+26
View File
@@ -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",
});
}
+16
View File
@@ -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[];
}
+39
View File
@@ -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",
});
}
+72
View File
@@ -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;
}
-12
View File
@@ -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> {
-12
View File
@@ -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;
}
-19
View File
@@ -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",
});
}
-15
View File
@@ -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;
}
+80
View File
@@ -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);
});
+24 -9
View File
@@ -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",
+24 -9
View File
@@ -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>
+5 -5
View File
@@ -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();
}
+19 -40
View File
@@ -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;
@@ -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);
}
+25 -11
View File
@@ -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
View File
@@ -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']>
+8 -8
View File
@@ -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>
+156
View File
@@ -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>
+9 -9
View File
@@ -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>
@@ -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>