fix26.2: полная UI-полировка peer/dashboard и завершение i18n
This commit is contained in:
@@ -25,6 +25,18 @@ export default {
|
||||
stale: "Dashboard data is stale. Retrying automatically...",
|
||||
topPeers24h: "Top peers (24h)",
|
||||
refreshFailed: "Failed to refresh dashboard data",
|
||||
cpu: "CPU",
|
||||
ram: "RAM",
|
||||
disk: "Disk",
|
||||
peers: "Peers",
|
||||
onlinePeers: "Online peers",
|
||||
onlineDevices: "Online devices",
|
||||
todayDownload: "Today download",
|
||||
todayUpload: "Today upload",
|
||||
trafficChart: "Traffic & System Timeseries",
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
total: "Total",
|
||||
},
|
||||
admin: {
|
||||
changePasswordTitle: "Change password",
|
||||
@@ -102,8 +114,22 @@ export default {
|
||||
"I want to be a shooting star, cutting through the darkness, just to illuminate your dreams, good night🌛!",
|
||||
},
|
||||
peer: {
|
||||
name: "Peer",
|
||||
remark: "Remark",
|
||||
username: "Username",
|
||||
secret: "Secret",
|
||||
maxDevices: "Max devices",
|
||||
disabled: "Disabled",
|
||||
status: "Status",
|
||||
traffic: "Traffic",
|
||||
devices: "Devices",
|
||||
lastConnectionAt: "Last connection",
|
||||
overview: "Overview",
|
||||
authId: "Auth ID",
|
||||
clientUri: "Client URI",
|
||||
clientQr: "Client QR",
|
||||
unlimited: "Unlimited",
|
||||
secretRotateConfirm: "Rotate peer secret? Existing client configurations will stop working until updated.",
|
||||
pass: "Pass",
|
||||
conPass: "ConPass",
|
||||
quota: "Quota",
|
||||
|
||||
@@ -23,6 +23,18 @@ export default {
|
||||
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
|
||||
topPeers24h: "Топ пиров (24ч)",
|
||||
refreshFailed: "Не удалось обновить данные дашборда",
|
||||
cpu: "CPU",
|
||||
ram: "RAM",
|
||||
disk: "Диск",
|
||||
peers: "Пиры",
|
||||
onlinePeers: "Пиры онлайн",
|
||||
onlineDevices: "Устройства онлайн",
|
||||
todayDownload: "Скачано за сегодня",
|
||||
todayUpload: "Отдано за сегодня",
|
||||
trafficChart: "Трафик и системные метрики",
|
||||
download: "Скачано",
|
||||
upload: "Отдано",
|
||||
total: "Всего",
|
||||
},
|
||||
admin: {
|
||||
changePasswordTitle: "Смена пароля",
|
||||
@@ -97,8 +109,22 @@ export default {
|
||||
greeting5: "Доброй ночи,",
|
||||
},
|
||||
peer: {
|
||||
name: "Пир",
|
||||
remark: "Комментарий",
|
||||
username: "Логин",
|
||||
secret: "Секрет",
|
||||
maxDevices: "Лимит устройств",
|
||||
disabled: "Отключён",
|
||||
status: "Статус",
|
||||
traffic: "Трафик",
|
||||
devices: "Устройства",
|
||||
lastConnectionAt: "Последнее подключение",
|
||||
overview: "Обзор",
|
||||
authId: "Auth ID",
|
||||
clientUri: "URI клиента",
|
||||
clientQr: "QR клиента",
|
||||
unlimited: "Безлимит",
|
||||
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
|
||||
pass: "Пароль входа",
|
||||
conPass: "Пароль подключения",
|
||||
quota: "Квота",
|
||||
|
||||
+4
@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
@@ -26,6 +27,9 @@ declare module '@vue/runtime-core' {
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
|
||||
@@ -29,27 +29,48 @@
|
||||
/>
|
||||
|
||||
<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-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.cpu") }}: {{ summary.system.cpuPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.ram") }}: {{ summary.system.memPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.disk") }}: {{ summary.system.diskPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.peers") }}: {{ summary.peers.total }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlinePeers") }}: {{ summary.peers.onlinePeers }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlineDevices") }}: {{ summary.peers.onlineDevices }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayDownload") }}: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayUpload") }}: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mt-3">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<span>{{ $t("dashboard.trafficChart") }}</span>
|
||||
<el-radio-group v-model="range" size="small" @change="loadDashboard">
|
||||
<el-radio-button label="1h">1h</el-radio-button>
|
||||
<el-radio-button label="24h">24h</el-radio-button>
|
||||
<el-radio-button label="7d">7d</el-radio-button>
|
||||
<el-radio-button label="30d">30d</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="timeseriesRows" size="small" v-loading="timeseriesLoading">
|
||||
<el-table-column prop="ts" :label="$t('common.createTime')" width="180" />
|
||||
<el-table-column prop="download" :label="$t('dashboard.download')" />
|
||||
<el-table-column prop="upload" :label="$t('dashboard.upload')" />
|
||||
<el-table-column prop="cpu" :label="$t('dashboard.cpu')" width="120" />
|
||||
<el-table-column prop="mem" :label="$t('dashboard.ram')" width="120" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt-3">
|
||||
<template #header>{{ $t("dashboard.topPeers24h") }}</template>
|
||||
<el-table :data="topPeers" size="small">
|
||||
<el-table-column prop="name" label="Peer" />
|
||||
<el-table-column prop="download" label="Download">
|
||||
<el-table-column prop="name" :label="$t('peer.name')" />
|
||||
<el-table-column prop="download" :label="$t('dashboard.download')">
|
||||
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upload" label="Upload">
|
||||
<el-table-column prop="upload" :label="$t('dashboard.upload')">
|
||||
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total" label="Total">
|
||||
<el-table-column prop="total" :label="$t('dashboard.total')">
|
||||
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -60,9 +81,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useIntervalFn } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||
import { DashboardSummaryVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
import { timestampToDateTime } from "@/utils/time";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -80,6 +102,9 @@ const summary = ref<DashboardSummaryVo>({
|
||||
});
|
||||
const topPeers = ref<DashboardTopPeerVo[]>([]);
|
||||
const securityRisks = ref<SecurityRiskVo[]>([]);
|
||||
const timeseries = ref<DashboardTimeseriesVo>({ range: "24h", traffic: [], system: [], collectedAt: 0 });
|
||||
const timeseriesLoading = ref(false);
|
||||
const range = ref("24h");
|
||||
const loadError = ref("");
|
||||
const loading = ref(false);
|
||||
const lastSuccessAt = ref(0);
|
||||
@@ -102,7 +127,7 @@ const loadDashboard = async () => {
|
||||
try {
|
||||
const [summaryRes, topRes, secRes] = await Promise.all([
|
||||
dashboardSummaryApi(),
|
||||
dashboardTopPeersApi("24h", 10),
|
||||
dashboardTopPeersApi(range.value, 10),
|
||||
dashboardSecurityApi(),
|
||||
]);
|
||||
summary.value = summaryRes.data;
|
||||
@@ -115,8 +140,33 @@ const loadDashboard = async () => {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
timeseriesLoading.value = true;
|
||||
try {
|
||||
const tsRes = await dashboardTimeseriesApi(range.value);
|
||||
timeseries.value = tsRes.data;
|
||||
} finally {
|
||||
timeseriesLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const timeseriesRows = computed(() => {
|
||||
const trafficMap: Record<number, { download: number; upload: number }> = {};
|
||||
for (const t of timeseries.value.traffic || []) {
|
||||
trafficMap[t.ts] = { download: t.download || 0, upload: t.upload || 0 };
|
||||
}
|
||||
return (timeseries.value.system || []).map((s) => {
|
||||
const tm = trafficMap[s.ts] || { download: 0, upload: 0 };
|
||||
return {
|
||||
ts: timestampToDateTime(s.ts),
|
||||
download: formatBytes(tm.download),
|
||||
upload: formatBytes(tm.upload),
|
||||
cpu: `${Math.round((s.cpu || 0) * 10) / 10}%`,
|
||||
mem: `${Math.round((s.mem || 0) * 10) / 10}%`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
||||
() => {
|
||||
loadDashboard();
|
||||
@@ -155,5 +205,11 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -17,27 +17,50 @@
|
||||
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="records">
|
||||
<el-table-column prop="id" :label="$t('common.id')" width="80" />
|
||||
<el-table-column prop="name" :label="$t('peer.username')" min-width="180" />
|
||||
<el-table-column prop="remark" :label="$t('peer.remark')" min-width="200" />
|
||||
<el-table-column :label="$t('peer.quota')" min-width="140">
|
||||
<template #default="scope">{{ formatBytes(scope.row.quotaBytes) }}</template>
|
||||
<el-table-column :label="$t('peer.name')" min-width="220">
|
||||
<template #default="scope">
|
||||
<div class="peer-title">{{ scope.row.name }}</div>
|
||||
<div class="peer-sub">{{ scope.row.remark || "-" }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('peer.download')" min-width="140">
|
||||
<template #default="scope">{{ formatBytes(scope.row.downloadBytes) }}</template>
|
||||
<el-table-column :label="$t('peer.status')" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="scope.row.disabled === 1 ? 'danger' : 'success'">{{ scope.row.disabled === 1 ? $t('common.disable') : $t('common.enable') }}</el-tag>
|
||||
<el-tag size="small" class="ml-1" :type="scope.row.online ? 'success' : 'info'">{{ scope.row.online ? $t('peer.online') : $t('peer.offline') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('peer.upload')" min-width="140">
|
||||
<template #default="scope">{{ formatBytes(scope.row.uploadBytes) }}</template>
|
||||
<el-table-column :label="$t('peer.traffic')" min-width="260">
|
||||
<template #default="scope">
|
||||
<div>{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }} / {{ quotaText(scope.row.quotaBytes) }}</div>
|
||||
<el-progress :percentage="trafficPercent(scope.row)" :status="scope.row.quotaBytes < 0 ? undefined : (trafficPercent(scope.row) >= 90 ? 'exception' : undefined)" :show-text="false" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('peer.devices')" min-width="120">
|
||||
<template #default="scope">{{ scope.row.onlineDevices }} / {{ scope.row.maxDevices }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('peer.expireTime')" min-width="170">
|
||||
<template #default="scope">{{ timestampToDateTime(scope.row.expiresAt) }}</template>
|
||||
<template #default="scope">{{ scope.row.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(scope.row.expiresAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operate')" width="320">
|
||||
<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="340" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="copyUri(scope.row)">URI</el-button>
|
||||
<el-button link type="primary" @click="showQr(scope.row)">QR</el-button>
|
||||
<el-button link type="primary" @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-button>
|
||||
<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.clientUri") }}</el-button>
|
||||
<el-button link type="primary" @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-button>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">{{ $t("common.operate") }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -51,14 +74,14 @@
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="620px">
|
||||
<el-form :model="dataForm" label-width="140px">
|
||||
<el-form-item label="Name"><el-input v-model="dataForm.name" /></el-form-item>
|
||||
<el-form-item label="Remark"><el-input v-model="dataForm.remark" /></el-form-item>
|
||||
<el-form-item label="Secret"><el-input v-model="dataForm.secret" show-password /></el-form-item>
|
||||
<el-form-item label="Quota"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item>
|
||||
<el-form-item label="Expires"><el-date-picker v-model="dataForm.expiresAt" type="datetime" value-format="x" /></el-form-item>
|
||||
<el-form-item label="Max devices"><el-input-number v-model="dataForm.maxDevices" :min="1" /></el-form-item>
|
||||
<el-form-item label="Disabled"><el-switch v-model="disabledBool" /></el-form-item>
|
||||
<el-form ref="formRef" :model="dataForm" :rules="rules" label-width="140px">
|
||||
<el-form-item :label="$t('peer.name')" prop="name"><el-input v-model="dataForm.name" /></el-form-item>
|
||||
<el-form-item :label="$t('peer.remark')"><el-input v-model="dataForm.remark" /></el-form-item>
|
||||
<el-form-item :label="$t('peer.secret')" prop="secret"><el-input v-model="dataForm.secret" show-password /></el-form-item>
|
||||
<el-form-item :label="$t('peer.quota')"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item>
|
||||
<el-form-item :label="$t('peer.expireTime')"><el-date-picker v-model="dataForm.expiresAt" type="datetime" value-format="x" /></el-form-item>
|
||||
<el-form-item :label="$t('peer.maxDevices')"><el-input-number v-model="dataForm.maxDevices" :min="1" /></el-form-item>
|
||||
<el-form-item :label="$t('peer.disabled')"><el-switch v-model="disabledBool" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
|
||||
@@ -68,6 +91,24 @@
|
||||
<el-dialog v-model="qrDialog" title="QR" width="420px">
|
||||
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="overview.visible" :title="$t('peer.overview')" size="45%">
|
||||
<div v-if="overview.data">
|
||||
<p><b>{{ $t('peer.authId') }}:</b> {{ overview.data.authId }}</p>
|
||||
<p><b>{{ $t('peer.name') }}:</b> {{ overview.data.name }}</p>
|
||||
<p><b>{{ $t('peer.remark') }}:</b> {{ overview.data.remark || '-' }}</p>
|
||||
<p><b>{{ $t('peer.quota') }}:</b> {{ quotaText(overview.data.quotaBytes) }}</p>
|
||||
<p><b>{{ $t('peer.download') }}:</b> {{ formatBytes(overview.data.downloadBytes) }}</p>
|
||||
<p><b>{{ $t('peer.upload') }}:</b> {{ formatBytes(overview.data.uploadBytes) }}</p>
|
||||
<p><b>{{ $t('peer.devices') }}:</b> {{ overview.data.onlineDevices }} / {{ overview.data.maxDevices }}</p>
|
||||
<p><b>{{ $t('peer.expireTime') }}:</b> {{ overview.data.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(overview.data.expiresAt) }}</p>
|
||||
<p><b>{{ $t('peer.lastConnectionAt') }}:</b> {{ overview.data.lastConnectionAt ? timestampToDateTime(overview.data.lastConnectionAt) : '-' }}</p>
|
||||
<div class="mt-2">
|
||||
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.clientUri") }}</el-button>
|
||||
<el-button @click="showQr(overview.data)">{{ $t("peer.clientQr") }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,13 +116,17 @@
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
||||
import {
|
||||
deletePeerApi,
|
||||
getPeerApi,
|
||||
getPeerClientConfigApi,
|
||||
kickPeerApi,
|
||||
pagePeerApi,
|
||||
releaseKickPeerApi,
|
||||
resetPeerTrafficApi,
|
||||
savePeerApi,
|
||||
updatePeerApi,
|
||||
} from "@/api/peer";
|
||||
@@ -93,6 +138,8 @@ const total = ref(0);
|
||||
const records = ref<PeerVo[]>([]);
|
||||
const qrDialog = ref(false);
|
||||
const qrSrc = ref("");
|
||||
const formRef = ref();
|
||||
const overview = reactive<{ visible: boolean; data: PeerVo | null }>({ visible: false, data: null });
|
||||
|
||||
const queryParams = reactive<PeerPageDto>({ pageNum: 1, pageSize: 10, name: undefined, remark: undefined, disabled: undefined });
|
||||
const dialog = reactive({ visible: false, title: "", editId: 0 });
|
||||
@@ -111,6 +158,25 @@ const disabledBool = computed({
|
||||
set: (v: boolean) => (dataForm.disabled = v ? 1 : 0),
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: t("common.required"), trigger: ["change", "blur"] }],
|
||||
};
|
||||
|
||||
function quotaText(v: number) {
|
||||
return v < 0 ? t("peer.unlimited") : formatBytes(v);
|
||||
}
|
||||
|
||||
function trafficPercent(row: PeerVo) {
|
||||
if (row.quotaBytes < 0 || row.quotaBytes === 0) return 0;
|
||||
const used = row.downloadBytes + row.uploadBytes;
|
||||
return Math.max(0, Math.min(100, Math.round((used / row.quotaBytes) * 100)));
|
||||
}
|
||||
|
||||
function openOverview(row: PeerVo) {
|
||||
overview.data = row;
|
||||
overview.visible = true;
|
||||
}
|
||||
|
||||
async function handleQuery() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -138,6 +204,13 @@ async function handleUpdate(row: PeerVo) {
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (formRef.value) {
|
||||
const ok = await formRef.value.validate().catch(() => false);
|
||||
if (!ok) return;
|
||||
}
|
||||
if (dialog.editId > 0 && dataForm.secret) {
|
||||
await ElMessageBox.confirm(t("peer.secretRotateConfirm"), t("common.warning"), { type: "warning" });
|
||||
}
|
||||
if (dialog.editId > 0) {
|
||||
const payload: PeerUpdateDto = { id: dialog.editId, name: dataForm.name, secret: dataForm.secret || undefined, quotaBytes: dataForm.quotaBytes, expiresAt: dataForm.expiresAt, maxDevices: dataForm.maxDevices, disabled: dataForm.disabled, remark: dataForm.remark };
|
||||
await updatePeerApi(payload);
|
||||
@@ -149,10 +222,27 @@ async function submitForm() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: PeerVo) {
|
||||
await ElMessageBox.confirm(t("common.deleteConfirm", { username: row.name }), t("common.warning"), { type: "warning" });
|
||||
await deletePeerApi({ id: row.id });
|
||||
await handleQuery();
|
||||
}
|
||||
|
||||
async function handleResetTraffic(row: PeerVo) {
|
||||
await ElMessageBox.confirm(t("common.resetTrafficConfirm"), t("common.warning"), { type: "warning" });
|
||||
await resetPeerTrafficApi({ id: row.id });
|
||||
await handleQuery();
|
||||
}
|
||||
|
||||
async function handleKick(row: PeerVo) {
|
||||
await kickPeerApi(row.id, { bannedUntil: Date.now() + 60 * 60 * 1000 });
|
||||
await handleQuery();
|
||||
}
|
||||
|
||||
async function handleReleaseKick(row: PeerVo) {
|
||||
await releaseKickPeerApi({ id: row.id });
|
||||
await handleQuery();
|
||||
}
|
||||
|
||||
async function copyUri(row: PeerVo) {
|
||||
const { data } = await getPeerClientConfigApi(row.id);
|
||||
copy(data.url);
|
||||
@@ -167,3 +257,8 @@ async function showQr(row: PeerVo) {
|
||||
onMounted(handleQuery);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.peer-title { font-weight: 600; }
|
||||
.peer-sub { color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user