Compare commits

...

206 Commits

Author SHA1 Message Date
Ho3ein
5487dc41cc Merge pull request #434 from hamid-gh98/main
[FIX] siderDrawer button functionality + [Update] redirect restart panel
2023-05-14 10:49:30 +03:30
Hamidreza Ghavami
5a908b9f58 FIX redirect after restart panel 2023-05-14 06:19:18 +04:30
Hamidreza Ghavami
61288db11e FIX siderDrawer button function 2023-05-14 06:08:49 +04:30
Hamidreza Ghavami
317f7fe9da FIX sideBar style 2023-05-14 06:08:34 +04:30
Ho3ein
7b5dd2d0ee Merge pull request #432 from hamid-gh98/main
[HOTFIX] Add basePath to Redirect Middleware
2023-05-14 01:51:02 +03:30
Hamidreza Ghavami
b1302c70fb Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-14 02:32:17 +04:30
Hamidreza Ghavami
addedb1adf HOTFIX redirect middleware to add basePath 2023-05-14 02:31:23 +04:30
MHSanaei
62bb42cfab v1.4.6 2023-05-14 01:10:51 +03:30
MHSanaei
f4be9f234a lang show 2023-05-14 01:09:31 +03:30
MHSanaei
947129a62a update UI - calendar 2023-05-14 01:08:29 +03:30
Ho3ein
66f0a13145 Merge pull request #431 from hamid-gh98/main
[HOTFIX] Redirect `/xui` to `/panel`
2023-05-14 01:07:17 +03:30
Hamidreza Ghavami
9626379731 Update README.md 2023-05-14 01:42:29 +04:30
Hamidreza Ghavami
c2c61cdd5b Add Redirect Middleware for Router 2023-05-14 01:42:08 +04:30
Hamidreza Ghavami
b5ae580d12 Update '/xui/API' to new path '/panel/api' 2023-05-14 01:41:18 +04:30
MHSanaei
63939244a4 v1.4.5 2023-05-13 22:31:13 +03:30
MHSanaei
213b693bd3 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-05-13 19:06:19 +03:30
MHSanaei
a289ef5d10 bug fixed - random user pass 2023-05-13 19:06:16 +03:30
Ho3ein
955eb8f142 Merge pull request #428 from LOVECHEN/main
Update docker-compose.yml
2023-05-13 18:43:53 +03:30
LOVECHEN
d396fb5d06 Update docker-compose.yml
Define your hostname to identify the host in telegram
2023-05-13 22:55:47 +08:00
Ho3ein
b5dd258074 Merge pull request #426 from hamid-gh98/main
FIX input bg color in login page
2023-05-13 18:15:39 +03:30
MHSanaei
c855a292cb random sub button 2023-05-13 17:22:13 +03:30
Hamidreza Ghavami
f2132c62e9 fix input style 2023-05-13 18:00:47 +04:30
Hamidreza Ghavami
94a3807353 fix input bg color in login page 2023-05-13 17:42:11 +04:30
MHSanaei
7cacfc074e remove duplicate random text gen
randomText by default
length set to 8
2023-05-13 15:42:46 +03:30
MHSanaei
9e8ac8a087 remove search Data files 2023-05-13 15:03:46 +03:30
MHSanaei
e64a9eeee6 random UUID 2023-05-13 14:51:07 +03:30
MHSanaei
a55a1a7102 fix 2023-05-13 13:53:17 +03:30
MHSanaei
46bc39c160 [bug] fix cloned inbound settings
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:33:13 +03:30
MHSanaei
2a182d8b9a [bug] fix login failure when tgbot is not active
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:31:46 +03:30
MHSanaei
77241c7fcf pruning some codes
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:29:10 +03:30
MHSanaei
fd6a85afd9 Set session max-age to default if defined zero
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:26:04 +03:30
MHSanaei
9a89d7bfab spin only in reload time
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:24:44 +03:30
MHSanaei
edd6b22109 remove duplicate remark assignments
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:21:27 +03:30
Ho3ein
5468069bef Merge pull request #420 from hamid-gh98/main
[fix] russia domains in settings and More....
2023-05-13 13:18:36 +03:30
Hamidreza Ghavami
0cce35784e FIX Login UI style 2023-05-13 00:52:12 +04:30
Hamidreza Ghavami
80c1e58ed5 Update README.md 2023-05-12 22:45:33 +04:30
Hamidreza Ghavami
b0871a6ef6 Change route path '/xui' to '/panel' 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
288374d5fa Update README.md 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
1f7c79c735 Add docker-compose.yml 2023-05-12 22:45:31 +04:30
Hamidreza Ghavami
251fd608df update translation 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
456941323b await secret status in login page 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
a6a77688dc Add block speedtest switch template 2023-05-12 22:45:29 +04:30
Hamidreza Ghavami
09cd2248dc fix show client name in QR modal 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
8143379645 Add copy button for sub link 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
5bd6baa055 Fix darkClass in ThemeSwitcher 2023-05-12 22:45:27 +04:30
Hamidreza Ghavami
41e9290574 Show client email in QR Modal 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
cf7d50617b add service function to search data files 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
95e006963c add searchDatafiles route 2023-05-12 22:45:25 +04:30
Hamidreza Ghavami
65588a4492 add check for geosite function 2023-05-12 22:45:24 +04:30
Hamidreza Ghavami
d39c7e4ae3 only get enabled inbounds and clients 2023-05-12 22:45:24 +04:30
Tara Rostami
3bec9ee273 Minor changes in UI (#415)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css

* Update custom.css

* Update antd.min.css
2023-05-12 09:13:08 +03:30
Tara Rostami
7b3628d33b Optimized Settings UI (#408)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css
2023-05-11 14:04:35 +03:30
Hossein Abaiyani
ad1aa5b2f9 Cleaner Docker file with much lighter base image (#387)
* updated dockerfile

* updated dockerfile

* Update Dockerfile

added platform

* added iran.dat

* added iran.dat

---------

Co-authored-by: Hossein Abaiyani <hossein.abaiyani@arvancloud.com>
2023-05-11 13:08:44 +03:30
Ho3ein
46ef8c503e Merge pull request #392 from MHSanaei/dependabot/go_modules/gorm.io/gorm-1.25.1
Bump gorm.io/gorm from 1.25.0 to 1.25.1
2023-05-09 19:57:19 +03:30
dependabot[bot]
721fec3b5a Bump gorm.io/gorm from 1.25.0 to 1.25.1
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.0 to 1.25.1.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.0...v1.25.1)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-09 10:03:47 +00:00
Ho3ein
30a5f66f26 Merge pull request #381 from hamid-gh98/main
[FIX] bug logout path + [UPDATE] login UI and more ...
2023-05-08 19:40:02 +03:30
Hamidreza Ghavami
bb6e6861ca fix style bg :D 2023-05-08 20:19:11 +04:30
Hamidreza Ghavami
4c0e391597 update theme-switch 2023-05-08 19:44:18 +04:30
Hamidreza Ghavami
43c1fc9aad Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-08 19:41:14 +04:30
Hamidreza Ghavami
7a48cbb191 fix style login ui 2023-05-08 19:38:36 +04:30
Hamidreza Ghavami
004d69392b fix use password component 2023-05-08 19:26:01 +04:30
Hamidreza Ghavami
fc0882805d update UI to use password-input component 2023-05-08 19:24:44 +04:30
Hamidreza Ghavami
f553922d53 add password component 2023-05-08 19:21:58 +04:30
Hamidreza Ghavami
7b2764566c update login UI 2023-05-08 19:20:13 +04:30
Hamidreza Ghavami
55d38dfa48 [FIX] bug logout path 2023-05-08 19:15:33 +04:30
Hamidreza Ghavami
0e266b88f0 update UI to use themeSwitcher 2023-05-08 19:14:22 +04:30
MHSanaei
7bb3e517b2 update pic v1.4.1 2023-05-08 17:39:29 +03:30
Hamidreza Ghavami
7d0c3b6517 remove themeChanger from siderDrawer 2023-05-08 18:19:59 +04:30
Hamidreza Ghavami
67201fc678 create theme-switch component 2023-05-08 18:15:08 +04:30
Hamidreza Ghavami
d137deccfa fix style height when rotating + move cookie util to their specific file 2023-05-08 18:04:12 +04:30
MHSanaei
00777e3a25 [feature] Russian lang 2023-05-08 14:43:02 +03:30
koid38
bcb2f125ff Create translate.ru_RU.toml (#375)
ru translate
2023-05-08 14:05:16 +03:30
MHSanaei
37ab8f42e9 domain-list-community (category update)
add cn and ru regexp
2023-05-08 13:50:43 +03:30
Ho3ein
cf1cfbee96 v1.4.1 2023-05-08 10:55:28 +03:30
MHSanaei
d89e03023f + 2023-05-08 10:40:32 +03:30
Tara Rostami
5ec1559c7b UI optimized by Tara (#370)
* Update antd.min.css

* Update custom.css

---------

Co-authored-by: Ho3ein <ho3ein.sanaei@gmail.com>
2023-05-08 10:32:51 +03:30
Ho3ein
cf2b1fd9ec Merge pull request #364 from M4hbod/main
Update UI
2023-05-08 10:20:47 +03:30
itspooya
ddbc1602ce Finally - Fix WorkDIR 2023-05-08 10:04:47 +03:30
itspooya
77cee098ad Finally 2023-05-08 10:04:47 +03:30
itspooya
89b94c2c90 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
350743fea3 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
8011d0b6c6 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
9e5d7ac1d0 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
e01fb9b605 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
52a468d586 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
c73c71cc83 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
2141d62069 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
f286c9a86a Added ARM64 2023-05-08 10:04:47 +03:30
itspooya
b4fd254c71 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
0a8cf7e41b 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
abd48551fd 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
bb9a10051f 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
5c6406ab58 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
a25137f215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
50c296eb28 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
b1a302de95 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
7b567458ff 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
13e3e23f20 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
efecdf5fd0 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
f21904caf2 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
15a97af215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
20a55c086e 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
c727b81772 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
77692e0298 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
Mahbod
5e3e965647 add margin to .ant-modal 2023-05-08 03:36:52 +03:30
Mahbod
88cde18bb2 update .ant-modal ui 2023-05-08 02:55:06 +03:30
MHSanaei
7fd93e25fd only blake3-aes support multi-user 2023-05-07 23:57:59 +03:30
Ho3ein
1c9643e6a3 Update docker.yml 2023-05-07 22:55:39 +03:30
Ho3ein
eb83516fa5 Merge pull request #360 from itspooya/main
Added Github Container registry auto build
2023-05-07 22:45:39 +03:30
itspooya
1b8df3c0a1 👷 Added Docker CI 2023-05-07 21:30:58 +03:30
itspooya
54d45cc029 👷 Added Docker CI 2023-05-07 20:45:30 +03:30
itspooya
e35767aff2 👷 Added Docker CI 2023-05-07 20:29:37 +03:30
MHSanaei
9bbcb74db6 sni for xtls 2023-05-07 19:59:06 +03:30
MHSanaei
515e7f7fef client bulk bug fix 2023-05-07 19:58:56 +03:30
MHSanaei
87a5190b7d v1.4.0 2023-05-07 17:12:47 +03:30
MHSanaei
9d47d74a7a design update 2023-05-07 14:36:43 +03:30
Ho3ein
92f5dfed1c Merge pull request #356 from M4hbod/main
Update UI
2023-05-07 14:12:56 +03:30
Mahbod
867e5ea022 update tabs 2023-05-07 11:34:36 +03:30
Mahbod
a24814104c update input 2023-05-07 11:33:52 +03:30
MHSanaei
024b65524d delayedStart 2023-05-06 20:33:56 +03:30
MHSanaei
f22dd6b53d [feature] multi-user shadowsocks @alireza0
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-06 20:21:14 +03:30
MHSanaei
735df6bd4e [feature] inbounds manual refresh
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-06 19:20:42 +03:30
MHSanaei
0e77547e98 update translate 2023-05-06 16:25:41 +03:30
MHSanaei
747a1e1f60 ant design selected - active item menu - new update
https://ant.design/components/menu
2023-05-06 16:17:51 +03:30
Ho3ein
ac31d6d9fb Merge pull request #347 from hamid-gh98/main
[Feature] import/export database in the panel
2023-05-06 12:53:41 +03:30
Hamidreza Ghavami
83c853ffb6 update ImportDB and enhancement 2023-05-06 04:47:57 +04:30
Hamidreza
058ab5f901 Merge branch 'MHSanaei:main' into main 2023-05-06 03:37:04 +03:30
Ho3ein
78638a9737 Merge pull request #346 from masoud-hidden/main
Some new buttons for bot and ability to use userId in tgId
2023-05-06 02:48:14 +03:30
Masoud Hidden
7f8f0b0f2d Fix ability to use userId in tgId 2023-05-06 02:36:46 +03:30
Masoud Hidden
7b9e0b946e Merge branch 'MHSanaei:main' into main 2023-05-06 01:36:22 +03:30
Hamidreza Ghavami
6c087ceb1a fix import db and always restart xray 2023-05-06 02:22:45 +04:30
Hamidreza
6602c55f3c Update main.go 2023-05-06 00:00:56 +03:30
Hamidreza
d405141ad0 Merge branch 'MHSanaei:main' into main 2023-05-05 23:57:06 +03:30
Ho3ein
3ee3432d8f Merge pull request #348 from MHSanaei/revert-331-main
Revert "feat: Adding Cobra to provide a more intuitive command line interface"
2023-05-05 23:35:20 +03:30
Ho3ein
b2d70a2a9b Revert "feat: Adding Cobra to provide a more intuitive command line interface" 2023-05-05 23:34:38 +03:30
Hamidreza Ghavami
26f160fb89 add MigrateDB func for a single source of truth 2023-05-06 00:22:39 +04:30
Hamidreza Ghavami
0a5811adf8 update style 2023-05-05 23:16:15 +04:30
Hamidreza Ghavami
733a011b28 update .gitignore 2023-05-05 22:59:31 +04:30
Hamidreza Ghavami
c8023b7c8d update README.md 2023-05-05 22:53:07 +04:30
Hamidreza Ghavami
bed3cd445d update translation 2023-05-05 22:52:48 +04:30
Hamidreza Ghavami
c8baf5ceee add modal and button for import/export db 2023-05-05 22:52:35 +04:30
Hamidreza Ghavami
85c715a2f6 update axios-init and db.go 2023-05-05 22:51:39 +04:30
Hamidreza Ghavami
55c1fe26fb add import db api route 2023-05-05 22:49:42 +04:30
Masoud Hidden
8d11f83ac7 Fix get client ips 2023-05-05 20:07:47 +03:30
Masoud Hidden
d349bffcd6 Fix bot client enable button 2023-05-05 19:50:40 +03:30
Masoud Hidden
5856160c30 Added some new buttons to bot and ability to use userId in tgId 2023-05-05 18:20:56 +03:30
MHSanaei
3cd3693b2c setting style fix bug 2023-05-05 17:48:01 +03:30
Masoud Hidden
bc3003be54 Change some InlineKeyboard data length 2023-05-05 16:02:16 +03:30
Ho3ein
e53615d119 Merge pull request #336 from masoud-hidden/main
Buttons for the client report in the telegram bot
2023-05-05 14:41:19 +03:30
Ho3ein
55232f9033 Merge pull request #339 from M4hbod/main
Update UI
2023-05-05 14:34:42 +03:30
Mahbod
fd40e97008 update ant-card 2023-05-05 13:44:09 +03:30
Mahbod
1598ad804d update ant-tabs 2023-05-05 13:43:21 +03:30
Mahbod
c8e666d8ae update selected menu item 2023-05-05 13:42:04 +03:30
Mahbod
e1533b9418 update menu background 2023-05-05 13:41:10 +03:30
Ho3ein
a60a8d8a2f Merge pull request #331 from kaveh-ahangar/main
feat: Adding Cobra to provide a more intuitive command line interface
2023-05-05 13:23:27 +03:30
kaveh-ahangar
73704e38d5 Merge branch 'MHSanaei:main' into main 2023-05-05 13:06:59 +03:30
kaveh-ahangar
1680bb36c3 feat: Rollback files (.gitignore) 2023-05-05 12:43:09 +03:30
kaveh-ahangar
cd483c191a feat: Rollback files (.gitignore) 2023-05-05 12:42:05 +03:30
kaveh-ahangar
7d09b4e840 feat: Rollback files (main.go , Makefile) 2023-05-05 12:41:21 +03:30
kaveh-ahangar
83ffa25d6f feat: Rollback files (main.go , Makefile) 2023-05-05 12:40:20 +03:30
Masoud Hidden
a53d2b927f fix ResetClientExpiryTimeByEmail 2023-05-05 04:34:39 +03:30
Mahbod
146dc6ce4a improve circle bar ui 2023-05-05 03:54:32 +03:30
Mahbod
e597ea5ab2 improve ant-tabs ui 2023-05-05 03:52:31 +03:30
Mahbod
3e833fca9b add custom scrollbar 2023-05-05 03:51:56 +03:30
Mahbod
c96cf85619 improve .ant-card-dark ui 2023-05-05 03:51:24 +03:30
Mahbod
0a63c75138 improve menu ui 2023-05-05 03:46:57 +03:30
Masoud Hidden
c6295085fe Fix restart service 2023-05-05 02:47:26 +03:30
Masoud Hidden
4b3bdebfa5 Fix typo 2023-05-05 02:14:56 +03:30
Ho3ein
1a603b2501 Merge pull request #335 from MHSanaei/dependabot/go_modules/google.golang.org/grpc-1.55.0
Bump google.golang.org/grpc from 1.54.0 to 1.55.0
2023-05-05 02:05:00 +03:30
dependabot[bot]
3fa5f834b8 Bump google.golang.org/grpc from 1.54.0 to 1.55.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.55.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-04 22:34:24 +00:00
Masoud Hidden
ff33539fba Refresh button for client report in telegram bot
Added unlimited button for expire days.
2023-05-05 01:48:37 +03:30
Masoud Hidden
961636b510 Client reset buttons for telegram bot 2023-05-05 01:16:43 +03:30
Ho3ein
88b7d0dc44 Merge pull request #328 from MHSanaei/dependabot/go_modules/go.uber.org/atomic-1.11.0
Bump go.uber.org/atomic from 1.10.0 to 1.11.0
2023-05-05 00:49:54 +03:30
Ho3ein
e164c7e780 Merge pull request #332 from hamid-gh98/main
[Update] sub remark + settings UI
2023-05-05 00:49:23 +03:30
Hamidreza Ghavami
481d4beabb update translation 2023-05-04 22:22:54 +04:30
Hamidreza Ghavami
19c991014e updated settings.html UI 2023-05-04 21:34:15 +04:30
Hamidreza Ghavami
12ec487241 updated custom.css 2023-05-04 21:28:36 +04:30
Hamidreza Ghavami
a18cbdcf11 changed number input tags to 'a-input-number' 2023-05-04 21:27:42 +04:30
Hamidreza Ghavami
4f8de18d1f renamed setting.html to settings.html and update its route name 2023-05-04 21:09:08 +04:30
Hamidreza Ghavami
dbac48f05d updated sub remark to include inbound name 2023-05-04 20:58:16 +04:30
kaveh-ahangar
3a02359325 feat: Adding Cobra to provide a more intuitive command line interface 2023-05-04 19:46:45 +03:30
kaveh-ahangar
9e63b0e2b3 feat: add Makefile and improve building 2023-05-04 19:20:52 +03:30
dependabot[bot]
07dc8c4803 Bump go.uber.org/atomic from 1.10.0 to 1.11.0
Bumps [go.uber.org/atomic](https://github.com/uber-go/atomic) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/uber-go/atomic/releases)
- [Changelog](https://github.com/uber-go/atomic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/atomic/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: go.uber.org/atomic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-04 10:04:21 +00:00
MHSanaei
20bfd71cf1 update dependencies 2023-05-02 16:26:19 +03:30
MHSanaei
6a33a48a9a logout after update your password or secret token 2023-05-02 16:19:49 +03:30
MHSanaei
1885a8c0bf fixed - set Default Cert for Xtls 2023-05-01 20:58:00 +03:30
Ho3ein
4ce53920fe v1.3.4 2023-05-01 02:29:24 +03:30
MHSanaei
5100bbba52 simplify ssl cert 2023-04-30 00:57:15 +03:30
MHSanaei
f93d912644 primary type for setDefaultCert 2023-04-29 22:51:33 +03:30
MHSanaei
ce8551b8c4 [darkmode] better colors + add sec to calendar
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:38:41 +03:30
MHSanaei
d26c21d900 [feature] inbounds auto refresh option
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:33:45 +03:30
MHSanaei
f5f9347661 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-29 22:28:55 +03:30
MHSanaei
a0f5875cb3 [darkmode] fix UTLS - cipherSuites 2023-04-29 22:28:50 +03:30
Ho3ein
3055c68615 Postman Collection thanks to @mehdikhody #303
thanks to @mehdikhody #303
2023-04-29 20:18:24 +03:30
MHSanaei
c3ed8051f3 [feature] add sniffing DestOverride options #298
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 18:47:44 +03:30
MHSanaei
d2cdc51c54 [feature] add quic to sniffingObject
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 16:26:39 +03:30
MHSanaei
ee896662f5 remove favicon from web root
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:49:32 +03:30
MHSanaei
177bd036a3 [bug] fix GetClientTrafficByEmail
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:40:33 +03:30
MHSanaei
d03e049320 v1.3.3 2023-04-28 01:03:59 +03:30
MHSanaei
957d9e24fb Revert "grpc.WithInsecure is deprecated"
This reverts commit 0b896d9c31.
2023-04-28 00:47:56 +03:30
MHSanaei
865e47e9a6 Update check_client_ip_job.go 2023-04-28 00:30:49 +03:30
MHSanaei
607c5d3598 [feature] add grpc multiMode 2023-04-28 00:15:06 +03:30
MHSanaei
8879541999 dark mode - default 2023-04-27 23:48:58 +03:30
MHSanaei
0b896d9c31 grpc.WithInsecure is deprecated 2023-04-27 23:48:22 +03:30
MHSanaei
6f4a2809e2 tls for ss - remove unused 2023-04-27 19:25:48 +03:30
MHSanaei
103a26edb6 [migrate] remove orphaned traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-27 19:05:36 +03:30
88 changed files with 4221 additions and 2494 deletions

41
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Release X-ui dockerhub
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.vscode
tmp tmp
backup/ backup/
bin/ bin/

22
DockerInit.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
#Build latest x-ui from source
FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip
COPY . .
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
#Build app image using latest x-ui
FROM alpine
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add ca-certificates tzdata
COPY --from=builder /app/build/ /app/
VOLUME [ "/etc/x-ui" ]
ENTRYPOINT [ "/app/x-ui" ]

View File

@@ -1,16 +1,20 @@
# 3x-ui # 3x-ui
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#) [![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment** 3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
**If you think this project is helpful to you, you may wish to give a** :star2: **If you think this project is helpful to you, you may wish to give a** :star2:
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)** **Buy Me a Coffee :**
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
# Install & Upgrade # Install & Upgrade
@@ -20,10 +24,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install custom version ## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.3.2`: To install your desired version you can add the version to the end of install command. Example for ver `v1.4.6`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.2 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.6
``` ```
# SSL # SSL
@@ -33,8 +37,8 @@ apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run certbot renew --dry-run
``` ```
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings # Default settings
@@ -45,12 +49,12 @@ or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
Before you set ssl on settings Before you set ssl on settings
- http://ip:2053/xui - http://ip:2053/panel
- http://domain:2053/xui - http://domain:2053/panel
After you set ssl on settings After you set ssl on settings
- https://yourdomain:2053/xui - https://yourdomain:2053/panel
# Environment Variables # Environment Variables
@@ -67,6 +71,31 @@ Example:
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
``` ```
# Install with Docker
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Run 3x-ui:
```sh
docker compose up -d
```
OR
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
# Xray Configurations: # Xray Configurations:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install) **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
@@ -116,6 +145,7 @@ If you want to use routing to WARP follow steps as below:
- For more advanced configuration items, please refer to the panel - For more advanced configuration items, please refer to the panel
- Fix api routes (user setting will create with api) - Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel - Support to change configs by different items provided in panel
- Support export/import database from panel
# Tg robot use # Tg robot use
@@ -157,7 +187,7 @@ Reference syntax:
## API routes ## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login - `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions: - `/panel/api/inbounds` base for following actions:
| Method | Path | Action | | Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- | | :----: | ---------------------------------- | ------------------------------------------- |
@@ -170,13 +200,21 @@ Reference syntax:
| `POST` | `"/clientIps/:email"` | Client Ip address | | `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address | | `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient"` | Add Client to inbound | | `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId | | `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by UID/Password as clientId | | `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic | | `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds | | `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound | | `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) | | `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
\*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
# A Special Thanks To # A Special Thanks To
- [alireza0](https://github.com/alireza0/) - [alireza0](https://github.com/alireza0/)
@@ -188,20 +226,14 @@ Reference syntax:
- CentOS 8+ - CentOS 8+
- Fedora 36+ - Fedora 36+
# Buy Me a Coffee
[![](https://img.shields.io/badge/Wallet-USDT__TRC20-green.svg)](#)
```
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
```
# Pictures # Pictures
![1](./media/1.png) ![1](./media/1.png)
![2](./media/2.png) ![2](./media/2.png)
![3](./media/3.png) ![3](./media/3.png)
![4](./media/4.png) ![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
## Stargazers over time ## Stargazers over time

View File

@@ -1 +1 @@
1.3.2 1.4.6

View File

@@ -1,6 +1,8 @@
package database package database
import ( import (
"bytes"
"io"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@@ -104,3 +106,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound return err == gorm.ErrRecordNotFound
} }
func IsSQLiteDB(file io.Reader) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
---
version: "3.9"
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: yourhostname
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
network_mode: host
restart: unless-stopped

12
go.mod
View File

@@ -13,13 +13,13 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7 github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.3 github.com/shirou/gopsutil/v3 v3.23.4
github.com/xtls/xray-core v1.8.1 github.com/xtls/xray-core v1.8.1
go.uber.org/atomic v1.10.0 go.uber.org/atomic v1.11.0
golang.org/x/text v0.9.0 golang.org/x/text v0.9.0
google.golang.org/grpc v1.54.0 google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.0 gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0 gorm.io/gorm v1.25.1
) )
require ( require (
@@ -30,7 +30,7 @@ require (
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.12.0 // indirect github.com/go-playground/validator/v10 v10.13.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
@@ -39,7 +39,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.3 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect

27
go.sum
View File

@@ -9,8 +9,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -44,8 +42,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
@@ -87,8 +85,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
@@ -136,9 +134,8 @@ github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw= github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE= github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU= github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ= github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ= github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c= github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
@@ -175,8 +172,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -237,8 +234,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
@@ -255,8 +252,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -25,9 +25,9 @@ echo "The OS release is: $release"
arch3xui() { arch3xui() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64 | x64 | amd64 ) echo 'amd64' ;; x86_64 | x64 | amd64) echo 'amd64' ;;
armv8 | arm64 | aarch64 ) echo 'arm64' ;; armv8 | arm64 | aarch64) echo 'arm64' ;;
* ) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac esac
} }
echo "arch: $(arch3xui)" echo "arch: $(arch3xui)"
@@ -39,7 +39,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi fi
@@ -59,21 +59,20 @@ fi
install_base() { install_base() {
case "${release}" in case "${release}" in
centos|fedora) centos | fedora)
yum install -y -q wget curl tar yum install -y -q wget curl tar
;; ;;
*) *)
apt install -y -q wget curl tar apt install -y -q wget curl tar
;; ;;
esac esac
} }
#This function will be called when user installed x-ui out of sercurity #This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username:" config_account read -p "Please set up your username:" config_account
echo -e "${yellow}Your username will be:${config_account}${plain}" echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password read -p "Please set up your password:" config_password
@@ -101,6 +100,7 @@ config_after_install() {
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}" echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi fi
fi fi
/usr/local/x-ui/x-ui migrate
} }
install_x-ui() { install_x-ui() {

View File

@@ -1,8 +1,9 @@
package logger package logger
import ( import (
"github.com/op/go-logging"
"os" "os"
"github.com/op/go-logging"
) )
var logger *logging.Logger var logger *logging.Logger

View File

@@ -212,8 +212,7 @@ func migrateDb() {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println("Start migrating database...") fmt.Println("Start migrating database...")
inboundService.MigrationRequirements() inboundService.MigrateDB()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!") fmt.Println("Migration done!")
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -6,8 +6,6 @@ import (
"x-ui/logger" "x-ui/logger"
) )
var CtxDone = errors.New("context done")
func NewErrorf(format string, a ...interface{}) error { func NewErrorf(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)

View File

@@ -1,9 +0,0 @@
package common
import "sort"
func IsSubString(target string, str_array []string) bool {
sort.Strings(str_array)
index := sort.SearchStrings(str_array, target)
return index < len(str_array) && str_array[index] == target
}

View File

@@ -1,12 +0,0 @@
package util
import "context"
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

View File

@@ -6,7 +6,7 @@ import (
type RawMessage []byte type RawMessage []byte
// MarshalJSON 自定义 json.RawMessage 默认行为 // MarshalJSON: Customize json.RawMessage default behavior
func (m RawMessage) MarshalJSON() ([]byte, error) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil return m, nil
} }
// UnmarshalJSON sets *m to a copy of data. // UnmarshalJSON: sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

View File

File diff suppressed because one or more lines are too long

View File

@@ -31,9 +31,9 @@ small{font-size:80%}
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline} sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}
sub{bottom:-.25em} sub{bottom:-.25em}
sup{top:-.5em} sup{top:-.5em}
a{color:#1890ff;text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects} a{color:rgb(0 150 112);text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects}
a:hover{color:#40a9ff} a:hover{color:rgb(0 150 112)}
a:active{color:#096dd9} a:active{color:rgb(10, 105, 82)}
a:active,a:hover{text-decoration:none;outline:0} a:active,a:hover{text-decoration:none;outline:0}
a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none} a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}
code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace} code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}
@@ -520,8 +520,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-arrow .ant-select-arrow-icon{display:block} .ant-select-arrow .ant-select-arrow-icon{display:block}
.ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s} .ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s}
.ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-select-selection:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-select-selection:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto} .ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto}
.ant-select-selection__clear:before{display:block} .ant-select-selection__clear:before{display:block}
.ant-select-selection__clear:hover{color:rgba(0,0,0,.45)} .ant-select-selection__clear:hover{color:rgba(0,0,0,.45)}
@@ -529,7 +529,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis} .ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0} .ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}
.ant-select-disabled{color:rgba(0,0,0,.25)} .ant-select-disabled{color:rgba(0,0,0,.25)}
.ant-select-disabled .ant-select-selection{background:#f5f5f5;cursor:not-allowed} .ant-select-disabled .ant-select-selection{background:rgb(0 150 112);cursor:not-allowed}
.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none} .ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}
.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none} .ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}
.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5} .ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}
@@ -582,7 +582,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px} .ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px}
.ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px} .ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px}
.ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)} .ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)}
.ant-select-open .ant-select-selection{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-select-open .ant-select-selection{border-color:rgb(0 150 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-combobox .ant-select-arrow{display:none} .ant-select-combobox .ant-select-arrow{display:none}
.ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%} .ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%}
.ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%} .ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%}
@@ -629,8 +629,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-input:-moz-placeholder-shown{text-overflow:ellipsis} .ant-input:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input:-ms-input-placeholder{text-overflow:ellipsis} .ant-input:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input:placeholder-shown{text-overflow:ellipsis} .ant-input:placeholder-shown{text-overflow:ellipsis}
.ant-input:focus,.ant-input:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-input:focus,.ant-input:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important;}
.ant-input:focus{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input:focus{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
.ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
@@ -716,7 +716,7 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px} .ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-btn>a:only-child{color:currentColor} .ant-btn>a:only-child{color:currentColor}
.ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn:focus,.ant-btn:hover{color:#40a9ff;background-color:#fff;border-color:#40a9ff} .ant-btn:focus,.ant-btn:hover{color:rgb(0 150 112);background-color:#fff;border-color:rgb(0 150 112)}
.ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor} .ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor}
.ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9} .ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9}
@@ -727,16 +727,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff} .ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff}
.ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none} .ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none}
.ant-btn-primary{color:#fff;background-color:#1890ff;border-color:#1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)} .ant-btn-primary{color:#fff;background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-primary>a:only-child{color:currentColor} .ant-btn-primary>a:only-child{color:currentColor}
.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:#40a9ff;border-color:#00000017} .ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:rgb(0, 185, 138);border-color:#00000017}
.ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor} .ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor}
.ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:#096dd9;border-color:#096dd9} .ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:rgb(25, 191, 149);border-color:rgb(25, 191, 149)}
.ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor} .ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor}
.ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor} .ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor}
.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff} .ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff}
@@ -769,16 +769,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor} .ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor}
.ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)} .ant-btn-danger{color:#ff4d4f;background-color:rgb(255 77 79 / 0%);border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-danger>a:only-child{color:currentColor} .ant-btn-danger>a:only-child{color:currentColor}
.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff7875;border-color:#ff7875} .ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f}
.ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor} .ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor}
.ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e} .ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e}
.ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor} .ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor}
.ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor} .ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor}
.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;box-shadow:none} .ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;box-shadow:none}
@@ -992,11 +992,11 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)} .ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)}
.ant-menu-item>.ant-badge>a:hover{color:#1890ff} .ant-menu-item>.ant-badge>a:hover{color:#1890ff}
.ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8} .ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8}
.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#fff;background-image:linear-gradient(-20deg,#1a61b3 0,#242d81 100%)} .ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#2d2d2d;background-image: linear-gradient(90deg,#99999980 0,#8888889e 100%);border-radius: 0.5rem}
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px} .ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent} .ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff} .ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background:linear-gradient(-20deg,#412ef0 0,#0a2d58 100%);color:#fff} .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
.ant-menu-vertical-right{border-left:1px solid #e8e8e8} .ant-menu-vertical-right{border-left:1px solid #e8e8e8}
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0} .ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0} .ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
@@ -1032,7 +1032,7 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff} .ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff}
.ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"} .ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"}
.ant-menu-inline .ant-menu-item,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item{position:relative} .ant-menu-inline .ant-menu-item,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item{position:relative}
.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0;/*! border-right:3px solid #1890ff; */transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""} .ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0/*;border-right:3px solid #1890ff*/;transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""}
.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis} .ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis}
.ant-menu-inline .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu{padding-bottom:.02px} .ant-menu-inline .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu{padding-bottom:.02px}
.ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child){margin-bottom:8px} .ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child){margin-bottom:8px}
@@ -1080,8 +1080,9 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-dark .ant-menu-item:hover{background-color:transparent} .ant-menu-dark .ant-menu-item:hover{background-color:transparent}
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0} .ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
.ant-menu-dark .ant-menu-item-selected:after{border-right:0} .ant-menu-dark .ant-menu-item-selected:after{border-right:0}
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#fff} .ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#1890ff} .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8} .ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important} .ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important} .ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
@@ -1235,18 +1236,18 @@ span.ant-radio+*{padding-right:8px;padding-left:8px}
.ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px} .ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px}
.ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0} .ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0}
.ant-radio-button-wrapper:first-child:last-child{border-radius:4px} .ant-radio-button-wrapper:first-child:last-child{border-radius:4px}
.ant-radio-button-wrapper:hover{position:relative;color:#1890ff} .ant-radio-button-wrapper:hover{position:relative;color:#009670}
.ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none} .ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#1890ff;background:#fff;border-color:#1890ff;box-shadow:-1px 0 0 0 #1890ff} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#009670;background:#fff;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#1890ff!important;opacity:.1} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#009670!important;opacity:.1}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#1890ff;box-shadow:none!important} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#009670;box-shadow:none!important}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#40a9ff;border-color:#40a9ff;box-shadow:-1px 0 0 0 #40a9ff} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#009670;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#096dd9;border-color:#096dd9;box-shadow:-1px 0 0 0 #096dd9} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#076e54;border-color:#076e54;box-shadow:-1px 0 0 0 #076e54}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#1890ff;border-color:#1890ff} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#40a9ff;border-color:#40a9ff} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#096dd9;border-color:#096dd9} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#076e54;border-color:#076e54}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper-disabled{cursor:not-allowed} .ant-radio-button-wrapper-disabled{cursor:not-allowed}
.ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9} .ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9}
@@ -1263,11 +1264,11 @@ to{transform:scale(1.6);opacity:0}
@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal){ @supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal){
.ant-radio{vertical-align:text-bottom} .ant-radio{vertical-align:text-bottom}
} }
.ant-card{box-sizing:border-box;margin:auto 5px 3px auto;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;background:#fff;border-radius:2px;transition:all .3s} .ant-card{box-sizing:border-box;margin:auto 10px 3px auto;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;background:#fff;border-radius:2px;transition:all .3s}
.ant-card-hoverable{cursor:pointer} .ant-card-hoverable{cursor:pointer}
.ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)} .ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)}
.ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0);box-shadow:0 3px 7.985px -.985px #9aafee} .ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0)/*;box-shadow:0 1px 7px -1px #0000005c*/}
.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:1px solid #e8e8e8;border-radius:2px 2px 0 0;zoom:1} .ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:2px 2px 0 0;zoom:1}
.ant-card-head:after,.ant-card-head:before{display:table;content:""} .ant-card-head:after,.ant-card-head:before{display:table;content:""}
.ant-card-head:after{clear:both} .ant-card-head:after{clear:both}
.ant-card-head-wrapper{display:flex;align-items:center} .ant-card-head-wrapper{display:flex;align-items:center}
@@ -1356,11 +1357,12 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px} .ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff} .ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff}
.ant-tabs{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;overflow:hidden;zoom:1} .ant-tabs{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;overflow:hidden;zoom:1;border-radius:1.5rem;/*box-shadow:0 1px 7px -1px #0000005c;*/transition:all .3s;background-color: white}
.ant-tabs:hover{box-shadow:0 3px 12px -.8px #0000005c}
.ant-tabs:after,.ant-tabs:before{display:table;content:""} .ant-tabs:after,.ant-tabs:before{display:table;content:""}
.ant-tabs:after{clear:both} .ant-tabs:after{clear:both}
.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:#1890ff;transform-origin:0 0} .ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:rgb(0 150 112);transform-origin:0 0}
.ant-tabs-bar{margin:0 0 16px;border-bottom:1px solid #e8e8e8;outline:0} .ant-tabs-bar{margin:1.5rem 1.5rem 0rem 1.5rem!important;border-bottom:2px solid rgb(153 153 153 / 20%);outline:0}
.ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)} .ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1} .ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1}
.ant-tabs-nav-container:after,.ant-tabs-nav-container:before{display:table;content:""} .ant-tabs-nav-container:after,.ant-tabs-nav-container:before{display:table;content:""}
@@ -1388,10 +1390,10 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)} .ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;transition:all .3s;content:"";pointer-events:none} .ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;transition:all .3s;content:"";pointer-events:none}
.ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0} .ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0}
.ant-tabs-nav .ant-tabs-tab:hover{color:#40a9ff} .ant-tabs-nav .ant-tabs-tab:hover{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab:active{color:#096dd9} .ant-tabs-nav .ant-tabs-tab:active{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px} .ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px}
.ant-tabs-nav .ant-tabs-tab-active{color:#1890ff;font-weight:500} .ant-tabs-nav .ant-tabs-tab-active{color:rgb(0 150 112);font-weight:500}
.ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed} .ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}
.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px} .ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px}
.ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px} .ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px}
@@ -2436,14 +2438,14 @@ to{transform:scale(1.6);opacity:0}
.ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)} .ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)}
.ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d} .ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d}
.ant-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:0;cursor:pointer} .ant-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:0;cursor:pointer}
.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:#1890ff} .ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:rgb(0, 150, 112)}
.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""} .ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid rgb(0, 150, 112);border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}
.ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible} .ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible}
.ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s} .ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}
.ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "} .ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}
.ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0} .ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}
.ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "} .ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}
.ant-checkbox-checked .ant-checkbox-inner{background-color:#1890ff;border-color:#1890ff} .ant-checkbox-checked .ant-checkbox-inner{background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112)}
.ant-checkbox-disabled{cursor:not-allowed} .ant-checkbox-disabled{cursor:not-allowed}
.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none} .ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}
.ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed} .ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed}
@@ -2462,9 +2464,9 @@ to{transform:scale(1.6);opacity:0}
.ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9} .ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9}
.ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "} .ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}
.ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)} .ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}
.ant-collapse{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:4px} .ant-collapse{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"/*;background-color:#fafafa*/;border:1px solid rgba(100, 100, 100, 0.2);/*border-bottom:0;*/border-radius:0.5rem}
.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9} .ant-collapse>.ant-collapse-item{border-bottom:1px solid rgb(217 217 217 / 20%)}
.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 4px 4px} .ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0.5rem}
.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s} .ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)} .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1} .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1}
@@ -2478,7 +2480,7 @@ to{transform:scale(1.6);opacity:0}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px} .ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto} .ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto}
.ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)} .ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)}
.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid #d9d9d9} .ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid rgb(0 150 112)}
.ant-collapse-content>.ant-collapse-content-box{padding:16px} .ant-collapse-content>.ant-collapse-content-box{padding:16px}
.ant-collapse-content-inactive{display:none} .ant-collapse-content-inactive{display:none}
.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px} .ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px}
@@ -2537,7 +2539,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-picker-input{outline:0} .ant-calendar-picker-input{outline:0}
.ant-calendar-picker-input.ant-input{line-height:1.5} .ant-calendar-picker-input.ant-input{line-height:1.5}
.ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0} .ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0}
.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff} .ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:rgb(0 150 112)}
.ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none} .ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none}
@@ -2547,7 +2549,7 @@ to{transform:scale(1.6);opacity:0}
.ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed} .ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed}
.ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px} .ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px}
.ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:0;box-shadow:0 2px 8px rgba(0,0,0,.15)} .ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:0;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid #e8e8e8} .ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto} .ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto}
.ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1} .ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1}
.ant-calendar-input:-ms-input-placeholder{color:#bfbfbf} .ant-calendar-input:-ms-input-placeholder{color:#bfbfbf}
@@ -2557,7 +2559,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-input:placeholder-shown{text-overflow:ellipsis} .ant-calendar-input:placeholder-shown{text-overflow:ellipsis}
.ant-calendar-week-number{width:286px} .ant-calendar-week-number{width:286px}
.ant-calendar-week-number-cell{text-align:center} .ant-calendar-week-number-cell{text-align:center}
.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid rgb(153 153 153 / 25%);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-header a:hover{color:#40a9ff} .ant-calendar-header a:hover{color:#40a9ff}
.ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px} .ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}
.ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none} .ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none}
@@ -2593,8 +2595,8 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:0 0;border:1px solid transparent;border-radius:2px;transition:background .3s ease} .ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:0 0;border:1px solid transparent;border-radius:2px;transition:background .3s ease}
.ant-calendar-date-panel{position:relative;outline:0} .ant-calendar-date-panel{position:relative;outline:0}
.ant-calendar-date:hover{background:#e6f7ff;cursor:pointer} .ant-calendar-date:hover{background:#e6f7ff;cursor:pointer}
.ant-calendar-date:active{color:#fff;background:#40a9ff} .ant-calendar-date:active{color:rgba(0,0,0,.65);background:#bae7ff}
.ant-calendar-today .ant-calendar-date{color:#1890ff;font-weight:700;border-color:#1890ff} .ant-calendar-today .ant-calendar-date{color:rgb(0 150 112);font-weight:700;border-color:rgb(0 150 112)}
.ant-calendar-selected-day .ant-calendar-date{background:#bae7ff} .ant-calendar-selected-day .ant-calendar-date{background:#bae7ff}
.ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:0 0;border-color:transparent} .ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:0 0;border-color:transparent}
.ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed} .ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed}
@@ -2604,7 +2606,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "} .ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "}
.ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px} .ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px}
.ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px} .ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px}
.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid #e8e8e8} .ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-footer:empty{border-top:0} .ant-calendar-footer:empty{border-top:0}
.ant-calendar-footer-btn{display:block;text-align:center} .ant-calendar-footer-btn{display:block;text-align:center}
.ant-calendar-footer-extra{text-align:left} .ant-calendar-footer-extra{text-align:left}
@@ -2614,7 +2616,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px} .ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px}
.ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease} .ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease}
.ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)} .ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)}
.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:#1890ff;border:1px solid #1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px} .ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:rgb(0 150 112);border:1px solid rgb(0 150 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px}
.ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1} .ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1}
.ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0} .ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0}
.ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none} .ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none}
@@ -2625,13 +2627,13 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px} .ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:#40a9ff;border-color:#40a9ff} .ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:rgb(0 150 112);border-color:rgb(0 150 112)}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9} .ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0} .ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0}
@@ -2942,7 +2944,7 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2} .ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}
.ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f} .ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f}
.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911} .ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}
.ant-tag-green{color:#52c41a;background:#f6ffed;border-color:#b7eb8f} .ant-tag-green{color:#19bf95;background:#ebfffa;border-color:#8ce7d0}
.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a} .ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}
.ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff} .ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff}
.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff} .ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}
@@ -2977,8 +2979,8 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-divider{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background:#e8e8e8} .ant-divider{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background:#e8e8e8}
.ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle} .ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle}
.ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0} .ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0}
.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:16px 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0} .ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:0 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0}
.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid #e8e8e8;transform:translateY(50%);content:""} .ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid rgb(0 150 112 / 50%);transform:translateY(50%);content:""}
.ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px} .ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px}
.ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%} .ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%}
.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%} .ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%}
@@ -3214,7 +3216,7 @@ to{transform:scale(1)}
.ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis} .ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input-number:-ms-input-placeholder{text-overflow:ellipsis} .ant-input-number:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input-number:placeholder-shown{text-overflow:ellipsis} .ant-input-number:placeholder-shown{text-overflow:ellipsis}
.ant-input-number:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input-number:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px !important}
.ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}
textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s} textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}
@@ -3228,7 +3230,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block} .ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block}
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none} .ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block} .ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
.ant-input-number-focused,.ant-input-number:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
@@ -3268,7 +3270,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5} .ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5}
.ant-layout-content{flex:auto;min-height:0} .ant-layout-content{flex:auto;min-height:0}
.ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s} .ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s}
.ant-layout-sider-children{height:100%;margin-top:-.1px;padding-top:.1px} .ant-layout-sider-children{height:100%;margin-top:-.1px;padding:0.5rem}
.ant-layout-sider-has-trigger{padding-bottom:48px} .ant-layout-sider-has-trigger{padding-bottom:48px}
.ant-layout-sider-right{order:1} .ant-layout-sider-right{order:1}
.ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s} .ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s}
@@ -3278,7 +3280,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px} .ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px}
.ant-layout-sider-light{background:#fff} .ant-layout-sider-light{background:#fff}
.ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff} .ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff}
.ant-list{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative} .ant-list{box-sizing:border-box;margin:1.5em;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative}
.ant-list *{outline:0} .ant-list *{outline:0}
.ant-list-pagination{margin-top:24px;text-align:right} .ant-list-pagination{margin-top:24px;text-align:right}
.ant-list-pagination .ant-pagination-options{text-align:left} .ant-list-pagination .ant-pagination-options{text-align:left}
@@ -3303,7 +3305,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-list-footer,.ant-list-header{background:0 0} .ant-list-footer,.ant-list-header{background:0 0}
.ant-list-footer,.ant-list-header{padding-top:12px;padding-bottom:12px} .ant-list-footer,.ant-list-header{padding-top:12px;padding-bottom:12px}
.ant-list-empty{padding:16px 0;color:rgba(0,0,0,.45);font-size:12px;text-align:center} .ant-list-empty{padding:16px 0;color:rgba(0,0,0,.45);font-size:12px;text-align:center}
.ant-list-split .ant-list-item{border-bottom:1px solid #e8e8e8} .ant-list-split .ant-list-item{border-bottom:1px solid rgb(153 153 153 / 20%)}
.ant-list-split .ant-list-item:last-child{border-bottom:none} .ant-list-split .ant-list-item:last-child{border-bottom:none}
.ant-list-split .ant-list-header{border-bottom:1px solid #e8e8e8} .ant-list-split .ant-list-header{border-bottom:1px solid #e8e8e8}
.ant-list-loading .ant-list-spin-nested-loading{min-height:32px} .ant-list-loading .ant-list-spin-nested-loading{min-height:32px}
@@ -3530,9 +3532,9 @@ to{max-height:0;padding:0;opacity:0}
.ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:0 0;border:0;outline:0;cursor:pointer;transition:color .3s} .ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:0 0;border:0;outline:0;cursor:pointer;transition:color .3s}
.ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto} .ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto}
.ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none} .ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none}
.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0} .ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid rgba(100, 100, 100, 0.2);border-radius:4px 4px 0 0}
.ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word} .ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}
.ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid rgba(100, 100, 100, 0.2);border-radius:0 0 4px 4px}
.ant-modal-footer button+button{margin-bottom:0;margin-left:8px} .ant-modal-footer button+button{margin-bottom:0;margin-left:8px}
.ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)} .ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)}
@@ -3671,14 +3673,15 @@ to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}
.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px} .ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px}
.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px} .ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}
.ant-progress{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block} .ant-progress{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}
.ant-progress{box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;}
.ant-progress-line{position:relative;width:100%;font-size:14px} .ant-progress-line{position:relative;width:100%;font-size:14px}
.ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px} .ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px}
.ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0} .ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}
.ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)} .ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)}
.ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px} .ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px}
.ant-progress-circle-trail{stroke:#f5f5f5} .ant-progress-circle-trail{stroke:rgb(100 100 100 / 15%) !important}
.ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s} .ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s}
.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#1890ff} .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:rgb(0 150 112) !important}
.ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s} .ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s}
.ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a} .ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a}
.ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal} .ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal}
@@ -3966,7 +3969,7 @@ to{background-position:0 50%}
.ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px} .ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px}
.ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px} .ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px}
.ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)} .ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)}
.ant-switch-checked{background-color:#1890ff} .ant-switch-checked{background-color:rgb(0 150 112)}
.ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px} .ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px}
.ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)} .ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)}
.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px} .ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px}
@@ -3988,7 +3991,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important} .ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important}
.ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0} .ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0}
.ant-table-layout-fixed table{table-layout:fixed} .ant-table-layout-fixed table{table-layout:fixed}
.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:1px solid #e8e8e8;transition:background .3s ease} .ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:2px solid rgba(155, 155, 155, 0.15);transition:background .3s ease}
.ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center} .ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center}
.ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s} .ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s}
.ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px} .ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px}
@@ -4049,7 +4052,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0} .ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0}
.ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8} .ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8}
.ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8} .ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8}
.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:2px solid rgba(155, 155, 155, 0.15);border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:0 0 4px 4px}
.ant-table-pagination.ant-pagination{float:right;margin:16px 0} .ant-table-pagination.ant-pagination{float:right;margin:16px 0}
.ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15)} .ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;box-shadow:none} .ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;box-shadow:none}
@@ -4072,8 +4075,8 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer} .ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer}
.ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)} .ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)}
.ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:0;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:0;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:#40a9ff} .ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active{color:#096dd9} .ant-table-row-expand-icon:active{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor} .ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor}
.ant-table-row-expanded:after{content:"-"} .ant-table-row-expanded:after{content:"-"}
.ant-table-row-collapsed:after{content:"+"} .ant-table-row-collapsed:after{content:"+"}
@@ -4491,4 +4494,4 @@ to{width:0;height:0;margin:0;padding:0;opacity:0}
} }
@keyframes uploadAnimateInlineOut{ @keyframes uploadAnimateInlineOut{
to{width:0;height:0;margin:0;padding:0;opacity:0} to{width:0;height:0;margin:0;padding:0;opacity:0}
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,23 @@
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
}
#app { #app {
height: 100%; height: 100%;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
overflow: auto;
} }
.ant-space { .ant-space {
@@ -10,8 +28,14 @@
display: none; display: none;
} }
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card { .ant-card {
border-radius: 30px; border-radius: 1.5rem;
} }
.ant-card-hoverable { .ant-card-hoverable {
@@ -169,7 +193,7 @@
.ant-layout-sider-zero-width-trigger, .ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu, .ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu { .ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22 background:#1A202B
} }
.ant-card-dark { .ant-card-dark {
@@ -180,9 +204,35 @@
.ant-card-dark:hover { .ant-card-dark:hover {
border-color: #e8e8e8; border-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(255,255,255,.15); box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%);
} }
.ant-setting-textarea {
margin-top: 1.5rem;
min-height: 300px !important;
/*max-height: 800px !important;*/
}
.ant-card-dark-box-nohover{
padding: 0 20px 20px !important;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-box-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
/*background-color: rgb(36 44 58 / 50%);*/
}
.ant-card-dark-securitybox-nohover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-securitybox-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
/* .ant-card-bordered:hover {
box-shadow: 0 3px 12px -0.8px #0000005c;
} */
.ant-card-dark .ant-table-thead th { .ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
@@ -199,6 +249,7 @@
.ant-card-dark .ant-input-group-addon { .ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #262f3d; background-color: #262f3d;
border: 1px solid rgb(149 149 149 / 30%);
} }
.ant-card-dark .ant-list-item-meta-title, .ant-card-dark .ant-list-item-meta-title,
@@ -212,6 +263,7 @@
.ant-card-dark .ant-modal-close, .ant-card-dark .ant-modal-close,
.ant-card-dark i, .ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item, .ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select, .ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select, .ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date, .ant-card-dark .ant-calendar-date,
@@ -223,10 +275,13 @@
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td, .ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled), .ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-select-dropdown-menu-item-active {
background-color: #11314d;
}
.ant-card-dark .ant-calendar-date:hover, .ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected { .ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488; background-color: rgb(4, 119, 90);
} }
.ant-card-dark tbody .ant-table-expanded-row, .ant-card-dark tbody .ant-table-expanded-row,
@@ -235,6 +290,10 @@
background-color: #1a212a; background-color: #1a212a;
} }
.ant-input-number {
min-width: 100px;
}
.ant-card-dark .ant-input, .ant-card-dark .ant-input,
.ant-card-dark .ant-input-number, .ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap, .ant-card-dark .ant-input-number-handler-wrap,
@@ -243,7 +302,17 @@
.ant-card-dark .ant-select-selection, .ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear { .ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #2e3b52; background-color: rgb(46 59 82 / 50%);
border: 1px solid rgb(0 150 112 / 0%);
}
.ant-layout:not(.login) .ant-input:focus,
.ant-layout:not(.login) .ant-input:hover,
.ant-layout:not(.login) .ant-input-number:focus,
.ant-layout:not(.login) .ant-input-number:hover,
.ant-layout:not(.login) .ant-calendar-input:focus,
.ant-layout:not(.login) .ant-calendar-input:hover {
background-color: rgba(0, 149, 111, 0.1);
} }
.ant-card-dark .ant-select-disabled .ant-select-selection { .ant-card-dark .ant-select-disabled .ant-select-selection {
@@ -254,22 +323,30 @@
.ant-card-dark .ant-collapse-item { .ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
border-radius: 0.5rem 0.5rem 0 0;
} }
.ant-dropdown-menu-dark, .ant-dropdown-menu-dark,
.ant-card-dark .ant-modal-content { .ant-card-dark .ant-modal-content {
border: 1px solid rgba(255, 255, 255, 0.65); border:1px solid rgb(100 100 100 / 20%);
box-shadow: 0 2px 8px rgba(255,255,255,.15); box-shadow: 0 2px 8px rgba(255,255,255,.15);
} }
.ant-card-dark .ant-modal-content, .ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body, .ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header, .ant-card-dark .ant-modal-header {
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #222a37; background-color: #222a37;
} }
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: rgb(0 150 112);
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header { .client-table-header {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
@@ -316,9 +393,9 @@
} }
.ant-card-dark .ant-tag-green { .ant-card-dark .ant-tag-green {
color: #6abe39; color: #37b998;
background: #162312; background: #101e1a;
border-color: #274916; border-color: #144237;
} }
.ant-card-dark .ant-tag-cyan { .ant-card-dark .ant-tag-cyan {
@@ -345,7 +422,7 @@
} }
.ant-card-dark .ant-switch-checked { .ant-card-dark .ant-switch-checked {
background-color: #0c61b0; background-color: #009670;
} }
.ant-card-dark .ant-btn, .ant-card-dark .ant-btn,
@@ -356,19 +433,19 @@
} }
.ant-card-dark .ant-radio-button-wrapper:hover { .ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc; color: #009670;
} }
.ant-card-dark .ant-btn-primary { .ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #073763; background-color: rgb(7 98 75 / 50%);
border-color: #1890ff; border-color: #009670;
text-shadow: 0 -1px 0 rgba(255,255,255,.12); text-shadow: 0 -1px 0 rgba(255,255,255,.12);
box-shadow: 0 2px 0 rgba(255,255,255,.045); box-shadow: 0 2px 0 rgba(255,255,255,.045);
} }
.ant-card-dark .ant-btn-primary:hover { .ant-card-dark .ant-btn-primary:hover {
background-color: #40a9ff; background-color: #009670;
border-color: #40a9ff; border-color: #40a9ff00;
} }
.ant-dark .ant-popover-content { .ant-dark .ant-popover-content {
@@ -388,4 +465,21 @@
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow { .ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
border-color: transparent #2e3b52 #2e3b52 transparent; border-color: transparent #2e3b52 #2e3b52 transparent;
} }
::-webkit-scrollbar {
width: 0.7em;
}
::-webkit-scrollbar-track {
background: rgb(50 62 82 / 25%);
}
::-webkit-scrollbar-thumb {
background: rgb(133 133 133 / 80%);
border-radius: 100vw;
}
::-webkit-scrollbar-thumb:hover {
background: #919191;
}

View File

@@ -2,11 +2,15 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use( axios.interceptors.request.use(
config => { (config) => {
config.data = Qs.stringify(config.data, { if (config.data instanceof FormData) {
arrayFormat: 'repeat' config.headers['Content-Type'] = 'multipart/form-data';
}); } else {
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat',
});
}
return config; return config;
}, },
error => Promise.reject(error) (error) => Promise.reject(error),
); );

