fix28: полный production-фикс графиков, machine-auth, i18n и peer import/export
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user