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
+54 -54
View File
@@ -6,63 +6,63 @@
"author": "HY2XS",
"packageManager": "pnpm@9.15.9",
"scripts": {
"dev": "vite serve --mode development",
"build:prod": "vite build --mode production && vue-tsc --noEmit",
"lint:eslint": "eslint --fix --ext .ts,.js,.vue ./src ",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
"dev": "vite serve --mode development",
"build:prod": "vite build --mode production && vue-tsc --noEmit",
"lint:eslint": "eslint --fix --ext .ts,.js,.vue ./src ",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
},
"dependencies": {
"@element-plus/icons-vue": "^1.0.0",
"@vueuse/core": "^9.1.1",
"axios": "^1.3.4",
"copy-to-clipboard": "^3.3.3",
"echarts": "^5.5.1",
"element-plus": "^2.3.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^2.0.33",
"qrcode.vue": "3.4.1",
"vue": "^3.2.45",
"vue-echarts": "^7.0.3",
"vue-i18n": "9",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0"
},
"dependencies": {
"@element-plus/icons-vue": "^1.0.0",
"@vueuse/core": "^9.1.1",
"axios": "^1.3.4",
"copy-to-clipboard": "^3.3.3",
"echarts": "^5.5.1",
"element-plus": "^2.3.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^2.0.33",
"qrcode.vue": "3.4.1",
"vue": "^3.2.45",
"vue-echarts": "^7.0.3",
"vue-i18n": "9",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.8",
"@types/nprogress": "^0.2.0",
"@types/path-browserify": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.7",
"sass": "^1.58.3",
"stylelint": "^15.5.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.0.0",
"stylelint-config-recommended-scss": "^9.0.1",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^32.0.0",
"stylelint-config-standard-scss": "^8.0.0",
"typescript": "^4.9.3",
"unocss": "^0.50.1",
"unplugin-auto-import": "^0.13.0",
"unplugin-icons": "^0.15.1",
"unplugin-vue-components": "^0.23.0",
"vite": "^4.3.1",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^0.35.0"
"@iconify-json/ep": "^1.1.8",
"@types/nprogress": "^0.2.0",
"@types/path-browserify": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.7",
"sass": "^1.58.3",
"stylelint": "^15.5.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.0.0",
"stylelint-config-recommended-scss": "^9.0.1",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^32.0.0",
"stylelint-config-standard-scss": "^8.0.0",
"typescript": "^4.9.3",
"unocss": "^0.50.1",
"unplugin-auto-import": "^0.13.0",
"unplugin-icons": "^0.15.1",
"unplugin-vue-components": "^0.23.0",
"vite": "^4.3.1",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^0.35.0"
},
"engines": {
"node": ">=18.12.0"
"node": ">=18.12.0"
}
}
-2
View File
@@ -9,5 +9,3 @@ const appStore = useAppStore();
<router-view />
</el-config-provider>
</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({
url: "/admin/change-password",
method: "post",
-1
View File
@@ -24,4 +24,3 @@ export interface AdminChangePasswordDto {
export interface AdminSecurityVo {
forcePasswordChange: boolean;
}
-2
View File
@@ -110,5 +110,3 @@ export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
headers: { "Content-Type": "multipart/form-data" },
});
}
-2
View File
@@ -300,5 +300,3 @@ export interface Hysteria2AcmePathVo {
crtPath: 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({
url: "/dashboard/timeseries",
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({
url: "/dashboard/top-peers",
method: "get",
@@ -36,4 +41,3 @@ export function dashboardSecurityApi(): AxiosPromise<SecurityRiskVo[]> {
method: "get",
});
}
-1
View File
@@ -69,4 +69,3 @@ export interface DashboardTopPeerVo {
upload: number;
total: number;
}
-2
View File
@@ -33,5 +33,3 @@ export function exportLogApi(data: LogExportDto): AxiosPromise {
responseType: "blob",
});
}
-2
View File
@@ -22,5 +22,3 @@ export interface LogHysteria2Vo {
msg: string;
time: string;
}
-1
View File
@@ -96,4 +96,3 @@ export function exportPeerApi(): AxiosPromise {
responseType: "blob",
});
}
@@ -58,5 +58,3 @@ function toggleClick() {
}
}
</style>
@@ -75,5 +75,3 @@ const handleClose = (tag: string): void => {
flex-wrap: wrap;
}
</style>
@@ -34,5 +34,3 @@ function handleLanguageChange(lang: string) {
</template>
</el-dropdown>
</template>
@@ -180,5 +180,3 @@ const closeDialog = (): void => {
flex-wrap: wrap;
}
</style>
@@ -86,5 +86,3 @@ function handleCurrentChange(val: number) {
}
}
</style>
@@ -41,5 +41,3 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
fill: currentcolor; /* currentColor наследует текущий цвет элемента или родителя */
}
</style>
@@ -70,7 +70,3 @@ watch(
{ immediate: true }
);
</script>
-2
View File
@@ -7,5 +7,3 @@ export function setupDirective(app: App<Element>) {
// Делает v-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") {
return defaultSettings.language || "ru";
}
return window.localStorage.getItem("language") || defaultSettings.language || "ru";
return (
window.localStorage.getItem("language") || defaultSettings.language || "ru"
);
}
const messages = {
@@ -28,5 +30,3 @@ const i18n = createI18n({
});
export default i18n;
+2 -3
View File
@@ -139,7 +139,8 @@ export default {
clientUri: "Client URI",
clientQr: "Client QR",
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",
download: "Download",
upload: "Upload",
@@ -406,5 +407,3 @@ export default {
},
},
};
+20 -8
View File
@@ -23,7 +23,8 @@ export default {
passwordFormatIncorrect: "Неверный формат пароля",
},
dashboard: {
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
stale:
"Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
topPeers24h: "Топ пиров (24ч)",
topPeersRange: "Топ пиров ({range})",
refreshFailed: "Не удалось обновить данные дашборда",
@@ -134,7 +135,8 @@ export default {
clientUri: "URI клиента",
clientQr: "QR клиента",
unlimited: "Безлимит",
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
secretRotateConfirm:
"Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
quota: "Квота",
download: "Скачано",
upload: "Отдано",
@@ -181,7 +183,8 @@ export default {
mustBeInteger: "Поле должно быть целым числом",
invalidWebContext:
"Поле должно начинаться с / и содержать только строчные буквы (a-z) и цифры (0-9)",
invalidTrafficTime: "Поле должно быть числом максимум с одним знаком после запятой",
invalidTrafficTime:
"Поле должно быть числом максимум с одним знаком после запятой",
},
monitor: {
huiVersion: "Версия HY2XS admin",
@@ -313,8 +316,15 @@ export default {
outbounds: {
name: "Имя outbound",
type: "Тип",
socks5: { addr: "Адрес SOCKS5", username: "Логин SOCKS5", password: "Пароль SOCKS5" },
http: { url: "URL HTTP/HTTPS proxy", insecure: "Отключить TLS-проверку proxy" },
socks5: {
addr: "Адрес SOCKS5",
username: "Логин SOCKS5",
password: "Пароль SOCKS5",
},
http: {
url: "URL HTTP/HTTPS proxy",
insecure: "Отключить TLS-проверку proxy",
},
direct: {
mode: "Тип",
bindIPv4: "Локальный IPv4",
@@ -332,7 +342,11 @@ export default {
rewriteHost: "Переписывать Host header",
insecure: "Отключить TLS-проверку",
},
string: { content: "Ответ строкой", headers: "HTTP headers", statusCode: "HTTP status code" },
string: {
content: "Ответ строкой",
headers: "HTTP headers",
statusCode: "HTTP status code",
},
listenHTTP: "HTTP listen address",
listenHTTPS: "HTTPS listen address",
forceHTTPS: "Принудительно использовать HTTPS",
@@ -340,5 +354,3 @@ export default {
},
},
};
@@ -17,5 +17,3 @@
background-color: var(--el-bg-color-page);
}
</style>
@@ -35,11 +35,9 @@ function logout() {
cancelButtonText: t("common.cancel"),
type: "warning",
}).then(() => {
adminStore
.logout()
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
adminStore.logout().then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
@@ -35,5 +35,3 @@ function push() {
<slot />
</div>
</template>
@@ -1,5 +1,4 @@
<script lang="ts" setup>
defineProps({
collapse: {
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" />
</router-link>
<router-link
v-else
key="expand"
class="sidebar-logo-link"
to="/"
>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img :src="logo" class="sidebar-logo-image" />
<span class="sidebar-logo-title">HY2XS</span>
</router-link>
@@ -73,5 +67,3 @@ const logo = ref(new URL(`../../../assets/logo.png`, import.meta.url).href);
opacity: 0;
}
</style>
@@ -117,7 +117,9 @@ function resolvePath(routePath: string) {
:content="translateRouteTitleI18n(item.meta.title)"
placement="right"
>
<span class="menu-title">{{ translateRouteTitleI18n(item.meta.title) }}</span>
<span class="menu-title">{{
translateRouteTitleI18n(item.meta.title)
}}</span>
</el-tooltip>
</template>
@@ -139,4 +141,3 @@ function resolvePath(routePath: string) {
white-space: nowrap;
}
</style>
@@ -38,4 +38,3 @@ const route = useRoute();
</el-scrollbar>
</div>
</template>
@@ -1,4 +1,2 @@
export { default as Navbar } from "./Navbar.vue";
export { default as AppMain } from "./AppMain.vue";
-1
View File
@@ -97,4 +97,3 @@ function handleOutsideClick() {
opacity: 0.3;
}
</style>
-2
View File
@@ -23,5 +23,3 @@ setupDirective(app);
setupStore(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 hasRoles = adminStore.roles && adminStore.roles.length > 0;
if (hasRoles) {
if (adminStore.forcePasswordChange && to.path !== "/admin/change-password") {
next({ path: "/admin/change-password", query: { redirect: to.fullPath } });
if (
adminStore.forcePasswordChange &&
to.path !== "/admin/change-password"
) {
next({
path: "/admin/change-password",
query: { redirect: to.fullPath },
});
return;
}
// Если маршрут не найден, перейти на 404
@@ -34,14 +40,19 @@ router.beforeEach(async (to, from, next) => {
}
} else {
try {
const { roles, forcePasswordChange } = await adminStore.getAdminInfo();
const { roles, forcePasswordChange } =
await adminStore.getAdminInfo();
const permissionStore = usePermissionStoreHook();
const accessRoutes = permissionStore.generateRoutes(roles);
accessRoutes.forEach((route) => {
router.addRoute(route);
});
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;
}
next({ ...to, replace: true });
-1
View File
@@ -7,4 +7,3 @@ const defaultSettings: DefaultSettings = {
};
export default defaultSettings;
-2
View File
@@ -9,5 +9,3 @@ export function setupStore(app: App<Element>) {
}
export { store };
+5 -2
View File
@@ -19,7 +19,11 @@ export const useAdminStore = defineStore("admin", () => {
return new Promise<void>((resolve, reject) => {
loginApi(adminLoginDto)
.then((response) => {
const { tokenType, accessToken, forcePasswordChange: mustChange } = response.data;
const {
tokenType,
accessToken,
forcePasswordChange: mustChange,
} = response.data;
token.value = tokenType + " " + accessToken;
forcePasswordChange.value = !!mustChange;
resolve();
@@ -84,4 +88,3 @@ export const useAdminStore = defineStore("admin", () => {
export function useAdminStoreHook() {
return useAdminStore(store);
}
-2
View File
@@ -72,5 +72,3 @@ export const useAppStore = defineStore("app", () => {
openSideBar,
};
});
@@ -95,5 +95,3 @@ export const usePermissionStore = defineStore("permission", () => {
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
@@ -40,5 +40,3 @@
.el-table__header col[name="gutter"] {
display: table-cell !important;
}
-2
View File
@@ -73,5 +73,3 @@ a:active,
div:focus {
outline: none;
}
@@ -4,5 +4,3 @@
menuText: $menuText;
menuActiveText: $menuActiveText;
}
-1
View File
@@ -32,4 +32,3 @@ $menuActiveBorder: var(--menuActiveBorder);
$sideBarWidth: 210px;
$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
// We suggest you to commit this file into source control
// 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 {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
Hamburger: typeof import('./../components/Hamburger/index.vue')['default']
IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
IEpDownload: typeof import('~icons/ep/download')['default']
IEpMoreFilled: typeof import('~icons/ep/more-filled')['default']
IEpRefresh: typeof import('~icons/ep/refresh')['default']
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default']
IEpUpload: typeof import('~icons/ep/upload')['default']
IEpUserFilled: typeof import('~icons/ep/user-filled')['default']
ImputMultiple: typeof import('./../components/ImputMultiple/index.vue')['default']
LangSelect: typeof import('./../components/LangSelect/index.vue')['default']
MapAdd: typeof import('./../components/MapAdd/index.vue')['default']
Pagination: typeof import('./../components/Pagination/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
UnitSelect: typeof import('./../components/UnitSelect/index.vue')['default']
ElAlert: typeof import("element-plus/es")["ElAlert"];
ElAvatar: typeof import("element-plus/es")["ElAvatar"];
ElButton: typeof import("element-plus/es")["ElButton"];
ElCard: typeof import("element-plus/es")["ElCard"];
ElCol: typeof import("element-plus/es")["ElCol"];
ElDatePicker: typeof import("element-plus/es")["ElDatePicker"];
ElDialog: typeof import("element-plus/es")["ElDialog"];
ElDrawer: typeof import("element-plus/es")["ElDrawer"];
ElDropdown: typeof import("element-plus/es")["ElDropdown"];
ElDropdownItem: typeof import("element-plus/es")["ElDropdownItem"];
ElDropdownMenu: typeof import("element-plus/es")["ElDropdownMenu"];
ElEmpty: typeof import("element-plus/es")["ElEmpty"];
ElForm: typeof import("element-plus/es")["ElForm"];
ElFormItem: typeof import("element-plus/es")["ElFormItem"];
ElInput: typeof import("element-plus/es")["ElInput"];
ElInputNumber: typeof import("element-plus/es")["ElInputNumber"];
ElMenu: typeof import("element-plus/es")["ElMenu"];
ElMenuItem: typeof import("element-plus/es")["ElMenuItem"];
ElOption: typeof import("element-plus/es")["ElOption"];
ElPagination: typeof import("element-plus/es")["ElPagination"];
ElProgress: typeof import("element-plus/es")["ElProgress"];
ElRadioButton: typeof import("element-plus/es")["ElRadioButton"];
ElRadioGroup: typeof import("element-plus/es")["ElRadioGroup"];
ElRow: typeof import("element-plus/es")["ElRow"];
ElScrollbar: typeof import("element-plus/es")["ElScrollbar"];
ElSelect: typeof import("element-plus/es")["ElSelect"];
ElSubMenu: typeof import("element-plus/es")["ElSubMenu"];
ElSwitch: typeof import("element-plus/es")["ElSwitch"];
ElTable: typeof import("element-plus/es")["ElTable"];
ElTableColumn: typeof import("element-plus/es")["ElTableColumn"];
ElTabPane: typeof import("element-plus/es")["ElTabPane"];
ElTabs: typeof import("element-plus/es")["ElTabs"];
ElTag: typeof import("element-plus/es")["ElTag"];
ElTooltip: typeof import("element-plus/es")["ElTooltip"];
ElUpload: typeof import("element-plus/es")["ElUpload"];
Hamburger: typeof import("./../components/Hamburger/index.vue")["default"];
IEpCaretBottom: typeof import("~icons/ep/caret-bottom")["default"];
IEpDownload: typeof import("~icons/ep/download")["default"];
IEpMoreFilled: typeof import("~icons/ep/more-filled")["default"];
IEpRefresh: typeof import("~icons/ep/refresh")["default"];
IEpRefreshRight: typeof import("~icons/ep/refresh-right")["default"];
IEpUpload: typeof import("~icons/ep/upload")["default"];
IEpUserFilled: typeof import("~icons/ep/user-filled")["default"];
ImputMultiple: typeof import("./../components/ImputMultiple/index.vue")["default"];
LangSelect: typeof import("./../components/LangSelect/index.vue")["default"];
MapAdd: typeof import("./../components/MapAdd/index.vue")["default"];
Pagination: typeof import("./../components/Pagination/index.vue")["default"];
RouterLink: typeof import("vue-router")["RouterLink"];
RouterView: typeof import("vue-router")["RouterView"];
SvgIcon: typeof import("./../components/SvgIcon/index.vue")["default"];
UnitSelect: typeof import("./../components/UnitSelect/index.vue")["default"];
}
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 {
readonly env: ImportMetaEnv;
}
-2
View File
@@ -16,5 +16,3 @@ declare global {
}
}
export {};
-2
View File
@@ -93,5 +93,3 @@ export const formatStorageUnit = (bytes: number): string => {
// Возврат отформатированной единицы хранения
return sizes[i];
};
-2
View File
@@ -49,5 +49,3 @@ export const deepCopy = <T>(source: Partial<T>): T => {
return objCopy as T;
};
-2
View File
@@ -10,5 +10,3 @@ export function translateRouteTitleI18n(title: any) {
}
return title;
}
-2
View File
@@ -37,5 +37,3 @@ export function isExternal(path: string) {
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
return isExternal;
}
-2
View File
@@ -67,5 +67,3 @@ export const scrollTo = (to: number, duration: number, callback?: any) => {
};
animateScroll();
};
-2
View File
@@ -99,5 +99,3 @@ export const getYearLater = (): number => {
date.setFullYear(date.getFullYear() + 1);
return date.getTime();
};
@@ -18,7 +18,9 @@
<el-input v-model="form.newPassword" show-password />
</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>
</el-card>
@@ -47,12 +49,28 @@ const form = reactive({
const passwordPattern = /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,64}$/;
const rules: FormRules = {
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: [
{ 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;
try {
await adminChangePasswordApi({ oldPassword: form.oldPassword, newPassword: form.newPassword });
await adminChangePasswordApi({
oldPassword: form.oldPassword,
newPassword: form.newPassword,
});
adminStore.forcePasswordChange = false;
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);
} finally {
saving.value = false;
@@ -82,4 +104,3 @@ const submit = async () => {
max-width: 760px;
}
</style>
@@ -84,7 +84,12 @@
/>
</el-form-item>
<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
v-for="item in huiHttpsList"
:key="item.key"
@@ -165,11 +170,7 @@ import {
updateConfigsApi,
} from "@/api/config";
import { ConfigsUpdateDto } from "@/api/config/types";
import {
UploadFile,
UploadRawFile,
UploadRequestOptions,
} from "element-plus";
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
+136 -26
View File
@@ -1,7 +1,9 @@
<template>
<div class="dashboard-container">
<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>
<el-alert
@@ -23,20 +25,63 @@
v-for="risk in securityRisks"
:key="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"
class="mb-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-card shadow="never">{{ $t("dashboard.ram") }}: {{ summary.system.memPercent }}%</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.disk") }}: {{ summary.system.diskPercent }}%</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.peers") }}: {{ summary.peers.total }}</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlinePeers") }}: {{ summary.peers.onlinePeers }}</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.onlineDevices") }}: {{ summary.peers.onlineDevices }}</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayDownload") }}: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">{{ $t("dashboard.todayUpload") }}: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.cpu") }}: {{ summary.system.cpuPercent }}%</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.ram") }}: {{ summary.system.memPercent }}%</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.disk") }}:
{{ summary.system.diskPercent }}%</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.peers") }}: {{ summary.peers.total }}</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.onlinePeers") }}:
{{ summary.peers.onlinePeers }}</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.onlineDevices") }}:
{{ summary.peers.onlineDevices }}</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.todayDownload") }}:
{{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card
></el-col
>
<el-col :xs="24" :sm="12" :lg="6"
><el-card shadow="never"
>{{ $t("dashboard.todayUpload") }}:
{{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card
></el-col
>
</el-row>
<el-card shadow="never" class="mt-3">
@@ -62,17 +107,25 @@
</el-card>
<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-column prop="name" :label="$t('peer.name')" />
<el-table-column prop="download" :label="$t('dashboard.download')">
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
<template #default="scope">{{
formatBytes(scope.row.download || 0)
}}</template>
</el-table-column>
<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 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>
</el-card>
@@ -93,20 +146,61 @@ import {
TitleComponent,
} from "echarts/components";
import VChart from "vue-echarts";
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
import {
dashboardSecurityApi,
dashboardSummaryApi,
dashboardTimeseriesApi,
dashboardTopPeersApi,
} from "@/api/dashboard";
import {
DashboardSummaryVo,
DashboardTimeseriesVo,
DashboardTopPeerVo,
SecurityRiskVo,
} from "@/api/dashboard/types";
import { formatBytes } from "@/utils/byte";
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, TitleComponent]);
use([
CanvasRenderer,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
TitleComponent,
]);
const { t } = useI18n();
const summary = ref<DashboardSummaryVo>({
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 },
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 },
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" },
@@ -115,7 +209,12 @@ const summary = ref<DashboardSummaryVo>({
});
const topPeers = ref<DashboardTopPeerVo[]>([]);
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 range = ref("24h");
const loadError = ref("");
@@ -172,10 +271,14 @@ const trafficUploadData = 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(() =>
(timeseries.value.system || []).map((item) => [item.ts, item.cpu || 0])
@@ -186,7 +289,11 @@ const systemMemData = 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: {
trigger: "axis",
valueFormatter: (value: number) => formatBytes(value || 0),
@@ -223,7 +330,11 @@ const trafficChartOption = 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: {
trigger: "axis",
valueFormatter: (value: number) => `${Math.round((value || 0) * 10) / 10}%`,
@@ -319,4 +430,3 @@ onUnmounted(() => {
height: 340px;
}
</style>
+3 -3
View File
@@ -27,7 +27,9 @@ function back() {
<template>
<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-col :span="12">
<h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1>
@@ -98,5 +100,3 @@ function back() {
}
}
</style>
+3 -3
View File
@@ -46,7 +46,9 @@ function message() {
<div class="bullshit__info">
{{ $t("errorPage.notFoundInfo") }}
</div>
<a href="/#/" class="bullshit__return-home">{{ $t("errorPage.notFoundBackHome") }}</a>
<a href="/#/" class="bullshit__return-home">{{
$t("errorPage.notFoundBackHome")
}}</a>
</div>
</div>
</div>
@@ -272,5 +274,3 @@ function message() {
}
}
</style>
@@ -380,5 +380,3 @@ const closeDialog = (): void => {
flex-wrap: wrap;
}
</style>
@@ -43,17 +43,14 @@
</el-form-item>
</el-form>
<el-form
<el-form
ref="dataFormRef"
:rules="dataFormRules"
label-position="top"
:model="dataForm"
:disabled="true"
>
<el-tabs
v-model="activeName"
class="tabs"
>
<el-tabs v-model="activeName" class="tabs">
<el-tab-pane :label="$t('hysteria.extension')" name="extension">
<el-tooltip
:content="$t('hysteria.config.remark')"
@@ -980,8 +977,8 @@ export default {
<script setup lang="ts">
import {
defaultHysteria2ServerConfig,
Hysteria2ServerConfig,
defaultHysteria2ServerConfig,
Hysteria2ServerConfig,
} from "@/api/config/types";
import Outbounds from "./components/Outbounds/index.vue";
import {
@@ -107,5 +107,3 @@ onMounted(() => {
setRecords();
});
</script>
@@ -107,4 +107,3 @@ onMounted(() => {
setRecords();
});
</script>
+297 -62
View File
@@ -8,13 +8,17 @@
<el-input v-model="queryParams.remark" clearable style="width: 220px" />
</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>
<el-card shadow="never">
<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
v-model:file-list="importFileList"
:http-request="handleImport"
@@ -26,7 +30,9 @@
>
<el-button>{{ $t("common.import") }}</el-button>
</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>
<el-table v-loading="loading" :data="records">
<el-table-column :label="$t('peer.name')" min-width="220">
@@ -37,42 +43,107 @@
</el-table-column>
<el-table-column :label="$t('peer.status')" min-width="180">
<template #default="scope">
<el-tag size="small" :type="scope.row.disabled === 1 ? 'danger' : 'success'">{{ scope.row.disabled === 1 ? $t('common.disable') : $t('common.enable') }}</el-tag>
<el-tag size="small" class="ml-1" :type="scope.row.online ? 'success' : 'info'">{{ scope.row.online ? $t('peer.online') : $t('peer.offline') }}</el-tag>
<el-tag
size="small"
:type="scope.row.disabled === 1 ? 'danger' : 'success'"
>{{
scope.row.disabled === 1
? $t("common.disable")
: $t("common.enable")
}}</el-tag
>
<el-tag
size="small"
class="ml-1"
:type="scope.row.online ? 'success' : 'info'"
>{{
scope.row.online ? $t("peer.online") : $t("peer.offline")
}}</el-tag
>
</template>
</el-table-column>
<el-table-column :label="$t('peer.traffic')" min-width="260">
<template #default="scope">
<div>{{ formatBytes(scope.row.downloadBytes + scope.row.uploadBytes) }} / {{ quotaText(scope.row.quotaBytes) }}</div>
<el-progress :percentage="trafficPercent(scope.row)" :status="scope.row.quotaBytes < 0 ? undefined : (trafficPercent(scope.row) >= 90 ? 'exception' : undefined)" :show-text="false" />
<div>
{{
formatBytes(scope.row.downloadBytes + scope.row.uploadBytes)
}}
/ {{ quotaText(scope.row.quotaBytes) }}
</div>
<el-progress
:percentage="trafficPercent(scope.row)"
:status="
scope.row.quotaBytes < 0
? undefined
: trafficPercent(scope.row) >= 90
? 'exception'
: undefined
"
:show-text="false"
/>
</template>
</el-table-column>
<el-table-column :label="$t('peer.devices')" min-width="120">
<template #default="scope">{{ scope.row.onlineDevices }} / {{ scope.row.maxDevices }}</template>
<template #default="scope"
>{{ scope.row.onlineDevices }} /
{{ scope.row.maxDevices }}</template
>
</el-table-column>
<el-table-column :label="$t('peer.expireTime')" min-width="170">
<template #default="scope">{{ 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 :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 :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">
<div class="peer-actions">
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.copyUri") }}</el-button>
<el-button link type="primary" @click="openOverview(scope.row)">{{
$t("peer.overview")
}}</el-button>
<el-button link type="primary" @click="copyUri(scope.row)">{{
$t("peer.copyUri")
}}</el-button>
<el-dropdown trigger="click" placement="bottom-end">
<el-button text circle class="peer-more-btn" @click.stop>
<i-ep-more-filled />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-dropdown-item>
<el-dropdown-item @click="handleUpdate(scope.row)">{{ $t("common.edit") }}</el-dropdown-item>
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{ $t("common.resetTraffic") }}</el-dropdown-item>
<el-dropdown-item @click="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item>
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{ $t("peer.releaseKick") }}</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(scope.row)">{{ $t("common.delete") }}</el-dropdown-item>
<el-dropdown-item @click="showQr(scope.row)">{{
$t("peer.clientQr")
}}</el-dropdown-item>
<el-dropdown-item @click="handleUpdate(scope.row)">{{
$t("common.edit")
}}</el-dropdown-item>
<el-dropdown-item @click="handleResetTraffic(scope.row)">{{
$t("common.resetTraffic")
}}</el-dropdown-item>
<el-dropdown-item @click="handleKick(scope.row)">{{
$t("peer.kick")
}}</el-dropdown-item>
<el-dropdown-item @click="handleReleaseKick(scope.row)">{{
$t("peer.releaseKick")
}}</el-dropdown-item>
<el-dropdown-item
divided
@click="handleDelete(scope.row)"
>{{ $t("common.delete") }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -90,17 +161,41 @@
</el-card>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="620px">
<el-form ref="formRef" :model="dataForm" :rules="rules" label-width="140px">
<el-form-item :label="$t('peer.name')" prop="name"><el-input v-model="dataForm.name" /></el-form-item>
<el-form-item :label="$t('peer.remark')"><el-input v-model="dataForm.remark" /></el-form-item>
<el-form-item :label="$t('peer.secret')" prop="secret"><el-input v-model="dataForm.secret" show-password /></el-form-item>
<el-form-item :label="$t('peer.quota')"><el-input-number v-model="dataForm.quotaBytes" :min="-1" /></el-form-item>
<el-form-item :label="$t('peer.expireTime')"><el-date-picker v-model="dataForm.expiresAt" type="datetime" value-format="x" /></el-form-item>
<el-form-item :label="$t('peer.maxDevices')"><el-input-number v-model="dataForm.maxDevices" :min="1" /></el-form-item>
<el-form-item :label="$t('peer.disabled')"><el-switch v-model="disabledBool" /></el-form-item>
<el-form
ref="formRef"
:model="dataForm"
:rules="rules"
label-width="140px"
>
<el-form-item :label="$t('peer.name')" prop="name"
><el-input v-model="dataForm.name"
/></el-form-item>
<el-form-item :label="$t('peer.remark')"
><el-input v-model="dataForm.remark"
/></el-form-item>
<el-form-item :label="$t('peer.secret')" prop="secret"
><el-input v-model="dataForm.secret" show-password
/></el-form-item>
<el-form-item :label="$t('peer.quota')"
><el-input-number v-model="dataForm.quotaBytes" :min="-1"
/></el-form-item>
<el-form-item :label="$t('peer.expireTime')"
><el-date-picker
v-model="dataForm.expiresAt"
type="datetime"
value-format="x"
/></el-form-item>
<el-form-item :label="$t('peer.maxDevices')"
><el-input-number v-model="dataForm.maxDevices" :min="1"
/></el-form-item>
<el-form-item :label="$t('peer.disabled')"
><el-switch v-model="disabledBool"
/></el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submitForm">{{ $t("common.confirm") }}</el-button>
<el-button type="primary" @click="submitForm">{{
$t("common.confirm")
}}</el-button>
</template>
</el-dialog>
@@ -110,32 +205,104 @@
</div>
</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">
<p><b>{{ $t('peer.authId') }}:</b> {{ overview.data.authId }}</p>
<p><b>{{ $t('peer.name') }}:</b> {{ overview.data.name }}</p>
<p><b>{{ $t('peer.remark') }}:</b> {{ overview.data.remark || '-' }}</p>
<p><b>{{ $t('peer.createdAt') }}:</b> {{ overview.data.createTime || '-' }}</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>
<p>
<b>{{ $t("peer.authId") }}:</b> {{ overview.data.authId }}
</p>
<p>
<b>{{ $t("peer.name") }}:</b> {{ overview.data.name }}
</p>
<p>
<b>{{ $t("peer.remark") }}:</b> {{ overview.data.remark || "-" }}
</p>
<p>
<b>{{ $t("peer.createdAt") }}:</b>
{{ overview.data.createTime || "-" }}
</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">
<p><b>{{ $t('peer.clientUri') }}:</b></p>
<p>
<b>{{ $t("peer.clientUri") }}:</b>
</p>
<el-input :model-value="overviewClientUrl" readonly />
</div>
<div class="mt-2">
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.copyUri") }}</el-button>
<el-button @click="loadOverviewQr(overview.data)">{{ $t("peer.clientQr") }}</el-button>
<el-button type="primary" @click="copyUri(overview.data)">{{
$t("peer.copyUri")
}}</el-button>
<el-button @click="loadOverviewQr(overview.data)">{{
$t("peer.clientQr")
}}</el-button>
</div>
<div class="mt-2" v-if="overviewQrUrl">
<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>
@@ -164,7 +331,12 @@ import {
updatePeerApi,
exportPeerApi,
} 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";
const { t } = useI18n();
@@ -177,9 +349,18 @@ const importFileList = ref<UploadFile[]>([]);
const overviewQrUrl = ref("");
const overviewClientUrl = 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 dataForm = reactive<PeerSaveDto & { id?: number }>({
name: "",
@@ -197,7 +378,13 @@ const disabledBool = computed({
});
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) {
@@ -241,7 +428,16 @@ async function handleQuery() {
}
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.editId = 0;
dialog.visible = true;
@@ -261,10 +457,23 @@ async function submitForm() {
if (!ok) return;
}
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) {
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);
} else {
await savePeerApi(dataForm);
@@ -274,13 +483,21 @@ async function submitForm() {
}
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 handleQuery();
}
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 handleQuery();
}
@@ -332,7 +549,9 @@ function beforeImport(file: UploadRawFile) {
async function handleExport() {
try {
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 a = document.createElement("a");
document.body.appendChild(a);
@@ -351,10 +570,26 @@ onMounted(handleQuery);
</script>
<style scoped>
.peer-title { font-weight: 600; }
.peer-sub { 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; }
.peer-title {
font-weight: 600;
}
.peer-sub {
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>
@@ -13,5 +13,3 @@ const { path } = params;
router.replace({ path: "/" + path, query });
</script>