Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+85
-163
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user