Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f15fb82cb | |||
| 320a7dc342 | |||
| 42ba9e88ad | |||
| 4a002333d4 | |||
| 6384237782 | |||
| 557c5cdc29 | |||
| f9d7736e48 | |||
| 5b64389544 | |||
| 23833b47d1 | |||
| c3eff0a0e7 | |||
| 6f7bc37233 | |||
| b2e8fadaf3 | |||
| 315c8f9afe | |||
| 0fb0d8bc11 | |||
| c356bc26ff | |||
| 23c974a94d | |||
| ee286dd629 | |||
| 4418c47f4f | |||
| 2d88a3a81d | |||
| 1f29506179 | |||
| c90175b6eb | |||
| 5cf573ab03 | |||
| fc55e9544d | |||
| 855d1b096f | |||
| 686aa2d0ba | |||
| a593eac119 | |||
| 7e1d551d8f | |||
| 1fc3a10c67 | |||
| 9bc3a940ba | |||
| 3652819ac9 | |||
| c761ec7538 | |||
| 5bac933fbc | |||
| 8a02310752 | |||
| 574b043be6 | |||
| e3f9ef6ebe | |||
| 4e7d249902 | |||
| 5db5343e4b | |||
| 1c0ecaecbd | |||
| 13c33378aa | |||
| b4946f7a06 | |||
| 012e83a25c | |||
| 1841552b1b | |||
| 4198871eef | |||
| e5455aa1dc | |||
| 046740f037 | |||
| fea2171d54 | |||
| c940f3c209 | |||
| 28813d2d3a | |||
| 2d1effb7ab | |||
| 3883646566 | |||
| e3a31eb709 | |||
| 47f2c5853c | |||
| 3a3d2236e2 | |||
| fdaf11b654 | |||
| 6733fc211c |
+188
-61
@@ -1,79 +1,206 @@
|
||||
<a name="readme-top"></a>
|
||||
# Changelog
|
||||
|
||||
# [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))
|
||||
# [1.0.0-beta.19](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2025-02-23)
|
||||
|
||||
|
||||
### ✨ 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))
|
||||
* **form**: Make version field optional and set default value; update site domain placeholder for clarity ([42ba9e8](https://github.com/perfect-panel/ppanel-web/commit/42ba9e8))
|
||||
|
||||
|
||||
### 🐛 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))
|
||||
* **auth**: Refactor forms to use Turnstile ref for reset functionality ([320a7dc](https://github.com/perfect-panel/ppanel-web/commit/320a7dc))
|
||||
|
||||
# [1.0.0-beta.18](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2025-02-22)
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* **platform**: Update platform naming and add keywords and custom HTML fields ([6384237](https://github.com/perfect-panel/ppanel-web/commit/6384237))
|
||||
* **site**: Added localization support for custom HTML and keyword fields ([f9d7736](https://github.com/perfect-panel/ppanel-web/commit/f9d7736))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **locales**: Update custom HTML description for clarity across multiple languages ([557c5cd](https://github.com/perfect-panel/ppanel-web/commit/557c5cd))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
|
||||
# [1.0.0-beta.17](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **auth**: Refactor reset password form to simplify code input and update placeholder text ([23833b4](https://github.com/perfect-panel/ppanel-web/commit/23833b4))
|
||||
|
||||
# [1.0.0-beta.16](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **auth**: Simplify email verification code input rendering ([6f7bc37](https://github.com/perfect-panel/ppanel-web/commit/6f7bc37))
|
||||
|
||||
# [1.0.0-beta.15](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **profile**: Restore filter to ensure only valid OAuth accounts are shown ([315c8f9](https://github.com/perfect-panel/ppanel-web/commit/315c8f9))
|
||||
|
||||
# [1.0.0-beta.14](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **locales**: Fixed description in multilingual files, updated text related to email registration functionality ([c356bc2](https://github.com/perfect-panel/ppanel-web/commit/c356bc2))
|
||||
|
||||
# [1.0.0-beta.13](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **api**: Fix type error in API request and add return URL parameter ([ee286dd](https://github.com/perfect-panel/ppanel-web/commit/ee286dd))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
|
||||
# [1.0.0-beta.12](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2025-02-18)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- More bugs ([2d88a3a](https://github.com/perfect-panel/ppanel-web/commit/2d88a3a))
|
||||
|
||||
# [1.0.0-beta.11](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2025-02-16)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **email**: Update platform configuration handling to use current ref for consistency ([c90175b](https://github.com/perfect-panel/ppanel-web/commit/c90175b))
|
||||
|
||||
# [1.0.0-beta.10](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2025-02-16)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **subscribe**: Update forms to include refetch functionality and improve toast messages ([fc55e95](https://github.com/perfect-panel/ppanel-web/commit/fc55e95))
|
||||
|
||||
# [1.0.0-beta.9](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2025-02-15)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **register**: Adjust user email verification logic to handle domain suffix checks correctly ([686aa2d](https://github.com/perfect-panel/ppanel-web/commit/686aa2d))
|
||||
|
||||
# [1.0.0-beta.8](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2025-02-15)
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **accounts**: Update third-party account binding and unbinding ([1841552](https://github.com/perfect-panel/ppanel-web/commit/1841552))
|
||||
- **auth-control**: Adding phone number labels to mobile verification configurations in multiple languages ([046740f](https://github.com/perfect-panel/ppanel-web/commit/046740f))
|
||||
- **auth-control**: Update general ([3883646](https://github.com/perfect-panel/ppanel-web/commit/3883646))
|
||||
- **auth**: Add type parameter to SendCode and update related API typings ([4198871](https://github.com/perfect-panel/ppanel-web/commit/4198871))
|
||||
- **auth**: Refactor mobile authentication config to support whitelist functionality ([c761ec7](https://github.com/perfect-panel/ppanel-web/commit/c761ec7))
|
||||
- **device**: Modify IMEI to device identifier support ([e3f9ef6](https://github.com/perfect-panel/ppanel-web/commit/e3f9ef6))
|
||||
- **imei**: Add IMEI related internationalization support and menu items ([13c3337](https://github.com/perfect-panel/ppanel-web/commit/13c3337))
|
||||
- **locales**: Add kick offline confirmation and success messages in multiple languages ([5db5343](https://github.com/perfect-panel/ppanel-web/commit/5db5343))
|
||||
- **locales**: Update 'sms' to 'mobile' in authentication methods across multiple languages ([fea2171](https://github.com/perfect-panel/ppanel-web/commit/fea2171))
|
||||
- **log**: Add message log retrieval functionality and update related typings ([1c0ecae](https://github.com/perfect-panel/ppanel-web/commit/1c0ecae))
|
||||
- **profile**: Update localization strings and enhance third-party account binding ([2d1effb](https://github.com/perfect-panel/ppanel-web/commit/2d1effb))
|
||||
- **release**: Extend supported platforms for Docker images, closes [#9](https://github.com/perfect-panel/ppanel-web/issues/9) ([e3a31eb](https://github.com/perfect-panel/ppanel-web/commit/e3a31eb))
|
||||
- **subscription**: Add delete user subscription functionality ([1fc3a10](https://github.com/perfect-panel/ppanel-web/commit/1fc3a10))
|
||||
- **ui**: Update input components and enhance card minimum width for better layout ([8a02310](https://github.com/perfect-panel/ppanel-web/commit/8a02310))
|
||||
- **user**: Add user Detail ([3a3d223](https://github.com/perfect-panel/ppanel-web/commit/3a3d223))
|
||||
- **user**: Add User Detail ([fdaf11b](https://github.com/perfect-panel/ppanel-web/commit/fdaf11b))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **auth-control**: Fix citation error for platform values ([c940f3c](https://github.com/perfect-panel/ppanel-web/commit/c940f3c))
|
||||
- **auth-control**: Fix citation error for platform values ([28813d2](https://github.com/perfect-panel/ppanel-web/commit/28813d2))
|
||||
- **auth-control**: Rename phone_variable to phone_number in mobile verification configuration ([e5455aa](https://github.com/perfect-panel/ppanel-web/commit/e5455aa))
|
||||
- **auth**: Update authentication configuration and localization strings ([47f2c58](https://github.com/perfect-panel/ppanel-web/commit/47f2c58))
|
||||
- **locales**: Update expiration time description from minutes to seconds in multiple languages ([5bac933](https://github.com/perfect-panel/ppanel-web/commit/5bac933))
|
||||
- **notify**: Ensure user info is updated after notification settings submission ([9bc3a94](https://github.com/perfect-panel/ppanel-web/commit/9bc3a94))
|
||||
- **notify**: Set default values for notification settings to false ([3652819](https://github.com/perfect-panel/ppanel-web/commit/3652819))
|
||||
- **third-party-accounts**: Remove mobile display logic from third-party accounts component ([b4946f7](https://github.com/perfect-panel/ppanel-web/commit/b4946f7))
|
||||
- **third-party-accounts**: Update redirect property name in binding response handling ([012e83a](https://github.com/perfect-panel/ppanel-web/commit/012e83a))
|
||||
- **user**: Refactor user form validation and reset password fields ([6733fc2](https://github.com/perfect-panel/ppanel-web/commit/6733fc2))
|
||||
- **user**: Update locales ([4e7d249](https://github.com/perfect-panel/ppanel-web/commit/4e7d249))
|
||||
- **user**: Update notification and verify code settings ([574b043](https://github.com/perfect-panel/ppanel-web/commit/574b043))
|
||||
|
||||
### 💄 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))
|
||||
- **dashboard**: Enhance card components with full height and improved empty state handling ([7e1d551](https://github.com/perfect-panel/ppanel-web/commit/7e1d551))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
|
||||
# [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))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ export default function RegisterForm({
|
||||
try {
|
||||
const domain = email.split('@')[1];
|
||||
const isValid =
|
||||
!auth.email.email_enable_verify ||
|
||||
auth.email?.email_domain_suffix_list.split('\n').includes(domain || '');
|
||||
!auth.email.enable_verify ||
|
||||
auth.email?.domain_suffix_list.split('\n').includes(domain || '');
|
||||
return !isValid;
|
||||
} catch (error) {
|
||||
console.log('Error checking user:', error);
|
||||
@@ -52,7 +52,7 @@ export default function RegisterForm({
|
||||
}),
|
||||
password: z.string(),
|
||||
repeat_password: z.string(),
|
||||
code: auth.email.email_enable_verify ? z.string() : z.string().nullish(),
|
||||
code: auth.email.enable_verify ? z.string() : z.string().nullish(),
|
||||
invite: invite.forced_invite ? z.string() : z.string().nullish(),
|
||||
cf_token:
|
||||
verify.enable_register_verify && verify.turnstile_site_key
|
||||
@@ -120,7 +120,7 @@ export default function RegisterForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{auth.email.email_enable_verify && (
|
||||
{auth.email.enable_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='code'
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function ResetForm({
|
||||
const formSchema = z.object({
|
||||
email: z.string().email(t('email')),
|
||||
password: z.string(),
|
||||
code: auth?.email?.email_enable_verify ? z.string() : z.string().nullish(),
|
||||
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
||||
cf_token:
|
||||
verify.enable_register_verify && verify.turnstile_site_key
|
||||
? z.string()
|
||||
@@ -61,45 +61,42 @@ export default function ResetForm({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter your password...' type='password' {...field} />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter your New password...' type='password' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{auth?.email?.email_enable_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{verify.enable_reset_password_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -37,10 +37,11 @@ export default function SendCode({ type, params }: SendCodeProps) {
|
||||
};
|
||||
|
||||
const getPhoneCode = async () => {
|
||||
if (params.telephone && params.telephone_area_code) {
|
||||
if (params.telephone && params.telephone_area_code && params.type) {
|
||||
await sendSmsCode({
|
||||
telephone: params.telephone,
|
||||
telephone_area_code: params.telephone_area_code,
|
||||
type: params.type,
|
||||
});
|
||||
setTargetDate(Date.now() + 60000);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -14,21 +14,21 @@ export default function Page() {
|
||||
const t = useTranslations('apple');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getOAuthByPlatform', 'apple'],
|
||||
queryKey: ['getAuthMethodConfig', 'apple'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getOAuthByPlatform({
|
||||
platform: 'apple',
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'apple',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateOAuthConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateOAuthConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -131,9 +131,14 @@ export default function Page() {
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder='https://your-domain.com/v1/auth/oauth/callback/apple'
|
||||
value={data?.redirect}
|
||||
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||
placeholder='https://your-domain.com'
|
||||
value={data?.config.redirect_url}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
redirect_url: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
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 { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('device');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getAuthMethodConfig', 'device'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'device',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
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>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
|
||||
import {
|
||||
getAuthMethodConfig,
|
||||
testEmailSend,
|
||||
updateAuthMethodConfig,
|
||||
} from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
@@ -20,17 +24,20 @@ import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { LogsTable } from '../log';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('email');
|
||||
const ref = useRef<Partial<API.EmailSmtpConfig>>({});
|
||||
const ref = useRef<Partial<API.AuthMethodConfig>>({});
|
||||
const [email, setEmail] = useState<string>();
|
||||
|
||||
const { data, refetch, isFetching } = useQuery({
|
||||
queryKey: ['getEmailSmtpConfig'],
|
||||
queryKey: ['getAuthMethodConfig', 'email'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getEmailSmtpConfig();
|
||||
ref.current = data.data as API.EmailSmtpConfig;
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'email',
|
||||
});
|
||||
ref.current = data.data as API.AuthMethodConfig;
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
@@ -38,10 +45,10 @@ export default function Page() {
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateEmailSmtpConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...ref.current,
|
||||
[key]: value,
|
||||
} as API.EmailSmtpConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -50,103 +57,202 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='basic'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='basic'>{t('emailBasicConfig')}</TabsTrigger>
|
||||
<TabsTrigger value='template'>{t('emailTemplate')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('emailLogs')}</TabsTrigger>
|
||||
<Tabs defaultValue='settings'>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
|
||||
<TabsTrigger value='template'>{t('template')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='basic'>
|
||||
<TabsContent value='settings'>
|
||||
<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('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('emailVerification')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.config?.enable_verify}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig('config', { ...data?.config, enable_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?.config?.enable_domain_suffix}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig('config', { ...data?.config, enable_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-32'
|
||||
placeholder={t('whitelistSuffixesPlaceholder')}
|
||||
defaultValue={data?.config?.domain_suffix_list}
|
||||
onBlur={(e) =>
|
||||
updateConfig('config', { ...data?.config, domain_suffix_list: e.target.value })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('smtpServerAddress')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('smtpServerAddressDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config?.platform_config?.host}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.platform_config,
|
||||
host: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('smtpServerPort')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('smtpServerPortDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config?.platform_config?.port}
|
||||
type='number'
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...ref.current?.config?.platform_config,
|
||||
port: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('smtpEncryptionMethod')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('smtpEncryptionMethodDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.config?.platform_config?.ssl}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...ref.current?.config?.platform_config,
|
||||
ssl: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('smtpAccount')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('smtpAccountDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config?.platform_config?.user}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...ref.current?.config?.platform_config,
|
||||
user: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('smtpPassword')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('smtpPasswordDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config?.platform_config?.pass}
|
||||
type='password'
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...ref.current?.config?.platform_config,
|
||||
pass: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('senderAddress')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('senderAddressDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.config?.platform_config?.from}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...ref.current?.config?.platform_config,
|
||||
from: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('sendTestEmail')}</Label>
|
||||
@@ -164,7 +270,7 @@ export default function Page() {
|
||||
onClick={async () => {
|
||||
if (!email) return;
|
||||
try {
|
||||
await testEmailSmtp({ email });
|
||||
await testEmailSend({ email });
|
||||
toast.success(t('sendSuccess'));
|
||||
} catch {
|
||||
toast.error(t('sendFailure'));
|
||||
@@ -193,8 +299,13 @@ export default function Page() {
|
||||
<CardContent>
|
||||
<HTMLEditor
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.[templateKey as keyof API.EmailSmtpConfig] as string}
|
||||
onBlur={(value) => updateConfig(templateKey, value)}
|
||||
value={data?.config?.[templateKey] as string}
|
||||
onBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
[templateKey]: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -203,7 +314,9 @@ export default function Page() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='logs'>Logs</TabsContent>
|
||||
<TabsContent value='logs'>
|
||||
<LogsTable type='email' />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -13,21 +13,21 @@ export default function Page() {
|
||||
const t = useTranslations('facebook');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getOAuthByPlatform', 'facebook'],
|
||||
queryKey: ['getAuthMethodConfig', 'facebook'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getOAuthByPlatform({
|
||||
platform: 'facebook',
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'facebook',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateOAuthConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateOAuthConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -86,19 +86,6 @@ export default function Page() {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, 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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function Invite() {
|
||||
const t = useTranslations('auth-control.invite');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getInviteConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getInviteConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateInviteConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.InviteConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('inviteSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enableForcedInvite')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('enableForcedInviteDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.forced_invite}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('forced_invite', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('inviteCommissionPercentage')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('inviteCommissionPercentageDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.referral_percentage}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
suffix='%'
|
||||
onValueBlur={(value) => updateConfig('referral_percentage', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('commissionFirstTimeOnly')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('commissionFirstTimeOnlyDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.only_first_purchase}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('only_first_purchase', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { getRegisterConfig, updateRegisterConfig } 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';
|
||||
import { Invite } from './invite';
|
||||
import { Register } from './register';
|
||||
import { Verify } from './verify';
|
||||
import { VerifyCode } from './verify-code';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('auth-control');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getRegisterConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getRegisterConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateRegisterConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.RegisterConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stopNewUserRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('stopNewUserRegistrationDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.stop_register}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('stop_register', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('ipRegistrationLimit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('ipRegistrationLimitDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_ip_register_limit}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_ip_register_limit', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('registrationLimitCount')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('registrationLimitCountDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('penaltyTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit_duration}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('trialRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_trial}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_trial', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className='space-y-3'>
|
||||
<Invite />
|
||||
<Register />
|
||||
<VerifyCode />
|
||||
<Verify />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client';
|
||||
|
||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
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 { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function Register() {
|
||||
const t = useTranslations('auth-control.register');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getRegisterConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getRegisterConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
await updateRegisterConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.RegisterConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
}
|
||||
|
||||
const { data: subscribe } = useQuery({
|
||||
queryKey: ['getSubscribeList', 'all'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeList({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
});
|
||||
return data.data?.list as API.Subscribe[];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('registerSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stopNewUserRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('stopNewUserRegistrationDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.stop_register}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('stop_register', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('ipRegistrationLimit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('ipRegistrationLimitDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_ip_register_limit}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_ip_register_limit', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('registrationLimitCount')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('registrationLimitCountDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('penaltyTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit_duration}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('trialRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_trial}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_trial', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('trialSubscribePlan')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('trialSubscribePlanDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('trialDuration')}
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.trial_time}
|
||||
onValueBlur={(value) => updateConfig('trial_time', value)}
|
||||
prefix={
|
||||
<Select
|
||||
value={String(data?.trial_subscribe)}
|
||||
onValueChange={(value) => updateConfig('trial_subscribe', Number(value))}
|
||||
>
|
||||
<SelectTrigger className='bg-secondary rounded-r-none'>
|
||||
{data?.trial_subscribe ? (
|
||||
<SelectValue placeholder='Select Subscribe' />
|
||||
) : (
|
||||
'Select Subscribe'
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subscribe?.map((item) => (
|
||||
<SelectItem key={item.id} value={String(item.id)}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
suffix={
|
||||
<Combobox
|
||||
className='bg-secondary rounded-l-none'
|
||||
value={data?.trial_time_unit}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
updateConfig('trial_time_unit', value);
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ label: t('noLimit'), value: 'NoLimit' },
|
||||
{ label: t('year'), value: 'Year' },
|
||||
{ label: t('month'), value: 'Month' },
|
||||
{ label: t('day'), value: 'Day' },
|
||||
{ label: t('hour'), value: 'Hour' },
|
||||
{ label: t('minute'), value: 'Minute' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { getVerifyCodeConfig, updateVerifyCodeConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
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 function VerifyCode() {
|
||||
const t = useTranslations('auth-control.verify-code');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getVerifyCodeConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getVerifyCodeConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateVerifyCodeConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.VerifyCodeConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='mb-6'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('verifyCodeSettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('expireTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('expireTimeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
placeholder='300'
|
||||
value={data?.verify_code_expire_time}
|
||||
onValueBlur={(value) => updateConfig('verify_code_expire_time', Number(value))}
|
||||
suffix={t('second')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('interval')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('intervalDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
placeholder='60'
|
||||
value={data?.verify_code_interval}
|
||||
onValueBlur={(value) => updateConfig('verify_code_interval', Number(value))}
|
||||
suffix={t('second')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('dailyLimit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('dailyLimitDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
placeholder='15'
|
||||
value={data?.verify_code_limit}
|
||||
onValueBlur={(value) => updateConfig('verify_code_limit', Number(value))}
|
||||
suffix={t('times')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, 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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function Verify() {
|
||||
const t = useTranslations('auth-control.verify');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getVerifyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getVerifyConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateVerifyConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.VerifyConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='mb-6'>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('verifySettings')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>Turnstile Site Key</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('turnstileSiteKeyDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.turnstile_site_key}
|
||||
onValueBlur={(value) => updateConfig('turnstile_site_key', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>Turnstile Site Secret</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('turnstileSecretDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.turnstile_secret}
|
||||
onValueBlur={(value) => updateConfig('turnstile_secret', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('registrationVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('registrationVerificationCodeDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_register_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_register_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('loginVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('loginVerificationCodeDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_login_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_login_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('resetPasswordVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('resetPasswordVerificationCodeDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_reset_password_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_reset_password_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -13,21 +13,21 @@ export default function Page() {
|
||||
const t = useTranslations('github');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getOAuthByPlatform', 'github'],
|
||||
queryKey: ['getAuthMethodConfig', 'github'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getOAuthByPlatform({
|
||||
platform: 'github',
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'github',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateOAuthConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateOAuthConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -85,19 +85,6 @@ export default function Page() {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -13,21 +13,21 @@ export default function Page() {
|
||||
const t = useTranslations('google');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getOAuthByPlatform', 'google'],
|
||||
queryKey: ['getAuthMethodConfig', 'google'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getOAuthByPlatform({
|
||||
platform: 'google',
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'google',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateOAuthConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateOAuthConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -86,19 +86,6 @@ export default function Page() {
|
||||
/>
|
||||
</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,108 @@
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { getMessageLogList } from '@/services/admin/log';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function LogsTable({ type }: { type: 'email' | 'mobile' }) {
|
||||
const t = useTranslations('auth-control.log');
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<
|
||||
API.MessageLog,
|
||||
{
|
||||
platform?: string;
|
||||
to?: string;
|
||||
subject?: string;
|
||||
content?: string;
|
||||
status?: number;
|
||||
}
|
||||
>
|
||||
action={ref}
|
||||
header={{
|
||||
title: t(`${type}Log`),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'platform',
|
||||
header: t('platform'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'to',
|
||||
header: t('to'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'subject',
|
||||
header: t('subject'),
|
||||
},
|
||||
{
|
||||
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')),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: t('updatedAt'),
|
||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||
},
|
||||
]}
|
||||
params={[
|
||||
// {
|
||||
// key: 'platform',
|
||||
// placeholder: t('platform'),
|
||||
// },
|
||||
{
|
||||
key: 'to',
|
||||
placeholder: t('to'),
|
||||
},
|
||||
{
|
||||
key: 'subject',
|
||||
placeholder: t('subject'),
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
placeholder: t('content'),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
placeholder: t('status'),
|
||||
options: [
|
||||
{ label: t('sendSuccess'), value: '1' },
|
||||
{ label: t('sendFailed'), value: '0' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getMessageLogList({
|
||||
...pagination,
|
||||
...filter,
|
||||
status: filter.status === undefined ? undefined : Number(filter.status),
|
||||
type: type,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { getSmsList } from '@/services/admin/sms';
|
||||
import {
|
||||
getSmsConfig,
|
||||
getAuthMethodConfig,
|
||||
getSmsPlatform,
|
||||
testSmsSend,
|
||||
updateSmsConfig,
|
||||
} from '@/services/admin/system';
|
||||
updateAuthMethodConfig,
|
||||
} from '@/services/admin/authMethod';
|
||||
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 {
|
||||
@@ -25,18 +22,21 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
|
||||
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 TagInput from '@workspace/ui/custom-components/tag-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { LogsTable } from '../log';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('phone');
|
||||
const { data, refetch, isFetching } = useQuery({
|
||||
queryKey: ['getSmsConfig'],
|
||||
queryKey: ['getAuthMethodConfig', 'mobile'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSmsConfig();
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'mobile',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
@@ -49,25 +49,27 @@ export default function Page() {
|
||||
},
|
||||
});
|
||||
|
||||
const selectedPlatform = platforms?.find((platform) => platform.platform === data?.sms_platform);
|
||||
const selectedPlatform = platforms?.find(
|
||||
(platform) => platform.platform === data?.config?.platform,
|
||||
);
|
||||
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
|
||||
try {
|
||||
await updateSmsConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.SmsConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
const [params, setParams] = useState<API.SendSmsRequest>({
|
||||
const [params, setParams] = useState<API.TestSmsSendRequest>({
|
||||
telephone: '',
|
||||
content: t('testSmsContent'),
|
||||
area_code: '1',
|
||||
});
|
||||
|
||||
@@ -88,59 +90,38 @@ export default function Page() {
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.sms_enabled}
|
||||
onCheckedChange={(checked) => updateConfig('sms_enabled', checked)}
|
||||
checked={data?.enabled}
|
||||
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('expireTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('expireTimeTip')}</p>
|
||||
<Label>{t('whitelistValidation')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('whitelistValidationTip')}</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')}
|
||||
<Switch
|
||||
defaultValue={data?.config?.enable_whitelist}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfig('config', { ...data?.config, enable_whitelist: checked })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('limit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('limitTip')}</p>
|
||||
<Label>{t('whitelistAreaCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('whitelistAreaCodeTip')}</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 className='w-1/2 text-right'>
|
||||
<TagInput
|
||||
placeholder='1, 852, 886, 888'
|
||||
value={data?.config?.whitelist || []}
|
||||
onChange={(value) =>
|
||||
updateConfig('config', { ...data?.config, whitelist: value })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -151,8 +132,13 @@ export default function Page() {
|
||||
</TableCell>
|
||||
<TableCell className='flex items-center gap-1 text-right'>
|
||||
<Select
|
||||
value={data?.sms_platform}
|
||||
onValueChange={(value) => updateConfig('sms_platform', value)}
|
||||
value={data?.config?.platform}
|
||||
onValueChange={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform: value,
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -177,91 +163,189 @@ export default function Page() {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>Key</Label>
|
||||
<Label>{t('accessLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sms_key })}
|
||||
{t('platformConfigTip', { key: platformConfig?.access })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_key ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_key', value)}
|
||||
value={data?.config?.platform_config.access ?? ''}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
access: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.sms_key })}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.access })}
|
||||
/>
|
||||
</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 && (
|
||||
{platformConfig?.endpoint && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateCode')}</Label>
|
||||
<Label>{t('endpointLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||
{t('platformConfigTip', { key: platformConfig?.endpoint })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_code ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_template_code', value)}
|
||||
value={data?.config?.platform_config.endpoint ?? ''}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
endpoint: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.endpoint })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{platformConfig?.sms_template_param && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('secretLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.secret })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.config?.platform_config?.secret ?? ''}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
secret: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
type='password'
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.secret })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{platformConfig?.template_code && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateParam')}</Label>
|
||||
<Label>{t('templateCodeLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sms_template_param })}
|
||||
{t('platformConfigTip', { key: platformConfig?.template_code })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_param ?? 'code'}
|
||||
onValueBlur={(value) => updateConfig('sms_template_param', value)}
|
||||
value={data?.config?.platform_config?.template_code ?? 'code'}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
template_code: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', { key: platformConfig?.template_code })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{platformConfig?.sign_name && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('signNameLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.sign_name })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.config?.platform_config?.sign_name ?? ''}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
sign_name: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', {
|
||||
key: platformConfig?.sms_template_param,
|
||||
key: platformConfig?.sign_name,
|
||||
})}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{platformConfig?.phone_number && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('phoneNumberLabel')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('platformConfigTip', { key: platformConfig?.phone_number })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.config?.platform_config?.phone_number ?? ''}
|
||||
onValueBlur={(value) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
phone_number: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('platformConfigTip', {
|
||||
key: platformConfig?.phone_number,
|
||||
})}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{platformConfig?.code_variable && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('template')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('templateTip', { code: platformConfig?.code_variable })}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
defaultValue={data?.config?.platform_config?.template ?? ''}
|
||||
onBlur={(e) =>
|
||||
updateConfig('config', {
|
||||
...data?.config,
|
||||
platform_config: {
|
||||
...data?.config?.platform_config,
|
||||
template: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.template', {
|
||||
code: platformConfig?.code_variable,
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
@@ -304,67 +388,8 @@ export default function Page() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='logs'>
|
||||
<LogsTable />
|
||||
<LogsTable type='mobile' />
|
||||
</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,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -13,21 +13,21 @@ export default function Page() {
|
||||
const t = useTranslations('telegram');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getOAuthByPlatform', 'telegram'],
|
||||
queryKey: ['getAuthMethodConfig', 'telegram'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getOAuthByPlatform({
|
||||
platform: 'telegram',
|
||||
const { data } = await getAuthMethodConfig({
|
||||
method: 'telegram',
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
|
||||
try {
|
||||
await updateOAuthConfig({
|
||||
await updateAuthMethodConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.UpdateOAuthConfig);
|
||||
} as API.UpdateAuthMethodConfigRequest);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
@@ -86,19 +86,6 @@ export default function Page() {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { cn } from '@workspace/ui/lib/utils';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { UserDetail } from '../user/user-detail';
|
||||
|
||||
export default function Page() {
|
||||
export default function Page(props: any) {
|
||||
const t = useTranslations('order');
|
||||
|
||||
const statusOptions = [
|
||||
@@ -115,7 +115,7 @@ export default function Page() {
|
||||
<Separator className='my-4' />
|
||||
<ul className='grid gap-3'>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>支付方式</span>
|
||||
<span className='text-muted-foreground'>{t('method')}</span>
|
||||
<span>{t(`methods.${row.original.method}`)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -177,10 +177,19 @@ export default function Page() {
|
||||
value: String(item.id),
|
||||
})),
|
||||
},
|
||||
{ key: 'user_id', placeholder: `${t('user')}ID` },
|
||||
]}
|
||||
].concat(
|
||||
props.userId
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: 'user_id',
|
||||
placeholder: `${t('user')} ID`,
|
||||
options: undefined,
|
||||
},
|
||||
],
|
||||
)}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getOrderList({ ...pagination, ...filter });
|
||||
const { data } = await getOrderList({ ...pagination, ...filter, user_id: props.userId });
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
|
||||
@@ -35,7 +35,7 @@ function getTimeRangeData(slots: API.TimePeriod[]) {
|
||||
.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 [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;
|
||||
@@ -117,7 +117,7 @@ export default function NodeConfig() {
|
||||
}, [timeSlots]);
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
return chartTimeSlots.reduce(
|
||||
return chartTimeSlots?.reduce(
|
||||
(acc, item, index) => {
|
||||
acc[item.name] = {
|
||||
label: item.name,
|
||||
|
||||
@@ -50,7 +50,7 @@ const defaultValues = {
|
||||
|
||||
const versionSchema = z.object({
|
||||
url: z.string(),
|
||||
version: z.string(),
|
||||
version: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
});
|
||||
@@ -221,6 +221,7 @@ export default function SubscribeAppForm<
|
||||
name: 'version',
|
||||
type: 'text',
|
||||
placeholder: t('version'),
|
||||
defaultValue: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
@@ -251,7 +252,10 @@ export default function SubscribeAppForm<
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
form.setValue(
|
||||
field.name,
|
||||
value.filter((item) => item.url),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function SubscribeConfig() {
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.SubscribeConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
server_group: z.array(z.number()).optional().default([]),
|
||||
server: z.array(z.number()).optional().default([]),
|
||||
deduction_ratio: z.number().optional().default(0),
|
||||
purchase_with_discount: z.boolean().optional().default(false),
|
||||
allow_deduction: z.boolean().optional().default(false),
|
||||
reset_cycle: z.number().optional().default(0),
|
||||
renewal_reset: z.boolean().optional().default(false),
|
||||
});
|
||||
@@ -639,7 +639,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='purchase_with_discount'
|
||||
name='allow_deduction'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className='flex items-center justify-between'>
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getInviteConfig, updateInviteConfig } 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 Invite() {
|
||||
const t = useTranslations('system.invite');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getInviteConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getInviteConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateInviteConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.InviteConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enableForcedInvite')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableForcedInviteDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.forced_invite}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('forced_invite', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('inviteCommissionPercentage')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('inviteCommissionPercentageDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.referral_percentage}
|
||||
type='number'
|
||||
min={0}
|
||||
max={100}
|
||||
suffix='%'
|
||||
onValueBlur={(value) => updateConfig('referral_percentage', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('commissionFirstTimeOnly')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('commissionFirstTimeOnlyDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.only_first_purchase}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('only_first_purchase', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import Currency from './currency';
|
||||
import Invite from './invite';
|
||||
import Site from './site';
|
||||
import Telegram from './telegram';
|
||||
import Tos from './tos';
|
||||
import Verify from './verify';
|
||||
|
||||
export default async function Page() {
|
||||
const t = await getTranslations('system');
|
||||
@@ -16,9 +12,6 @@ export default async function Page() {
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
|
||||
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
|
||||
<TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger>
|
||||
<TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger>
|
||||
<TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger>
|
||||
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='site'>
|
||||
@@ -27,15 +20,6 @@ export default async function Page() {
|
||||
<TabsContent value='currency'>
|
||||
<Currency />
|
||||
</TabsContent>
|
||||
<TabsContent value='verify'>
|
||||
<Verify />
|
||||
</TabsContent>
|
||||
<TabsContent value='invite'>
|
||||
<Invite />
|
||||
</TabsContent>
|
||||
<TabsContent value='telegram'>
|
||||
<Telegram />
|
||||
</TabsContent>
|
||||
<TabsContent value='tos'>
|
||||
<Tos />
|
||||
</TabsContent>
|
||||
|
||||
@@ -85,6 +85,35 @@ export default function Site() {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('keywords')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('keywordsDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('keywordsDescription')}
|
||||
value={data?.keywords}
|
||||
onValueBlur={(value) => updateConfig('keywords', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('customHtml')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('customHtmlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('customHtmlDescription')}
|
||||
defaultValue={data?.custom_html}
|
||||
onBlur={(e) => {
|
||||
updateConfig('custom_html', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('siteDomain')}</Label>
|
||||
@@ -93,7 +122,7 @@ export default function Site() {
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('siteDomainPlaceholder')}
|
||||
placeholder={`${t('siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
|
||||
defaultValue={data?.host}
|
||||
onBlur={(e) => {
|
||||
updateConfig('host', e.target.value);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getTelegramConfig, updateTelegramConfig } 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 Telegram() {
|
||||
const t = useTranslations('system.telegram');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getTelegramConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getTelegramConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateTelegramConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.TelegramConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('webhookDomain')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('webhookDomainDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholderWebhookDomain')}
|
||||
value={data?.telegram_web_hook_domain}
|
||||
onValueBlur={(value) => updateConfig('telegram_web_hook_domain', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('botToken')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('botTokenDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholderBotToken')}
|
||||
value={data?.telegram_bot_token}
|
||||
onValueBlur={(value) => updateConfig('telegram_bot_token', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enableBotNotifications')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('enableBotNotificationsDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.telegram_notify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('telegram_notify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('groupURL')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('groupURLDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholderGroupURL')}
|
||||
value={data?.telegram_group_url}
|
||||
onValueBlur={(value) => updateConfig('telegram_group_url', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getVerifyConfig, updateVerifyConfig } 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 Verify() {
|
||||
const t = useTranslations('system.verify');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getVerifyConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getVerifyConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateVerifyConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.VerifyConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('turnstileSiteKey')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('turnstileSiteKeyDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
defaultValue={data?.turnstile_site_key}
|
||||
onValueBlur={(value) => updateConfig('turnstile_site_key', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('turnstileSecret')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('turnstileSecretDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
defaultValue={data?.turnstile_secret}
|
||||
onValueBlur={(value) => updateConfig('turnstile_secret', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('registrationVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('registrationVerificationCodeDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_register_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_register_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('loginVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('loginVerificationCodeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_login_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_login_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('resetPasswordVerificationCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('resetPasswordVerificationCodeDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_reset_password_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_reset_password_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import UserOrderList from '@/app/dashboard/order/page';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import UserLoginHistory from './user-login-history';
|
||||
import { UserProfileForm } from './user-profile';
|
||||
import UserSubscription from './user-subscription';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: number }> }) {
|
||||
const t = await getTranslations('user');
|
||||
const { id } = await params;
|
||||
return (
|
||||
<Tabs defaultValue='profile'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='profile'>{t('userProfile')}</TabsTrigger>
|
||||
<TabsTrigger value='subscriptions'>{t('userSubscriptions')}</TabsTrigger>
|
||||
<TabsTrigger value='orders'>{t('userOrders')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('userLogs')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='profile'>
|
||||
<UserProfileForm />
|
||||
</TabsContent>
|
||||
<TabsContent value='subscriptions'>
|
||||
<UserSubscription userId={id} />
|
||||
</TabsContent>
|
||||
<TabsContent value='orders'>
|
||||
<UserOrderList userId={id} />
|
||||
</TabsContent>
|
||||
<TabsContent value='logs'>
|
||||
<UserLoginHistory />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import { getUserLoginLogs } from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export default function UserLoginHistory() {
|
||||
const t = useTranslations('user');
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<ProTable<API.UserLoginLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'success',
|
||||
header: t('loginStatus'),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('success') ? 'default' : 'destructive'}>
|
||||
{row.getValue('success') ? t('success') : t('failed')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'login_ip',
|
||||
header: t('loginIp'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('loginTime'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getUserLoginLogs({
|
||||
user_id: Number(id),
|
||||
...pagination,
|
||||
...filter,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createUserAuthMethod,
|
||||
deleteUserAuthMethod,
|
||||
updateUserAuthMethod,
|
||||
} from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function AuthMethodsForm({ user, refetch }: { user: API.User; refetch: () => void }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const [emailChanges, setEmailChanges] = useState<Record<string, string>>({});
|
||||
|
||||
const handleRemoveAuth = async (authType: string) => {
|
||||
await deleteUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: authType,
|
||||
});
|
||||
toast.success(t('deleteSuccess'));
|
||||
};
|
||||
|
||||
const handleUpdateEmail = async (email: string) => {
|
||||
await updateUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: 'email',
|
||||
auth_identifier: email,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCreateEmail = async (email: string) => {
|
||||
await createUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: 'email',
|
||||
auth_identifier: email,
|
||||
});
|
||||
toast.success(t('createSuccess'));
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleEmailChange = (authType: string, value: string) => {
|
||||
setEmailChanges((prev) => ({
|
||||
...prev,
|
||||
[authType]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const emailMethod = user.auth_methods.find((method) => method.auth_type === 'email');
|
||||
const otherMethods = user.auth_methods.filter((method) => method.auth_type !== 'email');
|
||||
|
||||
const defaultEmailMethod = {
|
||||
auth_type: 'email',
|
||||
auth_identifier: '',
|
||||
verified: false,
|
||||
...emailMethod,
|
||||
};
|
||||
|
||||
const isEmailExists = !!emailMethod;
|
||||
const handleEmailAction = () => {
|
||||
const email = emailChanges['email'];
|
||||
if (isEmailExists) {
|
||||
handleUpdateEmail(email as string);
|
||||
} else {
|
||||
handleCreateEmail(email as string);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('authMethodsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-6'>
|
||||
<Card className='border-none shadow-none'>
|
||||
<CardContent className='space-y-3 p-0'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='font-medium uppercase'>email</div>
|
||||
<Badge variant={defaultEmailMethod.verified ? 'default' : 'destructive'}>
|
||||
{defaultEmailMethod.verified ? t('verified') : t('unverified')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex-1'>
|
||||
<EnhancedInput
|
||||
value={defaultEmailMethod.auth_identifier}
|
||||
placeholder={t('pleaseEnterEmail')}
|
||||
onValueChange={(value) => handleEmailChange('email', value as string)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleEmailAction}
|
||||
disabled={
|
||||
!emailChanges['email'] ||
|
||||
(isEmailExists && emailChanges['email'] === defaultEmailMethod.auth_identifier)
|
||||
}
|
||||
>
|
||||
{isEmailExists ? t('update') : t('add')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{otherMethods.map((method) => (
|
||||
<Card key={method.auth_type} className='border-none shadow-none'>
|
||||
<CardContent className='space-y-3 p-0'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='font-medium uppercase'>{method.auth_type}</div>
|
||||
<Badge variant={method.verified ? 'default' : 'destructive'}>
|
||||
{method.verified ? t('verified') : t('unverified')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='flex-1'>
|
||||
<div className='text-muted-foreground text-sm'>{method.auth_identifier}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => handleRemoveAuth(method.auth_type)}
|
||||
>
|
||||
{t('remove')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { updateUserBasicInfo } from '@/services/admin/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
|
||||
const basicInfoSchema = z.object({
|
||||
avatar: z.string().optional(),
|
||||
balance: z.number().optional(),
|
||||
commission: z.number().optional(),
|
||||
gift_amount: z.number().optional(),
|
||||
refer_code: z.string().optional(),
|
||||
referer_id: z.number().optional(),
|
||||
is_admin: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
enable: z.boolean(),
|
||||
});
|
||||
|
||||
type BasicInfoValues = z.infer<typeof basicInfoSchema>;
|
||||
|
||||
export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () => void }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const { common } = useGlobalStore();
|
||||
const { currency } = common;
|
||||
|
||||
const form = useForm<BasicInfoValues>({
|
||||
resolver: zodResolver(basicInfoSchema),
|
||||
defaultValues: {
|
||||
avatar: user.avatar,
|
||||
balance: user.balance,
|
||||
commission: user.commission,
|
||||
gift_amount: user.gift_amount,
|
||||
refer_code: user.refer_code,
|
||||
referer_id: user.referer_id,
|
||||
is_admin: user.is_admin,
|
||||
enable: user.enable,
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: BasicInfoValues) {
|
||||
await updateUserBasicInfo({
|
||||
user_id: user.id,
|
||||
telegram: user.telegram,
|
||||
...data,
|
||||
} as API.UpdateUserBasiceInfoRequest);
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>{t('basicInfoTitle')}</CardTitle>
|
||||
<Button type='submit' size='sm'>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('accountEnable')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='is_admin'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('administrator')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='balance'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('balance')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={field.value}
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='commission'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('commission')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={field.value}
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='gift_amount'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('giftAmount')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={field.value}
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='refer_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('referralCode')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as string);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='referer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('referrerUserId')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='avatar'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('avatar')}</FormLabel>
|
||||
<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'
|
||||
returnType='base64'
|
||||
onChange={(value) => form.setValue('avatar', value as string)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder={t('passwordPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { getUserDetail } from '@/services/admin/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AuthMethodsForm } from './auth-methods-form';
|
||||
import { BasicInfoForm } from './basic-info-form';
|
||||
import { NotifySettingsForm } from './notify-settings-form';
|
||||
|
||||
export function UserProfileForm() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { data: user, refetch } = useQuery({
|
||||
queryKey: ['user', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await getUserDetail({
|
||||
id: Number(id),
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||
<div className='md:col-span-2 xl:col-span-1'>
|
||||
<BasicInfoForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
<div>
|
||||
<NotifySettingsForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
<div>
|
||||
<AuthMethodsForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { updateUserNotifySetting } from '@/services/admin/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '@workspace/ui/components/form';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
|
||||
const notifySettingsSchema = z.object({
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
|
||||
|
||||
export function NotifySettingsForm({ user, refetch }: { user: API.User; refetch: () => void }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const form = useForm<NotifySettingsValues>({
|
||||
resolver: zodResolver(notifySettingsSchema),
|
||||
defaultValues: {
|
||||
enable_balance_notify: user.enable_balance_notify,
|
||||
enable_login_notify: user.enable_login_notify,
|
||||
enable_subscribe_notify: user.enable_subscribe_notify,
|
||||
enable_trade_notify: user.enable_trade_notify,
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: NotifySettingsValues) {
|
||||
await updateUserNotifySetting({
|
||||
...data,
|
||||
user_id: user.id,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>{t('notifySettingsTitle')}</CardTitle>
|
||||
<Button type='submit' size='sm'>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='grid grid-cols-1 gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_balance_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('balanceNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_login_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('loginNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_subscribe_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('subscriptionNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enable_trade_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>{t('tradeNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import {
|
||||
createUserSubscribe,
|
||||
deleteUserSubscribe,
|
||||
getUserSubscribe,
|
||||
updateUserSubscribe,
|
||||
} from '@/services/admin/user';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { SubscriptionDetail } from './subscription-detail';
|
||||
import { SubscriptionForm } from './subscription-form';
|
||||
|
||||
export default function UserSubscription({ userId }: { userId: number }) {
|
||||
const t = useTranslations('user');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
||||
action={ref}
|
||||
header={{
|
||||
title: t('subscriptionList'),
|
||||
toolbar: (
|
||||
<SubscriptionForm
|
||||
key='create'
|
||||
trigger={t('add')}
|
||||
title={t('createSubscription')}
|
||||
loading={loading}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
await createUserSubscribe({
|
||||
user_id: Number(userId),
|
||||
...values,
|
||||
});
|
||||
toast.success(t('createSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('subscriptionName'),
|
||||
cell: ({ row }) => row.original.subscribe.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'upload',
|
||||
header: t('upload'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'download',
|
||||
header: t('download'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'traffic',
|
||||
header: t('totalTraffic'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} unlimited />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'speed_limit',
|
||||
header: t('speedLimit'),
|
||||
cell: ({ row }) => {
|
||||
const speed = row.original?.subscribe?.speed_limit;
|
||||
return <Display type='trafficSpeed' value={speed} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'device_limit',
|
||||
header: t('deviceLimit'),
|
||||
cell: ({ row }) => {
|
||||
const limit = row.original?.subscribe?.device_limit;
|
||||
return <Display type='number' value={limit} unlimited />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'reset_time',
|
||||
header: t('resetTime'),
|
||||
cell: ({ row }) => {
|
||||
return <Display type='number' value={row.getValue('reset_time')} unlimited />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'expire_time',
|
||||
header: t('expireTime'),
|
||||
cell: ({ row }) =>
|
||||
row.getValue('expire_time') ? formatDate(row.getValue('expire_time')) : t('permanent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('createdAt'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribe({
|
||||
user_id: userId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => {
|
||||
return [
|
||||
<SubscriptionForm
|
||||
key='edit'
|
||||
trigger={t('edit')}
|
||||
title={t('editSubscription')}
|
||||
loading={loading}
|
||||
initialData={row}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
await updateUserSubscribe({
|
||||
user_id: Number(userId),
|
||||
user_subscribe_id: row.id,
|
||||
...values,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
return true;
|
||||
}}
|
||||
/>,
|
||||
<SubscriptionDetail
|
||||
key='detail'
|
||||
trigger={<Button variant='secondary'>{t('detail')}</Button>}
|
||||
userId={userId}
|
||||
subscriptionId={row.id}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
title={t('confirmDelete')}
|
||||
description={t('deleteSubscriptionDescription')}
|
||||
onConfirm={async () => {
|
||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||
toast.success(t('deleteSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import {
|
||||
getUserSubscribeDevices,
|
||||
getUserSubscribeLogs,
|
||||
getUserSubscribeTrafficLogs,
|
||||
kickOfflineByUserDevice,
|
||||
} from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@workspace/ui/components/dialog';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SubscriptionDetail({
|
||||
trigger,
|
||||
userId,
|
||||
subscriptionId,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
userId: number;
|
||||
subscriptionId: number;
|
||||
}) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className='max-w-5xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('subscriptionDetails')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='mt-4'>
|
||||
<Tabs defaultValue='logs'>
|
||||
<TabsList className='w-full'>
|
||||
<TabsTrigger value='logs' className='flex-1'>
|
||||
{t('subscriptionLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='traffic' className='flex-1'>
|
||||
{t('trafficLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='devices' className='flex-1'>
|
||||
{t('onlineDevices')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='logs'>
|
||||
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'token',
|
||||
header: t('token'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('time'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeLogs({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='traffic'>
|
||||
<ProTable<API.TrafficLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'download',
|
||||
header: t('download'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'upload',
|
||||
header: t('upload'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'timestamp',
|
||||
header: t('time'),
|
||||
cell: ({ row }) => formatDate(row.getValue('timestamp')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeTrafficLogs({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='devices'>
|
||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: t('enable'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
checked={row.getValue('enabled')}
|
||||
onChange={(checked) => {
|
||||
console.log('Switch:', checked);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'identifier',
|
||||
header: 'IMEI',
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
},
|
||||
{
|
||||
accessorKey: 'online',
|
||||
header: t('loginStatus'),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
|
||||
{row.getValue('online') ? t('online') : t('offline')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: t('lastSeen'),
|
||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeDevices({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => {
|
||||
if (!row.identifier) return [];
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='offline'
|
||||
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
||||
title={t('confirmOffline')}
|
||||
description={t('kickOfflineConfirm', { ip: row.ip })}
|
||||
onConfirm={async () => {
|
||||
await kickOfflineByUserDevice({ id: row.id });
|
||||
toast.success(t('kickOfflineSuccess'));
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
'use client';
|
||||
|
||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||
import { DatePicker } from '@workspace/ui/custom-components/date-picker';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface Props {
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
loading?: boolean;
|
||||
initialData?: API.UserSubscribe;
|
||||
onSubmit: (values: any) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
subscribe_id: z.number().optional(),
|
||||
traffic: z.number().optional(),
|
||||
speed_limit: z.number().optional(),
|
||||
device_limit: z.number().optional(),
|
||||
expired_at: z.number().nullish().optional(),
|
||||
});
|
||||
|
||||
export function SubscriptionForm({ trigger, title, loading, initialData, onSubmit }: Props) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
subscribe_id: initialData?.subscribe_id || 0,
|
||||
traffic: initialData?.traffic || 0,
|
||||
upload: initialData?.upload || 0,
|
||||
download: initialData?.download || 0,
|
||||
expired_at: initialData?.expire_time || 0,
|
||||
...(initialData && { id: initialData.id }),
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const success = await onSubmit(values);
|
||||
if (success) {
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const { data: subscribe } = useQuery({
|
||||
queryKey: ['getSubscribeList', 'all'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeList({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
});
|
||||
return data.data?.list as API.Subscribe[];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className='mt-4 space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='subscribe_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('subscription')}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox<number, false>
|
||||
placeholder='Select Subscription'
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
options={subscribe?.map((item: API.Subscribe) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='traffic'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('trafficLimit')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('unlimited')}
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='upload'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('uploadTraffic')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder='0'
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='download'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('downloadTraffic')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder='0'
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='expired_at'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('expiredAt')}</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
placeholder={t('permanent')}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
if (value === field.value) {
|
||||
form.setValue(field.name, 0);
|
||||
} else {
|
||||
form.setValue(field.name, value!);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
|
||||
import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
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';
|
||||
import { UserDetail } from './user-detail';
|
||||
@@ -56,10 +58,10 @@ export default function Page() {
|
||||
<Switch
|
||||
defaultChecked={row.getValue('enable')}
|
||||
onCheckedChange={async (checked) => {
|
||||
await updateUser({
|
||||
await updateUserBasicInfo({
|
||||
...row.original,
|
||||
enable: checked,
|
||||
} as unknown as API.UpdateUserRequest);
|
||||
} as unknown as API.UpdateUserBasiceInfoRequest);
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
@@ -74,7 +76,17 @@ export default function Page() {
|
||||
{
|
||||
accessorKey: 'auth_methods',
|
||||
header: t('userName'),
|
||||
cell: ({ row }) => row.original.auth_methods?.[0]?.auth_identifier || '--',
|
||||
cell: ({ row }) => {
|
||||
const method = row.original.auth_methods?.[0];
|
||||
return (
|
||||
<div>
|
||||
<Badge className='mr-1 uppercase' title={method?.verified ? t('verified') : ''}>
|
||||
{method?.auth_type}
|
||||
</Badge>
|
||||
{method?.auth_identifier}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'balance',
|
||||
@@ -117,31 +129,9 @@ export default function Page() {
|
||||
actions={{
|
||||
render: (row) => {
|
||||
return [
|
||||
<UserForm<API.UpdateUserRequest>
|
||||
key='edit'
|
||||
trigger={t('edit')}
|
||||
title={t('editUser')}
|
||||
loading={loading}
|
||||
initialValues={row as unknown as API.UpdateUserRequest}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateUser({
|
||||
...row,
|
||||
...values,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
<Button key='detail' asChild>
|
||||
<Link href={`/dashboard/user/${row.id}`}>{t('edit')}</Link>
|
||||
</Button>,
|
||||
<ConfirmButton
|
||||
key='edit'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@workspace/ui/components/button';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function UserDetail({ id }: { id: number }) {
|
||||
@@ -27,8 +28,10 @@ export function UserDetail({ id }: { id: number }) {
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild onMouseEnter={() => setShouldFetch(true)}>
|
||||
<Button variant='link' className='p-0'>
|
||||
@{data?.email?.split('@')[0] || 'Loading...'}
|
||||
<Button variant='link' className='p-0' asChild>
|
||||
<Link href={`/dashboard/user/${id}`}>
|
||||
{data?.auth_methods[0]?.auth_identifier || t('loading')}
|
||||
</Link>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
@@ -38,10 +41,6 @@ export function UserDetail({ id }: { id: number }) {
|
||||
<span className='text-muted-foreground'>ID</span>
|
||||
<span>{data?.id}</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('email')}</span>
|
||||
<span>{data?.email}</span>
|
||||
</li>
|
||||
<li className='flex items-center justify-between font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('balance')}</span>
|
||||
<span>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const formSchema = z.object({
|
||||
email: z.string().email(t('form.invalidEmailFormat')),
|
||||
email: z.string().email(t('invalidEmailFormat')),
|
||||
telephone_area_code: z.string().optional(),
|
||||
telephone: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
@@ -104,10 +104,10 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.userEmail')}</FormLabel>
|
||||
<FormLabel>{t('userEmail')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('form.userEmailPlaceholder')}
|
||||
placeholder={t('userEmailPlaceholder')}
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
@@ -124,7 +124,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='telephone'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.telephone')}</FormLabel>
|
||||
<FormLabel>{t('telephone')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
prefix={
|
||||
@@ -137,7 +137,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
<AreaCodeSelect
|
||||
className='w-32 rounded-none border-y-0 border-l-0'
|
||||
simple
|
||||
placeholder={t('form.areaCodePlaceholder')}
|
||||
placeholder={t('areaCodePlaceholder')}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value.phone);
|
||||
@@ -149,7 +149,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
)}
|
||||
/>
|
||||
}
|
||||
placeholder={t('form.telephonePlaceholder')}
|
||||
placeholder={t('telephonePlaceholder')}
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
@@ -165,10 +165,10 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.password')}</FormLabel>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('form.passwordPlaceholder')}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
@@ -184,10 +184,10 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='referer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.refererId')}</FormLabel>
|
||||
<FormLabel>{t('refererId')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('form.refererIdPlaceholder')}
|
||||
placeholder={t('refererIdPlaceholder')}
|
||||
{...field}
|
||||
type='number'
|
||||
onValueChange={(value) => {
|
||||
@@ -204,10 +204,10 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='refer_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.inviteCode')}</FormLabel>
|
||||
<FormLabel>{t('inviteCode')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('form.inviteCodePlaceholder')}
|
||||
placeholder={t('inviteCodePlaceholder')}
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
@@ -223,11 +223,11 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='balance'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.balance')}</FormLabel>
|
||||
<FormLabel>{t('balance')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
placeholder={t('form.balancePlaceholder')}
|
||||
placeholder={t('balancePlaceholder')}
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
@@ -247,11 +247,11 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='gift_amount'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.giftAmount')}</FormLabel>
|
||||
<FormLabel>{t('giftAmount')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
placeholder={t('form.giftAmountPlaceholder')}
|
||||
placeholder={t('giftAmountPlaceholder')}
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
@@ -271,11 +271,11 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='commission'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.commission')}</FormLabel>
|
||||
<FormLabel>{t('commission')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
prefix={currency?.currency_symbol ?? '$'}
|
||||
placeholder={t('form.commissionPlaceholder')}
|
||||
placeholder={t('commissionPlaceholder')}
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||
@@ -295,7 +295,7 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
name='is_admin'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.manager')}</FormLabel>
|
||||
<FormLabel>{t('manager')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className='pt-2'>
|
||||
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
|
||||
@@ -316,11 +316,10 @@ export default function UserForm<T extends Record<string, any>>({
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('form.cancel')}
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
|
||||
{t('form.confirm')}
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />} {t('confirm')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
@@ -59,7 +59,7 @@ export function RevenueStatisticsCard() {
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='today'>
|
||||
<Card>
|
||||
<Card className='h-full'>
|
||||
<CardHeader className='flex !flex-row items-center justify-between'>
|
||||
<CardTitle>{t('revenueTitle')}</CardTitle>
|
||||
<TabsList>
|
||||
@@ -68,8 +68,8 @@ export function RevenueStatisticsCard() {
|
||||
<TabsTrigger value='total'>{t('total')}</TabsTrigger>
|
||||
</TabsList>
|
||||
</CardHeader>
|
||||
<TabsContent value='today'>
|
||||
<CardContent>
|
||||
<TabsContent value='today' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{RevenueStatistics?.today.new_order_amount ||
|
||||
RevenueStatistics?.today.renewal_order_amount ? (
|
||||
<ChartContainer config={IncomeStatisticsConfig} className='mx-auto max-h-80'>
|
||||
@@ -129,7 +129,9 @@ export function RevenueStatisticsCard() {
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
@@ -162,8 +164,8 @@ export function RevenueStatisticsCard() {
|
||||
</CardFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='month'>
|
||||
<CardContent>
|
||||
<TabsContent value='month' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{RevenueStatistics?.monthly.list && RevenueStatistics?.monthly.list.length > 0 ? (
|
||||
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
||||
<BarChart
|
||||
@@ -196,7 +198,9 @@ export function RevenueStatisticsCard() {
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
@@ -232,8 +236,8 @@ export function RevenueStatisticsCard() {
|
||||
</CardFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='total'>
|
||||
<CardContent>
|
||||
<TabsContent value='total' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{RevenueStatistics?.all.list && RevenueStatistics?.all.list.length > 0 ? (
|
||||
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
|
||||
<AreaChart
|
||||
@@ -283,7 +287,9 @@ export function RevenueStatisticsCard() {
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function Statistics() {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className='h-80'>
|
||||
<div className='mb-6 flex items-center justify-between'>
|
||||
<h4 className='font-semibold'>
|
||||
{dataType === 'nodes' ? t('nodeTraffic') : t('userTraffic')}
|
||||
@@ -230,7 +230,9 @@ export default function Statistics() {
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -61,7 +61,7 @@ export function UserStatisticsCard() {
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='today'>
|
||||
<Card>
|
||||
<Card className='h-full'>
|
||||
<CardHeader className='flex !flex-row items-center justify-between'>
|
||||
<CardTitle>{t('userTitle')}</CardTitle>
|
||||
<TabsList>
|
||||
@@ -71,8 +71,8 @@ export function UserStatisticsCard() {
|
||||
</TabsList>
|
||||
</CardHeader>
|
||||
|
||||
<TabsContent value='today'>
|
||||
<CardContent>
|
||||
<TabsContent value='today' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{UserStatistics?.today.register ||
|
||||
UserStatistics?.today.new_order_users ||
|
||||
UserStatistics?.today.renewal_order_users ? (
|
||||
@@ -133,7 +133,9 @@ export function UserStatisticsCard() {
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
@@ -168,8 +170,8 @@ export function UserStatisticsCard() {
|
||||
</CardFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='month'>
|
||||
<CardContent>
|
||||
<TabsContent value='month' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{UserStatistics?.monthly.list && UserStatistics?.monthly.list.length > 0 ? (
|
||||
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
||||
<BarChart
|
||||
@@ -204,7 +206,9 @@ export function UserStatisticsCard() {
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
@@ -239,8 +243,8 @@ export function UserStatisticsCard() {
|
||||
</CardFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='total'>
|
||||
<CardContent>
|
||||
<TabsContent value='total' className='h-full'>
|
||||
<CardContent className='h-80'>
|
||||
{UserStatistics?.all.list && UserStatistics?.all.list.length > 0 ? (
|
||||
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
|
||||
<AreaChart
|
||||
@@ -299,7 +303,9 @@ export function UserStatisticsCard() {
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
) : (
|
||||
<Empty />
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex h-20 flex-row border-t p-4'>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ProTable as _ProTable, ProTableProps } from '@workspace/ui/custom-components/pro-table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Empty } from './empty';
|
||||
export { type ProTableActions } from '@workspace/ui/custom-components/pro-table';
|
||||
|
||||
export function ProTable<
|
||||
@@ -25,6 +26,7 @@ export function ProTable<
|
||||
},
|
||||
textRowsPerPage: t('rowsPerPage'),
|
||||
}}
|
||||
empty={<Empty />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ export const AuthControl = [
|
||||
title: 'GitHub',
|
||||
url: '/dashboard/auth-control/github',
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
url: '/dashboard/auth-control/device',
|
||||
},
|
||||
];
|
||||
|
||||
export const navs = [
|
||||
@@ -101,6 +105,12 @@ export const navs = [
|
||||
title: 'User Management',
|
||||
url: '/dashboard/user',
|
||||
icon: 'flat-color-icons:conference-call',
|
||||
items: [
|
||||
{
|
||||
title: 'User Detail',
|
||||
url: '/dashboard/user/:id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Announcement Management',
|
||||
@@ -127,9 +137,15 @@ export const navs = [
|
||||
];
|
||||
|
||||
export function findNavByUrl(url: string) {
|
||||
function matchDynamicRoute(pattern: string, path: string): boolean {
|
||||
const regexPattern = pattern.replace(/:[^/]+/g, '[^/]+').replace(/\//g, '\\/');
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(path);
|
||||
}
|
||||
|
||||
function findNav(items: any[], url: string, path: any[] = []): any[] {
|
||||
for (const item of items) {
|
||||
if (item.url === url) {
|
||||
if (item.url === url || (item.url && matchDynamicRoute(item.url, url))) {
|
||||
return [...path, item];
|
||||
}
|
||||
if (item.items) {
|
||||
|
||||
@@ -14,50 +14,34 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||
site_name: '',
|
||||
site_desc: '',
|
||||
site_logo: '',
|
||||
keywords: '',
|
||||
custom_html: '',
|
||||
},
|
||||
verify: {
|
||||
turnstile_site_key: '',
|
||||
enable_login_verify: false,
|
||||
enable_register_verify: false,
|
||||
enable_reset_password_verify: false,
|
||||
turnstile_site_key: '',
|
||||
},
|
||||
auth: {
|
||||
sms: {
|
||||
sms_enabled: false,
|
||||
sms_limit: 0,
|
||||
sms_interval: 0,
|
||||
sms_expire_time: 0,
|
||||
mobile: {
|
||||
enable: false,
|
||||
enable_whitelist: false,
|
||||
whitelist: [],
|
||||
},
|
||||
email: {
|
||||
email_enabled: false,
|
||||
email_enable_verify: false,
|
||||
email_enable_domain_suffix: false,
|
||||
email_domain_suffix_list: '',
|
||||
enable: false,
|
||||
enable_verify: false,
|
||||
enable_domain_suffix: false,
|
||||
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: {
|
||||
stop_register: false,
|
||||
enable_email_verify: false,
|
||||
enable_email_domain_suffix: false,
|
||||
email_domain_suffix_list: '',
|
||||
enable_trial: false,
|
||||
enable_ip_register_limit: false,
|
||||
ip_register_limit: 0,
|
||||
ip_register_limit_duration: 0,
|
||||
sms: {
|
||||
sms_enabled: false,
|
||||
sms_limit: 0,
|
||||
sms_interval: 0,
|
||||
sms_expire_time: 0,
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
forced_invite: false,
|
||||
referral_percentage: 0,
|
||||
@@ -66,7 +50,6 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||
currency: {
|
||||
currency_unit: 'USD',
|
||||
currency_symbol: '$',
|
||||
access_key: '',
|
||||
},
|
||||
subscribe: {
|
||||
single_model: false,
|
||||
@@ -74,6 +57,11 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||
subscribe_domain: '',
|
||||
pan_domain: false,
|
||||
},
|
||||
verify_code: {
|
||||
verify_code_expire_time: 5,
|
||||
verify_code_limit: 15,
|
||||
verify_code_interval: 60,
|
||||
},
|
||||
oauth_methods: [],
|
||||
},
|
||||
user: undefined,
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"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í",
|
||||
"redirectUri": "Přesměrovací URL",
|
||||
"redirectUriDescription": "Prosím vyplňte adresu API přesměrované URL po úspěšném ověření pomocí Apple. Nepoužívejte / na konci.",
|
||||
"saveFailed": "Uložení se nezdařilo",
|
||||
"saveSuccess": "Uložení bylo úspěšné",
|
||||
"teamId": "ID týmu",
|
||||
|
||||
@@ -1,13 +1,74 @@
|
||||
{
|
||||
"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í"
|
||||
"invite": {
|
||||
"commissionFirstTimeOnly": "Provize pouze za první nákup",
|
||||
"commissionFirstTimeOnlyDescription": "Pokud je povoleno, provize se generuje pouze při první platbě pozývatele; jednotlivé uživatele můžete konfigurovat v uživatelské správě",
|
||||
"enableForcedInvite": "Povolit nucené pozvání",
|
||||
"enableForcedInviteDescription": "Pokud je povoleno, mohou se registrovat pouze pozvaní uživatelé",
|
||||
"inputPlaceholder": "Zadejte",
|
||||
"inviteCommissionPercentage": "Procento provize za pozvání",
|
||||
"inviteCommissionPercentageDescription": "Výchozí globální poměr rozdělení provize; jednotlivé poměry můžete konfigurovat v uživatelské správě",
|
||||
"inviteSettings": "Nastavení pozvání",
|
||||
"saveSuccess": "Uložení úspěšné"
|
||||
},
|
||||
"log": {
|
||||
"content": "Obsah",
|
||||
"createdAt": "Vytvořeno",
|
||||
"emailLog": "Emailový záznam",
|
||||
"mobileLog": "Mobilní záznam",
|
||||
"platform": "Platforma",
|
||||
"sendFailed": "Selhalo",
|
||||
"sendSuccess": "Úspěch",
|
||||
"status": "Stav",
|
||||
"subject": "Předmět",
|
||||
"to": "Příjemce",
|
||||
"updatedAt": "Aktualizováno"
|
||||
},
|
||||
"register": {
|
||||
"day": "Den",
|
||||
"hour": "Hodina",
|
||||
"ipRegistrationLimit": "Omezení registrace podle IP",
|
||||
"ipRegistrationLimitDescription": "Pokud je povoleno, IP adresy splňující pravidla budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo proxy serverům",
|
||||
"minute": "Minuta",
|
||||
"month": "Měsíc",
|
||||
"noLimit": "Bez omezení",
|
||||
"penaltyTime": "Doba trestu (minuty)",
|
||||
"penaltyTimeDescription": "Uživatelé musí počkat, až doba trestu vyprší, než se znovu zaregistrují",
|
||||
"registerSettings": "Nastavení registrace",
|
||||
"registrationLimitCount": "Počet omezení registrace",
|
||||
"registrationLimitCountDescription": "Povolit trest po dosažení registračního limitu",
|
||||
"saveSuccess": "Uložení úspěšné",
|
||||
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
|
||||
"stopNewUserRegistrationDescription": "Pokud je povoleno, nikdo se nemůže registrovat",
|
||||
"trialDuration": "Doba zkušební verze",
|
||||
"trialRegistration": "Zkušební registrace",
|
||||
"trialRegistrationDescription": "Povolit zkušební registraci; nejprve upravte zkušební balíček a dobu trvání",
|
||||
"trialSubscribePlan": "Plán zkušebního předplatného",
|
||||
"trialSubscribePlanDescription": "Vyberte plán zkušebního předplatného",
|
||||
"year": "Rok"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Zadejte",
|
||||
"loginVerificationCode": "Ověřovací kód pro přihlášení",
|
||||
"loginVerificationCodeDescription": "Ověření člověka během přihlášení",
|
||||
"registrationVerificationCode": "Ověřovací kód pro registraci",
|
||||
"registrationVerificationCodeDescription": "Ověření člověka během registrace",
|
||||
"resetPasswordVerificationCode": "Ověřovací kód pro resetování hesla",
|
||||
"resetPasswordVerificationCodeDescription": "Ověření člověka během resetování hesla",
|
||||
"saveSuccess": "Uložení úspěšné",
|
||||
"turnstileSecretDescription": "Tajný klíč turniketu poskytovaný Cloudflare",
|
||||
"turnstileSiteKeyDescription": "Klíč pro web turniketu poskytovaný Cloudflare",
|
||||
"verifySettings": "Nastavení ověření"
|
||||
},
|
||||
"verify-code": {
|
||||
"dailyLimit": "Denní limit",
|
||||
"dailyLimitDescription": "Maximální počet ověřovacích kódů, které lze odeslat za den",
|
||||
"expireTime": "Čas vypršení",
|
||||
"expireTimeDescription": "Doba platnosti ověřovacího kódu (vteřiny)",
|
||||
"interval": "Interval odesílání",
|
||||
"intervalDescription": "Minimální interval mezi odesíláním ověřovacích kódů (sekundy)",
|
||||
"saveSuccess": "Uložení úspěšné",
|
||||
"second": "sekundy",
|
||||
"times": "krát",
|
||||
"verifyCodeSettings": "Nastavení ověřovacího kódu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"logout": "Odhlásit se",
|
||||
"methods": {
|
||||
"email": "E-mail",
|
||||
"sms": "Telefon"
|
||||
"mobile": "Telefon"
|
||||
},
|
||||
"orWithEmail": "nebo pomocí e-mailu",
|
||||
"register": {
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
"60002": "Toto předplatné nelze momentálně použít, zkuste to prosím později.",
|
||||
"60003": "Bylo zjištěno existující předplatné. Zrušte ho, než budete pokračovat.",
|
||||
"60004": "Nelze momentálně odstranit, protože předplatné má aktivní uživatele.",
|
||||
"60005": "Režim jednotného předplatného překročil limit uživatelů",
|
||||
"70001": "Ověřovací kód je nesprávný, zadejte ho prosím znovu.",
|
||||
"80001": "Úkol nebyl úspěšně přidán do fronty, zkuste to prosím později.",
|
||||
"90001": "Vypněte prosím režim DEBUG a zkuste to znovu.",
|
||||
"90015": "Tento účet dnes dosáhl limitu pro odesílání, zkuste to prosím znovu zítra",
|
||||
"undefined": "Došlo k chybě systému, zkuste to prosím později."
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"enable": "Povolit",
|
||||
"enableDescription": "Po povolení jsou podporovány různé identifikátory zařízení, jako jsou IMEI/IDFA/IDFV/AndroidID/Mac adresa pro přihlášení a registraci"
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"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",
|
||||
"enableDescription": "Po aktivaci budou povoleny funkce registrace e-mailem, přihlášení, propojení a odpojení.",
|
||||
"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...",
|
||||
"logs": "Protokoly",
|
||||
"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",
|
||||
@@ -27,6 +25,7 @@
|
||||
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
|
||||
"sent": "Odesláno",
|
||||
"sentAt": "Odesláno",
|
||||
"settings": "Nastavení",
|
||||
"smtpAccount": "SMTP účet",
|
||||
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
|
||||
"smtpEncryptionMethod": "Metoda šifrování SMTP",
|
||||
@@ -39,6 +38,7 @@
|
||||
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
|
||||
"status": "Stav",
|
||||
"subject": "Předmět",
|
||||
"template": "Šablona",
|
||||
"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",
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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é"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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é"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"Auth Control": "Řízení ověřování",
|
||||
"Coupon Management": "Správa kupónů",
|
||||
"Dashboard": "Přístrojová deska",
|
||||
"Device": "Zařízení",
|
||||
"Document Management": "Správa dokumentů",
|
||||
"Email": "E-mail",
|
||||
"Facebook": "Facebook",
|
||||
@@ -24,5 +25,6 @@
|
||||
"Ticket Management": "Správa lístků",
|
||||
"Twitter": "Twitter",
|
||||
"User": "Uživatel",
|
||||
"User Detail": "Podrobnosti o uživateli",
|
||||
"User Management": "Správa uživatelů"
|
||||
}
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
{
|
||||
"SmsList": "Seznam SMS záznamů",
|
||||
"accessLabel": "Přístup",
|
||||
"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í",
|
||||
"endpointLabel": "Koncový bod",
|
||||
"logs": "Protokoly",
|
||||
"phoneNumberLabel": "Telefonní číslo",
|
||||
"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",
|
||||
"secretLabel": "Tajný klíč",
|
||||
"sendFailed": "Odeslání se nezdařilo",
|
||||
"sendSuccess": "Úspěšně odesláno",
|
||||
"settings": "Nastavení",
|
||||
"status": "Stav",
|
||||
"telephone": "Telefonní číslo",
|
||||
"signNameLabel": "Název podpisu",
|
||||
"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",
|
||||
"templateCodeLabel": "Kód šablony",
|
||||
"templateTip": "Prosím vyplňte SMS šablonu, ponechte {code} uprostřed, jinak SMS funkce 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á"
|
||||
"updateSuccess": "Aktualizace úspěšná",
|
||||
"whitelistAreaCode": "Kód oblasti v bílém seznamu",
|
||||
"whitelistAreaCodeTip": "Zadejte kódy oblastí v bílém seznamu, např. 1, 852, 886, 888",
|
||||
"whitelistValidation": "Ověření bílého seznamu",
|
||||
"whitelistValidationTip": "Pokud je povoleno, mohou SMS odesílat pouze kódy oblastí v bílém seznamu"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"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",
|
||||
"updateSuccess": "Úspěšně aktualizováno",
|
||||
"wildcardResolution": "Řešení zástupných znaků",
|
||||
"wildcardResolutionDescription": "Používá se pro předplatné"
|
||||
},
|
||||
|
||||
@@ -8,17 +8,11 @@
|
||||
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
|
||||
"saveSuccess": "Úspěšně uloženo"
|
||||
},
|
||||
"invite": {
|
||||
"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ů.",
|
||||
"enableForcedInvite": "Povolit povinné pozvání",
|
||||
"enableForcedInviteDescription": "Po aktivaci se mohou registrovat pouze pozvaní uživatelé.",
|
||||
"inputPlaceholder": "Prosím zadejte",
|
||||
"inviteCommissionPercentage": "Procento provize za pozvání",
|
||||
"inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.",
|
||||
"saveSuccess": "Úspěšně uloženo"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Vlastní HTML",
|
||||
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu <body> na stránce.",
|
||||
"keywords": "Klíčová slova",
|
||||
"keywordsDescription": "Používá se pro SEO účely",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
|
||||
"logoPlaceholder": "Zadejte URL adresu LOGA, nekončete '/'",
|
||||
@@ -35,42 +29,11 @@
|
||||
},
|
||||
"tabs": {
|
||||
"currency": "Měna",
|
||||
"invite": "Pozvat",
|
||||
"site": "Stránka",
|
||||
"telegram": "Telegram",
|
||||
"tos": "Podmínky služby",
|
||||
"verify": "Ověřit"
|
||||
},
|
||||
"telegram": {
|
||||
"botToken": "Token robota",
|
||||
"botTokenDescription": "Zadejte token poskytnutý Botfatherem",
|
||||
"enableBotNotifications": "Povolit oznámení robota",
|
||||
"enableBotNotificationsDescription": "Po povolení bude robot posílat základní oznámení správcům a uživatelům propojeným s Telegramem",
|
||||
"groupURL": "URL skupiny",
|
||||
"groupURLDescription": "Po vyplnění se zobrazí na uživatelské straně nebo tam, kde je potřeba",
|
||||
"inputPlaceholderBotToken": "0000000000:xxxxxxxxx_xxxxxxxxxxxxxxx",
|
||||
"inputPlaceholderGroupURL": "https://t.me/xxxxxx",
|
||||
"inputPlaceholderWebhookDomain": "https://example.com",
|
||||
"saveSuccess": "Úspěšně uloženo",
|
||||
"webhookDomain": "Doména webhooku",
|
||||
"webhookDomainDescription": "Zadejte název domény serveru webhooku"
|
||||
"tos": "Podmínky služby"
|
||||
},
|
||||
"tos": {
|
||||
"saveSuccess": "Uložení bylo úspěšné",
|
||||
"title": "Podmínky služby"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Prosím zadejte",
|
||||
"loginVerificationCode": "Ověřovací kód pro přihlášení",
|
||||
"loginVerificationCodeDescription": "Ověření člověka při přihlášení",
|
||||
"registrationVerificationCode": "Ověřovací kód pro registraci",
|
||||
"registrationVerificationCodeDescription": "Ověření člověka při registraci",
|
||||
"resetPasswordVerificationCode": "Ověřovací kód pro resetování hesla",
|
||||
"resetPasswordVerificationCodeDescription": "Ověření člověka při resetování hesla",
|
||||
"saveSuccess": "Úspěšně uloženo",
|
||||
"turnstileSecret": "Turnstile tajný klíč",
|
||||
"turnstileSecretDescription": "Turnstile tajný klíč poskytovaný Cloudflare",
|
||||
"turnstileSiteKey": "Turnstile klíč webu",
|
||||
"turnstileSiteKeyDescription": "Turnstile klíč webu poskytovaný Cloudflare"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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é"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,108 @@
|
||||
{
|
||||
"accountEnable": "Povolení účtu",
|
||||
"actions": "akce",
|
||||
"add": "Přidat",
|
||||
"administrator": "Administrátor",
|
||||
"areaCode": "Směrový kód",
|
||||
"areaCodePlaceholder": "Kód oblasti",
|
||||
"auth": "Autentizace",
|
||||
"authIdentifier": "Identifikátor ověření",
|
||||
"authIdentifierPlaceholder": "Zadejte identifikátor ověření",
|
||||
"authMethodsTitle": "Nastavení ověřování",
|
||||
"authType": "Typ ověření",
|
||||
"authUser": "Autentizovaný uživatel",
|
||||
"avatar": "Avatar",
|
||||
"balance": "Zůstatek",
|
||||
"balanceNotifications": "Oznámení o změně zůstatku",
|
||||
"balancePlaceholder": "Zůstatek",
|
||||
"basicInfoTitle": "Základní informace",
|
||||
"cancel": "zrušit",
|
||||
"commission": "Provize",
|
||||
"commissionPlaceholder": "Zadejte provizi",
|
||||
"confirm": "Potvrdit",
|
||||
"confirmDelete": "Opravdu chcete smazat?",
|
||||
"confirmOffline": "Potvrdit offline",
|
||||
"create": "Vytvořit",
|
||||
"createSubscription": "Vytvořit předplatné",
|
||||
"createSuccess": "Vytvoření úspěšné",
|
||||
"createUser": "Vytvořit uživatele",
|
||||
"createdAt": "Datum registrace",
|
||||
"delete": "smazat",
|
||||
"deleteDescription": "Po odstranění nelze data obnovit, prosím, postupujte opatrně.",
|
||||
"deleteSubscriptionDescription": "Opravdu chcete smazat toto předplatné?",
|
||||
"deleteSuccess": "Úspěšně odstraněno",
|
||||
"detail": "Detail",
|
||||
"deviceLimit": "Limit zařízení",
|
||||
"download": "Stáhnout",
|
||||
"downloadTraffic": "Stáhnout provoz",
|
||||
"edit": "Upravit",
|
||||
"editSubscription": "Upravit předplatné",
|
||||
"editUser": "Upravit uživatele",
|
||||
"email": "e-mail",
|
||||
"enable": "Povolit",
|
||||
"form": {
|
||||
"areaCode": "Směrové číslo",
|
||||
"areaCodePlaceholder": "Směrové číslo",
|
||||
"balance": "Zůstatek",
|
||||
"balancePlaceholder": "Zůstatek",
|
||||
"cancel": "Zrušit",
|
||||
"commission": "Provize",
|
||||
"commissionPlaceholder": "Zadejte provizi",
|
||||
"confirm": "Potvrdit",
|
||||
"giftAmount": "Částka daru",
|
||||
"giftAmountPlaceholder": "Zadejte částku daru",
|
||||
"invalidEmailFormat": "Neplatný formát e-mailu",
|
||||
"inviteCode": "Pozvánkový kód",
|
||||
"inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)",
|
||||
"manager": "Správce",
|
||||
"password": "Heslo",
|
||||
"passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)",
|
||||
"refererId": "ID doporučitele",
|
||||
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
||||
"telephone": "Telefonní číslo",
|
||||
"telephonePlaceholder": "Zadejte telefonní číslo",
|
||||
"userEmail": "E-mail uživatele",
|
||||
"userEmailPlaceholder": "Zadejte e-mail uživatele"
|
||||
},
|
||||
"expireTime": "Čas vypršení",
|
||||
"expiredAt": "Platnost vypršela",
|
||||
"failed": "Neúspěch",
|
||||
"giftAmount": "Částka dárku",
|
||||
"giftAmountPlaceholder": "Zadejte částku dárku",
|
||||
"invalidEmailFormat": "Neplatný formát e-mailu",
|
||||
"inviteCode": "Pozvánkový kód",
|
||||
"inviteCodePlaceholder": "Zadejte kód pozvánky (ponechte prázdné pro vygenerování)",
|
||||
"kickOfflineConfirm": "Opravdu chcete odpojit IP {ip}?",
|
||||
"kickOfflineSuccess": "Úspěšně oznámeno odpojení",
|
||||
"lastSeen": "Naposledy viděno",
|
||||
"loading": "Načítání...",
|
||||
"loginIp": "Přihlašovací IP",
|
||||
"loginNotifications": "Oznámení o přihlášení",
|
||||
"loginStatus": "Stav přihlášení",
|
||||
"loginTime": "Čas přihlášení",
|
||||
"manager": "Manažer",
|
||||
"notifySettingsTitle": "Nastavení oznámení",
|
||||
"onlineDevices": "Online zařízení",
|
||||
"password": "Heslo",
|
||||
"passwordPlaceholder": "Zadejte nové heslo (volitelné)",
|
||||
"permanent": "Trvalý",
|
||||
"pleaseEnterEmail": "Prosím, zadejte e-mail",
|
||||
"referer": "Doporučitel",
|
||||
"refererId": "ID doporučitele",
|
||||
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
||||
"referralCode": "Doporučovací kód",
|
||||
"referrerUserId": "Odesílatel (uživatelské ID)",
|
||||
"remove": "Odstranit",
|
||||
"resetTime": "Obnovit čas",
|
||||
"save": "Uložit",
|
||||
"searchIp": "Vyhledat IP adresu",
|
||||
"selectAuthType": "Vyberte typ ověření",
|
||||
"speedLimit": "Rychlostní limit",
|
||||
"subscription": "Předplatné",
|
||||
"subscriptionDetails": "Podrobnosti o předplatném",
|
||||
"subscriptionList": "Seznam předplatných",
|
||||
"subscriptionLogs": "Záznamy o předplatném",
|
||||
"subscriptionName": "Název předplatného",
|
||||
"subscriptionNotifications": "Oznámení o předplatném",
|
||||
"success": "Úspěch",
|
||||
"telephone": "Telefonní číslo",
|
||||
"telephonePlaceholder": "Zadejte telefonní číslo",
|
||||
"time": "Čas",
|
||||
"token": "Token",
|
||||
"totalTraffic": "Celkový provoz",
|
||||
"tradeNotifications": "Obchodní oznámení",
|
||||
"trafficLimit": "Limit provozu",
|
||||
"trafficLogs": "Protokoly provozu",
|
||||
"unlimited": "Neomezený",
|
||||
"unverified": "Neověřeno",
|
||||
"update": "Aktualizovat",
|
||||
"updateSuccess": "Aktualizace byla úspěšná",
|
||||
"upload": "Nahrát",
|
||||
"uploadTraffic": "Nahrát provoz",
|
||||
"userAgent": "Uživatelský agent",
|
||||
"userEmail": "Uživatelský e-mail",
|
||||
"userEmailPlaceholder": "Zadejte e-mail uživatele",
|
||||
"userList": "Seznam uživatelů",
|
||||
"userName": "Identifikátor uživatele"
|
||||
"userLogs": "Historie přihlášení",
|
||||
"userName": "Identifikátor uživatele",
|
||||
"userOrders": "Objednávky",
|
||||
"userProfile": "Profil",
|
||||
"userSubscriptions": "Předplatné",
|
||||
"verified": "Ověřeno"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"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",
|
||||
"redirectUri": "Weiterleitungs-URL",
|
||||
"redirectUriDescription": "Bitte geben Sie die API-Adresse der umgeleiteten URL ein, nachdem die Apple-Authentifizierung erfolgreich abgeschlossen wurde. Verwenden Sie kein / am Ende.",
|
||||
"saveFailed": "Speichern fehlgeschlagen",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"teamId": "Team-ID",
|
||||
|
||||
@@ -1,13 +1,74 @@
|
||||
{
|
||||
"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"
|
||||
"invite": {
|
||||
"commissionFirstTimeOnly": "Provision nur beim ersten Kauf",
|
||||
"commissionFirstTimeOnlyDescription": "Wenn aktiviert, wird die Provision nur bei der ersten Zahlung des Einladers generiert; individuelle Benutzer können in der Benutzerverwaltung konfiguriert werden",
|
||||
"enableForcedInvite": "Erzwinge Einladung",
|
||||
"enableForcedInviteDescription": "Wenn aktiviert, können sich nur eingeladene Benutzer registrieren",
|
||||
"inputPlaceholder": "Eingeben",
|
||||
"inviteCommissionPercentage": "Einladungsprovisionssatz",
|
||||
"inviteCommissionPercentageDescription": "Standardmäßiges globales Provisionsverteilungsschema; individuelle Sätze können in der Benutzerverwaltung konfiguriert werden",
|
||||
"inviteSettings": "Einladungseinstellungen",
|
||||
"saveSuccess": "Erfolgreich gespeichert"
|
||||
},
|
||||
"log": {
|
||||
"content": "Inhalt",
|
||||
"createdAt": "Erstellt am",
|
||||
"emailLog": "E-Mail-Protokoll",
|
||||
"mobileLog": "Mobilprotokoll",
|
||||
"platform": "Plattform",
|
||||
"sendFailed": "Fehlgeschlagen",
|
||||
"sendSuccess": "Erfolgreich",
|
||||
"status": "Status",
|
||||
"subject": "Betreff",
|
||||
"to": "Empfänger",
|
||||
"updatedAt": "Aktualisiert am"
|
||||
},
|
||||
"register": {
|
||||
"day": "Tag",
|
||||
"hour": "Stunde",
|
||||
"ipRegistrationLimit": "IP-Registrierungsgrenze",
|
||||
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die IP-Bestimmung aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann",
|
||||
"minute": "Minute",
|
||||
"month": "Monat",
|
||||
"noLimit": "Keine Begrenzung",
|
||||
"penaltyTime": "Strafzeit (Minuten)",
|
||||
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren",
|
||||
"registerSettings": "Registrierungseinstellungen",
|
||||
"registrationLimitCount": "Registrierungsgrenzanzahl",
|
||||
"registrationLimitCountDescription": "Strafe aktivieren, nachdem die Registrierungsgrenze erreicht wurde",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
|
||||
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
|
||||
"trialDuration": "Testdauer",
|
||||
"trialRegistration": "Testregistrierung",
|
||||
"trialRegistrationDescription": "Testregistrierung aktivieren; Testpaket und -dauer zuerst ändern",
|
||||
"trialSubscribePlan": "Testabonnement-Plan",
|
||||
"trialSubscribePlanDescription": "Wählen Sie einen Testabonnement-Plan aus",
|
||||
"year": "Jahr"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Eingeben",
|
||||
"loginVerificationCode": "Anmeldebestätigungscode",
|
||||
"loginVerificationCodeDescription": "Menschliche Überprüfung während der Anmeldung",
|
||||
"registrationVerificationCode": "Registrierungsbestätigungscode",
|
||||
"registrationVerificationCodeDescription": "Menschliche Überprüfung während der Registrierung",
|
||||
"resetPasswordVerificationCode": "Bestätigungscode zum Zurücksetzen des Passworts",
|
||||
"resetPasswordVerificationCodeDescription": "Menschliche Überprüfung während der Passwortzurücksetzung",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"turnstileSecretDescription": "Turnstile-Geheimschlüssel, bereitgestellt von Cloudflare",
|
||||
"turnstileSiteKeyDescription": "Turnstile-Seitenschlüssel, bereitgestellt von Cloudflare",
|
||||
"verifySettings": "Überprüfungseinstellungen"
|
||||
},
|
||||
"verify-code": {
|
||||
"dailyLimit": "Tägliches Limit",
|
||||
"dailyLimitDescription": "Maximale Anzahl von Bestätigungscodes, die pro Tag gesendet werden können",
|
||||
"expireTime": "Ablaufzeit",
|
||||
"expireTimeDescription": "Ablaufzeit des Bestätigungscodes (Sekunden)",
|
||||
"interval": "Sendeintervall",
|
||||
"intervalDescription": "Minimales Intervall zwischen dem Versenden von Bestätigungscodes (Sekunden)",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"second": "Sekunden",
|
||||
"times": "Mal",
|
||||
"verifyCodeSettings": "Einstellungen für den Bestätigungscode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"logout": "Abmelden",
|
||||
"methods": {
|
||||
"email": "E-Mail",
|
||||
"sms": "Telefon"
|
||||
"mobile": "Telefon"
|
||||
},
|
||||
"orWithEmail": "oder mit E-Mail",
|
||||
"register": {
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
"60002": "Das Abonnement kann derzeit nicht verwendet werden, bitte versuchen Sie es später erneut.",
|
||||
"60003": "Ein bestehendes Abonnement wurde erkannt. Bitte kündigen Sie es, bevor Sie fortfahren.",
|
||||
"60004": "Kann derzeit nicht gelöscht werden, da das Abonnement aktive Benutzer hat.",
|
||||
"60005": "Im Single-Abo-Modus wurde das Benutzerlimit überschritten",
|
||||
"70001": "Der Bestätigungscode ist falsch, bitte erneut eingeben.",
|
||||
"80001": "Die Aufgabe wurde nicht erfolgreich in die Warteschlange aufgenommen, bitte versuchen Sie es später erneut.",
|
||||
"90001": "Bitte deaktivieren Sie den DEBUG-Modus und versuchen Sie es erneut.",
|
||||
"90015": "Dieses Konto hat das tägliche Sendelimit erreicht. Bitte versuchen Sie es morgen erneut",
|
||||
"undefined": "Es ist ein Systemfehler aufgetreten, bitte versuchen Sie es später erneut."
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"enable": "Aktivieren",
|
||||
"enableDescription": "Nach der Aktivierung werden mehrere Geräteidentifikatoren wie IMEI/IDFA/IDFV/AndroidID/Mac-Adresse für die Anmeldung und Registrierung unterstützt."
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"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",
|
||||
"enableDescription": "Nach der Aktivierung werden die Funktionen zur E-Mail-Registrierung, Anmeldung, Bindung und Entbindung 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...",
|
||||
"logs": "Protokolle",
|
||||
"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",
|
||||
@@ -27,6 +25,7 @@
|
||||
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
|
||||
"sent": "Gesendet",
|
||||
"sentAt": "Gesendet am",
|
||||
"settings": "Einstellungen",
|
||||
"smtpAccount": "SMTP-Konto",
|
||||
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
|
||||
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
|
||||
@@ -39,6 +38,7 @@
|
||||
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
|
||||
"status": "Status",
|
||||
"subject": "Betreff",
|
||||
"template": "Vorlage",
|
||||
"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",
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"Auth Control": "Authentifizierungskontrolle",
|
||||
"Coupon Management": "Gutscheinverwaltung",
|
||||
"Dashboard": "Armaturenbrett",
|
||||
"Device": "Gerät",
|
||||
"Document Management": "Dokumentenverwaltung",
|
||||
"Email": "E-Mail",
|
||||
"Facebook": "Facebook",
|
||||
@@ -24,5 +25,6 @@
|
||||
"Ticket Management": "Ticketverwaltung",
|
||||
"Twitter": "Twitter",
|
||||
"User": "Benutzer",
|
||||
"User Detail": "Benutzerdetails",
|
||||
"User Management": "Benutzerverwaltung"
|
||||
}
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
{
|
||||
"SmsList": "SMS-Protokollliste",
|
||||
"accessLabel": "Zugriff",
|
||||
"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",
|
||||
"endpointLabel": "Endpunkt",
|
||||
"logs": "Protokolle",
|
||||
"phoneNumberLabel": "Telefonnummer",
|
||||
"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",
|
||||
"secretLabel": "Geheimnis",
|
||||
"sendFailed": "Senden fehlgeschlagen",
|
||||
"sendSuccess": "Erfolgreich gesendet",
|
||||
"settings": "Einstellungen",
|
||||
"status": "Status",
|
||||
"telephone": "Telefonnummer",
|
||||
"signNameLabel": "Signaturname",
|
||||
"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.",
|
||||
"templateCodeLabel": "Vorlagen-Code",
|
||||
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus, lassen Sie {code} in der Mitte, andernfalls funktioniert die SMS-Funktion nicht.",
|
||||
"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"
|
||||
"updateSuccess": "Aktualisierung erfolgreich",
|
||||
"whitelistAreaCode": "Whitelist Bereichscode",
|
||||
"whitelistAreaCodeTip": "Bitte geben Sie die Whitelist Bereichscodes ein, z.B. 1, 852, 886, 888",
|
||||
"whitelistValidation": "Whitelist Überprüfung",
|
||||
"whitelistValidationTip": "Wenn aktiviert, können nur Bereichscodes in der Whitelist SMS senden"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"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",
|
||||
"updateSuccess": "Erfolgreich aktualisiert",
|
||||
"wildcardResolution": "Wildcard-Auflösung",
|
||||
"wildcardResolutionDescription": "Wird für Abonnements verwendet"
|
||||
},
|
||||
|
||||
@@ -8,17 +8,11 @@
|
||||
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
|
||||
"saveSuccess": "Erfolgreich gespeichert"
|
||||
},
|
||||
"invite": {
|
||||
"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.",
|
||||
"enableForcedInvite": "Erzwungene Einladung aktivieren",
|
||||
"enableForcedInviteDescription": "Nach Aktivierung können sich nur eingeladene Benutzer registrieren.",
|
||||
"inputPlaceholder": "Bitte eingeben",
|
||||
"inviteCommissionPercentage": "Einladungsprovisionsprozentsatz",
|
||||
"inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.",
|
||||
"saveSuccess": "Erfolgreich gespeichert"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Benutzerdefiniertes HTML",
|
||||
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des <body>-Tags der Seite eingefügt werden soll.",
|
||||
"keywords": "Schlüsselwörter",
|
||||
"keywordsDescription": "Für SEO-Zwecke verwendet",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Position zur Anzeige des LOGOs",
|
||||
"logoPlaceholder": "Bitte geben Sie die URL-Adresse des LOGOs ein, ohne '/' am Ende",
|
||||
@@ -35,42 +29,11 @@
|
||||
},
|
||||
"tabs": {
|
||||
"currency": "Währung",
|
||||
"invite": "Einladen",
|
||||
"site": "Seite",
|
||||
"telegram": "Telegram",
|
||||
"tos": "Nutzungsbedingungen",
|
||||
"verify": "Verifizieren"
|
||||
},
|
||||
"telegram": {
|
||||
"botToken": "Bot-Token",
|
||||
"botTokenDescription": "Bitte geben Sie das von Botfather bereitgestellte Token ein",
|
||||
"enableBotNotifications": "Bot-Benachrichtigungen aktivieren",
|
||||
"enableBotNotificationsDescription": "Nach Aktivierung sendet der Bot grundlegende Benachrichtigungen an Administratoren und Benutzer, die mit Telegram verbunden sind",
|
||||
"groupURL": "Gruppen-URL",
|
||||
"groupURLDescription": "Nach Eingabe wird sie auf der Benutzerseite angezeigt oder dort verwendet, wo sie benötigt wird",
|
||||
"inputPlaceholderBotToken": "0000000000:xxxxxxxxx_xxxxxxxxxxxxxxx",
|
||||
"inputPlaceholderGroupURL": "https://t.me/xxxxxx",
|
||||
"inputPlaceholderWebhookDomain": "https://example.com",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"webhookDomain": "Webhook-Domain",
|
||||
"webhookDomainDescription": "Geben Sie den Domainnamen des Webhook-Servers ein"
|
||||
"tos": "Nutzungsbedingungen"
|
||||
},
|
||||
"tos": {
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"title": "Nutzungsbedingungen"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Bitte eingeben",
|
||||
"loginVerificationCode": "Anmeldebestätigungscode",
|
||||
"loginVerificationCodeDescription": "Mensch-Maschine-Verifizierung bei der Anmeldung",
|
||||
"registrationVerificationCode": "Registrierungsbestätigungscode",
|
||||
"registrationVerificationCodeDescription": "Mensch-Maschine-Verifizierung bei der Registrierung",
|
||||
"resetPasswordVerificationCode": "Passwort-zurücksetzen-Bestätigungscode",
|
||||
"resetPasswordVerificationCodeDescription": "Mensch-Maschine-Verifizierung beim Zurücksetzen des Passworts",
|
||||
"saveSuccess": "Erfolgreich gespeichert",
|
||||
"turnstileSecret": "Turnstile-Schlüssel",
|
||||
"turnstileSecretDescription": "Von Cloudflare bereitgestellter Turnstile-Schlüssel",
|
||||
"turnstileSiteKey": "Turnstile-Standortschlüssel",
|
||||
"turnstileSiteKeyDescription": "Von Cloudflare bereitgestellter Turnstile-Standortschlüssel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,108 @@
|
||||
{
|
||||
"accountEnable": "Konto aktivieren",
|
||||
"actions": "Aktionen",
|
||||
"add": "Hinzufügen",
|
||||
"administrator": "Administrator",
|
||||
"areaCode": "Vorwahl",
|
||||
"areaCodePlaceholder": "Vorwahl",
|
||||
"auth": "Authentifizierung",
|
||||
"authIdentifier": "Authentifizierungskennung",
|
||||
"authIdentifierPlaceholder": "Geben Sie die Authentifizierungskennung ein",
|
||||
"authMethodsTitle": "Authentifizierungseinstellungen",
|
||||
"authType": "Authentifizierungstyp",
|
||||
"authUser": "Authentifizierter Benutzer",
|
||||
"avatar": "Avatar",
|
||||
"balance": "Kontostand",
|
||||
"balanceNotifications": "Benachrichtigungen über Kontostandsänderungen",
|
||||
"balancePlaceholder": "Kontostand",
|
||||
"basicInfoTitle": "Grundinformationen",
|
||||
"cancel": "Abbrechen",
|
||||
"commission": "Provision",
|
||||
"commissionPlaceholder": "Provision eingeben",
|
||||
"confirm": "Bestätigen",
|
||||
"confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?",
|
||||
"confirmOffline": "Offline bestätigen",
|
||||
"create": "Erstellen",
|
||||
"createSubscription": "Abonnement erstellen",
|
||||
"createSuccess": "Erstellung erfolgreich",
|
||||
"createUser": "Benutzer erstellen",
|
||||
"createdAt": "Registrierungsdatum",
|
||||
"delete": "Löschen",
|
||||
"deleteDescription": "Nach dem Löschen können die Daten nicht wiederhergestellt werden. Bitte gehen Sie vorsichtig vor.",
|
||||
"deleteSubscriptionDescription": "Sind Sie sicher, dass Sie dieses Abonnement löschen möchten?",
|
||||
"deleteSuccess": "Erfolgreich gelöscht",
|
||||
"detail": "Detail",
|
||||
"deviceLimit": "Gerätelimit",
|
||||
"download": "Herunterladen",
|
||||
"downloadTraffic": "Download-Verkehr",
|
||||
"edit": "Bearbeiten",
|
||||
"editSubscription": "Abonnement bearbeiten",
|
||||
"editUser": "Benutzer bearbeiten",
|
||||
"email": "E-Mail",
|
||||
"enable": "Aktivieren",
|
||||
"form": {
|
||||
"areaCode": "Vorwahl",
|
||||
"areaCodePlaceholder": "Vorwahl eingeben",
|
||||
"balance": "Kontostand",
|
||||
"balancePlaceholder": "Kontostand",
|
||||
"cancel": "Abbrechen",
|
||||
"commission": "Provision",
|
||||
"commissionPlaceholder": "Geben Sie die Provision ein",
|
||||
"confirm": "Bestätigen",
|
||||
"giftAmount": "Geschenkbetrag",
|
||||
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
|
||||
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
||||
"inviteCode": "Einladungscode",
|
||||
"inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)",
|
||||
"manager": "Administrator",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)",
|
||||
"refererId": "Empfehler-ID",
|
||||
"refererIdPlaceholder": "Bitte Empfehler-ID eingeben",
|
||||
"telephone": "Telefonnummer",
|
||||
"telephonePlaceholder": "Telefonnummer eingeben",
|
||||
"userEmail": "Benutzer-E-Mail",
|
||||
"userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben"
|
||||
},
|
||||
"expireTime": "Ablaufzeit",
|
||||
"expiredAt": "Abgelaufen am",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"giftAmount": "Geschenkbetrag",
|
||||
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
|
||||
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
||||
"inviteCode": "Einladungscode",
|
||||
"inviteCodePlaceholder": "Einladungscode eingeben (leer lassen, um zu generieren)",
|
||||
"kickOfflineConfirm": "Sind Sie sicher, dass Sie die IP {ip} abmelden möchten?",
|
||||
"kickOfflineSuccess": "Erfolgreich offline benachrichtigt",
|
||||
"lastSeen": "Zuletzt gesehen",
|
||||
"loading": "Laden...",
|
||||
"loginIp": "Login-IP",
|
||||
"loginNotifications": "Anmeldebenachrichtigungen",
|
||||
"loginStatus": "Anmeldestatus",
|
||||
"loginTime": "Anmeldezeit",
|
||||
"manager": "Manager",
|
||||
"notifySettingsTitle": "Benachrichtigungseinstellungen",
|
||||
"onlineDevices": "Online-Geräte",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Neues Passwort eingeben (optional)",
|
||||
"permanent": "Dauerhaft",
|
||||
"pleaseEnterEmail": "Bitte E-Mail eingeben",
|
||||
"referer": "Empfehler",
|
||||
"refererId": "Referenz-ID",
|
||||
"refererIdPlaceholder": "Geben Sie die Referenz-ID ein",
|
||||
"referralCode": "Empfehlungscode",
|
||||
"referrerUserId": "Empfehler (Benutzer-ID)",
|
||||
"remove": "Entfernen",
|
||||
"resetTime": "Zeit zurücksetzen",
|
||||
"save": "Speichern",
|
||||
"searchIp": "IP-Adresse suchen",
|
||||
"selectAuthType": "Authentifizierungstyp auswählen",
|
||||
"speedLimit": "Geschwindigkeitsbegrenzung",
|
||||
"subscription": "Abonnement",
|
||||
"subscriptionDetails": "Abonnementdetails",
|
||||
"subscriptionList": "Abonnementsliste",
|
||||
"subscriptionLogs": "Abonnementprotokolle",
|
||||
"subscriptionName": "Abonnementname",
|
||||
"subscriptionNotifications": "Abonnement-Benachrichtigungen",
|
||||
"success": "Erfolg",
|
||||
"telephone": "Telefonnummer",
|
||||
"telephonePlaceholder": "Telefonnummer eingeben",
|
||||
"time": "Zeit",
|
||||
"token": "Token",
|
||||
"totalTraffic": "Gesamtverkehr",
|
||||
"tradeNotifications": "Handelsbenachrichtigungen",
|
||||
"trafficLimit": "Verkehrslimit",
|
||||
"trafficLogs": "Verkehrsprotokolle",
|
||||
"unlimited": "Unbegrenzt",
|
||||
"unverified": "Unbestätigt",
|
||||
"update": "Aktualisieren",
|
||||
"updateSuccess": "Aktualisierung erfolgreich",
|
||||
"upload": "Hochladen",
|
||||
"uploadTraffic": "Datenverkehr hochladen",
|
||||
"userAgent": "Benutzeragent",
|
||||
"userEmail": "Benutzer-E-Mail",
|
||||
"userEmailPlaceholder": "Benutzer-E-Mail eingeben",
|
||||
"userList": "Benutzerliste",
|
||||
"userName": "Benutzerkennung"
|
||||
"userLogs": "Anmeldeverlauf",
|
||||
"userName": "Benutzerkennung",
|
||||
"userOrders": "Bestellungen",
|
||||
"userProfile": "Profil",
|
||||
"userSubscriptions": "Abonnements",
|
||||
"verified": "Verifiziert"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"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",
|
||||
"redirectUri": "Redirect URL",
|
||||
"redirectUriDescription": "Please fill in the API address of the redirected URL after successfully passing Apple authentication. Do not use / at the end.",
|
||||
"saveFailed": "Save failed",
|
||||
"saveSuccess": "Save successful",
|
||||
"teamId": "Team ID",
|
||||
|
||||
@@ -1,13 +1,74 @@
|
||||
{
|
||||
"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"
|
||||
"invite": {
|
||||
"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",
|
||||
"enableForcedInvite": "Enable Forced Invite",
|
||||
"enableForcedInviteDescription": "When enabled, only invited users can register",
|
||||
"inputPlaceholder": "Enter",
|
||||
"inviteCommissionPercentage": "Invite Commission Percentage",
|
||||
"inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management",
|
||||
"inviteSettings": "Invite Settings",
|
||||
"saveSuccess": "Save Successful"
|
||||
},
|
||||
"log": {
|
||||
"content": "Content",
|
||||
"createdAt": "Created At",
|
||||
"emailLog": "Email Log",
|
||||
"mobileLog": "Mobile Log",
|
||||
"platform": "Platform",
|
||||
"sendFailed": "Failed",
|
||||
"sendSuccess": "Success",
|
||||
"status": "Status",
|
||||
"subject": "Subject",
|
||||
"to": "Recipient",
|
||||
"updatedAt": "Updated At"
|
||||
},
|
||||
"register": {
|
||||
"day": "Day",
|
||||
"hour": "Hour",
|
||||
"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",
|
||||
"minute": "Minute",
|
||||
"month": "Month",
|
||||
"noLimit": "No Limit",
|
||||
"penaltyTime": "Penalty Time (minutes)",
|
||||
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
|
||||
"registerSettings": "Register Settings",
|
||||
"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",
|
||||
"trialDuration": "Trial Duration",
|
||||
"trialRegistration": "Trial Registration",
|
||||
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first",
|
||||
"trialSubscribePlan": "Trial Subscription Plan",
|
||||
"trialSubscribePlanDescription": "Select trial subscription plan",
|
||||
"year": "Year"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Enter",
|
||||
"loginVerificationCode": "Login Verification Code",
|
||||
"loginVerificationCodeDescription": "Human verification during login",
|
||||
"registrationVerificationCode": "Registration Verification Code",
|
||||
"registrationVerificationCodeDescription": "Human verification during registration",
|
||||
"resetPasswordVerificationCode": "Reset Password Verification Code",
|
||||
"resetPasswordVerificationCodeDescription": "Human verification during password reset",
|
||||
"saveSuccess": "Save Successful",
|
||||
"turnstileSecretDescription": "Turnstile secret key provided by Cloudflare",
|
||||
"turnstileSiteKeyDescription": "Turnstile site key provided by Cloudflare",
|
||||
"verifySettings": "Verify Settings"
|
||||
},
|
||||
"verify-code": {
|
||||
"dailyLimit": "Daily Limit",
|
||||
"dailyLimitDescription": "Maximum number of verification codes that can be sent per day",
|
||||
"expireTime": "Expire Time",
|
||||
"expireTimeDescription": "Verification code expiration time (seconds)",
|
||||
"interval": "Send Interval",
|
||||
"intervalDescription": "Minimum interval between sending verification codes (seconds)",
|
||||
"saveSuccess": "Save Successful",
|
||||
"second": "seconds",
|
||||
"times": "times",
|
||||
"verifyCodeSettings": "Verification Code Settings"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"logout": "Logout",
|
||||
"methods": {
|
||||
"email": "Email",
|
||||
"sms": "Telephone"
|
||||
"mobile": "Telephone"
|
||||
},
|
||||
"orWithEmail": "Or with Email",
|
||||
"register": {
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
"60002": "Unable to use the subscription at the moment, please try again later.",
|
||||
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
||||
"60004": "Unable to delete at the moment as the subscription has active users.",
|
||||
"60005": "Single subscription mode has exceeded user limit",
|
||||
"70001": "Incorrect verification code, please re-enter.",
|
||||
"80001": "Task was not successfully queued, please try again later.",
|
||||
"90001": "Please disable DEBUG mode and try again.",
|
||||
"90015": "This account has reached the limit of sending times today, please try again tomorrow.",
|
||||
"undefined": "An error occurred in the system, please try again later."
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"enable": "Enable",
|
||||
"enableDescription": "After enabling, multiple device identifiers such as IMEI/IDFA/IDFV/AndroidID/Mac address are supported for login and registration"
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"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",
|
||||
"enableDescription": "After enabling, email 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...",
|
||||
"logs": "Logs",
|
||||
"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",
|
||||
@@ -27,6 +25,7 @@
|
||||
"senderAddressDescription": "The default email address used for sending emails.",
|
||||
"sent": "Sent",
|
||||
"sentAt": "Sent At",
|
||||
"settings": "Settings",
|
||||
"smtpAccount": "SMTP Account",
|
||||
"smtpAccountDescription": "The email account used for authentication.",
|
||||
"smtpEncryptionMethod": "SMTP Encryption Method",
|
||||
@@ -39,6 +38,7 @@
|
||||
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
|
||||
"status": "Status",
|
||||
"subject": "Subject",
|
||||
"template": "Template",
|
||||
"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",
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"Auth Control": "Auth Control",
|
||||
"Coupon Management": "Coupon Management",
|
||||
"Dashboard": "Dashboard",
|
||||
"Device": "Device",
|
||||
"Document Management": "Document Management",
|
||||
"Email": "Email",
|
||||
"Facebook": "Facebook",
|
||||
@@ -24,5 +25,6 @@
|
||||
"Ticket Management": "Ticket Management",
|
||||
"Twitter": "Twitter",
|
||||
"User": "User",
|
||||
"User Detail": "User Detail",
|
||||
"User Management": "User Management"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
"stripe_wechat_pay": "Stripe (WeChat)"
|
||||
},
|
||||
"orderNumber": "Order Number",
|
||||
"status": "Order Status",
|
||||
"status": {
|
||||
"0": "Status",
|
||||
"1": "Pending",
|
||||
"2": "Paid",
|
||||
"3": "Cancelled",
|
||||
"4": "Closed",
|
||||
"5": "Completed"
|
||||
},
|
||||
"subscribe": "Subscribe",
|
||||
"subscribePrice": "Subscription Price",
|
||||
"total": "Total",
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
{
|
||||
"SmsList": "SMS Log List",
|
||||
"accessLabel": "Access",
|
||||
"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",
|
||||
"endpointLabel": "Endpoint",
|
||||
"logs": "Logs",
|
||||
"phoneNumberLabel": "Phone Number",
|
||||
"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",
|
||||
"secretLabel": "Secret",
|
||||
"sendFailed": "Send Failed",
|
||||
"sendSuccess": "Send Success",
|
||||
"settings": "Settings",
|
||||
"status": "Status",
|
||||
"telephone": "Phone Number",
|
||||
"signNameLabel": "Sign Name",
|
||||
"template": "SMS Template",
|
||||
"templateCode": "Template Code",
|
||||
"templateParam": "Template Parameter",
|
||||
"templateCodeLabel": "Template Code",
|
||||
"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"
|
||||
"testSmsTip": "Send a test message to verify your configuration",
|
||||
"updateSuccess": "Update Success",
|
||||
"whitelistAreaCode": "Whitelist Area Code",
|
||||
"whitelistAreaCodeTip": "Please enter whitelist area codes, e.g., 1, 852, 886, 888",
|
||||
"whitelistValidation": "Whitelist Verification",
|
||||
"whitelistValidationTip": "When enabled, only area codes in the whitelist can send SMS"
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"subscriptionPath": "Subscription Path",
|
||||
"subscriptionPathDescription": "Used for subscription; be sure to restart the system after modification for optimal performance",
|
||||
"subscriptionPathPlaceholder": "Enter",
|
||||
"updateSuccess": "Updated successfully",
|
||||
"wildcardResolution": "Wildcard Resolution",
|
||||
"wildcardResolutionDescription": "Used for subscription"
|
||||
},
|
||||
|
||||
@@ -8,17 +8,11 @@
|
||||
"currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system",
|
||||
"saveSuccess": "Save Successful"
|
||||
},
|
||||
"invite": {
|
||||
"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",
|
||||
"enableForcedInvite": "Enable Forced Invite",
|
||||
"enableForcedInviteDescription": "When enabled, only invited users can register",
|
||||
"inputPlaceholder": "Enter",
|
||||
"inviteCommissionPercentage": "Invite Commission Percentage",
|
||||
"inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management",
|
||||
"saveSuccess": "Save Successful"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Custom HTML",
|
||||
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's <body> tag.",
|
||||
"keywords": "Keywords",
|
||||
"keywordsDescription": "Used for SEO purposes",
|
||||
"logo": "Logo",
|
||||
"logoDescription": "Used for displaying the logo in designated locations",
|
||||
"logoPlaceholder": "Enter the URL of the logo, without ending with '/'",
|
||||
@@ -35,42 +29,11 @@
|
||||
},
|
||||
"tabs": {
|
||||
"currency": "Currency",
|
||||
"invite": "Invite",
|
||||
"site": "Site",
|
||||
"telegram": "Telegram",
|
||||
"tos": "Terms of Service",
|
||||
"verify": "Verify"
|
||||
},
|
||||
"telegram": {
|
||||
"botToken": "Bot Token",
|
||||
"botTokenDescription": "Enter the token provided by Botfather",
|
||||
"enableBotNotifications": "Enable Bot Notifications",
|
||||
"enableBotNotificationsDescription": "When enabled, the bot will send basic notifications to linked Telegram admins and users",
|
||||
"groupURL": "Group URL",
|
||||
"groupURLDescription": "If filled, will be displayed to users or used in required locations",
|
||||
"inputPlaceholderBotToken": "0000000000:xxxxxxxxx_xxxxxxxxxxxxxxx",
|
||||
"inputPlaceholderGroupURL": "https://t.me/xxxxxx",
|
||||
"inputPlaceholderWebhookDomain": "https://example.com",
|
||||
"saveSuccess": "Save Successful",
|
||||
"webhookDomain": "Webhook Domain",
|
||||
"webhookDomainDescription": "Enter the domain name of the webhook server"
|
||||
"tos": "Terms of Service"
|
||||
},
|
||||
"tos": {
|
||||
"saveSuccess": "Save Successful",
|
||||
"title": "Terms of Service"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Enter",
|
||||
"loginVerificationCode": "Login Verification Code",
|
||||
"loginVerificationCodeDescription": "Human verification during login",
|
||||
"registrationVerificationCode": "Registration Verification Code",
|
||||
"registrationVerificationCodeDescription": "Human verification during registration",
|
||||
"resetPasswordVerificationCode": "Reset Password Verification Code",
|
||||
"resetPasswordVerificationCodeDescription": "Human verification during password reset",
|
||||
"saveSuccess": "Save Successful",
|
||||
"turnstileSecret": "Turnstile Secret Key",
|
||||
"turnstileSecretDescription": "Turnstile secret key provided by Cloudflare",
|
||||
"turnstileSiteKey": "Turnstile Site Key",
|
||||
"turnstileSiteKeyDescription": "Turnstile site key provided by Cloudflare"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,50 +1,108 @@
|
||||
{
|
||||
"accountEnable": "Account Enable",
|
||||
"actions": "Actions",
|
||||
"add": "Add",
|
||||
"administrator": "Administrator",
|
||||
"areaCode": "Area Code",
|
||||
"areaCodePlaceholder": "Area code",
|
||||
"auth": "Auth",
|
||||
"authIdentifier": "Auth Identifier",
|
||||
"authIdentifierPlaceholder": "Enter auth identifier",
|
||||
"authMethodsTitle": "Authentication Settings",
|
||||
"authType": "Auth Type",
|
||||
"authUser": "Auth User",
|
||||
"avatar": "Avatar",
|
||||
"balance": "Balance",
|
||||
"balanceNotifications": "Balance Change Notifications",
|
||||
"balancePlaceholder": "Balance",
|
||||
"basicInfoTitle": "Basic Information",
|
||||
"cancel": "Cancel",
|
||||
"commission": "Commission",
|
||||
"commissionPlaceholder": "Enter commission",
|
||||
"confirm": "Confirm",
|
||||
"confirmDelete": "Are you sure you want to delete?",
|
||||
"confirmDelete": "Confirm Delete",
|
||||
"confirmOffline": "Confirm Offline",
|
||||
"create": "Create",
|
||||
"createSubscription": "Create Subscription",
|
||||
"createSuccess": "Create successful",
|
||||
"createUser": "Create User",
|
||||
"createdAt": "Created At",
|
||||
"delete": "Delete",
|
||||
"deleteDescription": "Data cannot be recovered after deletion, please proceed with caution.",
|
||||
"deleteSubscriptionDescription": "Are you sure to delete this subscription?",
|
||||
"deleteSuccess": "Delete successful",
|
||||
"detail": "Detail",
|
||||
"deviceLimit": "Device Limit",
|
||||
"download": "Download",
|
||||
"downloadTraffic": "Download Traffic",
|
||||
"edit": "Edit",
|
||||
"editSubscription": "Edit Subscription",
|
||||
"editUser": "Edit User",
|
||||
"email": "Email",
|
||||
"enable": "Enable",
|
||||
"form": {
|
||||
"areaCode": "Area Code",
|
||||
"areaCodePlaceholder": "Area code",
|
||||
"balance": "Balance",
|
||||
"balancePlaceholder": "Balance",
|
||||
"cancel": "Cancel",
|
||||
"commission": "Commission",
|
||||
"commissionPlaceholder": "Enter commission",
|
||||
"confirm": "Confirm",
|
||||
"giftAmount": "Gift Amount",
|
||||
"giftAmountPlaceholder": "Enter gift amount",
|
||||
"invalidEmailFormat": "Invalid email format",
|
||||
"inviteCode": "Invite Code",
|
||||
"inviteCodePlaceholder": "Enter invite code (leave blank to generate)",
|
||||
"manager": "Manager",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter new password (optional)",
|
||||
"refererId": "Referer ID",
|
||||
"refererIdPlaceholder": "Enter referer ID",
|
||||
"telephone": "Phone Number",
|
||||
"telephonePlaceholder": "Enter phone number",
|
||||
"userEmail": "User Email",
|
||||
"userEmailPlaceholder": "Enter user email"
|
||||
},
|
||||
"expireTime": "Expire Time",
|
||||
"expiredAt": "Expired At",
|
||||
"failed": "Failed",
|
||||
"giftAmount": "Gift Amount",
|
||||
"giftAmountPlaceholder": "Enter gift amount",
|
||||
"invalidEmailFormat": "Invalid email format",
|
||||
"inviteCode": "Invite Code",
|
||||
"inviteCodePlaceholder": "Enter invite code (leave blank to generate)",
|
||||
"kickOfflineConfirm": "Are you sure to kick offline IP {ip}?",
|
||||
"kickOfflineSuccess": "Successfully notified offline",
|
||||
"lastSeen": "Last Seen",
|
||||
"loading": "Loading...",
|
||||
"loginIp": "Login IP",
|
||||
"loginNotifications": "Login Notifications",
|
||||
"loginStatus": "Login Status",
|
||||
"loginTime": "Login Time",
|
||||
"manager": "Manager",
|
||||
"notifySettingsTitle": "Notification Settings",
|
||||
"onlineDevices": "Online Devices",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter new password (optional)",
|
||||
"permanent": "Permanent",
|
||||
"pleaseEnterEmail": "Please enter email",
|
||||
"referer": "Referer",
|
||||
"refererId": "Referer ID",
|
||||
"refererIdPlaceholder": "Enter referer ID",
|
||||
"referralCode": "Referral Code",
|
||||
"referrerUserId": "Referrer (User ID)",
|
||||
"remove": "Remove",
|
||||
"resetTime": "Reset Time",
|
||||
"save": "Save",
|
||||
"searchIp": "Search IP address",
|
||||
"selectAuthType": "Select auth type",
|
||||
"speedLimit": "Speed Limit",
|
||||
"subscription": "Subscription",
|
||||
"subscriptionDetails": "Subscription Details",
|
||||
"subscriptionList": "Subscription List",
|
||||
"subscriptionLogs": "Subscription Logs",
|
||||
"subscriptionName": "Subscription Name",
|
||||
"subscriptionNotifications": "Subscription Notifications",
|
||||
"success": "Success",
|
||||
"telephone": "Phone Number",
|
||||
"telephonePlaceholder": "Enter phone number",
|
||||
"time": "Time",
|
||||
"token": "Token",
|
||||
"totalTraffic": "Total Traffic",
|
||||
"tradeNotifications": "Trade Notifications",
|
||||
"trafficLimit": "Traffic Limit",
|
||||
"trafficLogs": "Traffic Logs",
|
||||
"unlimited": "Unlimited",
|
||||
"unverified": "Unverified",
|
||||
"update": "Update",
|
||||
"updateSuccess": "Update successful",
|
||||
"upload": "Upload",
|
||||
"uploadTraffic": "Upload Traffic",
|
||||
"userAgent": "User Agent",
|
||||
"userEmail": "User Email",
|
||||
"userEmailPlaceholder": "Enter user email",
|
||||
"userList": "User List",
|
||||
"userName": "User Identifier"
|
||||
"userLogs": "Login History",
|
||||
"userName": "User Identifier",
|
||||
"userOrders": "Orders",
|
||||
"userProfile": "Profile",
|
||||
"userSubscriptions": "Subscriptions",
|
||||
"verified": "Verified"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su ID de Apple",
|
||||
"keyId": "ID de clave",
|
||||
"keyIdDescription": "El ID de tu clave privada del Portal de Desarrolladores de Apple",
|
||||
"redirectUri": "URL de retorno",
|
||||
"redirectUriDescription": "La URL a la que Apple redirigirá después de una autenticación exitosa",
|
||||
"redirectUri": "URL de redirección",
|
||||
"redirectUriDescription": "Por favor, complete la dirección API de la URL redirigida después de pasar con éxito la autenticación de Apple. No use / al final.",
|
||||
"saveFailed": "Error al guardar",
|
||||
"saveSuccess": "Guardado exitoso",
|
||||
"teamId": "ID del equipo",
|
||||
|
||||
@@ -1,13 +1,74 @@
|
||||
{
|
||||
"ipRegistrationLimit": "Límite de Registro de IP",
|
||||
"ipRegistrationLimitDescription": "Cuando está habilitado, las IPs que cumplan con los requisitos de la regla serán restringidas de registrarse; tenga en cuenta que la determinación de IP puede causar problemas debido a CDNs o proxies frontend",
|
||||
"penaltyTime": "Tiempo de Penalización (minutos)",
|
||||
"penaltyTimeDescription": "Los usuarios deben esperar a que expire el tiempo de penalización antes de registrarse nuevamente",
|
||||
"registrationLimitCount": "Límite de Registro",
|
||||
"registrationLimitCountDescription": "Habilitar penalización después de alcanzar el límite de registro",
|
||||
"saveSuccess": "Guardado Exitoso",
|
||||
"stopNewUserRegistration": "Detener el registro de nuevos usuarios",
|
||||
"stopNewUserRegistrationDescription": "Cuando está habilitado, nadie puede registrarse",
|
||||
"trialRegistration": "Registro de Prueba",
|
||||
"trialRegistrationDescription": "Habilitar el registro de prueba; modifique primero el paquete y la duración de la prueba"
|
||||
"invite": {
|
||||
"commissionFirstTimeOnly": "Comisión Solo por la Primera Compra",
|
||||
"commissionFirstTimeOnlyDescription": "Cuando está habilitado, la comisión se genera solo en el primer pago del invitador; puedes configurar usuarios individuales en la gestión de usuarios",
|
||||
"enableForcedInvite": "Habilitar Invitación Forzada",
|
||||
"enableForcedInviteDescription": "Cuando está habilitado, solo los usuarios invitados pueden registrarse",
|
||||
"inputPlaceholder": "Introducir",
|
||||
"inviteCommissionPercentage": "Porcentaje de Comisión por Invitación",
|
||||
"inviteCommissionPercentageDescription": "Proporción de distribución de comisión global por defecto; puedes configurar proporciones individuales en la gestión de usuarios",
|
||||
"inviteSettings": "Configuración de Invitaciones",
|
||||
"saveSuccess": "Guardado Exitoso"
|
||||
},
|
||||
"log": {
|
||||
"content": "Contenido",
|
||||
"createdAt": "Creado En",
|
||||
"emailLog": "Registro de Correo Electrónico",
|
||||
"mobileLog": "Registro Móvil",
|
||||
"platform": "Plataforma",
|
||||
"sendFailed": "Fallido",
|
||||
"sendSuccess": "Éxito",
|
||||
"status": "Estado",
|
||||
"subject": "Asunto",
|
||||
"to": "Destinatario",
|
||||
"updatedAt": "Actualizado En"
|
||||
},
|
||||
"register": {
|
||||
"day": "Día",
|
||||
"hour": "Hora",
|
||||
"ipRegistrationLimit": "Límite de Registro por IP",
|
||||
"ipRegistrationLimitDescription": "Cuando está habilitado, las IP que cumplan con los requisitos de la regla estarán restringidas para registrarse; ten en cuenta que la determinación de IP puede causar problemas debido a CDNs o proxies de frontend",
|
||||
"minute": "Minuto",
|
||||
"month": "Mes",
|
||||
"noLimit": "Sin Límite",
|
||||
"penaltyTime": "Tiempo de Penalización (minutos)",
|
||||
"penaltyTimeDescription": "Los usuarios deben esperar a que expire el tiempo de penalización antes de registrarse nuevamente",
|
||||
"registerSettings": "Configuración de Registro",
|
||||
"registrationLimitCount": "Conteo de Límite de Registro",
|
||||
"registrationLimitCountDescription": "Habilitar penalización después de alcanzar el límite de registro",
|
||||
"saveSuccess": "Guardado Exitoso",
|
||||
"stopNewUserRegistration": "Detener Registro de Nuevos Usuarios",
|
||||
"stopNewUserRegistrationDescription": "Cuando está habilitado, nadie puede registrarse",
|
||||
"trialDuration": "Duración de la Prueba",
|
||||
"trialRegistration": "Registro de Prueba",
|
||||
"trialRegistrationDescription": "Habilitar registro de prueba; modifica el paquete de prueba y la duración primero",
|
||||
"trialSubscribePlan": "Plan de Suscripción de Prueba",
|
||||
"trialSubscribePlanDescription": "Selecciona el plan de suscripción de prueba",
|
||||
"year": "Año"
|
||||
},
|
||||
"verify": {
|
||||
"inputPlaceholder": "Introducir",
|
||||
"loginVerificationCode": "Código de Verificación de Inicio de Sesión",
|
||||
"loginVerificationCodeDescription": "Verificación humana durante el inicio de sesión",
|
||||
"registrationVerificationCode": "Código de Verificación de Registro",
|
||||
"registrationVerificationCodeDescription": "Verificación humana durante el registro",
|
||||
"resetPasswordVerificationCode": "Código de Verificación para Restablecer Contraseña",
|
||||
"resetPasswordVerificationCodeDescription": "Verificación humana durante el restablecimiento de contraseña",
|
||||
"saveSuccess": "Guardado Exitoso",
|
||||
"turnstileSecretDescription": "Clave secreta de Turnstile proporcionada por Cloudflare",
|
||||
"turnstileSiteKeyDescription": "Clave del sitio de Turnstile proporcionada por Cloudflare",
|
||||
"verifySettings": "Configuración de Verificación"
|
||||
},
|
||||
"verify-code": {
|
||||
"dailyLimit": "Límite Diario",
|
||||
"dailyLimitDescription": "Número máximo de códigos de verificación que se pueden enviar por día",
|
||||
"expireTime": "Tiempo de Expiración",
|
||||
"expireTimeDescription": "Tiempo de expiración del código de verificación (segundos)",
|
||||
"interval": "Intervalo de Envío",
|
||||
"intervalDescription": "Intervalo mínimo entre el envío de códigos de verificación (segundos)",
|
||||
"saveSuccess": "Guardado Exitoso",
|
||||
"second": "segundos",
|
||||
"times": "veces",
|
||||
"verifyCodeSettings": "Configuración del Código de Verificación"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"logout": "Cerrar sesión",
|
||||
"methods": {
|
||||
"email": "Correo electrónico",
|
||||
"sms": "Teléfono"
|
||||
"mobile": "Teléfono"
|
||||
},
|
||||
"orWithEmail": "o con correo electrónico",
|
||||
"register": {
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
"60002": "No se puede usar la suscripción por el momento, por favor intente más tarde.",
|
||||
"60003": "Se ha detectado una suscripción existente. Por favor, cancélala antes de continuar.",
|
||||
"60004": "No se puede eliminar en este momento ya que la suscripción tiene usuarios activos.",
|
||||
"60005": "El modo de suscripción única ha superado el límite de usuarios",
|
||||
"70001": "El código de verificación es incorrecto, por favor ingréselo nuevamente.",
|
||||
"80001": "La tarea no se agregó exitosamente a la cola, por favor intente de nuevo más tarde.",
|
||||
"90001": "Por favor desactive el modo DEBUG e intente nuevamente.",
|
||||
"90015": "Esta cuenta ha alcanzado el límite de envíos de hoy, inténtelo de nuevo mañana",
|
||||
"undefined": "Ocurrió un error en el sistema, por favor intente más tarde."
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"enable": "Habilitar",
|
||||
"enableDescription": "Después de habilitar, se admiten múltiples identificadores de dispositivos como IMEI/IDFA/IDFV/AndroidID/dirección MAC para el inicio de sesión y el registro"
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"emailBasicConfig": "Configuración Básica",
|
||||
"emailBasicConfigDescription": "Configura los ajustes del servidor SMTP y las opciones de verificación de correo electrónico",
|
||||
"emailLogs": "Registros de Correo Electrónico",
|
||||
"emailLogsDescription": "Ver el historial de correos electrónicos enviados y su estado",
|
||||
"emailSuffixWhitelist": "Lista blanca de sufijos de correo electrónico",
|
||||
"emailSuffixWhitelistDescription": "Cuando está habilitado, solo los correos electrónicos con sufijos en la lista pueden registrarse",
|
||||
"emailTemplate": "Plantilla de correo electrónico",
|
||||
"emailVerification": "Verificación de correo electrónico",
|
||||
"emailVerificationDescription": "Cuando esté habilitado, los usuarios deberán verificar su correo electrónico",
|
||||
"enable": "Habilitar",
|
||||
"enableDescription": "Después de habilitar, se activarán las funciones de registro, inicio de sesión, vinculación y desvinculación de teléfonos móviles",
|
||||
"enableDescription": "Después de habilitar, se activarán las funciones de registro por correo electrónico, inicio de sesión, vinculación y desvinculación.",
|
||||
"expiration_email_template": "Plantilla de Aviso de Vencimiento",
|
||||
"expiration_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
|
||||
"failed": "Fallido",
|
||||
"inputPlaceholder": "Ingrese valor...",
|
||||
"logs": "Registros",
|
||||
"maintenance_email_template": "Plantilla de Aviso de Mantenimiento",
|
||||
"maintenance_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrese de mantener estas variables.",
|
||||
"recipient": "Destinatario",
|
||||
@@ -27,6 +25,7 @@
|
||||
"senderAddressDescription": "La dirección de correo electrónico predeterminada utilizada para enviar correos electrónicos.",
|
||||
"sent": "Enviado",
|
||||
"sentAt": "Enviado A",
|
||||
"settings": "Configuración",
|
||||
"smtpAccount": "Cuenta SMTP",
|
||||
"smtpAccountDescription": "La cuenta de correo electrónico utilizada para la autenticación.",
|
||||
"smtpEncryptionMethod": "Método de Cifrado SMTP",
|
||||
@@ -39,6 +38,7 @@
|
||||
"smtpServerPortDescription": "Especifique el puerto utilizado para conectarse al servidor SMTP.",
|
||||
"status": "Estado",
|
||||
"subject": "Asunto",
|
||||
"template": "Plantilla",
|
||||
"verify_email_template": "Plantilla de Correo Electrónico de Verificación",
|
||||
"verify_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrese de mantener estas variables.",
|
||||
"whitelistSuffixes": "Sufijos de la lista blanca",
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"clientSecretDescription": "Secreto de la aplicación de Facebook desde la Consola de Desarrolladores de Facebook",
|
||||
"enable": "Habilitar",
|
||||
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su cuenta de Facebook",
|
||||
"redirectUri": "URI de redirección autorizada",
|
||||
"redirectUriDescription": "La URL a la que se redirigirá al usuario después de la autenticación de Facebook",
|
||||
"saveFailed": "Error al guardar",
|
||||
"saveSuccess": "Guardado exitoso"
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
"clientSecretDescription": "El secreto del cliente de la configuración de tu aplicación OAuth de GitHub",
|
||||
"enable": "Habilitar la autenticación de GitHub",
|
||||
"enableDescription": "Permitir a los usuarios iniciar sesión con sus cuentas de GitHub",
|
||||
"redirectUri": "URL de redirección de autorización",
|
||||
"redirectUriDescription": "La URL en tu aplicación a la que los usuarios serán redirigidos después de la autenticación de GitHub",
|
||||
"saveFailed": "Error al guardar la configuración de GitHub",
|
||||
"saveSuccess": "Configuración de GitHub guardada con éxito"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user