View File

@@ -1,36 +1,41 @@
supportLangs = [ const supportLangs = [
{ {
name : "English", name: 'English',
value : "en-US", value: 'en-US',
icon : "🇺🇸" icon: '🇺🇸',
}, },
{ {
name : "Farsi", name: 'فارسی',
value : "fa_IR", value: 'fa_IR',
icon : "🇮🇷" icon: '🇮🇷',
}, },
{ {
name : "汉语", name: '汉语',
value : "zh-Hans", value: 'zh-Hans',
icon : "🇨🇳" icon: '🇨🇳',
}, },
] {
name: 'Русский',
value: 'ru_RU',
icon: '🇷🇺',
},
];
function getLang(){ function getLang() {
let lang = getCookie('lang') let lang = getCookie('lang');
if (! lang){ if (!lang) {
if (window.navigator){ if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage; lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)){ if (isSupportLang(lang)) {
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
} }
@@ -38,47 +43,21 @@ function getLang(){
return lang; return lang;
} }
function setLang(lang){ function setLang(lang) {
if (!isSupportLang(lang)) {
if (!isSupportLang(lang)){
lang = 'en-US'; lang = 'en-US';
} }
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
window.location.reload(); window.location.reload();
} }
function isSupportLang(lang){ function isSupportLang(lang) {
for (l of supportLangs){ for (l of supportLangs) {
if (l.value === lang){ if (l.value === lang) {
return true; return true;
} }
} }
return false; return false;
} }
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -17,29 +17,8 @@ const VmessMethods = {
}; };
const SSMethods = { const SSMethods = {
AES_128_GCM: 'aes-128-gcm',
AES_256_GCM: 'aes-256-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const RULE_IP = {
PRIVATE: 'geoip:private',
CN: 'geoip:cn',
};
const RULE_DOMAIN = {
ADS: 'geosite:category-ads',
ADS_ALL: 'geosite:category-ads-all',
CN: 'geosite:cn',
GOOGLE: 'geosite:google',
FACEBOOK: 'geosite:facebook',
SPEEDTEST: 'geosite:speedtest',
}; };
const XTLS_FLOW_CONTROL = { const XTLS_FLOW_CONTROL = {
@@ -93,21 +72,26 @@ const UTLS_FINGERPRINT = {
}; };
const ALPN_OPTION = { const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1", HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
}; };
Object.freeze(Protocols); Object.freeze(Protocols);
Object.freeze(VmessMethods); Object.freeze(VmessMethods);
Object.freeze(SSMethods); Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(XTLS_FLOW_CONTROL); Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL); Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION); Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION); Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION); Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
class XrayCommonClass { class XrayCommonClass {
@@ -456,18 +440,26 @@ class QuicStreamSettings extends XrayCommonClass {
} }
class GrpcStreamSettings extends XrayCommonClass { class GrpcStreamSettings extends XrayCommonClass {
constructor(serviceName="") { constructor(
serviceName="",
multiMode=false
) {
super(); super();
this.serviceName = serviceName; this.serviceName = serviceName;
this.multiMode = multiMode;
} }
static fromJson(json={}) { static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName); return new GrpcStreamSettings(
json.serviceName,
json.multiMode
);
} }
toJson() { toJson() {
return { return {
serviceName: this.serviceName, serviceName: this.serviceName,
multiMode: this.multiMode
} }
} }
} }
@@ -888,7 +880,7 @@ class StreamSettings extends XrayCommonClass {
} }
class Sniffing extends XrayCommonClass { class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls']) { constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
super(); super();
this.enabled = enabled; this.enabled = enabled;
this.destOverride = destOverride; this.destOverride = destOverride;
@@ -898,7 +890,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride); let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) { if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) { if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls']; destOverride = ['http', 'tls', 'quic'];
} }
} }
return new Sniffing( return new Sniffing(
@@ -940,7 +932,7 @@ class Inbound extends XrayCommonClass {
this._protocol = protocol; this._protocol = protocol;
this.settings = Inbound.Settings.getSettings(protocol); this.settings = Inbound.Settings.getSettings(protocol);
if (protocol === Protocols.TROJAN) { if (protocol === Protocols.TROJAN) {
this.tls = true; this.tls = false;
} }
} }
@@ -1013,66 +1005,6 @@ class Inbound extends XrayCommonClass {
return this.network === "http"; return this.network === "http";
} }
// VMess & VLess
get uuid() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].id;
case Protocols.VLESS:
return this.settings.vlesses[0].id;
default:
return "";
}
}
// VLess & Trojan
get flow() {
switch (this.protocol) {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.trojans[0].flow;
default:
return "";
}
}
// VMess
get alterId() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].alterId;
default:
return "";
}
}
// Socks & HTTP
get username() {
switch (this.protocol) {
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].user;
default:
return "";
}
}
// Trojan & Shadowsocks & Socks & HTTP
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.trojans[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].pass;
default:
return "";
}
}
// Shadowsocks // Shadowsocks
get method() { get method() {
switch (this.protocol) { switch (this.protocol) {
@@ -1147,9 +1079,13 @@ class Inbound extends XrayCommonClass {
return this.settings.vlesses[index].expiryTime < new Date().getTime(); return this.settings.vlesses[index].expiryTime < new Date().getTime();
return false return false
case Protocols.TROJAN: case Protocols.TROJAN:
if(this.settings.trojans[index].expiryTime > 0) if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime(); return this.settings.trojans[index].expiryTime < new Date().getTime();
return false return false
case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false
default: default:
return false; return false;
} }
@@ -1228,7 +1164,6 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true; return true;
default: default:
return false; return false;
@@ -1261,50 +1196,6 @@ class Inbound extends XrayCommonClass {
if (this.protocol !== Protocols.VMESS) { if (this.protocol !== Protocols.VMESS) {
return ''; return '';
} }
let network = this.stream.network;
let type = 'none';
let host = '';
let path = '';
if (network === 'tcp') {
let tcp = this.stream.tcp;
type = tcp.type;
if (type === 'http') {
let request = tcp.request;
path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
type = kcp.type;
path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = ws.headers[index].value;
}
} else if (network === 'http') {
network = 'h2';
path = this.stream.http.path;
host = this.stream.http.host.join(',');
} else if (network === 'quic') {
type = this.stream.quic.type;
host = this.stream.quic.security;
path = this.stream.quic.key;
} else if (network === 'grpc') {
path = this.stream.grpc.serviceName;
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
}
let obj = { let obj = {
v: '2', v: '2',
ps: remark, ps: remark,
@@ -1312,16 +1203,66 @@ class Inbound extends XrayCommonClass {
port: this.port, port: this.port,
id: this.settings.vmesses[clientIndex].id, id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId, aid: this.settings.vmesses[clientIndex].alterId,
net: network, net: this.stream.network,
type: type, type: 'none',
host: host,
path: path,
tls: this.stream.security, tls: this.stream.security,
sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings.allowInsecure,
}; };
let network = this.stream.network;
if (network === 'tcp') {
let tcp = this.stream.tcp;
obj.type = tcp.type;
if (tcp.type === 'http') {
let request = tcp.request;
obj.path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
obj.type = kcp.type;
obj.path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
obj.path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = ws.headers[index].value;
}
} else if (network === 'http') {
obj.net = 'h2';
obj.path = this.stream.http.path;
obj.host = this.stream.http.host.join(',');
} else if (network === 'quic') {
obj.type = this.stream.quic.type;
obj.host = this.stream.quic.security;
obj.path = this.stream.quic.key;
} else if (network === 'grpc') {
obj.path = this.stream.grpc.serviceName;
if (this.stream.grpc.multiMode){
obj.type = 'multi'
}
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
obj.add = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
obj.sni = this.stream.tls.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
obj.fp = this.stream.tls.settings.fingerprint;
}
if (this.stream.tls.alpn.length>0){
obj.alpn = this.stream.tls.alpn.join(',');
}
if (this.stream.tls.settings.allowInsecure){
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
}
}
return 'vmess://' + base64(JSON.stringify(obj, null, 2)); return 'vmess://' + base64(JSON.stringify(obj, null, 2));
} }
@@ -1374,6 +1315,9 @@ class Inbound extends XrayCommonClass {
case "grpc": case "grpc":
const grpc = this.stream.grpc; const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName); params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break; break;
} }
@@ -1404,6 +1348,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) { if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server; address = this.stream.xtls.server;
} }
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
@@ -1434,13 +1381,11 @@ class Inbound extends XrayCommonClass {
return url.toString(); return url.toString();
} }
genSSLink(address='', remark='') { genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings; let settings = this.settings;
const server = this.stream.tls.server; const port = this.port;
if (!ObjectUtil.isEmpty(server)) {
address = server; return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
} }
genTrojanLink(address = '', remark = '', clientIndex = 0) { genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1491,6 +1436,9 @@ class Inbound extends XrayCommonClass {
case "grpc": case "grpc":
const grpc = this.stream.grpc; const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName); params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break; break;
} }
@@ -1533,10 +1481,13 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) { if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server; address = this.stream.xtls.server;
} }
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.trojans[clientIndex].flow); params.set("flow", this.settings.trojans[clientIndex].flow);
} }
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
url.searchParams.set(key, value) url.searchParams.set(key, value)
@@ -1557,7 +1508,11 @@ class Inbound extends XrayCommonClass {
remark += '-' + this.settings.vlesses[clientIndex].email remark += '-' + this.settings.vlesses[clientIndex].email
} }
return this.genVLESSLink(address, remark, clientIndex); return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark); case Protocols.SHADOWSOCKS:
if (this.settings.shadowsockses[clientIndex].email != ""){
remark = this.settings.shadowsockses[clientIndex].email
}
return this.genSSLink(address, remark, clientIndex);
case Protocols.TROJAN: case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){ if (this.settings.trojans[clientIndex].email != ""){
remark += '-' + this.settings.trojans[clientIndex].email remark += '-' + this.settings.trojans[clientIndex].email
@@ -2021,13 +1976,15 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings { Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
method=SSMethods.BLAKE3_AES_256_GCM, method=SSMethods.BLAKE3_AES_256_GCM,
password=RandomUtil.randomSeq(44), password=RandomUtil.randomShadowsocksPassword(),
network='tcp,udp' network='tcp,udp',
shadowsockses=[new Inbound.ShadowsocksSettings.Shadowsocks()]
) { ) {
super(protocol); super(protocol);
this.method = method; this.method = method;
this.password = password; this.password = password;
this.network = network; this.network = network;
this.shadowsockses = shadowsockses;
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -2036,6 +1993,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
json.method, json.method,
json.password, json.password,
json.network, json.network,
json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
); );
} }
@@ -2044,10 +2002,77 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
method: this.method, method: this.method,
password: this.password, password: this.password,
network: this.network, network: this.network,
clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses)
}; };
} }
}; };
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.password = password;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
toJson() {
return {
password: this.password,
email: this.email,
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
};
}
static fromJson(json = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks(
json.password,
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
}
};
Inbound.DokodemoSettings = class extends Inbound.Settings { Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp', followRedirect=false) { constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol); super(protocol);

View File

@@ -56,14 +56,37 @@ function toFixed(num, n) {
return Math.round(num * n) / n; return Math.round(num * n) / n;
} }
function debounce (fn, delay) { function debounce(fn, delay) {
var timeoutID = null var timeoutID = null;
return function () { return function () {
clearTimeout(timeoutID) clearTimeout(timeoutID);
var args = arguments var args = arguments;
var that = this var that = this;
timeoutID = setTimeout(function () { timeoutID = setTimeout(function () {
fn.apply(that, args) fn.apply(that, args);
}, delay) }, delay);
};
}
function getCookie(cname) {
let name = cname + '=';
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
} }
} return '';
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
}

