fix27: полный production-фикс dashboard/peer/i18n без легаси-костылей
This commit is contained in:
@@ -17,11 +17,13 @@
|
|||||||
"@vueuse/core": "^9.1.1",
|
"@vueuse/core": "^9.1.1",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.3.4",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
"element-plus": "^2.3.1",
|
"element-plus": "^2.3.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-i18n": "9",
|
"vue-i18n": "9",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
|
|||||||
Generated
+61
@@ -20,6 +20,9 @@ importers:
|
|||||||
copy-to-clipboard:
|
copy-to-clipboard:
|
||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
|
echarts:
|
||||||
|
specifier: ^5.5.1
|
||||||
|
version: 5.6.0
|
||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(vue@3.2.45)
|
version: 2.3.1(vue@3.2.45)
|
||||||
@@ -35,6 +38,9 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.2.45
|
specifier: ^3.2.45
|
||||||
version: 3.2.45
|
version: 3.2.45
|
||||||
|
vue-echarts:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45)
|
||||||
vue-i18n:
|
vue-i18n:
|
||||||
specifier: '9'
|
specifier: '9'
|
||||||
version: 9.0.0(vue@3.2.45)
|
version: 9.0.0(vue@3.2.45)
|
||||||
@@ -1183,6 +1189,9 @@ packages:
|
|||||||
duplexer@0.1.2:
|
duplexer@0.1.2:
|
||||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
|
|
||||||
|
echarts@5.6.0:
|
||||||
|
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||||
|
|
||||||
electron-to-chromium@1.4.529:
|
electron-to-chromium@1.4.529:
|
||||||
resolution: {integrity: sha512-6uyPyXTo8lkv8SWAmjKFbG42U073TXlzD4R8rW3EzuznhFS2olCIAfjjQtV2dV2ar/vRF55KUd3zQYnCB0dd3A==}
|
resolution: {integrity: sha512-6uyPyXTo8lkv8SWAmjKFbG42U073TXlzD4R8rW3EzuznhFS2olCIAfjjQtV2dV2ar/vRF55KUd3zQYnCB0dd3A==}
|
||||||
|
|
||||||
@@ -2698,6 +2707,9 @@ packages:
|
|||||||
tslib@1.14.1:
|
tslib@1.14.1:
|
||||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||||
|
|
||||||
tsutils@3.21.0:
|
tsutils@3.21.0:
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -2868,6 +2880,17 @@ packages:
|
|||||||
terser:
|
terser:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vue-demi@0.13.11:
|
||||||
|
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.0.0-rc.1
|
||||||
|
vue: ^3.0.0-0 || ^2.6.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
vue-demi@0.14.6:
|
vue-demi@0.14.6:
|
||||||
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2879,6 +2902,16 @@ packages:
|
|||||||
'@vue/composition-api':
|
'@vue/composition-api':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vue-echarts@7.0.3:
|
||||||
|
resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/runtime-core': ^3.0.0
|
||||||
|
echarts: ^5.5.1
|
||||||
|
vue: ^2.7.0 || ^3.1.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/runtime-core':
|
||||||
|
optional: true
|
||||||
|
|
||||||
vue-eslint-parser@9.3.1:
|
vue-eslint-parser@9.3.1:
|
||||||
resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
|
resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
|
||||||
engines: {node: ^14.17.0 || >=16.0.0}
|
engines: {node: ^14.17.0 || >=16.0.0}
|
||||||
@@ -2952,6 +2985,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zrender@5.6.1:
|
||||||
|
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@aashutoshrathi/word-wrap@1.2.6': {}
|
'@aashutoshrathi/word-wrap@1.2.6': {}
|
||||||
@@ -4155,6 +4191,11 @@ snapshots:
|
|||||||
|
|
||||||
duplexer@0.1.2: {}
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
|
echarts@5.6.0:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
zrender: 5.6.1
|
||||||
|
|
||||||
electron-to-chromium@1.4.529: {}
|
electron-to-chromium@1.4.529: {}
|
||||||
|
|
||||||
element-plus@2.3.1(vue@3.2.45):
|
element-plus@2.3.1(vue@3.2.45):
|
||||||
@@ -5742,6 +5783,8 @@ snapshots:
|
|||||||
|
|
||||||
tslib@1.14.1: {}
|
tslib@1.14.1: {}
|
||||||
|
|
||||||
|
tslib@2.3.0: {}
|
||||||
|
|
||||||
tsutils@3.21.0(typescript@4.9.3):
|
tsutils@3.21.0(typescript@4.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 1.14.1
|
tslib: 1.14.1
|
||||||
@@ -5962,10 +6005,24 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.58.3
|
sass: 1.58.3
|
||||||
|
|
||||||
|
vue-demi@0.13.11(vue@3.2.45):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.2.45
|
||||||
|
|
||||||
vue-demi@0.14.6(vue@3.2.45):
|
vue-demi@0.14.6(vue@3.2.45):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
|
|
||||||
|
vue-echarts@7.0.3(@vue/runtime-core@3.2.45)(echarts@5.6.0)(vue@3.2.45):
|
||||||
|
dependencies:
|
||||||
|
echarts: 5.6.0
|
||||||
|
vue: 3.2.45
|
||||||
|
vue-demi: 0.13.11(vue@3.2.45)
|
||||||
|
optionalDependencies:
|
||||||
|
'@vue/runtime-core': 3.2.45
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
|
||||||
vue-eslint-parser@9.3.1(eslint@8.34.0):
|
vue-eslint-parser@9.3.1(eslint@8.34.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
@@ -6037,3 +6094,7 @@ snapshots:
|
|||||||
yargs-parser@20.2.9: {}
|
yargs-parser@20.2.9: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zrender@5.6.1:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
|||||||
@@ -50,21 +50,21 @@ export function updatePeerApi(data: PeerUpdateDto): AxiosPromise {
|
|||||||
export function resetPeerTrafficApi(data: IdDto): AxiosPromise {
|
export function resetPeerTrafficApi(data: IdDto): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: `/peers/${data.id}/reset-traffic`,
|
url: `/peers/${data.id}/reset-traffic`,
|
||||||
method: "post",
|
method: "patch",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function releaseKickPeerApi(data: IdDto): AxiosPromise {
|
export function releaseKickPeerApi(data: IdDto): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: `/peers/${data.id}/release-kick`,
|
url: `/peers/${data.id}/release-kick`,
|
||||||
method: "post",
|
method: "patch",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function kickPeerApi(id: number, data: KickPeerDto): AxiosPromise {
|
export function kickPeerApi(id: number, data: KickPeerDto): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: `/peers/${id}/kick`,
|
url: `/peers/${id}/kick`,
|
||||||
method: "post",
|
method: "patch",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export function getPeerClientConfigApi(
|
|||||||
|
|
||||||
export function importPeerApi(data: FormData): AxiosPromise {
|
export function importPeerApi(data: FormData): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: "/peers/import",
|
url: "/peer-import",
|
||||||
method: "post",
|
method: "post",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
@@ -91,7 +91,7 @@ export function importPeerApi(data: FormData): AxiosPromise {
|
|||||||
|
|
||||||
export function exportPeerApi(): AxiosPromise {
|
export function exportPeerApi(): AxiosPromise {
|
||||||
return request({
|
return request({
|
||||||
url: "/peers/export",
|
url: "/peer-export",
|
||||||
method: "post",
|
method: "post",
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export default {
|
|||||||
username: "Username",
|
username: "Username",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
login: "Login",
|
login: "Login",
|
||||||
|
capsLockOn: "Caps lock is On",
|
||||||
|
usernameFormatIncorrect: "Username format is incorrect",
|
||||||
|
passwordFormatIncorrect: "Password format is incorrect",
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
stale: "Dashboard data is stale. Retrying automatically...",
|
stale: "Dashboard data is stale. Retrying automatically...",
|
||||||
@@ -34,9 +37,19 @@ export default {
|
|||||||
todayDownload: "Today download",
|
todayDownload: "Today download",
|
||||||
todayUpload: "Today upload",
|
todayUpload: "Today upload",
|
||||||
trafficChart: "Traffic & System Timeseries",
|
trafficChart: "Traffic & System Timeseries",
|
||||||
|
trafficSeriesChart: "Traffic: download/upload",
|
||||||
|
systemSeriesChart: "System: CPU/RAM",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
upload: "Upload",
|
upload: "Upload",
|
||||||
total: "Total",
|
total: "Total",
|
||||||
|
security: {
|
||||||
|
hysteriaStopped: "Hysteria2 is stopped",
|
||||||
|
trafficApiUnavailable: "Traffic API is unavailable",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
trafficApiUnavailable: "Traffic API is unavailable",
|
||||||
|
collectorStale: "Collector data is stale",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
changePasswordTitle: "Change password",
|
changePasswordTitle: "Change password",
|
||||||
@@ -93,8 +106,8 @@ export default {
|
|||||||
yearLater: "A year later",
|
yearLater: "A year later",
|
||||||
hourLater: "A hour later",
|
hourLater: "A hour later",
|
||||||
dayLater: "A day later",
|
dayLater: "A day later",
|
||||||
deleteConfirm: "Are you sure to delete the user \u300c{username}\u300d?",
|
deleteConfirm: "Are you sure to delete peer \u300c{username}\u300d?",
|
||||||
resetTrafficConfirm: "Are you sure to reset traffic?",
|
resetTrafficConfirm: "Are you sure to reset peer traffic?",
|
||||||
invalid: "Invalid value",
|
invalid: "Invalid value",
|
||||||
switchLanguageSuccess: "Language switched successfully",
|
switchLanguageSuccess: "Language switched successfully",
|
||||||
sizeChanged: "Interface size changed",
|
sizeChanged: "Interface size changed",
|
||||||
@@ -116,7 +129,6 @@ export default {
|
|||||||
peer: {
|
peer: {
|
||||||
name: "Peer",
|
name: "Peer",
|
||||||
remark: "Remark",
|
remark: "Remark",
|
||||||
username: "Username",
|
|
||||||
secret: "Secret",
|
secret: "Secret",
|
||||||
maxDevices: "Max devices",
|
maxDevices: "Max devices",
|
||||||
disabled: "Disabled",
|
disabled: "Disabled",
|
||||||
@@ -130,8 +142,6 @@ export default {
|
|||||||
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.",
|
||||||
pass: "Pass",
|
|
||||||
conPass: "ConPass",
|
|
||||||
quota: "Quota",
|
quota: "Quota",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
upload: "Upload",
|
upload: "Upload",
|
||||||
@@ -143,7 +153,6 @@ export default {
|
|||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
device: "Online Devices",
|
device: "Online Devices",
|
||||||
role: "Role",
|
|
||||||
unit: "Unit",
|
unit: "Unit",
|
||||||
loginAt: "Last login time",
|
loginAt: "Last login time",
|
||||||
conAt: "Last connection time",
|
conAt: "Last connection time",
|
||||||
@@ -153,6 +162,11 @@ export default {
|
|||||||
kickTip: "Force user to log off",
|
kickTip: "Force user to log off",
|
||||||
releaseKick: "Release",
|
releaseKick: "Release",
|
||||||
releaseKickTip: "Remove offline status",
|
releaseKickTip: "Remove offline status",
|
||||||
|
createdAt: "Created at",
|
||||||
|
bannedUntil: "Banned until",
|
||||||
|
totalTraffic: "Total traffic",
|
||||||
|
copyUri: "Copy URI",
|
||||||
|
more: "More",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
huiWebPort: "HY2XS admin Web Port",
|
huiWebPort: "HY2XS admin Web Port",
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export default {
|
|||||||
username: "Логин",
|
username: "Логин",
|
||||||
password: "Пароль",
|
password: "Пароль",
|
||||||
login: "Войти",
|
login: "Войти",
|
||||||
|
capsLockOn: "Caps Lock включён",
|
||||||
|
usernameFormatIncorrect: "Неверный формат логина",
|
||||||
|
passwordFormatIncorrect: "Неверный формат пароля",
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
|
stale: "Данные дашборда устарели. Выполняется автоматическая повторная попытка...",
|
||||||
@@ -32,9 +35,19 @@ export default {
|
|||||||
todayDownload: "Скачано за сегодня",
|
todayDownload: "Скачано за сегодня",
|
||||||
todayUpload: "Отдано за сегодня",
|
todayUpload: "Отдано за сегодня",
|
||||||
trafficChart: "Трафик и системные метрики",
|
trafficChart: "Трафик и системные метрики",
|
||||||
|
trafficSeriesChart: "Трафик: download/upload",
|
||||||
|
systemSeriesChart: "Система: CPU/RAM",
|
||||||
download: "Скачано",
|
download: "Скачано",
|
||||||
upload: "Отдано",
|
upload: "Отдано",
|
||||||
total: "Всего",
|
total: "Всего",
|
||||||
|
security: {
|
||||||
|
hysteriaStopped: "Hysteria2 остановлена",
|
||||||
|
trafficApiUnavailable: "Traffic API недоступен",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
trafficApiUnavailable: "Traffic API недоступен",
|
||||||
|
collectorStale: "Данные коллектора устарели",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
changePasswordTitle: "Смена пароля",
|
changePasswordTitle: "Смена пароля",
|
||||||
@@ -89,8 +102,8 @@ export default {
|
|||||||
yearLater: "Через год",
|
yearLater: "Через год",
|
||||||
hourLater: "Через час",
|
hourLater: "Через час",
|
||||||
dayLater: "Через день",
|
dayLater: "Через день",
|
||||||
deleteConfirm: "Вы уверены, что хотите удалить пользователя «{username}»?",
|
deleteConfirm: "Удалить пир «{username}»?",
|
||||||
resetTrafficConfirm: "Сбросить трафик для пользователя?",
|
resetTrafficConfirm: "Сбросить трафик для пира?",
|
||||||
invalid: "Некорректное значение",
|
invalid: "Некорректное значение",
|
||||||
switchLanguageSuccess: "Язык переключён",
|
switchLanguageSuccess: "Язык переключён",
|
||||||
sizeChanged: "Размер интерфейса изменён",
|
sizeChanged: "Размер интерфейса изменён",
|
||||||
@@ -111,7 +124,6 @@ export default {
|
|||||||
peer: {
|
peer: {
|
||||||
name: "Пир",
|
name: "Пир",
|
||||||
remark: "Комментарий",
|
remark: "Комментарий",
|
||||||
username: "Логин",
|
|
||||||
secret: "Секрет",
|
secret: "Секрет",
|
||||||
maxDevices: "Лимит устройств",
|
maxDevices: "Лимит устройств",
|
||||||
disabled: "Отключён",
|
disabled: "Отключён",
|
||||||
@@ -125,8 +137,6 @@ export default {
|
|||||||
clientQr: "QR клиента",
|
clientQr: "QR клиента",
|
||||||
unlimited: "Безлимит",
|
unlimited: "Безлимит",
|
||||||
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
|
secretRotateConfirm: "Сменить секрет пира? Текущие клиентские конфиги перестанут работать до обновления.",
|
||||||
pass: "Пароль входа",
|
|
||||||
conPass: "Пароль подключения",
|
|
||||||
quota: "Квота",
|
quota: "Квота",
|
||||||
download: "Скачано",
|
download: "Скачано",
|
||||||
upload: "Отдано",
|
upload: "Отдано",
|
||||||
@@ -138,7 +148,11 @@ export default {
|
|||||||
online: "Онлайн",
|
online: "Онлайн",
|
||||||
offline: "Офлайн",
|
offline: "Офлайн",
|
||||||
device: "Устройства",
|
device: "Устройства",
|
||||||
role: "Роль",
|
createdAt: "Создан",
|
||||||
|
bannedUntil: "Блокировка до",
|
||||||
|
totalTraffic: "Суммарный трафик",
|
||||||
|
copyUri: "Копировать URI",
|
||||||
|
more: "Ещё",
|
||||||
unit: "Ед. изм.",
|
unit: "Ед. изм.",
|
||||||
loginAt: "Последний вход",
|
loginAt: "Последний вход",
|
||||||
conAt: "Последнее подключение",
|
conAt: "Последнее подключение",
|
||||||
|
|||||||
@@ -51,13 +51,10 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table :data="timeseriesRows" size="small" v-loading="timeseriesLoading">
|
<div v-loading="timeseriesLoading" class="chart-grid">
|
||||||
<el-table-column prop="ts" :label="$t('common.createTime')" width="180" />
|
<v-chart class="chart" autoresize :option="trafficChartOption" />
|
||||||
<el-table-column prop="download" :label="$t('dashboard.download')" />
|
<v-chart class="chart" autoresize :option="systemChartOption" />
|
||||||
<el-table-column prop="upload" :label="$t('dashboard.upload')" />
|
</div>
|
||||||
<el-table-column prop="cpu" :label="$t('dashboard.cpu')" width="120" />
|
|
||||||
<el-table-column prop="mem" :label="$t('dashboard.ram')" width="120" />
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" class="mt-3">
|
<el-card shadow="never" class="mt-3">
|
||||||
@@ -81,11 +78,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useIntervalFn } from "@vueuse/core";
|
import { useIntervalFn } from "@vueuse/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { use } from "echarts/core";
|
||||||
|
import { CanvasRenderer } from "echarts/renderers";
|
||||||
|
import { LineChart } from "echarts/charts";
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
TitleComponent,
|
||||||
|
} from "echarts/components";
|
||||||
|
import VChart from "vue-echarts";
|
||||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
|
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||||
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||||
import { formatBytes } from "@/utils/byte";
|
import { formatBytes } from "@/utils/byte";
|
||||||
import { timestampToDateTime } from "@/utils/time";
|
import { timestampToDateTime } from "@/utils/time";
|
||||||
|
|
||||||
|
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, TitleComponent]);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const summary = ref<DashboardSummaryVo>({
|
const summary = ref<DashboardSummaryVo>({
|
||||||
@@ -150,23 +160,109 @@ const loadDashboard = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeseriesRows = computed(() => {
|
const xAxisLabels = computed(() => {
|
||||||
const trafficMap: Record<number, { download: number; upload: number }> = {};
|
const allTs = new Set<number>();
|
||||||
for (const t of timeseries.value.traffic || []) {
|
for (const item of timeseries.value.traffic || []) {
|
||||||
trafficMap[t.ts] = { download: t.download || 0, upload: t.upload || 0 };
|
allTs.add(item.ts);
|
||||||
}
|
}
|
||||||
return (timeseries.value.system || []).map((s) => {
|
for (const item of timeseries.value.system || []) {
|
||||||
const tm = trafficMap[s.ts] || { download: 0, upload: 0 };
|
allTs.add(item.ts);
|
||||||
return {
|
}
|
||||||
ts: timestampToDateTime(s.ts),
|
return Array.from(allTs).sort((a, b) => a - b);
|
||||||
download: formatBytes(tm.download),
|
|
||||||
upload: formatBytes(tm.upload),
|
|
||||||
cpu: `${Math.round((s.cpu || 0) * 10) / 10}%`,
|
|
||||||
mem: `${Math.round((s.mem || 0) * 10) / 10}%`,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const trafficMap = computed(() => {
|
||||||
|
const m = new Map<number, { download: number; upload: number }>();
|
||||||
|
for (const item of timeseries.value.traffic || []) {
|
||||||
|
m.set(item.ts, { download: item.download || 0, upload: item.upload || 0 });
|
||||||
|
}
|
||||||
|
return m;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const systemMap = computed(() => {
|
||||||
|
const m = new Map<number, { cpu: number; mem: number }>();
|
||||||
|
for (const item of timeseries.value.system || []) {
|
||||||
|
m.set(item.ts, { cpu: item.cpu || 0, mem: item.mem || 0 });
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
const trafficChartOption = computed(() => ({
|
||||||
|
title: { text: t("dashboard.trafficSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } },
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
valueFormatter: (value: number) => formatBytes(value || 0),
|
||||||
|
},
|
||||||
|
legend: { top: 0 },
|
||||||
|
grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true },
|
||||||
|
dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }],
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: number) => formatBytes(v),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: t("dashboard.download"),
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("dashboard.upload"),
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const systemChartOption = computed(() => ({
|
||||||
|
title: { text: t("dashboard.systemSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } },
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
valueFormatter: (value: number) => `${Math.round((value || 0) * 10) / 10}%`,
|
||||||
|
},
|
||||||
|
legend: { top: 0 },
|
||||||
|
grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true },
|
||||||
|
dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }],
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: { formatter: "{value}%" },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: t("dashboard.cpu"),
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t("dashboard.ram"),
|
||||||
|
type: "line",
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
||||||
() => {
|
() => {
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
@@ -211,5 +307,16 @@ onUnmounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:disabled="isCapslock === false"
|
:disabled="isCapslock === false"
|
||||||
content="Caps lock is On"
|
:content="$t('login.capsLockOn')"
|
||||||
placement="right"
|
placement="right"
|
||||||
>
|
>
|
||||||
<el-form-item prop="pass">
|
<el-form-item prop="pass">
|
||||||
@@ -123,7 +123,7 @@ const loginRules = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||||
message: "Username format is incorrect",
|
message: t("login.usernameFormatIncorrect"),
|
||||||
trigger: ["change", "blur"],
|
trigger: ["change", "blur"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -135,7 +135,7 @@ const loginRules = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
pattern: /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/,
|
||||||
message: "Password format is incorrect",
|
message: t("login.passwordFormatIncorrect"),
|
||||||
trigger: ["change", "blur"],
|
trigger: ["change", "blur"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-form :model="queryParams" :inline="true" class="mb-3">
|
<el-form :model="queryParams" :inline="true" class="mb-3">
|
||||||
<el-form-item :label="$t('peer.username')">
|
<el-form-item :label="$t('peer.name')">
|
||||||
<el-input v-model="queryParams.name" clearable style="width: 220px" />
|
<el-input v-model="queryParams.name" clearable style="width: 220px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('peer.remark')">
|
<el-form-item :label="$t('peer.remark')">
|
||||||
@@ -44,15 +44,15 @@
|
|||||||
<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="340" fixed="right">
|
<el-table-column :label="$t('common.operate')" width="230" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
|
<el-button link type="primary" @click="openOverview(scope.row)">{{ $t("peer.overview") }}</el-button>
|
||||||
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.clientUri") }}</el-button>
|
<el-button link type="primary" @click="copyUri(scope.row)">{{ $t("peer.copyUri") }}</el-button>
|
||||||
<el-button link type="primary" @click="showQr(scope.row)">{{ $t("peer.clientQr") }}</el-button>
|
|
||||||
<el-dropdown>
|
<el-dropdown>
|
||||||
<span class="el-dropdown-link">{{ $t("common.operate") }}</span>
|
<span class="el-dropdown-link">{{ $t("peer.more") }}</span>
|
||||||
<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="handleUpdate(scope.row)">{{ $t("common.edit") }}</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="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="handleKick(scope.row)">{{ $t("peer.kick") }}</el-dropdown-item>
|
||||||
@@ -97,6 +97,10 @@
|
|||||||
<p><b>{{ $t('peer.authId') }}:</b> {{ overview.data.authId }}</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.name') }}:</b> {{ overview.data.name }}</p>
|
||||||
<p><b>{{ $t('peer.remark') }}:</b> {{ overview.data.remark || '-' }}</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.quota') }}:</b> {{ quotaText(overview.data.quotaBytes) }}</p>
|
||||||
<p><b>{{ $t('peer.download') }}:</b> {{ formatBytes(overview.data.downloadBytes) }}</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.upload') }}:</b> {{ formatBytes(overview.data.uploadBytes) }}</p>
|
||||||
@@ -104,8 +108,15 @@
|
|||||||
<p><b>{{ $t('peer.expireTime') }}:</b> {{ overview.data.expiresAt === 0 ? $t('peer.unlimited') : timestampToDateTime(overview.data.expiresAt) }}</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.lastConnectionAt') }}:</b> {{ overview.data.lastConnectionAt ? timestampToDateTime(overview.data.lastConnectionAt) : '-' }}</p>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<el-button type="primary" @click="copyUri(overview.data)">{{ $t("peer.clientUri") }}</el-button>
|
<p><b>{{ $t('peer.clientUri') }}:</b></p>
|
||||||
<el-button @click="showQr(overview.data)">{{ $t("peer.clientQr") }}</el-button>
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2" v-if="overviewQrSrc">
|
||||||
|
<el-image style="width:220px;height:220px" :src="overviewQrSrc" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
@@ -138,6 +149,8 @@ const total = ref(0);
|
|||||||
const records = ref<PeerVo[]>([]);
|
const records = ref<PeerVo[]>([]);
|
||||||
const qrDialog = ref(false);
|
const qrDialog = ref(false);
|
||||||
const qrSrc = ref("");
|
const qrSrc = ref("");
|
||||||
|
const overviewQrSrc = 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 });
|
||||||
|
|
||||||
@@ -175,6 +188,20 @@ function trafficPercent(row: PeerVo) {
|
|||||||
function openOverview(row: PeerVo) {
|
function openOverview(row: PeerVo) {
|
||||||
overview.data = row;
|
overview.data = row;
|
||||||
overview.visible = true;
|
overview.visible = true;
|
||||||
|
overviewQrSrc.value = "";
|
||||||
|
overviewClientUrl.value = "";
|
||||||
|
void loadOverviewClientConfig(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOverviewClientConfig(row: PeerVo) {
|
||||||
|
const { data } = await getPeerClientConfigApi(row.id);
|
||||||
|
overviewClientUrl.value = data.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOverviewQr(row: PeerVo) {
|
||||||
|
const { data } = await getPeerClientConfigApi(row.id);
|
||||||
|
overviewClientUrl.value = data.url;
|
||||||
|
overviewQrSrc.value = `data:image/png;base64,${data.qrCode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleQuery() {
|
async function handleQuery() {
|
||||||
|
|||||||
+6
-5
@@ -14,15 +14,16 @@ func initPeerRouter(peerApi *gin.RouterGroup) {
|
|||||||
peers.PATCH("/:id", controller.UpdatePeer)
|
peers.PATCH("/:id", controller.UpdatePeer)
|
||||||
peers.DELETE("/:id", controller.DeletePeer)
|
peers.DELETE("/:id", controller.DeletePeer)
|
||||||
|
|
||||||
peers.POST("/:id/reset-traffic", controller.ResetTraffic)
|
peers.PATCH("/:id/reset-traffic", controller.ResetTraffic)
|
||||||
peers.POST("/:id/release-kick", controller.ReleaseKickPeer)
|
peers.PATCH("/:id/release-kick", controller.ReleaseKickPeer)
|
||||||
peers.POST("/:id/kick", controller.KickPeer)
|
peers.PATCH("/:id/kick", controller.KickPeer)
|
||||||
peers.GET("/:id/client-config", controller.PeerClientConfig)
|
peers.GET("/:id/client-config", controller.PeerClientConfig)
|
||||||
peers.POST("/import", controller.ImportPeer)
|
|
||||||
peers.POST("/export", controller.ExportPeer)
|
|
||||||
|
|
||||||
// aliases на один миграционный релиз
|
// aliases на один миграционный релиз
|
||||||
peers.GET("/:id/client-url", controller.PeerClientConfig)
|
peers.GET("/:id/client-url", controller.PeerClientConfig)
|
||||||
peers.GET("/:id/qr", controller.PeerClientConfig)
|
peers.GET("/:id/qr", controller.PeerClientConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerApi.POST("/peer-import", controller.ImportPeer)
|
||||||
|
peerApi.POST("/peer-export", controller.ExportPeer)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user