fix: полный продакшен-фикс dashboard/peer/hysteria по fix35

This commit is contained in:
2026-05-09 15:38:30 +05:00
parent a4157d1363
commit 78656e74b0
18 changed files with 190 additions and 85 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ export interface PeerVo extends IdDto {
export interface PeerClientConfigVo {
url: string;
qrCode: string | Uint8Array;
qrCode?: string | Uint8Array;
}
export interface KickPeerDto {
+1
View File
@@ -38,6 +38,7 @@ export default {
todayUpload: "Today upload",
trafficChart: "Traffic & System Timeseries",
trafficSeriesChart: "Traffic: download/upload",
noTrafficData: "No traffic data for the selected period",
systemSeriesChart: "System: CPU/RAM",
download: "Download",
upload: "Upload",
+1
View File
@@ -36,6 +36,7 @@ export default {
todayUpload: "Отдано за сегодня",
trafficChart: "Трафик и системные метрики",
trafficSeriesChart: "Трафик: download/upload",
noTrafficData: "Нет данных трафика за выбранный период",
systemSeriesChart: "Система: CPU/RAM",
download: "Скачано",
upload: "Отдано",
+15 -8
View File
@@ -82,7 +82,9 @@ function logout() {
<!-- Аватар пользователя -->
<el-dropdown trigger="click">
<div class="avatar-container">
<img src="/src/assets/logo.png" />
<el-avatar :size="32" class="avatar-icon">
<i-ep-user-filled />
</el-avatar>
<i-ep-caret-bottom class="w-3 h-3" />
</div>
<template #dropdown>
@@ -128,16 +130,21 @@ function logout() {
.avatar-container {
display: flex;
align-items: center;
justify-items: center;
margin: 0 5px;
justify-content: center;
gap: 6px;
height: 36px;
margin: 0 8px;
padding: 0 8px;
border-radius: 8px;
cursor: pointer;
img {
width: 40px;
height: 40px;
border-radius: 5px;
&:hover {
background: rgb(249 250 251 / 100%);
}
.avatar-icon {
font-size: 18px;
}
}
}
</style>
+4 -1
View File
@@ -8,6 +8,7 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
@@ -18,9 +19,9 @@ declare module '@vue/runtime-core' {
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
@@ -46,10 +47,12 @@ declare module '@vue/runtime-core' {
IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpDownload: typeof import('~icons/ep/download')['default']
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default']
IEpRefresh: typeof import('~icons/ep/refresh')['default']
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default']
IEpSetting: typeof import('~icons/ep/setting')['default']
IEpUpload: typeof import('~icons/ep/upload')['default']
IEpUserFilled: typeof import('~icons/ep/user-filled')['default']
ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default']
LangSelect: typeof import('./../components/LangSelect/index.vue')['default']
MapAdd: typeof import('./../components/MapAdd/index.vue')['default']
+24 -22
View File
@@ -52,7 +52,12 @@
</div>
</template>
<div v-loading="timeseriesLoading" class="chart-grid">
<v-chart class="chart" autoresize :option="trafficChartOption" />
<el-empty
v-if="!timeseriesLoading && (timeseries.traffic?.length || 0) === 0"
:description="$t('dashboard.noTrafficData')"
class="chart-empty"
/>
<v-chart v-else class="chart" autoresize :option="trafficChartOption" />
<v-chart class="chart" autoresize :option="systemChartOption" />
</div>
</el-card>
@@ -160,14 +165,6 @@ const loadDashboard = async () => {
}
};
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 || []) {
@@ -176,13 +173,15 @@ const systemXAxisLabels = computed(() => {
return Array.from(allTs).sort((a, b) => a - b);
});
const trafficMap = computed(() => {
const m = new Map<number, { download: number; upload: number }>();
for (const item of timeseries.value.traffic || []) {
m.set(item.ts, { download: item.download || 0, upload: item.upload || 0 });
}
return m;
});
const trafficDownloadData = computed(() =>
(timeseries.value.traffic || []).map((item) => [item.ts, item.download || 0])
);
const trafficUploadData = computed(() =>
(timeseries.value.traffic || []).map((item) => [item.ts, item.upload || 0])
);
const hasSingleTrafficPoint = computed(() => (timeseries.value.traffic || []).length === 1);
const systemMap = computed(() => {
const m = new Map<number, { cpu: number; mem: number }>();
@@ -202,9 +201,8 @@ const trafficChartOption = computed(() => ({
grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true },
dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }],
xAxis: {
type: "category",
type: "time",
boundaryGap: false,
data: trafficXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
},
yAxis: {
type: "value",
@@ -217,15 +215,15 @@ const trafficChartOption = computed(() => ({
name: t("dashboard.download"),
type: "line",
smooth: true,
showSymbol: false,
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
showSymbol: hasSingleTrafficPoint.value,
data: trafficDownloadData.value,
},
{
name: t("dashboard.upload"),
type: "line",
smooth: true,
showSymbol: false,
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
showSymbol: hasSingleTrafficPoint.value,
data: trafficUploadData.value,
},
],
}));
@@ -323,5 +321,9 @@ onUnmounted(() => {
width: 100%;
height: 340px;
}
.chart-empty {
height: 340px;
}
</style>
@@ -23,7 +23,7 @@
<el-card shadow="never">
<el-form inline>
<el-form-item>
<el-tag style="height: 32px">
<el-tag style="height: 32px" class="hysteria-version-tag">
{{ $t("hysteria.hysteria2Version") }}:
{{ hysteria2Monitor.version ? hysteria2Monitor.version : "-" }}
</el-tag>
@@ -1251,4 +1251,11 @@ onMounted(() => {
max-width: 1000px;
margin: 0 auto;
}
.hysteria-version-tag {
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+36 -24
View File
@@ -56,23 +56,27 @@
<el-table-column :label="$t('peer.lastConnectionAt')" min-width="170">
<template #default="scope">{{ scope.row.lastConnectionAt ? timestampToDateTime(scope.row.lastConnectionAt) : '-' }}</template>
</el-table-column>
<el-table-column :label="$t('common.operate')" width="230" fixed="right">
<el-table-column :label="$t('common.operate')" width="230" fixed="right" align="right" header-align="right">
<template #default="scope">
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.copyUri") }}</el-button>
<el-dropdown>
<span class="el-dropdown-link">{{ $t("peer.more") }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-dropdown-item>
<el-dropdown-item @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-dropdown-item>
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{ $t("common.resetTraffic") }}</el-dropdown-item>
<el-dropdown-item @click="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item>
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{ $t("peer.releaseKick") }}</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="peer-actions">
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.copyUri") }}</el-button>
<el-dropdown trigger="click" placement="bottom-end">
<el-button text circle class="peer-more-btn" @click.stop>
<i-ep-more-filled />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-dropdown-item>
<el-dropdown-item @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-dropdown-item>
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{ $t("common.resetTraffic") }}</el-dropdown-item>
<el-dropdown-item @click="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item>
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{ $t("peer.releaseKick") }}</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
@@ -101,7 +105,9 @@
</el-dialog>
<el-dialog v-model="qrDialog" :title="$t('peer.clientQr')" width="420px">
<el-image style="width:300px;height:300px" :src="qrSrc" />
<div class="qr-dialog-body">
<qrcode-vue :value="qrUrl" :size="260" level="M" render-as="svg" />
</div>
</el-dialog>
<el-drawer v-model="overview.visible" :title="$t('peer.overview')" size="45%">
@@ -127,8 +133,10 @@
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.copyUri") }}</el-button>
<el-button @click="loadOverviewQr(overview.data)">{{ $t("peer.clientQr") }}</el-button>
</div>
<div class="mt-2" v-if="overviewQrSrc">
<el-image style="width:220px;height:220px" :src="overviewQrSrc" />
<div class="mt-2" v-if="overviewQrUrl">
<div class="qr-dialog-body">
<qrcode-vue :value="overviewQrUrl" :size="220" level="M" render-as="svg" />
</div>
</div>
</div>
</el-drawer>
@@ -137,6 +145,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import QrcodeVue from "qrcode.vue";
import { useI18n } from "vue-i18n";
import copy from "copy-to-clipboard";
import { ElMessage, ElMessageBox } from "element-plus";
@@ -163,9 +172,9 @@ const loading = ref(false);
const total = ref(0);
const records = ref<PeerVo[]>([]);
const qrDialog = ref(false);
const qrSrc = ref("");
const qrUrl = ref("");
const importFileList = ref<UploadFile[]>([]);
const overviewQrSrc = ref("");
const overviewQrUrl = ref("");
const overviewClientUrl = ref("");
const formRef = ref();
const overview = reactive<{ visible: boolean; data: PeerVo | null }>({ visible: false, data: null });
@@ -204,7 +213,7 @@ function trafficPercent(row: PeerVo) {
function openOverview(row: PeerVo) {
overview.data = row;
overview.visible = true;
overviewQrSrc.value = "";
overviewQrUrl.value = "";
overviewClientUrl.value = "";
void loadOverviewClientConfig(row);
}
@@ -217,7 +226,7 @@ async function loadOverviewClientConfig(row: PeerVo) {
async function loadOverviewQr(row: PeerVo) {
const { data } = await getPeerClientConfigApi(row.id);
overviewClientUrl.value = data.url;
overviewQrSrc.value = `data:image/png;base64,${data.qrCode}`;
overviewQrUrl.value = data.url;
}
async function handleQuery() {
@@ -293,7 +302,7 @@ async function copyUri(row: PeerVo) {
async function showQr(row: PeerVo) {
const { data } = await getPeerClientConfigApi(row.id);
qrSrc.value = `data:image/png;base64,${data.qrCode}`;
qrUrl.value = data.url;
qrDialog.value = true;
}
@@ -344,5 +353,8 @@ onMounted(handleQuery);
<style scoped>
.peer-title { font-weight: 600; }
.peer-sub { color: #909399; font-size: 12px; }
.peer-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.peer-more-btn { font-size: 16px; }
.qr-dialog-body { display: flex; justify-content: center; align-items: center; padding: 12px 0 20px; }
</style>