View File

@@ -1,67 +1,67 @@
const oneMinute = 1000 * 60; // 一分钟的毫秒数 const oneMinute = 1000 * 60; // MilliseConds in a Minute
const oneHour = oneMinute * 60; // 一小时的毫秒数 const oneHour = oneMinute * 60; // The milliseconds of one hour
const oneDay = oneHour * 24; // 一天的毫秒数 const oneDay = oneHour * 24; // The Number of MilliseConds A Day
const oneWeek = oneDay * 7; // 一星期的毫秒数 const oneWeek = oneDay * 7; // The milliseconds per week
const oneMonth = oneDay * 30; // 一个月的毫秒数 const oneMonth = oneDay * 30; // The milliseconds of a month
/** /**
* 按天数减少 * Decrease according to the number of days
* *
* @param days 要减少的天数 * @param days to reduce the number of days to be reduced
*/ */
Date.prototype.minusDays = function (days) { Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days); return this.minusMillis(oneDay * days);
}; };
/** /**
* 按天数增加 * Increase according to the number of days
* *
* @param days 要增加的天数 * @param days The number of days to be increased
*/ */
Date.prototype.plusDays = function (days) { Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days); return this.plusMillis(oneDay * days);
}; };
/** /**
* 按小时减少 * A few
* *
* @param hours 要减少的小时数 * @param hours to be reduced
*/ */
Date.prototype.minusHours = function (hours) { Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours); return this.minusMillis(oneHour * hours);
}; };
/** /**
* 按小时增加 * Increase hourly
* *
* @param hours 要增加的小时数 * @param hours to increase the number of hours
*/ */
Date.prototype.plusHours = function (hours) { Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours); return this.plusMillis(oneHour * hours);
}; };
/** /**
* 按分钟减少 * Make reduction in minutes
* *
* @param minutes 要减少的分钟数 * @param minutes to reduce the number of minutes
*/ */
Date.prototype.minusMinutes = function (minutes) { Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes); return this.minusMillis(oneMinute * minutes);
}; };
/** /**
* 按分钟增加 * Add in minutes
* *
* @param minutes 要增加的分钟数 * @param minutes to increase the number of minutes
*/ */
Date.prototype.plusMinutes = function (minutes) { Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes); return this.plusMillis(oneMinute * minutes);
}; };
/** /**
* 按毫秒减少 * Decrease in milliseconds
* *
* @param millis 要减少的毫秒数 * @param millis to reduce the milliseconds
*/ */
Date.prototype.minusMillis = function(millis) { Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis; let time = this.getTime() - millis;
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
}; };
/** /**
* 按毫秒增加 * Add in milliseconds to increase
* *
* @param millis 要增加的毫秒数 * @param millis to increase the milliseconds to increase
*/ */
Date.prototype.plusMillis = function(millis) { Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis; let time = this.getTime() + millis;
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
}; };
/** /**
* 设置时间为当天的 00:00:00.000 * Setting time is 00: 00: 00.000 on the day
*/ */
Date.prototype.setMinTime = function () { Date.prototype.setMinTime = function () {
this.setHours(0); this.setHours(0);
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
}; };
/** /**
* 设置时间为当天的 23:59:59.999 * Setting time is 23: 59: 59.999 on the same day
*/ */
Date.prototype.setMaxTime = function () { Date.prototype.setMaxTime = function () {
this.setHours(23); this.setHours(23);
@@ -105,37 +105,36 @@ Date.prototype.setMaxTime = function () {
}; };
/** /**
* 格式化日期 * Formatting date
*/ */
Date.prototype.formatDate = function () { Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
}; };
/** /**
* 格式化时间 * Format time
*/ */
Date.prototype.formatTime = function () { Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
}; };
/** /**
* 格式化日期加时间 * Formatting date plus time
* *
* @param split 日期和时间之间的分隔符,默认是一个空格 * @param split Date and time separation symbols, default is a space
*/ */
Date.prototype.formatDateTime = function (split = ' ') { Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime(); return this.formatDate() + split + this.formatTime();
}; };
class DateUtil { class DateUtil {
// String string to date object
// 字符串转 Date 对象
static parseDate(str) { static parseDate(str) {
return new Date(str.replace(/-/g, '/')); return new Date(str.replace(/-/g, '/'));
} }
static formatMillis(millis) { static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s') return moment(millis).format('YYYY-M-D H:m:s');
} }
static firstDayOfMonth() { static firstDayOfMonth() {
@@ -144,4 +143,4 @@ class DateUtil {
date.setMinTime(); date.setMinTime();
return date; return date;
} }
} }

View File

@@ -68,13 +68,11 @@ class HttpUtil {
} }
class PromiseUtil { class PromiseUtil {
static async sleep(timeout) { static async sleep(timeout) {
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, timeout) setTimeout(resolve, timeout)
}); });
} }
} }
const seq = [ const seq = [
@@ -95,7 +93,6 @@ const shortIdSeq = [
]; ];
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10); return parseInt(Math.random() * (max - min) + min, 10);
} }
@@ -153,8 +150,7 @@ class RandomUtil {
static randomText() { static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';
var len = 6 + Math.floor(Math.random() * 5) for (var ii = 0; ii < 8; ii++) {
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)]; string += chars[Math.floor(Math.random() * chars.length)];
} }
return string; return string;
@@ -162,13 +158,18 @@ class RandomUtil {
static randowShortId() { static randowShortId() {
let str = ''; let str = '';
str += this.randomShortIdSeq(8) str += this.randomShortIdSeq(8);
return str; return str;
} }
static randomShadowsocksPassword() {
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array));
}
} }
class ObjectUtil { class ObjectUtil {
static getPropIgnoreCase(obj, prop) { static getPropIgnoreCase(obj, prop) {
for (const name in obj) { for (const name in obj) {
if (!obj.hasOwnProperty(name)) { if (!obj.hasOwnProperty(name)) {
@@ -316,5 +317,4 @@ class ObjectUtil {
} }
return true; return true;
} }
} }

View File

@@ -14,7 +14,7 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
} }
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui/API/inbounds") g = g.Group("/panel/api/inbounds")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/list", a.getAllInbounds) g.GET("/list", a.getAllInbounds)
@@ -35,21 +35,27 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
} }
func (a *APIController) getAllInbounds(c *gin.Context) { func (a *APIController) getAllInbounds(c *gin.Context) {
a.inboundController.getInbounds(c) a.inboundController.getInbounds(c)
} }
func (a *APIController) getSingleInbound(c *gin.Context) { func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c) a.inboundController.getInbound(c)
} }
func (a *APIController) getClientTraffics(c *gin.Context) { func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c) a.inboundController.getClientTraffics(c)
} }
func (a *APIController) addInbound(c *gin.Context) { func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c) a.inboundController.addInbound(c)
} }
func (a *APIController) delInbound(c *gin.Context) { func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c) a.inboundController.delInbound(c)
} }
func (a *APIController) updateInbound(c *gin.Context) { func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c) a.inboundController.updateInbound(c)
} }
@@ -61,24 +67,31 @@ func (a *APIController) getClientIps(c *gin.Context) {
func (a *APIController) clearClientIps(c *gin.Context) { func (a *APIController) clearClientIps(c *gin.Context) {
a.inboundController.clearClientIps(c) a.inboundController.clearClientIps(c)
} }
func (a *APIController) addInboundClient(c *gin.Context) { func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c) a.inboundController.addInboundClient(c)
} }
func (a *APIController) delInboundClient(c *gin.Context) { func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c) a.inboundController.delInboundClient(c)
} }
func (a *APIController) updateInboundClient(c *gin.Context) { func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c) a.inboundController.updateInboundClient(c)
} }
func (a *APIController) resetClientTraffic(c *gin.Context) { func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c) a.inboundController.resetClientTraffic(c)
} }
func (a *APIController) resetAllTraffics(c *gin.Context) { func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c) a.inboundController.resetAllTraffics(c)
} }
func (a *APIController) resetAllClientTraffics(c *gin.Context) { func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c) a.inboundController.resetAllClientTraffics(c)
} }
func (a *APIController) delDepletedClients(c *gin.Context) { func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c) a.inboundController.delDepletedClients(c)
} }

