fix28: полный production-фикс графиков, machine-auth, i18n и peer import/export

This commit is contained in:
2026-05-09 02:14:59 +05:00
parent 3c569061fe
commit 1139c428b6
11 changed files with 230 additions and 28 deletions
+20
View File
@@ -169,6 +169,7 @@ export default {
more: "More",
},
config: {
orchestratorManaged: "Managed by hy2xs-orchestrator reconfigure",
huiWebPort: "HY2XS admin Web Port",
huiWebContext: "HY2XS admin Web Context",
hysteria2TrafficTime: "Hysteria2 Traffic Time",
@@ -185,6 +186,10 @@ export default {
"Scheduled task expression, reference: https://pkg.go.dev/github.com/robfig/cron/v3",
resetTrafficMonth: "Run once a month, midnight, first of month",
resetTrafficWeek: "Run once a week, midnight between Sat/Sun",
mustBeInteger: "Field must be an integer",
invalidWebContext:
"Field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
invalidTrafficTime: "Field must be a number with up to one decimal place",
},
monitor: {
huiVersion: "HY2XS admin Version",
@@ -200,6 +205,21 @@ export default {
},
log: {
numLine: "Number of lines",
level: "Level",
message: "Message",
time: "Time",
},
errorPage: {
back: "Back",
oops: "Oops!",
forbiddenTitle: "You do not have permission to access this page",
forbiddenGo: "Or you can go:",
backHome: "Back to Home",
forbiddenImageAlt: "Girl has dropped her ice cream.",
notFoundHeadline: "The webmaster said that you can not enter this page...",
notFoundInfo:
"Please check that the URL you entered is correct, or click the button below to return to the homepage.",
notFoundBackHome: "Back to home",
},
hysteria: {
enable: "Enable",
+20
View File
@@ -164,6 +164,7 @@ export default {
releaseKickTip: "Снять офлайн-статус",
},
config: {
orchestratorManaged: "Управляется hy2xs-orchestrator reconfigure",
huiWebPort: "Порт HY2XS admin",
huiWebContext: "Web-контекст HY2XS admin",
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
@@ -179,6 +180,10 @@ export default {
resetTrafficCronTip: "Cron-выражение для планового сброса трафика",
resetTrafficMonth: "Раз в месяц, в полночь первого дня",
resetTrafficWeek: "Раз в неделю, в полночь между субботой и воскресеньем",
mustBeInteger: "Поле должно быть целым числом",
invalidWebContext:
"Поле должно начинаться с / и содержать только строчные буквы (a-z) и цифры (0-9)",
invalidTrafficTime: "Поле должно быть числом максимум с одним знаком после запятой",
},
monitor: {
huiVersion: "Версия HY2XS admin",
@@ -194,6 +199,21 @@ export default {
},
log: {
numLine: "Количество строк",
level: "Уровень",
message: "Сообщение",
time: "Время",
},
errorPage: {
back: "Назад",
oops: "Упс!",
forbiddenTitle: "У вас нет прав для доступа к этой странице",
forbiddenGo: "Или вы можете перейти:",
backHome: "На главную",
forbiddenImageAlt: "Девочка уронила своё мороженое.",
notFoundHeadline: "Вебмастер сообщил, что вам нельзя на эту страницу...",
notFoundInfo:
"Проверьте правильность URL или нажмите кнопку ниже, чтобы вернуться на главную страницу.",
notFoundBackHome: "На главную",
},
hysteria: {
enable: "Включить",
@@ -44,7 +44,7 @@
</div>
<el-alert
title="Managed by hy2xs-orchestrator reconfigure"
:title="$t('config.orchestratorManaged')"
type="info"
:closable="false"
style="margin-bottom: 12px"
@@ -205,7 +205,7 @@ const dataFormRules = {
},
{
pattern: /^\d+$/,
message: "field must be a integer",
message: t("config.mustBeInteger"),
trigger: ["change", "blur"],
},
],
@@ -217,7 +217,7 @@ const dataFormRules = {
},
{
pattern: /^\/([a-z0-9]+(\/[a-z0-9]+)*)?$/,
message: "field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
message: t("config.invalidWebContext"),
trigger: ["change", "blur"],
},
],
@@ -229,7 +229,7 @@ const dataFormRules = {
},
{
pattern: /^\d+(\.\d)?$/,
message: "field must be a number with up to one decimal place",
message: t("config.invalidTrafficTime"),
trigger: ["change", "blur"],
},
],
+12 -7
View File
@@ -160,11 +160,16 @@ const loadDashboard = async () => {
}
};
const xAxisLabels = computed(() => {
const trafficXAxisLabels = computed(() => {
const allTs = new Set<number>();
for (const item of timeseries.value.traffic || []) {
allTs.add(item.ts);
}
return Array.from(allTs).sort((a, b) => a - b);
});
const systemXAxisLabels = computed(() => {
const allTs = new Set<number>();
for (const item of timeseries.value.system || []) {
allTs.add(item.ts);
}
@@ -199,7 +204,7 @@ const trafficChartOption = computed(() => ({
xAxis: {
type: "category",
boundaryGap: false,
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
data: trafficXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
},
yAxis: {
type: "value",
@@ -213,14 +218,14 @@ const trafficChartOption = computed(() => ({
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
},
{
name: t("dashboard.upload"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
},
],
}));
@@ -237,7 +242,7 @@ const systemChartOption = computed(() => ({
xAxis: {
type: "category",
boundaryGap: false,
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
data: systemXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
},
yAxis: {
type: "value",
@@ -251,14 +256,14 @@ const systemChartOption = computed(() => ({
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
},
{
name: t("dashboard.ram"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
},
],
}));
+9 -6
View File
@@ -8,6 +8,9 @@ export default {
<script setup lang="ts">
import { reactive, toRefs } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const state = reactive({
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
@@ -24,15 +27,15 @@ function back() {
<template>
<div class="errPage-container">
<el-button class="pan-back-btn" @click="back">Back</el-button>
<el-button class="pan-back-btn" @click="back">{{ $t("errorPage.back") }}</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">Oops!</h1>
<h2>You do not have permission to access this page</h2>
<h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1>
<h2>{{ $t("errorPage.forbiddenTitle") }}</h2>
<ul class="list-unstyled">
<li>Or you can go:</li>
<li>{{ $t("errorPage.forbiddenGo") }}</li>
<li class="link-type">
<router-link to="/">Back to Home</router-link>
<router-link to="/">{{ $t("errorPage.backHome") }}</router-link>
</li>
</ul>
</el-col>
@@ -41,7 +44,7 @@ function back() {
:src="errGif"
width="313"
height="428"
alt="Girl has dropped her ice cream."
:alt="t('errorPage.forbiddenImageAlt')"
/>
</el-col>
</el-row>
+8 -5
View File
@@ -6,8 +6,12 @@ export default {
</script>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
function message() {
return "The webmaster said that you can not enter this page...";
return t("errorPage.notFoundHeadline");
}
</script>
@@ -37,13 +41,12 @@ function message() {
/>
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__oops">{{ $t("errorPage.oops") }}</div>
<div class="bullshit__headline">{{ message() }}</div>
<div class="bullshit__info">
Please check that the URL you entered is correct, or click the button
below to return to the homepage.
{{ $t("errorPage.notFoundInfo") }}
</div>
<a href="" class="bullshit__return-home">Back to home</a>
<a href="/#/" class="bullshit__return-home">{{ $t("errorPage.notFoundBackHome") }}</a>
</div>
</div>
</div>
+59 -2
View File
@@ -15,6 +15,18 @@
<el-card shadow="never">
<div class="mb-3">
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
<el-upload
v-model:file-list="importFileList"
:http-request="handleImport"
:show-file-list="false"
accept=".json"
:limit="1"
:before-upload="beforeImport"
class="inline-block ml-2"
>
<el-button>{{ $t("common.import") }}</el-button>
</el-upload>
<el-button class="ml-2" @click="handleExport">{{ $t("common.export") }}</el-button>
</div>
<el-table v-loading="loading" :data="records">
<el-table-column :label="$t('peer.name')" min-width="220">
@@ -88,7 +100,7 @@
</template>
</el-dialog>
<el-dialog v-model="qrDialog" title="QR" width="420px">
<el-dialog v-model="qrDialog" :title="$t('peer.clientQr')" width="420px">
<el-image style="width:300px;height:300px" :src="qrSrc" />
</el-dialog>
@@ -127,21 +139,24 @@
import { computed, onMounted, reactive, ref } from "vue";
import { useI18n } from "vue-i18n";
import copy from "copy-to-clipboard";
import { ElMessageBox } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatBytes } from "@/utils/byte";
import { getMonthLater, timestampToDateTime } from "@/utils/time";
import {
deletePeerApi,
getPeerApi,
getPeerClientConfigApi,
importPeerApi,
kickPeerApi,
pagePeerApi,
releaseKickPeerApi,
resetPeerTrafficApi,
savePeerApi,
updatePeerApi,
exportPeerApi,
} from "@/api/peer";
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types";
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus/lib/components";
const { t } = useI18n();
const loading = ref(false);
@@ -149,6 +164,7 @@ const total = ref(0);
const records = ref<PeerVo[]>([]);
const qrDialog = ref(false);
const qrSrc = ref("");
const importFileList = ref<UploadFile[]>([]);
const overviewQrSrc = ref("");
const overviewClientUrl = ref("");
const formRef = ref();
@@ -281,6 +297,47 @@ async function showQr(row: PeerVo) {
qrDialog.value = true;
}
async function handleImport(params: UploadRequestOptions) {
if (importFileList.value.length <= 0) {
return;
}
const formData = new FormData();
formData.append("file", params.file);
await importPeerApi(formData);
importFileList.value = [];
await handleQuery();
}
function beforeImport(file: UploadRawFile) {
if (!file.name.endsWith(".json")) {
ElMessage.error(t("common.fileFormatUnsupported"));
return false;
}
if (file.size / 1024 / 1024 > 2) {
ElMessage.error(t("common.fileTooLarge"));
return false;
}
return true;
}
async function handleExport() {
try {
const response = await exportPeerApi();
const blob = new Blob([response.data], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.href = url;
const dis = response.headers["content-disposition"] || "";
a.download = dis.split("attachment; filename=")[1] || "peers-export.json";
a.click();
window.URL.revokeObjectURL(url);
ElMessage.success(t("common.downloadSuccess"));
} catch {
ElMessage.error(t("common.invalid"));
}
}
onMounted(handleQuery);
</script>