Полная зачистка 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
@@ -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>