View File

@@ -1,9 +1,10 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin"
) )
type BaseController struct { type BaseController struct {

View File

@@ -93,7 +93,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err) jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
@@ -101,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Enable = true inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound) inbound, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err) jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -123,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
inbound := &model.Inbound{ inbound := &model.Inbound{
@@ -131,11 +131,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
} }
err = c.ShouldBind(inbound) err = c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
inbound, err = a.inboundService.UpdateInbound(inbound) inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err) jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -156,7 +156,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
err := a.inboundService.ClearClientIps(email) err := a.inboundService.ClearClientIps(email)
if err != nil { if err != nil {
jsonMsg(c, "Revise", err) jsonMsg(c, "Update", err)
return return
} }
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
@@ -165,7 +165,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -183,7 +183,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
func (a *InboundController) delInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
clientId := c.Param("clientId") clientId := c.Param("clientId")
@@ -205,7 +205,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -223,7 +223,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
email := c.Param("email") email := c.Param("email")
@@ -251,7 +251,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
func (a *InboundController) resetAllClientTraffics(c *gin.Context) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -266,7 +266,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
func (a *InboundController) delDepletedClients(c *gin.Context) { func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
err = a.inboundService.DelDepletedClients(id) err = a.inboundService.DelDepletedClients(id)

View File

@@ -39,7 +39,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
func (a *IndexController) index(c *gin.Context) { func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) { if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "xui/") c.Redirect(http.StatusTemporaryRedirect, "panel/")
return return
} }
html(c, "login.html", "pages.login.title", nil) html(c, "login.html", "pages.login.title", nil)
@@ -77,9 +77,11 @@ func (a *IndexController) login(c *gin.Context) {
logger.Infof("Unable to get session's max age from DB") logger.Infof("Unable to get session's max age from DB")
} }
err = session.SetMaxAge(c, sessionMaxAge*60) if sessionMaxAge > 0 {
if err != nil { err = session.SetMaxAge(c, sessionMaxAge*60)
logger.Infof("Unable to set session's max age") if err != nil {
logger.Infof("Unable to set session's max age")
}
} }
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)
@@ -101,5 +103,4 @@ func (a *IndexController) getSecretStatus(c *gin.Context) {
if err == nil { if err == nil {
jsonObj(c, status, nil) jsonObj(c, status, nil)
} }
} }

View File

@@ -41,6 +41,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewX25519Cert", a.getNewX25519Cert)
} }
@@ -99,8 +100,8 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
return return
} }
jsonMsg(c, "Xray stoped", err) jsonMsg(c, "Xray stoped", err)
} }
func (a *ServerController) restartXrayService(c *gin.Context) { func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService() err := a.serverService.RestartXrayService()
if err != nil { if err != nil {
@@ -108,7 +109,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
return return
} }
jsonMsg(c, "Xray restarted", err) jsonMsg(c, "Xray restarted", err)
} }
func (a *ServerController) getLogs(c *gin.Context) { func (a *ServerController) getLogs(c *gin.Context) {
@@ -144,6 +144,28 @@ func (a *ServerController) getDb(c *gin.Context) {
c.Writer.Write(db) c.Writer.Write(db)
} }
func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "Error reading db file", err)
return
}
defer file.Close()
// Always restart Xray before return
defer a.serverService.RestartXrayService()
defer func() {
a.lastGetStatusTime = time.Now()
}()
// Import it
err = a.serverService.ImportDB(file)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, "Import DB", nil)
}
func (a *ServerController) getNewX25519Cert(c *gin.Context) { func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert() cert, err := a.serverService.GetNewX25519Cert()
if err != nil { if err != nil {

View File

@@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
@@ -58,7 +58,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) { func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig() defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, defaultJsonConfig, nil) jsonObj(c, defaultJsonConfig, nil)
@@ -67,22 +67,22 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff() expireDiff, err := a.settingService.GetExpireDiff()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
trafficDiff, err := a.settingService.GetTrafficDiff() trafficDiff, err := a.settingService.GetTrafficDiff()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
defaultCert, err := a.settingService.GetCertFile() defaultCert, err := a.settingService.GetCertFile()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
defaultKey, err := a.settingService.GetKeyFile() defaultKey, err := a.settingService.GetKeyFile()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
result := map[string]interface{}{ result := map[string]interface{}{
@@ -98,27 +98,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting) err := c.ShouldBind(allSetting)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
err = a.settingService.UpdateAllSetting(allSetting) err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
} }
func (a *SettingController) updateUser(c *gin.Context) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword { if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect"))) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
return return
} }
if form.NewUsername == "" || form.NewPassword == "" { if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty"))) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return return
} }
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -127,19 +127,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword user.Password = form.NewPassword
session.SetLoginUser(c, user) session.SetLoginUser(c, user)
} }
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err) jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
} }
func (a *SettingController) updateSecret(c *gin.Context) { func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{} form := &updateSecretForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret) err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
@@ -147,8 +147,9 @@ func (a *SettingController) updateSecret(c *gin.Context) {
user.LoginSecret = form.LoginSecret user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user) session.SetLoginUser(c, user)
} }
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) getUserSecret(c *gin.Context) { func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c) loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id) user := a.userService.GetUserSecret(loginUser.Id)

View File

@@ -18,12 +18,12 @@ func NewXUIController(g *gin.RouterGroup) *XUIController {
} }
func (a *XUIController) initRouter(g *gin.RouterGroup) { func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui") g = g.Group("/panel")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/", a.index) g.GET("/", a.index)
g.GET("/inbounds", a.inbounds) g.GET("/inbounds", a.inbounds)
g.GET("/setting", a.setting) g.GET("/settings", a.settings)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil) html(c, "inbounds.html", "pages.inbounds.title", nil)
} }
func (a *XUIController) setting(c *gin.Context) { func (a *XUIController) settings(c *gin.Context) {
html(c, "setting.html", "pages.setting.title", nil) html(c, "settings.html", "pages.settings.title", nil)
} }

View File

@@ -2,8 +2,9 @@ package global
import ( import (
"context" "context"
"github.com/robfig/cron/v3"
_ "unsafe" _ "unsafe"
"github.com/robfig/cron/v3"
) )
var webServer WebServer var webServer WebServer

View File

@@ -7,7 +7,8 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css"> <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css"> <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"> <link rel=icon type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style> <style>
[v-cloak] { [v-cloak] {
display: none; display: none;

View File

@@ -1,7 +1,7 @@
{{define "promptModal"}} {{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false" :closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type" <a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value" v-model="promptModal.value"
@@ -36,12 +36,12 @@
}, },
confirm() {}, confirm() {},
open({ open({
title='', title = '',
type='text', type = 'text',
value='', value = '',
okText='{{ i18n "sure"}}', okText = '{{ i18n "sure"}}',
confirm=() => {}, confirm = () => {},
}) { }) {
this.title = title; this.title = title;
this.type = type; this.type = type;
this.value = value; this.value = value;

View File

@@ -1,11 +1,16 @@
{{define "qrcodeModal"}} {{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" :closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="300px"> width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag> <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas> {{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag>
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%; margin-top: 10px;"></canvas>
</a-modal> </a-modal>
<script> <script>
@@ -16,14 +21,16 @@
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
copyText: '', copyText: '',
clientName: null,
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') { show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clientName = clientName;
if (ObjectUtil.isEmpty(copyText)) { if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content; this.copyText = content;
} else { } else {
@@ -48,6 +55,7 @@
}; };
const qrModalApp = new Vue({ const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal', el: '#qrcode-modal',
data: { data: {
qrModal: qrModal, qrModal: qrModal,

View File

@@ -1,10 +1,11 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName"> :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]] {{ i18n "download" }} [[ txtModal.fileName ]]
</a-button> </a-button>
<a-input type="textarea" v-model="txtModal.content" <a-input type="textarea" v-model="txtModal.content"
@@ -20,7 +21,7 @@
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', fileName='') { show: function (title = '', content = '', fileName = '') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.fileName = fileName; this.fileName = fileName;

View File

@@ -18,6 +18,12 @@
border-radius: 30px; border-radius: 30px;
} }
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix { .ant-input-affix-wrapper .ant-input-prefix {
left: 23px; left: 23px;
} }
@@ -26,20 +32,26 @@
padding-left: 50px; padding-left: 50px;
} }
.selectLang{ .centered {
display: flex; display: flex;
text-align: center; text-align: center;
align-items: center;
justify-content: center; justify-content: center;
} }
.title {
font-size: 32px;
font-weight: bold;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak class="login" :class="themeSwitcher.darkCardClass">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content> <a-layout-content>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>{{ i18n "pages.login.title" }}</h1> <h1 class="title">{{ i18n "pages.login.title" }}</h1>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
@@ -48,35 +60,33 @@
<a-form-item> <a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}' <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus> @keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/> <a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-input type="password" v-model.trim="user.password" <password-input icon="lock" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/> </password-input>
</a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="secretEnable"> <a-form-item v-if="secretEnable">
<a-input type="text" placeholder='{{ i18n "secretToken" }}' v-model.trim="user.loginSecret" @keydown.enter.native="login"> <password-input icon="key" v-model.trim="user.loginSecret"
<a-icon slot="prefix" type="key" style="color: rgba(0,0,0,.25)"/> placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
</password-input>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button> <a-row justify="center" class="centered">
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</a-row>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered">
<a-row justify="center" class="selectLang"> <a-col :span="12">
<a-col :span="5"><span>Language :</span></a-col> <a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<a-col :span="7">
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
>
<a-select-option :value="l.value" label="English" v-for="l in supportLangs" >
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
@@ -84,6 +94,11 @@
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch />
</a-row>
</a-form-item>
</a-form> </a-form>
</a-col> </a-col>
</a-row> </a-row>
@@ -91,24 +106,24 @@
</transition> </transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
<script> <script>
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = background;
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
themeSwitcher,
loading: false, loading: false,
user: new User(), user: new User(),
secretEnable: false, secretEnable: false,
lang : "" lang: ""
}, },
created(){ async created() {
this.lang = getLang(); this.updateBackground();
this.secretEnable = this.getSecretStatus(); this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
}, },
methods: { methods: {
async login() { async login() {
@@ -116,20 +131,33 @@
const msg = await HttpUtil.post('/login', this.user); const msg = await HttpUtil.post('/login', this.user);
this.loading = false; this.loading = false;
if (msg.success) { if (msg.success) {
location.href = basePath + 'xui/'; location.href = basePath + 'panel/';
} }
}, },
async getSecretStatus() { async getSecretStatus() {
this.loading= true; this.loading = true;
const msg = await HttpUtil.post('/getSecretStatus'); const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false; this.loading = false;
if (msg.success){ if (msg.success) {
this.secretEnable = msg.obj; this.secretEnable = msg.obj;
return msg.obj; return msg.obj;
} }
} },
} updateBackground() {
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = this.themeSwitcher.isDarkTheme ? colors.dark.bg : background;
},
},
watch: {
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
this.updateBackground();
},
},
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
{{define "clientsBulkModal"}} {{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok" <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'> <a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option> <a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option>
@@ -33,6 +33,30 @@
<span slot="label">{{ i18n "pages.client.clientCount" }}</span> <span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number> <a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item>
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span> <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
@@ -43,29 +67,24 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="clientsBulkModal.limitIp" min="0" style="width: 70px;" ></a-input> <a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow"> <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline"> <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Subscription">
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram ID">
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -75,15 +94,17 @@
</span> </span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number> <a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch> <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart"> <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input> <a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -91,8 +112,8 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker> v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -122,37 +143,42 @@
delayedStart: false, delayedStart: false,
ok() { ok() {
clients = []; clients = [];
method=clientsBulkModal.emailMethod; method = clientsBulkModal.emailMethod;
if(method>1){ if (method > 1) {
start=clientsBulkModal.firstNum; start = clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1; end = clientsBulkModal.lastNum + 1;
} else { } else {
start=0; start = 0;
end=clientsBulkModal.quantity; end = clientsBulkModal.quantity;
} }
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : ""; prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1); useNum = (method > 1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : ""; postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol); newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==4) newClient.email = ""; if (method == 4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix; newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId; newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId; newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp; newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB; newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime; newClient._expiryTime = clientsBulkModal.expiryTime;
if(clientsBulkModal.inbound.canEnableTlsFlow()){ if (clientsBulkModal.inbound.canEnableTlsFlow()) {
newClient.flow = clientsBulkModal.flow; newClient.flow = clientsBulkModal.flow;
} }
if(clientsBulkModal.inbound.xtls){ if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow; newClient.flow = clientsBulkModal.flow;
} }
clients.push(newClient); clients.push(newClient);
} }
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id); ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
}, },
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) { show({
title = '',
okText = '{{ i18n "sure" }}',
dbInbound = null,
confirm = (inbound, dbInbound) => { }
}) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
@@ -160,21 +186,21 @@
this.quantity = 1; this.quantity = 1;
this.totalGB = 0; this.totalGB = 0;
this.expiryTime = 0; this.expiryTime = 0;
this.emailMethod= 0; this.emailMethod = 0;
this.limitIp= 0; this.limitIp = 0;
this.firstNum= 1; this.firstNum = 1;
this.lastNum= 1; this.lastNum = 1;
this.emailPrefix= ""; this.emailPrefix = "";
this.emailPostfix= ""; this.emailPostfix = "";
this.subId= ""; this.subId = "";
this.tgId= ""; this.tgId = "";
this.flow= ""; this.flow = "";
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.delayedStart = false; this.delayedStart = false;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -209,10 +235,11 @@
get delayedExpireDays() { get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0; return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.clientsBulkModal.expiryTime = -86400000 * days; this.clientsBulkModal.expiryTime = -86400000 * days;
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "clientsModal"}} {{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok" <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
@@ -23,13 +23,13 @@
isExpired: false, isExpired: false,
delayedStart: false, delayedStart: false,
ok() { ok() {
if(clientModal.isEdit){ if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else { } else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
} }
}, },
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) { show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
@@ -40,11 +40,11 @@
this.index = index === null ? this.clients.length : index; this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false; this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false; this.delayedStart = false;
if (isEdit){ if (isEdit) {
if (this.clients[index].expiryTime < 0){ if (this.clients[index].expiryTime < 0) {
this.delayedStart = true; this.delayedStart = true;
} }
this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id; this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else { } else {
this.addClient(this.inbound.protocol, this.clients); this.addClient(this.inbound.protocol, this.clients);
} }
@@ -52,18 +52,27 @@
this.confirm = confirm; this.confirm = confirm;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null; default: return null;
} }
}, },
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
addClient(protocol, clients) { addClient(protocol, clients) {
switch (protocol) { switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess()); case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan()); case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
default: return null; default: return null;
} }
}, },
@@ -94,39 +103,30 @@
return this.clientModal.isEdit; return this.clientModal.isEdit;
}, },
get isTrafficExhausted() { get isTrafficExhausted() {
if(!clientStats) return false if (!clientStats) return false
if(clientStats.total <= 0) return false if (clientStats.total <= 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false if (clientStats.up + clientStats.down < clientStats.total) return false
return true return true
}, },
get isExpiry() { get isExpiry() {
return this.clientModal.isExpired return this.clientModal.isExpired
}, },
get statsColor() { get statsColor() {
if(!clientStats) return 'blue' if (!clientStats) return 'blue'
if(clientStats.total <= 0) return 'blue' if (clientStats.total <= 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan' else if (clientStats.total > 0 && (clientStats.down + clientStats.up) < clientStats.total) return 'cyan'
else return 'red' else return 'red'
}, },
get delayedExpireDays() { get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0; return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
}, },
}, },
methods: { methods: {
getNewEmail(client) { async getDBClientIps(email, event) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email);
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -140,22 +140,22 @@
} }
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email); const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email);
if (!msg.success) { if (!msg.success) {
return; return;
} }
document.getElementById("clientIPs").value = "" document.getElementById("clientIPs").value = ""
}, },
resetClientTraffic(email,dbInboundId,iconElement) { resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email); const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;
@@ -166,5 +166,6 @@
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,17 +1,17 @@
{{define "menuItems"}} {{define "menuItems"}}
<a-menu-item key="{{ .base_path }}xui/"> <a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon> <a-icon type="dashboard"></a-icon>
<span>{{ i18n "menu.dashboard"}}</span> <span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/inbounds"> <a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon> <a-icon type="user"></a-icon>
<span>{{ i18n "menu.inbounds"}}</span> <span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting"> <a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon> <a-icon type="setting"></a-icon>
<span>{{ i18n "menu.setting"}}</span> <span>{{ i18n "menu.settings"}}</span>
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">--> <!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>--> <!-- <span>Client</span>-->
<!--</a-menu-item>--> <!--</a-menu-item>-->
@@ -23,17 +23,14 @@
{{define "commonSider"}} {{define "commonSider"}}
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0"> <a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
@@ -41,32 +38,25 @@
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''" :wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-style="{ padding: 0 }"> :wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div> </div>
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-drawer> </a-drawer>
<script> <script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() { show() {
this.visible = true; this.visible = true;
}, },
@@ -76,17 +66,6 @@
change() { change() {
this.visible = !this.visible; this.visible = !this.visible;
}, },
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
}; };
</script> </script>
{{end}} {{end}}

View File

@@ -0,0 +1,35 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
</a-input>
</template>
{{end}}
{{define "component/password"}}
<script>
Vue.component('password-input', {
props: ["title", "value", "placeholder", "icon"],
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -1,6 +1,13 @@
{{define "component/settingListItem"}} {{define "component/settingListItem"}}
<a-list-item style="padding: 20px"> <a-list-item style="padding: 20px">
<a-row> <a-row v-if="type === 'textarea'">
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10 }"></a-textarea>
<!--a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 30 }"></a-textarea-->
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/> <a-list-item-meta :title="title" :description="desc"/>
</a-col> </a-col>
@@ -9,10 +16,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input> <a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template> </template>
<template v-else-if="type === 'number'"> <template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)" :min="min"></a-input> <a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
</template> </template>
<template v-else-if="type === 'switch'"> <template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch> <a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
@@ -29,4 +33,4 @@
template: `{{template "component/settingListItem"}}`, template: `{{template "component/settingListItem"}}`,
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -0,0 +1,58 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-switch :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="themeSwitcher.toggleTheme()">
</a-switch>
</template>
{{end}}
{{define "component/themeSwitcher"}}
<script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light';
return {
isDarkTheme,
bgStyle: `background: ${colors[theme].bg};`,
textStyle: `color: ${colors[theme].text};`,
darkClass: isDarkTheme ? 'ant-dark' : '',
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
localStorage.setItem('dark-mode', this.isDarkTheme);
this.bgStyle = `background: ${colors[this.theme].bg};`;
this.textStyle = `color: ${colors[this.theme].text};`;
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
},
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({ themeSwitcher }),
});
</script>
{{end}}

View File

@@ -1,36 +1,62 @@
{{define "form/client"}} {{define "form/client"}}
<a-form layout="inline" v-if="client"> <a-form layout="inline" v-if="client">
<template v-if="isEdit"> <template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag> <a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">
Account is (Expired|Traffic Ended) And Disabled
</a-tag>
</template> </template>
<a-form-item> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
</a-form-item>
<a-form-item label="{{ i18n "pages.inbounds.enable" }}">
<a-switch v-model="client.enable"></a-switch> <a-switch v-model="client.enable"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN"> <br>
<a-input v-model.trim="client.password" style="width: 150px;" ></a-input> <a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input-number v-model="client.alterId"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"> <a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS"> <a-form-item v-if="client.email">
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input> <span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Subscription" v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId"></a-input> <span slot="label">
</a-form-item> Telegram ID
<a-form-item label="Telegram Username" v-if="client.email"> <a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input> <a-input v-model.trim="client.tgId"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -43,7 +69,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input> <a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit"> <a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label"> <span slot="label">
@@ -64,25 +90,29 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-form layout="block"> <a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }"> <a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email,$event)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea> </a-textarea>
</a-form> </a-form>
</a-form-item> </a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline"> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -90,9 +120,10 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats"> <template v-if="isEdit && clientStats">
<span>{{ i18n "usage" }}:</span> <br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="statsColor"> <a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] / [[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]] [[ sizeFormat(clientStats.down) ]]
@@ -100,19 +131,22 @@
</a-tag> </a-tag>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon> <a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch> <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart"> <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input> <a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -120,8 +154,8 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag> <a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item> </a-form-item>

View File

@@ -8,7 +8,7 @@
<a-switch v-model="dbInbound.enable"></a-switch> <a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -24,12 +24,14 @@
</span> </span>
<a-input v-model.trim="inbound.listen"></a-input> <a-input v-model.trim="inbound.listen"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input type="number" v-model.number="inbound.port"></a-input> <a-input-number v-model="inbound.port"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -41,7 +43,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -49,8 +51,8 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -4,15 +4,17 @@
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input type="number" v-model.number="inbound.settings.port"></a-input> <a-input-number v-model="inbound.settings.port"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp,udp">TCP+UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">UDP</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<br>
<a-form-item label="FollowRedirect"> <a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch> <a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item> </a-form-item>

View File

@@ -1,7 +1,113 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -9,7 +115,7 @@
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input> <a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp,udp">TCP+UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option> <a-select-option value="udp">UDP</a-select-option>

View File

@@ -1,10 +1,10 @@
{{define "form/socks"}} {{define "form/socks"}}
<a-form layout="inline"> <a-form layout="inline">
<!-- <a-form-item label="Password authentication">-->
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'" <a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch> @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item> </a-form-item>
<br>
<template v-if="inbound.settings.auth === 'password'"> <template v-if="inbound.settings.auth === 'password'">
<a-form-item label='{{ i18n "username" }}'> <a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input> <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
@@ -13,11 +13,11 @@
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input> <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<br>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.settings.udp" <a-form-item v-if="inbound.settings.udp" label="IP">
label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input> <a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -1,31 +1,48 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline"> <a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
</a-form> <a-form-item label="Password">
<a-form-item label="Password"> <a-input v-model.trim="client.password" style="width: 150px;"></a-input>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input> </a-form-item>
</a-form-item> <a-form-item v-if="client.email">
<a-form-item label="Subscription" v-if="client.email"> <span slot="label">
<a-input v-model.trim="client.subId"></a-input> Subscription
</a-form-item> <a-tooltip>
<a-form-item label="Telegram Username" v-if="client.email"> <template slot="title">
<a-input v-model.trim="client.tgId"></a-input> <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</a-form-item> </template>
<a-form-item> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span> <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip> <a-tooltip>
@@ -35,60 +52,69 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input> <a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow"> <br>
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
</a-select> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-form-item> </a-select>
<a-form-item> </a-form-item>
<span slot="label"> <a-form-item>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span slot="label">
<a-tooltip> <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<template slot="title"> <a-tooltip>
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> <template slot="title">
</template> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
<a-icon type="question-circle" theme="filled"></a-icon> </template>
</a-tooltip> <a-icon type="question-circle" theme="filled"></a-icon>
</span> </a-tooltip>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> </span>
</a-form-item> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<a-form-item> </a-form-item>
<span slot="label"> <br>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-tooltip> <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
<template slot="title"> </a-form-item>
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <br>
</template> <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-icon type="question-circle" theme="filled"></a-icon> <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-tooltip> </a-form-item>
</span> <a-form-item v-else>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <span slot="label">
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" <span>{{ i18n "pages.inbounds.expireDate" }}</span>
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> <a-tooltip>
</a-form-item> <template slot="title">
</a-collapse-panel> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</a-collapse> </template>
<a-collapse v-else> <a-icon type="question-circle" theme="filled"></a-icon>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length"> </a-tooltip>
<table width="100%"> </span>
<tr class="client-table-header"> <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th> :dropdown-class-name="themeSwitcher.darkCardClass"
</tr> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> </a-form-item>
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td> </a-collapse-panel>
</tr> </a-collapse>
</table> <a-collapse v-else>
</a-collapse-panel> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
</a-collapse> <table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp"> <template v-if="inbound.isTcp">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" <a-button type="primary" size="small" @click="inbound.settings.addTrojanFallback()">
@click="inbound.settings.addTrojanFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -115,7 +141,7 @@
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="xVer"> <a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input> <a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>

View File

@@ -1,106 +1,133 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline"> <a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
</a-form> <a-form-item label="ID">
<a-form-item label="ID"> <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;" ></a-input> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Subscription" v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId"></a-input> <span slot="label">
</a-form-item> Subscription
<a-form-item label="Telegram Username" v-if="client.email"> <a-tooltip>
<a-input v-model.trim="client.tgId"></a-input> <template slot="title">
</a-form-item> <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
<a-form-item> </template>
<span slot="label"> <a-icon type="question-circle" theme="filled"></a-icon>
<span>{{ i18n "pages.inbounds.IPLimit" }}</span> </a-tooltip>
<a-tooltip> </span>
<template slot="title"> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</template> </a-form-item>
<a-icon type="question-circle" theme="filled"></a-icon> <a-form-item v-if="client.email">
</a-tooltip> <span slot="label">
</span> Telegram ID
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input> <a-tooltip>
</a-form-item> <template slot="title">
<a-form-item v-if="inbound.xtls" label="Flow"> <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> </template>
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-icon type="question-circle" theme="filled"></a-icon>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-tooltip>
</a-select> </span>
</a-form-item> <a-input v-model.trim="client.tgId"></a-input>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline"> </a-form-item>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-form-item>
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <span slot="label">
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
</a-select> <a-tooltip>
</a-form-item> <template slot="title">
<a-form-item> <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
<span slot="label"> </template>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <a-icon type="question-circle" theme="filled"></a-icon>
<a-tooltip> </a-tooltip>
<template slot="title"> </span>
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> <a-input-number v-model="client.limitIp" min="0"></a-input-number>
</template> </a-form-item>
<a-icon type="question-circle" theme="filled"></a-icon> <br>
</a-tooltip> <a-form-item v-if="inbound.xtls" label="Flow">
</span> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
</a-form-item> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
<a-form-item> </a-select>
<span slot="label"> </a-form-item>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-tooltip> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<template slot="title"> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</template> </a-select>
<a-icon type="question-circle" theme="filled"></a-icon> </a-form-item>
</a-tooltip> <a-form-item>
</span> <span slot="label">
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" <a-tooltip>
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> <template slot="title">
</a-form-item> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</a-collapse-panel> </template>
</a-collapse> <a-icon type="question-circle" theme="filled"></a-icon>
<a-collapse v-else> </a-tooltip>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length"> </span>
<table width="100%"> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<tr class="client-table-header"> </a-form-item>
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th> <br>
</tr> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td> </a-form-item>
</tr> <br>
</table> <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
</a-collapse-panel> <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-collapse> </a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp"> <template v-if="inbound.isTcp">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" <a-button type="primary" size="small" @click="inbound.settings.addFallback()">
@click="inbound.settings.addFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider> <a-divider>
@@ -121,7 +148,7 @@
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="xVer"> <a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input> <a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>

