style: прогнать prettier по frontend

This commit is contained in:
2026-05-21 15:52:43 +05:00
parent b0cdf6207c
commit 95b8d97ccf
63 changed files with 1517 additions and 863 deletions
-2
View File
@@ -9,5 +9,3 @@ const appStore = useAppStore();
<router-view /> <router-view />
</el-config-provider> </el-config-provider>
</template> </template>
+3 -1
View File
@@ -23,7 +23,9 @@ export function getAdminInfoApi(): AxiosPromise<AdminInfo> {
}); });
} }
export function adminChangePasswordApi(data: AdminChangePasswordDto): AxiosPromise { export function adminChangePasswordApi(
data: AdminChangePasswordDto
): AxiosPromise {
return request({ return request({
url: "/admin/change-password", url: "/admin/change-password",
method: "post", method: "post",
-1
View File
@@ -24,4 +24,3 @@ export interface AdminChangePasswordDto {
export interface AdminSecurityVo { export interface AdminSecurityVo {
forcePasswordChange: boolean; forcePasswordChange: boolean;
} }
-2
View File
@@ -110,5 +110,3 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
}); });
} }
-2
View File
@@ -300,5 +300,3 @@ export interface Hysteria2AcmePathVo {
crtPath: string; crtPath: string;
keyPath: string; keyPath: string;
} }
+7 -3
View File
@@ -14,7 +14,9 @@ export function dashboardSummaryApi(): AxiosPromise<DashboardSummaryVo> {
}); });
} }
export function dashboardTimeseriesApi(range = "24h"): AxiosPromise<DashboardTimeseriesVo> { export function dashboardTimeseriesApi(
range = "24h"
): AxiosPromise<DashboardTimeseriesVo> {
return request({ return request({
url: "/dashboard/timeseries", url: "/dashboard/timeseries",
method: "get", method: "get",
@@ -22,7 +24,10 @@ export function dashboardTimeseriesApi(range = "24h"): AxiosPromise<DashboardTim
}); });
} }
export function dashboardTopPeersApi(range = "24h", limit = 10): AxiosPromise<DashboardTopPeerVo[]> { export function dashboardTopPeersApi(
range = "24h",
limit = 10
): AxiosPromise<DashboardTopPeerVo[]> {
return request({ return request({
url: "/dashboard/top-peers", url: "/dashboard/top-peers",
method: "get", method: "get",
@@ -36,4 +41,3 @@ export function dashboardSecurityApi(): AxiosPromise<SecurityRiskVo[]> {
method: "get", method: "get",
}); });
} }
-1
View File
@@ -69,4 +69,3 @@ export interface DashboardTopPeerVo {
upload: number; upload: number;
total: number; total: number;
} }
-2
View File
@@ -33,5 +33,3 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
responseType: "blob", responseType: "blob",
}); });
} }
-2
View File
@@ -22,5 +22,3 @@ export interface LogHysteria2Vo {
msg: string; msg: string;
time: string; time: string;
} }
-1
View File
@@ -96,4 +96,3 @@ export function exportPeerApi(): AxiosPromise {
responseType: "blob", responseType: "blob",
}); });
} }
@@ -58,5 +58,3 @@ function toggleClick() {
} }
} }
</style> </style>
@@ -75,5 +75,3 @@ const handleClose = (tag: string): void => {
flex-wrap: wrap; flex-wrap: wrap;
} }
</style> </style>
@@ -34,5 +34,3 @@ function handleLanguageChange(lang: string) {
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
@@ -180,5 +180,3 @@ const closeDialog = (): void => {
flex-wrap: wrap; flex-wrap: wrap;
} }
</style> </style>
@@ -86,5 +86,3 @@ function handleCurrentChange(val: number) {
} }
} }
</style> </style>
@@ -41,5 +41,3 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
fill: currentcolor; /* currentColor наследует текущий цвет элемента или родителя */ fill: currentcolor; /* currentColor наследует текущий цвет элемента или родителя */
} }
</style> </style>
@@ -70,7 +70,3 @@ watch(
{ immediate: true } { immediate: true }
); );
</script> </script>
-2
View File
@@ -7,5 +7,3 @@ export function setupDirective(app: App<Element>) {
// Делает v-hasRole доступным во всех компонентах // Делает v-hasRole доступным во всех компонентах
app.directive("hasRole", hasRole); app.directive("hasRole", hasRole);
} }
@@ -23,5 +23,3 @@ export const hasRole: Directive = {
} }
}, },
}; };
+3 -3
View File
@@ -7,7 +7,9 @@ function initialLocale(): string {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return defaultSettings.language || "ru"; return defaultSettings.language || "ru";
} }
return window.localStorage.getItem("language") || defaultSettings.language || "ru"; return (
window.localStorage.getItem("language") || defaultSettings.language || "ru"
);
} }
const messages = { const messages = {
@@ -28,5 +30,3 @@ const i18n = createI18n({
}); });
export default i18n; export default i18n;
+2 -3
View File
@@ -139,7 +139,8 @@ export default {
clientUri: "Client URI", clientUri: "Client URI",
clientQr: "Client QR", clientQr: "Client QR",
unlimited: "Unlimited", unlimited: "Unlimited",
secretRotateConfirm: "Rotate peer secret? Existing client configurations will stop working until updated.", secretRotateConfirm:
"Rotate peer secret? Existing client configurations will stop working until updated.",
quota: "Quota", quota: "Quota",
download: "Download", download: "Download",
upload: "Upload", upload: "Upload",
@@ -406,5 +407,3 @@ export default {
}, },
}, },
}; };
+20 -8
View File
@@ -23,7 +23,8 @@ export default {
passwordFormatIncorrect: "Неверный формат пароля", passwordFormatIncorrect: "Неверный формат пароля",
}, },
dashboard: { dashboard: {
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...", stale:
"Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
topPeers24h: "Топ пиров (24ч)", topPeers24h: "Топ пиров (24ч)",
topPeersRange: "Топ пиров ({range})", topPeersRange: "Топ пиров ({range})",
refreshFailed: "Не удалось обновить данные дашборда", refreshFailed: "Не удалось обновить данные дашборда",
@@ -134,7 +135,8 @@ export default {
clientUri: "URI клиента", clientUri: "URI клиента",
clientQr: "QR клиента", clientQr: "QR клиента",
unlimited: "Безлимит", unlimited: "Безлимит",
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.", secretRotateConfirm:
"Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
quota: "Квота", quota: "Квота",
download: "Скачано", download: "Скачано",
upload: "Отдано", upload: "Отдано",
@@ -181,7 +183,8 @@ export default {
mustBeInteger: "Поле должно быть целым числом", mustBeInteger: "Поле должно быть целым числом",
invalidWebContext: invalidWebContext:
"Поле должно начинаться с / и содержать только строчные буквы (a-z) и цифры (0-9)", "Поле должно начинаться с / и содержать только строчные буквы (a-z) и цифры (0-9)",
invalidTrafficTime: "Поле должно быть числом максимум с одним знаком после запятой", invalidTrafficTime:
"Поле должно быть числом максимум с одним знаком после запятой",
}, },
monitor: { monitor: {
huiVersion: "Версия HY2XS admin", huiVersion: "Версия HY2XS admin",
@@ -313,8 +316,15 @@ export default {
outbounds: { outbounds: {
name: "Имя outbound", name: "Имя outbound",
type: "Тип", type: "Тип",
socks5: { addr: "Адрес SOCKS5", username: "Логин SOCKS5", password: "Пароль SOCKS5" }, socks5: {
http: { url: "URL HTTP/HTTPS proxy", insecure: "Отключить TLS-проверку proxy" }, addr: "Адрес SOCKS5",
username: "Логин SOCKS5",
password: "Пароль SOCKS5",
},
http: {
url: "URL HTTP/HTTPS proxy",
insecure: "Отключить TLS-проверку proxy",
},
direct: { direct: {
mode: "Тип", mode: "Тип",
bindIPv4: "Локальный IPv4", bindIPv4: "Локальный IPv4",
@@ -332,7 +342,11 @@ export default {
rewriteHost: "Переписывать Host header", rewriteHost: "Переписывать Host header",
insecure: "Отключить TLS-проверку", insecure: "Отключить TLS-проверку",
}, },
string: { content: "Ответ строкой", headers: "HTTP headers", statusCode: "HTTP status code" }, string: {
content: "Ответ строкой",
headers: "HTTP headers",
statusCode: "HTTP status code",
},
listenHTTP: "HTTP listen address", listenHTTP: "HTTP listen address",
listenHTTPS: "HTTPS listen address", listenHTTPS: "HTTPS listen address",
forceHTTPS: "Принудительно использовать HTTPS", forceHTTPS: "Принудительно использовать HTTPS",
@@ -340,5 +354,3 @@ export default {
}, },
}, },
}; };
@@ -17,5 +17,3 @@
background-color: var(--el-bg-color-page); background-color: var(--el-bg-color-page);
} }
</style> </style>
@@ -35,9 +35,7 @@ function logout() {
cancelButtonText: t("common.cancel"), cancelButtonText: t("common.cancel"),
type: "warning", type: "warning",
}).then(() => { }).then(() => {
adminStore adminStore.logout().then(() => {
.logout()
.then(() => {
router.push(`/login?redirect=${route.fullPath}`); router.push(`/login?redirect=${route.fullPath}`);
}); });
}); });
@@ -35,5 +35,3 @@ function push() {
<slot /> <slot />
</div> </div>
</template> </template>
@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps({ defineProps({
collapse: { collapse: {
type: Boolean, type: Boolean,
@@ -22,12 +21,7 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
<img :src="logo" class="sidebar-logo-image" /> <img :src="logo" class="sidebar-logo-image" />
</router-link> </router-link>
<router-link <router-link v-else key="expand" class="sidebar-logo-link" to="/">
v-else
key="expand"
class="sidebar-logo-link"
to="/"
>
<img :src="logo" class="sidebar-logo-image" /> <img :src="logo" class="sidebar-logo-image" />
<span class="sidebar-logo-title">HY2XS</span> <span class="sidebar-logo-title">HY2XS</span>
</router-link> </router-link>
@@ -73,5 +67,3 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
opacity: 0; opacity: 0;
} }
</style> </style>
@@ -117,7 +117,9 @@ function resolvePath(routePath: string) {
:content="translateRouteTitleI18n(item.meta.title)" :content="translateRouteTitleI18n(item.meta.title)"
placement="right" placement="right"
> >
<span class="menu-title">{{ translateRouteTitleI18n(item.meta.title) }}</span> <span class="menu-title">{{
translateRouteTitleI18n(item.meta.title)
}}</span>
</el-tooltip> </el-tooltip>
</template> </template>
@@ -139,4 +141,3 @@ function resolvePath(routePath: string) {
white-space: nowrap; white-space: nowrap;
} }
</style> </style>
@@ -38,4 +38,3 @@ const route = useRoute();
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
@@ -1,4 +1,2 @@
export { default as Navbar } from "./Navbar.vue"; export { default as Navbar } from "./Navbar.vue";
export { default as AppMain } from "./AppMain.vue"; export { default as AppMain } from "./AppMain.vue";
-1
View File
@@ -97,4 +97,3 @@ function handleOutsideClick() {
opacity: 0.3; opacity: 0.3;
} }
</style> </style>
-2
View File
@@ -23,5 +23,3 @@ setupDirective(app);
setupStore(app); setupStore(app);
app.use(router).use(i18n).mount("#app"); app.use(router).use(i18n).mount("#app");
+15 -4
View File
@@ -22,8 +22,14 @@ router.beforeEach(async (to, from, next) => {
const adminStore = useAdminStoreHook(); const adminStore = useAdminStoreHook();
const hasRoles = adminStore.roles && adminStore.roles.length > 0; const hasRoles = adminStore.roles && adminStore.roles.length > 0;
if (hasRoles) { if (hasRoles) {
if (adminStore.forcePasswordChange && to.path !== "/admin/change-password") { if (
next({ path: "/admin/change-password", query: { redirect: to.fullPath } }); adminStore.forcePasswordChange &&
to.path !== "/admin/change-password"
) {
next({
path: "/admin/change-password",
query: { redirect: to.fullPath },
});
return; return;
} }
// Если маршрут не найден, перейти на 404 // Если маршрут не найден, перейти на 404
@@ -34,14 +40,19 @@ router.beforeEach(async (to, from, next) => {
} }
} else { } else {
try { try {
const { roles, forcePasswordChange } = await adminStore.getAdminInfo(); const { roles, forcePasswordChange } =
await adminStore.getAdminInfo();
const permissionStore = usePermissionStoreHook(); const permissionStore = usePermissionStoreHook();
const accessRoutes = permissionStore.generateRoutes(roles); const accessRoutes = permissionStore.generateRoutes(roles);
accessRoutes.forEach((route) => { accessRoutes.forEach((route) => {
router.addRoute(route); router.addRoute(route);
}); });
if (forcePasswordChange && to.path !== "/admin/change-password") { if (forcePasswordChange && to.path !== "/admin/change-password") {
next({ path: "/admin/change-password", query: { redirect: to.fullPath }, replace: true }); next({
path: "/admin/change-password",
query: { redirect: to.fullPath },
replace: true,
});
return; return;
} }
next({ ...to, replace: true }); next({ ...to, replace: true });
-1
View File
@@ -7,4 +7,3 @@ const defaultSettings: DefaultSettings = {
}; };
export default defaultSettings; export default defaultSettings;
-2
View File
@@ -9,5 +9,3 @@ export function setupStore(app: App<Element>) {
} }
export { store }; export { store };
+5 -2
View File
@@ -19,7 +19,11 @@ export const useAdminStore = defineStore("admin", () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
loginApi(adminLoginDto) loginApi(adminLoginDto)
.then((response) => { .then((response) => {
const { tokenType, accessToken, forcePasswordChange: mustChange } = response.data; const {
tokenType,
accessToken,
forcePasswordChange: mustChange,
} = response.data;
token.value = tokenType + " " + accessToken; token.value = tokenType + " " + accessToken;
forcePasswordChange.value = !!mustChange; forcePasswordChange.value = !!mustChange;
resolve(); resolve();
@@ -84,4 +88,3 @@ export const useAdminStore = defineStore("admin", () => {
export function useAdminStoreHook() { export function useAdminStoreHook() {
return useAdminStore(store); return useAdminStore(store);
} }
-2
View File
@@ -72,5 +72,3 @@ export const useAppStore = defineStore("app", () => {
openSideBar, openSideBar,
}; };
}); });
@@ -95,5 +95,3 @@ export const usePermissionStore = defineStore("permission", () => {
export function usePermissionStoreHook() { export function usePermissionStoreHook() {
return usePermissionStore(store); return usePermissionStore(store);
} }
@@ -40,5 +40,3 @@
.el-table__header col[name="gutter"] { .el-table__header col[name="gutter"] {
display: table-cell !important; display: table-cell !important;
} }
-2
View File
@@ -73,5 +73,3 @@ a:active,
div:focus { div:focus {
outline: none; outline: none;
} }
@@ -4,5 +4,3 @@
menuText: $menuText; menuText: $menuText;
menuActiveText: $menuActiveText; menuActiveText: $menuActiveText;
} }
-1
View File
@@ -32,4 +32,3 @@ $menuActiveBorder: var(--menuActiveBorder);
$sideBarWidth: 210px; $sideBarWidth: 210px;
$sideBarCollapsedWidth: 54px; $sideBarCollapsedWidth: 54px;
File diff suppressed because it is too large Load Diff
+55 -55
View File
@@ -1,65 +1,65 @@
// generated by unplugin-vue-components // generated by unplugin-vue-components
// We suggest you to commit this file into source control // We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core' import "@vue/runtime-core";
export {} export {};
declare module '@vue/runtime-core' { declare module "@vue/runtime-core" {
export interface GlobalComponents { export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import("element-plus/es")["ElAlert"];
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import("element-plus/es")["ElAvatar"];
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import("element-plus/es")["ElButton"];
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import("element-plus/es")["ElCard"];
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import("element-plus/es")["ElCol"];
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import("element-plus/es")["ElDatePicker"];
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import("element-plus/es")["ElDialog"];
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import("element-plus/es")["ElDrawer"];
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import("element-plus/es")["ElDropdown"];
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import("element-plus/es")["ElDropdownItem"];
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import("element-plus/es")["ElDropdownMenu"];
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import("element-plus/es")["ElEmpty"];
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import("element-plus/es")["ElForm"];
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import("element-plus/es")["ElFormItem"];
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import("element-plus/es")["ElInput"];
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import("element-plus/es")["ElInputNumber"];
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import("element-plus/es")["ElMenu"];
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import("element-plus/es")["ElMenuItem"];
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import("element-plus/es")["ElOption"];
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import("element-plus/es")["ElPagination"];
ElProgress: typeof import('element-plus/es')['ElProgress'] ElProgress: typeof import("element-plus/es")["ElProgress"];
ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioButton: typeof import("element-plus/es")["ElRadioButton"];
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import("element-plus/es")["ElRadioGroup"];
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import("element-plus/es")["ElRow"];
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import("element-plus/es")["ElScrollbar"];
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import("element-plus/es")["ElSelect"];
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import("element-plus/es")["ElSubMenu"];
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import("element-plus/es")["ElSwitch"];
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import("element-plus/es")["ElTable"];
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import("element-plus/es")["ElTableColumn"];
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import("element-plus/es")["ElTabPane"];
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import("element-plus/es")["ElTabs"];
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import("element-plus/es")["ElTag"];
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import("element-plus/es")["ElTooltip"];
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import("element-plus/es")["ElUpload"];
Hamburger: typeof import('./../components/Hamburger/index.vue')['default'] Hamburger: typeof import("./../components/Hamburger/index.vue")["default"];
IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default'] IEpCaretBottom: typeof import("~icons/ep/caret-bottom")["default"];
IEpDownload: typeof import('~icons/ep/download')['default'] IEpDownload: typeof import("~icons/ep/download")["default"];
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default'] IEpMoreFilled: typeof import("~icons/ep/more-filled")["default"];
IEpRefresh: typeof import('~icons/ep/refresh')['default'] IEpRefresh: typeof import("~icons/ep/refresh")["default"];
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default'] IEpRefreshRight: typeof import("~icons/ep/refresh-right")["default"];
IEpUpload: typeof import('~icons/ep/upload')['default'] IEpUpload: typeof import("~icons/ep/upload")["default"];
IEpUserFilled: typeof import('~icons/ep/user-filled')['default'] IEpUserFilled: typeof import("~icons/ep/user-filled")["default"];
ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default'] ImputMultiple: typeof import("./../components/ImputMultiple/index.vue")["default"];
LangSelect: typeof import('./../components/LangSelect/index.vue')['default'] LangSelect: typeof import("./../components/LangSelect/index.vue")["default"];
MapAdd: typeof import('./../components/MapAdd/index.vue')['default'] MapAdd: typeof import("./../components/MapAdd/index.vue")["default"];
Pagination: typeof import('./../components/Pagination/index.vue')['default'] Pagination: typeof import("./../components/Pagination/index.vue")["default"];
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import("vue-router")["RouterLink"];
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import("vue-router")["RouterView"];
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] SvgIcon: typeof import("./../components/SvgIcon/index.vue")["default"];
UnitSelect: typeof import('./../components/UnitSelect/index.vue')['default'] UnitSelect: typeof import("./../components/UnitSelect/index.vue")["default"];
} }
export interface ComponentCustomProperties { export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import("element-plus/es")["ElLoadingDirective"];
} }
} }
-2
View File
@@ -13,5 +13,3 @@ type ImportMetaEnv = Record<string, string | boolean | undefined>;
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }
-2
View File
@@ -16,5 +16,3 @@ declare global {
} }
} }
export {}; export {};
-2
View File
@@ -93,5 +93,3 @@ export const formatStorageUnit = (bytes: number): string => {
// Возврат отформатированной единицы хранения // Возврат отформатированной единицы хранения
return sizes[i]; return sizes[i];
}; };
-2
View File
@@ -49,5 +49,3 @@ export const deepCopy = <T>(source: Partial<T>): T => {
return objCopy as T; return objCopy as T;
}; };
-2
View File
@@ -10,5 +10,3 @@ export function translateRouteTitleI18n(title: any) {
} }
return title; return title;
} }
-2
View File
@@ -37,5 +37,3 @@ export function isExternal(path: string) {
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
return isExternal; return isExternal;
} }
-2
View File
@@ -67,5 +67,3 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
}; };
animateScroll(); animateScroll();
}; };
-2
View File
@@ -99,5 +99,3 @@ export const getYearLater = (): number => {
date.setFullYear(date.getFullYear() + 1); date.setFullYear(date.getFullYear() + 1);
return date.getTime(); return date.getTime();
}; };
@@ -18,7 +18,9 @@
<el-input v-model="form.newPassword" show-password /> <el-input v-model="form.newPassword" show-password />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="saving" @click="submit">{{ $t("common.confirm") }}</el-button> <el-button type="primary" :loading="saving" @click="submit">{{
$t("common.confirm")
}}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@@ -47,12 +49,28 @@ const form = reactive({
const passwordPattern = /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,64}$/; const passwordPattern = /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,64}$/;
const rules: FormRules = { const rules: FormRules = {
oldPassword: [ oldPassword: [
{ required: true, message: t("common.required"), trigger: ["change", "blur"] }, {
{ pattern: passwordPattern, message: t("common.invalid"), trigger: ["change", "blur"] }, required: true,
message: t("common.required"),
trigger: ["change", "blur"],
},
{
pattern: passwordPattern,
message: t("common.invalid"),
trigger: ["change", "blur"],
},
], ],
newPassword: [ newPassword: [
{ required: true, message: t("common.required"), trigger: ["change", "blur"] }, {
{ pattern: passwordPattern, message: t("common.invalid"), trigger: ["change", "blur"] }, required: true,
message: t("common.required"),
trigger: ["change", "blur"],
},
{
pattern: passwordPattern,
message: t("common.invalid"),
trigger: ["change", "blur"],
},
], ],
}; };
@@ -66,10 +84,14 @@ const submit = async () => {
} }
saving.value = true; saving.value = true;
try { try {
await adminChangePasswordApi({ oldPassword: form.oldPassword, newPassword: form.newPassword }); await adminChangePasswordApi({
oldPassword: form.oldPassword,
newPassword: form.newPassword,
});
adminStore.forcePasswordChange = false; adminStore.forcePasswordChange = false;
ElMessage.success(t("admin.changedSuccess")); ElMessage.success(t("admin.changedSuccess"));
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/"; const redirect =
typeof route.query.redirect === "string" ? route.query.redirect : "/";
router.push(redirect); router.push(redirect);
} finally { } finally {
saving.value = false; saving.value = false;
@@ -82,4 +104,3 @@ const submit = async () => {
max-width: 760px; max-width: 760px;
} }
</style> </style>
@@ -84,7 +84,12 @@
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('config.huiHttps')" prop="huiHttps"> <el-form-item :label="$t('config.huiHttps')" prop="huiHttps">
<el-select v-model="huiHttps" style="width: 50%" ref="huiHttpsRef" disabled> <el-select
v-model="huiHttps"
style="width: 50%"
ref="huiHttpsRef"
disabled
>
<el-option <el-option
v-for="item in huiHttpsList" v-for="item in huiHttpsList"
:key="item.key" :key="item.key"
@@ -165,11 +170,7 @@ import {
updateConfigsApi, updateConfigsApi,
} from "@/api/config"; } from "@/api/config";
import { ConfigsUpdateDto } from "@/api/config/types"; import { ConfigsUpdateDto } from "@/api/config/types";
import { import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
UploadFile,
UploadRawFile,
UploadRequestOptions,
} from "element-plus";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
+136 -26
View File
@@ -1,7 +1,9 @@
<template> <template>
<div class="dashboard-container"> <div class="dashboard-container">
<div class="dashboard-actions mb-2"> <div class="dashboard-actions mb-2">
<el-button size="small" @click="loadDashboard">{{ $t("common.refresh") }}</el-button> <el-button size="small" @click="loadDashboard">{{
$t("common.refresh")
}}</el-button>
</div> </div>
<el-alert <el-alert
@@ -23,20 +25,63 @@
v-for="risk in securityRisks" v-for="risk in securityRisks"
:key="risk.key" :key="risk.key"
:title="$t(risk.key)" :title="$t(risk.key)"
:type="risk.severity === 'critical' ? 'error' : risk.severity === 'warning' ? 'warning' : 'info'" :type="
risk.severity === 'critical'
? 'error'
: risk.severity === 'warning'
? 'warning'
: 'info'
"
:closable="risk.dismissible" :closable="risk.dismissible"
class="mb-2" class="mb-2"
/> />
<el-row :gutter="10" class="mt-2"> <el-row :gutter="10" class="mt-2">
<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-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.ram") }}: {{ summary.system.memPercent }}%</el-card></el-col> ><el-card shadow="never"
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.disk") }}: {{ summary.system.diskPercent }}%</el-card></el-col> >{{ $t("dashboard.cpu") }}: {{ summary.system.cpuPercent }}%</el-card
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.peers") }}: {{ summary.peers.total }}</el-card></el-col> ></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-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayDownload") }}: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col> ><el-card shadow="never"
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayUpload") }}: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col> >{{ $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-row>
<el-card shadow="never" class="mt-3"> <el-card shadow="never" class="mt-3">
@@ -62,17 +107,25 @@
</el-card> </el-card>
<el-card shadow="never" class="mt-3"> <el-card shadow="never" class="mt-3">
<template #header>{{ $t("dashboard.topPeersRange", { range }) }}</template> <template #header>{{
$t("dashboard.topPeersRange", { range })
}}</template>
<el-table :data="topPeers" size="small"> <el-table :data="topPeers" size="small">
<el-table-column prop="name" :label="$t('peer.name')" /> <el-table-column prop="name" :label="$t('peer.name')" />
<el-table-column prop="download" :label="$t('dashboard.download')"> <el-table-column prop="download" :label="$t('dashboard.download')">
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template> <template #default="scope">{{
formatBytes(scope.row.download || 0)
}}</template>
</el-table-column> </el-table-column>
<el-table-column prop="upload" :label="$t('dashboard.upload')"> <el-table-column prop="upload" :label="$t('dashboard.upload')">
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template> <template #default="scope">{{
formatBytes(scope.row.upload || 0)
}}</template>
</el-table-column> </el-table-column>
<el-table-column prop="total" :label="$t('dashboard.total')"> <el-table-column prop="total" :label="$t('dashboard.total')">
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template> <template #default="scope">{{
formatBytes(scope.row.total || 0)
}}</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
@@ -93,20 +146,61 @@ import {
TitleComponent, TitleComponent,
} from "echarts/components"; } from "echarts/components";
import VChart from "vue-echarts"; import VChart from "vue-echarts";
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard"; import {
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types"; dashboardSecurityApi,
dashboardSummaryApi,
dashboardTimeseriesApi,
dashboardTopPeersApi,
} from "@/api/dashboard";
import {
DashboardSummaryVo,
DashboardTimeseriesVo,
DashboardTopPeerVo,
SecurityRiskVo,
} from "@/api/dashboard/types";
import { formatBytes } from "@/utils/byte"; import { formatBytes } from "@/utils/byte";
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, TitleComponent]); use([
CanvasRenderer,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
TitleComponent,
]);
const { t } = useI18n(); const { t } = useI18n();
const summary = ref<DashboardSummaryVo>({ const summary = ref<DashboardSummaryVo>({
collectedAt: 0, collectedAt: 0,
system: { cpuPercent: 0, memUsedBytes: 0, memTotalBytes: 0, memPercent: 0, diskUsedBytes: 0, diskTotalBytes: 0, diskPercent: 0 }, system: {
cpuPercent: 0,
memUsedBytes: 0,
memTotalBytes: 0,
memPercent: 0,
diskUsedBytes: 0,
diskTotalBytes: 0,
diskPercent: 0,
},
hysteria: { version: "-", running: false, apiReachable: false }, hysteria: { version: "-", running: false, apiReachable: false },
peers: { total: 0, enabled: 0, disabled: 0, expired: 0, onlinePeers: 0, onlineDevices: 0 }, peers: {
traffic: { downloadBytes: 0, uploadBytes: 0, totalBytes: 0, todayDownloadBytes: 0, todayUploadBytes: 0, sinceResetDownloadBytes: 0, sinceResetUploadBytes: 0 }, 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: { health: {
collector: { status: "stale" }, collector: { status: "stale" },
hysteria: { status: "ok" }, hysteria: { status: "ok" },
@@ -115,7 +209,12 @@ const summary = ref<DashboardSummaryVo>({
}); });
const topPeers = ref<DashboardTopPeerVo[]>([]); const topPeers = ref<DashboardTopPeerVo[]>([]);
const securityRisks = ref<SecurityRiskVo[]>([]); const securityRisks = ref<SecurityRiskVo[]>([]);
const timeseries = ref<DashboardTimeseriesVo>({ range: "24h", traffic: [], system: [], collectedAt: 0 }); const timeseries = ref<DashboardTimeseriesVo>({
range: "24h",
traffic: [],
system: [],
collectedAt: 0,
});
const timeseriesLoading = ref(false); const timeseriesLoading = ref(false);
const range = ref("24h"); const range = ref("24h");
const loadError = ref(""); const loadError = ref("");
@@ -172,10 +271,14 @@ const trafficUploadData = computed(() =>
); );
const hasTrafficData = computed(() => const hasTrafficData = computed(() =>
(timeseries.value.traffic || []).some((item) => (item.download || 0) > 0 || (item.upload || 0) > 0) (timeseries.value.traffic || []).some(
(item) => (item.download || 0) > 0 || (item.upload || 0) > 0
)
); );
const hasSingleTrafficPoint = computed(() => (timeseries.value.traffic || []).length === 1); const hasSingleTrafficPoint = computed(
() => (timeseries.value.traffic || []).length === 1
);
const systemCpuData = computed(() => const systemCpuData = computed(() =>
(timeseries.value.system || []).map((item) => [item.ts, item.cpu || 0]) (timeseries.value.system || []).map((item) => [item.ts, item.cpu || 0])
@@ -186,7 +289,11 @@ const systemMemData = computed(() =>
); );
const trafficChartOption = computed(() => ({ const trafficChartOption = computed(() => ({
title: { text: t("dashboard.trafficSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } }, title: {
text: t("dashboard.trafficSeriesChart"),
left: "left",
textStyle: { fontSize: 14, fontWeight: 600 },
},
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
valueFormatter: (value: number) => formatBytes(value || 0), valueFormatter: (value: number) => formatBytes(value || 0),
@@ -223,7 +330,11 @@ const trafficChartOption = computed(() => ({
})); }));
const systemChartOption = computed(() => ({ const systemChartOption = computed(() => ({
title: { text: t("dashboard.systemSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } }, title: {
text: t("dashboard.systemSeriesChart"),
left: "left",
textStyle: { fontSize: 14, fontWeight: 600 },
},
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
valueFormatter: (value: number) => `${Math.round((value || 0) * 10) / 10}%`, valueFormatter: (value: number) => `${Math.round((value || 0) * 10) / 10}%`,
@@ -319,4 +430,3 @@ onUnmounted(() => {
height: 340px; height: 340px;
} }
</style> </style>
+3 -3
View File
@@ -27,7 +27,9 @@ function back() {
<template> <template>
<div class="errPage-container"> <div class="errPage-container">
<el-button class="pan-back-btn" @click="back">{{ $t("errorPage.back") }}</el-button> <el-button class="pan-back-btn" @click="back">{{
$t("errorPage.back")
}}</el-button>
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1> <h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1>
@@ -98,5 +100,3 @@ function back() {
} }
} }
</style> </style>
+3 -3
View File
@@ -46,7 +46,9 @@ function message() {
<div class="bullshit__info"> <div class="bullshit__info">
{{ $t("errorPage.notFoundInfo") }} {{ $t("errorPage.notFoundInfo") }}
</div> </div>
<a href="/#/" class="bullshit__return-home">{{ $t("errorPage.notFoundBackHome") }}</a> <a href="/#/" class="bullshit__return-home">{{
$t("errorPage.notFoundBackHome")
}}</a>
</div> </div>
</div> </div>
</div> </div>
@@ -272,5 +274,3 @@ function message() {
} }
} }
</style> </style>
@@ -380,5 +380,3 @@ const closeDialog = (): void => {
flex-wrap: wrap; flex-wrap: wrap;
} }
</style> </style>
@@ -50,10 +50,7 @@
:model="dataForm" :model="dataForm"
:disabled="true" :disabled="true"
> >
<el-tabs <el-tabs v-model="activeName" class="tabs">
v-model="activeName"
class="tabs"
>
<el-tab-pane :label="$t('hysteria.extension')" name="extension"> <el-tab-pane :label="$t('hysteria.extension')" name="extension">
<el-tooltip <el-tooltip
:content="$t('hysteria.config.remark')" :content="$t('hysteria.config.remark')"
@@ -107,5 +107,3 @@ onMounted(() => {
setRecords(); setRecords();
}); });
</script> </script>
@@ -107,4 +107,3 @@ onMounted(() => {
setRecords(); setRecords();
}); });
</script> </script>
+297 -62
View File
@@ -8,13 +8,17 @@
<el-input v-model="queryParams.remark" clearable style="width: 220px" /> <el-input v-model="queryParams.remark" clearable style="width: 220px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleQuery">{{ $t("common.search") }}</el-button> <el-button type="primary" @click="handleQuery">{{
$t("common.search")
}}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-card shadow="never"> <el-card shadow="never">
<div class="mb-3"> <div class="mb-3">
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button> <el-button type="primary" @click="handleAdd">{{
$t("common.add")
}}</el-button>
<el-upload <el-upload
v-model:file-list="importFileList" v-model:file-list="importFileList"
:http-request="handleImport" :http-request="handleImport"
@@ -26,7 +30,9 @@
> >
<el-button>{{ $t("common.import") }}</el-button> <el-button>{{ $t("common.import") }}</el-button>
</el-upload> </el-upload>
<el-button class="ml-2" @click="handleExport">{{ $t("common.export") }}</el-button> <el-button class="ml-2" @click="handleExport">{{
$t("common.export")
}}</el-button>
</div> </div>
<el-table v-loading="loading" :data="records"> <el-table v-loading="loading" :data="records">
<el-table-column :label="$t('peer.name')" min-width="220"> <el-table-column :label="$t('peer.name')" min-width="220">
@@ -37,42 +43,107 @@
</el-table-column> </el-table-column>
<el-table-column :label="$t('peer.status')" min-width="180"> <el-table-column :label="$t('peer.status')" min-width="180">
<template #default="scope"> <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
<el-tag size="small" class="ml-1" :type="scope.row.online ? 'success' : 'info'">{{ scope.row.online ? $t('peer.online') : $t('peer.offline') }}</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> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('peer.traffic')" min-width="260"> <el-table-column :label="$t('peer.traffic')" min-width="260">
<template #default="scope"> <template #default="scope">
<div>{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }} / {{ quotaText(scope.row.quotaBytes) }}</div> <div>
<el-progress :percentage="trafficPercent(scope.row)" :status="scope.row.quotaBytes < 0 ? undefined : (trafficPercent(scope.row) >= 90 ? 'exception' : undefined)" :show-text="false" /> {{
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> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('peer.devices')" min-width="120"> <el-table-column :label="$t('peer.devices')" min-width="120">
<template #default="scope">{{ scope.row.onlineDevices }} / {{ scope.row.maxDevices }}</template> <template #default="scope"
>{{ scope.row.onlineDevices }} /
{{ scope.row.maxDevices }}</template
>
</el-table-column> </el-table-column>
<el-table-column :label="$t('peer.expireTime')" min-width="170"> <el-table-column :label="$t('peer.expireTime')" min-width="170">
<template #default="scope">{{ scope.row.expiresAt === 0 ? $t('peer.unlimited') : 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>
<el-table-column :label="$t('peer.lastConnectionAt')" min-width="170"> <el-table-column :label="$t('peer.lastConnectionAt')" min-width="170">
<template #default="scope">{{ scope.row.lastConnectionAt ? timestampToDateTime(scope.row.lastConnectionAt) : '-' }}</template> <template #default="scope">{{
scope.row.lastConnectionAt
? timestampToDateTime(scope.row.lastConnectionAt)
: "-"
}}</template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('common.operate')" width="230" fixed="right" align="right" header-align="right"> <el-table-column
:label="$t('common.operate')"
width="230"
fixed="right"
align="right"
header-align="right"
>
<template #default="scope"> <template #default="scope">
<div class="peer-actions"> <div class="peer-actions">
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button> <el-button link type="primary" @click="openOverview(scope.row)">{{
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.copyUri") }}</el-button> $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-dropdown trigger="click" placement="bottom-end">
<el-button text circle class="peer-more-btn" @click.stop> <el-button text circle class="peer-more-btn" @click.stop>
<i-ep-more-filled /> <i-ep-more-filled />
</el-button> </el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-dropdown-item> <el-dropdown-item @click="showQr(scope.row)">{{
<el-dropdown-item @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-dropdown-item> $t("peer.clientQr")
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{ $t("common.resetTraffic") }}</el-dropdown-item> }}</el-dropdown-item>
<el-dropdown-item @click="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item> <el-dropdown-item @click="handleUpdate(scope.row)">{{
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{ $t("peer.releaseKick") }}</el-dropdown-item> $t("common.edit")
<el-dropdown-item divided @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-dropdown-item> }}</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> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -90,17 +161,41 @@
</el-card> </el-card>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="620px"> <el-dialog v-model="dialog.visible" :title="dialog.title" width="620px">
<el-form ref="formRef" :model="dataForm" :rules="rules" label-width="140px"> <el-form
<el-form-item :label="$t('peer.name')" prop="name"><el-input v-model="dataForm.name" /></el-form-item> ref="formRef"
<el-form-item :label="$t('peer.remark')"><el-input v-model="dataForm.remark" /></el-form-item> :model="dataForm"
<el-form-item :label="$t('peer.secret')" prop="secret"><el-input v-model="dataForm.secret" show-password /></el-form-item> :rules="rules"
<el-form-item :label="$t('peer.quota')"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item> label-width="140px"
<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.name')" prop="name"
<el-form-item :label="$t('peer.disabled')"><el-switch v-model="disabledBool" /></el-form-item> ><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> </el-form>
<template #footer> <template #footer>
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button> <el-button type="primary" @click="submitForm">{{
$t("common.confirm")
}}</el-button>
</template> </template>
</el-dialog> </el-dialog>
@@ -110,32 +205,104 @@
</div> </div>
</el-dialog> </el-dialog>
<el-drawer v-model="overview.visible" :title="$t('peer.overview')" size="45%"> <el-drawer
v-model="overview.visible"
:title="$t('peer.overview')"
size="45%"
>
<div v-if="overview.data"> <div v-if="overview.data">
<p><b>{{ $t('peer.authId') }}:</b> {{ overview.data.authId }}</p> <p>
<p><b>{{ $t('peer.name') }}:</b> {{ overview.data.name }}</p> <b>{{ $t("peer.authId") }}:</b> {{ overview.data.authId }}
<p><b>{{ $t('peer.remark') }}:</b> {{ overview.data.remark || '-' }}</p> </p>
<p><b>{{ $t('peer.createdAt') }}:</b> {{ overview.data.createTime || '-' }}</p> <p>
<p><b>{{ $t('peer.status') }}:</b> {{ overview.data.disabled === 1 ? $t('common.disable') : $t('common.enable') }}</p> <b>{{ $t("peer.name") }}:</b> {{ overview.data.name }}
<p><b>{{ $t('peer.bannedUntil') }}:</b> {{ overview.data.bannedUntil > Date.now() ? timestampToDateTime(overview.data.bannedUntil) : '-' }}</p> </p>
<p><b>{{ $t('peer.totalTraffic') }}:</b> {{ formatBytes((overview.data.downloadBytes || 0) + (overview.data.uploadBytes || 0)) }}</p> <p>
<p><b>{{ $t('peer.quota') }}:</b> {{ quotaText(overview.data.quotaBytes) }}</p> <b>{{ $t("peer.remark") }}:</b> {{ overview.data.remark || "-" }}
<p><b>{{ $t('peer.download') }}:</b> {{ formatBytes(overview.data.downloadBytes) }}</p> </p>
<p><b>{{ $t('peer.upload') }}:</b> {{ formatBytes(overview.data.uploadBytes) }}</p> <p>
<p><b>{{ $t('peer.devices') }}:</b> {{ overview.data.onlineDevices }} / {{ overview.data.maxDevices }}</p> <b>{{ $t("peer.createdAt") }}:</b>
<p><b>{{ $t('peer.expireTime') }}:</b> {{ overview.data.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(overview.data.expiresAt) }}</p> {{ overview.data.createTime || "-" }}
<p><b>{{ $t('peer.lastConnectionAt') }}:</b> {{ overview.data.lastConnectionAt ? timestampToDateTime(overview.data.lastConnectionAt) : '-' }}</p> </p>
<p>
<b>{{ $t("peer.status") }}:</b>
{{
overview.data.disabled === 1
? $t("common.disable")
: $t("common.enable")
}}
</p>
<p>
<b>{{ $t("peer.bannedUntil") }}:</b>
{{
overview.data.bannedUntil > Date.now()
? timestampToDateTime(overview.data.bannedUntil)
: "-"
}}
</p>
<p>
<b>{{ $t("peer.totalTraffic") }}:</b>
{{
formatBytes(
(overview.data.downloadBytes || 0) +
(overview.data.uploadBytes || 0)
)
}}
</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"> <div class="mt-2">
<p><b>{{ $t('peer.clientUri') }}:</b></p> <p>
<b>{{ $t("peer.clientUri") }}:</b>
</p>
<el-input :model-value="overviewClientUrl" readonly /> <el-input :model-value="overviewClientUrl" readonly />
</div> </div>
<div class="mt-2"> <div class="mt-2">
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.copyUri") }}</el-button> <el-button type="primary" @click="copyUri(overview.data)">{{
<el-button @click="loadOverviewQr(overview.data)">{{ $t("peer.clientQr") }}</el-button> $t("peer.copyUri")
}}</el-button>
<el-button @click="loadOverviewQr(overview.data)">{{
$t("peer.clientQr")
}}</el-button>
</div> </div>
<div class="mt-2" v-if="overviewQrUrl"> <div class="mt-2" v-if="overviewQrUrl">
<div class="qr-dialog-body"> <div class="qr-dialog-body">
<qrcode-vue :value="overviewQrUrl" :size="220" level="M" render-as="svg" /> <qrcode-vue
:value="overviewQrUrl"
:size="220"
level="M"
render-as="svg"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -164,7 +331,12 @@ import {
updatePeerApi, updatePeerApi,
exportPeerApi, exportPeerApi,
} from "@/api/peer"; } from "@/api/peer";
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types"; import {
PeerPageDto,
PeerSaveDto,
PeerUpdateDto,
PeerVo,
} from "@/api/peer/types";
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus"; import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
const { t } = useI18n(); const { t } = useI18n();
@@ -177,9 +349,18 @@ const importFileList = ref<UploadFile[]>([]);
const overviewQrUrl = ref(""); const overviewQrUrl = ref("");
const overviewClientUrl = ref(""); const overviewClientUrl = ref("");
const formRef = ref(); const formRef = ref();
const overview = reactive<{ visible: boolean; data: PeerVo | null }>({ visible: false, data: null }); 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 queryParams = reactive<PeerPageDto>({
pageNum: 1,
pageSize: 10,
name: undefined,
remark: undefined,
disabled: undefined,
});
const dialog = reactive({ visible: false, title: "", editId: 0 }); const dialog = reactive({ visible: false, title: "", editId: 0 });
const dataForm = reactive<PeerSaveDto & { id?: number }>({ const dataForm = reactive<PeerSaveDto & { id?: number }>({
name: "", name: "",
@@ -197,7 +378,13 @@ const disabledBool = computed({
}); });
const rules = { const rules = {
name: [{ required: true, message: t("common.required"), trigger: ["change", "blur"] }], name: [
{
required: true,
message: t("common.required"),
trigger: ["change", "blur"],
},
],
}; };
function quotaText(v: number) { function quotaText(v: number) {
@@ -241,7 +428,16 @@ async function handleQuery() {
} }
function handleAdd() { function handleAdd() {
Object.assign(dataForm, { id: undefined, name: "", secret: "", quotaBytes: -1, expiresAt: getMonthLater(), maxDevices: 3, disabled: 0, remark: "" }); Object.assign(dataForm, {
id: undefined,
name: "",
secret: "",
quotaBytes: -1,
expiresAt: getMonthLater(),
maxDevices: 3,
disabled: 0,
remark: "",
});
dialog.title = t("common.add"); dialog.title = t("common.add");
dialog.editId = 0; dialog.editId = 0;
dialog.visible = true; dialog.visible = true;
@@ -261,10 +457,23 @@ async function submitForm() {
if (!ok) return; if (!ok) return;
} }
if (dialog.editId > 0 && dataForm.secret) { if (dialog.editId > 0 && dataForm.secret) {
await ElMessageBox.confirm(t("peer.secretRotateConfirm"), t("common.warning"), { type: "warning" }); await ElMessageBox.confirm(
t("peer.secretRotateConfirm"),
t("common.warning"),
{ type: "warning" }
);
} }
if (dialog.editId > 0) { 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 }; 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); await updatePeerApi(payload);
} else { } else {
await savePeerApi(dataForm); await savePeerApi(dataForm);
@@ -274,13 +483,21 @@ async function submitForm() {
} }
async function handleDelete(row: PeerVo) { async function handleDelete(row: PeerVo) {
await ElMessageBox.confirm(t("common.deleteConfirm", { username: row.name }), t("common.warning"), { type: "warning" }); await ElMessageBox.confirm(
t("common.deleteConfirm", { username: row.name }),
t("common.warning"),
{ type: "warning" }
);
await deletePeerApi({ id: row.id }); await deletePeerApi({ id: row.id });
await handleQuery(); await handleQuery();
} }
async function handleResetTraffic(row: PeerVo) { async function handleResetTraffic(row: PeerVo) {
await ElMessageBox.confirm(t("common.resetTrafficConfirm"), t("common.warning"), { type: "warning" }); await ElMessageBox.confirm(
t("common.resetTrafficConfirm"),
t("common.warning"),
{ type: "warning" }
);
await resetPeerTrafficApi({ id: row.id }); await resetPeerTrafficApi({ id: row.id });
await handleQuery(); await handleQuery();
} }
@@ -332,7 +549,9 @@ function beforeImport(file: UploadRawFile) {
async function handleExport() { async function handleExport() {
try { try {
const response = await exportPeerApi(); const response = await exportPeerApi();
const blob = new Blob([response.data], { type: "application/octet-stream" }); const blob = new Blob([response.data], {
type: "application/octet-stream",
});
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
document.body.appendChild(a); document.body.appendChild(a);
@@ -351,10 +570,26 @@ onMounted(handleQuery);
</script> </script>
<style scoped> <style scoped>
.peer-title { font-weight: 600; } .peer-title {
.peer-sub { font-size: 12px; color: #909399; } font-weight: 600;
.peer-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; } }
.peer-more-btn { font-size: 16px; } .peer-sub {
.qr-dialog-body { display: flex; align-items: center; justify-content: center; padding: 12px 0 20px; } font-size: 12px;
color: #909399;
}
.peer-actions {
display: flex;
gap: 8px;
align-items: center;
justify-content: flex-end;
}
.peer-more-btn {
font-size: 16px;
}
.qr-dialog-body {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0 20px;
}
</style> </style>
@@ -13,5 +13,3 @@ const { path } = params;
router.replace({ path: "/" + path, query }); router.replace({ path: "/" + path, query });
</script> </script>