Compare commits

...

73 Commits

Author SHA1 Message Date
semantic-release-bot f60d40c06d 🔖 chore(release): v1.0.0-beta.7 [skip ci]
# [1.0.0-beta.7](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2025-01-28)

### ♻ Code Refactoring

* **sbscribe**: Rename and reorganize components for better structure and clarity ([5e5e4ed](https://github.com/perfect-panel/ppanel-web/commit/5e5e4ed))

###  Features

* **auth**: Add email and SMS code sending functionality with localization updates ([57eaa55](https://github.com/perfect-panel/ppanel-web/commit/57eaa55))
* **auth**: Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple ([18ee600](https://github.com/perfect-panel/ppanel-web/commit/18ee600))
* **auth**: Add SMS and email configuration options to global store and update localization ([4acf7b1](https://github.com/perfect-panel/ppanel-web/commit/4acf7b1))
* **auth**: Enhance user registration with invite handling and logo display ([207bc24](https://github.com/perfect-panel/ppanel-web/commit/207bc24))
* **auth**: Redirect user after OAuth login and add logos icon collection ([aa6dda8](https://github.com/perfect-panel/ppanel-web/commit/aa6dda8))
* **config**: Add application selection and encryption settings to configuration form ([88b3504](https://github.com/perfect-panel/ppanel-web/commit/88b3504))
* **config**: Update encryption fields in configuration form and refactor OAuth callback parameters ([652e032](https://github.com/perfect-panel/ppanel-web/commit/652e032))
* **global**: Add SMS configuration options to global store ([39a9ce6](https://github.com/perfect-panel/ppanel-web/commit/39a9ce6))
* **locales**: Add area code and telephone fields to user forms in multiple languages ([9b8258c](https://github.com/perfect-panel/ppanel-web/commit/9b8258c))
* **locales**: Add description information of communication keys and encryption methods to enhance client configuration capabilities ([d1f5a9b](https://github.com/perfect-panel/ppanel-web/commit/d1f5a9b))
* **node**: Add tags ([f408fdf](https://github.com/perfect-panel/ppanel-web/commit/f408fdf))
* **node**: Move the node configuration to the server module ([7f0f5ce](https://github.com/perfect-panel/ppanel-web/commit/7f0f5ce))
* **oauth**: Add certification component for handling OAuth login callbacks and improve user authentication flow ([5ed04c0](https://github.com/perfect-panel/ppanel-web/commit/5ed04c0))
* **oauth**: Implement OAuth token retrieval and refactor login callback handling ([40a6f7c](https://github.com/perfect-panel/ppanel-web/commit/40a6f7c))
* **oauth**: Refactor platform parameter handling and improve logout redirection logic ([8346c85](https://github.com/perfect-panel/ppanel-web/commit/8346c85))
* **oauth**: Update OAuth login handling to use callback parameter and improve URL parameter retrieval ([9227411](https://github.com/perfect-panel/ppanel-web/commit/9227411))
* **sms**: Update locales ([938363b](https://github.com/perfect-panel/ppanel-web/commit/938363b))
* **subscribe**: Add 'sold' column to SubscribeTable and update inventory terminology ([19619fd](https://github.com/perfect-panel/ppanel-web/commit/19619fd))
* **subscribe**: Move subscription configuration and application to subscription module ([f90d4d2](https://github.com/perfect-panel/ppanel-web/commit/f90d4d2))
* **subscribe**: Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting ([f26f1c2](https://github.com/perfect-panel/ppanel-web/commit/f26f1c2))
* **tutorial**: Fetch the latest tutorial version from GitHub API for dynamic URL generation ([28f8c78](https://github.com/perfect-panel/ppanel-web/commit/28f8c78))
* **user**: Add telephone input with area code selection and update localization ([585b99c](https://github.com/perfect-panel/ppanel-web/commit/585b99c))
* Update Auth Control ([c59742a](https://github.com/perfect-panel/ppanel-web/commit/c59742a))

### 🐛 Bug Fixes

* **api**: Remove redundant requestType parameter in appleLoginCallback ([0aa5d5b](https://github.com/perfect-panel/ppanel-web/commit/0aa5d5b))
* **api**: Rename app-related functions and types to application for consistency ([9d8b814](https://github.com/perfect-panel/ppanel-web/commit/9d8b814))
* **api**: Update subscription_protocol to subscribe_type for consistency across services ([b6da51b](https://github.com/perfect-panel/ppanel-web/commit/b6da51b))
* **auth**: Change Textarea value to defaultValue for client_secret in Apple auth page ([69fc670](https://github.com/perfect-panel/ppanel-web/commit/69fc670))
* **auth**: Remove unused telephone code login function and update typings for telephone login requests ([7239685](https://github.com/perfect-panel/ppanel-web/commit/7239685))
* **auth**: Require minimum length for invite string when forced invite is enabled ([a604f28](https://github.com/perfect-panel/ppanel-web/commit/a604f28))
* **auth**: Update user authentication flow to include email and phone code verification ([5d078fd](https://github.com/perfect-panel/ppanel-web/commit/5d078fd))
* **dashboard**:  Improve URL encoding for subscription links and enhance success message handling ([4983c33](https://github.com/perfect-panel/ppanel-web/commit/4983c33))
* **dashboard**: Update icon imports for platform consistency and adjust icon size ([3e8912e](https://github.com/perfect-panel/ppanel-web/commit/3e8912e))
* **dashboard**: Update platform detection logic and improve layout responsiveness ([b0aa364](https://github.com/perfect-panel/ppanel-web/commit/b0aa364))
* **deps**: Remove outdated @iconify/react dependency and add iconify-json packages ([d6fbc38](https://github.com/perfect-panel/ppanel-web/commit/d6fbc38))
* **locales**: Add error message for incorrect user information ([52c1d1f](https://github.com/perfect-panel/ppanel-web/commit/52c1d1f))
* **locales**: Add error message for incorrect user information ([3d92902](https://github.com/perfect-panel/ppanel-web/commit/3d92902))
* **locales**: Add logout message to authentication localization files ([1d0d911](https://github.com/perfect-panel/ppanel-web/commit/1d0d911))
* **nav**: Comment out unused social login options to simplify navigation configuration ([cefcb31](https://github.com/perfect-panel/ppanel-web/commit/cefcb31))
* **node-config**: Add null checks for time slots and ensure proper handling of undefined values ([1cdb7e7](https://github.com/perfect-panel/ppanel-web/commit/1cdb7e7))
* **node**: Add country and city fields to the form schema and localization files ([8775fb6](https://github.com/perfect-panel/ppanel-web/commit/8775fb6))
* **oauth**: Refactor OAuth configuration types and update related API methods ([6227ba9](https://github.com/perfect-panel/ppanel-web/commit/6227ba9))
* **oauth**: Remove redundant checks when updating configuration to simplify logic ([9140b8a](https://github.com/perfect-panel/ppanel-web/commit/9140b8a))
* **payment**: Replace window.open with window.location.href for checkout links ([1d8c765](https://github.com/perfect-panel/ppanel-web/commit/1d8c765))
* **phone**: Update SMS expiration time field to use 'sms_expire_time' with default value of 300 ([18b07c7](https://github.com/perfect-panel/ppanel-web/commit/18b07c7))
* **redirect**: Simplify redirect URL logic by removing unnecessary condition for sessionStorage ([c53ac61](https://github.com/perfect-panel/ppanel-web/commit/c53ac61))
* **redirect**: Update redirect URL logic to ensure proper handling of OAuth and auth paths ([7954762](https://github.com/perfect-panel/ppanel-web/commit/7954762))
* **site**: Add image upload functionality for site logo configuration ([4ea6e4a](https://github.com/perfect-panel/ppanel-web/commit/4ea6e4a))
* **sort**: Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance ([331bbea](https://github.com/perfect-panel/ppanel-web/commit/331bbea))
* **subscription**: Add reset functionality for user subscription token ([39e89bf](https://github.com/perfect-panel/ppanel-web/commit/39e89bf))
* **type**: Fix ts type check error ([3cb0629](https://github.com/perfect-panel/ppanel-web/commit/3cb0629))
* **user-nav**: Update user avatar and label to display telephone if email is not available ([7b6bb7b](https://github.com/perfect-panel/ppanel-web/commit/7b6bb7b))
* **user**: Update user identifier field and localizations ([1b6befa](https://github.com/perfect-panel/ppanel-web/commit/1b6befa))

### 💄 Styles

* **dashboard**: Adjust grid layout and update image dimensions in application display ([f3204b7](https://github.com/perfect-panel/ppanel-web/commit/f3204b7))
* **globals**: Refactor delete confirmation button and update badge styles in node and subscribe tables ([30ae781](https://github.com/perfect-panel/ppanel-web/commit/30ae781))
* Update node secret UI and add telephone code field to authentication form ([770932e](https://github.com/perfect-panel/ppanel-web/commit/770932e))
2025-01-28 16:03:56 +00:00
web@ppanel 1b6befac87 🐛 fix(user): Update user identifier field and localizations 2025-01-28 23:00:49 +07:00
web@ppanel 3cb0629d64 🐛 fix(type): Fix ts type check error 2025-01-28 13:55:43 +07:00
web@ppanel 1d8c765c14 🐛 fix(payment): Replace window.open with window.location.href for checkout links 2025-01-28 13:45:55 +07:00
web@ppanel 39e89bfef5 🐛 fix(subscription): Add reset functionality for user subscription token 2025-01-28 13:45:25 +07:00
web@ppanel 52c1d1fc60 🐛 fix(locales): Add error message for incorrect user information 2025-01-28 13:44:31 +07:00
web@ppanel 3d92902f1b 🐛 fix(locales): Add error message for incorrect user information 2025-01-28 13:43:53 +07:00
web@ppanel 331bbea45f 🐛 fix(sort): Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance 2025-01-28 13:43:18 +07:00
web@ppanel a604f28fef 🐛 fix(auth): Require minimum length for invite string when forced invite is enabled 2025-01-23 16:10:44 +07:00
web@ppanel 69fc670ba3 🐛 fix(auth): Change Textarea value to defaultValue for client_secret in Apple auth page 2025-01-23 00:29:40 +07:00
web@ppanel c53ac61501 🐛 fix(redirect): Simplify redirect URL logic by removing unnecessary condition for sessionStorage 2025-01-22 22:42:02 +07:00
web@ppanel 795476209a 🐛 fix(redirect): Update redirect URL logic to ensure proper handling of OAuth and auth paths 2025-01-22 22:41:17 +07:00
web@ppanel 9227411d74 feat(oauth): Update OAuth login handling to use callback parameter and improve URL parameter retrieval 2025-01-22 22:16:04 +07:00
web@ppanel 0aa5d5b0a9 🐛 fix(api): Remove redundant requestType parameter in appleLoginCallback 2025-01-21 23:38:14 +07:00
web@ppanel 40a6f7ca81 feat(oauth): Implement OAuth token retrieval and refactor login callback handling 2025-01-21 23:32:12 +07:00
web@ppanel 8346c85109 feat(oauth): Refactor platform parameter handling and improve logout redirection logic 2025-01-21 22:39:23 +07:00
web@ppanel aa6dda894b feat(auth): Redirect user after OAuth login and add logos icon collection 2025-01-20 23:55:58 +07:00
web@ppanel 5ed04c0a59 feat(oauth): Add certification component for handling OAuth login callbacks and improve user authentication flow 2025-01-20 23:50:42 +07:00
web@ppanel 652e0323fd feat(config): Update encryption fields in configuration form and refactor OAuth callback parameters 2025-01-20 22:23:04 +07:00
web@ppanel d1f5a9b825 feat(locales): Add description information of communication keys and encryption methods to enhance client configuration capabilities 2025-01-20 20:19:57 +07:00
web@ppanel 88b3504735 feat(config): Add application selection and encryption settings to configuration form 2025-01-20 20:17:49 +07:00
web@ppanel 28f8c78963 feat(tutorial): Fetch the latest tutorial version from GitHub API for dynamic URL generation 2025-01-20 16:02:53 +07:00
web@ppanel 9140b8ad1e 🐛 fix(oauth): Remove redundant checks when updating configuration to simplify logic 2025-01-19 22:36:50 +07:00
web@ppanel 6227ba91d0 🐛 fix(oauth): Refactor OAuth configuration types and update related API methods 2025-01-19 22:34:12 +07:00
web@ppanel 18ee600836 feat(auth): Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple 2025-01-19 20:43:35 +07:00
web@ppanel cefcb310d6 🐛 fix(nav): Comment out unused social login options to simplify navigation configuration 2025-01-18 21:53:44 +07:00
web@ppanel 4ea6e4a2a3 🐛 fix(site): Add image upload functionality for site logo configuration 2025-01-18 21:46:05 +07:00
web@ppanel 4983c33bb3 🐛 fix(dashboard): Improve URL encoding for subscription links and enhance success message handling 2025-01-18 21:40:01 +07:00
web@ppanel 3e8912e7bb 🐛 fix(dashboard): Update icon imports for platform consistency and adjust icon size 2025-01-18 19:40:36 +07:00
web@ppanel b6da51b618 🐛 fix(api): Update subscription_protocol to subscribe_type for consistency across services 2025-01-18 19:36:39 +07:00
web@ppanel 9d8b814212 🐛 fix(api): Rename app-related functions and types to application for consistency 2025-01-18 18:48:28 +07:00
web@ppanel 5e5e4edd2e ♻️ refactor(sbscribe): Rename and reorganize components for better structure and clarity 2025-01-18 17:39:57 +07:00
web@ppanel d6fbc38795 🐛 fix(deps): Remove outdated @iconify/react dependency and add iconify-json packages 2025-01-18 17:39:26 +07:00
web@ppanel b0aa3645f8 🐛 fix(dashboard): Update platform detection logic and improve layout responsiveness 2025-01-16 21:57:30 +07:00
web@ppanel 30ae781f05 💄 style(globals): Refactor delete confirmation button and update badge styles in node and subscribe tables 2025-01-16 15:29:59 +07:00
web@ppanel f3204b757f 💄 style(dashboard): Adjust grid layout and update image dimensions in application display 2025-01-16 14:50:27 +07:00
web@ppanel 1d0d911741 🐛 fix(locales): Add logout message to authentication localization files 2025-01-16 14:40:08 +07:00
web@ppanel 5d078fdaca 🐛 fix(auth): Update user authentication flow to include email and phone code verification 2025-01-16 14:26:33 +07:00
web@ppanel 8775fb620e 🐛 fix(node): Add country and city fields to the form schema and localization files 2025-01-16 14:15:40 +07:00
web@ppanel 770932e957 💄 style: Update node secret UI and add telephone code field to authentication form 2025-01-16 12:13:44 +07:00
web@ppanel 1cdb7e7872 🐛 fix(node-config): Add null checks for time slots and ensure proper handling of undefined values 2025-01-15 22:50:12 +07:00
web@ppanel 72396851ff 🐛 fix(auth): Remove unused telephone code login function and update typings for telephone login requests 2025-01-15 22:49:40 +07:00
web@ppanel 7b6bb7bc69 🐛 fix(user-nav): Update user avatar and label to display telephone if email is not available 2025-01-15 21:46:30 +07:00
web@ppanel 57eaa5532d feat(auth): Add email and SMS code sending functionality with localization updates 2025-01-15 21:41:54 +07:00
web@ppanel 4acf7b1837 feat(auth): Add SMS and email configuration options to global store and update localization 2025-01-15 15:49:11 +07:00
web@ppanel 9b8258ce5a feat(locales): Add area code and telephone fields to user forms in multiple languages 2025-01-14 19:26:38 +07:00
web@ppanel 585b99c2cc feat(user): Add telephone input with area code selection and update localization 2025-01-14 19:24:46 +07:00
web@ppanel 39a9ce60de feat(global): Add SMS configuration options to global store 2025-01-14 16:40:02 +07:00
web@ppanel 207bc247f4 feat(auth): Enhance user registration with invite handling and logo display 2025-01-14 16:33:09 +07:00
web@ppanel 938363b1eb feat(sms): Update locales 2025-01-14 16:15:25 +07:00
web@ppanel 7f0f5ce9db feat(node): Move the node configuration to the server module 2025-01-14 14:41:34 +07:00
web@ppanel f90d4d2ce6 feat(subscribe): Move subscription configuration and application to subscription module 2025-01-14 14:12:26 +07:00
web@ppanel 18b07c750d 🐛 fix(phone): Update SMS expiration time field to use 'sms_expire_time' with default value of 300 2025-01-13 22:06:14 +07:00
web@ppanel 19619fdbe0 feat(subscribe): Add 'sold' column to SubscribeTable and update inventory terminology 2025-01-13 21:54:49 +07:00
web@ppanel f408fdffae feat(node): Add tags 2025-01-13 21:37:56 +07:00
web@ppanel f26f1c24ae feat(subscribe): Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting 2025-01-13 21:15:35 +07:00
web@ppanel c59742a4f3 feat: Update Auth Control 2025-01-13 21:07:27 +07:00
semantic-release-bot dd23279e3e 🔖 chore(release): v1.0.0-beta.6 [skip ci]
# [1.0.0-beta.6](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2025-01-10)

### 🐛 Bug Fixes

* **auth**: Update UserCheckForm to use setInitialValues and modify onSubmit type ([c984c0d](https://github.com/perfect-panel/ppanel-web/commit/c984c0d))
2025-01-10 06:08:16 +00:00
web@ppanel c984c0d9c0 🐛 fix(auth): Update UserCheckForm to use setInitialValues and modify onSubmit type 2025-01-10 13:04:33 +07:00
semantic-release-bot f275f01f25 🔖 chore(release): v1.0.0-beta.5 [skip ci]
# [1.0.0-beta.5](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2025-01-09)

###  Features

* **locales**: Replace 'nodeGroupId' with 'groupId' in multiple language files for consistency ([a4e9d5d](https://github.com/perfect-panel/ppanel-web/commit/a4e9d5d))
* **locales**: Update 'deductBalance' to 'giftAmount' across multiple languages and fix newline in announcement.json ([70497af](https://github.com/perfect-panel/ppanel-web/commit/70497af))
* **stats**: Replace dynamic stat fetching with environment constants for user, server, and location counts ([46ae166](https://github.com/perfect-panel/ppanel-web/commit/46ae166))
* **subscribe**: Update suffix from 'MB' to 'Mbps' and enhance speed limit display logic ([3547bb1](https://github.com/perfect-panel/ppanel-web/commit/3547bb1))
* **user**: Add 'gift_amount' field and update related references in user services and components ([b13c77e](https://github.com/perfect-panel/ppanel-web/commit/b13c77e))

### 🎫 Chores

* **deps**: Update package dependencies across multiple projects for improved stability and performance ([b01a5bc](https://github.com/perfect-panel/ppanel-web/commit/b01a5bc))
* **ui**: Update package dependencies for improved stability and performance ([25da429](https://github.com/perfect-panel/ppanel-web/commit/25da429))

### 🐛 Bug Fixes

* **api**: Replace 'deduction' with 'gift_amount' and add 'commission' field in type definitions ([77edf1d](https://github.com/perfect-panel/ppanel-web/commit/77edf1d))
* **api**: Update API type definitions to replace 'deduction' with 'gift_amount' and make 'commission' field optional ([c2af060](https://github.com/perfect-panel/ppanel-web/commit/c2af060))
* **auth**: Refactor user authentication forms to remove global store dependency and improve type handling ([12026b0](https://github.com/perfect-panel/ppanel-web/commit/12026b0))
* **coupon**: Rename 'server' field to 'subscribe' in coupon form and update coupon update request type ([f8b6d82](https://github.com/perfect-panel/ppanel-web/commit/f8b6d82))
* **types**: Add 'gift_amount' field to API type definitions ([8f8a12a](https://github.com/perfect-panel/ppanel-web/commit/8f8a12a))
* **user**: Add the 'gift_amount' field to the user service's type definition ([6301409](https://github.com/perfect-panel/ppanel-web/commit/6301409))
2025-01-09 14:42:36 +00:00
web@ppanel c2af06052c 🐛 fix(api): Update API type definitions to replace 'deduction' with 'gift_amount' and make 'commission' field optional 2025-01-09 21:40:02 +07:00
web@ppanel 8f8a12a510 🐛 fix(types): Add 'gift_amount' field to API type definitions 2025-01-09 21:31:25 +07:00
web@ppanel 77edf1d6ec 🐛 fix(api): Replace 'deduction' with 'gift_amount' and add 'commission' field in type definitions 2025-01-09 21:25:21 +07:00
web@ppanel f8b6d82780 🐛 fix(coupon): Rename 'server' field to 'subscribe' in coupon form and update coupon update request type 2025-01-09 20:25:34 +07:00
web@ppanel b01a5bc83b 🔧 chore(deps): Update package dependencies across multiple projects for improved stability and performance 2025-01-09 17:28:34 +07:00
web@ppanel 25da429da6 🔧 chore(ui): Update package dependencies for improved stability and performance 2025-01-09 13:30:11 +07:00
web@ppanel 12026b02cd 🐛 fix(auth): Refactor user authentication forms to remove global store dependency and improve type handling 2025-01-09 13:19:18 +07:00
web@ppanel 6301409531 🐛 fix(user): Add the 'gift_amount' field to the user service's type definition 2025-01-08 23:42:34 +07:00
web@ppanel b13c77e9b7 feat(user): Add 'gift_amount' field and update related references in user services and components 2025-01-08 23:25:13 +07:00
web@ppanel a4e9d5da59 feat(locales): Replace 'nodeGroupId' with 'groupId' in multiple language files for consistency 2025-01-08 23:13:32 +07:00
web@ppanel 70497af0aa feat(locales): Update 'deductBalance' to 'giftAmount' across multiple languages and fix newline in announcement.json 2025-01-08 12:55:10 +07:00
web@ppanel 46ae1665be feat(stats): Replace dynamic stat fetching with environment constants for user, server, and location counts 2025-01-08 12:54:43 +07:00
web@ppanel 3547bb1596 feat(subscribe): Update suffix from 'MB' to 'Mbps' and enhance speed limit display logic 2025-01-08 11:14:16 +07:00
595 changed files with 13875 additions and 4652 deletions
+129 -32
View File
@@ -1,62 +1,159 @@
<a name="readme-top"></a> <a name="readme-top"></a>
# Changelog # Changelog
# [1.0.0-beta.4](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2025-01-07) # [1.0.0-beta.7](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2025-01-28)
### ♻ Code Refactoring ### ♻ Code Refactoring
* **auth**: Refactor user authorization handling and improve error logging ([68bc18f](https://github.com/perfect-panel/ppanel-web/commit/68bc18f)) * **sbscribe**: Rename and reorganize components for better structure and clarity ([5e5e4ed](https://github.com/perfect-panel/ppanel-web/commit/5e5e4ed))
### ✨ Features ### ✨ Features
* **affiliate**: Add Affiliate component with commission display and invite link functionality ([4aea4e8](https://github.com/perfect-panel/ppanel-web/commit/4aea4e8)) * **auth**: Add email and SMS code sending functionality with localization updates ([57eaa55](https://github.com/perfect-panel/ppanel-web/commit/57eaa55))
* **affiliate**: Update affiliate component to display total commission and improve data fetching ([cc834ca](https://github.com/perfect-panel/ppanel-web/commit/cc834ca)) * **auth**: Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple ([18ee600](https://github.com/perfect-panel/ppanel-web/commit/18ee600))
* **api**: Add new subscription properties and locale support for deduction ratios and reset cycles ([fec80f5](https://github.com/perfect-panel/ppanel-web/commit/fec80f5)) * **auth**: Add SMS and email configuration options to global store and update localization ([4acf7b1](https://github.com/perfect-panel/ppanel-web/commit/4acf7b1))
* **api**: Add Time Period Configuration ([837157c](https://github.com/perfect-panel/ppanel-web/commit/837157c)) * **auth**: Enhance user registration with invite handling and logo display ([207bc24](https://github.com/perfect-panel/ppanel-web/commit/207bc24))
* **favicon**: Update SVG favicon design for admin and user interfaces ([1d91738](https://github.com/perfect-panel/ppanel-web/commit/1d91738)) * **auth**: Redirect user after OAuth login and add logos icon collection ([aa6dda8](https://github.com/perfect-panel/ppanel-web/commit/aa6dda8))
* **node**: Add serverKey ([25ce37e](https://github.com/perfect-panel/ppanel-web/commit/25ce37e)) * **config**: Add application selection and encryption settings to configuration form ([88b3504](https://github.com/perfect-panel/ppanel-web/commit/88b3504))
* **relay**: Add relay mode configuration and update related schemas ([3cc9477](https://github.com/perfect-panel/ppanel-web/commit/3cc9477)) * **config**: Update encryption fields in configuration form and refactor OAuth callback parameters ([652e032](https://github.com/perfect-panel/ppanel-web/commit/652e032))
* **schema**: Add security field to hysteria2 and tuic schemas ([cd59d44](https://github.com/perfect-panel/ppanel-web/commit/cd59d44)) * **global**: Add SMS configuration options to global store ([39a9ce6](https://github.com/perfect-panel/ppanel-web/commit/39a9ce6))
* **subscribe**: Add reset_time to API typings and update unsubscribe logic ([eeea165](https://github.com/perfect-panel/ppanel-web/commit/eeea165)) * **locales**: Add area code and telephone fields to user forms in multiple languages ([9b8258c](https://github.com/perfect-panel/ppanel-web/commit/9b8258c))
* **subscribe**: Add subscribe_discount type ([f99c604](https://github.com/perfect-panel/ppanel-web/commit/f99c604)) * **locales**: Add description information of communication keys and encryption methods to enhance client configuration capabilities ([d1f5a9b](https://github.com/perfect-panel/ppanel-web/commit/d1f5a9b))
* **subscribe**: Add subscription credits ([5bc7905](https://github.com/perfect-panel/ppanel-web/commit/5bc7905)) * **node**: Add tags ([f408fdf](https://github.com/perfect-panel/ppanel-web/commit/f408fdf))
* **subscribe**: Add unsubscribe functionality with confirmation messages and localized strings ([b2a2f42](https://github.com/perfect-panel/ppanel-web/commit/b2a2f42)) * **node**: Move the node configuration to the server module ([7f0f5ce](https://github.com/perfect-panel/ppanel-web/commit/7f0f5ce))
* **subscribe**: Improve error handling in subscription forms and update component props ([d28a10b](https://github.com/perfect-panel/ppanel-web/commit/d28a10b)) * **oauth**: Add certification component for handling OAuth login callbacks and improve user authentication flow ([5ed04c0](https://github.com/perfect-panel/ppanel-web/commit/5ed04c0))
* **subscribe**: Improve layout and styling in subscription components ([5766376](https://github.com/perfect-panel/ppanel-web/commit/5766376)) * **oauth**: Implement OAuth token retrieval and refactor login callback handling ([40a6f7c](https://github.com/perfect-panel/ppanel-web/commit/40a6f7c))
* **subscription**: Add localized messages for existing subscriptions and deletion restrictions ([e8a72d5](https://github.com/perfect-panel/ppanel-web/commit/e8a72d5)) * **oauth**: Refactor platform parameter handling and improve logout redirection logic ([8346c85](https://github.com/perfect-panel/ppanel-web/commit/8346c85))
* **oauth**: Update OAuth login handling to use callback parameter and improve URL parameter retrieval ([9227411](https://github.com/perfect-panel/ppanel-web/commit/9227411))
* **sms**: Update locales ([938363b](https://github.com/perfect-panel/ppanel-web/commit/938363b))
### 🎫 Chores * **subscribe**: Add 'sold' column to SubscribeTable and update inventory terminology ([19619fd](https://github.com/perfect-panel/ppanel-web/commit/19619fd))
* **subscribe**: Move subscription configuration and application to subscription module ([f90d4d2](https://github.com/perfect-panel/ppanel-web/commit/f90d4d2))
* Update changelog, enhance prepare script, and add openapi command ([a93db4e](https://github.com/perfect-panel/ppanel-web/commit/a93db4e)) * **subscribe**: Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting ([f26f1c2](https://github.com/perfect-panel/ppanel-web/commit/f26f1c2))
* **tutorial**: Fetch the latest tutorial version from GitHub API for dynamic URL generation ([28f8c78](https://github.com/perfect-panel/ppanel-web/commit/28f8c78))
* **user**: Add telephone input with area code selection and update localization ([585b99c](https://github.com/perfect-panel/ppanel-web/commit/585b99c))
* Update Auth Control ([c59742a](https://github.com/perfect-panel/ppanel-web/commit/c59742a))
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* **dashboard**: Correct progress value calculations and update groupId accessor ([36c7667](https://github.com/perfect-panel/ppanel-web/commit/36c7667)) * **api**: Remove redundant requestType parameter in appleLoginCallback ([0aa5d5b](https://github.com/perfect-panel/ppanel-web/commit/0aa5d5b))
* **layout**: Remove unnecessary cookie initialization in Logout function ([3065c3a](https://github.com/perfect-panel/ppanel-web/commit/3065c3a)) * **api**: Rename app-related functions and types to application for consistency ([9d8b814](https://github.com/perfect-panel/ppanel-web/commit/9d8b814))
* **locales**: Update Hong Kong ([6d0d069](https://github.com/perfect-panel/ppanel-web/commit/6d0d069)) * **api**: Update subscription_protocol to subscribe_type for consistency across services ([b6da51b](https://github.com/perfect-panel/ppanel-web/commit/b6da51b))
* **subscribe**: Update value validation to check for number type in subscribe form ([6de29d5](https://github.com/perfect-panel/ppanel-web/commit/6de29d5)) * **auth**: Change Textarea value to defaultValue for client_secret in Apple auth page ([69fc670](https://github.com/perfect-panel/ppanel-web/commit/69fc670))
* **auth**: Remove unused telephone code login function and update typings for telephone login requests ([7239685](https://github.com/perfect-panel/ppanel-web/commit/7239685))
* **auth**: Require minimum length for invite string when forced invite is enabled ([a604f28](https://github.com/perfect-panel/ppanel-web/commit/a604f28))
* **auth**: Update user authentication flow to include email and phone code verification ([5d078fd](https://github.com/perfect-panel/ppanel-web/commit/5d078fd))
* **dashboard**: Improve URL encoding for subscription links and enhance success message handling ([4983c33](https://github.com/perfect-panel/ppanel-web/commit/4983c33))
* **dashboard**: Update icon imports for platform consistency and adjust icon size ([3e8912e](https://github.com/perfect-panel/ppanel-web/commit/3e8912e))
* **dashboard**: Update platform detection logic and improve layout responsiveness ([b0aa364](https://github.com/perfect-panel/ppanel-web/commit/b0aa364))
* **deps**: Remove outdated @iconify/react dependency and add iconify-json packages ([d6fbc38](https://github.com/perfect-panel/ppanel-web/commit/d6fbc38))
* **locales**: Add error message for incorrect user information ([52c1d1f](https://github.com/perfect-panel/ppanel-web/commit/52c1d1f))
* **locales**: Add error message for incorrect user information ([3d92902](https://github.com/perfect-panel/ppanel-web/commit/3d92902))
* **locales**: Add logout message to authentication localization files ([1d0d911](https://github.com/perfect-panel/ppanel-web/commit/1d0d911))
* **nav**: Comment out unused social login options to simplify navigation configuration ([cefcb31](https://github.com/perfect-panel/ppanel-web/commit/cefcb31))
* **node-config**: Add null checks for time slots and ensure proper handling of undefined values ([1cdb7e7](https://github.com/perfect-panel/ppanel-web/commit/1cdb7e7))
* **node**: Add country and city fields to the form schema and localization files ([8775fb6](https://github.com/perfect-panel/ppanel-web/commit/8775fb6))
* **oauth**: Refactor OAuth configuration types and update related API methods ([6227ba9](https://github.com/perfect-panel/ppanel-web/commit/6227ba9))
* **oauth**: Remove redundant checks when updating configuration to simplify logic ([9140b8a](https://github.com/perfect-panel/ppanel-web/commit/9140b8a))
* **payment**: Replace window.open with window.location.href for checkout links ([1d8c765](https://github.com/perfect-panel/ppanel-web/commit/1d8c765))
* **phone**: Update SMS expiration time field to use 'sms_expire_time' with default value of 300 ([18b07c7](https://github.com/perfect-panel/ppanel-web/commit/18b07c7))
* **redirect**: Simplify redirect URL logic by removing unnecessary condition for sessionStorage ([c53ac61](https://github.com/perfect-panel/ppanel-web/commit/c53ac61))
* **redirect**: Update redirect URL logic to ensure proper handling of OAuth and auth paths ([7954762](https://github.com/perfect-panel/ppanel-web/commit/7954762))
* **site**: Add image upload functionality for site logo configuration ([4ea6e4a](https://github.com/perfect-panel/ppanel-web/commit/4ea6e4a))
* **sort**: Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance ([331bbea](https://github.com/perfect-panel/ppanel-web/commit/331bbea))
* **subscription**: Add reset functionality for user subscription token ([39e89bf](https://github.com/perfect-panel/ppanel-web/commit/39e89bf))
* **type**: Fix ts type check error ([3cb0629](https://github.com/perfect-panel/ppanel-web/commit/3cb0629))
* **user-nav**: Update user avatar and label to display telephone if email is not available ([7b6bb7b](https://github.com/perfect-panel/ppanel-web/commit/7b6bb7b))
* **user**: Update user identifier field and localizations ([1b6befa](https://github.com/perfect-panel/ppanel-web/commit/1b6befa))
### 💄 Styles ### 💄 Styles
* **locales**: Remove unused subscription labels from multiple locale files ([fb0c510](https://github.com/perfect-panel/ppanel-web/commit/fb0c510)) * **dashboard**: Adjust grid layout and update image dimensions in application display ([f3204b7](https://github.com/perfect-panel/ppanel-web/commit/f3204b7))
* **locales**: Update server.json to reorganize relay mode options and improve labels ([701cdee](https://github.com/perfect-panel/ppanel-web/commit/701cdee)) * **globals**: Refactor delete confirmation button and update badge styles in node and subscribe tables ([30ae781](https://github.com/perfect-panel/ppanel-web/commit/30ae781))
* **node**: Improve layout and spacing in NodeStatusCell component ([136287d](https://github.com/perfect-panel/ppanel-web/commit/136287d)) * Update node secret UI and add telephone code field to authentication form ([770932e](https://github.com/perfect-panel/ppanel-web/commit/770932e))
* **time-slot**: Add chart display ([c44ad47](https://github.com/perfect-panel/ppanel-web/commit/c44ad47))
<a name="readme-top"></a> <a name="readme-top"></a>
# Changelog # Changelog
# [1.0.0-beta.3](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-12-26) # [1.0.0-beta.6](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2025-01-10)
### 🐛 Bug Fixes
- **auth**: Update UserCheckForm to use setInitialValues and modify onSubmit type ([c984c0d](https://github.com/perfect-panel/ppanel-web/commit/c984c0d))
# [1.0.0-beta.5](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2025-01-09)
### ✨ Features
- **locales**: Replace 'nodeGroupId' with 'groupId' in multiple language files for consistency ([a4e9d5d](https://github.com/perfect-panel/ppanel-web/commit/a4e9d5d))
- **locales**: Update 'deductBalance' to 'giftAmount' across multiple languages and fix newline in announcement.json ([70497af](https://github.com/perfect-panel/ppanel-web/commit/70497af))
- **stats**: Replace dynamic stat fetching with environment constants for user, server, and location counts ([46ae166](https://github.com/perfect-panel/ppanel-web/commit/46ae166))
- **subscribe**: Update suffix from 'MB' to 'Mbps' and enhance speed limit display logic ([3547bb1](https://github.com/perfect-panel/ppanel-web/commit/3547bb1))
- **user**: Add 'gift_amount' field and update related references in user services and components ([b13c77e](https://github.com/perfect-panel/ppanel-web/commit/b13c77e))
### 🎫 Chores
- **deps**: Update package dependencies across multiple projects for improved stability and performance ([b01a5bc](https://github.com/perfect-panel/ppanel-web/commit/b01a5bc))
- **ui**: Update package dependencies for improved stability and performance ([25da429](https://github.com/perfect-panel/ppanel-web/commit/25da429))
### 🐛 Bug Fixes
- **api**: Replace 'deduction' with 'gift_amount' and add 'commission' field in type definitions ([77edf1d](https://github.com/perfect-panel/ppanel-web/commit/77edf1d))
- **api**: Update API type definitions to replace 'deduction' with 'gift_amount' and make 'commission' field optional ([c2af060](https://github.com/perfect-panel/ppanel-web/commit/c2af060))
- **auth**: Refactor user authentication forms to remove global store dependency and improve type handling ([12026b0](https://github.com/perfect-panel/ppanel-web/commit/12026b0))
- **coupon**: Rename 'server' field to 'subscribe' in coupon form and update coupon update request type ([f8b6d82](https://github.com/perfect-panel/ppanel-web/commit/f8b6d82))
- **types**: Add 'gift_amount' field to API type definitions ([8f8a12a](https://github.com/perfect-panel/ppanel-web/commit/8f8a12a))
- **user**: Add the 'gift_amount' field to the user service's type definition ([6301409](https://github.com/perfect-panel/ppanel-web/commit/6301409))
<a name="readme-top"></a>
# Changelog
# [1.0.0-beta.4](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2025-01-07)
### ♻ Code Refactoring ### ♻ Code Refactoring
- **config**: Simplify environment variable handling and improve build script ([cf54d0f](https://github.com/perfect-panel/ppanel-web/commit/cf54d0f)) - **auth**: Refactor user authorization handling and improve error logging ([68bc18f](https://github.com/perfect-panel/ppanel-web/commit/68bc18f))
### ✨ Features
- **affiliate**: Add Affiliate component with commission display and invite link functionality ([4aea4e8](https://github.com/perfect-panel/ppanel-web/commit/4aea4e8))
- **affiliate**: Update affiliate component to display total commission and improve data fetching ([cc834ca](https://github.com/perfect-panel/ppanel-web/commit/cc834ca))
- **api**: Add new subscription properties and locale support for deduction ratios and reset cycles ([fec80f5](https://github.com/perfect-panel/ppanel-web/commit/fec80f5))
- **api**: Add Time Period Configuration ([837157c](https://github.com/perfect-panel/ppanel-web/commit/837157c))
- **favicon**: Update SVG favicon design for admin and user interfaces ([1d91738](https://github.com/perfect-panel/ppanel-web/commit/1d91738))
- **node**: Add serverKey ([25ce37e](https://github.com/perfect-panel/ppanel-web/commit/25ce37e))
- **relay**: Add relay mode configuration and update related schemas ([3cc9477](https://github.com/perfect-panel/ppanel-web/commit/3cc9477))
- **schema**: Add security field to hysteria2 and tuic schemas ([cd59d44](https://github.com/perfect-panel/ppanel-web/commit/cd59d44))
- **subscribe**: Add reset_time to API typings and update unsubscribe logic ([eeea165](https://github.com/perfect-panel/ppanel-web/commit/eeea165))
- **subscribe**: Add subscribe_discount type ([f99c604](https://github.com/perfect-panel/ppanel-web/commit/f99c604))
- **subscribe**: Add subscription credits ([5bc7905](https://github.com/perfect-panel/ppanel-web/commit/5bc7905))
- **subscribe**: Add unsubscribe functionality with confirmation messages and localized strings ([b2a2f42](https://github.com/perfect-panel/ppanel-web/commit/b2a2f42))
- **subscribe**: Improve error handling in subscription forms and update component props ([d28a10b](https://github.com/perfect-panel/ppanel-web/commit/d28a10b))
- **subscribe**: Improve layout and styling in subscription components ([5766376](https://github.com/perfect-panel/ppanel-web/commit/5766376))
- **subscription**: Add localized messages for existing subscriptions and deletion restrictions ([e8a72d5](https://github.com/perfect-panel/ppanel-web/commit/e8a72d5))
### 🎫 Chores
- Update changelog, enhance prepare script, and add openapi command ([a93db4e](https://github.com/perfect-panel/ppanel-web/commit/a93db4e))
### 🐛 Bug Fixes
- **dashboard**: Correct progress value calculations and update groupId accessor ([36c7667](https://github.com/perfect-panel/ppanel-web/commit/36c7667))
- **layout**: Remove unnecessary cookie initialization in Logout function ([3065c3a](https://github.com/perfect-panel/ppanel-web/commit/3065c3a))
- **locales**: Update Hong Kong ([6d0d069](https://github.com/perfect-panel/ppanel-web/commit/6d0d069))
- **subscribe**: Update value validation to check for number type in subscribe form ([6de29d5](https://github.com/perfect-panel/ppanel-web/commit/6de29d5))
### 💄 Styles
- **locales**: Remove unused subscription labels from multiple locale files ([fb0c510](https://github.com/perfect-panel/ppanel-web/commit/fb0c510))
- **locales**: Update server.json to reorganize relay mode options and improve labels ([701cdee](https://github.com/perfect-panel/ppanel-web/commit/701cdee))
- **node**: Improve layout and spacing in NodeStatusCell component ([136287d](https://github.com/perfect-panel/ppanel-web/commit/136287d))
- **time-slot**: Add chart display ([c44ad47](https://github.com/perfect-panel/ppanel-web/commit/c44ad47))
<a name="readme-top"></a> <a name="readme-top"></a>
@@ -1,27 +1,24 @@
'use client'; 'use client';
import { resetPassword, userLogin, userRegister } from '@/services/common/auth';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { ReactNode, useState, useTransition } from 'react';
import { toast } from 'sonner';
import { import {
NEXT_PUBLIC_DEFAULT_USER_EMAIL, NEXT_PUBLIC_DEFAULT_USER_EMAIL,
NEXT_PUBLIC_DEFAULT_USER_PASSWORD, NEXT_PUBLIC_DEFAULT_USER_PASSWORD,
} from '@/config/constants'; } from '@/config/constants';
import useGlobalStore from '@/config/use-global';
import { checkUser, resetPassword, userLogin, userRegister } from '@/services/common/auth';
import { getRedirectUrl, setAuthorization } from '@/utils/common'; import { getRedirectUrl, setAuthorization } from '@/utils/common';
import { useTranslations } from 'next-intl'; import LoginForm from './login-form';
import { useRouter } from 'next/navigation'; import RegisterForm from './register-form';
import { ReactNode, useState, useTransition } from 'react'; import ResetForm from './reset-form';
import { toast } from 'sonner';
import UserCheckForm from './user-check-form';
import UserLoginForm from './user-login-form';
import UserRegisterForm from './user-register-form';
import UserResetForm from './user-reset-form';
export default function UserAuthForm() { export default function EmailAuthForm() {
const t = useTranslations('auth'); const t = useTranslations('auth');
const { common } = useGlobalStore();
const { register } = common;
const router = useRouter(); const router = useRouter();
const [type, setType] = useState<'login' | 'register' | 'reset'>(); const [type, setType] = useState<'login' | 'register' | 'reset'>('login');
const [loading, startTransition] = useTransition(); const [loading, startTransition] = useTransition();
const [initialValues, setInitialValues] = useState<{ const [initialValues, setInitialValues] = useState<{
email?: string; email?: string;
@@ -41,44 +38,35 @@ export default function UserAuthForm() {
startTransition(async () => { startTransition(async () => {
try { try {
switch (type) { switch (type) {
case 'login': case 'login': {
// eslint-disable-next-line no-case-declarations
const login = await userLogin(params); const login = await userLogin(params);
toast.success(t('login.success')); toast.success(t('login.success'));
onLogin(login.data.data?.token); onLogin(login.data.data?.token);
break; break;
case 'register': }
// eslint-disable-next-line no-case-declarations case 'register': {
const create = await userRegister(params); const create = await userRegister(params);
toast.success(t('register.success')); toast.success(t('register.success'));
onLogin(create.data.data?.token); onLogin(create.data.data?.token);
break; break;
}
case 'reset': case 'reset':
await resetPassword(params); await resetPassword(params);
toast.success(t('reset.success')); toast.success(t('reset.success'));
setType('login'); setType('login');
break; break;
default:
if (type === 'reset') break;
// eslint-disable-next-line no-case-declarations
const response = await checkUser(params);
setInitialValues({
...initialValues,
...params,
});
setType(response.data.data?.exist ? 'login' : 'register');
break;
} }
} catch (error) { } catch (error) {
/* empty */ /* empty */
} }
}); });
}; };
let UserForm: ReactNode = null; let UserForm: ReactNode = null;
switch (type) { switch (type) {
case 'login': case 'login':
UserForm = ( UserForm = (
<UserLoginForm <LoginForm
loading={loading} loading={loading}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
initialValues={initialValues} initialValues={initialValues}
@@ -89,7 +77,7 @@ export default function UserAuthForm() {
break; break;
case 'register': case 'register':
UserForm = ( UserForm = (
<UserRegisterForm <RegisterForm
loading={loading} loading={loading}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
initialValues={initialValues} initialValues={initialValues}
@@ -100,7 +88,7 @@ export default function UserAuthForm() {
break; break;
case 'reset': case 'reset':
UserForm = ( UserForm = (
<UserResetForm <ResetForm
loading={loading} loading={loading}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
initialValues={initialValues} initialValues={initialValues}
@@ -109,15 +97,6 @@ export default function UserAuthForm() {
/> />
); );
break; break;
default:
UserForm = (
<UserCheckForm
loading={loading}
onSubmit={handleFormSubmit}
initialValues={initialValues}
/>
);
break;
} }
return ( return (
@@ -128,34 +107,6 @@ export default function UserAuthForm() {
{t(`${type || 'check'}.description`)} {t(`${type || 'check'}.description`)}
</div> </div>
</div> </div>
{/* {!((type === 'register' && register.stop_register) || type === 'reset') && (
<>
<div className='mb-3 flex flex-wrap items-center justify-center gap-3 font-bold'>
<Button type='button' variant='outline'>
<Icon icon='uil:telegram' className='mr-2 size-5' />
Telegram
</Button>
<Button type='button' variant='outline'>
<Icon icon='uil:google' className='mr-2 size-5' />
Google
</Button>
<Button type='button' variant='outline'>
<Icon icon='uil:apple' className='mr-2 size-5' />
Apple
</Button>
</div>
<div
className={cn(
'my-14 flex h-0 items-center text-center',
'before:mr-4 before:block before:w-1/2 before:border-b-[1px]',
'after:ml-4 after:w-1/2 after:border-b-[1px]',
)}
>
<span className='text-muted-foreground w-[125px] text-sm'>{t('orWithEmail')}</span>
</div>
</>
)} */}
{UserForm} {UserForm}
</> </>
); );
@@ -1,16 +1,16 @@
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import CloudFlareTurnstile from './turnstile'; import CloudFlareTurnstile from '../turnstile';
export default function UserLoginForm({ export default function LoginForm({
loading, loading,
onSubmit, onSubmit,
initialValues, initialValues,
@@ -21,14 +21,14 @@ export default function UserLoginForm({
onSubmit: (data: any) => void; onSubmit: (data: any) => void;
initialValues: any; initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>; setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: (type?: 'register' | 'reset') => void; onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
}) { }) {
const t = useTranslations('auth.login'); const t = useTranslations('auth.login');
const { common } = useGlobalStore(); const { common } = useGlobalStore();
const { verify } = common; const { verify } = common;
const formSchema = z.object({ const formSchema = z.object({
email: z.string(), email: z.string().email(t('email')),
password: z.string(), password: z.string(),
cf_token: cf_token:
verify.enable_login_verify && verify.turnstile_site_key ? z.string() : z.string().optional(), verify.enable_login_verify && verify.turnstile_site_key ? z.string() : z.string().optional(),
@@ -48,7 +48,7 @@ export default function UserLoginForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input disabled placeholder='Enter your email...' type='email' {...field} /> <Input placeholder='Enter your email...' type='email' {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -60,12 +60,7 @@ export default function UserLoginForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input placeholder='Enter your password...' type='password' {...field} />
disabled={loading}
placeholder='Enter your password...'
type='password'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -100,10 +95,10 @@ export default function UserLoginForm({
className='p-0' className='p-0'
onClick={() => { onClick={() => {
setInitialValues(undefined); setInitialValues(undefined);
onSwitchForm(undefined); onSwitchForm('register');
}} }}
> >
{t('switchAccount')} {t('registerAccount')}
</Button> </Button>
</div> </div>
</> </>
@@ -1,19 +1,18 @@
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { sendEmailCode } from '@/services/common/common';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { Markdown } from '@workspace/ui/custom-components/markdown'; import { Markdown } from '@workspace/ui/custom-components/markdown';
import { useCountDown } from 'ahooks';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction, useState } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import CloudFlareTurnstile from './turnstile'; import SendCode from '../send-code';
import CloudFlareTurnstile from '../turnstile';
export default function UserRegisterForm({ export default function RegisterForm({
loading, loading,
onSubmit, onSubmit,
initialValues, initialValues,
@@ -24,33 +23,36 @@ export default function UserRegisterForm({
onSubmit: (data: any) => void; onSubmit: (data: any) => void;
initialValues: any; initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>; setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: (type?: 'register' | 'reset') => void; onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
}) { }) {
const t = useTranslations('auth.register'); const t = useTranslations('auth.register');
const { common } = useGlobalStore(); const { common } = useGlobalStore();
const { verify, register, invite } = common; const { verify, auth, invite } = common;
const [targetDate, setTargetDate] = useState<number>(); const handleCheckUser = async (email: string) => {
const [, { seconds }] = useCountDown({ try {
targetDate, const domain = email.split('@')[1];
onEnd: () => { const isValid =
setTargetDate(undefined); !auth.email.email_enable_verify ||
}, auth.email?.email_domain_suffix_list.split('\n').includes(domain || '');
}); return !isValid;
const handleSendCode = async () => { } catch (error) {
await sendEmailCode({ console.log('Error checking user:', error);
email: initialValues.email, return false;
type: 1, }
});
setTargetDate(Date.now() + 60000);
}; };
const formSchema = z const formSchema = z
.object({ .object({
email: z.string(), email: z
.string()
.email(t('email'))
.refine(handleCheckUser, {
message: t('whitelist'),
}),
password: z.string(), password: z.string(),
repeat_password: z.string(), repeat_password: z.string(),
code: register.enable_email_verify ? z.string() : z.string().nullish(), code: auth.email.email_enable_verify ? z.string() : z.string().nullish(),
invite: invite.forced_invite ? z.string() : z.string().nullish(), invite: invite.forced_invite ? z.string() : z.string().nullish(),
cf_token: cf_token:
verify.enable_register_verify && verify.turnstile_site_key verify.enable_register_verify && verify.turnstile_site_key
@@ -66,17 +68,18 @@ export default function UserRegisterForm({
}); });
} }
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
...initialValues, ...initialValues,
invite: sessionStorage.getItem('invite'), invite: localStorage.getItem('invite') || '',
}, },
}); });
return ( return (
<> <>
{register.stop_register ? ( {auth.register.stop_register ? (
<Markdown>{t('message')}</Markdown> <Markdown>{t('message')}</Markdown>
) : ( ) : (
<Form {...form}> <Form {...form}>
@@ -87,7 +90,7 @@ export default function UserRegisterForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input disabled placeholder='Enter your email...' type='email' {...field} /> <Input placeholder='Enter your email...' type='email' {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -99,12 +102,7 @@ export default function UserRegisterForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input placeholder='Enter your password...' type='password' {...field} />
disabled={loading}
placeholder='Enter your password...'
type='password'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -116,18 +114,13 @@ export default function UserRegisterForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input placeholder='Enter password again...' type='password' {...field} />
disabled={loading}
placeholder='Enter password again...'
type='password'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{register.enable_email_verify && ( {auth.email.email_enable_verify && (
<FormField <FormField
control={form.control} control={form.control}
name='code' name='code'
@@ -136,15 +129,18 @@ export default function UserRegisterForm({
<FormControl> <FormControl>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Input <Input
disabled={loading}
placeholder='Enter code...' placeholder='Enter code...'
type='text' type='text'
{...field} {...field}
value={field.value as string} value={field.value as string}
/> />
<Button type='button' onClick={handleSendCode} disabled={seconds > 0}> <SendCode
{seconds > 0 ? `${seconds}s` : t('get')} type='email'
</Button> params={{
...form.getValues(),
type: 1,
}}
/>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -159,7 +155,7 @@ export default function UserRegisterForm({
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input
disabled={loading} disabled={loading || !!localStorage.getItem('invite')}
placeholder={t('invite')} placeholder={t('invite')}
{...field} {...field}
value={field.value || ''} value={field.value || ''}
@@ -191,13 +187,13 @@ export default function UserRegisterForm({
</Form> </Form>
)} )}
<div className='mt-4 text-right text-sm'> <div className='mt-4 text-right text-sm'>
{t('existingAccount')} {t('existingAccount')}&nbsp;
<Button <Button
variant='link' variant='link'
className='p-0' className='p-0'
onClick={() => { onClick={() => {
setInitialValues(undefined); setInitialValues(undefined);
onSwitchForm(undefined); onSwitchForm('login');
}} }}
> >
{t('switchToLogin')} {t('switchToLogin')}
@@ -1,18 +1,17 @@
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { sendEmailCode } from '@/services/common/common';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react/dist/iconify.js';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { useCountDown } from 'ahooks'; import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction, useState } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import CloudFlareTurnstile from './turnstile'; import SendCode from '../send-code';
import CloudFlareTurnstile from '../turnstile';
export default function UserResetForm({ export default function ResetForm({
loading, loading,
onSubmit, onSubmit,
initialValues, initialValues,
@@ -23,32 +22,17 @@ export default function UserResetForm({
onSubmit: (data: any) => void; onSubmit: (data: any) => void;
initialValues: any; initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>; setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: (type?: 'register' | 'reset') => void; onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
}) { }) {
const t = useTranslations('auth.reset'); const t = useTranslations('auth.reset');
const { common } = useGlobalStore(); const { common } = useGlobalStore();
const { verify, register } = common; const { verify, auth } = common;
const [targetDate, setTargetDate] = useState<number>();
const [, { seconds }] = useCountDown({
targetDate,
onEnd: () => {
setTargetDate(undefined);
},
});
const handleSendCode = async () => {
await sendEmailCode({
email: initialValues.email,
type: 2,
});
setTargetDate(Date.now() + 60000); // 60秒倒计时
};
const formSchema = z.object({ const formSchema = z.object({
email: z.string(), email: z.string().email(t('email')),
password: z.string(), password: z.string(),
code: register.enable_email_verify ? z.string() : z.string().nullish(), code: auth?.email?.email_enable_verify ? z.string() : z.string().nullish(),
cf_token: cf_token:
verify.enable_register_verify && verify.turnstile_site_key verify.enable_register_verify && verify.turnstile_site_key
? z.string() ? z.string()
@@ -69,7 +53,7 @@ export default function UserResetForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input disabled placeholder='Enter your email...' type='email' {...field} /> <Input placeholder='Enter your email...' type='email' {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -81,19 +65,14 @@ export default function UserResetForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input placeholder='Enter your password...' type='password' {...field} />
disabled={loading}
placeholder='Enter your password...'
type='password'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{register.enable_email_verify && ( {auth?.email?.email_enable_verify && (
<FormField <FormField
control={form.control} control={form.control}
name='code' name='code'
@@ -102,15 +81,18 @@ export default function UserResetForm({
<FormControl> <FormControl>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Input <Input
disabled={loading}
placeholder='Enter code...' placeholder='Enter code...'
type='text' type='text'
{...field} {...field}
value={field.value as string} value={field.value as string}
/> />
<Button type='button' onClick={handleSendCode} disabled={seconds > 0}> <SendCode
{seconds > 0 ? `${seconds}s` : t('get')} type='email'
</Button> params={{
...form.getValues(),
type: 2,
}}
/>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -139,13 +121,13 @@ export default function UserResetForm({
</form> </form>
</Form> </Form>
<div className='mt-4 text-right text-sm'> <div className='mt-4 text-right text-sm'>
{t('existingAccount')} {t('existingAccount')}&nbsp;
<Button <Button
variant='link' variant='link'
className='p-0' className='p-0'
onClick={() => { onClick={() => {
setInitialValues(undefined); setInitialValues(undefined);
onSwitchForm(undefined); onSwitchForm('login');
}} }}
> >
{t('switchToLogin')} {t('switchToLogin')}
+2 -2
View File
@@ -8,7 +8,7 @@ import LoginLottie from '@workspace/ui/lotties/login.json';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Image from 'next/legacy/image'; import Image from 'next/legacy/image';
import Link from 'next/link'; import Link from 'next/link';
import UserAuthForm from './user-auth-form'; import EmailAuthForm from './email/auth-form';
export default function Page() { export default function Page() {
const t = useTranslations('auth'); const t = useTranslations('auth');
@@ -45,7 +45,7 @@ export default function Page() {
<div className='lg:bg-background flex flex-col items-center rounded-2xl md:w-[600px] lg:flex-auto lg:p-10 lg:shadow'> <div className='lg:bg-background flex flex-col items-center rounded-2xl md:w-[600px] lg:flex-auto lg:p-10 lg:shadow'>
<div className='flex flex-col items-stretch justify-center md:w-[400px] lg:h-full'> <div className='flex flex-col items-stretch justify-center md:w-[400px] lg:h-full'>
<div className='flex flex-col justify-center pb-14 lg:flex-auto lg:pb-20'> <div className='flex flex-col justify-center pb-14 lg:flex-auto lg:pb-20'>
<UserAuthForm /> <EmailAuthForm />
</div> </div>
<div className='flex items-center justify-end'> <div className='flex items-center justify-end'>
{/* <div className='text-primary flex gap-5 text-sm font-semibold'> {/* <div className='text-primary flex gap-5 text-sm font-semibold'>
+65
View File
@@ -0,0 +1,65 @@
'use client';
import { sendEmailCode, sendSmsCode } from '@/services/common/common';
import { Button } from '@workspace/ui/components/button';
import { useCountDown } from 'ahooks';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
interface SendCodeProps {
type: 'email' | 'phone';
params: {
email?: string;
type?: 1 | 2;
telephone_area_code?: string;
telephone?: string;
};
}
export default function SendCode({ type, params }: SendCodeProps) {
const t = useTranslations('auth');
const [targetDate, setTargetDate] = useState<number>();
const [, { seconds }] = useCountDown({
targetDate,
onEnd: () => {
setTargetDate(undefined);
},
});
const getEmailCode = async () => {
if (params.email && params.type) {
await sendEmailCode({
email: params.email,
type: params.type,
});
setTargetDate(Date.now() + 60000);
}
};
const getPhoneCode = async () => {
if (params.telephone && params.telephone_area_code) {
await sendSmsCode({
telephone: params.telephone,
telephone_area_code: params.telephone_area_code,
});
setTargetDate(Date.now() + 60000);
}
};
const handleSendCode = async () => {
if (type === 'email') {
getEmailCode();
} else {
getPhoneCode();
}
};
const disabled =
seconds > 0 ||
(type === 'email' ? !params.email : !params.telephone || !params.telephone_area_code);
return (
<Button type='button' onClick={handleSendCode} disabled={disabled}>
{seconds > 0 ? `${seconds}s` : t('get')}
</Button>
);
}
-109
View File
@@ -1,109 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input';
import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export default function UserCheckForm({
loading,
onSubmit,
initialValues,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
}) {
const t = useTranslations('auth.check');
// const { common } = useGlobalStore();
// const { register } = common;
const formSchema = z.object({
email: z.string().email(t('email')),
// .refine(
// (email) => {
// if (!register.enable_email_domain_suffix) return true;
// const domain = email.split('@')[1] as string;
// return register.email_domain_suffix_list.split('\n').includes(domain);
// },
// {
// message: t('whitelist'),
// },
// ),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleEmailChange = (value: string) => {
form.setValue('email', value);
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(async () => {
const isValid = await form.trigger('email');
if (isValid) {
setIsSubmitting(true);
form.handleSubmit(onSubmit)();
} else {
setIsSubmitting(false);
}
}, 500);
};
useEffect(() => {
if (initialValues && initialValues.email) {
handleEmailChange(initialValues.email);
}
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormControl>
<Input
disabled={loading}
placeholder='Enter your email...'
type='email'
{...field}
onChange={(e) => handleEmailChange(e.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit' disabled={!isSubmitting}>
{isSubmitting ? (
<>
<Icon icon='mdi:loading' className='mr-2 size-5 animate-spin' />
{t('checking')}
</>
) : (
t('continue')
)}
</Button>
</form>
</Form>
);
}
@@ -1,5 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
Form, Form,
@@ -20,6 +19,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { MarkdownEditor } from '@workspace/ui/custom-components/editor'; import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -0,0 +1,143 @@
'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('apple');
const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'apple'],
queryFn: async () => {
const { data } = await getOAuthByPlatform({
platform: 'apple',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
try {
await updateOAuthConfig({
...data,
[key]: value,
} as API.UpdateOAuthConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('teamId')}</Label>
<p className='text-muted-foreground text-xs'>{t('teamIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='ABCDE1FGHI'
value={data?.config?.team_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
team_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('keyId')}</Label>
<p className='text-muted-foreground text-xs'>{t('keyIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='ABC1234567'
value={data?.config?.key_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
key_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='com.your.app.service'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-20'
placeholder={`-----BEGIN PRIVATE KEY-----\nMIGTAgEA...\n-----END PRIVATE KEY-----`}
defaultValue={data?.config?.client_secret}
onBlur={(e) => {
updateConfig('config', {
...data?.config,
client_secret: e.target.value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/apple'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,209 @@
'use client';
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('email');
const ref = useRef<Partial<API.EmailSmtpConfig>>({});
const [email, setEmail] = useState<string>();
const { data, refetch, isFetching } = useQuery({
queryKey: ['getEmailSmtpConfig'],
queryFn: async () => {
const { data } = await getEmailSmtpConfig();
ref.current = data.data as API.EmailSmtpConfig;
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateEmailSmtpConfig({
...ref.current,
[key]: value,
} as API.EmailSmtpConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Tabs defaultValue='basic'>
<TabsList>
<TabsTrigger value='basic'>{t('emailBasicConfig')}</TabsTrigger>
<TabsTrigger value='template'>{t('emailTemplate')}</TabsTrigger>
<TabsTrigger value='logs'>{t('emailLogs')}</TabsTrigger>
</TabsList>
<TabsContent value='basic'>
<Table>
<TableBody>
{[
{
key: 'email_enabled',
label: t('enable'),
description: t('enableDescription'),
component: 'switch',
},
{
key: 'enable_email_verify',
label: t('emailVerification'),
description: t('emailVerificationDescription'),
component: 'switch',
},
{
key: 'enable_email_domain_suffix',
label: t('emailSuffixWhitelist'),
description: t('emailSuffixWhitelistDescription'),
component: 'switch',
},
{
key: 'email_domain_suffix_list',
label: t('whitelistSuffixes'),
description: t('whitelistSuffixesDescription'),
component: 'textarea',
},
{
key: 'email_smtp_host',
label: t('smtpServerAddress'),
description: t('smtpServerAddressDescription'),
},
{
key: 'email_smtp_port',
label: t('smtpServerPort'),
description: t('smtpServerPortDescription'),
type: 'number',
},
{
key: 'email_smtp_ssl',
label: t('smtpEncryptionMethod'),
description: t('smtpEncryptionMethodDescription'),
component: 'switch',
},
{
key: 'email_smtp_user',
label: t('smtpAccount'),
description: t('smtpAccountDescription'),
},
{
key: 'email_smtp_pass',
label: t('smtpPassword'),
description: t('smtpPasswordDescription'),
type: 'password',
},
{
key: 'email_smtp_from',
label: t('senderAddress'),
description: t('senderAddressDescription'),
},
].map(({ key, label, description, type = 'text', component = 'input' }) => (
<TableRow key={key}>
<TableCell className={component === 'textarea' ? 'align-top' : undefined}>
<Label>{label}</Label>
<p className='text-muted-foreground text-xs'>{description}</p>
</TableCell>
<TableCell className='text-right'>
{component === 'input' ? (
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.[key]}
type={type}
onValueBlur={(value) => updateConfig(key, value)}
/>
) : component === 'switch' ? (
<Switch
checked={data?.[key]}
onCheckedChange={(checked) => updateConfig(key, checked)}
/>
) : (
<Textarea
className='h-32'
placeholder={t('whitelistSuffixesPlaceholder')}
defaultValue={data?.[key]}
onBlur={(e) => updateConfig(key, e.target.value)}
/>
)}
</TableCell>
</TableRow>
))}
<TableRow>
<TableCell>
<Label>{t('sendTestEmail')}</Label>
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<EnhancedInput
placeholder='test@example.com'
value={email}
type='email'
onValueChange={(value) => setEmail(value as string)}
/>
<Button
disabled={!email || isFetching}
onClick={async () => {
if (!email) return;
try {
await testEmailSmtp({ email });
toast.success(t('sendSuccess'));
} catch {
toast.error(t('sendFailure'));
}
}}
>
{t('sendTestEmail')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value='template'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
{['verify_email_template', 'expiration_email_template', 'maintenance_email_template'].map(
(templateKey) => (
<Card key={templateKey}>
<CardHeader>
<CardTitle>{t(`${templateKey}`)}</CardTitle>
<CardDescription>
{t(`${templateKey}Description`, { after: '{{', before: '}}' })}
</CardDescription>
</CardHeader>
<CardContent>
<HTMLEditor
placeholder={t('inputPlaceholder')}
value={data?.[templateKey as keyof API.EmailSmtpConfig] as string}
onBlur={(value) => updateConfig(templateKey, value)}
/>
</CardContent>
</Card>
),
)}
</div>
</TabsContent>
<TabsContent value='logs'>Logs</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,105 @@
'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('facebook');
const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'facebook'],
queryFn: async () => {
const { data } = await getOAuthByPlatform({
platform: 'facebook',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
try {
await updateOAuthConfig({
...data,
[key]: value,
} as API.UpdateOAuthConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='1234567890123456'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='1234567890abcdef1234567890abcdef'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/facebook'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -5,13 +5,12 @@ import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table'; import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { toast } from 'sonner'; import { toast } from 'sonner';
export default function Register() { export default function Page() {
const t = useTranslations('system.register'); const t = useTranslations('auth-control');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getRegisterConfig'], queryKey: ['getRegisterConfig'],
@@ -55,50 +54,6 @@ export default function Register() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('emailVerification')}</Label>
<p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_email_verify}
onCheckedChange={(checked) => {
updateConfig('enable_email_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('emailSuffixWhitelist')}</Label>
<p className='text-muted-foreground text-xs'>{t('emailSuffixWhitelistDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_email_domain_suffix}
onCheckedChange={(checked) => {
updateConfig('enable_email_domain_suffix', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('whitelistSuffixes')}</Label>
<p className='text-muted-foreground text-xs'>{t('whitelistSuffixesDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={t('whitelistSuffixesPlaceholder')}
defaultValue={data?.email_domain_suffix_list}
onBlur={(e) => {
updateConfig('email_domain_suffix_list', e.target.value);
}}
/>
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('ipRegistrationLimit')}</Label> <Label>{t('ipRegistrationLimit')}</Label>
@@ -0,0 +1,104 @@
'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('github');
const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'github'],
queryFn: async () => {
const { data } = await getOAuthByPlatform({
platform: 'github',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
try {
await updateOAuthConfig({
...data,
[key]: value,
} as API.UpdateOAuthConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='e.g., Iv1.1234567890abcdef'
value={data?.config?.client_id}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
client_id: value,
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='e.g., 1234567890abcdef1234567890abcdef12345678'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/github'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,105 @@
'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('google');
const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'google'],
queryFn: async () => {
const { data } = await getOAuthByPlatform({
platform: 'google',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
try {
await updateOAuthConfig({
...data,
[key]: value,
} as API.UpdateOAuthConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='123456789-abc123def456.apps.googleusercontent.com'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/google'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,29 @@
'use client';
import { AuthControl } from '@/config/navs';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface AuthControlLayoutProps {
children: React.ReactNode;
}
export default function AuthControlLayout({ children }: Readonly<AuthControlLayoutProps>) {
const pathname = usePathname();
const t = useTranslations('menu');
if (!pathname) return null;
return (
<Tabs value={pathname}>
<TabsList className='h-full flex-wrap'>
{AuthControl.map((item) => (
<TabsTrigger key={item.url} value={item.url} asChild>
<Link href={item.url}>{t(item.title)}</Link>
</TabsTrigger>
))}
</TabsList>
<TabsContent value={pathname}>{children}</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function Page() {
return redirect('/dashboard/auth-control/general');
}
@@ -0,0 +1,370 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { getSmsList } from '@/services/admin/sms';
import {
getSmsConfig,
getSmsPlatform,
testSmsSend,
updateSmsConfig,
} from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import { Label } from '@workspace/ui/components/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('phone');
const { data, refetch, isFetching } = useQuery({
queryKey: ['getSmsConfig'],
queryFn: async () => {
const { data } = await getSmsConfig();
return data.data;
},
});
const { data: platforms } = useQuery({
queryKey: ['getSmsPlatform'],
queryFn: async () => {
const { data } = await getSmsPlatform();
return data.data?.list;
},
});
const selectedPlatform = platforms?.find((platform) => platform.platform === data?.sms_platform);
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateSmsConfig({
...data,
[key]: value,
} as API.SmsConfig);
toast.success(t('updateSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
const [params, setParams] = useState<API.SendSmsRequest>({
telephone: '',
content: t('testSmsContent'),
area_code: '1',
});
return (
<Tabs defaultValue='settings' className='w-full'>
<TabsList>
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
</TabsList>
<TabsContent value='settings'>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableTip')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.sms_enabled}
onCheckedChange={(checked) => updateConfig('sms_enabled', checked)}
disabled={isFetching}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('expireTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('expireTimeTip')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.sms_expire_time ?? 300}
onValueBlur={(value) => updateConfig('sms_expire_time', value)}
suffix='S'
disabled={isFetching}
placeholder={t('placeholders.expireTime')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('interval')}</Label>
<p className='text-muted-foreground text-xs'>{t('intervalTip')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
value={data?.sms_interval ?? 60}
min={0}
onValueBlur={(value) => updateConfig('sms_interval', value)}
suffix='S'
disabled={isFetching}
placeholder={t('placeholders.interval')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('limit')}</Label>
<p className='text-muted-foreground text-xs'>{t('limitTip')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
value={data?.sms_limit ?? 20}
min={0}
onValueBlur={(value) => updateConfig('sms_limit', value)}
disabled={isFetching}
placeholder={t('placeholders.limit')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('platform')}</Label>
<p className='text-muted-foreground text-xs'>{t('platformTip')}</p>
</TableCell>
<TableCell className='flex items-center gap-1 text-right'>
<Select
value={data?.sms_platform}
onValueChange={(value) => updateConfig('sms_platform', value)}
disabled={isFetching}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{platforms?.map((item) => (
<SelectItem key={item.platform} value={item.platform}>
{item.platform}
</SelectItem>
))}
</SelectContent>
</Select>
{platform_url && (
<Button size='sm' asChild>
<Link href={platform_url} target='_blank'>
{t('applyPlatform')}
</Link>
</Button>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>Key</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_key })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.sms_key ?? ''}
onValueBlur={(value) => updateConfig('sms_key', value)}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.sms_key })}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>Secret</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_secret })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.sms_secret ?? ''}
onValueBlur={(value) => updateConfig('sms_secret', value)}
disabled={isFetching}
type='password'
placeholder={t('platformConfigTip', { key: platformConfig?.sms_secret })}
/>
</TableCell>
</TableRow>
{platformConfig?.sms_template_code && (
<TableRow>
<TableCell>
<Label>{t('templateCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_code })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.sms_template_code ?? ''}
onValueBlur={(value) => updateConfig('sms_template_code', value)}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.sms_template_param && (
<TableRow>
<TableCell>
<Label>{t('templateParam')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_param })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.sms_template_param ?? 'code'}
onValueBlur={(value) => updateConfig('sms_template_param', value)}
disabled={isFetching}
placeholder={t('platformConfigTip', {
key: platformConfig?.sms_template_param,
})}
/>
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell>
<Label>{t('template')}</Label>
<p className='text-muted-foreground text-xs'>
{t('templateTip', { code: platformConfig?.sms_template })}
</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
defaultValue={data?.sms_template ?? ''}
onBlur={(e) => updateConfig('sms_template', e.target.value)}
disabled={isFetching}
placeholder={t('placeholders.template', { code: platformConfig?.sms_template })}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('testSms')}</Label>
<p className='text-muted-foreground text-xs'>{t('testSmsTip')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<AreaCodeSelect
value={params.area_code}
onChange={(value) => {
if (value.phone) {
setParams((prev) => ({ ...prev, area_code: value.phone! }));
}
}}
/>
<EnhancedInput
placeholder={t('testSmsPhone')}
value={params.telephone}
onValueChange={(value) => {
setParams((prev) => ({ ...prev, telephone: value as string }));
}}
/>
<Button
disabled={!params.telephone || !params.area_code}
onClick={async () => {
if (isFetching || !params.telephone || !params.area_code) return;
try {
await testSmsSend(params);
toast.success(t('sendSuccess'));
} catch {
toast.error(t('sendFailed'));
}
}}
>
{t('testSms')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value='logs'>
<LogsTable />
</TabsContent>
</Tabs>
);
}
function LogsTable() {
const t = useTranslations('phone');
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.Sms, { telephone: string }>
action={ref}
header={{
title: t('SmsList'),
}}
columns={[
{
accessorKey: 'platform',
header: t('platform'),
},
{
accessorKey: 'areaCode',
header: t('areaCode'),
},
{
accessorKey: 'telephone',
header: t('telephone'),
},
{
accessorKey: 'content',
header: t('content'),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
const status = row.getValue('status');
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
params={[
{
key: 'telephone',
placeholder: t('search'),
},
]}
request={async (pagination, filter) => {
const { data } = await getSmsList({ ...pagination, ...filter });
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}
@@ -0,0 +1,105 @@
'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('telegram');
const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'telegram'],
queryFn: async () => {
const { data } = await getOAuthByPlatform({
platform: 'telegram',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
try {
await updateOAuthConfig({
...data,
[key]: value,
} as API.UpdateOAuthConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='6123456789'
value={data?.config?.bot}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
bot: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='6123456789:AAHn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={data?.config?.bot_token}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
bot_token: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/telegram'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -2,7 +2,6 @@
import { getSubscribeList } from '@/services/admin/subscribe'; import { getSubscribeList } from '@/services/admin/subscribe';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
@@ -26,6 +25,7 @@ import {
import { Combobox } from '@workspace/ui/custom-components/combobox'; import { Combobox } from '@workspace/ui/custom-components/combobox';
import { DatePicker } from '@workspace/ui/custom-components/date-picker'; import { DatePicker } from '@workspace/ui/custom-components/date-picker';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { unitConversion } from '@workspace/ui/utils'; import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -235,7 +235,7 @@ export default function CouponForm<T extends Record<string, any>>({
)} )}
<FormField <FormField
control={form.control} control={form.control}
name='server' name='subscribe'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.specifiedServer')}</FormLabel> <FormLabel>{t('form.specifiedServer')}</FormLabel>
+2 -2
View File
@@ -96,9 +96,9 @@ export default function Page() {
defaultChecked={row.getValue('enable')} defaultChecked={row.getValue('enable')}
onCheckedChange={async (checked) => { onCheckedChange={async (checked) => {
await updateCoupon({ await updateCoupon({
...row, ...row.original,
enable: checked, enable: checked,
} as any); } as API.UpdateCouponRequest);
ref.current?.refresh(); ref.current?.refresh();
}} }}
/> />
@@ -1,5 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
Form, Form,
@@ -20,6 +19,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { MarkdownEditor } from '@workspace/ui/custom-components/editor'; import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
import { Icon } from '@workspace/ui/custom-components/icon';
import { TagInput } from '@workspace/ui/custom-components/tag-input'; import { TagInput } from '@workspace/ui/custom-components/tag-input';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -91,6 +91,9 @@ const protocolConfigSchema = z.discriminatedUnion('protocol', [
const baseFormSchema = z.object({ const baseFormSchema = z.object({
name: z.string(), name: z.string(),
tags: z.array(z.string()).nullish().default([]),
country: z.string().nullish(),
city: z.string().nullish(),
server_addr: z.string(), server_addr: z.string(),
speed_limit: z.number().nullish(), speed_limit: z.number().nullish(),
traffic_ratio: z.number().default(1), traffic_ratio: z.number().default(1),
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
Form, Form,
@@ -21,6 +20,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -14,8 +14,8 @@ import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs'; import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { DicesIcon } from 'lucide-react'; import { DicesIcon } from 'lucide-react';
import { nanoid } from 'nanoid';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { uid } from 'radash';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Cell, Legend, Pie, PieChart } from 'recharts'; import { Cell, Legend, Pie, PieChart } from 'recharts';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -28,8 +28,53 @@ const COLORS = [
'hsl(var(--chart-5))', 'hsl(var(--chart-5))',
]; ];
export default function Node() { const MINUTES_IN_DAY = 1440; // 24 * 60
const t = useTranslations('system.node');
function getTimeRangeData(slots: API.TimePeriod[]) {
const timePoints = slots
.filter((slot) => slot.start_time && slot.end_time)
.flatMap((slot) => {
const [startH = 0, startM = 0] = slot.start_time.split(':').map(Number);
const [endH = 0, endM = 0] = slot?.end_time?.split(':').map(Number);
const start = startH * 60 + startM;
let end = endH * 60 + endM;
if (end < start) end += MINUTES_IN_DAY;
return { start, end, multiplier: slot.multiplier };
})
.sort((a, b) => a.start - b.start);
const result = [];
let currentMinute = 0;
timePoints.forEach((point) => {
if (point.start > currentMinute) {
result.push({
name: `${Math.floor(currentMinute / 60)}:${String(currentMinute % 60).padStart(2, '0')} - ${Math.floor(point.start / 60)}:${String(point.start % 60).padStart(2, '0')}`,
value: point.start - currentMinute,
multiplier: 1,
});
}
result.push({
name: `${Math.floor(point.start / 60)}:${String(point.start % 60).padStart(2, '0')} - ${Math.floor((point.end / 60) % 24)}:${String(point.end % 60).padStart(2, '0')}`,
value: point.end - point.start,
multiplier: point.multiplier,
});
currentMinute = point.end % MINUTES_IN_DAY;
});
if (currentMinute < MINUTES_IN_DAY) {
result.push({
name: `${Math.floor(currentMinute / 60)}:${String(currentMinute % 60).padStart(2, '0')} - 24:00`,
value: MINUTES_IN_DAY - currentMinute,
multiplier: 1,
});
}
return result;
}
export default function NodeConfig() {
const t = useTranslations('server.config');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getNodeConfig'], queryKey: ['getNodeConfig'],
@@ -68,10 +113,7 @@ export default function Node() {
}); });
const chartTimeSlots = useMemo(() => { const chartTimeSlots = useMemo(() => {
return timeSlots.map((slot) => ({ return getTimeRangeData(timeSlots);
name: `${slot.start_time} - ${slot.end_time}`,
value: slot.multiplier,
}));
}, [timeSlots]); }, [timeSlots]);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
@@ -102,11 +144,16 @@ export default function Node() {
value={data?.node_secret} value={data?.node_secret}
onValueBlur={(value) => updateConfig('node_secret', value)} onValueBlur={(value) => updateConfig('node_secret', value)}
suffix={ suffix={
<DicesIcon <div className='bg-muted flex h-9 items-center text-nowrap px-3'>
onClick={() => { <DicesIcon
updateConfig('node_secret', nanoid()); onClick={() => {
}} const id = uid(32).toLowerCase();
/> const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
updateConfig('node_secret', formatted);
}}
className='cursor-pointer'
/>
</div>
} }
/> />
</TableCell> </TableCell>
@@ -136,6 +183,7 @@ export default function Node() {
<EnhancedInput <EnhancedInput
type='number' type='number'
min={0} min={0}
step={0.1}
value={data?.node_push_interval} value={data?.node_push_interval}
onValueBlur={(value) => updateConfig('node_push_interval', value)} onValueBlur={(value) => updateConfig('node_push_interval', value)}
placeholder={t('inputPlaceholder')} placeholder={t('inputPlaceholder')}
@@ -187,8 +235,8 @@ export default function Node() {
outerRadius='80%' outerRadius='80%'
fill='#8884d8' fill='#8884d8'
dataKey='value' dataKey='value'
label={({ name, percent, value }) => label={({ name, percent, multiplier }) =>
`${value?.toFixed(2)}x (${(percent * 100).toFixed(0)}%)` `${(multiplier || 0)?.toFixed(2)}x (${(percent * 100).toFixed(0)}%)`
} }
> >
{chartTimeSlots.map((entry, index) => ( {chartTimeSlots.map((entry, index) => (
@@ -214,7 +262,7 @@ export default function Node() {
<span className='text-muted-foreground text-[0.70rem] uppercase'> <span className='text-muted-foreground text-[0.70rem] uppercase'>
{t('multiplier')} {t('multiplier')}
</span> </span>
<span className='font-bold'>{data.value.toFixed(2)}x</span> <span className='font-bold'>{data.multiplier.toFixed(2)}x</span>
</div> </div>
</div> </div>
</div> </div>
@@ -236,7 +284,7 @@ export default function Node() {
type: 'time', type: 'time',
}, },
{ name: 'end_time', prefix: t('endTime'), type: 'time' }, { name: 'end_time', prefix: t('endTime'), type: 'time' },
{ name: 'multiplier', prefix: t('multiplier'), type: 'number' }, { name: 'multiplier', prefix: t('multiplier'), type: 'number', placeholder: '0' },
]} ]}
value={timeSlots} value={timeSlots}
onChange={setTimeSlots} onChange={setTimeSlots}
+63 -3
View File
@@ -2,7 +2,6 @@
import { getNodeGroupList } from '@/services/admin/server'; import { getNodeGroupList } from '@/services/admin/server';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
@@ -35,6 +34,8 @@ import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Combobox } from '@workspace/ui/custom-components/combobox'; import { Combobox } from '@workspace/ui/custom-components/combobox';
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs'; import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import TagInput from '@workspace/ui/custom-components/tag-input';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
import { unitConversion } from '@workspace/ui/utils'; import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@@ -63,6 +64,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
const form = useForm({ const form = useForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
tags: [],
traffic_ratio: 1, traffic_ratio: 1,
protocol: 'shadowsocks', protocol: 'shadowsocks',
...initialValues, ...initialValues,
@@ -139,7 +141,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='group_id' name='group_id'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.nodeGroupId')}</FormLabel> <FormLabel>{t('form.groupId')}</FormLabel>
<FormControl> <FormControl>
<Combobox<number, false> <Combobox<number, false>
placeholder={t('form.selectNodeGroup')} placeholder={t('form.selectNodeGroup')}
@@ -148,6 +150,64 @@ export default function NodeForm<T extends { [x: string]: any }>({
value: item.id, value: item.id,
label: item.name, label: item.name,
}))} }))}
onChange={(value) => {
form.setValue(field.name, value || 0);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid grid-cols-5 gap-2'>
<FormField
control={form.control}
name='tags'
render={({ field }) => (
<FormItem className='col-span-3'>
<FormLabel>{t('form.tags')}</FormLabel>
<FormControl>
<TagInput
placeholder={t('form.tagsPlaceholder')}
value={field.value || []}
onChange={(value) => form.setValue(field.name, value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='country'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.country')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='city'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.city')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -190,7 +250,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
}} }}
suffix='MB' suffix='Mbps'
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
+58 -30
View File
@@ -116,6 +116,24 @@ export default function NodeTable() {
); );
}, },
}, },
{
accessorKey: 'name',
header: t('name'),
},
{
accessorKey: 'server_addr',
header: t('serverAddr'),
cell: ({ row }) => {
return (
<div className='flex gap-1'>
<Badge variant='outline'>
{row.original.country} - {row.original.city}
</Badge>
<Badge variant='outline'>{row.getValue('server_addr')}</Badge>
</div>
);
},
},
{ {
accessorKey: 'status', accessorKey: 'status',
header: t('status'), header: t('status'),
@@ -123,19 +141,11 @@ export default function NodeTable() {
return <NodeStatusCell status={row.original?.status} />; return <NodeStatusCell status={row.original?.status} />;
}, },
}, },
{
accessorKey: 'name',
header: t('name'),
},
{
accessorKey: 'server_addr',
header: t('serverAddr'),
},
{ {
accessorKey: 'speed_limit', accessorKey: 'speed_limit',
header: t('speedLimit'), header: t('speedLimit'),
cell: ({ row }) => ( cell: ({ row }) => (
<Display type='traffic' value={row.getValue('speed_limit')} unlimited /> <Display type='trafficSpeed' value={row.getValue('speed_limit')} unlimited />
), ),
}, },
{ {
@@ -152,6 +162,24 @@ export default function NodeTable() {
return name ? <Badge variant='outline'>{name}</Badge> : '--'; return name ? <Badge variant='outline'>{name}</Badge> : '--';
}, },
}, },
{
accessorKey: 'tags',
header: t('tags'),
cell: ({ row }) => {
const tags = (row.getValue('tags') as string[]) || [];
return tags.length > 0 ? (
<div className='flex gap-1'>
{tags.map((tag) => (
<Badge key={tag} variant='outline'>
{tag}
</Badge>
))}
</div>
) : (
'--'
);
},
},
]} ]}
params={[ params={[
{ {
@@ -200,9 +228,24 @@ export default function NodeTable() {
} }
}} }}
/>, />,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteNode({
id: row.id,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
<Button <Button
key='copy' key='copy'
variant='secondary' variant='outline'
onClick={async () => { onClick={async () => {
setLoading(true); setLoading(true);
try { try {
@@ -223,21 +266,6 @@ export default function NodeTable() {
> >
{t('copy')} {t('copy')}
</Button>, </Button>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteNode({
id: row.id,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
], ],
batchRender(rows) { batchRender(rows) {
return [ return [
@@ -263,20 +291,20 @@ export default function NodeTable() {
const sourceIndex = items.findIndex((item) => String(item.id) === source); const sourceIndex = items.findIndex((item) => String(item.id) === source);
const targetIndex = items.findIndex((item) => String(item.id) === target); const targetIndex = items.findIndex((item) => String(item.id) === target);
const originalSortMap = new Map(items.map((item) => [item.id, item.sort || item.id])); const originalSorts = items.map((item) => item.sort);
const [movedItem] = items.splice(sourceIndex, 1); const [movedItem] = items.splice(sourceIndex, 1);
items.splice(targetIndex, 0, movedItem!); items.splice(targetIndex, 0, movedItem!);
const updatedItems = items.map((item, index) => { const updatedItems = items.map((item, index) => {
const originalSort = originalSortMap.get(item.id); const originalSort = originalSorts[index];
const newSort = originalSort !== undefined ? originalSort : item.sort; const newSort = originalSort !== undefined ? originalSort : item.sort;
return { ...item, sort: newSort }; return { ...item, sort: newSort };
}); });
const changedItems = updatedItems.filter( const changedItems = updatedItems.filter((item, index) => {
(item) => originalSortMap.get(item.id) !== item.sort, return item.sort !== items[index]?.sort;
); });
if (changedItems.length > 0) { if (changedItems.length > 0) {
nodeSort({ nodeSort({
+5
View File
@@ -2,6 +2,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import GroupTable from './group-table'; import GroupTable from './group-table';
import NodeConfig from './node-config';
import NodeTable from './node-table'; import NodeTable from './node-table';
export default async function Page() { export default async function Page() {
@@ -12,6 +13,7 @@ export default async function Page() {
<TabsList> <TabsList>
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger> <TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
<TabsTrigger value='group'>{t('tabs.nodeGroup')}</TabsTrigger> <TabsTrigger value='group'>{t('tabs.nodeGroup')}</TabsTrigger>
<TabsTrigger value='config'>{t('tabs.nodeConfig')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='node'> <TabsContent value='node'>
<NodeTable /> <NodeTable />
@@ -19,6 +21,9 @@ export default async function Page() {
<TabsContent value='group'> <TabsContent value='group'>
<GroupTable /> <GroupTable />
</TabsContent> </TabsContent>
<TabsContent value='config'>
<NodeConfig />
</TabsContent>
</Tabs> </Tabs>
); );
} }
@@ -0,0 +1,282 @@
'use client';
import {
getApplication,
getApplicationConfig,
updateApplicationConfig,
} from '@/services/admin/system';
import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Textarea } from '@workspace/ui/components/textarea';
import { Combobox } from '@workspace/ui/custom-components/combobox';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
import { DicesIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { uid } from 'radash';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const formSchema = z.object({
app_id: z.number().optional(),
encryption_key: z.string().optional(),
encryption_method: z.string().optional(),
startup_picture: z.string().optional(),
startup_picture_skip_time: z.number().optional(),
domains: z.array(z.string()).optional(),
});
type FormSchema = z.infer<typeof formSchema>;
export default function ConfigForm() {
const t = useTranslations('subscribe.app');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
app_id: 0,
encryption_key: '',
encryption_method: '',
startup_picture: '',
startup_picture_skip_time: 0,
domains: [],
},
});
const { data, refetch } = useQuery({
queryKey: ['getApplicationConfig'],
queryFn: async () => {
const { data } = await getApplicationConfig();
return data.data;
},
});
const { data: applications } = useQuery({
queryKey: ['getApplication'],
queryFn: async () => {
const { data } = await getApplication();
return data.data?.applications || [];
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: FormSchema) {
setLoading(true);
try {
await updateApplicationConfig({
...values,
domains: values.domains?.filter((domain) => domain),
} as API.ApplicationConfig);
toast.success(t('updateSuccess'));
refetch();
setOpen(false);
} catch (error) {
/* empty */
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant='outline'>
<Icon icon='mdi:cog' className='mr-2' />
{t('config')}
</Button>
</SheetTrigger>
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('configApp')}</SheetTitle>
</SheetHeader>
<ScrollArea className='h-[calc(100dvh-48px-36px-36px)]'>
<Form {...form}>
<form className='space-y-4 py-4'>
<FormField
control={form.control}
name='app_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('selectApp')}</FormLabel>
<FormDescription>{t('selectAppDescription')}</FormDescription>
<FormControl>
<Combobox
{...field}
options={
applications?.map((app) => ({
label: app.name,
value: app.id,
})) || []
}
value={field.value}
onChange={(value) => form.setValue(field.name, value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='encryption_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('communicationKey')}</FormLabel>
<FormDescription>{t('communicationKeyDescription')}</FormDescription>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={(value) => form.setValue(field.name, value as string)}
suffix={
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
<DicesIcon
onClick={() => {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
form.setValue(field.name, formatted);
}}
className='cursor-pointer'
/>
</div>
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='encryption_method'
render={({ field }) => (
<FormItem>
<FormLabel>{t('encryption')}</FormLabel>
<FormDescription>{t('encryptionDescription')}</FormDescription>
<FormControl>
<Combobox
options={[
{ label: 'none', value: 'none' },
{ label: 'AES', value: 'aes' },
]}
value={field.value}
onChange={(value) => form.setValue(field.name, value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='startup_picture'
render={({ field }) => (
<FormItem>
<FormLabel>{t('startupPicture')}</FormLabel>
<FormDescription>{t('startupPictureDescription')}</FormDescription>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={(value) => form.setValue(field.name, value as string)}
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
onChange={(value) => form.setValue('startup_picture', value as string)}
/>
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='startup_picture_skip_time'
render={({ field }) => (
<FormItem>
<FormLabel>{t('startupPictureSkip')}</FormLabel>
<FormDescription>{t('startupPictureSkipDescription')}</FormDescription>
<FormControl>
<EnhancedInput
{...field}
type='number'
min={0}
suffix='S'
value={field.value}
onValueChange={(value) =>
form.setValue('startup_picture_skip_time', Number(value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='domains'
render={({ field }) => (
<FormItem className='px-1'>
<FormLabel>{t('backupDomains')}</FormLabel>
<FormDescription>{t('backupDomainsDescription')}</FormDescription>
<FormControl>
<Textarea
className='h-28'
placeholder='example.com'
value={field.value?.join('\n')}
onChange={(e) => {
form.setValue(field.name, e.target.value.split('\n'));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' onClick={() => setOpen(false)}>
{t('cancel')}
</Button>
<Button onClick={form.handleSubmit(onSubmit)} disabled={loading}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,287 @@
'use client';
import { getSubscribeType } from '@/services/admin/system';
import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
import { useTranslations } from 'next-intl';
import { assign, shake } from 'radash';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const platforms = ['windows', 'macos', 'linux', 'android', 'ios', 'harmony'];
const defaultValues = {
subscribe_type: 'Clash',
name: '',
icon: '',
url: '',
};
const versionSchema = z.object({
url: z.string(),
version: z.string(),
description: z.string().optional(),
is_default: z.boolean().optional(),
});
const formSchema = z.object({
icon: z.string(),
name: z.string(),
subscribe_type: z.string(),
platform: z.object({
windows: z.array(versionSchema).optional(),
macos: z.array(versionSchema).optional(),
linux: z.array(versionSchema).optional(),
android: z.array(versionSchema).optional(),
ios: z.array(versionSchema).optional(),
harmony: z.array(versionSchema).optional(),
}),
});
interface FormProps<T> {
trigger: React.ReactNode | string;
title: string;
initialValues?: Partial<T>;
onSubmit: (values: T) => Promise<boolean>;
loading?: boolean;
}
export default function SubscribeAppForm<
T extends API.CreateApplicationRequest | API.UpdateApplicationRequest,
>({ trigger, title, loading, initialValues, onSubmit }: FormProps<T>) {
const t = useTranslations('subscribe.app');
const [open, setOpen] = useState(false);
type FormSchema = z.infer<typeof formSchema>;
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
defaultValues: assign(
defaultValues,
shake(initialValues, (value) => value === null),
),
});
useEffect(() => {
form.reset(
assign(
defaultValues,
shake(initialValues, (value) => value === null),
),
);
}, [form, initialValues]);
const { data: subscribe_types } = useQuery<string[]>({
queryKey: ['getSubscribeType'],
queryFn: async () => {
const { data } = await getSubscribeType();
return data.data?.subscribe_types || [];
},
});
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
{typeof trigger === 'string' ? <Button>{trigger}</Button> : trigger}
</SheetTrigger>
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='h-[calc(100dvh-48px-36px-36px)]'>
<Form {...form}>
<form className='space-y-4 py-4'>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='icon'
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>{t('appIcon')}</FormLabel>
<FormControl>
<EnhancedInput
required
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
onChange={(value) => {
form.setValue(field.name, value as string);
}}
/>
}
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value as string);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('appName')}</FormLabel>
<FormControl>
<EnhancedInput
required
type='text'
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value as string);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='subscribe_type'
render={({ field }) => (
<FormItem>
<FormLabel>{t('subscriptionProtocol')}</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder={t('subscriptionProtocol')} />
</SelectTrigger>
<SelectContent>
{subscribe_types?.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormItem>
<FormLabel>{t('platform')}</FormLabel>
<Tabs defaultValue={platforms[0]}>
<TabsList>
{platforms.map((platform) => (
<TabsTrigger key={platform} value={platform} className='uppercase'>
{platform}
</TabsTrigger>
))}
</TabsList>
{platforms.map((platform) => (
<TabsContent key={platform} value={platform}>
<FormField
control={form.control}
name={`platform.${platform as keyof FormSchema['platform']}`}
render={({ field }) => (
<FormItem>
<FormControl>
<ArrayInput
isReverse
className='grid grid-cols-3 gap-4'
fields={[
{
name: 'version',
type: 'text',
placeholder: t('version'),
},
{
name: 'description',
type: 'text',
placeholder: t('description'),
},
{
name: 'is_default',
type: 'boolean',
placeholder: t('defaultVersion'),
calculateValue: (value) => {
const newField = field.value?.map((item) => {
if (item.is_default) {
item.is_default = false;
}
return item;
});
form.setValue(field.name, newField);
return value;
},
},
{
name: 'url',
type: 'text',
placeholder: t('downloadLink'),
className: 'col-span-3',
},
]}
value={field.value}
onChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
))}
</Tabs>
</FormItem>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' onClick={() => setOpen(false)}>
{t('cancel')}
</Button>
<Button
onClick={form.handleSubmit(async (values) => {
const success = await onSubmit(values as unknown as T);
if (success) setOpen(false);
})}
disabled={loading}
>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,143 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
createApplication,
deleteApplication,
getApplication,
updateApplication,
} from '@/services/admin/system';
import { Button } from '@workspace/ui/components/button';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { useTranslations } from 'next-intl';
import Image from 'next/legacy/image';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
import ConfigForm from './config';
import SubscribeAppForm from './form';
export default function SubscribeApp() {
const t = useTranslations('subscribe.app');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.ApplicationResponseInfo, Record<string, unknown>>
action={ref}
header={{
title: t('appList'),
toolbar: (
<div className='flex items-center gap-2'>
<ConfigForm />
<SubscribeAppForm<API.CreateApplicationRequest>
trigger={t('create')}
title={t('createApp')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createApplication(values);
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
</div>
),
}}
request={async (_pagination, filters) => {
const { data } = await getApplication();
return {
list: data.data?.applications || [],
total: 0,
};
}}
columns={[
{
accessorKey: 'icon',
header: t('appIcon'),
cell: ({ row }) => (
<Image
src={row.getValue('icon')}
alt={row.getValue('name')}
className='h-8 w-8 rounded-md'
width={32}
height={32}
/>
),
},
{
accessorKey: 'name',
header: t('appName'),
},
{
accessorKey: 'subscribe_type',
header: t('subscriptionProtocol'),
cell: ({ row }) => row.getValue('subscribe_type'),
},
]}
actions={{
render: (row) => [
<SubscribeAppForm<API.UpdateApplicationRequest>
key='edit'
trigger={<Button>{t('edit')}</Button>}
title={t('editApp')}
loading={loading}
initialValues={{
...row,
}}
onSubmit={async (values) => {
setLoading(true);
try {
await updateApplication({
...values,
id: row.id,
});
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteApplication({ id: row.id! });
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
batchRender: (rows) => [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('batchDelete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await Promise.all(rows.map((row) => deleteApplication({ id: row.id! })));
toast.success(t('deleteSuccess'));
ref.current?.reset();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
}}
/>
);
}
@@ -1,5 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
Form, Form,
@@ -19,6 +18,7 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -14,7 +14,7 @@ import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import GroupForm from './group-form'; import GroupForm from './form';
const GroupTable = () => { const GroupTable = () => {
const t = useTranslations('subscribe'); const t = useTranslations('subscribe');
+11 -1
View File
@@ -2,7 +2,9 @@ import { getTranslations } from 'next-intl/server';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import GroupTable from './group-table'; import SubscribeApp from './app/table';
import GroupTable from './group/table';
import SubscribeConfig from './subscribe-config';
import SubscribeTable from './subscribe-table'; import SubscribeTable from './subscribe-table';
export default async function Page() { export default async function Page() {
@@ -13,6 +15,8 @@ export default async function Page() {
<TabsList> <TabsList>
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger> <TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger> <TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
<TabsTrigger value='config'>{t('tabs.subscribeConfig')}</TabsTrigger>
<TabsTrigger value='app'>{t('tabs.subscribeApp')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='subscribe'> <TabsContent value='subscribe'>
<SubscribeTable /> <SubscribeTable />
@@ -20,6 +24,12 @@ export default async function Page() {
<TabsContent value='group'> <TabsContent value='group'>
<GroupTable /> <GroupTable />
</TabsContent> </TabsContent>
<TabsContent value='config'>
<SubscribeConfig />
</TabsContent>
<TabsContent value='app'>
<SubscribeApp />
</TabsContent>
</Tabs> </Tabs>
); );
} }
@@ -0,0 +1,106 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { getSubscribeConfig, updateSubscribeConfig } from '@/services/admin/system';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
export default function SubscribeConfig() {
const t = useTranslations('subscribe.config');
const { data, refetch } = useQuery({
queryKey: ['getSubscribeConfig'],
queryFn: async () => {
const { data } = await getSubscribeConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateSubscribeConfig({
...data,
[key]: value,
} as API.SubscribeConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('singleSubscriptionMode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('singleSubscriptionModeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.single_model}
onCheckedChange={(checked) => {
updateConfig('single_model', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('wildcardResolution')}</Label>
<p className='text-muted-foreground text-xs'>{t('wildcardResolutionDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.pan_domain}
onCheckedChange={(checked) => {
updateConfig('pan_domain', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('subscriptionPath')}</Label>
<p className='text-muted-foreground text-xs'>{t('subscriptionPathDescription')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<EnhancedInput
placeholder={t('subscriptionPathPlaceholder')}
value={data?.subscribe_path}
onValueBlur={(value) => updateConfig('subscribe_path', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('subscriptionDomain')}</Label>
<p className='text-muted-foreground text-xs'>{t('subscriptionDomainDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={t('subscriptionDomainPlaceholder')}
defaultValue={data?.subscribe_domain}
onBlur={(e) => {
updateConfig('subscribe_domain', e.target.value);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -3,7 +3,6 @@
import { getNodeGroupList, getNodeList } from '@/services/admin/server'; import { getNodeGroupList, getNodeList } from '@/services/admin/server';
import { getSubscribeGroupList } from '@/services/admin/subscribe'; import { getSubscribeGroupList } from '@/services/admin/subscribe';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import {
Accordion, Accordion,
@@ -38,6 +37,7 @@ import { Combobox } from '@workspace/ui/custom-components/combobox';
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs'; import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { JSONEditor } from '@workspace/ui/custom-components/editor'; import { JSONEditor } from '@workspace/ui/custom-components/editor';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { evaluateWithPrecision, unitConversion } from '@workspace/ui/utils'; import { evaluateWithPrecision, unitConversion } from '@workspace/ui/utils';
import { CreditCard, Server, Settings } from 'lucide-react'; import { CreditCard, Server, Settings } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@@ -56,7 +56,7 @@ interface SubscribeFormProps<T> {
} }
const defaultValues = { const defaultValues = {
inventory: -1, inventory: 0,
speed_limit: 0, speed_limit: 0,
device_limit: 0, device_limit: 0,
traffic: 0, traffic: 0,
@@ -219,13 +219,13 @@ export default function SubscribeForm<T extends Record<string, any>>({
name='group_id' name='group_id'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.subscribeGroup')}</FormLabel> <FormLabel>{t('form.groupId')}</FormLabel>
<FormControl> <FormControl>
<Combobox<number, false> <Combobox<number, false>
placeholder={t('form.selectSubscribeGroup')} placeholder={t('form.selectSubscribeGroup')}
{...field} {...field}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value || 0);
}} }}
options={group?.map((item) => ({ options={group?.map((item) => ({
label: item.name, label: item.name,
@@ -277,7 +277,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
{...field} {...field}
formatInput={(value) => unitConversion('bitsToMb', value)} formatInput={(value) => unitConversion('bitsToMb', value)}
formatOutput={(value) => unitConversion('mbToBits', value)} formatOutput={(value) => unitConversion('mbToBits', value)}
suffix='MB' suffix='Mbps'
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
}} }}
@@ -319,10 +319,10 @@ export default function SubscribeForm<T extends Record<string, any>>({
<FormLabel>{t('form.inventory')}</FormLabel> <FormLabel>{t('form.inventory')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
placeholder={`-1 ${t('form.noLimit')}`} placeholder={t('form.noLimit')}
type='number' type='number'
{...field} value={field.value === -1 ? 0 : field.value}
min={-1} min={0}
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
}} }}
@@ -36,7 +36,7 @@ export default function SubscribeTable() {
}); });
const ref = useRef<ProTableActions>(null); const ref = useRef<ProTableActions>(null);
return ( return (
<ProTable<API.Subscribe, { group_id: number; query: string }> <ProTable<API.SubscribeItem, { group_id: number; query: string }>
action={ref} action={ref}
header={{ header={{
toolbar: ( toolbar: (
@@ -183,10 +183,15 @@ export default function SubscribeTable() {
return name ? <Badge variant='outline'>{name}</Badge> : '--'; return name ? <Badge variant='outline'>{name}</Badge> : '--';
}, },
}, },
{
accessorKey: 'sold',
header: t('sold'),
cell: ({ row }) => <Badge variant='outline'>{row.getValue('sold')}</Badge>,
},
]} ]}
actions={{ actions={{
render: (row) => [ render: (row) => [
<SubscribeForm<API.Subscribe> <SubscribeForm<API.SubscribeItem>
key='edit' key='edit'
trigger={t('edit')} trigger={t('edit')}
title={t('editSubscribe')} title={t('editSubscribe')}
@@ -210,6 +215,21 @@ export default function SubscribeTable() {
} }
}} }}
/>, />,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteSubscribe({
id: row.id!,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
<Button <Button
key='copy' key='copy'
variant='secondary' variant='secondary'
@@ -221,7 +241,7 @@ export default function SubscribeTable() {
...params, ...params,
show: false, show: false,
sell: false, sell: false,
}); } as API.CreateSubscribeRequest);
toast.success(t('copySuccess')); toast.success(t('copySuccess'));
ref.current?.refresh(); ref.current?.refresh();
setLoading(false); setLoading(false);
@@ -234,21 +254,6 @@ export default function SubscribeTable() {
> >
{t('copy')} {t('copy')}
</Button>, </Button>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteSubscribe({
id: row.id,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
], ],
batchRender: (rows) => [ batchRender: (rows) => [
<ConfirmButton <ConfirmButton
@@ -258,7 +263,7 @@ export default function SubscribeTable() {
description={t('deleteWarning')} description={t('deleteWarning')}
onConfirm={async () => { onConfirm={async () => {
await batchDeleteSubscribe({ await batchDeleteSubscribe({
ids: rows.map((item) => item.id), ids: rows.map((item) => item.id) as number[],
}); });
toast.success(t('deleteSuccess')); toast.success(t('deleteSuccess'));
@@ -273,24 +278,24 @@ export default function SubscribeTable() {
const sourceIndex = items.findIndex((item) => String(item.id) === source); const sourceIndex = items.findIndex((item) => String(item.id) === source);
const targetIndex = items.findIndex((item) => String(item.id) === target); const targetIndex = items.findIndex((item) => String(item.id) === target);
const originalSortMap = new Map(items.map((item) => [item.id, item.sort || item.id])); const originalSorts = items.map((item) => item.sort);
const [movedItem] = items.splice(sourceIndex, 1); const [movedItem] = items.splice(sourceIndex, 1);
items.splice(targetIndex, 0, movedItem!); items.splice(targetIndex, 0, movedItem!);
const updatedItems = items.map((item, index) => { const updatedItems = items.map((item, index) => {
const originalSort = originalSortMap.get(item.id); const originalSort = originalSorts[index];
const newSort = originalSort !== undefined ? originalSort : item.sort; const newSort = originalSort !== undefined ? originalSort : item.sort;
return { ...item, sort: newSort }; return { ...item, sort: newSort };
}); });
const changedItems = updatedItems.filter( const changedItems = updatedItems.filter((item, index) => {
(item) => originalSortMap.get(item.id) !== item.sort, return item.sort !== items[index]?.sort;
); });
if (changedItems.length > 0) { if (changedItems.length > 0) {
subscribeSort({ subscribeSort({
sort: changedItems.map((item) => ({ id: item.id, sort: item.sort })), sort: changedItems.map((item) => ({ id: item.id, sort: item.sort })) as API.SortItem[],
}); });
} }
-150
View File
@@ -1,150 +0,0 @@
'use client';
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { toast } from 'sonner';
export default function Email() {
const t = useTranslations('system.email');
const { data, refetch, isFetching } = useQuery({
queryKey: ['getEmailSmtpConfig'],
queryFn: async () => {
const { data } = await getEmailSmtpConfig();
return data.data;
},
});
const [email, setEmail] = useState<string>();
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateEmailSmtpConfig({
...data,
[key]: value,
} as API.EmailSmtpConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<>
<Table>
<TableBody>
{[
{
key: 'email_smtp_host',
label: t('smtpServerAddress'),
description: t('smtpServerAddressDescription'),
},
{
key: 'email_smtp_port',
label: t('smtpServerPort'),
description: t('smtpServerPortDescription'),
type: 'number',
},
{
key: 'email_smtp_ssl',
label: t('smtpEncryptionMethod'),
description: t('smtpEncryptionMethodDescription'),
component: 'switch',
},
{
key: 'email_smtp_user',
label: t('smtpAccount'),
description: t('smtpAccountDescription'),
},
{
key: 'email_smtp_pass',
label: t('smtpPassword'),
description: t('smtpPasswordDescription'),
type: 'password',
},
{
key: 'email_smtp_from',
label: t('senderAddress'),
description: t('senderAddressDescription'),
},
].map(({ key, label, description, type = 'text', component = 'input' }) => (
<TableRow key={key}>
<TableCell>
<Label>{label}</Label>
<p className='text-muted-foreground text-xs'>{description}</p>
</TableCell>
<TableCell className='text-right'>
{component === 'input' ? (
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.[key]}
type={type}
onValueBlur={(value) => updateConfig(key, value)}
/>
) : (
<Switch
checked={data?.[key]}
onCheckedChange={(checked) => updateConfig(key, checked)}
/>
)}
</TableCell>
</TableRow>
))}
<TableRow>
<TableCell>
<Label>{t('sendTestEmail')}</Label>
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={email}
onValueChange={(value) => setEmail(value as string)}
/>
<Button
disabled={!email}
onClick={async () => {
if (isFetching || !email) return;
try {
await testEmailSmtp({ email });
toast.success(t('sendSuccess'));
} catch {
toast.error(t('sendFailure'));
}
}}
>
{t('sendTestEmail')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<div className='grid grid-cols-1 gap-4 py-4 md:grid-cols-3'>
{['verify_email_template', 'expiration_email_template', 'maintenance_email_template'].map(
(templateKey) => (
<HTMLEditor
key={templateKey}
title={t(`${templateKey}`)}
description={t(`${templateKey}Description`, { after: '{{', before: '}}' })}
placeholder={t('inputPlaceholder')}
value={data?.[templateKey]}
onBlur={(value) => {
updateConfig(templateKey, value);
}}
/>
),
)}
</div>
</>
);
}
-20
View File
@@ -2,12 +2,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import Currency from './currency'; import Currency from './currency';
import Email from './email';
import Invite from './invite'; import Invite from './invite';
import Node from './node';
import Register from './register';
import Site from './site'; import Site from './site';
import Subscription from './subscription';
import Telegram from './telegram'; import Telegram from './telegram';
import Tos from './tos'; import Tos from './tos';
import Verify from './verify'; import Verify from './verify';
@@ -20,11 +16,7 @@ export default async function Page() {
<TabsList className='h-full flex-wrap'> <TabsList className='h-full flex-wrap'>
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger> <TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger> <TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
<TabsTrigger value='subscription'>{t('tabs.subscription')}</TabsTrigger>
<TabsTrigger value='register'>{t('tabs.register')}</TabsTrigger>
<TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger> <TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger>
<TabsTrigger value='email'>{t('tabs.email')}</TabsTrigger>
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
<TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger> <TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger>
<TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger> <TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger>
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger> <TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
@@ -35,21 +27,9 @@ export default async function Page() {
<TabsContent value='currency'> <TabsContent value='currency'>
<Currency /> <Currency />
</TabsContent> </TabsContent>
<TabsContent value='subscription'>
<Subscription />
</TabsContent>
<TabsContent value='register'>
<Register />
</TabsContent>
<TabsContent value='verify'> <TabsContent value='verify'>
<Verify /> <Verify />
</TabsContent> </TabsContent>
<TabsContent value='email'>
<Email />
</TabsContent>
<TabsContent value='node'>
<Node />
</TabsContent>
<TabsContent value='invite'> <TabsContent value='invite'>
<Invite /> <Invite />
</TabsContent> </TabsContent>
+9
View File
@@ -6,6 +6,7 @@ import { Label } from '@workspace/ui/components/label';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table'; import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Textarea } from '@workspace/ui/components/textarea'; import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -47,6 +48,14 @@ export default function Site() {
placeholder={t('logoPlaceholder')} placeholder={t('logoPlaceholder')}
value={data?.site_logo} value={data?.site_logo}
onValueBlur={(value) => updateConfig('site_logo', value)} onValueBlur={(value) => updateConfig('site_logo', value)}
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
onChange={(value) => {
updateConfig('site_logo', value);
}}
/>
}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -1,340 +0,0 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import {
createApplication,
deleteApplication,
getApplication,
getSubscribeConfig,
getSubscribeType,
updateApplication,
updateSubscribeConfig,
} from '@/services/admin/system';
import { Button } from '@workspace/ui/components/button';
import { Label } from '@workspace/ui/components/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
function compareData(
originalData: API.ApplicationResponse,
modifiedData: API.ApplicationResponse,
): {
added: API.Application[];
deleted: API.Application[];
updated: API.Application[];
} {
const added: API.Application[] = [];
const deleted: API.Application[] = [];
const updated: API.Application[] = [];
const findById = (array: API.Application[], id: number): API.Application | undefined => {
return array.find((item) => item.id === id);
};
const isUpdated = (original: API.Application, modified: API.Application): boolean => {
return (
original.name !== modified.name ||
original.platform !== modified.platform ||
original.subscribe_type !== modified.subscribe_type ||
original.url !== modified.url ||
original.icon !== modified.icon
);
};
Object.values(originalData).forEach((platformData) => {
platformData.forEach((originalApp) => {
const modifiedApp = findById(
modifiedData[originalApp.platform as API.CreateApplicationRequest['platform']],
originalApp.id,
);
if (!modifiedApp) {
deleted.push(originalApp);
} else if (isUpdated(originalApp, modifiedApp)) {
updated.push(modifiedApp);
}
});
});
Object.values(modifiedData).forEach((platformData) => {
platformData.forEach((modifiedApp) => {
if (
!findById(
originalData[modifiedApp.platform as API.CreateApplicationRequest['platform']],
modifiedApp.id,
)
) {
added.push(modifiedApp);
}
});
});
return { added, deleted, updated };
}
export default function Subscription() {
const t = useTranslations('system.subscription');
const { data, refetch } = useQuery({
queryKey: ['getSubscribeConfig'],
queryFn: async () => {
const { data } = await getSubscribeConfig();
return data.data;
},
});
const { data: apps, refetch: appsRefetch } = useQuery({
queryKey: ['getApplication'],
queryFn: async () => {
const { data } = await getApplication();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateSubscribeConfig({
...data,
[key]: value,
} as API.SubscribeConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
const { data: subscribe_types } = useQuery<string[]>({
queryKey: ['getSubscribeType'],
queryFn: async () => {
const { data } = await getSubscribeType();
return data.data?.subscribe_types || [];
},
});
const [app, setApp] = useState<API.ApplicationResponse>();
const appTypes = Object.keys(apps || {}) as (keyof API.ApplicationResponse)[];
useEffect(() => {
if (!app) setApp(apps);
}, [app, apps]);
return (
<>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('singleSubscriptionMode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('singleSubscriptionModeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.single_model}
onCheckedChange={(checked) => {
updateConfig('single_model', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('wildcardResolution')}</Label>
<p className='text-muted-foreground text-xs'>{t('wildcardResolutionDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.pan_domain}
onCheckedChange={(checked) => {
updateConfig('pan_domain', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('subscriptionPath')}</Label>
<p className='text-muted-foreground text-xs'>{t('subscriptionPathDescription')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<EnhancedInput
placeholder={t('subscriptionPathPlaceholder')}
value={data?.subscribe_path}
onValueBlur={(value) => updateConfig('subscribe_path', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('subscriptionDomain')}</Label>
<p className='text-muted-foreground text-xs'>{t('subscriptionDomainDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={t('subscriptionDomainPlaceholder')}
defaultValue={data?.subscribe_domain}
onBlur={(e) => {
updateConfig('subscribe_domain', e.target.value);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('app')}</Label>
<p className='text-muted-foreground text-xs'>{t('appDescription')}</p>
</TableCell>
<TableCell className='flex justify-end gap-2'>
<Button
size='sm'
variant='outline'
onClick={() => {
setApp(apps);
}}
>
{t('reset')}
</Button>
<Button
size='sm'
onClick={() => {
const { added, deleted, updated } = compareData(apps!, app!);
added.forEach(async (item) => {
await createApplication(item as API.CreateApplicationRequest);
});
deleted.forEach(async (item) => {
await deleteApplication({
id: item.id,
});
});
updated.forEach(async (item) => {
await updateApplication(item);
});
toast.success(t('saveSuccess'));
appsRefetch();
}}
>
{t('save')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Tabs defaultValue='windows'>
<TabsList className='h-full flex-wrap'>
{appTypes.map((type) => {
return (
<TabsTrigger value={type} key={type} className='uppercase'>
{type}
</TabsTrigger>
);
})}
</TabsList>
{appTypes.map((type) => {
const list = (app?.[type] || []) as API.Application[];
const updatedList = (key: string, value: string, index: number) => {
const newList = list.map((item, i) => (i === index ? { ...item, [key]: value } : item));
setApp({
...app,
[type]: newList,
} as API.ApplicationResponse);
};
return (
<TabsContent value={type} key={type} className='mt-4 space-y-4'>
{list.map((item, index) => {
return (
<div className='flex flex-col items-center gap-2 lg:flex-row' key={index}>
<Select
value={item.subscribe_type}
onValueChange={(value) => {
updatedList('subscribe_type', value, index);
}}
>
<SelectTrigger>
<SelectValue placeholder={t('subscriptionProtocol')} />
</SelectTrigger>
<SelectContent>
{subscribe_types?.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
<EnhancedInput
placeholder={t('appName')}
value={item.name}
onValueChange={(value) => updatedList('name', value as string, index)}
/>
<EnhancedInput
placeholder={t('appIcon')}
value={item.icon}
onValueChange={(value) => updatedList('icon', value as string, index)}
/>
<EnhancedInput
placeholder={t('appDownloadURL')}
value={item.url}
onValueChange={(value) => updatedList('url', value as string, index)}
/>
<Button
variant='destructive'
size='sm'
onClick={() => {
setApp({
...app,
[type]: list.filter((l, i) => i !== index),
} as API.ApplicationResponse);
}}
>
{t('delete')}
</Button>
</div>
);
})}
<Button
className='w-full'
variant='outline'
onClick={() => {
setApp({
...app,
[type]: [
...list,
{
platform: type,
},
],
} as API.ApplicationResponse);
}}
>
{t('add')}
</Button>
</TabsContent>
);
})}
</Tabs>
</>
);
}
+1 -1
View File
@@ -7,7 +7,6 @@ import {
getTicketList, getTicketList,
updateTicketStatus, updateTicketStatus,
} from '@/services/admin/ticket'; } from '@/services/admin/ticket';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
@@ -22,6 +21,7 @@ import { Input } from '@workspace/ui/components/input';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { Icon } from '@workspace/ui/custom-components/icon';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/utils';
import { formatDate } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
+1 -1
View File
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { getSystemLog, restartSystem } from '@/services/admin/tool'; import { getSystemLog, restartSystem } from '@/services/admin/tool';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import {
Accordion, Accordion,
@@ -30,6 +29,7 @@ import {
CardTitle, CardTitle,
} from '@workspace/ui/components/card'; } from '@workspace/ui/components/card';
import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useState } from 'react'; import { useState } from 'react';
+20 -3
View File
@@ -1,11 +1,12 @@
'use client'; 'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table'; import { ProTable, ProTableActions } from '@/components/pro-table';
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user'; import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate, unitConversion } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -71,13 +72,29 @@ export default function Page() {
header: 'ID', header: 'ID',
}, },
{ {
accessorKey: 'email', accessorKey: 'auth_methods',
header: t('userName'), header: t('userName'),
cell: ({ row }) => row.original.auth_methods?.[0]?.auth_identifier || '--',
}, },
{ {
accessorKey: 'balance', accessorKey: 'balance',
header: t('balance'), header: t('balance'),
cell: ({ row }) => unitConversion('centsToDollars', row.getValue('balance')), cell: ({ row }) => <Display type='currency' value={row.getValue('balance')} />,
},
{
accessorKey: 'gift_amount',
header: t('giftAmount'),
cell: ({ row }) => <Display type='currency' value={row.getValue('gift_amount')} />,
},
{
accessorKey: 'commission',
header: t('commission'),
cell: ({ row }) => <Display type='currency' value={row.getValue('commission')} />,
},
{
accessorKey: 'refer_code',
header: t('inviteCode'),
cell: ({ row }) => row.getValue('refer_code') || '--',
}, },
{ {
accessorKey: 'referer_id', accessorKey: 'referer_id',
+17 -2
View File
@@ -1,10 +1,11 @@
'use client'; 'use client';
import { Display } from '@/components/display';
import { getUserDetail } from '@/services/admin/user'; import { getUserDetail } from '@/services/admin/user';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
import { formatDate, unitConversion } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useState } from 'react'; import { useState } from 'react';
@@ -43,7 +44,21 @@ export function UserDetail({ id }: { id: number }) {
</li> </li>
<li className='flex items-center justify-between font-semibold'> <li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>{t('balance')}</span> <span className='text-muted-foreground'>{t('balance')}</span>
<span>{unitConversion('centsToDollars', data?.balance)}</span> <span>
<Display type='currency' value={data?.balance} />
</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>{t('giftAmount')}</span>
<span>
<Display type='currency' value={data?.gift_amount} />
</span>
</li>
<li className='flex items-center justify-between'>
<span className='text-muted-foreground'>{t('commission')}</span>
<span>
<Display type='currency' value={data?.commission} />
</span>
</li> </li>
<li className='flex items-center justify-between'> <li className='flex items-center justify-between'>
<span className='text-muted-foreground'>{t('createdAt')}</span> <span className='text-muted-foreground'>{t('createdAt')}</span>
+101 -3
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import useGlobalStore from '@/config/use-global';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Icon } from '@iconify/react';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
Form, Form,
@@ -21,7 +21,9 @@ import {
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { unitConversion } from '@workspace/ui/utils'; import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -43,17 +45,23 @@ export default function UserForm<T extends Record<string, any>>({
loading, loading,
trigger, trigger,
title, title,
}: UserFormProps<T>) { }: Readonly<UserFormProps<T>>) {
const t = useTranslations('user'); const t = useTranslations('user');
const { common } = useGlobalStore();
const { currency } = common;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(t('form.invalidEmailFormat')), email: z.string().email(t('form.invalidEmailFormat')),
telephone_area_code: z.string().optional(),
telephone: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
referer_id: z.number().optional(), referer_id: z.number().optional(),
refer_code: z.string().optional(), refer_code: z.string().optional(),
is_admin: z.boolean().optional(), is_admin: z.boolean().optional(),
balance: z.number().optional(), balance: z.number().optional(),
gift_amount: z.number().optional(),
commission: z.number().optional(),
}); });
const form = useForm({ const form = useForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@@ -110,6 +118,48 @@ export default function UserForm<T extends Record<string, any>>({
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name='telephone'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.telephone')}</FormLabel>
<FormControl>
<EnhancedInput
prefix={
<FormField
control={form.control}
name='telephone_area_code'
render={({ field }) => (
<FormItem>
<FormControl>
<AreaCodeSelect
className='w-32 rounded-none border-y-0 border-l-0'
simple
placeholder={t('form.areaCodePlaceholder')}
value={field.value}
onChange={(value) => {
form.setValue(field.name, value.phone);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
}
placeholder={t('form.telephonePlaceholder')}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name='password' name='password'
@@ -176,7 +226,7 @@ export default function UserForm<T extends Record<string, any>>({
<FormLabel>{t('form.balance')}</FormLabel> <FormLabel>{t('form.balance')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
prefix='¥' prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.balancePlaceholder')} placeholder={t('form.balancePlaceholder')}
type='number' type='number'
{...field} {...field}
@@ -192,6 +242,54 @@ export default function UserForm<T extends Record<string, any>>({
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name='gift_amount'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.giftAmount')}</FormLabel>
<FormControl>
<EnhancedInput
prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.giftAmountPlaceholder')}
type='number'
{...field}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
min={0}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='commission'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.commission')}</FormLabel>
<FormControl>
<EnhancedInput
prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.commissionPlaceholder')}
type='number'
{...field}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
min={0}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name='is_admin' name='is_admin'
+6 -1
View File
@@ -93,7 +93,12 @@ export default async function RootLayout({
user = await currentUser({ user = await currentUser({
skipErrorHandler: true, skipErrorHandler: true,
Authorization, Authorization,
}).then((res) => res.data.data); }).then((res) => {
if (res.data.data?.is_admin) {
return res.data.data;
}
return undefined;
});
} catch (error) { } catch (error) {
console.log('Error fetching current user:', error); console.log('Error fetching current user:', error);
} }
+34 -11
View File
@@ -3,8 +3,6 @@ import { Card, CardDescription, CardHeader, CardTitle } from '@workspace/ui/comp
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import Link from 'next/link'; import Link from 'next/link';
const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets/billing/index.json';
interface BillingProps { interface BillingProps {
type: 'dashboard' | 'payment'; type: 'dashboard' | 'payment';
} }
@@ -17,23 +15,48 @@ interface ItemType {
href: string; href: string;
} }
async function getBillingURL() {
try {
const response = await fetch(
'https://api.github.com/repos/perfect-panel/ppanel-assets/commits',
);
const json = await response.json();
const version = json[0]?.sha;
const url = new URL('https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets');
url.pathname += `@${version}/billing/index.json`;
return url.toString();
} catch (error) {
return 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets/billing/index.json';
}
}
export default async function Billing({ type }: BillingProps) { export default async function Billing({ type }: BillingProps) {
const t = await getTranslations('common.billing'); const t = await getTranslations('common.billing');
let list: ItemType[] = []; let list: ItemType[] = [];
try { try {
const response = await fetch(BASE_URL, { cache: 'no-store' }); const url = await getBillingURL();
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const response = await fetch(url, {
const data = await response.json(); headers: {
const now = new Date().getTime(); Accept: 'application/json',
list = data[type].filter((item: { expiryDate: string }) => { },
const expiryDate = Date.parse(item.expiryDate);
return !isNaN(expiryDate) && expiryDate > now;
}); });
const data = await response.json();
const now = Date.now();
list = Array.isArray(data[type])
? data[type].filter((item: { expiryDate: string }) => {
const expiryDate = Date.parse(item.expiryDate);
return !isNaN(expiryDate) && expiryDate > now;
})
: [];
} catch (error) { } catch (error) {
console.log('Error fetching billing data:', error); console.log(error);
return null; return null;
} }
if (list && list.length === 0) return null;
if (!list?.length) return null;
return ( return (
<> <>
<h1 className='text mt-2 font-bold'> <h1 className='text mt-2 font-bold'>
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console'; import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@workspace/ui/components/chart'; import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@workspace/ui/components/chart';
@@ -13,6 +12,7 @@ import {
SelectValue, SelectValue,
} from '@workspace/ui/components/select'; } from '@workspace/ui/components/select';
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Icon } from '@workspace/ui/custom-components/icon';
import { formatBytes } from '@workspace/ui/utils'; import { formatBytes } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
+7 -3
View File
@@ -4,7 +4,7 @@ import useGlobalStore from '@/config/use-global';
import { formatBytes, unitConversion } from '@workspace/ui/utils'; import { formatBytes, unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
type DisplayType = 'currency' | 'traffic' | 'number'; type DisplayType = 'currency' | 'traffic' | 'number' | 'trafficSpeed';
interface DisplayProps<T> { interface DisplayProps<T> {
value?: T; value?: T;
@@ -26,12 +26,16 @@ export function Display<T extends number | undefined | null>({
return formattedValue; return formattedValue;
} }
if (['traffic', 'number'].includes(type) && unlimited && !value) { if (['traffic', 'trafficSpeed', 'number'].includes(type) && unlimited && !value) {
return t('unlimited'); return t('unlimited');
} }
if (type === 'traffic') { if (type === 'traffic') {
return value ? formatBytes(value as number) : '0'; return value ? formatBytes(value) : '0';
}
if (type === 'trafficSpeed') {
return value ? formatBytes(value).replace('B', 'b') + 'ps' : '0';
} }
if (type === 'number') { if (type === 'number') {
+1 -1
View File
@@ -2,7 +2,6 @@
import { locales } from '@/config/constants'; import { locales } from '@/config/constants';
import { setLocale } from '@/utils/common'; import { setLocale } from '@/utils/common';
import { Icon } from '@iconify/react';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -10,6 +9,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@workspace/ui/components/select'; } from '@workspace/ui/components/select';
import { Icon } from '@workspace/ui/custom-components/icon';
import { getCountry } from '@workspace/ui/utils'; import { getCountry } from '@workspace/ui/utils';
import { useLocale, useTranslations } from 'next-intl'; import { useLocale, useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
+6 -2
View File
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { navs } from '@/config/navs'; import { navs } from '@/config/navs';
import useGlobalStore from '@/config/use-global'; import useGlobalStore from '@/config/use-global';
import { Icon } from '@iconify/react';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -13,6 +12,7 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
} from '@workspace/ui/components/sidebar'; } from '@workspace/ui/components/sidebar';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Image from 'next/legacy/image'; import Image from 'next/legacy/image';
import Link from 'next/link'; import Link from 'next/link';
@@ -61,7 +61,11 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
<SidebarMenuButton <SidebarMenuButton
asChild asChild
tooltip={t(item.title)} tooltip={t(item.title)}
isActive={item.url === pathname} isActive={
item.url === '/dashboard'
? pathname === item.url
: pathname.startsWith(item.url)
}
> >
<Link href={item.url}> <Link href={item.url}>
{item.icon && <Icon icon={item.icon} />} {item.icon && <Icon icon={item.icon} />}
+4 -2
View File
@@ -27,7 +27,7 @@ export function UserNav() {
<Avatar className='size-8'> <Avatar className='size-8'>
<AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} /> <AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} />
<AvatarFallback className='rounded-none bg-transparent'> <AvatarFallback className='rounded-none bg-transparent'>
{user?.email?.[0]?.toUpperCase()} {user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
</Button> </Button>
@@ -35,7 +35,9 @@ export function UserNav() {
<DropdownMenuContent forceMount align='end' className='w-56'> <DropdownMenuContent forceMount align='end' className='w-56'>
<DropdownMenuLabel className='font-normal'> <DropdownMenuLabel className='font-normal'>
<div className='flex flex-col space-y-1'> <div className='flex flex-col space-y-1'>
<p className='text-sm font-medium leading-none'>{user?.email}</p> <p className='text-sm font-medium leading-none'>
{user?.auth_methods?.[0]?.auth_identifier}
</p>
{/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */} {/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */}
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
+58 -9
View File
@@ -1,3 +1,42 @@
export const AuthControl = [
{
title: 'General',
url: '/dashboard/auth-control/general',
},
{
title: 'Email',
url: '/dashboard/auth-control/email',
},
{
title: 'Phone Number',
url: '/dashboard/auth-control/phone',
},
{
title: 'Telegram',
url: '/dashboard/auth-control/telegram',
},
{
title: 'Apple',
url: '/dashboard/auth-control/apple',
},
{
title: 'Google',
url: '/dashboard/auth-control/google',
},
{
title: 'Facebook',
url: '/dashboard/auth-control/facebook',
},
// {
// title: 'Twitter',
// url: '/dashboard/auth-control/twitter',
// },
{
title: 'GitHub',
url: '/dashboard/auth-control/github',
},
];
export const navs = [ export const navs = [
{ {
title: 'Dashboard', title: 'Dashboard',
@@ -12,6 +51,12 @@ export const navs = [
url: '/dashboard/system', url: '/dashboard/system',
icon: 'flat-color-icons:services', icon: 'flat-color-icons:services',
}, },
{
title: 'Auth Control',
url: '/dashboard/auth-control',
icon: 'flat-color-icons:lock-portrait',
items: AuthControl,
},
{ {
title: 'Payment Config', title: 'Payment Config',
url: '/dashboard/payment', url: '/dashboard/payment',
@@ -82,16 +127,20 @@ export const navs = [
]; ];
export function findNavByUrl(url: string) { export function findNavByUrl(url: string) {
for (const nav of navs) { function findNav(items: any[], url: string, path: any[] = []): any[] {
if (nav.url && nav.url === url) { for (const item of items) {
return [nav]; if (item.url === url) {
} return [...path, item];
if (nav.items) { }
const current = nav.items.find((item) => item.url === url); if (item.items) {
if (current) { const result = findNav(item.items, url, [...path, item]);
return [nav, current]; if (result.length) {
return result;
}
} }
} }
return [];
} }
return [];
return findNav(navs, url);
} }
+28
View File
@@ -21,6 +21,27 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
enable_reset_password_verify: false, enable_reset_password_verify: false,
turnstile_site_key: '', turnstile_site_key: '',
}, },
auth: {
sms: {
sms_enabled: false,
sms_limit: 0,
sms_interval: 0,
sms_expire_time: 0,
},
email: {
email_enabled: false,
email_enable_verify: false,
email_enable_domain_suffix: false,
email_domain_suffix_list: '',
},
register: {
stop_register: false,
enable_trial: false,
enable_ip_register_limit: false,
ip_register_limit: 0,
ip_register_limit_duration: 0,
},
},
register: { register: {
stop_register: false, stop_register: false,
enable_email_verify: false, enable_email_verify: false,
@@ -30,6 +51,12 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
enable_ip_register_limit: false, enable_ip_register_limit: false,
ip_register_limit: 0, ip_register_limit: 0,
ip_register_limit_duration: 0, ip_register_limit_duration: 0,
sms: {
sms_enabled: false,
sms_limit: 0,
sms_interval: 0,
sms_expire_time: 0,
},
}, },
invite: { invite: {
forced_invite: false, forced_invite: false,
@@ -47,6 +74,7 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
subscribe_domain: '', subscribe_domain: '',
pan_domain: false, pan_domain: false,
}, },
oauth_methods: [],
}, },
user: undefined, user: undefined,
setCommon: (common) => setCommon: (common) =>
+16
View File
@@ -0,0 +1,16 @@
{
"clientId": "ID služby",
"clientIdDescription": "Apple Service ID, který můžete získat z Apple Developer Portálu",
"clientSecret": "Soukromý klíč",
"clientSecretDescription": "Obsah soukromého klíče (.p8 soubor) používaný pro autentizaci s Apple",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID",
"keyId": "ID klíče",
"keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu",
"redirectUri": "Návratová URL",
"redirectUriDescription": "URL, na kterou Apple přesměruje po úspěšném ověření",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné",
"teamId": "ID týmu",
"teamIdDescription": "ID týmu Apple Developer"
}
@@ -0,0 +1,13 @@
{
"ipRegistrationLimit": "Limit registrace IP",
"ipRegistrationLimitDescription": "Když je povoleno, IP adresy, které splňují požadavky pravidla, budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo frontendovým proxy serverům",
"penaltyTime": "Čas trestu (minuty)",
"penaltyTimeDescription": "Uživatelé musí počkat, až vyprší doba penalizace, než se mohou znovu zaregistrovat",
"registrationLimitCount": "Počet registrací",
"registrationLimitCountDescription": "Povolit sankci po dosažení limitu registrací",
"saveSuccess": "Uložení bylo úspěšné",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
"stopNewUserRegistrationDescription": "Když je povoleno, nikdo se nemůže registrovat",
"trialRegistration": "Registrace do zkušební verze",
"trialRegistrationDescription": "Povolit registraci na zkušební verzi; nejprve upravte zkušební balíček a dobu trvání"
}
+17 -5
View File
@@ -4,20 +4,28 @@
"continue": "Pokračovat", "continue": "Pokračovat",
"description": "Zadejte svůj e-mail pro pokračování v přihlášení nebo registraci.", "description": "Zadejte svůj e-mail pro pokračování v přihlášení nebo registraci.",
"email": "Zadejte platnou e-mailovou adresu.", "email": "Zadejte platnou e-mailovou adresu.",
"title": "Přihlášení/Registrace", "title": "Přihlášení/Registrace"
"whitelist": "Doména e-mailu není na povoleném seznamu."
}, },
"get": "Získat",
"login": { "login": {
"codeLogin": "Přihlášení pomocí ověřovacího kódu",
"description": "Zadejte prosím své přihlašovací údaje pro přihlášení.", "description": "Zadejte prosím své přihlašovací údaje pro přihlášení.",
"email": "Zadejte platnou e-mailovou adresu.",
"forgotPassword": "Zapomněli jste heslo?", "forgotPassword": "Zapomněli jste heslo?",
"passwordLogin": "Přihlášení pomocí hesla",
"registerAccount": "Registrace účtu",
"success": "Přihlášení bylo úspěšné!", "success": "Přihlášení bylo úspěšné!",
"switchAccount": "Registrace/Přepnout účet",
"title": "Přihlášení" "title": "Přihlášení"
}, },
"logout": "Odhlásit se", "logout": "Odhlásit se",
"methods": {
"email": "E-mail",
"sms": "Telefon"
},
"orWithEmail": "nebo pomocí e-mailu", "orWithEmail": "nebo pomocí e-mailu",
"register": { "register": {
"description": "Vytvořte si nový účet, vyplňte své údaje pro registraci.", "description": "Vytvořte si nový účet, vyplňte své údaje pro registraci.",
"email": "Zadejte platnou e-mailovou adresu.",
"existingAccount": "Již máte účet?", "existingAccount": "Již máte účet?",
"get": "Získat", "get": "Získat",
"invite": "Pozvánkový kód", "invite": "Pozvánkový kód",
@@ -25,15 +33,19 @@
"passwordMismatch": "Hesla se neshodují", "passwordMismatch": "Hesla se neshodují",
"success": "Registrace úspěšná, automaticky přihlášeno!", "success": "Registrace úspěšná, automaticky přihlášeno!",
"switchToLogin": "Přihlásit se/Obnovit e-mail", "switchToLogin": "Přihlásit se/Obnovit e-mail",
"title": "Registrace" "title": "Registrace",
"whitelist": "Doména e-mailu není na povoleném seznamu."
}, },
"reset": { "reset": {
"description": "Zadejte svou e-mailovou adresu pro obnovení hesla.", "description": "Zadejte svou e-mailovou adresu pro obnovení hesla.",
"email": "Zadejte prosím platnou e-mailovou adresu.",
"existingAccount": "Máte již účet?", "existingAccount": "Máte již účet?",
"get": "Získat", "get": "Získat",
"success": "Obnovení hesla bylo úspěšné, automaticky přepnuto na přihlášení!", "success": "Obnovení hesla bylo úspěšné, automaticky přepnuto na přihlášení!",
"switchToLogin": "Přihlášení/Registrace", "switchToLogin": "Přihlášení/Registrace",
"title": "Obnovení hesla" "title": "Obnovení hesla"
}, },
"tos": "Podmínky služby" "tos": "Podmínky služby",
"verifyAccount": "Ověření účtu",
"verifyAccountDesc": "Prosím, ověřte svůj účet, abyste mohli pokračovat."
} }
+1
View File
@@ -26,6 +26,7 @@
"20004": "Tento uživatel byl deaktivován, pokud máte dotazy, kontaktujte zákaznický servis.", "20004": "Tento uživatel byl deaktivován, pokud máte dotazy, kontaktujte zákaznický servis.",
"20005": "Nedostatečný zůstatek, prosím dobijte a zkuste to znovu.", "20005": "Nedostatečný zůstatek, prosím dobijte a zkuste to znovu.",
"20006": "Funkce registrace není momentálně dostupná, zkuste to prosím později.", "20006": "Funkce registrace není momentálně dostupná, zkuste to prosím později.",
"20008": "Informace o uživateli jsou nesprávné, zkontrolujte je a zkuste to znovu.",
"30001": "Tento uzel již existuje, nepřidávejte ho znovu.", "30001": "Tento uzel již existuje, nepřidávejte ho znovu.",
"30002": "Příslušný uzel nebyl nalezen, zkontrolujte a zkuste to znovu.", "30002": "Příslušný uzel nebyl nalezen, zkontrolujte a zkuste to znovu.",
"30003": "Skupina již existuje, zkuste použít jiný název.", "30003": "Skupina již existuje, zkuste použít jiný název.",
+47
View File
@@ -0,0 +1,47 @@
{
"emailBasicConfig": "Základní konfigurace",
"emailBasicConfigDescription": "Nakonfigurujte nastavení SMTP serveru a možnosti ověření e-mailu",
"emailLogs": "Záznamy e-mailů",
"emailLogsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav",
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
"emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu",
"emailTemplate": "Šablona e-mailu",
"emailVerification": "Ověření e-mailu",
"emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail",
"enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"expiration_email_template": "Šablona oznámení o vypršení platnosti",
"expiration_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"failed": "Neúspěch",
"inputPlaceholder": "Zadejte hodnotu...",
"maintenance_email_template": "Šablona oznámení o údržbě",
"maintenance_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"recipient": "Příjemce",
"saveFailed": "Nepodařilo se uložit konfiguraci",
"saveSuccess": "Konfigurace byla úspěšně uložena.",
"sendFailure": "Nepodařilo se odeslat testovací e-mail, zkontrolujte prosím konfiguraci.",
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
"sendTestEmail": "Odeslat testovací e-mail",
"sendTestEmailDescription": "Odeslat testovací e-mail pro ověření konfigurace.",
"senderAddress": "Adresa odesílatele",
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
"sent": "Odesláno",
"sentAt": "Odesláno",
"smtpAccount": "SMTP účet",
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
"smtpEncryptionMethod": "Metoda šifrování SMTP",
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
"smtpPassword": "SMTP heslo",
"smtpPasswordDescription": "Heslo pro účet SMTP.",
"smtpServerAddress": "Adresa SMTP serveru",
"smtpServerAddressDescription": "Zadejte adresu serveru používanou pro odesílání e-mailů.",
"smtpServerPort": "Port SMTP serveru",
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
"status": "Stav",
"subject": "Předmět",
"verify_email_template": "Šablona ověřovacího e-mailu",
"verify_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"whitelistSuffixes": "Povolené přípony",
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu během registrace; jeden na řádek",
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, každou na samostatný řádek"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "ID klienta",
"clientIdDescription": "ID aplikace Facebook z konzole pro vývojáře Facebooku",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Facebook App Secret z Facebook Developers Console",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku",
"redirectUri": "Autorizovaná přesměrovací URI",
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření na Facebooku",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "ID klienta GitHub",
"clientIdDescription": "ID klienta z nastavení vaší GitHub OAuth aplikace",
"clientSecret": "GitHub klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace",
"enable": "Povolit ověřování GitHub",
"enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu",
"redirectUri": "URL pro zpětné volání autorizace",
"redirectUriDescription": "URL ve vaší aplikaci, kam budou uživatelé přesměrováni po autentizaci na GitHubu",
"saveFailed": "Nepodařilo se uložit nastavení GitHubu",
"saveSuccess": "Nastavení GitHubu bylo úspěšně uloženo"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "ID klienta",
"clientIdDescription": "ID klienta Google OAuth 2.0 z Google Cloud Console",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google",
"redirectUri": "Autorizovaná přesměrovací URI",
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření Googlem",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}
+10
View File
@@ -1,18 +1,28 @@
{ {
"Announcement Management": "Správa oznámení", "Announcement Management": "Správa oznámení",
"Apple": "Apple ID",
"Auth Control": "Řízení ověřování",
"Coupon Management": "Správa kupónů", "Coupon Management": "Správa kupónů",
"Dashboard": "Přístrojová deska", "Dashboard": "Přístrojová deska",
"Document Management": "Správa dokumentů", "Document Management": "Správa dokumentů",
"Email": "E-mail",
"Facebook": "Facebook",
"Finance": "Finance", "Finance": "Finance",
"General": "Obecné",
"GitHub": "GitHub",
"Google": "Google",
"Order Management": "Správa objednávek", "Order Management": "Správa objednávek",
"Payment Config": "Konfigurace platby", "Payment Config": "Konfigurace platby",
"Phone Number": "Telefonní číslo",
"Server": "Server", "Server": "Server",
"Server Management": "Správa serveru", "Server Management": "Správa serveru",
"Settings": "Nastavení", "Settings": "Nastavení",
"Subscribe Management": "Správa předplatného", "Subscribe Management": "Správa předplatného",
"System Config": "Systémová konfigurace", "System Config": "Systémová konfigurace",
"System Tool": "Systémový nástroj", "System Tool": "Systémový nástroj",
"Telegram": "Telegram",
"Ticket Management": "Správa lístků", "Ticket Management": "Správa lístků",
"Twitter": "Twitter",
"User": "Uživatel", "User": "Uživatel",
"User Management": "Správa uživatelů" "User Management": "Správa uživatelů"
} }
+41
View File
@@ -0,0 +1,41 @@
{
"SmsList": "Seznam SMS záznamů",
"applyPlatform": "Použít platformu",
"areaCode": "Směrový kód",
"content": "Obsah",
"createdAt": "Čas odeslání",
"enable": "Povolit",
"enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"expireTime": "Čas vypršení",
"expireTimeTip": "Doba platnosti ověřovacího kódu SMS (v sekundách)",
"interval": "Interval",
"intervalTip": "Interval odesílání SMS ověřovacího kódu pro stejné telefonní číslo (v sekundách), 0 znamená bez omezení",
"limit": "Denní limit",
"limitTip": "Denní limit pro odesílání ověřovacích SMS kódů na stejné telefonní číslo, 0 znamená bez omezení",
"logs": "Protokoly",
"placeholders": {
"expireTime": "Zadejte dobu vypršení, výchozí je 300",
"interval": "Zadejte interval, výchozí je 60",
"limit": "Zadejte denní limit, výchozí je 20",
"template": "Váš ověřovací kód je {code}, platný po dobu 5 minut",
"templateCode": "Zadejte kód šablony"
},
"platform": "SMS platforma",
"platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}",
"platformTip": "Vyberte prosím platformu pro SMS",
"search": "Vyhledat telefonní číslo",
"sendFailed": "Odeslání se nezdařilo",
"sendSuccess": "Úspěšně odesláno",
"settings": "Nastavení",
"status": "Stav",
"telephone": "Telefonní číslo",
"template": "Šablona SMS",
"templateCode": "Kód šablony",
"templateParam": "Parametr šablony",
"templateTip": "Vyplňte prosím šablonu SMS, ponechte {code} uprostřed, jinak funkce SMS nebude fungovat",
"testSms": "Odeslat testovací SMS",
"testSmsContent": "Toto je testovací zpráva",
"testSmsPhone": "Zadejte telefonní číslo",
"testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace",
"updateSuccess": "Aktualizace úspěšná"
}
+27 -1
View File
@@ -1,4 +1,24 @@
{ {
"config": {
"addTimeSlot": "Přidat časový úsek",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Klíč pro komunikaci uzlů k zajištění bezpečnosti dat",
"delete": "Smazat",
"dynamicMultiplier": "Dynamický multiplikátor",
"dynamicMultiplierDescription": "Dynamický multiplikátor označuje zpracování posílaných dat s různými multiplikátory pro různé uzly během různých časových období.",
"endTime": "Konec",
"inputPlaceholder": "Zadejte",
"multiplier": "Multiplikátor",
"nodePullInterval": "Interval stahování uzlu",
"nodePullIntervalDescription": "Frekvence (v sekundách) pro stahování dat uzly z panelu",
"nodePushInterval": "Interval odesílání uzlu",
"nodePushIntervalDescription": "Frekvence odesílání dat uzly do panelu",
"reset": "Resetovat",
"save": "Uložit",
"saveSuccess": "Úspěšně uloženo",
"startTime": "Začátek",
"timeSlot": "Časový úsek"
},
"group": { "group": {
"actions": "Akce", "actions": "Akce",
"cancel": "Zrušit", "cancel": "Zrušit",
@@ -46,17 +66,19 @@
"form": { "form": {
"allowInsecure": "Povolit nezabezpečené", "allowInsecure": "Povolit nezabezpečené",
"cancel": "Zrušit", "cancel": "Zrušit",
"city": "Město",
"confirm": "Potvrdit", "confirm": "Potvrdit",
"country": "Země",
"edit": "Upravit", "edit": "Upravit",
"editSecurity": "Upravit nastavení zabezpečení", "editSecurity": "Upravit nastavení zabezpečení",
"enableTLS": "Povolit TLS", "enableTLS": "Povolit TLS",
"encryptionMethod": "Metoda šifrování", "encryptionMethod": "Metoda šifrování",
"flow": "Algoritmus řízení toku", "flow": "Algoritmus řízení toku",
"groupId": "Skupina uzlů",
"hopInterval": "Interval skoků", "hopInterval": "Interval skoků",
"hopPorts": "Porty skoků", "hopPorts": "Porty skoků",
"hopPortsPlaceholder": "Více portů oddělených čárkou", "hopPortsPlaceholder": "Více portů oddělených čárkou",
"name": "Název", "name": "Název",
"nodeGroupId": "ID skupiny uzlů",
"obfsPassword": "Heslo pro zmatení", "obfsPassword": "Heslo pro zmatení",
"obfsPasswordPlaceholder": "Nechte prázdné, pokud nechcete zmást", "obfsPasswordPlaceholder": "Nechte prázdné, pokud nechcete zmást",
"path": "Cesta", "path": "Cesta",
@@ -100,6 +122,8 @@
"serverName": "Název služby", "serverName": "Název služby",
"speedLimit": "Omezení rychlosti", "speedLimit": "Omezení rychlosti",
"speedLimitPlaceholder": "Bez omezení", "speedLimitPlaceholder": "Bez omezení",
"tags": "Štítky",
"tagsPlaceholder": "Použijte Enter nebo čárku (,) pro zadání více štítků",
"trafficRatio": "Poměr provozu", "trafficRatio": "Poměr provozu",
"transport": "Přenosový protokol", "transport": "Přenosový protokol",
"transportConfig": "Nastavení přenosového protokolu", "transportConfig": "Nastavení přenosového protokolu",
@@ -120,6 +144,7 @@
"serverAddr": "Adresa serveru", "serverAddr": "Adresa serveru",
"speedLimit": "Omezení rychlosti", "speedLimit": "Omezení rychlosti",
"status": "Stav", "status": "Stav",
"tags": "Štítky",
"trafficRatio": "Poměr provozu", "trafficRatio": "Poměr provozu",
"type": "Typ", "type": "Typ",
"updateSuccess": "Úspěšně aktualizováno", "updateSuccess": "Úspěšně aktualizováno",
@@ -127,6 +152,7 @@
}, },
"tabs": { "tabs": {
"node": "Uzlu", "node": "Uzlu",
"nodeConfig": "Konfigurace uzlu",
"nodeGroup": "Skupina uzlů" "nodeGroup": "Skupina uzlů"
} }
} }
+61 -5
View File
@@ -1,6 +1,59 @@
{ {
"actions": "akce", "actions": "akce",
"app": {
"appDownloadURL": "URL pro stažení aplikace",
"appIcon": "Ikona aplikace",
"appList": "Seznam aplikací",
"appName": "Název aplikace",
"backupDomains": "Seznam záložních domén",
"backupDomainsDescription": "Seznam záložních domén pro řešení domén, jedna doména na řádek",
"batchDelete": "Hromadné smazání",
"cancel": "Zrušit",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Klíč používaný pro komunikaci klienta",
"config": "Konfigurace",
"configApp": "Konfigurace aplikace",
"confirm": "Potvrdit",
"confirmDelete": "Potvrdit smazání",
"create": "Vytvořit",
"createApp": "Vytvořit aplikaci",
"createSuccess": "Úspěšně vytvořeno",
"defaultVersion": "Výchozí",
"delete": "Smazat",
"deleteWarning": "Tuto akci nelze vrátit zpět",
"describeDescription": "Používá se k popisu aplikace, zobrazuje se v seznamu aplikací",
"description": "Popis",
"downloadLink": "Odkaz ke stažení",
"edit": "Upravit",
"editApp": "Upravit aplikaci",
"encryption": "Metoda šifrování",
"encryptionDescription": "Vyberte metodu šifrování pro komunikaci klienta. Pokud je vybrána, klient použije tuto metodu ke komunikaci se serverem",
"nameDescription": "Název aplikace, zobrazuje se v seznamu aplikací",
"platform": "Platforma",
"selectApp": "Vybrat aplikaci",
"selectAppDescription": "Vyberte aplikaci k nastavení, všechna nastavení se použijí na vybranou aplikaci",
"startupPicture": "Úvodní obrázek",
"startupPictureDescription": "Úvodní obrázek, podporuje síťové a lokální obrázky. Pro síťové obrázky zadejte úplnou URL adresu obrázku",
"startupPicturePreview": "Náhled úvodního obrázku",
"startupPictureSkip": "Čas přeskočení úvodního obrázku",
"startupPictureSkipDescription": "Doba zobrazení úvodního obrázku v sekundách, zadejte 0 pro nezobrazení",
"subscriptionProtocol": "Protokol předplatného",
"updateSuccess": "Úspěšně aktualizováno",
"version": "Verze"
},
"cancel": "Zrušit", "cancel": "Zrušit",
"config": {
"singleSubscriptionMode": "Režim jednorázového předplatného",
"singleSubscriptionModeDescription": "Když je povoleno, všechna uživatelská předplatná budou převedena na zůstatek",
"subscriptionDomain": "Doména předplatného",
"subscriptionDomainDescription": "Používá se pro předplatné; nechte prázdné pro použití domény webu",
"subscriptionDomainPlaceholder": "Zadejte doménu předplatného, jednu na řádek",
"subscriptionPath": "Cesta předplatného",
"subscriptionPathDescription": "Používá se pro předplatné; po úpravě nezapomeňte restartovat systém pro optimální výkon",
"subscriptionPathPlaceholder": "Zadejte",
"wildcardResolution": "Řešení zástupných znaků",
"wildcardResolutionDescription": "Používá se pro předplatné"
},
"confirm": "Potvrdit", "confirm": "Potvrdit",
"confirmDelete": "Opravdu chcete smazat?", "confirmDelete": "Opravdu chcete smazat?",
"copy": "Kopírovat", "copy": "Kopírovat",
@@ -35,14 +88,15 @@
"discountPercent": "Procento slevy", "discountPercent": "Procento slevy",
"discount_price": "Cena po slevě", "discount_price": "Cena po slevě",
"duration": "Doba trvání (měsíce)", "duration": "Doba trvání (měsíce)",
"inventory": "Sklad", "groupId": "Skupina předplatného",
"inventory": "Limit předplatného",
"monthlyReset": "Měsíční Reset", "monthlyReset": "Měsíční Reset",
"name": "Název", "name": "Název",
"noLimit": "Bez omezení", "noLimit": "Bez omezení",
"noReset": "Žádný Reset", "noReset": "Žádný Reset",
"pricing": "Cenotvorba", "pricing": "Cenotvorba",
"purchaseWithDiscount": "Povolit Odpočet", "purchaseWithDiscount": "Povolit Odpočet",
"purchaseWithDiscountDescription": "Povolit nebo zakázat funkci odpočtu. Když je povolena, systém zpracová odpočty na základě nakonfigurovaných pravidel a poměrů", "purchaseWithDiscountDescription": "Povolit nebo zakázat funkci odhlášení. Po aktivaci systém provede zpracová odpočtu podle nakonfigurovaných pravidel a poměrů a zbývající hodnota bude vrácena na zůstatek.",
"quota": "Limit nákupu", "quota": "Limit nákupu",
"renewalReset": "Reset při Obnovení", "renewalReset": "Reset při Obnovení",
"renewalResetDescription": "Resetovací cyklus při obnovení", "renewalResetDescription": "Resetovací cyklus při obnovení",
@@ -55,8 +109,7 @@
"server": "Server", "server": "Server",
"serverGroup": "Skupina serverů", "serverGroup": "Skupina serverů",
"servers": "Servery", "servers": "Servery",
"speedLimit": "Omezení rychlosti (Mbps)", "speedLimit": "Omezení rychlosti ",
"subscribeGroup": "Skupina předplatného",
"traffic": "Přenos dat", "traffic": "Přenos dat",
"unitPrice": "Jednotková cena", "unitPrice": "Jednotková cena",
"unitTime": "Jednotka času" "unitTime": "Jednotka času"
@@ -86,16 +139,19 @@
"updateSuccess": "Úspěšně aktualizováno", "updateSuccess": "Úspěšně aktualizováno",
"updatedAt": "Čas aktualizace" "updatedAt": "Čas aktualizace"
}, },
"inventory": "inventář", "inventory": "Limit předplatného",
"name": "název", "name": "název",
"quota": "Limit na nákup/čas", "quota": "Limit na nákup/čas",
"replacement": "Obnovení ceny/krát", "replacement": "Obnovení ceny/krát",
"sell": "Prodej", "sell": "Prodej",
"show": "Zobrazit na hlavní stránce", "show": "Zobrazit na hlavní stránce",
"sold": "Počet předplatných",
"subscribe": "Předplatit", "subscribe": "Předplatit",
"subscribeGroup": "Přihlásit se ke skupině", "subscribeGroup": "Přihlásit se ke skupině",
"tabs": { "tabs": {
"subscribe": "Předplatit", "subscribe": "Předplatit",
"subscribeApp": "Konfigurace aplikace",
"subscribeConfig": "Konfigurace předplatného",
"subscribeGroup": "Skupina předplatných" "subscribeGroup": "Skupina předplatných"
}, },
"traffic": "provoz", "traffic": "provoz",
-93
View File
@@ -8,32 +8,6 @@
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému", "currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
"saveSuccess": "Úspěšně uloženo" "saveSuccess": "Úspěšně uloženo"
}, },
"email": {
"expiration_email_template": "Šablona oznámení o vypršení platnosti",
"expiration_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte.",
"inputPlaceholder": "Zadejte obsah...",
"maintenance_email_template": "Šablona oznámení o údržbě",
"maintenance_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte.",
"saveSuccess": "Konfigurace byla úspěšně uložena.",
"sendFailure": "Odeslání testovacího e-mailu selhalo, zkontrolujte konfiguraci.",
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
"sendTestEmail": "Odeslat testovací e-mail",
"sendTestEmailDescription": "Odešlete testovací e-mail pro ověření správnosti konfigurace.",
"senderAddress": "Adresa odesílatele",
"senderAddressDescription": "Výchozí e-mailová adresa použitá k odesílání e-mailů.",
"smtpAccount": "Účet SMTP",
"smtpAccountDescription": "E-mailový účet pro ověření identity.",
"smtpEncryptionMethod": "Metoda šifrování SMTP",
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
"smtpPassword": "Heslo SMTP",
"smtpPasswordDescription": "Heslo k účtu SMTP.",
"smtpServerAddress": "Adresa serveru SMTP",
"smtpServerAddressDescription": "Zadejte adresu serveru pro odesílání e-mailů.",
"smtpServerPort": "Port serveru SMTP",
"smtpServerPortDescription": "Zadejte číslo portu pro připojení k serveru SMTP.",
"verify_email_template": "Šablona ověřovacího e-mailu",
"verify_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte."
},
"invite": { "invite": {
"commissionFirstTimeOnly": "Provize pouze při prvním nákupu", "commissionFirstTimeOnly": "Provize pouze při prvním nákupu",
"commissionFirstTimeOnlyDescription": "Po aktivaci se provize generuje pouze při první platbě pozvaného uživatele. Můžete konfigurovat jednotlivé uživatele ve správě uživatelů.", "commissionFirstTimeOnlyDescription": "Po aktivaci se provize generuje pouze při první platbě pozvaného uživatele. Můžete konfigurovat jednotlivé uživatele ve správě uživatelů.",
@@ -44,46 +18,6 @@
"inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.", "inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.",
"saveSuccess": "Úspěšně uloženo" "saveSuccess": "Úspěšně uloženo"
}, },
"node": {
"addTimeSlot": "Přidat časový úsek",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Klíč pro komunikaci uzlu, aby se zajistilo, že data nebudou zachycena jinými osobami",
"delete": "Smazat",
"dynamicMultiplier": "Dynamický násobitel",
"dynamicMultiplierDescription": "Dynamický násobitel označuje zpracování posunutých dat s různými násobiteli pro různé uzly během různých časových období.",
"endTime": "Čas konce",
"inputPlaceholder": "Prosím zadejte",
"multiplier": "Násobitel",
"nodePullInterval": "Interval stahování uzlu",
"nodePullIntervalDescription": "Frekvence, s jakou uzel získává data z panelu (v sekundách)",
"nodePushInterval": "Interval odesílání uzlu",
"nodePushIntervalDescription": "Frekvence, s jakou uzel odesílá data do panelu",
"reset": "Resetovat",
"save": "Uložit",
"saveSuccess": "Úspěšně uloženo",
"startTime": "Čas začátku",
"timeSlot": "Časový úsek"
},
"register": {
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
"emailSuffixWhitelistDescription": "Po aktivaci se mohou registrovat pouze e-maily s příponami uvedenými v seznamu",
"emailVerification": "Ověření e-mailu",
"emailVerificationDescription": "Po aktivaci budou uživatelé muset ověřit svůj e-mail",
"ipRegistrationLimit": "Omezení registrace podle IP",
"ipRegistrationLimitDescription": "Po aktivaci budou IP adresy, které splňují pravidla, omezeny v registraci. Upozorňujeme, že kvůli CDN nebo front-end proxy může dojít k problémům s určením IP adresy",
"penaltyTime": "Doba trestu (minuty)",
"penaltyTimeDescription": "Uživatelé musí počkat, až uplyne doba trestu, než se budou moci znovu registrovat",
"registrationLimitCount": "Počet omezení registrace",
"registrationLimitCountDescription": "Po dosažení limitu registrace se aktivuje trest",
"saveSuccess": "Úspěšně uloženo",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
"stopNewUserRegistrationDescription": "Po aktivaci se nikdo nebude moci registrovat",
"trialRegistration": "Zkušební registrace",
"trialRegistrationDescription": "Aktivujte zkušební registraci, nejprve upravte zkušební balíček a dobu trvání",
"whitelistSuffixes": "Povolené přípony",
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu při registraci, jedna přípona na řádek",
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, jednu na řádek"
},
"site": { "site": {
"logo": "LOGO", "logo": "LOGO",
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO", "logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
@@ -99,37 +33,10 @@
"siteNameDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit název webu", "siteNameDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit název webu",
"siteNamePlaceholder": "Zadejte název webu" "siteNamePlaceholder": "Zadejte název webu"
}, },
"subscription": {
"add": "Přidat",
"app": "Aplikace",
"appDescription": "Nastavte adresu pro stažení aplikace a pravidla importu, nezapomeňte po úpravách uložit",
"appDownloadURL": "Adresa pro stažení aplikace",
"appIcon": "Ikona aplikace",
"appName": "Název aplikace",
"delete": "Smazat",
"reset": "Resetovat",
"save": "Uložit",
"saveSuccess": "Úspěšně uloženo",
"singleSubscriptionMode": "Režim jednorázového předplatného",
"singleSubscriptionModeDescription": "Po aktivaci se všechny uživatelské balíčky převedou na zůstatek",
"subscriptionDomain": "Doména předplatného",
"subscriptionDomainDescription": "Používá se pro předplatné, pokud je prázdné, použije se doména webu",
"subscriptionDomainPlaceholder": "Zadejte doménu předplatného, pro více domén použijte jeden řádek pro každou",
"subscriptionPath": "Cesta předplatného",
"subscriptionPathDescription": "Používá se pro předplatné, po úpravě je nutné restartovat systém pro zajištění nejlepšího výkonu",
"subscriptionPathPlaceholder": "Zadejte",
"subscriptionProtocol": "Protokol předplatného",
"wildcardResolution": "Řešení zástupných znaků",
"wildcardResolutionDescription": "Používá se pro předplatné"
},
"tabs": { "tabs": {
"currency": "Měna", "currency": "Měna",
"email": "E-mail",
"invite": "Pozvat", "invite": "Pozvat",
"node": "Uzlový bod",
"register": "Registrace",
"site": "Stránka", "site": "Stránka",
"subscription": "Předplatné",
"telegram": "Telegram", "telegram": "Telegram",
"tos": "Podmínky služby", "tos": "Podmínky služby",
"verify": "Ověřit" "verify": "Ověřit"
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "ID robota",
"clientIdDescription": "ID Telegram bota, které můžete získat od @BotFather",
"clientSecret": "Token robota",
"clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather",
"enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"redirectUri": "Přesměrovací URL",
"redirectUriDescription": "URL, na kterou Telegram přesměruje po úspěšném ověření",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}
+13 -1
View File
@@ -2,6 +2,7 @@
"actions": "akce", "actions": "akce",
"balance": "Zůstatek", "balance": "Zůstatek",
"cancel": "zrušit", "cancel": "zrušit",
"commission": "Provize",
"confirm": "Potvrdit", "confirm": "Potvrdit",
"confirmDelete": "Opravdu chcete smazat?", "confirmDelete": "Opravdu chcete smazat?",
"create": "Vytvořit", "create": "Vytvořit",
@@ -16,10 +17,16 @@
"email": "e-mail", "email": "e-mail",
"enable": "Povolit", "enable": "Povolit",
"form": { "form": {
"areaCode": "Směrové číslo",
"areaCodePlaceholder": "Směrové číslo",
"balance": "Zůstatek", "balance": "Zůstatek",
"balancePlaceholder": "Zůstatek", "balancePlaceholder": "Zůstatek",
"cancel": "Zrušit", "cancel": "Zrušit",
"commission": "Provize",
"commissionPlaceholder": "Zadejte provizi",
"confirm": "Potvrdit", "confirm": "Potvrdit",
"giftAmount": "Částka daru",
"giftAmountPlaceholder": "Zadejte částku daru",
"invalidEmailFormat": "Neplatný formát e-mailu", "invalidEmailFormat": "Neplatný formát e-mailu",
"inviteCode": "Pozvánkový kód", "inviteCode": "Pozvánkový kód",
"inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)", "inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)",
@@ -28,11 +35,16 @@
"passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)", "passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)",
"refererId": "ID doporučitele", "refererId": "ID doporučitele",
"refererIdPlaceholder": "Zadejte ID doporučitele", "refererIdPlaceholder": "Zadejte ID doporučitele",
"telephone": "Telefonní číslo",
"telephonePlaceholder": "Zadejte telefonní číslo",
"userEmail": "E-mail uživatele", "userEmail": "E-mail uživatele",
"userEmailPlaceholder": "Zadejte e-mail uživatele" "userEmailPlaceholder": "Zadejte e-mail uživatele"
}, },
"giftAmount": "Částka dárku",
"inviteCode": "Pozvánkový kód",
"referer": "Doporučitel", "referer": "Doporučitel",
"telephone": "Telefonní číslo",
"updateSuccess": "Aktualizace byla úspěšná", "updateSuccess": "Aktualizace byla úspěšná",
"userList": "Seznam uživatelů", "userList": "Seznam uživatelů",
"userName": "Uživatelský e-mail" "userName": "Identifikátor uživatele"
} }
+16
View File
@@ -0,0 +1,16 @@
{
"clientId": "Dienst-ID",
"clientIdDescription": "Apple-Dienst-ID, die Sie im Apple Developer Portal erhalten können",
"clientSecret": "Privater Schlüssel",
"clientSecretDescription": "Der private Schlüsselinhalt (.p8-Datei), der für die Authentifizierung bei Apple verwendet wird",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden",
"keyId": "Schlüssel-ID",
"keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal",
"redirectUri": "Rückkehr-URL",
"redirectUriDescription": "Die URL, zu der Apple nach erfolgreicher Authentifizierung weiterleitet",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert",
"teamId": "Team-ID",
"teamIdDescription": "Apple Developer-Team-ID"
}
@@ -0,0 +1,13 @@
{
"ipRegistrationLimit": "IP-Registrierungsgrenze",
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die Bestimmung der IPs aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann",
"penaltyTime": "Strafzeit (Minuten)",
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren können",
"registrationLimitCount": "Anzahl der Anmeldungen",
"registrationLimitCountDescription": "Strafe aktivieren, nachdem das Registrierungslimit erreicht wurde",
"saveSuccess": "Erfolgreich gespeichert",
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
"trialRegistration": "Studienregistrierung",
"trialRegistrationDescription": "Testregistrierung aktivieren; zuerst Testpaket und -dauer anpassen"
}
+17 -5
View File
@@ -4,20 +4,28 @@
"continue": "Fortfahren", "continue": "Fortfahren",
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden oder zu registrieren.", "description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden oder zu registrieren.",
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", "email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"title": "Anmelden/Registrieren", "title": "Anmelden/Registrieren"
"whitelist": "Die E-Mail-Domain befindet sich nicht auf der zugelassenen Whitelist."
}, },
"get": "Holen",
"login": { "login": {
"codeLogin": "Anmeldung mit Bestätigungscode",
"description": "Bitte geben Sie Ihre Kontoinformationen ein, um sich anzumelden.", "description": "Bitte geben Sie Ihre Kontoinformationen ein, um sich anzumelden.",
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"forgotPassword": "Passwort vergessen?", "forgotPassword": "Passwort vergessen?",
"passwordLogin": "Anmeldung mit Passwort",
"registerAccount": "Konto registrieren",
"success": "Erfolgreich eingeloggt!", "success": "Erfolgreich eingeloggt!",
"switchAccount": "Registrieren/Konto wechseln",
"title": "Anmeldung" "title": "Anmeldung"
}, },
"logout": "Abmelden", "logout": "Abmelden",
"methods": {
"email": "E-Mail",
"sms": "Telefon"
},
"orWithEmail": "oder mit E-Mail", "orWithEmail": "oder mit E-Mail",
"register": { "register": {
"description": "Erstellen Sie ein neues Konto, indem Sie Ihre Informationen ausfüllen, um sich zu registrieren.", "description": "Erstellen Sie ein neues Konto, indem Sie Ihre Informationen ausfüllen, um sich zu registrieren.",
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"existingAccount": "Bereits ein Konto?", "existingAccount": "Bereits ein Konto?",
"get": "Erhalten", "get": "Erhalten",
"invite": "Einladungscode", "invite": "Einladungscode",
@@ -25,15 +33,19 @@
"passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein", "passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein",
"success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!", "success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!",
"switchToLogin": "Anmelden/E-Mail zurücksetzen", "switchToLogin": "Anmelden/E-Mail zurücksetzen",
"title": "Registrieren" "title": "Registrieren",
"whitelist": "Die E-Mail-Domain befindet sich nicht in der erlaubten Whitelist."
}, },
"reset": { "reset": {
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um das Passwort zurückzusetzen.", "description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um das Passwort zurückzusetzen.",
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"existingAccount": "Bereits ein Konto?", "existingAccount": "Bereits ein Konto?",
"get": "Erhalten", "get": "Erhalten",
"success": "Passwort erfolgreich zurückgesetzt, automatisch zum Login gewechselt!", "success": "Passwort erfolgreich zurückgesetzt, automatisch zum Login gewechselt!",
"switchToLogin": "Anmelden/Registrieren", "switchToLogin": "Anmelden/Registrieren",
"title": "Passwort zurücksetzen" "title": "Passwort zurücksetzen"
}, },
"tos": "Nutzungsbedingungen" "tos": "Nutzungsbedingungen",
"verifyAccount": "Kontoverifizierung",
"verifyAccountDesc": "Bitte verifizieren Sie Ihr Konto, um fortzufahren."
} }
+1
View File
@@ -26,6 +26,7 @@
"20004": "Dieser Benutzer wurde deaktiviert, bei Fragen wenden Sie sich bitte an den Kundenservice.", "20004": "Dieser Benutzer wurde deaktiviert, bei Fragen wenden Sie sich bitte an den Kundenservice.",
"20005": "Unzureichendes Guthaben, bitte aufladen und erneut versuchen.", "20005": "Unzureichendes Guthaben, bitte aufladen und erneut versuchen.",
"20006": "Die Registrierungsfunktion ist derzeit nicht verfügbar, bitte versuchen Sie es später erneut.", "20006": "Die Registrierungsfunktion ist derzeit nicht verfügbar, bitte versuchen Sie es später erneut.",
"20008": "Die Benutzerinformationen sind falsch, bitte überprüfen Sie sie und versuchen Sie es erneut.",
"30001": "Dieser Knoten existiert bereits, bitte nicht erneut hinzufügen.", "30001": "Dieser Knoten existiert bereits, bitte nicht erneut hinzufügen.",
"30002": "Kein relevanter Knoten gefunden, bitte überprüfen und erneut versuchen.", "30002": "Kein relevanter Knoten gefunden, bitte überprüfen und erneut versuchen.",
"30003": "Die Gruppe existiert bereits, bitte versuchen Sie einen anderen Namen zu verwenden.", "30003": "Die Gruppe existiert bereits, bitte versuchen Sie einen anderen Namen zu verwenden.",
+47
View File
@@ -0,0 +1,47 @@
{
"emailBasicConfig": "Grundkonfiguration",
"emailBasicConfigDescription": "Konfigurieren Sie die SMTP-Servereinstellungen und E-Mail-Verifizierungsoptionen",
"emailLogs": "E-Mail-Protokolle",
"emailLogsDescription": "Sehen Sie sich den Verlauf gesendeter E-Mails und deren Status an",
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren",
"emailTemplate": "E-Mail-Vorlage",
"emailVerification": "E-Mail-Verifizierung",
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"expiration_email_template": "Vorlage für Ablaufbenachrichtigung",
"expiration_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"failed": "Fehlgeschlagen",
"inputPlaceholder": "Wert eingeben...",
"maintenance_email_template": "Wartungshinweis Vorlage",
"maintenance_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"recipient": "Empfänger",
"saveFailed": "Konfiguration konnte nicht gespeichert werden",
"saveSuccess": "Konfiguration erfolgreich gespeichert.",
"sendFailure": "Senden der Test-E-Mail fehlgeschlagen, bitte überprüfen Sie die Konfiguration.",
"sendSuccess": "Test-E-Mail erfolgreich gesendet.",
"sendTestEmail": "Test-E-Mail senden",
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um die Konfiguration zu überprüfen.",
"senderAddress": "Absenderadresse",
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
"sent": "Gesendet",
"sentAt": "Gesendet am",
"smtpAccount": "SMTP-Konto",
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
"smtpPassword": "SMTP-Passwort",
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
"smtpServerAddress": "SMTP-Server-Adresse",
"smtpServerAddressDescription": "Geben Sie die Serveradresse an, die zum Senden von E-Mails verwendet wird.",
"smtpServerPort": "SMTP-Server-Port",
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
"status": "Status",
"subject": "Betreff",
"verify_email_template": "E-Mail-Bestätigungsvorlage",
"verify_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass diese Variablen beibehalten werden.",
"whitelistSuffixes": "Whitelist-Suffixe",
"whitelistSuffixesDescription": "Wird zur E-Mail-Verifizierung während der Registrierung verwendet; eine pro Zeile",
"whitelistSuffixesPlaceholder": "Geben Sie E-Mail-Suffixe ein, eines pro Zeile"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Kunden-ID",
"clientIdDescription": "Facebook-App-ID aus der Facebook-Entwicklerkonsole",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden",
"redirectUri": "Autorisierte Umleitungs-URI",
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Facebook-Authentifizierung weitergeleitet wird",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "GitHub-Client-ID",
"clientIdDescription": "Die Client-ID aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"clientSecret": "GitHub-Clientgeheimnis",
"clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"enable": "GitHub-Authentifizierung aktivieren",
"enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden",
"redirectUri": "Autorisierungs-Callback-URL",
"redirectUriDescription": "Die URL in Ihrer Anwendung, zu der Benutzer nach der GitHub-Authentifizierung weitergeleitet werden",
"saveFailed": "Speichern der GitHub-Einstellungen fehlgeschlagen",
"saveSuccess": "GitHub-Einstellungen erfolgreich gespeichert"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Kunden-ID",
"clientIdDescription": "Google OAuth 2.0-Client-ID aus der Google Cloud Console",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden",
"redirectUri": "Autorisierte Umleitungs-URI",
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Google-Authentifizierung weitergeleitet wird",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}
+10
View File
@@ -1,18 +1,28 @@
{ {
"Announcement Management": "Ankündigungsverwaltung", "Announcement Management": "Ankündigungsverwaltung",
"Apple": "Apple-ID",
"Auth Control": "Authentifizierungskontrolle",
"Coupon Management": "Gutscheinverwaltung", "Coupon Management": "Gutscheinverwaltung",
"Dashboard": "Armaturenbrett", "Dashboard": "Armaturenbrett",
"Document Management": "Dokumentenverwaltung", "Document Management": "Dokumentenverwaltung",
"Email": "E-Mail",
"Facebook": "Facebook",
"Finance": "Finanzen", "Finance": "Finanzen",
"General": "Allgemein",
"GitHub": "GitHub",
"Google": "Google",
"Order Management": "Bestellverwaltung", "Order Management": "Bestellverwaltung",
"Payment Config": "Zahlungskonfiguration", "Payment Config": "Zahlungskonfiguration",
"Phone Number": "Telefonnummer",
"Server": "Dienst", "Server": "Dienst",
"Server Management": "Serververwaltung", "Server Management": "Serververwaltung",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"Subscribe Management": "Abonnementverwaltung", "Subscribe Management": "Abonnementverwaltung",
"System Config": "Systemkonfiguration", "System Config": "Systemkonfiguration",
"System Tool": "Systemwerkzeug", "System Tool": "Systemwerkzeug",
"Telegram": "Telegram",
"Ticket Management": "Ticketverwaltung", "Ticket Management": "Ticketverwaltung",
"Twitter": "Twitter",
"User": "Benutzer", "User": "Benutzer",
"User Management": "Benutzerverwaltung" "User Management": "Benutzerverwaltung"
} }
+41
View File
@@ -0,0 +1,41 @@
{
"SmsList": "SMS-Protokollliste",
"applyPlatform": "Plattform anwenden",
"areaCode": "Vorwahl",
"content": "Inhalt",
"createdAt": "Sendezeit",
"enable": "Aktivieren",
"enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"expireTime": "Ablaufzeit",
"expireTimeTip": "Gültigkeitsdauer des SMS-Bestätigungscodes (Sekunden)",
"interval": "Intervall",
"intervalTip": "SMS-Bestätigungscode-Sendeintervall für dieselbe Telefonnummer (Sekunden), 0 bedeutet keine Begrenzung",
"limit": "Tageslimit",
"limitTip": "Tägliches Limit für das Senden von SMS-Verifizierungscodes an dieselbe Telefonnummer, 0 bedeutet kein Limit",
"logs": "Protokolle",
"placeholders": {
"expireTime": "Geben Sie die Ablaufzeit ein, Standard ist 300",
"interval": "Geben Sie das Intervall ein, Standard ist 60",
"limit": "Geben Sie das tägliche Limit ein, Standard ist 20",
"template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten",
"templateCode": "Geben Sie den Vorlagen-Code ein"
},
"platform": "SMS-Plattform",
"platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus",
"platformTip": "Bitte wählen Sie die SMS-Plattform aus",
"search": "Telefonnummer suchen",
"sendFailed": "Senden fehlgeschlagen",
"sendSuccess": "Erfolgreich gesendet",
"settings": "Einstellungen",
"status": "Status",
"telephone": "Telefonnummer",
"template": "SMS-Vorlage",
"templateCode": "Vorlagen-Code",
"templateParam": "Vorlagenparameter",
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus und lassen Sie {code} in der Mitte stehen, da sonst die SMS-Funktion nicht funktioniert.",
"testSms": "Test-SMS senden",
"testSmsContent": "Dies ist eine Testnachricht",
"testSmsPhone": "Telefonnummer eingeben",
"testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen",
"updateSuccess": "Aktualisierung erfolgreich"
}
+27 -1
View File
@@ -1,4 +1,24 @@
{ {
"config": {
"addTimeSlot": "Zeitfenster hinzufügen",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Schlüssel für die Knotenkommunikation zur Gewährleistung der Datensicherheit",
"delete": "Löschen",
"dynamicMultiplier": "Dynamischer Multiplikator",
"dynamicMultiplierDescription": "Dynamischer Multiplikator bezieht sich auf die Verarbeitung von übertragenen Daten mit unterschiedlichen Multiplikatoren für verschiedene Knoten während unterschiedlicher Zeiträume.",
"endTime": "Endzeit",
"inputPlaceholder": "Eingeben",
"multiplier": "Multiplikator",
"nodePullInterval": "Knotenabfrageintervall",
"nodePullIntervalDescription": "Häufigkeit (in Sekunden), mit der Knoten Daten vom Panel abrufen",
"nodePushInterval": "Knotenübertragungsintervall",
"nodePushIntervalDescription": "Häufigkeit, mit der Knoten Daten an das Panel übertragen",
"reset": "Zurücksetzen",
"save": "Speichern",
"saveSuccess": "Erfolgreich gespeichert",
"startTime": "Startzeit",
"timeSlot": "Zeitfenster"
},
"group": { "group": {
"actions": "Aktionen", "actions": "Aktionen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
@@ -46,17 +66,19 @@
"form": { "form": {
"allowInsecure": "Unsicher zulassen", "allowInsecure": "Unsicher zulassen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"city": "Stadt",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"country": "Land",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"editSecurity": "Sicherheitskonfiguration bearbeiten", "editSecurity": "Sicherheitskonfiguration bearbeiten",
"enableTLS": "TLS aktivieren", "enableTLS": "TLS aktivieren",
"encryptionMethod": "Verschlüsselungsmethode", "encryptionMethod": "Verschlüsselungsmethode",
"flow": "Flusskontrollalgorithmus", "flow": "Flusskontrollalgorithmus",
"groupId": "Knotengruppe",
"hopInterval": "Sprungintervall", "hopInterval": "Sprungintervall",
"hopPorts": "Sprungports", "hopPorts": "Sprungports",
"hopPortsPlaceholder": "Mehrere Ports durch Kommas trennen", "hopPortsPlaceholder": "Mehrere Ports durch Kommas trennen",
"name": "Name", "name": "Name",
"nodeGroupId": "Knotengruppen-ID",
"obfsPassword": "Verschleierungspasswort", "obfsPassword": "Verschleierungspasswort",
"obfsPasswordPlaceholder": "Leer lassen für keine Verschleierung", "obfsPasswordPlaceholder": "Leer lassen für keine Verschleierung",
"path": "Pfad", "path": "Pfad",
@@ -100,6 +122,8 @@
"serverName": "Dienstname", "serverName": "Dienstname",
"speedLimit": "Geschwindigkeitsbegrenzung", "speedLimit": "Geschwindigkeitsbegrenzung",
"speedLimitPlaceholder": "Keine Begrenzung", "speedLimitPlaceholder": "Keine Begrenzung",
"tags": "Tags",
"tagsPlaceholder": "Verwenden Sie die Eingabetaste oder das Komma (,), um mehrere Tags einzugeben",
"trafficRatio": "Verkehrsrate", "trafficRatio": "Verkehrsrate",
"transport": "Transportprotokoll", "transport": "Transportprotokoll",
"transportConfig": "Transportprotokollkonfiguration", "transportConfig": "Transportprotokollkonfiguration",
@@ -120,6 +144,7 @@
"serverAddr": "Serveradresse", "serverAddr": "Serveradresse",
"speedLimit": "Geschwindigkeitsbegrenzung", "speedLimit": "Geschwindigkeitsbegrenzung",
"status": "Status", "status": "Status",
"tags": "Stichwörter",
"trafficRatio": "Verkehrsrate", "trafficRatio": "Verkehrsrate",
"type": "Typ", "type": "Typ",
"updateSuccess": "Erfolgreich aktualisiert", "updateSuccess": "Erfolgreich aktualisiert",
@@ -127,6 +152,7 @@
}, },
"tabs": { "tabs": {
"node": "Knoten", "node": "Knoten",
"nodeConfig": "Knoten-Konfiguration",
"nodeGroup": "Knotengruppe" "nodeGroup": "Knotengruppe"
} }
} }
+61 -5
View File
@@ -1,6 +1,59 @@
{ {
"actions": "Aktionen", "actions": "Aktionen",
"app": {
"appDownloadURL": "App-Download-URL",
"appIcon": "App-Symbol",
"appList": "App-Liste",
"appName": "App-Name",
"backupDomains": "Liste der Backup-Domains",
"backupDomainsDescription": "Liste der Backup-Domains für die Domain-Auflösung, eine Domain pro Zeile",
"batchDelete": "Stapel löschen",
"cancel": "Abbrechen",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Schlüssel, der für die Kommunikation mit dem Client verwendet wird",
"config": "Konfiguration",
"configApp": "App-Konfiguration",
"confirm": "Bestätigen",
"confirmDelete": "Löschen bestätigen",
"create": "Erstellen",
"createApp": "App erstellen",
"createSuccess": "Erfolgreich erstellt",
"defaultVersion": "Standard",
"delete": "Löschen",
"deleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden",
"describeDescription": "Wird verwendet, um die Anwendung zu beschreiben, wird in der App-Liste angezeigt",
"description": "Beschreibung",
"downloadLink": "Download-Link",
"edit": "Bearbeiten",
"editApp": "App bearbeiten",
"encryption": "Verschlüsselungsmethode",
"encryptionDescription": "Wählen Sie die Verschlüsselungsmethode für die Kommunikation mit dem Client. Wenn ausgewählt, wird der Client diese Methode verwenden, um mit dem Server zu kommunizieren.",
"nameDescription": "Anwendungsname, wird in der App-Liste angezeigt",
"platform": "Plattform",
"selectApp": "App auswählen",
"selectAppDescription": "Wählen Sie die App aus, die konfiguriert werden soll. Alle Einstellungen gelten für die ausgewählte App.",
"startupPicture": "Startbild",
"startupPictureDescription": "Startbild, unterstützt Netzwerk- und lokale Bilder. Für Netzwerkbilder bitte die vollständige Bild-URL eingeben",
"startupPicturePreview": "Vorschau des Startbildes",
"startupPictureSkip": "Überspringzeit des Startbildes",
"startupPictureSkipDescription": "Anzeigedauer des Startbildes in Sekunden, 0 eingeben, um nicht anzuzeigen",
"subscriptionProtocol": "Abonnementprotokoll",
"updateSuccess": "Erfolgreich aktualisiert",
"version": "Version"
},
"cancel": "Abbrechen", "cancel": "Abbrechen",
"config": {
"singleSubscriptionMode": "Einzelabonnement-Modus",
"singleSubscriptionModeDescription": "Wenn aktiviert, werden alle Benutzerabonnements in Guthaben umgewandelt",
"subscriptionDomain": "Abonnement-Domain",
"subscriptionDomainDescription": "Wird für Abonnements verwendet; leer lassen, um die Site-Domain zu verwenden",
"subscriptionDomainPlaceholder": "Abonnement-Domain eingeben, eine pro Zeile",
"subscriptionPath": "Abonnement-Pfad",
"subscriptionPathDescription": "Wird für Abonnements verwendet; starten Sie das System nach der Änderung neu, um die optimale Leistung zu gewährleisten",
"subscriptionPathPlaceholder": "Eingeben",
"wildcardResolution": "Wildcard-Auflösung",
"wildcardResolutionDescription": "Wird für Abonnements verwendet"
},
"confirm": "Bestätigen", "confirm": "Bestätigen",
"confirmDelete": "Möchten Sie wirklich löschen?", "confirmDelete": "Möchten Sie wirklich löschen?",
"copy": "Kopieren", "copy": "Kopieren",
@@ -35,14 +88,15 @@
"discountPercent": "Rabattprozentsatz", "discountPercent": "Rabattprozentsatz",
"discount_price": "Rabattpreis", "discount_price": "Rabattpreis",
"duration": "Dauer (Monate)", "duration": "Dauer (Monate)",
"inventory": "Bestand", "groupId": "Abonnementgruppe",
"inventory": "Abonnementslimit",
"monthlyReset": "Monatliches Zurücksetzen", "monthlyReset": "Monatliches Zurücksetzen",
"name": "Name", "name": "Name",
"noLimit": "Keine Begrenzung", "noLimit": "Keine Begrenzung",
"noReset": "Kein Zurücksetzen", "noReset": "Kein Zurücksetzen",
"pricing": "Preisgestaltung", "pricing": "Preisgestaltung",
"purchaseWithDiscount": "Abzug erlauben", "purchaseWithDiscount": "Abzug erlauben",
"purchaseWithDiscountDescription": "Aktivieren oder deaktivieren Sie die Abzugsfunktion. Wenn aktiviert, verarbeitet das System Abzüge basierend auf den konfigurierten Regeln und Verhältnissen.", "purchaseWithDiscountDescription": "Aktivieren oder deaktivieren Sie die Abmeldefunktion. Nach der Aktivierung führt das System die Abzugsverarbeitung gemäß den konfigurierten Regeln und Anteilen durch, und der verbleibende Wert wird dem Guthaben gutgeschrieben.",
"quota": "Kaufbeschränkung", "quota": "Kaufbeschränkung",
"renewalReset": "Zurücksetzen bei Erneuerung", "renewalReset": "Zurücksetzen bei Erneuerung",
"renewalResetDescription": "Zurücksetzungszyklus bei Erneuerung", "renewalResetDescription": "Zurücksetzungszyklus bei Erneuerung",
@@ -55,8 +109,7 @@
"server": "Dienst", "server": "Dienst",
"serverGroup": "Dienstgruppe", "serverGroup": "Dienstgruppe",
"servers": "Server", "servers": "Server",
"speedLimit": "Geschwindigkeitsbegrenzung (Mbps)", "speedLimit": "Geschwindigkeitsbegrenzung ",
"subscribeGroup": "Abonnementgruppe",
"traffic": "Datenvolumen", "traffic": "Datenvolumen",
"unitPrice": "Einheitspreis", "unitPrice": "Einheitspreis",
"unitTime": "Zeiteinheit" "unitTime": "Zeiteinheit"
@@ -86,16 +139,19 @@
"updateSuccess": "Erfolgreich aktualisiert", "updateSuccess": "Erfolgreich aktualisiert",
"updatedAt": "Aktualisierungszeit" "updatedAt": "Aktualisierungszeit"
}, },
"inventory": "Inventar", "inventory": "Abonnementslimit",
"name": "Name", "name": "Name",
"quota": "Kaufbeschränkung/pro Einkauf", "quota": "Kaufbeschränkung/pro Einkauf",
"replacement": "Preis pro Zurücksetzung", "replacement": "Preis pro Zurücksetzung",
"sell": "Verkauf", "sell": "Verkauf",
"show": "Startseite anzeigen", "show": "Startseite anzeigen",
"sold": "Abonnentenzahl",
"subscribe": "Abonnieren", "subscribe": "Abonnieren",
"subscribeGroup": "Gruppe abonnieren", "subscribeGroup": "Gruppe abonnieren",
"tabs": { "tabs": {
"subscribe": "Abonnieren", "subscribe": "Abonnieren",
"subscribeApp": "App-Konfiguration",
"subscribeConfig": "Abonnement-Konfiguration",
"subscribeGroup": "Abonnieren Gruppe" "subscribeGroup": "Abonnieren Gruppe"
}, },
"traffic": "Verkehr", "traffic": "Verkehr",
-93
View File
@@ -8,32 +8,6 @@
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert", "currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
}, },
"email": {
"expiration_email_template": "Ablaufbenachrichtigungsvorlage",
"expiration_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben.",
"inputPlaceholder": "Bitte Inhalt eingeben...",
"maintenance_email_template": "Wartungsbenachrichtigungsvorlage",
"maintenance_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben.",
"saveSuccess": "Die Konfiguration wurde erfolgreich gespeichert.",
"sendFailure": "Der Versand der Test-E-Mail ist fehlgeschlagen. Bitte überprüfen Sie die Konfiguration.",
"sendSuccess": "Die Test-E-Mail wurde erfolgreich gesendet.",
"sendTestEmail": "Test-E-Mail senden",
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um zu überprüfen, ob die Konfiguration korrekt ist.",
"senderAddress": "Absenderadresse",
"senderAddressDescription": "Standard-E-Mail-Adresse, die zum Senden von E-Mails verwendet wird.",
"smtpAccount": "SMTP-Konto",
"smtpAccountDescription": "E-Mail-Konto zur Authentifizierung.",
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
"smtpPassword": "SMTP-Passwort",
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
"smtpServerAddress": "SMTP-Serveradresse",
"smtpServerAddressDescription": "Geben Sie die Serveradresse zum Senden von E-Mails an.",
"smtpServerPort": "SMTP-Serverport",
"smtpServerPortDescription": "Geben Sie die Portnummer für die Verbindung zum SMTP-Server an.",
"verify_email_template": "Verifizierungsmailvorlage",
"verify_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben."
},
"invite": { "invite": {
"commissionFirstTimeOnly": "Nur Provision beim ersten Kauf", "commissionFirstTimeOnly": "Nur Provision beim ersten Kauf",
"commissionFirstTimeOnlyDescription": "Nach Aktivierung wird die Provision nur bei der ersten Zahlung des Eingeladenen generiert. Sie können in der Benutzerverwaltung einen individuellen Benutzer konfigurieren.", "commissionFirstTimeOnlyDescription": "Nach Aktivierung wird die Provision nur bei der ersten Zahlung des Eingeladenen generiert. Sie können in der Benutzerverwaltung einen individuellen Benutzer konfigurieren.",
@@ -44,46 +18,6 @@
"inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.", "inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
}, },
"node": {
"addTimeSlot": "Zeitfenster hinzufügen",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Schlüssel für die Kommunikation des Knotens, um sicherzustellen, dass Daten nicht von anderen abgefangen werden",
"delete": "Löschen",
"dynamicMultiplier": "Dynamischer Multiplikator",
"dynamicMultiplierDescription": "Dynamischer Multiplikator bezieht sich auf die Verarbeitung von übermittelten Daten mit unterschiedlichen Multiplikatoren für verschiedene Knoten während unterschiedlicher Zeiträume.",
"endTime": "Endzeit",
"inputPlaceholder": "Bitte eingeben",
"multiplier": "Multiplikator",
"nodePullInterval": "Knotenabfrageintervall",
"nodePullIntervalDescription": "Häufigkeit, mit der der Knoten Daten vom Panel abruft (Sekunden)",
"nodePushInterval": "Knotenübertragungsintervall",
"nodePushIntervalDescription": "Häufigkeit, mit der der Knoten Daten an das Panel überträgt",
"reset": "Zurücksetzen",
"save": "Speichern",
"saveSuccess": "Erfolgreich gespeichert",
"startTime": "Startzeit",
"timeSlot": "Zeitfenster"
},
"register": {
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit den in der Liste enthaltenen Suffixen registrieren",
"emailVerification": "E-Mail-Verifizierung",
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail verifizieren",
"ipRegistrationLimit": "IP-Registrierungsbeschränkung",
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die den Regelanforderungen entsprechen, in der Registrierung eingeschränkt. Bitte beachten Sie, dass die Bestimmung der IP aufgrund von CDN oder Frontend-Proxy zu Problemen führen kann",
"penaltyTime": "Strafzeit (Minuten)",
"penaltyTimeDescription": "Benutzer müssen die Strafzeit abwarten, bevor sie sich erneut registrieren können",
"registrationLimitCount": "Registrierungsbeschränkungsanzahl",
"registrationLimitCountDescription": "Nach Erreichen der Registrierungsbeschränkung wird die Strafe aktiviert",
"saveSuccess": "Erfolgreich gespeichert",
"stopNewUserRegistration": "Registrierung neuer Benutzer stoppen",
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
"trialRegistration": "Testregistrierung",
"trialRegistrationDescription": "Testregistrierung aktivieren, bitte zuerst Testpaket und -dauer ändern",
"whitelistSuffixes": "Whitelist-Suffixe",
"whitelistSuffixesDescription": "Für die E-Mail-Verifizierung bei der Registrierung, ein Suffix pro Zeile",
"whitelistSuffixesPlaceholder": "Bitte geben Sie E-Mail-Suffixe ein, eines pro Zeile"
},
"site": { "site": {
"logo": "LOGO", "logo": "LOGO",
"logoDescription": "Position zur Anzeige des LOGOs", "logoDescription": "Position zur Anzeige des LOGOs",
@@ -99,37 +33,10 @@
"siteNameDescription": "Position zur Anzeige des Seitennamens", "siteNameDescription": "Position zur Anzeige des Seitennamens",
"siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein" "siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein"
}, },
"subscription": {
"add": "Hinzufügen",
"app": "Anwendung",
"appDescription": "Konfigurieren Sie die Download-Adresse und Importregeln der App. Vergessen Sie nicht, nach Änderungen zu speichern.",
"appDownloadURL": "App-Download-Adresse",
"appIcon": "App-Symbol",
"appName": "App-Name",
"delete": "Löschen",
"reset": "Zurücksetzen",
"save": "Speichern",
"saveSuccess": "Erfolgreich gespeichert",
"singleSubscriptionMode": "Einzelabonnement-Modus",
"singleSubscriptionModeDescription": "Wenn aktiviert, werden alle Benutzerpakete in Guthaben umgewandelt.",
"subscriptionDomain": "Abonnement-Domain",
"subscriptionDomainDescription": "Für Abonnements, leer lassen, um die Site-Domain zu verwenden",
"subscriptionDomainPlaceholder": "Bitte geben Sie die Abonnement-Domain ein, bei mehreren Domains bitte eine pro Zeile",
"subscriptionPath": "Abonnement-Pfad",
"subscriptionPathDescription": "Für Abonnements, bitte starten Sie das System nach Änderungen neu, um die beste Leistung zu gewährleisten",
"subscriptionPathPlaceholder": "Bitte eingeben",
"subscriptionProtocol": "Abonnement-Protokoll",
"wildcardResolution": "Wildcard-Auflösung",
"wildcardResolutionDescription": "Für Abonnements"
},
"tabs": { "tabs": {
"currency": "Währung", "currency": "Währung",
"email": "E-Mail",
"invite": "Einladen", "invite": "Einladen",
"node": "Knoten",
"register": "Registrieren",
"site": "Seite", "site": "Seite",
"subscription": "Abonnement",
"telegram": "Telegram", "telegram": "Telegram",
"tos": "Nutzungsbedingungen", "tos": "Nutzungsbedingungen",
"verify": "Verifizieren" "verify": "Verifizieren"
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Bot-ID",
"clientIdDescription": "Telegram-Bot-ID, die Sie von @BotFather erhalten können",
"clientSecret": "Bot-Token",
"clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"redirectUri": "Weiterleitungs-URL",
"redirectUriDescription": "Die URL, zu der Telegram nach erfolgreicher Authentifizierung weiterleitet",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}
+13 -1
View File
@@ -2,6 +2,7 @@
"actions": "Aktionen", "actions": "Aktionen",
"balance": "Kontostand", "balance": "Kontostand",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"commission": "Provision",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?", "confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?",
"create": "Erstellen", "create": "Erstellen",
@@ -16,10 +17,16 @@
"email": "E-Mail", "email": "E-Mail",
"enable": "Aktivieren", "enable": "Aktivieren",
"form": { "form": {
"areaCode": "Vorwahl",
"areaCodePlaceholder": "Vorwahl eingeben",
"balance": "Kontostand", "balance": "Kontostand",
"balancePlaceholder": "Kontostand", "balancePlaceholder": "Kontostand",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"commission": "Provision",
"commissionPlaceholder": "Geben Sie die Provision ein",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"giftAmount": "Geschenkbetrag",
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
"invalidEmailFormat": "Ungültiges E-Mail-Format", "invalidEmailFormat": "Ungültiges E-Mail-Format",
"inviteCode": "Einladungscode", "inviteCode": "Einladungscode",
"inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)", "inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)",
@@ -28,11 +35,16 @@
"passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)", "passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)",
"refererId": "Empfehler-ID", "refererId": "Empfehler-ID",
"refererIdPlaceholder": "Bitte Empfehler-ID eingeben", "refererIdPlaceholder": "Bitte Empfehler-ID eingeben",
"telephone": "Telefonnummer",
"telephonePlaceholder": "Telefonnummer eingeben",
"userEmail": "Benutzer-E-Mail", "userEmail": "Benutzer-E-Mail",
"userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben" "userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben"
}, },
"giftAmount": "Geschenkbetrag",
"inviteCode": "Einladungscode",
"referer": "Empfehler", "referer": "Empfehler",
"telephone": "Telefonnummer",
"updateSuccess": "Aktualisierung erfolgreich", "updateSuccess": "Aktualisierung erfolgreich",
"userList": "Benutzerliste", "userList": "Benutzerliste",
"userName": "Benutzer-E-Mail" "userName": "Benutzerkennung"
} }
+16
View File
@@ -0,0 +1,16 @@
{
"clientId": "Service ID",
"clientIdDescription": "Apple Service ID, you can get it from Apple Developer Portal",
"clientSecret": "Private Key",
"clientSecretDescription": "The private key content (.p8 file) used for authentication with Apple",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Apple ID",
"keyId": "Key ID",
"keyIdDescription": "The ID of your private key from Apple Developer Portal",
"redirectUri": "Return URL",
"redirectUriDescription": "The URL that Apple will redirect to after successful authentication",
"saveFailed": "Save failed",
"saveSuccess": "Save successful",
"teamId": "Team ID",
"teamIdDescription": "Apple Developer Team ID"
}
@@ -0,0 +1,13 @@
{
"ipRegistrationLimit": "IP Registration Limit",
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies",
"penaltyTime": "Penalty Time (minutes)",
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
"registrationLimitCount": "Registration Limit Count",
"registrationLimitCountDescription": "Enable penalty after reaching registration limit",
"saveSuccess": "Save Successful",
"stopNewUserRegistration": "Stop New User Registration",
"stopNewUserRegistrationDescription": "When enabled, no one can register",
"trialRegistration": "Trial Registration",
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first"
}
+18 -6
View File
@@ -4,36 +4,48 @@
"continue": "Continue", "continue": "Continue",
"description": "Please enter your email to continue logging in or registering.", "description": "Please enter your email to continue logging in or registering.",
"email": "Please enter a valid email address.", "email": "Please enter a valid email address.",
"title": "Login/Register", "title": "Login/Register"
"whitelist": "The email domain is not in the allowed whitelist."
}, },
"get": "Get",
"login": { "login": {
"codeLogin": "Verification Code Login",
"description": "Please enter your account information to log in.", "description": "Please enter your account information to log in.",
"email": "Please enter a valid email address.",
"forgotPassword": "Forgot Password?", "forgotPassword": "Forgot Password?",
"passwordLogin": "Password Login",
"registerAccount": "Register Account",
"success": "Login successful!", "success": "Login successful!",
"switchAccount": "Register/Switch Account",
"title": "Login" "title": "Login"
}, },
"logout": "Logout", "logout": "Logout",
"methods": {
"email": "Email",
"sms": "Telephone"
},
"orWithEmail": "Or with Email", "orWithEmail": "Or with Email",
"register": { "register": {
"description": "Create a new account, fill in your information to register.", "description": "Create a new account, fill in your information to register.",
"email": "Please enter a valid email address.",
"existingAccount": "Already have an account?", "existingAccount": "Already have an account?",
"get": "Get", "get": "Get",
"invite": "Invitation code", "invite": "Invitation code",
"message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**", "message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**",
"passwordMismatch": "Passwords do not match", "passwordMismatch": "Passwords do not match",
"success": "Registration successful, automatically logged in!", "success": "Registration successful, automatically logged in!",
"switchToLogin": "Login/Reset Email", "switchToLogin": "Login",
"title": "Register" "title": "Register",
"whitelist": "The email domain is not in the allowed whitelist."
}, },
"reset": { "reset": {
"description": "Please enter your email address to reset your password.", "description": "Please enter your email address to reset your password.",
"email": "Please enter a valid email address.",
"existingAccount": "Already have an account?", "existingAccount": "Already have an account?",
"get": "Get", "get": "Get",
"success": "Password reset successful, automatically switched to login!", "success": "Password reset successful, automatically switched to login!",
"switchToLogin": "Login/Register", "switchToLogin": "Login/Register",
"title": "Reset Password" "title": "Reset Password"
}, },
"tos": "Terms of Service" "tos": "Terms of Service",
"verifyAccount": "Account Verification",
"verifyAccountDesc": " Please verify your account to continue."
} }
+1
View File
@@ -26,6 +26,7 @@
"20004": "The user is disabled, please contact customer service if you have questions.", "20004": "The user is disabled, please contact customer service if you have questions.",
"20005": "Insufficient balance, please recharge and try again.", "20005": "Insufficient balance, please recharge and try again.",
"20006": "The registration function is temporarily unavailable, please try again later.", "20006": "The registration function is temporarily unavailable, please try again later.",
"20008": "User information is incorrect, please check and try again.",
"30001": "The node already exists, please do not add it again.", "30001": "The node already exists, please do not add it again.",
"30002": "Related node not found, please check and try again.", "30002": "Related node not found, please check and try again.",
"30003": "Group already exists, please try using a different name.", "30003": "Group already exists, please try using a different name.",
+47
View File
@@ -0,0 +1,47 @@
{
"emailBasicConfig": "Basic Configuration",
"emailBasicConfigDescription": "Configure SMTP server settings and email verification options",
"emailLogs": "Email Logs",
"emailLogsDescription": "View the history of sent emails and their status",
"emailSuffixWhitelist": "Email Suffix Whitelist",
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
"emailTemplate": "Email Template",
"emailVerification": "Email Verification",
"emailVerificationDescription": "When enabled, users will need to verify their email",
"enable": "Enable",
"enableDescription": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"expiration_email_template": "Expiration Notice Template",
"expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"failed": "Failed",
"inputPlaceholder": "Enter value...",
"maintenance_email_template": "Maintenance Notice Template",
"maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"recipient": "Recipient",
"saveFailed": "Failed to save configuration",
"saveSuccess": "Configuration saved successfully.",
"sendFailure": "Failed to send test email, please check the configuration.",
"sendSuccess": "Test email sent successfully.",
"sendTestEmail": "Send Test Email",
"sendTestEmailDescription": "Send a test email to verify the configuration.",
"senderAddress": "Sender Address",
"senderAddressDescription": "The default email address used for sending emails.",
"sent": "Sent",
"sentAt": "Sent At",
"smtpAccount": "SMTP Account",
"smtpAccountDescription": "The email account used for authentication.",
"smtpEncryptionMethod": "SMTP Encryption Method",
"smtpEncryptionMethodDescription": "Choose whether to enable SSL/TLS encryption.",
"smtpPassword": "SMTP Password",
"smtpPasswordDescription": "The password for the SMTP account.",
"smtpServerAddress": "SMTP Server Address",
"smtpServerAddressDescription": "Specify the server address used for sending emails.",
"smtpServerPort": "SMTP Server Port",
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
"status": "Status",
"subject": "Subject",
"verify_email_template": "Verification Email Template",
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"whitelistSuffixes": "Whitelist Suffixes",
"whitelistSuffixesDescription": "Used for email verification during registration; one per line",
"whitelistSuffixesPlaceholder": "Enter email suffixes, one per line"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Client ID",
"clientIdDescription": "Facebook App ID from Facebook Developers Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Facebook App Secret from Facebook Developers Console",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Facebook account",
"redirectUri": "Authorized redirect URI",
"redirectUriDescription": "The URL to which the user will be redirected after Facebook authentication",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "GitHub Client ID",
"clientIdDescription": "The client ID from your GitHub OAuth application settings",
"clientSecret": "GitHub Client Secret",
"clientSecretDescription": "The client secret from your GitHub OAuth application settings",
"enable": "Enable GitHub Authentication",
"enableDescription": "Allow users to sign in with their GitHub accounts",
"redirectUri": "Authorization callback URL",
"redirectUriDescription": "The URL in your application where users will be redirected after GitHub authentication",
"saveFailed": "Failed to save GitHub settings",
"saveSuccess": "GitHub settings saved successfully"
}
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Client ID",
"clientIdDescription": "Google OAuth 2.0 Client ID from Google Cloud Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Google account",
"redirectUri": "Authorized redirect URI",
"redirectUriDescription": "The URL to which the user will be redirected after Google authentication",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}
+10
View File
@@ -1,18 +1,28 @@
{ {
"Announcement Management": "Announcement Management", "Announcement Management": "Announcement Management",
"Apple": "Apple ID",
"Auth Control": "Auth Control",
"Coupon Management": "Coupon Management", "Coupon Management": "Coupon Management",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Document Management": "Document Management", "Document Management": "Document Management",
"Email": "Email",
"Facebook": "Facebook",
"Finance": "Finance", "Finance": "Finance",
"General": "General",
"GitHub": "GitHub",
"Google": "Google",
"Order Management": "Order Management", "Order Management": "Order Management",
"Payment Config": "Payment Config", "Payment Config": "Payment Config",
"Phone Number": "Phone Number",
"Server": "Server", "Server": "Server",
"Server Management": "Server Management", "Server Management": "Server Management",
"Settings": "Settings", "Settings": "Settings",
"Subscribe Management": "Subscribe Management", "Subscribe Management": "Subscribe Management",
"System Config": "System Config", "System Config": "System Config",
"System Tool": "System Tool", "System Tool": "System Tool",
"Telegram": "Telegram",
"Ticket Management": "Ticket Management", "Ticket Management": "Ticket Management",
"Twitter": "Twitter",
"User": "User", "User": "User",
"User Management": "User Management" "User Management": "User Management"
} }
+41
View File
@@ -0,0 +1,41 @@
{
"SmsList": "SMS Log List",
"applyPlatform": "Apply Platform",
"areaCode": "Area Code",
"content": "Content",
"createdAt": "Send Time",
"enable": "Enable",
"enableTip": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"expireTime": "Expire Time",
"expireTimeTip": "SMS verification code validity period (seconds)",
"interval": "Interval",
"intervalTip": "SMS verification code sending interval for the same phone number (seconds), 0 means no limit",
"limit": "Daily Limit",
"limitTip": "Daily SMS verification code sending limit for the same phone number, 0 means no limit",
"logs": "Logs",
"placeholders": {
"expireTime": "Enter expire time, default is 300",
"interval": "Enter interval time, default is 60",
"limit": "Enter daily limit, default is 20",
"template": "Your verification code is {code}, valid for 5 minutes",
"templateCode": "Enter template code"
},
"platform": "SMS Platform",
"platformConfigTip": "Please fill in the provided {key} configuration",
"platformTip": "Please select SMS platform",
"search": "Search phone number",
"sendFailed": "Send Failed",
"sendSuccess": "Send Success",
"settings": "Settings",
"status": "Status",
"telephone": "Phone Number",
"template": "SMS Template",
"templateCode": "Template Code",
"templateParam": "Template Parameter",
"templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work",
"testSms": "Send Test SMS",
"testSmsContent": "This is a test message",
"testSmsPhone": "Enter phone number",
"testSmsTip": "Send a test SMS to verify your configuration",
"updateSuccess": "Update Success"
}
+27 -1
View File
@@ -1,4 +1,24 @@
{ {
"config": {
"addTimeSlot": "Add Time Slot",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Key for node communication to ensure data security",
"delete": "Delete",
"dynamicMultiplier": "Dynamic Multiplier",
"dynamicMultiplierDescription": "Dynamic Multiplier refers to processing pushed data with different multipliers for different nodes during different time periods.",
"endTime": "End Time",
"inputPlaceholder": "Enter",
"multiplier": "Multiplier",
"nodePullInterval": "Node Pull Interval",
"nodePullIntervalDescription": "Frequency (in seconds) for nodes to fetch data from the panel",
"nodePushInterval": "Node Push Interval",
"nodePushIntervalDescription": "Frequency for nodes to push data to the panel",
"reset": "Reset",
"save": "Save",
"saveSuccess": "Save Successful",
"startTime": "Start Time",
"timeSlot": "Time Slot"
},
"group": { "group": {
"actions": "Actions", "actions": "Actions",
"cancel": "Cancel", "cancel": "Cancel",
@@ -46,17 +66,19 @@
"form": { "form": {
"allowInsecure": "Allow Insecure", "allowInsecure": "Allow Insecure",
"cancel": "Cancel", "cancel": "Cancel",
"city": "City",
"confirm": "Confirm", "confirm": "Confirm",
"country": "Country",
"edit": "Edit", "edit": "Edit",
"editSecurity": "Edit Security Configuration", "editSecurity": "Edit Security Configuration",
"enableTLS": "Enable TLS", "enableTLS": "Enable TLS",
"encryptionMethod": "Encryption Method", "encryptionMethod": "Encryption Method",
"flow": "Flow Control Algorithm", "flow": "Flow Control Algorithm",
"groupId": "Node Group",
"hopInterval": "Hop Interval", "hopInterval": "Hop Interval",
"hopPorts": "Hop Ports", "hopPorts": "Hop Ports",
"hopPortsPlaceholder": "Separate multiple ports with commas", "hopPortsPlaceholder": "Separate multiple ports with commas",
"name": "Name", "name": "Name",
"nodeGroupId": "Node Group ID",
"obfsPassword": "Obfuscation Password", "obfsPassword": "Obfuscation Password",
"obfsPasswordPlaceholder": "Leave blank for no obfuscation", "obfsPasswordPlaceholder": "Leave blank for no obfuscation",
"path": "Path", "path": "Path",
@@ -100,6 +122,8 @@
"serverName": "Service Name", "serverName": "Service Name",
"speedLimit": "Speed Limit", "speedLimit": "Speed Limit",
"speedLimitPlaceholder": "Unlimited", "speedLimitPlaceholder": "Unlimited",
"tags": "Tags",
"tagsPlaceholder": "Use Enter or comma (,) to enter multiple tags",
"trafficRatio": "Traffic Rate", "trafficRatio": "Traffic Rate",
"transport": "Transport Protocol Configuration", "transport": "Transport Protocol Configuration",
"transportConfig": "Transport Protocol Configuration", "transportConfig": "Transport Protocol Configuration",
@@ -120,6 +144,7 @@
"serverAddr": "Server Address", "serverAddr": "Server Address",
"speedLimit": "Speed Limit", "speedLimit": "Speed Limit",
"status": "Status", "status": "Status",
"tags": "Tags",
"trafficRatio": "Traffic Rate", "trafficRatio": "Traffic Rate",
"type": "Type", "type": "Type",
"updateSuccess": "Update successful", "updateSuccess": "Update successful",
@@ -127,6 +152,7 @@
}, },
"tabs": { "tabs": {
"node": "Node", "node": "Node",
"nodeConfig": "Node Configuration",
"nodeGroup": "Node Group" "nodeGroup": "Node Group"
} }
} }
+61 -5
View File
@@ -1,6 +1,59 @@
{ {
"actions": "Actions", "actions": "Actions",
"app": {
"appDownloadURL": "App Download URL",
"appIcon": "App Icon",
"appList": "App List",
"appName": "App Name",
"backupDomains": "Backup Domain List",
"backupDomainsDescription": "Backup domain list for domain resolution, one domain per line",
"batchDelete": "Batch Delete",
"cancel": "Cancel",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Key used for client communication",
"config": "Config",
"configApp": "App Configuration",
"confirm": "Confirm",
"confirmDelete": "Are you sure you want to delete?",
"create": "Create",
"createApp": "Create App",
"createSuccess": "Created successfully",
"defaultVersion": "Default",
"delete": "Delete",
"deleteWarning": "Data cannot be recovered after deletion. Please proceed with caution.",
"describeDescription": "Used to describe the application, displayed in the app list",
"description": "Description",
"downloadLink": "Download Link",
"edit": "Edit",
"editApp": "Edit App",
"encryption": "Encryption Method",
"encryptionDescription": "Choose the encryption method for client communication. If selected, the client will use this method to communicate with the server",
"nameDescription": "Application name, displayed in the app list",
"platform": "Platform",
"selectApp": "Select App",
"selectAppDescription": "Select the app to configure, all settings will apply to the selected app",
"startupPicture": "Startup Picture",
"startupPictureDescription": "Startup picture, supports network and local images. For network images, please enter the complete image URL",
"startupPicturePreview": "Startup Picture Preview",
"startupPictureSkip": "Startup Picture Skip Time",
"startupPictureSkipDescription": "Startup picture display time in seconds, enter 0 to not display",
"subscriptionProtocol": "Subscription Protocol",
"updateSuccess": "Updated successfully",
"version": "Version"
},
"cancel": "Cancel", "cancel": "Cancel",
"config": {
"singleSubscriptionMode": "Single Subscription Mode",
"singleSubscriptionModeDescription": "When enabled, all user subscriptions will be converted to balance",
"subscriptionDomain": "Subscription Domain",
"subscriptionDomainDescription": "Used for subscription; leave blank to use site domain",
"subscriptionDomainPlaceholder": "Enter subscription domain, one per line",
"subscriptionPath": "Subscription Path",
"subscriptionPathDescription": "Used for subscription; be sure to restart the system after modification for optimal performance",
"subscriptionPathPlaceholder": "Enter",
"wildcardResolution": "Wildcard Resolution",
"wildcardResolutionDescription": "Used for subscription"
},
"confirm": "Confirm", "confirm": "Confirm",
"confirmDelete": "Are you sure you want to delete?", "confirmDelete": "Are you sure you want to delete?",
"copy": "Copy", "copy": "Copy",
@@ -35,14 +88,15 @@
"discountPercent": "Discount Percentage", "discountPercent": "Discount Percentage",
"discount_price": "Discount Price", "discount_price": "Discount Price",
"duration": "Duration (months)", "duration": "Duration (months)",
"inventory": "Inventory", "groupId": "Subscription Group",
"inventory": "Subscription Limit",
"monthlyReset": "Monthly Reset", "monthlyReset": "Monthly Reset",
"name": "Name", "name": "Name",
"noLimit": "No Limit", "noLimit": "No Limit",
"noReset": "No Reset", "noReset": "No Reset",
"pricing": "Pricing", "pricing": "Pricing",
"purchaseWithDiscount": "Allow Deduction", "purchaseWithDiscount": "Allow Deduction",
"purchaseWithDiscountDescription": "Enable or disable the deduction feature. When enabled, the system will process deductions based on the configured rules and ratios", "purchaseWithDiscountDescription": "Enable or disable unsubscribe functionality. After activation, the system will perform deduction processing according to the configured rules and proportions, and the remaining value will be returned to the balance",
"quota": "Purchase Limit", "quota": "Purchase Limit",
"renewalReset": "Renewal Reset", "renewalReset": "Renewal Reset",
"renewalResetDescription": "Reset cycle upon renewal", "renewalResetDescription": "Reset cycle upon renewal",
@@ -55,8 +109,7 @@
"server": "Server", "server": "Server",
"serverGroup": "Server Group", "serverGroup": "Server Group",
"servers": "Servers", "servers": "Servers",
"speedLimit": "Speed Limit (Mbps)", "speedLimit": "Speed Limit ",
"subscribeGroup": "Subscription Group",
"traffic": "Traffic", "traffic": "Traffic",
"unitPrice": "Unit Price", "unitPrice": "Unit Price",
"unitTime": "Unit Time" "unitTime": "Unit Time"
@@ -86,16 +139,19 @@
"updateSuccess": "Update Successful", "updateSuccess": "Update Successful",
"updatedAt": "Updated At" "updatedAt": "Updated At"
}, },
"inventory": "Inventory", "inventory": "Subscription Limit",
"name": "Name", "name": "Name",
"quota": "Purchase Limit/Time", "quota": "Purchase Limit/Time",
"replacement": "Reset Price/Time", "replacement": "Reset Price/Time",
"sell": "Sell", "sell": "Sell",
"show": "Home display", "show": "Home display",
"sold": "Subscription Count",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"subscribeGroup": "Subscription Group", "subscribeGroup": "Subscription Group",
"tabs": { "tabs": {
"subscribe": "Subscribe", "subscribe": "Subscribe",
"subscribeApp": "App Configuration",
"subscribeConfig": "Subscription Configuration",
"subscribeGroup": "Subscription Group" "subscribeGroup": "Subscription Group"
}, },
"traffic": "Traffic", "traffic": "Traffic",
-93
View File
@@ -8,32 +8,6 @@
"currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system", "currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system",
"saveSuccess": "Save Successful" "saveSuccess": "Save Successful"
}, },
"email": {
"expiration_email_template": "Expiration Notice Template",
"expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"inputPlaceholder": "Enter value...",
"maintenance_email_template": "Maintenance Notice Template",
"maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"saveSuccess": "Configuration saved successfully.",
"sendFailure": "Failed to send test email, please check the configuration.",
"sendSuccess": "Test email sent successfully.",
"sendTestEmail": "Send Test Email",
"sendTestEmailDescription": "Send a test email to verify the configuration.",
"senderAddress": "Sender Address",
"senderAddressDescription": "The default email address used for sending emails.",
"smtpAccount": "SMTP Account",
"smtpAccountDescription": "The email account used for authentication.",
"smtpEncryptionMethod": "SMTP Encryption Method",
"smtpEncryptionMethodDescription": "Choose whether to enable SSL/TLS encryption.",
"smtpPassword": "SMTP Password",
"smtpPasswordDescription": "The password for the SMTP account.",
"smtpServerAddress": "SMTP Server Address",
"smtpServerAddressDescription": "Specify the server address used for sending emails.",
"smtpServerPort": "SMTP Server Port",
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
"verify_email_template": "Verification Email Template",
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables."
},
"invite": { "invite": {
"commissionFirstTimeOnly": "Commission for First Purchase Only", "commissionFirstTimeOnly": "Commission for First Purchase Only",
"commissionFirstTimeOnlyDescription": "When enabled, commission is generated only on the inviter's first payment; you can configure individual users in user management", "commissionFirstTimeOnlyDescription": "When enabled, commission is generated only on the inviter's first payment; you can configure individual users in user management",
@@ -44,46 +18,6 @@
"inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management", "inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management",
"saveSuccess": "Save Successful" "saveSuccess": "Save Successful"
}, },
"node": {
"addTimeSlot": "Add Time Slot",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Key for node communication to ensure data security",
"delete": "Delete",
"dynamicMultiplier": "Dynamic Multiplier",
"dynamicMultiplierDescription": "Dynamic Multiplier refers to processing pushed data with different multipliers for different nodes during different time periods.",
"endTime": "End Time",
"inputPlaceholder": "Enter",
"multiplier": "Multiplier",
"nodePullInterval": "Node Pull Interval",
"nodePullIntervalDescription": "Frequency (in seconds) for nodes to fetch data from the panel",
"nodePushInterval": "Node Push Interval",
"nodePushIntervalDescription": "Frequency for nodes to push data to the panel",
"reset": "Reset",
"save": "Save",
"saveSuccess": "Save Successful",
"startTime": "Start Time",
"timeSlot": "Time Slot"
},
"register": {
"emailSuffixWhitelist": "Email Suffix Whitelist",
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
"emailVerification": "Email Verification",
"emailVerificationDescription": "When enabled, users will need to verify their email",
"ipRegistrationLimit": "IP Registration Limit",
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies",
"penaltyTime": "Penalty Time (minutes)",
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
"registrationLimitCount": "Registration Limit Count",
"registrationLimitCountDescription": "Enable penalty after reaching registration limit",
"saveSuccess": "Save Successful",
"stopNewUserRegistration": "Stop New User Registration",
"stopNewUserRegistrationDescription": "When enabled, no one can register",
"trialRegistration": "Trial Registration",
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first",
"whitelistSuffixes": "Whitelist Suffixes",
"whitelistSuffixesDescription": "Used for email verification during registration; one per line",
"whitelistSuffixesPlaceholder": "Enter email suffixes, one per line"
},
"site": { "site": {
"logo": "Logo", "logo": "Logo",
"logoDescription": "Used for displaying the logo in designated locations", "logoDescription": "Used for displaying the logo in designated locations",
@@ -99,37 +33,10 @@
"siteNameDescription": "Used for displaying the site name in designated locations", "siteNameDescription": "Used for displaying the site name in designated locations",
"siteNamePlaceholder": "Enter site name" "siteNamePlaceholder": "Enter site name"
}, },
"subscription": {
"add": "Add",
"app": "App",
"appDescription": "Configure app download URL and import rules; remember to save after modification",
"appDownloadURL": "App Download URL",
"appIcon": "App Icon",
"appName": "App Name",
"delete": "Delete",
"reset": "Reset",
"save": "Save",
"saveSuccess": "Save Successful",
"singleSubscriptionMode": "Single Subscription Mode",
"singleSubscriptionModeDescription": "When enabled, all user subscriptions will be converted to balance",
"subscriptionDomain": "Subscription Domain",
"subscriptionDomainDescription": "Used for subscription; leave blank to use site domain",
"subscriptionDomainPlaceholder": "Enter subscription domain, one per line",
"subscriptionPath": "Subscription Path",
"subscriptionPathDescription": "Used for subscription; be sure to restart the system after modification for optimal performance",
"subscriptionPathPlaceholder": "Enter",
"subscriptionProtocol": "Subscription Protocol",
"wildcardResolution": "Wildcard Resolution",
"wildcardResolutionDescription": "Used for subscription"
},
"tabs": { "tabs": {
"currency": "Currency", "currency": "Currency",
"email": "Email",
"invite": "Invite", "invite": "Invite",
"node": "Node",
"register": "Register",
"site": "Site", "site": "Site",
"subscription": "Subscription",
"telegram": "Telegram", "telegram": "Telegram",
"tos": "Terms of Service", "tos": "Terms of Service",
"verify": "Verify" "verify": "Verify"
+12
View File
@@ -0,0 +1,12 @@
{
"clientId": "Bot ID",
"clientIdDescription": "Telegram Bot ID, you can get it from @BotFather",
"clientSecret": "Bot Token",
"clientSecretDescription": "Telegram Bot Token, you can get it from @BotFather",
"enable": "Enable",
"enableDescription": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"redirectUri": "Redirect URL",
"redirectUriDescription": "The URL that Telegram will redirect to after successful authentication",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}
+13 -1
View File
@@ -2,6 +2,7 @@
"actions": "Actions", "actions": "Actions",
"balance": "Balance", "balance": "Balance",
"cancel": "Cancel", "cancel": "Cancel",
"commission": "Commission",
"confirm": "Confirm", "confirm": "Confirm",
"confirmDelete": "Are you sure you want to delete?", "confirmDelete": "Are you sure you want to delete?",
"create": "Create", "create": "Create",
@@ -16,10 +17,16 @@
"email": "Email", "email": "Email",
"enable": "Enable", "enable": "Enable",
"form": { "form": {
"areaCode": "Area Code",
"areaCodePlaceholder": "Area code",
"balance": "Balance", "balance": "Balance",
"balancePlaceholder": "Balance", "balancePlaceholder": "Balance",
"cancel": "Cancel", "cancel": "Cancel",
"commission": "Commission",
"commissionPlaceholder": "Enter commission",
"confirm": "Confirm", "confirm": "Confirm",
"giftAmount": "Gift Amount",
"giftAmountPlaceholder": "Enter gift amount",
"invalidEmailFormat": "Invalid email format", "invalidEmailFormat": "Invalid email format",
"inviteCode": "Invite Code", "inviteCode": "Invite Code",
"inviteCodePlaceholder": "Enter invite code (leave blank to generate)", "inviteCodePlaceholder": "Enter invite code (leave blank to generate)",
@@ -28,11 +35,16 @@
"passwordPlaceholder": "Enter new password (optional)", "passwordPlaceholder": "Enter new password (optional)",
"refererId": "Referer ID", "refererId": "Referer ID",
"refererIdPlaceholder": "Enter referer ID", "refererIdPlaceholder": "Enter referer ID",
"telephone": "Phone Number",
"telephonePlaceholder": "Enter phone number",
"userEmail": "User Email", "userEmail": "User Email",
"userEmailPlaceholder": "Enter user email" "userEmailPlaceholder": "Enter user email"
}, },
"giftAmount": "Gift Amount",
"inviteCode": "Invite Code",
"referer": "Referer", "referer": "Referer",
"telephone": "Phone Number",
"updateSuccess": "Update successful", "updateSuccess": "Update successful",
"userList": "User List", "userList": "User List",
"userName": "User Email" "userName": "User Identifier"
} }

Some files were not shown because too many files have changed in this diff Show More