View File

@@ -1,34 +1,54 @@
{{define "form/vmess"}} {{define "form/vmess"}}
<a-form layout="inline"> <a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
</a-form> <br>
<a-form-item label="ID"> <a-form-item label='{{ i18n "additional" }} ID'>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-input-number v-model="client.alterId"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'> <br>
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input> <a-form-item label="ID">
</a-form-item> <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-form-item label="Subscription" v-if="client.email"> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
<a-input v-model.trim="client.subId"></a-input> </a-form-item>
</a-form-item> <a-form-item v-if="client.email">
<a-form-item label="Telegram Username" v-if="client.email"> <span slot="label">
<a-input v-model.trim="client.tgId"></a-input> Subscription
</a-form-item> <a-tooltip>
<a-form-item> <template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span> <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip> <a-tooltip>
@@ -38,52 +58,61 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;"></a-input> <a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <br>
<span slot="label"> <a-form-item>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span slot="label">
<a-tooltip> <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<template slot="title"> <a-tooltip>
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> <template slot="title">
</template> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
<a-icon type="question-circle" theme="filled"></a-icon> </template>
</a-tooltip> <a-icon type="question-circle" theme="filled"></a-icon>
</span> </a-tooltip>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> </span>
</a-form-item> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<a-form-item> </a-form-item>
<span slot="label"> <br>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-tooltip> <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
<template slot="title"> </a-form-item>
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <br>
</template> <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-icon type="question-circle" theme="filled"></a-icon> <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-tooltip> </a-form-item>
</span> <a-form-item v-else>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <span slot="label">
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" <span>{{ i18n "pages.inbounds.expireDate" }}</span>
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> <a-tooltip>
</a-form-item> <template slot="title">
</a-collapse-panel> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</a-collapse> </template>
<a-collapse v-else> <a-icon type="question-circle" theme="filled"></a-icon>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length"> </a-tooltip>
<table width="100%"> </span>
<tr class="client-table-header"> <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th> :dropdown-class-name="themeSwitcher.darkCardClass"
</tr> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> </a-form-item>
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td> </a-collapse-panel>
</tr> </a-collapse>
</table> <a-collapse v-else>
</a-collapse-panel> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
</a-collapse> <table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch> <a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,16 +1,21 @@
{{define "form/sniffing"}} {{define "form/sniffing"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
Sniffing Sniffing
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span> <span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch> <a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item> </a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -3,5 +3,8 @@
<a-form-item label="ServiceName"> <a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input> <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,38 +1,46 @@
{{define "form/streamKCP"}} {{define "form/streamKCP"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;"> <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">NoneNot Camouflage</a-select-option> <a-select-option value="none">None (Not Camouflage)</a-select-option>
<a-select-option value="srtp">SRTPCamouflage Video Call</a-select-option> <a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTPCamouflage BT Download</a-select-option> <a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>
<a-select-option value="wechat-video">Wechat-VideoCamouflage WeChat Video</a-select-option> <a-select-option value="wechat-video">Wechat-Video (Camouflage WeChat Video)</a-select-option>
<a-select-option value="dtls">DTLSCamouflage DTLS 1.2 Packages</a-select-option> <a-select-option value="dtls">DTLS (Camouflage DTLS 1.2 Packages)</a-select-option>
<a-select-option value="wireguard">WireguardCamouflage Wireguard Packages</a-select-option> <a-select-option value="wireguard">Wireguard (Camouflage Wireguard Packages)</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<br>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model.number="inbound.stream.kcp.seed"></a-input> <a-input v-model="inbound.stream.kcp.seed"></a-input>
</a-form-item> </a-form-item>
<br>
<a-form-item label="MTU"> <a-form-item label="MTU">
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input> <a-input-number v-model="inbound.stream.kcp.mtu"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label="TTI (ms)"> <a-form-item label="TTI (ms)">
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input> <a-input-number v-model="inbound.stream.kcp.tti"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Uplink Capacity (MB/S)"> <a-form-item label="Uplink Capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input> <a-input-number v-model="inbound.stream.kcp.upCap"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Downlink Capacity (MB/S)"> <a-form-item label="Downlink Capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input> <a-input-number v-model="inbound.stream.kcp.downCap"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Congestion"> <a-form-item label="Congestion">
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch> <a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Read Buffer Size (MB)"> <a-form-item label="Read Buffer Size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input> <a-input-number v-model="inbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item> </a-form-item>
<br>
<a-form-item label="Write Buffer Size (MB)"> <a-form-item label="Write Buffer Size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input> <a-input-number v-model="inbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}} {{define "form/streamQUIC"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none</a-select-option> <a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -11,13 +11,13 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input> <a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">nonenot camouflage</a-select-option> <a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option> <a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option> <a-select-option value="utp">utp (camouflage BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option> <a-select-option value="wechat-video">wechat-video (camouflage WeChat video)</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option> <a-select-option value="dtls">dtls (camouflage DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option> <a-select-option value="wireguard">wireguard (camouflage wireguard packages)</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -2,7 +2,7 @@
<!-- select stream network --> <!-- select stream network -->
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option> <a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WS</a-select-option> <a-select-option value="ws">WS</a-select-option>

View File

@@ -4,7 +4,7 @@
<a-form-item label="AcceptProxyProtocol"> <a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="HTTP {{ i18n "camouflage" }}"> <a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch <a-switch
:checked="inbound.stream.tcp.type === 'http'" :checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"> @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
@@ -13,8 +13,7 @@
</a-form> </a-form>
<!-- tcp request --> <!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'" <a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item> </a-form-item>
@@ -28,8 +27,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -39,8 +37,7 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.request.removeHeader(index)">
@click="inbound.stream.tcp.request.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>
@@ -50,8 +47,7 @@
</a-form> </a-form>
<!-- tcp response --> <!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'" <a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item> </a-form-item>
@@ -63,8 +59,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -74,8 +69,7 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">
@click="inbound.stream.tcp.response.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>

View File

@@ -10,8 +10,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">
@click="inbound.stream.ws.addHeader('Host', '')">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -21,8 +20,7 @@
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.ws.removeHeader(index)">
@click="inbound.stream.ws.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>

View File

@@ -10,7 +10,7 @@
Reality Reality
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.Realitydec" }}</span> <span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
@@ -22,7 +22,7 @@
XTLS XTLS
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.XTLSdec" }}</span> <span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
@@ -37,18 +37,18 @@
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">auto</a-select-option> <a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MinVersion"> <a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MaxVersion"> <a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -56,14 +56,15 @@
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px"> <a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value=''>None</a-select-option> <a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Alpn"> <a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px"> <a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox> <a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
@@ -82,7 +83,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input> <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -99,6 +100,9 @@
<a-form-item label='{{ i18n "domainName" }}'> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input> <a-input v-model.trim="inbound.stream.xtls.server"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="Alpn"> <a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px"> <a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox> <a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
@@ -120,7 +124,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input> <a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <a-button type="primary" icon="import" @click="setDefaultCertXtls">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -139,10 +143,11 @@
</a-switch> </a-switch>
</a-form-item> </a-form-item>
<a-form-item label="xVer"> <a-form-item label="xVer">
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input> <a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="uTLS" > <a-form-item label="uTLS">
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px"> <a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -164,7 +169,7 @@
<a-form-item label="Public Key"> <a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input> <a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item > <a-form-item>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button> <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -29,10 +29,12 @@
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag> <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template> </template>
<template slot="traffic" slot-scope="text, client"> <template slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag> <a-tag color="blue">
[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]
</a-tag>
<template v-if="client._totalGB > 0"> <template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag> <a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]] GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag> <a-tag v-else color="cyan">[[client._totalGB]] GB</a-tag>
</template> </template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template> </template>
@@ -42,7 +44,9 @@
[[ DateUtil.formatMillis(client._expiryTime) ]] [[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag> </a-tag>
</template> </template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag> <a-tag v-else-if="client.expiryTime < 0" color="cyan">
[[ client._expiryTime ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template> </template>
{{end}} {{end}}

View File

@@ -3,61 +3,66 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true" :closable="true"
:mask-closable="true" :mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="600px" width="600px"
> >
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr><td> <tr>
<table> <td>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr> <table>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr> <tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr> <tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
</table> <tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</td> </table>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> </td>
<table> <td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<tr> <table>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td> <tr>
</tr> <td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2"> </tr>
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr> <template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr> <tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</template> <tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<template v-if="inbound.isQuic"> </template>
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr> <template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr> <tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
</template> <tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
<template v-if="inbound.isKcp"> </template>
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr> <template v-if="inbound.isKcp">
</template> <tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
<template v-if="inbound.isGrpc"> </template>
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
</template> <template v-if="inbound.isGrpc">
</table> <tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
</td></tr> <tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
<tr colspan="2" v-if="dbInbound.hasLink()"> </template>
<td v-if="inbound.tls"> </table>
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> </td>
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> </tr>
</td> <tr colspan="2" v-if="dbInbound.hasLink()">
<td v-else-if="inbound.xtls"> <td v-if="inbound.tls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td> </td>
<td v-else-if="inbound.reality"> <td v-else-if="inbound.xtls">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td> </td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag> <td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>
tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
@@ -109,9 +114,10 @@
<tr v-if="infoModal.clientSettings.subId"> <tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td> <td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td> <td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
<td><a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon></td>
</tr> </tr>
<tr v-if="infoModal.clientSettings.tgId"> <tr v-if="infoModal.clientSettings.tgId">
<td>Telegram Username</td> <td>Telegram ID</td>
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td> <td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
</tr> </tr>
</table> </table>
@@ -123,7 +129,8 @@
<th>{{ i18n "encryption" }}</th> <th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th> <th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -135,7 +142,8 @@
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th> <th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th> <th>FollowRedirect</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -148,15 +156,18 @@
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th> <th>IP</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'"> </tr>
<tr v-if="inbound.settings.auth == 'password'">
<td> </td> <td> </td>
<td>{{ i18n "username" }}</td> <td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts"> </tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -168,7 +179,8 @@
<th> </th> <th> </th>
<th>{{ i18n "username" }}</th> <th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts"> </tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -179,10 +191,11 @@
<div v-if="dbInbound.hasLink()"> <div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p> <p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button> <button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
</div> </div>
</a-modal> </a-modal>
<script> <script>
const infoModal = { const infoModal = {
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
@@ -206,14 +219,6 @@
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true; this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
});
}, },
close() { close() {
infoModal.visible = false; infoModal.visible = false;
@@ -232,42 +237,41 @@
return this.infoModal.inbound; return this.infoModal.inbound;
}, },
get isActive() { get isActive() {
if(infoModal.clientStats){ if (infoModal.clientStats) {
return infoModal.clientStats.enable; return infoModal.clientStats.enable;
} }
return infoModal.dbInbound.isEnable; return infoModal.dbInbound.isEnable;
}, },
get isEnable() { get isEnable() {
if(infoModal.clientSettings){ if (infoModal.clientSettings) {
return infoModal.clientSettings.enable; return infoModal.clientSettings.enable;
} }
return infoModal.dbInbound.isEnable; return infoModal.dbInbound.isEnable;
}, },
get subBase() { get subBase() {
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + basePath + "sub/"; return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "") + basePath + "sub/";
}, },
get tgBase() { get tgBase() {
return "https://t.me/" return "https://t.me/"
}, },
}, },
methods: { methods: {
copyTextToClipboard(elmentId,content) { copyToClipboard(elmentId, content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, { this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
this.infoModal.clipboard.on('success', () => { this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}') app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy(); this.infoModal.clipboard.destroy();
}); });
}, },
statsColor(stats) { statsColor(stats) {
if(!stats) return 'blue' if (!stats) return 'blue'
if(stats['total'] === 0) return 'blue' if (stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan' else if (stats['total'] > 0 && (stats['down'] + stats['up']) < stats['total']) return 'cyan'
else return 'red' else return 'red'
} }
}, },
}); });
</script> </script>

View File

@@ -1,7 +1,7 @@
{{define "inboundModal"}} {{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
@@ -19,7 +19,7 @@
ok() { ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
}, },
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) { show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
if (inbound) { if (inbound) {
@@ -44,10 +44,11 @@
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null; default: return null;
} }
}, },
@@ -86,7 +87,7 @@
get delayedExpireDays() { get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0; return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
}, },
}, },
@@ -99,11 +100,15 @@
this.inModal.inbound.reality = false; this.inModal.inbound.reality = false;
} }
}, },
setDefaultCertData(){ setDefaultCertData() {
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey; inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
}, },
async getNewX25519Cert(){ setDefaultCertXtls() {
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[0].keyFile = app.defaultKey;
},
async getNewX25519Cert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert'); const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false); inModal.loading(false);
@@ -112,15 +117,6 @@
} }
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
} }
}, },
}); });

View File

@@ -12,10 +12,11 @@
margin-top: 10px; margin-top: 10px;
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear> <transition name="list" appear>
@@ -24,7 +25,7 @@
</a-tag> </a-tag>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}: {{ i18n "pages.inbounds.totalDownUp" }}:
@@ -41,19 +42,19 @@
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}: {{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag> <a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag> <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag> <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template> </template>
@@ -64,30 +65,45 @@
</a-card> </a-card>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<div slot="title"> <div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> <a-row>
<a-dropdown :trigger="['click']"> <a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme"> <a-dropdown :trigger="['click']">
<a-menu-item key="export"> <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-icon type="export"></a-icon> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
{{ i18n "pages.inbounds.export" }} <a-menu-item key="export">
</a-menu-item> <a-icon type="export"></a-icon>
<a-menu-item key="resetInbounds"> {{ i18n "pages.inbounds.export" }}
<a-icon type="reload"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.resetAllTraffic" }} <a-menu-item key="resetInbounds">
</a-menu-item> <a-icon type="reload"></a-icon>
<a-menu-item key="resetClients"> {{ i18n "pages.inbounds.resetAllTraffic" }}
<a-icon type="file-done"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.resetAllClientTraffics" }} <a-menu-item key="resetClients">
</a-menu-item> <a-icon type="file-done"></a-icon>
<a-menu-item key="delDepletedClients"> {{ i18n "pages.inbounds.resetAllClientTraffics" }}
<a-icon type="rest"></a-icon> </a-menu-item>
{{ i18n "pages.inbounds.delDepletedClients" }} <a-menu-item key="delDepletedClients">
</a-menu-item> <a-icon type="rest"></a-icon>
</a-menu> {{ i18n "pages.inbounds.delDepletedClients" }}
</a-dropdown> </a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
style="width: 65px;"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</div> </div>
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -100,16 +116,12 @@
<a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon> <a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a> <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme"> <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu-item key="edit"> <a-menu-item key="edit">
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess"> <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
<a-menu-item key="addClient"> <a-menu-item key="addClient">
<a-icon type="user-add"></a-icon> <a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}} {{ i18n "pages.client.add"}}
@@ -141,7 +153,7 @@
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="clone"> <a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}} <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
</a-menu-item> </a-menu-item>
<a-menu-item key="delete"> <a-menu-item key="delete">
<span style="color: #FF4D4F"> <span style="color: #FF4D4F">
@@ -153,7 +165,7 @@
</template> </template>
<template slot="protocol" slot-scope="text, dbInbound"> <template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag> <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
@@ -163,19 +175,19 @@
<template slot="clients" slot-scope="text, dbInbound"> <template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]"> <template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template> </template>
@@ -216,7 +228,7 @@
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
<a-table <a-table
v-else-if="record.protocol === Protocols.TROJAN" v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerTrojanColumns" :columns="innerTrojanColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
@@ -233,6 +245,7 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
@@ -249,7 +262,7 @@
title: "ID", title: "ID",
align: 'center', align: 'center',
dataIndex: "id", dataIndex: "id",
width: 30, width: 40,
}, { }, {
title: '{{ i18n "pages.inbounds.remark" }}', title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center', align: 'center',
@@ -263,7 +276,7 @@
}, { }, {
title: '{{ i18n "pages.inbounds.protocol" }}', title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left', align: 'left',
width: 80, width: 90,
scopedSlots: { customRender: 'protocol' }, scopedSlots: { customRender: 'protocol' },
}, { }, {
title: '{{ i18n "clients" }}', title: '{{ i18n "clients" }}',
@@ -284,7 +297,7 @@
const innerColumns = [ const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
@@ -293,11 +306,11 @@
const innerTrojanColumns = [ const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 120, dataIndex: "password" }, { title: 'Password', width: 170, dataIndex: "password" },
]; ];
const app = new Vue({ const app = new Vue({
@@ -305,6 +318,7 @@
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
spinning: false, spinning: false,
inbounds: [], inbounds: [],
dbInbounds: [], dbInbounds: [],
@@ -315,25 +329,27 @@
defaultCert: '', defaultCert: '',
defaultKey: '', defaultKey: '',
clientCount: {}, clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
}, },
methods: { methods: {
loading(spinning=true) { loading(spinning = true) {
this.spinning = spinning; this.spinning = spinning;
}, },
async getDBInbounds() { async getDBInbounds() {
this.loading(); this.refreshing = true;
const msg = await HttpUtil.post('/xui/inbound/list'); const msg = await HttpUtil.post('/panel/inbound/list');
this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
this.searchKey = ''; setTimeout(() => {
this.refreshing = false;
}, 500);
}, },
async getDefaultSettings() { async getDefaultSettings() {
this.loading(); const msg = await HttpUtil.post('/panel/setting/defaultSettings');
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -345,35 +361,34 @@
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0); this.inbounds.splice(0);
this.dbInbounds.splice(0); this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) { for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound); const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
this.searchedInbounds.push(dbInbound); if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol)) {
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){ this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
} }
} }
this.searchInbounds(this.searchKey);
}, },
getClientCounts(dbInbound,inbound){ getClientCounts(dbInbound, inbound) {
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats
now = new Date().getTime() now = new Date().getTime()
if(clients){ if (clients) {
clientCount = clients.length; clientCount = clients.length;
if(dbInbound.enable){ if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
client.enable ? active.push(client.email) : deactive.push(client.email); client.enable ? active.push(client.email) : deactive.push(client.email);
}); });
clientStats.forEach(client => { clientStats.forEach(client => {
if(!client.enable) { if (!client.enable) {
depleted.push(client.email); depleted.push(client.email);
} else { } else {
if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) || if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
(client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email); (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
} }
}); });
} else { } else {
@@ -399,10 +414,10 @@
if (ObjectUtil.deepSearch(inbound, key)) { if (ObjectUtil.deepSearch(inbound, key)) {
const newInbound = new DBInbound(inbound); const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings); const inboundSettings = JSON.parse(inbound.settings);
if (inboundSettings.hasOwnProperty('clients')){ if (inboundSettings.hasOwnProperty('clients')) {
const searchedSettings = { "clients": [] }; const searchedSettings = { "clients": [] };
inboundSettings.clients.forEach(client => { inboundSettings.clients.forEach(client => {
if (ObjectUtil.deepSearch(client, key)){ if (ObjectUtil.deepSearch(client, key)) {
searchedSettings.clients.push(client); searchedSettings.clients.push(client);
} }
}); });
@@ -413,7 +428,7 @@
}); });
} }
}, },
generalActions(action){ generalActions(action) {
switch (action.key) { switch (action.key) {
case "export": case "export":
this.exportAllLinks(); this.exportAllLinks();
@@ -466,9 +481,9 @@
break; break;
} }
}, },
openCloneInbound(dbInbound) { openCloneInbound(dbInbound) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}}' + dbInbound.remark, title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}', okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
cancelText: '{{ i18n "cancel" }}', cancelText: '{{ i18n "cancel" }}',
@@ -481,7 +496,6 @@
}); });
}, },
async cloneInbound(baseInbound, dbInbound) { async cloneInbound(baseInbound, dbInbound) {
const inbound = new Inbound();
const data = { const data = {
up: dbInbound.up, up: dbInbound.up,
down: dbInbound.down, down: dbInbound.down,
@@ -490,19 +504,19 @@
enable: dbInbound.enable, enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime, expiryTime: dbInbound.expiryTime,
listen: inbound.listen, listen: '',
port: inbound.port, port: RandomUtil.randomIntRange(10000, 60000),
protocol: baseInbound.protocol, protocol: baseInbound.protocol,
settings: inbound.settings.toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}', sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
}; };
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
openAddInbound() { openAddInbound() {
inModal.show({ inModal.show({
title: '{{ i18n "pages.inbounds.addInbound"}}', title: '{{ i18n "pages.inbounds.addInbound"}}',
okText: '{{ i18n "pages.inbounds.addTo"}}', okText: '{{ i18n "pages.inbounds.create"}}',
cancelText: '{{ i18n "close" }}', cancelText: '{{ i18n "close" }}',
confirm: async (inbound, dbInbound) => { confirm: async (inbound, dbInbound) => {
inModal.loading(); inModal.loading();
@@ -517,7 +531,7 @@
const inbound = dbInbound.toInbound(); const inbound = dbInbound.toInbound();
inModal.show({ inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}', title: '{{ i18n "pages.inbounds.modifyInbound"}}',
okText: '{{ i18n "pages.inbounds.revise"}}', okText: '{{ i18n "pages.inbounds.update"}}',
cancelText: '{{ i18n "close" }}', cancelText: '{{ i18n "close" }}',
inbound: inbound, inbound: inbound,
dbInbound: dbInbound, dbInbound: dbInbound,
@@ -546,7 +560,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
async updateInbound(inbound, dbInbound) { async updateInbound(inbound, dbInbound) {
const data = { const data = {
@@ -565,7 +579,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
}, },
openAddClient(dbInboundId) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -611,30 +625,30 @@
isEdit: true isEdit: true
}); });
}, },
findIndexOfClient(clients,client) { findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0]; firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]); return clients.findIndex(c => c[firstKey] === client[firstKey]);
}, },
async addClient(clients, dbInboundId) { async addClient(clients, dbInboundId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}', settings: '{"clients": [' + clients.toString() + ']}',
}; };
await this.submit(`/xui/inbound/addClient`, data); await this.submit(`/panel/inbound/addClient`, data);
}, },
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}', settings: '{"clients": [' + client.toString() + ']}',
}; };
await this.submit(`/xui/inbound/updateClient/${clientId}`, data); await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
}, },
resetTraffic(dbInboundId) { resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => {
@@ -649,42 +663,51 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId), onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId,client) { delClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = dbInbound.protocol == "trojan" ? client.password : client.id; clientId = this.getClientId(dbInbound.protocol, client);
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
}); });
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null; default: return null;
} }
}, },
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
const link = dbInbound.genLink(clientIndex); const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
}, },
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); infoModal.show(dbInbound, index);
}, },
switchEnable(dbInboundId) { switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound); this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
@@ -693,8 +716,8 @@
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client); index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable; clients[index].enable = !clients[index].enable;
clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id; clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index],dbInboundId, clientId); await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false); this.loading(false);
}, },
async submit(url, data) { async submit(url, data) {
@@ -704,69 +727,71 @@
} }
}, },
getInboundClients(dbInbound) { getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) { if (dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses return dbInbound.toInbound().settings.vlesses;
} else if(dbInbound.protocol == Protocols.VMESS) { } else if (dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses return dbInbound.toInbound().settings.vmesses;
} else if(dbInbound.protocol == Protocols.TROJAN) { } else if (dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans return dbInbound.toInbound().settings.trojans;
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses;
} }
}, },
resetClientTraffic(client,dbInboundId) { resetClientTraffic(client, dbInboundId) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email), onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
}) })
}, },
resetAllTraffic() { resetAllTraffic() {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}', title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllTraffics'), onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
}); });
}, },
resetAllClientTraffics(dbInboundId) { resetAllClientTraffics(dbInboundId) {
this.$confirm({ this.$confirm({
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}', title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}', content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
}) })
}, },
delDepletedClients(dbInboundId) { delDepletedClients(dbInboundId) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}', title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}', content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
}) })
}, },
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index) return dbInbound.toInbound().isExpiry(index)
}, },
getUpStats(dbInbound, email) { getUpStats(dbInbound, email) {
if(email.length == 0) return 0 if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0 return clientStats ? clientStats.up : 0
}, },
getDownStats(dbInbound, email) { getDownStats(dbInbound, email) {
if(email.length == 0) return 0 if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0 return clientStats ? clientStats.down : 0
}, },
isTrafficExhausted(dbInbound, email) { isTrafficExhausted(dbInbound, email) {
if(email.length == 0) return false if (email.length == 0) return false
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
}, },
@@ -774,19 +799,45 @@
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true return clientStats ? clientStats['enable'] : true
}, },
isRemovable(dbInbound_id){ isRemovable(dbInbound_id) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1 return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
}, },
inboundLinks(dbInboundId) { inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark); txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
}, },
exportAllLinks() { exportAllLinks() {
let copyText = ''; let copyText = '';
for (const dbInbound of this.dbInbounds) { for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks copyText += dbInbound.genInboundLinks
} }
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds'); txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getDBInbounds();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval() {
localStorage.setItem("refreshInterval", this.refreshInterval);
},
async manualRefresh() {
if (!this.refreshing) {
this.spinning = true;
await this.getDBInbounds();
this.spinning = false;
}
}, },
}, },
watch: { watch: {
@@ -795,8 +846,15 @@
}, 500) }, 500)
}, },
mounted() { mounted() {
this.loading();
this.getDefaultSettings(); this.getDefaultSettings();
this.getDBInbounds(); if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
}, },
computed: { computed: {
total() { total() {
@@ -833,5 +891,6 @@
{{template "inboundInfoModal"}} {{template "inboundInfoModal"}}
{{template "clientsModal"}} {{template "clientsModal"}}
{{template "clientsBulkModal"}} {{template "clientsBulkModal"}}
</body> </body>
</html> </html>

View File

@@ -13,32 +13,33 @@
} }
.ant-card-dark h2 { .ant-card-dark h2 {
color: hsla(0,0%,100%,.65); color: hsla(0, 0%, 100%, .65);
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU</div> <div>CPU</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -51,7 +52,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -60,7 +61,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -75,14 +76,14 @@
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> 3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag> Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a> Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip> <a-tooltip>
@@ -94,7 +95,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.xrayStatus" }}: {{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error"> <a-tooltip v-if="status.xray.state === State.Error">
@@ -109,20 +110,20 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}: {{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]] TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -133,7 +134,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
@@ -159,7 +160,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="cloud-upload"></a-icon> <a-icon type="cloud-upload"></a-icon>
@@ -188,9 +189,10 @@
</transition> </transition>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false" :closable="true" @ok="() => versionModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
footer=""> footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
@@ -201,9 +203,10 @@
</a-tag> </a-tag>
</template> </template>
</a-modal> </a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs" <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
width="800px" width="800px"
footer=""> footer="">
<a-form layout="inline"> <a-form layout="inline">
@@ -211,7 +214,7 @@
<a-select v-model="logModal.rows" <a-select v-model="logModal.rows"
style="width: 80px" style="width: 80px"
@change="openLogs(logModal.rows)" @change="openLogs(logModal.rows)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option> <a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option> <a-select-option value="50">50</a-select-option>
@@ -227,12 +230,31 @@
{{ i18n "download" }} x-ui.log {{ i18n "download" }} x-ui.log
</a-button> </a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true" <a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input> :autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal> </a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
</a-modal>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "textModal"}} {{template "textModal"}}
<script> <script>
@@ -275,13 +297,13 @@
this.disk = new CurTotal(0, 0); this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0]; this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0); this.mem = new CurTotal(0, 0);
this.netIO = {up: 0, down: 0}; this.netIO = { up: 0, down: 0 };
this.netTraffic = {sent: 0, recv: 0}; this.netTraffic = { sent: 0, recv: 0 };
this.swap = new CurTotal(0, 0); this.swap = new CurTotal(0, 0);
this.tcpCount = 0; this.tcpCount = 0;
this.udpCount = 0; this.udpCount = 0;
this.uptime = 0; this.uptime = 0;
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""}; this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
if (data == null) { if (data == null) {
return; return;
@@ -339,14 +361,39 @@
}, },
}; };
const backupModal = {
visible: false,
title: '',
description: '',
exportText: '',
importText: '',
show({
title = '{{ i18n "pages.index.backupTitle" }}',
description = '{{ i18n "pages.index.backupDescription" }}',
exportText = '{{ i18n "pages.index.exportDatabase" }}',
importText = '{{ i18n "pages.index.importDatabase" }}',
}) {
this.title = title;
this.description = description;
this.exportText = exportText;
this.importText = importText;
this.visible = true;
},
hide() {
this.visible = false;
},
};
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
backupModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
}, },
@@ -378,17 +425,16 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`); await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false); this.loading(false);
}, },
}); });
}, },
//here add stop xray function
async stopXrayService() { async stopXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService'); const msg = await HttpUtil.post('server/stopXrayService');
@@ -397,7 +443,6 @@
return; return;
} }
}, },
//here add restart xray function
async restartXrayService() { async restartXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService'); const msg = await HttpUtil.post('server/restartXrayService');
@@ -406,27 +451,67 @@
return; return;
} }
}, },
async openLogs(rows){ async openLogs(rows) {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/logs/'+rows); const msg = await HttpUtil.post('server/logs/' + rows);
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
logModal.show(msg.obj,rows); logModal.show(msg.obj, rows);
}, },
async openConfig(){ async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json'); txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
}, },
getBackup(){ openBackup() {
backupModal.show({
title: '{{ i18n "pages.index.backupTitle" }}',
description: '{{ i18n "pages.index.backupDescription" }}',
exportText: '{{ i18n "pages.index.exportDatabase" }}',
importText: '{{ i18n "pages.index.importDatabase" }}',
});
},
exportDatabase() {
window.location = basePath + 'server/getDb'; window.location = basePath + 'server/getDb';
} },
importDatabase() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.db';
fileInput.addEventListener('change', async (event) => {
const dbFile = event.target.files[0];
if (dbFile) {
const formData = new FormData();
formData.append('db', dbFile);
backupModal.hide();
this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
});
this.loading(false);
if (!uploadMsg.success) {
return;
}
this.loading(true);
const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false);
if (restartMsg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
});
fileInput.click();
},
}, },
async mounted() { async mounted() {
while (true) { while (true) {

View File

@@ -1,773 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
:not(.ant-card-dark)>.ant-tabs-top-bar {
background: white;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.sessionMaxAge" }}' desc='{{ i18n "pages.setting.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language" />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
<a-input type="password" v-model="user.oldPassword" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
<a-input type="password" v-model="user.newPassword" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.loginSecurity" }}' description='{{ i18n "pages.setting.loginSecurityDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.secretToken" }}' description='{{ i18n "pages.setting.secretTokenDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<svg
@click="getNewSecret"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
</svg>
<template>
<a-textarea type="text" id='token' :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
<a-button type="primary" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<a-divider>{{ i18n "pages.setting.actions"}}</a-divider>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.setting.resetDefaultConfig" }}</a-button>
</a-space>
<a-divider>{{ i18n "pages.setting.basicTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.setting.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.generalConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigAds"}}' desc='{{ i18n "pages.setting.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPorn"}}' desc='{{ i18n "pages.setting.xrayConfigPornDesc"}}' v-model="PornSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.countryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.countryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRIp"}}' desc='{{ i18n "pages.setting.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRDomain"}}' desc='{{ i18n "pages.setting.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigChinaIp"}}' desc='{{ i18n "pages.setting.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.setting.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.setting.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.setting.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.warpConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.warpConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.setting.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.setting.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.xrayConfigOutbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.xrayConfigRoutings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
spinning: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
user: new User(),
lang: getLang(),
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
warpSettings: {
tag: "WARP",
protocol: "socks",
settings: {
servers: [
{
address: "127.0.0.1",
port: 40000
}
]
}
},
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
ips: {
local: ["geoip:private"],
google: ["geoip:google"],
cn: ["geoip:cn"],
ir: ["geoip:ir"],
ru: ["geoip:ru"],
},
domains: {
ads: [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
],
porn: ["geosite:category-porn"],
openai: ["geosite:openai"],
google: ["geosite:google"],
spotify: ["geosite:spotify"],
netflix: ["geosite:netflix"],
cn: ["geosite:cn"],
ru: ["geosite:category-ru-gov"],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"ext:iran.dat:ads",
"geosite:category-ir"
]
},
}
},
methods: {
loading(spinning = true , obj) {
if(obj == null)
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
await this.getUserSecret();
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.setting.restartPanel" }}',
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
},
async getUserSecret(){
const user_msg = await HttpUtil.post("/xui/setting/getUserSecret", this.user);
if (user_msg.success){
this.user = user_msg.obj;
}
this.loading(false);
},
async updateSecret(){
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUserSecret", this.user);
if (msg.success){
this.user = msg.obj;
}
this.loading(false);
await this.updateAllSetting();
},
async getNewSecret(){
this.loading(true);
await PromiseUtil.sleep(1000);
var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
var string = '';
var len = 64;
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
this.user.loginSecret = string;
document.getElementById('token').value =this.user.loginSecret;
this.loading(false);
},
async toggleToken(value){
if(value)
this.getNewSecret();
else
this.user.loginSecret = "";
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
checkRequiredOutbounds() {
const newTemplateSettings = this.templateSettings;
const haveIPv4Outbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "IPv4");
const haveIPv4Rules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "IPv4");
const haveWARPOutbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "WARP");
const haveWARPRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "WARP");
if (haveWARPRules && !haveWARPOutbounds) {
newTemplateSettings.outbounds.push(this.warpSettings);
}
if (haveIPv4Rules && !haveIPv4Outbounds) {
newTemplateSettings.outbounds.push(this.ipv4Settings);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
let result = false;
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
result = true;
}
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { newValue, data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
if (newValue) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
return;
}
}
newRules.push(routingRule);
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
this.checkRequiredOutbounds();
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
},
privateIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
},
AdsSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
},
PornSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
},
GoogleIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
},
NetflixIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
IRIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
},
IRDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
},
ChinaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
},
ChinaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
},
RussiaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
},
RussiaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
},
GoogleWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
},
OpenAIWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
},
NetflixWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
SpotifyWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
},
}
});
</script>
</body>
</html>

806
web/html/xui/settings.html Normal file
View File

@@ -0,0 +1,806 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
:not(.ant-card-dark)>.ant-tabs-top-bar {
background: white;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
<a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass" >
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language" />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
<a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.darkCardClass">
<a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'>
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
<password-input v-model="user.oldPassword" style="max-width: 300px"></password-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
<password-input v-model="user.newPassword" style="max-width: 300px"></password-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="sec-2" tab='{{ i18n "pages.settings.security.secret"}}'>
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.security.loginSecurity" }}' description='{{ i18n "pages.settings.security.loginSecurityDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.settings.security.secretToken" }}' description='{{ i18n "pages.settings.security.secretTokenDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<svg
@click="getNewSecret"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"><path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/><path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
</svg>
<template>
<a-textarea type="text" id='token' :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
<a-button type="primary" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form>
</a-tab-pane>
</a-tabs>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<a-divider style="padding: 20px;">{{ i18n "pages.settings.actions"}}</a-divider>
<a-space direction="horizontal" style="padding: 0px 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space>
<a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider>
<a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPorn"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPornDesc"}}' v-model="PornSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpeedtest"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.countryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.countryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.warpConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane>
</a-tabs>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
spinning: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
user: new User(),
lang: getLang(),
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
warpSettings: {
tag: "WARP",
protocol: "socks",
settings: {
servers: [
{
address: "127.0.0.1",
port: 40000
}
]
}
},
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
ips: {
local: ["geoip:private"],
google: ["geoip:google"],
cn: ["geoip:cn"],
ir: ["geoip:ir"],
ru: ["geoip:ru"],
},
domains: {
ads: [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
],
porn: ["geosite:category-porn"],
speedtest: ["geosite:speedtest"],
openai: ["geosite:openai"],
google: ["geosite:google"],
spotify: ["geosite:spotify"],
netflix: ["geosite:netflix"],
cn: [
"geosite:cn",
"regexp:.*\\.cn$"
],
ru: [
"geosite:category-gov-ru",
"regexp:.*\\.ru$"
],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"ext:iran.dat:ads",
"geosite:category-ir"
]
},
}
},
methods: {
loading(spinning = true, obj) {
if (obj == null) this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
await this.getUserSecret();
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
window.location.replace(basePath + "logout")
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.settings.restartPanel" }}',
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
window.location.replace(this.allSetting.webBasePath + "panel/settings");
}
},
async getUserSecret() {
const user_msg = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
if (user_msg.success) {
this.user = user_msg.obj;
}
this.loading(false);
},
async updateSecret() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
if (msg.success) {
this.user = msg.obj;
window.location.replace(basePath + "logout")
}
this.loading(false);
await this.updateAllSetting();
},
async getNewSecret() {
this.loading(true);
await PromiseUtil.sleep(1000);
var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
var string = "";
var len = 64;
for (var ii = 0; ii < len; ii++) {
string += chars[Math.floor(Math.random() * chars.length)];
}
this.user.loginSecret = string;
document.getElementById("token").value = this.user.loginSecret;
this.loading(false);
},
async toggleToken(value) {
if (value) this.getNewSecret();
else this.user.loginSecret = "";
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
checkRequiredOutbounds() {
const newTemplateSettings = this.templateSettings;
const haveIPv4Outbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "IPv4");
const haveIPv4Rules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "IPv4");
const haveWARPOutbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "WARP");
const haveWARPRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "WARP");
if (haveWARPRules && !haveWARPOutbounds) {
newTemplateSettings.outbounds.push(this.warpSettings);
}
if (haveIPv4Rules && !haveIPv4Outbounds) {
newTemplateSettings.outbounds.push(this.ipv4Settings);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
let result = false;
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
result = true;
}
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { newValue, data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
if (newValue) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
return;
}
}
newRules.push(routingRule);
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
this.checkRequiredOutbounds();
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
},
privateIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
},
AdsSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
},
PornSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
},
SpeedTestSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.speedtest
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.speedtest
});
},
},
GoogleIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
},
NetflixIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
IRIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
},
IRDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
},
ChinaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
},
ChinaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
},
RussiaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
},
RussiaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
},
GoogleWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
},
OpenAIWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
},
NetflixWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
SpotifyWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
},
}
});
</script>
</body>
</html>

View File

@@ -4,23 +4,22 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"regexp" "regexp"
ss "strings"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/web/service" "x-ui/web/service"
"x-ui/xray" "x-ui/xray"
// "strconv"
"github.com/go-cmd/cmd"
"net" "net"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/go-cmd/cmd"
) )
type CheckClientIpJob struct { type CheckClientIpJob struct {
xrayService service.XrayService xrayService service.XrayService
inboundService service.InboundService
} }
var job *CheckClientIpJob var job *CheckClientIpJob
@@ -36,7 +35,7 @@ func (j *CheckClientIpJob) Run() {
processLogFile() processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"} // disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps, ",")) blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755) err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err) checkError(err)
@@ -58,7 +57,7 @@ func processLogFile() {
checkError(err) checkError(err)
} }
lines := ss.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
for _, line := range lines { for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`) ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`) emailRegx, _ := regexp.Compile(`email:.+`)
@@ -74,7 +73,7 @@ func processLogFile() {
if matchesEmail == "" { if matchesEmail == "" {
continue continue
} }
matchesEmail = ss.Split(matchesEmail, "email: ")[1] matchesEmail = strings.Split(matchesEmail, "email: ")[1]
if InboundClientIps[matchesEmail] != nil { if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) { if contains(InboundClientIps[matchesEmail], ip) {
@@ -92,14 +91,12 @@ func processLogFile() {
for clientEmail, ips := range InboundClientIps { for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail) inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips)) sort.Strings(ips)
if err != nil { if err != nil {
addInboundClientIps(clientEmail, ips) addInboundClientIps(clientEmail, ips)
} else { } else {
updateInboundClientIps(inboundClientIps, clientEmail, ips) updateInboundClientIps(inboundClientIps, clientEmail, ips)
} }
} }
// check if inbound connection is more than limited ip and drop connection // check if inbound connection is more than limited ip and drop connection
@@ -202,6 +199,8 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
var disAllowedIps []string // initialize the slice
for _, client := range clients { for _, client := range clients {
if client.Email == clientEmail { if client.Email == clientEmail {
@@ -214,7 +213,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
} }
} }
logger.Debug("disAllowedIps ", disAllowedIps) logger.Debug("disAllowedIps ", disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps)) sort.Strings(disAllowedIps)
db := database.GetDB() db := database.GetDB()
err = db.Save(inboundClientIps).Error err = db.Save(inboundClientIps).Error
@@ -223,6 +222,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
} }
return nil return nil
} }
func DisableInbound(id int) error { func DisableInbound(id int) error {
db := database.GetDB() db := database.GetDB()
result := db.Model(model.Inbound{}). result := db.Model(model.Inbound{}).

View File

@@ -332,6 +332,9 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
if oldInbound.Protocol == "trojan" { if oldInbound.Protocol == "trojan" {
client_key = "password" client_key = "password"
} }
if oldInbound.Protocol == "shadowsocks" {
client_key = "email"
}
inerfaceClients := settings["clients"].([]interface{}) inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{} var newClients []interface{}
@@ -398,6 +401,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
oldClientId := "" oldClientId := ""
if oldInbound.Protocol == "trojan" { if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password oldClientId = oldClient.Password
} else if oldInbound.Protocol == "shadowsocks" {
oldClientId = oldClient.Email
} else { } else {
oldClientId = oldClient.ID oldClientId = oldClient.ID
} }
@@ -595,6 +600,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) DisableInvalidClients() (int64, error) { func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB() db := database.GetDB()
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
@@ -605,7 +611,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) RemoveOrphanedTraffics() {
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB() db := database.GetDB()
db.Exec(` db.Exec(`
DELETE FROM client_traffics DELETE FROM client_traffics
@@ -616,6 +623,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
) )
`) `)
} }
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error { func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
@@ -634,6 +642,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
} }
return nil return nil
} }
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error { func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
@@ -664,6 +673,200 @@ func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
} }
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, nil, err
}
if len(traffics) > 0 {
inbound, err = s.GetInbound(traffics[0].InboundId)
return traffics[0], inbound, err
}
return nil, nil, nil
}
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return false, err
}
if inbound == nil {
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
if err != nil {
return false, err
}
clientId := ""
clientOldEnabled := false
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
clientOldEnabled = oldClient.Enable
break
}
}
if len(clientId) == 0 {
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, err
}
inbound.Settings = string(modifiedSettings)
return !clientOldEnabled, s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["limitIp"] = count
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.getClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["expiryTime"] = expiry_time
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error { func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB() db := database.GetDB()
@@ -830,12 +1033,14 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { logger.Warning(err)
logger.Warning(err) return nil, err
return nil, err
}
} }
return traffics[0], err if len(traffics) > 0 {
return traffics[0], nil
}
return nil, nil
} }
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -912,6 +1117,8 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
func (s *InboundService) MigrationRequirements() { func (s *InboundService) MigrationRequirements() {
db := database.GetDB() db := database.GetDB()
// Fix inbounds based problems
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
@@ -922,6 +1129,7 @@ func (s *InboundService) MigrationRequirements() {
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{}) clients, ok := settings["clients"].([]interface{})
if ok { if ok {
// Fix Clinet configuration problems
var newClients []interface{} var newClients []interface{}
for client_index := range clients { for client_index := range clients {
c := clients[client_index].(map[string]interface{}) c := clients[client_index].(map[string]interface{})
@@ -947,6 +1155,7 @@ func (s *InboundService) MigrationRequirements() {
inbounds[inbound_index].Settings = string(modifiedSettings) inbounds[inbound_index].Settings = string(modifiedSettings)
} }
// Add client traffic row for all clients which has email
modelClients, err := s.getClients(inbounds[inbound_index]) modelClients, err := s.getClients(inbounds[inbound_index])
if err != nil { if err != nil {
return return
@@ -962,4 +1171,12 @@ func (s *InboundService) MigrationRequirements() {
} }
} }
db.Save(inbounds) db.Save(inbounds)
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}
func (s *InboundService) MigrateDB() {
s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics()
} }

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"mime/multipart"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -14,7 +15,9 @@ import (
"strings" "strings"
"time" "time"
"x-ui/config" "x-ui/config"
"x-ui/database"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common"
"x-ui/util/sys" "x-ui/util/sys"
"x-ui/xray" "x-ui/xray"
@@ -73,7 +76,8 @@ type Release struct {
} }
type ServerService struct { type ServerService struct {
xrayService XrayService xrayService XrayService
inboundService InboundService
} }
func (s *ServerService) GetStatus(lastStatus *Status) *Status { func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -175,7 +179,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
func (s *ServerService) GetXrayVersions() ([]string, error) { func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/mhsanaei/Xray-core/releases" url := "https://api.github.com/repos/MHSanaei/Xray-core/releases"
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -242,7 +246,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
} }
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/mhsanaei/Xray-core/releases/download/%s/%s", version, fileName) url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return "", err return "", err
@@ -395,6 +399,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
return fileContents, nil return fileContents, nil
} }
func (s *ServerService) ImportDB(file multipart.File) error {
// Check if the file is a SQLite database
isValidDb, err := database.IsSQLiteDB(file)
if err != nil {
return common.NewErrorf("Error checking db file format: %v", err)
}
if !isValidDb {
return common.NewError("Invalid db file format")
}
// Reset the file reader to the beginning
_, err = file.Seek(0, 0)
if err != nil {
return common.NewErrorf("Error resetting file reader: %v", err)
}
// Save the file as temporary file
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
// Remove the existing fallback file (if any) before creating one
_, err = os.Stat(tempPath)
if err == nil {
errRemove := os.Remove(tempPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
}
}
// Create the temporary file
tempFile, err := os.Create(tempPath)
if err != nil {
return common.NewErrorf("Error creating temporary db file: %v", err)
}
defer tempFile.Close()
// Remove temp file before returning
defer os.Remove(tempPath)
// Save uploaded file to temporary file
_, err = io.Copy(tempFile, file)
if err != nil {
return common.NewErrorf("Error saving db: %v", err)
}
// Check if we can init db or not
err = database.InitDB(tempPath)
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
}
// Stop Xray
s.StopXrayService()
// Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
// Remove the existing fallback file (if any)
_, err = os.Stat(fallbackPath)
if err == nil {
errRemove := os.Remove(fallbackPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
}
}
// Move the current database to the fallback location
err = os.Rename(config.GetDBPath(), fallbackPath)
if err != nil {
return common.NewErrorf("Error backing up temporary db file: %v", err)
}
// Remove the temporary file before returning
defer os.Remove(fallbackPath)
// Move temp to DB path
err = os.Rename(tempPath, config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error moving db file: %v", err)
}
// Migrate DB
err = database.InitDB(config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error migrating db: %v", err)
}
s.inboundService.MigrateDB()
// Start Xray
err = s.RestartXrayService()
if err != nil {
return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
}
return nil
}
func (s *ServerService) GetNewX25519Cert() (interface{}, error) { func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519") cmd := exec.Command(xray.GetBinaryPath(), "x25519")

View File

@@ -38,7 +38,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
continue continue
} }
for _, client := range clients { for _, client := range clients {
if client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
@@ -73,7 +73,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
@@ -97,85 +97,97 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
return s.genVlessLink(inbound, email) return s.genVlessLink(inbound, email)
case "trojan": case "trojan":
return s.genTrojanLink(inbound, email) return s.genTrojanLink(inbound, email)
case "shadowsocks":
return s.genShadowsocksLink(inbound, email)
} }
return "" return ""
} }
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.VMess { if inbound.Protocol != model.VMess {
return "" return ""
} }
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{
"v": "2",
"ps": remark,
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{} var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream) json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string) network, _ := stream["network"].(string)
typeStr := "none" obj["net"] = network
host := ""
path := ""
sni := ""
fp := ""
var alpn []string
allowInsecure := false
switch network { switch network {
case "tcp": case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{}) tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{}) header, _ := tcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" { if typeStr == "http" {
request := header["request"].(map[string]interface{}) request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{}) requestPath, _ := request["path"].([]interface{})
path = requestPath[0].(string) obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{}) headers, _ := request["headers"].(map[string]interface{})
host = searchHost(headers) obj["host"] = searchHost(headers)
} }
case "kcp": case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{}) kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{}) header, _ := kcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) obj["type"], _ = header["type"].(string)
path, _ = kcp["seed"].(string) obj["path"], _ = kcp["seed"].(string)
case "ws": case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{}) ws, _ := stream["wsSettings"].(map[string]interface{})
path = ws["path"].(string) obj["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{}) headers, _ := ws["headers"].(map[string]interface{})
host = searchHost(headers) obj["host"] = searchHost(headers)
case "http": case "http":
network = "h2" obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{}) http, _ := stream["httpSettings"].(map[string]interface{})
path, _ = http["path"].(string) obj["path"], _ = http["path"].(string)
host = searchHost(http) obj["host"] = searchHost(http)
case "quic": case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{}) quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{}) header := quic["header"].(map[string]interface{})
typeStr, _ = header["type"].(string) obj["type"], _ = header["type"].(string)
host, _ = quic["security"].(string) obj["host"], _ = quic["security"].(string)
path, _ = quic["key"].(string) obj["path"], _ = quic["key"].(string)
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
path = grpc["serviceName"].(string) obj["path"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)
obj["tls"] = security
if security == "tls" { if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{}) alpns, _ := tlsSetting["alpn"].([]interface{})
for _, a := range alpns { if len(alpns) > 0 {
alpn = append(alpn, a.(string)) var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
} }
tlsSettings, _ := searchKey(tlsSetting, "settings") tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil { if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok { if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
sni, _ = sniValue.(string) obj["sni"], _ = sniValue.(string)
} }
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
fp, _ = fpValue.(string) obj["fp"], _ = fpValue.(string)
} }
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok { if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
allowInsecure, _ = insecure.(bool) obj["allowInsecure"], _ = insecure.(bool)
} }
} }
serverName, _ := tlsSetting["serverName"].(string) serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" { if serverName != "" {
address = serverName obj["add"] = serverName
} }
} }
@@ -187,24 +199,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
break break
} }
} }
obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": address,
"port": inbound.Port,
"id": clients[clientIndex].ID,
"aid": clients[clientIndex].AlterIds,
"net": network,
"type": typeStr,
"host": host,
"path": path,
"tls": security,
"sni": sni,
"fp": fp,
"alpn": strings.Join(alpn, ","),
"allowInsecure": allowInsecure,
}
jsonStr, _ := json.MarshalIndent(obj, "", " ") jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
} }
@@ -266,6 +263,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string) params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)
@@ -360,6 +360,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1" params["allowInsecure"] = "1"
} }
} }
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
} }
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -383,7 +386,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
url.Fragment = email remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
url.Fragment = remark
return url.String() return url.String()
} }
@@ -444,6 +448,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
case "grpc": case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{}) grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string) params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
} }
security, _ := stream["security"].(string) security, _ := stream["security"].(string)
@@ -534,6 +541,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1" params["allowInsecure"] = "1"
} }
} }
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
} }
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -558,10 +568,33 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
url.Fragment = email remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
url.Fragment = remark
return url.String() return url.String()
} }
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.getClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
}
func searchKey(data interface{}, key string) (interface{}, bool) { func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) { switch val := data.(type) {
case map[string]interface{}: case map[string]interface{}:

View File

@@ -31,6 +31,7 @@ type Tgbot struct {
inboundService InboundService inboundService InboundService
settingService SettingService settingService SettingService
serverService ServerService serverService ServerService
xrayService XrayService
lastStatus *Status lastStatus *Status
} }
@@ -148,6 +149,170 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
} }
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) { func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
if isAdmin {
dataArray := strings.Split(callbackQuery.Data, " ")
if len(dataArray) >= 2 && len(dataArray[1]) > 0 {
email := dataArray[1]
switch dataArray[0] {
case "client_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client Refreshed successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "client_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ips_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs Refreshed successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ips_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "reset_traffic":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Reset Traffic?", "reset_traffic_c "+email),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_traffic_c":
err := t.inboundService.ResetClientTrafficByEmail(email)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Traffic reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
case "reset_exp":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "reset_exp_c "+email+" 0"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1 Month", "reset_exp_c "+email+" 30"),
tgbotapi.NewInlineKeyboardButtonData("2 Months", "reset_exp_c "+email+" 60"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3 Months", "reset_exp_c "+email+" 90"),
tgbotapi.NewInlineKeyboardButtonData("6 Months", "reset_exp_c "+email+" 180"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("9 Months", "reset_exp_c "+email+" 270"),
tgbotapi.NewInlineKeyboardButtonData("12 Months", "reset_exp_c "+email+" 360"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("10 Days", "reset_exp_c "+email+" 10"),
tgbotapi.NewInlineKeyboardButtonData("20 Days", "reset_exp_c "+email+" 20"),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_exp_c":
if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2])
if err == nil {
var date int64 = 0
if days > 0 {
date = int64(-(days * 24 * 60 * 60000))
}
err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Expire days reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ip_limit":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel IP Limit", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "ip_limit_c "+email+" 0"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1", "ip_limit_c "+email+" 1"),
tgbotapi.NewInlineKeyboardButtonData("2", "ip_limit_c "+email+" 2"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3", "ip_limit_c "+email+" 3"),
tgbotapi.NewInlineKeyboardButtonData("4", "ip_limit_c "+email+" 4"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("5", "ip_limit_c "+email+" 5"),
tgbotapi.NewInlineKeyboardButtonData("6", "ip_limit_c "+email+" 6"),
tgbotapi.NewInlineKeyboardButtonData("7", "ip_limit_c "+email+" 7"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("8", "ip_limit_c "+email+" 8"),
tgbotapi.NewInlineKeyboardButtonData("9", "ip_limit_c "+email+" 9"),
tgbotapi.NewInlineKeyboardButtonData("10", "ip_limit_c "+email+" 10"),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "ip_limit_c":
if len(dataArray) == 3 {
count, err := strconv.Atoi(dataArray[2])
if err == nil {
err := t.inboundService.ResetClientIpLimitByEmail(email, count)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IP limit %d saved successfully.", email, count))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "clear_ips":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel", "ips_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Clear IPs?", "clear_ips_c "+email),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "clear_ips_c":
err := t.inboundService.ClearClientIps(email)
if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs cleared successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
case "ip_log":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get IP Log.", email))
t.searchClientIps(callbackQuery.From.ID, email)
case "toggle_enable":
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
if err == nil {
t.xrayService.SetToNeedRestart()
if enabled {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Enabled successfully.", email))
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Disabled successfully.", email))
}
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
}
return
}
}
// Respond to the callback query, telling Telegram to show the user // Respond to the callback query, telling Telegram to show the user
// a message with the data received. // a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data) callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
@@ -165,7 +330,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
case "get_backup": case "get_backup":
t.sendBackup(callbackQuery.From.ID) t.sendBackup(callbackQuery.From.ID)
case "client_traffic": case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName) t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName, strconv.FormatInt(callbackQuery.From.ID, 10))
case "client_commands": case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.") t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
case "commands": case "commands":
@@ -215,7 +380,10 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
} }
} }
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) { func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) {
if !isRunning {
return
}
var allMessages []string var allMessages []string
limit := 2000 limit := 2000
// paging message if it is big // paging message if it is big
@@ -236,6 +404,9 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
for _, message := range allMessages { for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message) info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML" info.ParseMode = "HTML"
if len(inlineKeyboard) > 0 {
info.ReplyMarkup = inlineKeyboard[0]
}
_, err := bot.Send(info) _, err := bot.Send(info)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
@@ -362,13 +533,8 @@ func (t *Tgbot) getInboundUsages() string {
return info return info
} }
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) { func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) {
if len(tgUserName) == 0 { traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
msg := "❌ Something went wrong!" msg := "❌ Something went wrong!"
@@ -376,7 +542,21 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
return return
} }
if len(traffics) == 0 { if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>" if len(tgUserName) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram user id in your configuration(s).\n\nYour user id: <b>" + tgUserID + "</b>"
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err = t.inboundService.GetClientTrafficTgBot(tgUserName)
}
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>\n\nYour user id: <b>" + tgUserID + "</b>"
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
} }
@@ -403,7 +583,28 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
t.SendAnswer(chatId, "Please choose:", false) t.SendAnswer(chatId, "Please choose:", false)
} }
func (t *Tgbot) searchClient(chatId int64, email string) { func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips, err := t.inboundService.GetInboundClientIps(email)
if err != nil || len(ips) == 0 {
ips = "No IP Record"
}
output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips)
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "ips_refresh "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Clear IPs", "clear_ips "+email),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
}
}
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
@@ -433,7 +634,29 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime) total, expiryTime)
t.SendMsgToTgbot(chatId, output) var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "client_refresh "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📈 Reset Traffic", "reset_traffic "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📅 Reset Expire Days", "reset_exp "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Log", "ip_log "+email),
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Limit", "ip_limit "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔘 Enable / Disable", "toggle_enable "+email),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
}
} }
func (t *Tgbot) searchInbound(chatId int64, remark string) { func (t *Tgbot) searchInbound(chatId int64, remark string) {
@@ -608,3 +831,28 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Warning("Error in uploading config.json: ", err) logger.Warning("Error in uploading config.json: ", err)
} }
} }
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
callback := tgbotapi.NewCallback(id, message)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
}
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard tgbotapi.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageReplyMarkup(chatId, messageID, inlineKeyboard)
if _, err := bot.Request(edit); err != nil {
logger.Warning(err)
}
}
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageText(chatId, messageID, text)
edit.ParseMode = "HTML"
if len(inlineKeyboard) > 0 {
edit.ReplyMarkup = &inlineKeyboard[0]
}
if _, err := bot.Request(edit); err != nil {
logger.Warning(err)
}
}

View File

@@ -22,7 +22,7 @@
"unlimited" = "Unlimited" "unlimited" = "Unlimited"
"none" = "None" "none" = "None"
"qrCode" = "QR Code" "qrCode" = "QR Code"
"info" = "More information" "info" = "More Information"
"edit" = "Edit" "edit" = "Edit"
"delete" = "Delete" "delete" = "Delete"
"reset" = "Reset" "reset" = "Reset"
@@ -43,29 +43,29 @@
"monitor" = "Listening IP" "monitor" = "Listening IP"
"certificate" = "Certificate" "certificate" = "Certificate"
"fail" = "Fail" "fail" = "Fail"
"success" = " Success" "success" = "Success"
"getVersion" = "Get version" "getVersion" = "Get version"
"install" = "Install" "install" = "Install"
"clients" = "Clients" "clients" = "Clients"
"usage" = "Usage" "usage" = "Usage"
"secretToken" = "Secret token" "secretToken" = "Secret Token"
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"setting" = "Panel Setting" "settings" = "Panel Settings"
"logout" = "Logout" "logout" = "Logout"
"link" = "Other" "link" = "Other"
[pages.login] [pages.login]
"title" = "Login" "title" = "Login"
"loginAgain" = "The login time limit has expired, please log in again" "loginAgain" = "The login time limit has expired. Please log in again."
[pages.login.toasts] [pages.login.toasts]
"invalidFormData" = "Input Data Format is Invalid" "invalidFormData" = "Input data format is invalid."
"emptyUsername" = "Please Enter Username" "emptyUsername" = "Please enter username."
"emptyPassword" = "Please Enter Password" "emptyPassword" = "Please enter password."
"wrongUsernameOrPassword" = "Invalid username or password" "wrongUsernameOrPassword" = "Invalid username or password."
"successLogin" = "Login" "successLogin" = "Login"
[pages.index] [pages.index]
@@ -81,21 +81,28 @@
"operationHours" = "Operation Hours" "operationHours" = "Operation Hours"
"operationHoursDesc" = "System uptime: time since startup." "operationHoursDesc" = "System uptime: time since startup."
"systemLoad" = "System Load" "systemLoad" = "System Load"
"connectionCount" = "Number of connections" "connectionCount" = "Number of Connections"
"connectionCountDesc" = "Total connections across all network cards" "connectionCountDesc" = "Total connections across all network cards."
"upSpeed" = "Total upload speed for all network cards" "upSpeed" = "Total upload speed for all network cards."
"downSpeed" = "Total download speed for all network cards" "downSpeed" = "Total download speed for all network cards."
"totalSent" = "Total upload traffic of all network cards since system startup" "totalSent" = "Total upload traffic of all network cards since system startup."
"totalReceive" = "Total download data across all network cards since system startup" "totalReceive" = "Total download data across all network cards since system startup."
"xraySwitchVersionDialog" = "Switch xray version" "xraySwitchVersionDialog" = "Switch Xray Version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to" "xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page" "dontRefresh" = "Installation is in progress, please do not refresh this page."
"logs" = "Logs"
"config" = "Config"
"backup" = "Backup & Restore"
"backupTitle" = "Backup & Restore Database"
"backupDescription" = "Remember to backup before importing a new database."
"exportDatabase" = "Download Database"
"importDatabase" = "Upload Database"
[pages.inbounds] [pages.inbounds]
"title" = "Inbounds" "title" = "Inbounds"
"totalDownUp" = "Total uploads/downloads" "totalDownUp" = "Total Uploads/Downloads"
"totalUsage" = "Total usage" "totalUsage" = "Total Usage"
"inboundCount" = "Number of inbound" "inboundCount" = "Number of Inbounds"
"operate" = "Menu" "operate" = "Menu"
"enable" = "Enable" "enable" = "Enable"
"remark" = "Remark" "remark" = "Remark"
@@ -104,77 +111,79 @@
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"
"expireDate" = "Expire date" "expireDate" = "Expire Date"
"resetTraffic" = "Reset traffic" "resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound" "addInbound" = "Add Inbound"
"generalActions" = "General Actions" "generalActions" = "General Actions"
"addTo" = "Create" "create" = "Create"
"revise" = "Update" "update" = "Update"
"modifyInbound" = "Modify InBound" "modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound" "deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Confirm deletion of inbound?" "deleteInboundContent" = "Confirm deletion of inbound?"
"resetTrafficContent" = "Confirm traffic reset?" "resetTrafficContent" = "Confirm traffic reset?"
"copyLink" = "Copy Link" "copyLink" = "Copy Link"
"address" = "Address" "address" = "Address"
"network" = "Network" "network" = "Network"
"destinationPort" = "Destination port" "destinationPort" = "Destination Port"
"targetAddress" = "Target address" "targetAddress" = "Target Address"
"disableInsecureEncryption" = "Disable insecure encryption" "disableInsecureEncryption" = "Disable Insecure Encryption"
"monitorDesc" = "Leave blank by default" "monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means no limit" "meansNoLimit" = "Means No Limit"
"totalFlow" = "Total flow" "totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave blank to set no expiration" "leaveBlankToNeverExpire" = "Leave Blank to Never Expire"
"noRecommendKeepDefault" = "No special requirements to maintain default settings" "noRecommendKeepDefault" = "No special requirements to maintain default settings"
"certificatePath" = "Certificate file path" "certificatePath" = "Certificate File Path"
"certificateContent" = "Certificate file content" "certificateContent" = "Certificate File Content"
"publicKeyPath" = "Public key path" "publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public key content" "publicKeyContent" = "Public Key Content"
"keyPath" = "Private Key path" "keyPath" = "Private Key Path"
"keyContent" = "Private Key content" "keyContent" = "Private Key Content"
"clickOnQRcode" = "Click on QR Code to Copy" "clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client" "client" = "Client"
"export" = "Export links" "export" = "Export Links"
"Clone" = "Clone" "clone" = "Clone"
"cloneInbound" = "Create" "cloneInbound" = "Clone"
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone" "cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
"cloneInboundOk" = "Creating a clone from" "cloneInboundOk" = "Clone"
"resetAllTraffic" = "Reset All Inbounds Traffic" "resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic" "resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?" "resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetAllTrafficOkText" = "Confirm" "resetAllTrafficOkText" = "Confirm"
"resetAllTrafficCancelText" = "Cancel" "resetAllTrafficCancelText" = "Cancel"
"IPLimit" = "IP Limit"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (Enter 0 to disable IP limit)"
"resetInboundClientTraffics" = "Reset Clients Traffic" "resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all clients traffic" "resetInboundClientTrafficTitle" = "Reset all client traffic"
"resetInboundClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?" "resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
"resetAllClientTraffics" = "Reset All Clients Traffic" "resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic" "resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of all clients ?" "resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
"delDepletedClients" = "Delete depleted clients" "delDepletedClients" = "Delete Depleted Clients"
"delDepletedClientsTitle" = "Delete depleted clients" "delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure to delete all depleted clients ?" "delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
"Email" = "Email" "email" = "Email"
"EmailDesc" = "Please provide a unique email address" "emailDesc" = "Please provide a unique email address."
"IPLimit" = "IP Limit"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (enter 0 to disable IP limit)."
"IPLimitlog" = "IP Log" "IPLimitlog" = "IP Log"
"IPLimitlogDesc" = "IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)" "IPLimitlogDesc" = "IPs history log (before enabling inbound after it has been disabled by IP limit, you should clear the log)."
"IPLimitlogclear" = "Clear The Log" "IPLimitlogclear" = "Clear The Log"
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"XTLSdec" = "Xray core needs to be 1.7.5" "xtlsDesc" = "Xray core needs to be 1.7.5"
"Realitydec" = "Xray core needs to be 1.8.0 and above" "realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
[pages.client] [pages.client]
"add" = "Add client" "add" = "Add Client"
"edit" = "Edit client" "edit" = "Edit Client"
"submitAdd" = "Add client" "submitAdd" = "Add Client"
"submitEdit" = "Save changes" "submitEdit" = "Save changes"
"clientCount" = "Number of clients" "clientCount" = "Number of Clients"
"bulk" = "Add bulk" "bulk" = "Add Bulk"
"method" = "Method" "method" = "Method"
"first" = "First" "first" = "First"
"last" = "Last" "last" = "Last"
"prefix" = "Prefix" "prefix" = "Prefix"
"postfix" = "postfix" "postfix" = "Postfix"
"delayedStart" = "Start after first use" "delayedStart" = "Start after first use"
"expireDays" = "Expire days" "expireDays" = "Expire days"
"days" = "day(s)" "days" = "day(s)"
@@ -199,113 +208,121 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "Encryption" "encryption" = "Encryption"
[pages.setting] [pages.settings]
"title" = "Setting" "title" = "Settings"
"save" = "Save" "save" = "Save"
"restartPanel" = "Restart Panel" "restartPanel" = "Restart Panel "
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information" "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
"actions" = "Actions" "actions" = "Actions"
"resetDefaultConfig" = "Reset to default config" "resetDefaultConfig" = "Reset to Default Configuration"
"panelConfig" = "Panel Configuration" "panelSettings" = "Panel Settings"
"userSetting" = "User Setting" "securitySettings" = "Security Settings"
"xrayConfiguration" = "Xray Configuration" "xrayConfiguration" = "Xray Configuration"
"TGReminder" = "TG Reminder Related Settings" "TGBotSettings" = "Telegram Bot Settings"
"otherSetting" = "Other Setting" "panelListeningIP" = "Panel Listening IP"
"panelListeningIP" = "Panel listening IP" "panelListeningIPDesc" = "Leave blank by default to monitor all IPs. Restart the panel to apply changes."
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
"panelPort" = "Panel Port" "panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to take effect" "panelPortDesc" = "Restart the panel to apply changes."
"publicKeyPath" = "Panel certificate public key file path" "publicKeyPath" = "Panel Certificate Public Key File Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" "publicKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"privateKeyPath" = "Panel certificate private key file path" "privateKeyPath" = "Panel Certificate Private Key File Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" "privateKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"panelUrlPath" = "panel url root path" "panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect" "panelUrlPathDesc" = "Must start with '/' and end with '/'. Restart the panel to apply changes."
"oldUsername" = "Current Username" "oldUsername" = "Current Username"
"currentPassword" = "Current Password" "currentPassword" = "Current Password"
"newUsername" = "New Username" "newUsername" = "New Username"
"newPassword" = "New Password" "newPassword" = "New Password"
"basicTemplate" = "Basic Template" "telegramBotEnable" = "Enable Telegram bot"
"advancedTemplate" = "Advanced Template parts" "telegramBotEnableDesc" = "Restart the panel to take effect."
"completeTemplate" = "Complete Template of Xray configuration"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "This options will prevent users from connecting to specific protocols and websites."
"countryConfigs" = "Country Configs"
"countryConfigsDesc" = "This options will prevent users from connecting to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "This options will be route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using this options, Install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
"xrayConfigTorrent" = "Ban bittorrent usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using bittorrent by users, restart the panel to take effect"
"xrayConfigPrivateIp" = "Ban private IP ranges to connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block Ads, restart the panel to take effect"
"xrayConfigPorn" = "Block Porn Websites"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to Porn websites, restart the panel to take effect"
"xrayConfigIRIp" = "Ban Iran IP ranges to connect"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges, restart the panel to take effect"
"xrayConfigIRDomain" = "Ban Iran Domains to connect"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains, restart the panel to take effect"
"xrayConfigChinaIp" = "Ban China IP ranges to connect"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges, restart the panel to take effect"
"xrayConfigChinaDomain" = "Ban China Domains to connect"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains, restart the panel to take effect"
"xrayConfigRussiaIp" = "Ban Russia IP ranges to connect"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges, restart the panel to take effect"
"xrayConfigRussiaDomain" = "Ban Russia Domains to connect"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains, restart the panel to take effect"
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for google to connect with IPv4, restart the panel to take effect"
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4, restart the panel to take effect"
"xrayConfigGoogleWARP" = "Route Google to WARP"
"xrayConfigGoogleWARPDesc" = "Add routing for Google to WARP, restart the panel to take effect"
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) to WARP"
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) to WARP, restart the panel to take effect"
"xrayConfigNetflixWARP" = "Route Netflix to WARP"
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix to WARP, restart the panel to take effect"
"xrayConfigSpotifyWARP" = "Route Spotify to WARP"
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify to WARP, restart the panel to take effect"
"xrayConfigIRWARP" = "Route Iran Domains to WARP"
"xrayConfigIRWARPDesc" = "Add routing for Iran Domains to WARP. restart the panel to take effect"
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept special clients, restart the panel to take effect"
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server, restart the panel to take effect"
"xrayConfigRoutings" = "Configuration of Routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration template to define Routing rules for this server, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect" "telegramTokenDesc" = "Restart the panel to take effect."
"telegramChatId" = "Telegram Admin ChatIds" "telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect" "telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs. Restart the panel to apply changes."
"telegramNotifyTime" = "Telegram bot notification time" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect" "telegramNotifyTimeDesc" = "Use Crontab timing format. Restart the panel to apply changes."
"tgNotifyBackup" = "Database backup" "tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect" "tgNotifyBackupDesc" = "Include database backup file with report notification. Restart the panel to apply changes."
"sessionMaxAge" = "Session maximum age" "sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)" "sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
"expireTimeDiff" = "Exhaustion time threshold" "expireTimeDiff" = "Expiration threshold for notification"
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)" "expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
"trafficDiff" = "Exhaustion traffic threshold" "trafficDiff" = "Traffic threshold for notification"
"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)" "trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
"tgNotifyCpu" = "CPU percentage alert threshold" "tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)" "tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
"timeZonee" = "Time Zone" "timeZone" = "Time zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect" "timeZoneDesc" = "Scheduled tasks run according to the time in this time zone. Restart the panel to apply changes."
"loginSecurity" = "Login security"
"loginSecurityDesc" = "Toggle additional step in user login page"
"secretToken" = "Secret Token"
"secretTokenDesc" = "Copy this secret token and keep it in a safe place; without this you won't be able to login. This can not be recovered from x-ui command tool neither"
[pages.setting.toasts] [pages.settings.templates]
"modifySetting" = "Modify setting" "title" = "Templates"
"getSetting" = "Get setting" "basicTemplate" = "Basic Template"
"modifyUser" = "Modify user" "advancedTemplate" = "Advanced Template"
"originalUserPassIncorrect" = "The original user name or original password is incorrect" "completeTemplate" = "Complete Template"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"countryConfigs" = "Country Configs"
"countryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template. Restart the panel to apply changes."
"xrayConfigTorrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users. Restart the panel to apply changes."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges. Restart the panel to apply changes."
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads. Restart the panel to apply changes."
"xrayConfigPorn" = "Block Porn Websites"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to porn websites. Restart the panel to apply changes."
"xrayConfigSpeedtest" = "Block Speedtest Websites"
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites. Restart the panel to apply changes."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges. Restart the panel to apply changes."
"xrayConfigIRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains. Restart the panel to apply changes."
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges. Restart the panel to apply changes."
"xrayConfigChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains. Restart the panel to apply changes."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges. Restart the panel to apply changes."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains. Restart the panel to apply changes."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4. Restart the panel to apply changes."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4. Restart the panel to apply changes."
"xrayConfigGoogleWARP" = "Route Google through WARP."
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP. Restart the panel to apply changes."
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP. Restart the panel to apply changes."
"xrayConfigNetflixWARP" = "Route Netflix through WARP."
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP. Restart the panel to apply changes."
"xrayConfigSpotifyWARP" = "Route Spotify through WARP."
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP. Restart the panel to apply changes."
"xrayConfigIRWARP" = "Route Iran domains through WARP."
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP. Restart the panel to apply changes."
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients. Restart the panel to apply changes."
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server. Restart the panel to apply changes."
"xrayConfigRoutings" = "Configuration of routing rules."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server. Restart the panel to apply changes."
[pages.settings.security]
"admin" = "Admin"
"secret" = "Secret Token"
"loginSecurity" = "Login security"
"loginSecurityDesc" = "Enable additional user login security step"
"secretToken" = "Secret Token"
"secretTokenDesc" = "Please copy and securely store this token in a safe place. This token is required for login and cannot be recovered from the x-ui command tool."
[pages.settings.toasts]
"modifySettings" = "Modify Settings "
"getSettings" = "Get Settings "
"modifyUser" = "Modify User "
"originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty" "userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -52,8 +52,8 @@
[menu] [menu]
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها" "inbounds" = "سرویس ها"
"setting" = "تنظیمات پنل" "settings" = "تنظیمات پنل"
"logout" = "خروج" "logout" = "خروج"
"link" = "دیگر" "link" = "دیگر"
@@ -89,7 +89,14 @@
"totalReceive" = "جمع کل ترافیک دانلود مصرفی" "totalReceive" = "جمع کل ترافیک دانلود مصرفی"
"xraySwitchVersionDialog" = "تغییر ورژن" "xraySwitchVersionDialog" = "تغییر ورژن"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین" "xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید " "dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
"logs" = "گزارش ها"
"config" = "تنظیمات"
"backup" = "پشتیبان گیری و بازیابی"
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
"exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس"
[pages.inbounds] [pages.inbounds]
"title" = "کاربران" "title" = "کاربران"
@@ -108,8 +115,8 @@
"resetTraffic" = "ریست ترافیک" "resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس" "addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی" "generalActions" = "عملیات کلی"
"addTo" = "اضافه کردن" "create" = "اضافه کردن"
"revise" = "ویرایش" "update" = "ویرایش"
"modifyInbound" = "ویرایش سرویس" "modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس" "deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟" "deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
@@ -134,7 +141,7 @@
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید" "clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر" "client" = "کاربر"
"export" = "استخراج لینکها" "export" = "استخراج لینکها"
"Clone" = "شبیه سازی" "clone" = "شبیه سازی"
"cloneInbound" = "ایجاد" "cloneInbound" = "ایجاد"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد" "cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
"cloneInboundOk" = "ساختن شبیه ساز" "cloneInboundOk" = "ساختن شبیه ساز"
@@ -150,16 +157,18 @@
"delDepletedClients" = "حذف کاربران منقضی" "delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی" "delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟" "delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"email" = "ایمیل"
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"IPLimit" = "محدودیت ای پی" "IPLimit" = "محدودیت ای پی"
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )" "IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
"Email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"IPLimitlog" = "گزارش ها" "IPLimitlog" = "گزارش ها"
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)" "IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
"IPLimitlogclear" = "پاک کردن گزارش ها" "IPLimitlogclear" = "پاک کردن گزارش ها"
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"XTLSdec" = "هسته Xray باید 1.7.5 باشد" "xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"Realitydec" = "هسته Xray باید 1.8.0 و بالاتر باشد" "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client] [pages.client]
"add" = "کاربر جدید" "add" = "کاربر جدید"
@@ -197,18 +206,17 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "رمزنگاری" "encryption" = "رمزنگاری"
[pages.setting] [pages.settings]
"title" = "تنظیمات" "title" = "تنظیمات"
"save" = "ذخیره" "save" = "ذخیره"
"restartPanel" = "ریستارت پنل" "restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید" "restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"actions" = "عملیات ها" "actions" = "عملیات ها"
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض" "resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelConfig" = "تنظیمات پنل" "panelSettings" = "تنظیمات پنل"
"userSetting" = "تنظیمات مدیر" "securitySettings" = "تنظیمات امنیتی"
"xrayConfiguration" = "تنظیمات Xray" "xrayConfiguration" = "تنظیمات Xray"
"TGReminder" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"otherSetting" = "دیگر تنظیمات"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
@@ -223,9 +231,32 @@
"currentPassword" = "رمز عبور فعلی" "currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید" "newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید" "newPassword" = "رمز عبور جدید"
"basicTemplate" = "بخش پایه" "telegramBotEnable" = "فعالسازی ربات تلگرام"
"advancedTemplate" = "بخش های پیشرفته الگو" "telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
[pages.settings.templates]
"title" = "الگوها"
"basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی" "generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند." "generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند."
"countryConfigs" = "تنظیمات برای کشورها" "countryConfigs" = "تنظیمات برای کشورها"
@@ -244,6 +275,8 @@
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن" "xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن"
"xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigSpeedtest" = "جلوگیری از اتصال به سایت های تست سرعت"
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران" "xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران" "xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
@@ -276,34 +309,18 @@
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی" "xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" [pages.settings.security]
"telegramToken" = "توکن تلگرام" "admin" = "مدیر"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "secret" = "توکن امنیتی"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
"loginSecurity" = "لاگین ایمن" "loginSecurity" = "لاگین ایمن"
"loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین" "loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین"
"secretToken" = "توکن امنیتی" "secretToken" = "توکن امنیتی"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه داری، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!" "secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه داری، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
[pages.setting.toasts] [pages.settings.toasts]
"modifySetting" = "ویرایش تنظیمات" "modifySettings" = "ویرایش تنظیمات"
"getSetting" = "دریافت تنظیمات" "getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر" "modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ." "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ." "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."

View File

@@ -0,0 +1,328 @@
"username" = "имя пользователя"
"password" = "пароль"
"login" = "логин"
"confirm" = "подтвердить"
"cancel" = "отмена"
"close" = "закрыть"
"copy" = "копировать"
"copied" = "скопировано"
"download" = "скачать"
"remark" = "примечание"
"enable" = "включить"
"protocol" = "протокол"
"search" = "поиск"
"loading" = "загрузка"
"second" = "секунда"
"minute" = "минута"
"hour" = "час"
"day" = "день"
"check" = "просмотр"
"indefinite" = "бессрочно"
"unlimited" = "безлимитно"
"none" = "пусто"
"qrCode" = "QR-код"
"info" = "больше информации"
"edit" = "изменить"
"delete" = "удалить"
"reset" = "обнулить"
"copySuccess" = "скопировано"
"sure" = "да"
"encryption" = "Шифрование"
"transmission" = "протокол передачи"
"host" = "хост"
"path" = "путь"
"camouflage" = "маскировка"
"status" = "статус"
"enabled" = "включено"
"disabled" = "отключено"
"depleted" = "исчерпано"
"depletingSoon" = "почти исчерпано"
"domainName" = "домен"
"additional" = "допольнительно"
"monitor" = "порт IP"
"certificate" = "сертификат"
"fail" = "неудача"
"success" = "успешно"
"getVersion" = "узнать версию"
"install" = "установка"
"clients" = "клиенты"
"usage" = "использование"
"secretToken" = "секретный токен"
[menu]
"dashboard" = "статус системы"
"inbounds" = "пользователи"
"settings" = "настройки"
"logout" = "выход"
"link" = "другое"
[pages.login]
"title" = "логин"
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова."
[pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных."
"emptyUsername" = "Введите имя пользователя."
"emptyPassword" = "Введите пароль."
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль."
"successLogin" = "успешный вход"
[pages.index]
"title" = "статус системы"
"memory" = "память"
"hard" = "жесткий диск"
"xrayStatus" = "статус Xray"
"stopXray" = "стоп"
"restartXray" = "рестарт Xray"
"xraySwitch" = "переключить версию"
"xraySwitchClick" = "Выберите желаемую версию."
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями."
"operationHours" = "Часы работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети."
"systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»."
"upSpeed" = "Общая скорость upload."
"downSpeed" = "Общая скорость download."
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы.."
"xraySwitchVersionDialog" = "переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу."
"logs" = "Логи"
"config" = "Конфиг"
"backup" = "Бекап и восстановление"
"backupTitle" = "База данных бекапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных."
"exportDatabase" = "Экспорт базы данных"
"importDatabase" = "Импорт базы данных"
[pages.inbounds]
"title" = "пользователи"
"totalDownUp" = "Всего входящих/исходящих"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"traffic" = "Траффик"
"details" = "Подробнее"
"transportConfig" = "Перенести"
"expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик"
"addInbound" = "Добавить пользователя"
"generalActions" = "Общие действия"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя"
"deleteInboundContent" = "Подтвердите удаление пользователя?"
"resetTrafficContent" = "Подтвердите обнуление траффика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым = бессрочно"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
"publicKeyPath" = "Путь к публичному ключу"
"publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу"
"keyContent" = "Содержимое приватного ключа"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"client" = "Клиент"
"export" = "Поделиться ключом"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы."
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Обнулить весь траффик"
"resetAllTrafficTitle" = "Обнуление всего траффика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetAllTrafficOkText" = "Подтвердить"
"resetAllTrafficCancelText" = "Отмена"
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"delDepletedClients" = "Удалить отключенных пользователей"
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "ограничение по IP"
"IPLimitDesc" = "Отключить ключ, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)."
"IPLimitlog" = "IP лог"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)."
"IPLimitlogclear" = "Очистить список"
"setDefaultCert" = "Установить сертификат с панели"
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
"realityDesc" = "Версия Xray должна быть 1.8.0 или выше."
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов."
[pages.client]
"add" = "Добавить пользователя"
"edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество пользователей"
"bulk" = "Добавить несколько"
"method" = "Метод"
"first" = "Первый"
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начать со времени первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
[pages.inbounds.toasts]
"obtain" = "Получить"
[pages.inbounds.stream.general]
"requestHeader" = "Требуется заголовок"
"name" = "Имя"
"value" = "Значение"
[pages.inbounds.stream.tcp]
"requestVersion" = "Требуется версия"
"requestMethod" = "Требуется метод"
"requestPath" = "Требуется путь"
"responseVersion" = "Указать версию"
"responseStatus" = "Указать статус"
"responseStatusDescription" = "Указать примечание статуса"
"responseHeader" = "Указать заголовок"
[pages.inbounds.stream.quic]
"encryption" = "Шифрование"
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"restartPanel" = "Рестарт панели"
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
"actions" = "Действия"
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelSettings" = "Настройки панели"
"securitySettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек."
"panelPort" = "Порт панели"
"panelPortDesc" = "Перезагрузите панель для применения настроек."
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек."
"panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек."
"oldUsername" = "Имя пользователя сейчас"
"currentPassword" = "Пароль сейчас"
"newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек."
"telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Перезагрузите панель для применения настроек."
"telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек."
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек."
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек."
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог траффика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
"timeZone" = "Временная зона"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек."
[pages.settings.templates]
"title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
"countryConfigs" = "Настройки по гео"
"countryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4 "
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4."
"warpConfigs" = "Настройки WARP"
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare."
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек."
"xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек."
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек."
"xrayConfigAds" = "Бокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек."
"xrayConfigPorn" = "Блокировка порносайтов"
"xrayConfigPornDesc" = "Измените конфигурацию, чтобы отключить подключения к порносайтам. Перезагрузите панель для применения настроек."
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
"xrayConfigSpeedtestDesc" = "Измените шаблон конфигурации, чтобы избежать подключения к веб-сайтам для тестирования скорости. Перезапустите панель, чтобы применить изменения."
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек."
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек."
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек."
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек."
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек."
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек."
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек."
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек."
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP."
"xrayConfigGoogleWARPDesc" = "Применить маршрутизацию Google через WARP. Перезагрузите панель для применения настроек."
"xrayConfigOpenAIWARP" = "Маршрут OpenAI (ChatGPT) через WARP."
"xrayConfigOpenAIWARPDesc" = "Применить маршрутизацию для OpenAI (ChatGPT) через WARP. Перезагрузите панель для применения настроек."
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP."
"xrayConfigNetflixWARPDesc" = "Применить маршрутизацию Netflix через WARP. Перезагрузите панель для применения настроек."
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP."
"xrayConfigSpotifyWARPDesc" = "Применить маршрутизацию Spotify через WARP. Перезагрузите панель для применения настроек."
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP."
"xrayConfigIRWARPDesc" = "Применить маршрутизацию для доменов Ирана через WARP. Перезагрузите панель для применения настроек."
"xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек."
"xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек."
"xrayConfigRoutings" = "Настройка правил маршрутизации."
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек."
[pages.settings.security]
"admin" = "Админ"
"secret" = "Секретный токен"
"loginSecurity" = "Безопасность входа"
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
"secretToken" = "Секретный токен"
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью командного инструмента x-ui."
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
"getSettings" = "Просмотр настроек"
"modifyUser" = "Изменение пользователя "
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"

View File

@@ -53,7 +53,7 @@
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
"inbounds" = "入站列表" "inbounds" = "入站列表"
"setting" = "面板设置" "settings" = "面板设置"
"logout" = "退出登录" "logout" = "退出登录"
"link" = "其他" "link" = "其他"
@@ -89,7 +89,14 @@
"totalReceive" = "系统启动以来所有网卡的总下载流量" "totalReceive" = "系统启动以来所有网卡的总下载流量"
"xraySwitchVersionDialog" = "切换 xray 版本" "xraySwitchVersionDialog" = "切换 xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至" "xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面" "dontRefresh" = "安装中,请不要刷新此页面"
"logs" = "日志"
"config" = "配置"
"backup" = "备份还原"
"backupTitle" = "备份和恢复数据库"
"backupDescription" = "请记住在导入新数据库之前进行备份。"
"exportDatabase" = "下载数据库"
"importDatabase" = "上传数据库"
[pages.inbounds] [pages.inbounds]
"title" = "入站列表" "title" = "入站列表"
@@ -108,8 +115,8 @@
"resetTraffic" = "重置流量" "resetTraffic" = "重置流量"
"addInbound" = "添加入" "addInbound" = "添加入"
"generalActions" = "通用操作" "generalActions" = "通用操作"
"addTo" = "添加" "create" = "添加"
"revise" = "修改" "update" = "修改"
"modifyInbound" = "修改入站" "modifyInbound" = "修改入站"
"deleteInbound" = "删除入站" "deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?" "deleteInboundContent" = "确定要删除入站吗?"
@@ -134,7 +141,7 @@
"clickOnQRcode" = "点击二维码复制" "clickOnQRcode" = "点击二维码复制"
"client" = "客户" "client" = "客户"
"export" = "导出链接" "export" = "导出链接"
"Clone" = "克隆" "clone" = "克隆"
"cloneInbound" = "创造" "cloneInbound" = "创造"
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆" "cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
"cloneInboundOk" = "从创建克隆" "cloneInboundOk" = "从创建克隆"
@@ -150,16 +157,18 @@
"delDepletedClients" = "删除耗尽的客户端" "delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户" "delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?" "delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"email" = "电子邮件"
"emailDesc" = "电子邮件必须完全唯"
"IPLimit" = "IP限制" "IPLimit" = "IP限制"
"IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip" "IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip"
"Email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯"
"IPLimitlog" = "IP日志" "IPLimitlog" = "IP日志"
"IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志" "IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志"
"IPLimitlogclear" = "清除日志" "IPLimitlogclear" = "清除日志"
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"XTLSdec" = "Xray核心需要1.7.5" "xtlsDesc" = "Xray核心需要1.7.5"
"Realitydec" = "Xray核心需要1.8.0及以上版本" "realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID您可以在此处获取 @userinfobot"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client] [pages.client]
"add" = "添加客户端" "add" = "添加客户端"
@@ -197,18 +206,17 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "加密" "encryption" = "加密"
[pages.setting] [pages.settings]
"title" = "设置" "title" = "设置"
"save" = "保存配置" "save" = "保存配置"
"restartPanel" = "重启面板" "restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息" "restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"actions" = "动作" "actions" = "动作"
"resetDefaultConfig" = "重置为默认配置" "resetDefaultConfig" = "重置为默认配置"
"panelConfig" = "面板配置" "panelSettings" = "面板配置"
"userSetting" = "用户设置" "securitySettings" = "安全设定"
"xrayConfiguration" = "xray 相关设置" "xrayConfiguration" = "xray 相关设置"
"TGReminder" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"otherSetting" = "其他设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP重启面板生效" "panelListeningIPDesc" = "默认留空监听所有 IP重启面板生效"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
@@ -223,6 +231,29 @@
"currentPassword" = "原密码" "currentPassword" = "原密码"
"newUsername" = "新用户名" "newUsername" = "新用户名"
"newPassword" = "新密码" "newPassword" = "新密码"
"telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
"trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZone" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
[pages.settings.templates]
"title" = "模板"
"basicTemplate" = "基本模板" "basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件" "advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板" "completeTemplate" = "Xray 配置的完整模板"
@@ -244,6 +275,8 @@
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效" "xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效"
"xrayConfigPorn" = "禁止色情网站连接" "xrayConfigPorn" = "禁止色情网站连接"
"xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效" "xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效"
"xrayConfigSpeedtest" = "阻止测速网站"
"xrayConfigSpeedtestDesc" = "更改配置模板以避免连接到速度测试网站。 重新启动面板以应用更改。"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接" "xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段重启面板生效" "xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段重启面板生效"
"xrayConfigIRDomain" = "禁止伊朗域连接" "xrayConfigIRDomain" = "禁止伊朗域连接"
@@ -276,34 +309,18 @@
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效" "xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置" "xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效" "xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效" [pages.settings.security]
"telegramToken" = "电报机器人TOKEN" "admin" = "行政"
"telegramTokenDesc" = "重启面板生效" "secret" = "秘密令牌"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
"trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
"loginSecurity" = "登录安全" "loginSecurity" = "登录安全"
"loginSecurityDesc" = "在用户登录页面中切换附加步骤" "loginSecurityDesc" = "在用户登录页面中切换附加步骤"
"secretToken" = "秘密令牌" "secretToken" = "秘密令牌"
"secretTokenDesc" = "复制此秘密令牌并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复" "secretTokenDesc" = "复制此秘密令牌并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
[pages.setting.toasts] [pages.settings.toasts]
"modifySetting" = "修改设置" "modifySettings" = "修改设置"
"getSetting" = "获取设置" "getSettings" = "获取设置"
"modifyUser" = "修改用户" "modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误" "originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空"

View File

@@ -33,9 +33,6 @@ import (
//go:embed assets/* //go:embed assets/*
var assetsFS embed.FS var assetsFS embed.FS
//go:embed assets/favicon.ico
var favicon []byte
//go:embed html/* //go:embed html/*
var htmlFS embed.FS var htmlFS embed.FS
@@ -86,7 +83,7 @@ type Server struct {
index *controller.IndexController index *controller.IndexController
server *controller.ServerController server *controller.ServerController
xui *controller.XUIController panel *controller.XUIController
api *controller.APIController api *controller.APIController
sub *controller.SUBController sub *controller.SUBController
@@ -150,6 +147,28 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
return t, nil return t, nil
} }
func redirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel'
path := c.Request.URL.Path
redirects := map[string]string{
"panel/API": "panel/api",
"xui/API": "panel/api",
"xui": "panel",
}
for from, to := range redirects {
from, to = basePath+from, basePath+to
if strings.HasPrefix(path, from) {
newPath := to + path[len(from):]
c.Redirect(http.StatusMovedPermanently, newPath)
c.Abort()
return
}
}
c.Next()
}
}
func (s *Server) initRouter() (*gin.Engine, error) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@@ -161,11 +180,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() engine := gin.Default()
// Add favicon
engine.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", favicon)
})
secret, err := s.settingService.GetSecret() secret, err := s.settingService.GetSecret()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -211,11 +225,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
} }
// Apply the redirect middleware (`/xui` to `/panel`)
engine.Use(redirectMiddleware(basePath))
g := engine.Group(basePath) g := engine.Group(basePath)
s.index = controller.NewIndexController(g) s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g) s.server = controller.NewServerController(g)
s.xui = controller.NewXUIController(g) s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g) s.api = controller.NewAPIController(g)
s.sub = controller.NewSUBController(g) s.sub = controller.NewSUBController(g)

209
x-ui.sh
View File

@@ -42,7 +42,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
fi fi
@@ -59,13 +59,13 @@ fi
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# > 1 ]]; then
echo && read -p "$1 [Default $2]: " temp echo && read -p "$1 [Default $2]: " temp
if [[ x"${temp}" == x"" ]]; then if [[ "${temp}" == "" ]]; then
temp=$2 temp=$2
fi fi
else else
read -p "$1 [y/n]: " temp read -p "$1 [y/n]: " temp
fi fi
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -342,7 +342,7 @@ check_status() {
return 2 return 2
fi fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ x"${temp}" == x"running" ]]; then if [[ "${temp}" == "running" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -351,7 +351,7 @@ check_status() {
check_enabled() { check_enabled() {
temp=$(systemctl is-enabled x-ui) temp=$(systemctl is-enabled x-ui)
if [[ x"${temp}" == x"enabled" ]]; then if [[ "${temp}" == "enabled" ]]; then
return 0 return 0
else else
return 1 return 1
@@ -431,35 +431,8 @@ show_xray_status() {
fi fi
} }
#this will be an entrance for ssl cert issue
#here we can provide two different methods to issue cert
#first.standalone mode second.DNS API mode
ssl_cert_issue() {
local method=""
echo -E ""
LOGD "********Usage********"
LOGI "this shell script will use acme to help issue certs."
LOGI "here we provide two methods for issuing certs:"
LOGI "method 1:acme standalone mode,need to keep port:80 open"
LOGI "method 2:acme DNS API mode,need provide Cloudflare Global API Key"
LOGI "recommend method 2 first,if it fails,you can try method 1."
LOGI "certs will be installed in /root/cert directory"
read -p "please choose which method do you want,type 1 or 2": method
LOGI "you choosed method:${method}"
if [ "${method}" == "1" ]; then
ssl_cert_issue_standalone
elif [ "${method}" == "2" ]; then
ssl_cert_issue_by_cloudflare
else
LOGE "invalid input,please check it..."
exit 1
fi
}
open_ports() { open_ports() {
if ! command -v ufw &> /dev/null if ! command -v ufw &>/dev/null; then
then
echo "ufw firewall is not installed. Installing now..." echo "ufw firewall is not installed. Installing now..."
sudo apt-get update sudo apt-get update
sudo apt-get install -y ufw sudo apt-get install -y ufw
@@ -486,22 +459,23 @@ open_ports() {
# Check if the input is valid # Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2; exit 1 echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
exit 1
fi fi
# Open the specified ports using ufw # Open the specified ports using ufw
IFS=',' read -ra PORT_LIST <<< "$ports" IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then if [[ $port == *-* ]]; then
# Split the range into start and end ports # Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1) start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2) end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port # Loop through the range and open each port
for ((i=start_port; i<=end_port; i++)); do for ((i = start_port; i <= end_port; i++)); do
sudo ufw allow $i sudo ufw allow $i
done done
else else
sudo ufw allow "$port" sudo ufw allow "$port"
fi fi
done done
@@ -544,7 +518,7 @@ install_acme() {
} }
#method for standalone mode #method for standalone mode
ssl_cert_issue_standalone() { ssl_cert_issue() {
#check for acme.sh first #check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it" echo "acme.sh could not be found. we will install it"
@@ -555,7 +529,7 @@ ssl_cert_issue_standalone() {
fi fi
fi fi
#install socat second #install socat second
if [[ x"${release}" == x"centos" ]]; then if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then
yum install socat -y yum install socat -y
else else
apt install socat -y apt install socat -y
@@ -569,7 +543,7 @@ ssl_cert_issue_standalone() {
#get the domain here,and we need verify it #get the domain here,and we need verify it
local domain="" local domain=""
read -p "please input your domain:" domain read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..." LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already #here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
@@ -581,16 +555,16 @@ ssl_cert_issue_standalone() {
else else
LOGI "your domain is ready for issuing cert now..." LOGI "your domain is ready for issuing cert now..."
fi fi
#create a directory for install cert #create a directory for install cert
certPath="/root/cert/${domain}" certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then if [ ! -d "$certPath" ]; then
mkdir -p "$certPath" mkdir -p "$certPath"
else else
rm -rf "$certPath" rm -rf "$certPath"
mkdir -p "$certPath" mkdir -p "$certPath"
fi fi
#get needed port here #get needed port here
local WebPort=80 local WebPort=80
read -p "please choose which port do you use,default will be 80 port:" WebPort read -p "please choose which port do you use,default will be 80 port:" WebPort
@@ -621,108 +595,19 @@ ssl_cert_issue_standalone() {
else else
LOGI "install certs succeed,enable auto renew..." LOGI "install certs succeed,enable auto renew..."
fi fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
} ~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
#method for DNS API mode LOGE "auto renew failed, certs details:"
ssl_cert_issue_by_cloudflare() { ls -lah cert/*
echo -E "" chmod 755 $certPath/*
LOGD "******Preconditions******" exit 1
LOGI "1.need Cloudflare account associated email"
LOGI "2.need Cloudflare Global API Key"
LOGI "3.your domain use Cloudflare as resolver"
confirm "I have confirmed all these info above[y/n]" "y"
if [ $? -eq 0 ]; then
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed,please check logs"
exit 1
fi
CF_Domain=""
CF_GlobalKey=""
CF_AccountEmail=""
LOGD "please input your domain:"
read -p "Input your domain here:" CF_Domain
LOGD "your domain is:${CF_Domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${CF_Domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already have certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
#create a directory for install cert
certPath="/root/cert/${CF_Domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
LOGD "please inout your cloudflare global API key:"
read -p "Input your key here:" CF_GlobalKey
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
LOGD "please input your cloudflare account email:"
read -p "Input your email here:" CF_AccountEmail
LOGD "your cloudflare account email:${CF_AccountEmail}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
if [ $? -ne 0 ]; then
LOGE "change the default CA to Lets'Encrypt failed,exit"
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email=${CF_AccountEmail}
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
if [ $? -ne 0 ]; then
LOGE "issue cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate issued Successfully, Installing..."
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--key-file /root/cert/${CF_Domain}/privkey.pem \
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
if [ $? -ne 0 ]; then
LOGE "install cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate installed Successfully,Turning on automatic updates..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
else else
show_menu LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi fi
} }
warp_fixchatgpt() { warp_fixchatgpt() {
@@ -733,21 +618,21 @@ warp_fixchatgpt() {
run_speedtest() { run_speedtest() {
# Check if Speedtest is already installed # Check if Speedtest is already installed
if ! command -v speedtest &> /dev/null; then if ! command -v speedtest &>/dev/null; then
# If not installed, install it # If not installed, install it
if command -v dnf &> /dev/null; then if command -v dnf &>/dev/null; then
sudo dnf install -y curl sudo dnf install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo dnf install -y speedtest sudo dnf install -y speedtest
elif command -v yum &> /dev/null; then elif command -v yum &>/dev/null; then
sudo yum install -y curl sudo yum install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo yum install -y speedtest sudo yum install -y speedtest
elif command -v apt-get &> /dev/null; then elif command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y curl sudo apt-get update && sudo apt-get install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt-get install -y speedtest sudo apt-get install -y speedtest
elif command -v apt &> /dev/null; then elif command -v apt &>/dev/null; then
sudo apt update && sudo apt install -y curl sudo apt update && sudo apt install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt install -y speedtest sudo apt install -y speedtest
@@ -761,8 +646,6 @@ run_speedtest() {
speedtest speedtest
} }
show_usage() { show_usage() {
echo "x-ui control menu usages: " echo "x-ui control menu usages: "
echo "------------------------------------------" echo "------------------------------------------"
@@ -874,7 +757,7 @@ show_menu() {
19) 19)
warp_fixchatgpt warp_fixchatgpt
;; ;;
20) 20)
run_speedtest run_speedtest
;; ;;
*) *)

View File

@@ -6,7 +6,7 @@ import (
) )
type InboundConfig struct { type InboundConfig struct {
Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串 Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
Port int `json:"port"` Port int `json:"port"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Settings json_util.RawMessage `json:"settings"` Settings json_util.RawMessage `json:"settings"`