Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7023875548 | |||
| a7aa5fee3e | |||
| 64cd842926 | |||
| 8b43e69bfe | |||
| ef153747bd | |||
| 328838d754 | |||
| 423b24077f | |||
| fa2fb2864f | |||
| 8d49daca21 | |||
| cf5c39cfe5 | |||
| 383638f702 | |||
| 5939763b57 | |||
| fa56eb8293 | |||
| 6a34bfb78d | |||
| b8f4f1e694 | |||
| aa1d42651d | |||
| 0c8f0911c7 | |||
| db0d9e003e | |||
| f219c52306 | |||
| 3f7b6d16ed | |||
| 6dbebd152d | |||
| bb3bd7b50c | |||
| fbad3b0e51 | |||
| e551232564 | |||
| bf58f25072 | |||
| 85f55def2e | |||
| 29bc3c7722 | |||
| f72df3a5e8 | |||
| 48a1b97051 | |||
| 786ba0e41c | |||
| ce3197298d | |||
| 092477b2d3 | |||
| b8316bba33 | |||
| dddc21c686 | |||
| d10ecc9fdf | |||
| 94822d902f | |||
| 85fdc36c14 | |||
| cfa3fc024d | |||
| c06ea49d6f | |||
| c3138a863d | |||
| 7fa3a57df4 | |||
| 70d6a38a29 | |||
| 5c710e1add | |||
| 6752420ba5 | |||
| 710947209a | |||
| fc0da761b5 | |||
| 136a1abd8b | |||
| a5e2079ddc | |||
| 35f92c9329 | |||
| f5d8fd3b65 | |||
| 0c907337e1 | |||
| e4630f8ca9 | |||
| 2215c7f2b9 | |||
| 3222016799 | |||
| d0a04679ce | |||
| d2732e650b | |||
| 79edea7973 | |||
| f17bf8db6e | |||
| 047a69886e | |||
| 71bf002370 | |||
| d0e6df044e | |||
| 8e16ef17ed | |||
| 2b80496637 | |||
| 01a3aa02ec | |||
| 35210fe8cf | |||
| d5847faeda | |||
| 0130e02ffd | |||
| c2b858e3fe | |||
| e11f18c034 | |||
| 2ca299224d |
+225
-19
@@ -1,33 +1,239 @@
|
|||||||
<a name="readme-top"></a>
|
<a name="readme-top"></a>
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
# [1.0.0-beta.23](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2025-02-24)
|
# [1.0.0-beta.34](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.33...v1.0.0-beta.34) (2025-04-02)
|
||||||
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* **auth**: Update email verification logic to use domain suffix check ([62662bb](https://github.com/perfect-panel/ppanel-web/commit/62662bb))
|
|
||||||
|
|
||||||
# [1.0.0-beta.22](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2025-02-23)
|
|
||||||
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* **locales**: Removed language file import to clean up unnecessary language support ([68f6ab2](https://github.com/perfect-panel/ppanel-web/commit/68f6ab2))
|
|
||||||
|
|
||||||
# [1.0.0-beta.21](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2025-02-23)
|
|
||||||
|
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
|
|
||||||
* **privacy-policy**: Add privacy policy related text and links ([baa68f0](https://github.com/perfect-panel/ppanel-web/commit/baa68f0))
|
* **admin**: Add application and rule management entries to localization files ([8b43e69](https://github.com/perfect-panel/ppanel-web/commit/8b43e69))
|
||||||
|
* **api**: Add an interface to obtain user subscription details, update related type definitions and localized text ([cf5c39c](https://github.com/perfect-panel/ppanel-web/commit/cf5c39c))
|
||||||
|
* **user**: Integrate subscription list into user management, update request parameters and types ([8d49dac](https://github.com/perfect-panel/ppanel-web/commit/8d49dac))
|
||||||
|
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
* **locales**: Removed multilingual files to clean up unnecessary language support ([5b151cd](https://github.com/perfect-panel/ppanel-web/commit/5b151cd))
|
* **admin**: Hidden versions and system upgrades ([64cd842](https://github.com/perfect-panel/ppanel-web/commit/64cd842))
|
||||||
* **locales**: Update custom HTML description in language file, ([87381da](https://github.com/perfect-panel/ppanel-web/commit/87381da))
|
* **admin**: Modify the label type in the rule form to a string array ([a7aa5fe](https://github.com/perfect-panel/ppanel-web/commit/a7aa5fe))
|
||||||
* **table**: Update privacy policy tab translation key and remove unnecessary requestType from OAuth callback ([14b3af5](https://github.com/perfect-panel/ppanel-web/commit/14b3af5))
|
* **node**: Handle potential null value for online users count ([fa2fb28](https://github.com/perfect-panel/ppanel-web/commit/fa2fb28))
|
||||||
|
* **subscribe**: Add value prop to field in subscription form for proper state management ([328838d](https://github.com/perfect-panel/ppanel-web/commit/328838d))
|
||||||
|
* **subscribe**: Refactor discount calculations and default selection logic in subscription forms ([423b240](https://github.com/perfect-panel/ppanel-web/commit/423b240))
|
||||||
|
* **subscribe**: Update default selection logic in subscription form to ensure proper state management ([ef15374](https://github.com/perfect-panel/ppanel-web/commit/ef15374))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.33](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.32...v1.0.0-beta.33) (2025-03-18)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **subscribe**: Handle optional values in price and discount calculations ([5939763](https://github.com/perfect-panel/ppanel-web/commit/5939763))
|
||||||
|
|
||||||
|
# [1.0.0-beta.32](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.31...v1.0.0-beta.32) (2025-03-17)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **forms**: Add step attribute to number inputs for better value control ([b8f4f1e](https://github.com/perfect-panel/ppanel-web/commit/b8f4f1e))
|
||||||
|
- **locales**: Update invite code text to indicate it's optional ([6a34bfb](https://github.com/perfect-panel/ppanel-web/commit/6a34bfb))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.31](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.30...v1.0.0-beta.31) (2025-03-15)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **site**: Se ref to store site configuration for updates ([0c8f091](https://github.com/perfect-panel/ppanel-web/commit/0c8f091))
|
||||||
|
|
||||||
|
# [1.0.0-beta.30](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.29...v1.0.0-beta.30) (2025-03-15)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **api**: Add CheckoutOrder request and response types, and update user purchase request parameters ([48a1b97](https://github.com/perfect-panel/ppanel-web/commit/48a1b97))
|
||||||
|
- **email**: Add traffic exhaustion template ([bb3bd7b](https://github.com/perfect-panel/ppanel-web/commit/bb3bd7b))
|
||||||
|
- **formatting**: Update differenceInDays function to return whole days or two decimal places ([bf58f25](https://github.com/perfect-panel/ppanel-web/commit/bf58f25))
|
||||||
|
- **global**: Add custom data ([6dbebd1](https://github.com/perfect-panel/ppanel-web/commit/6dbebd1))
|
||||||
|
- **input**: Add minimum value constraint and enhance number handling in EnhancedInput ([ce31972](https://github.com/perfect-panel/ppanel-web/commit/ce31972))
|
||||||
|
- **loading**: Replace loading animation with a simpler spinner and loading text ([f72df3a](https://github.com/perfect-panel/ppanel-web/commit/f72df3a))
|
||||||
|
- **node-form**: Update number input fields to enforce step, min, and max values ([3f7b6d1](https://github.com/perfect-panel/ppanel-web/commit/3f7b6d1))
|
||||||
|
- **payment**: Add isEdit prop to PaymentForm and disable fields when editing ([85f55de](https://github.com/perfect-panel/ppanel-web/commit/85f55de))
|
||||||
|
- **timeline**: Simplify timeline component layout and remove commented-out code ([fbad3b0](https://github.com/perfect-panel/ppanel-web/commit/fbad3b0))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- **release**: V1.0.0-beta.27 [skip ci] ([092477b](https://github.com/perfect-panel/ppanel-web/commit/092477b))
|
||||||
|
- **release**: V1.0.0-beta.28 [skip ci] ([786ba0e](https://github.com/perfect-panel/ppanel-web/commit/786ba0e))
|
||||||
|
- Merge branch 'beta' into develop ([f219c52](https://github.com/perfect-panel/ppanel-web/commit/f219c52))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **dashboard**: Update date display to use start_time if available ([e551232](https://github.com/perfect-panel/ppanel-web/commit/e551232))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.29](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.28...v1.0.0-beta.29) (2025-03-14)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **api**: Add CheckoutOrder request and response types, and update user purchase request parameters ([dddc21c](https://github.com/perfect-panel/ppanel-web/commit/dddc21c))
|
||||||
|
- **loading**: Replace loading animation with a simpler spinner and loading text ([b8316bb](https://github.com/perfect-panel/ppanel-web/commit/b8316bb))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.28](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.27...v1.0.0-beta.28) (2025-03-13)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **input**: Add minimum value constraint and enhance number handling in EnhancedInput ([94822d9](https://github.com/perfect-panel/ppanel-web/commit/94822d9))
|
||||||
|
|
||||||
|
# [1.0.0-beta.27](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.26...v1.0.0-beta.27) (2025-03-13)
|
||||||
|
|
||||||
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
|
- **payment**: Reconstruct the payment page ([7109472](https://github.com/perfect-panel/ppanel-web/commit/7109472))
|
||||||
|
- Enhance user navigation dropdown ui and styling ([d2732e6](https://github.com/perfect-panel/ppanel-web/commit/d2732e6))
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **cdn**: Add CDN URL configuration and update related references ([0c90733](https://github.com/perfect-panel/ppanel-web/commit/0c90733))
|
||||||
|
- **payment**: Add bank card payment ([7fa3a57](https://github.com/perfect-panel/ppanel-web/commit/7fa3a57))
|
||||||
|
- **subscription**: Improve layout and organization of subscription detail tabs ([e4630f8](https://github.com/perfect-panel/ppanel-web/commit/e4630f8))
|
||||||
|
- **subscription**: Refactor subscription handling and update imports for better organization ([2215c7f](https://github.com/perfect-panel/ppanel-web/commit/2215c7f))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- **merge**: Bump version to 1.0.0-beta.26 and update changelog ([3222016](https://github.com/perfect-panel/ppanel-web/commit/3222016))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **affiliate**: Update user identifier ([35f92c9](https://github.com/perfect-panel/ppanel-web/commit/35f92c9))
|
||||||
|
- **changelog**: Update change log style ([cfa3fc0](https://github.com/perfect-panel/ppanel-web/commit/cfa3fc0))
|
||||||
|
- **payment**: Add notification URL field to payment management interface ([5c710e1](https://github.com/perfect-panel/ppanel-web/commit/5c710e1))
|
||||||
|
- **payment**: Fix payment related type definitions and update payment method references ([c3138a8](https://github.com/perfect-panel/ppanel-web/commit/c3138a8))
|
||||||
|
- **payment**: Refactor purchaseCheckout usage and remove redundant code ([a5e2079](https://github.com/perfect-panel/ppanel-web/commit/a5e2079))
|
||||||
|
- **payment**: Update checkout type from 'link' to 'url' for consistency ([136a1ab](https://github.com/perfect-panel/ppanel-web/commit/136a1ab))
|
||||||
|
- **payment**: Update payment information ([70d6a38](https://github.com/perfect-panel/ppanel-web/commit/70d6a38))
|
||||||
|
- **payment**: Update payment method update logic to include row data ([6752420](https://github.com/perfect-panel/ppanel-web/commit/6752420))
|
||||||
|
- **purchasing**: Update payment type to lowercase and add optional chaining for discounts ([c06ea49](https://github.com/perfect-panel/ppanel-web/commit/c06ea49))
|
||||||
|
- **ui**: Improve dashboard layout and enhance button functionality; open checkout URLs in a new tab ([fc0da76](https://github.com/perfect-panel/ppanel-web/commit/fc0da76))
|
||||||
|
- **ui**: Multiple display bugs ([f5d8fd3](https://github.com/perfect-panel/ppanel-web/commit/f5d8fd3))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.28](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.27...v1.0.0-beta.28) (2025-03-13)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **input**: Add minimum value constraint and enhance number handling in EnhancedInput ([94822d9](https://github.com/perfect-panel/ppanel-web/commit/94822d9))
|
||||||
|
|
||||||
|
# [1.0.0-beta.27](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.26...v1.0.0-beta.27) (2025-03-13)
|
||||||
|
|
||||||
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
|
- **payment**: Reconstruct the payment page ([7109472](https://github.com/perfect-panel/ppanel-web/commit/7109472))
|
||||||
|
- Enhance user navigation dropdown ui and styling ([d2732e6](https://github.com/perfect-panel/ppanel-web/commit/d2732e6))
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **cdn**: Add CDN URL configuration and update related references ([0c90733](https://github.com/perfect-panel/ppanel-web/commit/0c90733))
|
||||||
|
- **payment**: Add bank card payment ([7fa3a57](https://github.com/perfect-panel/ppanel-web/commit/7fa3a57))
|
||||||
|
- **subscription**: Improve layout and organization of subscription detail tabs ([e4630f8](https://github.com/perfect-panel/ppanel-web/commit/e4630f8))
|
||||||
|
- **subscription**: Refactor subscription handling and update imports for better organization ([2215c7f](https://github.com/perfect-panel/ppanel-web/commit/2215c7f))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- **merge**: Bump version to 1.0.0-beta.26 and update changelog ([3222016](https://github.com/perfect-panel/ppanel-web/commit/3222016))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **affiliate**: Update user identifier ([35f92c9](https://github.com/perfect-panel/ppanel-web/commit/35f92c9))
|
||||||
|
- **changelog**: Update change log style ([cfa3fc0](https://github.com/perfect-panel/ppanel-web/commit/cfa3fc0))
|
||||||
|
- **payment**: Add notification URL field to payment management interface ([5c710e1](https://github.com/perfect-panel/ppanel-web/commit/5c710e1))
|
||||||
|
- **payment**: Fix payment related type definitions and update payment method references ([c3138a8](https://github.com/perfect-panel/ppanel-web/commit/c3138a8))
|
||||||
|
- **payment**: Refactor purchaseCheckout usage and remove redundant code ([a5e2079](https://github.com/perfect-panel/ppanel-web/commit/a5e2079))
|
||||||
|
- **payment**: Update checkout type from 'link' to 'url' for consistency ([136a1ab](https://github.com/perfect-panel/ppanel-web/commit/136a1ab))
|
||||||
|
- **payment**: Update payment information ([70d6a38](https://github.com/perfect-panel/ppanel-web/commit/70d6a38))
|
||||||
|
- **payment**: Update payment method update logic to include row data ([6752420](https://github.com/perfect-panel/ppanel-web/commit/6752420))
|
||||||
|
- **purchasing**: Update payment type to lowercase and add optional chaining for discounts ([c06ea49](https://github.com/perfect-panel/ppanel-web/commit/c06ea49))
|
||||||
|
- **ui**: Improve dashboard layout and enhance button functionality; open checkout URLs in a new tab ([fc0da76](https://github.com/perfect-panel/ppanel-web/commit/fc0da76))
|
||||||
|
- **ui**: Multiple display bugs ([f5d8fd3](https://github.com/perfect-panel/ppanel-web/commit/f5d8fd3))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.26](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.25...v1.0.0-beta.26) (2025-03-02)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **icon**: Comment out unused icon collection imports ([f17bf8d](https://github.com/perfect-panel/ppanel-web/commit/f17bf8d))
|
||||||
|
|
||||||
|
# [1.0.0-beta.25](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.24...v1.0.0-beta.25) (2025-03-01)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **auth**: Add privacy policy link to the footer ([8e16ef1](https://github.com/perfect-panel/ppanel-web/commit/8e16ef1))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **dashboard**: Display subscription creation date in user dashboard ([d0e6df0](https://github.com/perfect-panel/ppanel-web/commit/d0e6df0))
|
||||||
|
- **request**: Add error code 40005 to trigger logout ([71bf002](https://github.com/perfect-panel/ppanel-web/commit/71bf002))
|
||||||
|
- **subscribe**: Update payment return URL ([2b80496](https://github.com/perfect-panel/ppanel-web/commit/2b80496))
|
||||||
|
|
||||||
|
# [1.0.0-beta.24](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2025-02-27)
|
||||||
|
|
||||||
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
|
- **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992))
|
||||||
|
- Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c))
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **loading**: Add loading components and integrate them in Providers ([d5847fa](https://github.com/perfect-panel/ppanel-web/commit/d5847fa))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- **merge**: Add advertising module and device settings ([0130e02](https://github.com/perfect-panel/ppanel-web/commit/0130e02))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **locales**: Order recharge related fields ([35210fe](https://github.com/perfect-panel/ppanel-web/commit/35210fe))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.23](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2025-02-24)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **auth**: Update email verification logic to use domain suffix check ([62662bb](https://github.com/perfect-panel/ppanel-web/commit/62662bb))
|
||||||
|
|
||||||
|
# [1.0.0-beta.22](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2025-02-23)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **locales**: Removed language file import to clean up unnecessary language support ([68f6ab2](https://github.com/perfect-panel/ppanel-web/commit/68f6ab2))
|
||||||
|
|
||||||
|
# [1.0.0-beta.21](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2025-02-23)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **privacy-policy**: Add privacy policy related text and links ([baa68f0](https://github.com/perfect-panel/ppanel-web/commit/baa68f0))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **locales**: Removed multilingual files to clean up unnecessary language support ([5b151cd](https://github.com/perfect-panel/ppanel-web/commit/5b151cd))
|
||||||
|
- **locales**: Update custom HTML description in language file, ([87381da](https://github.com/perfect-panel/ppanel-web/commit/87381da))
|
||||||
|
- **table**: Update privacy policy tab translation key and remove unnecessary requestType from OAuth callback ([14b3af5](https://github.com/perfect-panel/ppanel-web/commit/14b3af5))
|
||||||
|
|
||||||
<a name="readme-top"></a>
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@workspace/ui/components/form';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
||||||
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@workspace/ui/components/sheet';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
type: z.enum(['image', 'video']),
|
||||||
|
content: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
target_url: z.string().url(),
|
||||||
|
start_time: z.number(),
|
||||||
|
end_time: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AdsFormProps<T> {
|
||||||
|
onSubmit: (data: T) => Promise<boolean> | boolean;
|
||||||
|
initialValues?: T;
|
||||||
|
loading?: boolean;
|
||||||
|
trigger: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdsForm<T extends Record<string, any>>({
|
||||||
|
onSubmit,
|
||||||
|
initialValues,
|
||||||
|
loading,
|
||||||
|
trigger,
|
||||||
|
title,
|
||||||
|
}: AdsFormProps<T>) {
|
||||||
|
const t = useTranslations('ads');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
...initialValues,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form?.reset(initialValues);
|
||||||
|
}, [form, initialValues]);
|
||||||
|
|
||||||
|
const type = form.watch('type');
|
||||||
|
const startTime = form.watch('start_time');
|
||||||
|
|
||||||
|
const renderContentField = () => {
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='content'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.content')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={
|
||||||
|
type === 'image'
|
||||||
|
? 'https://example.com/image.jpg'
|
||||||
|
: 'https://example.com/video.mp4'
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue('content', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleSubmit(data: { [x: string]: any }) {
|
||||||
|
const bool = await onSubmit(data as T);
|
||||||
|
if (bool) setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{title}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='title'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.title')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.enterTitle')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='type'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.type')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
className='flex gap-4'
|
||||||
|
>
|
||||||
|
<FormItem className='flex items-center space-x-3 space-y-0'>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value='image' />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className='font-normal'>{t('form.typeImage')}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem className='flex items-center space-x-3 space-y-0'>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value='video' />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className='font-normal'>{t('form.typeVideo')}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{renderContentField()}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='description'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.description')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.enterDescription')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='target_url'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.targetUrl')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.enterTargetUrl')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='start_time'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.startTime')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
type='datetime-local'
|
||||||
|
placeholder={t('form.enterStartTime')}
|
||||||
|
value={field.value ? new Date(field.value).toISOString().slice(0, 16) : ''}
|
||||||
|
min={Number(new Date().toISOString().slice(0, 16))}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const timestamp = value ? new Date(value).getTime() : 0;
|
||||||
|
form.setValue(field.name, timestamp);
|
||||||
|
const endTime = form.getValues('end_time');
|
||||||
|
if (endTime && timestamp > endTime) {
|
||||||
|
form.setValue('end_time', '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='end_time'
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.endTime')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
type='datetime-local'
|
||||||
|
placeholder={t('form.enterEndTime')}
|
||||||
|
value={
|
||||||
|
field.value ? new Date(field.value).toISOString().slice(0, 16) : ''
|
||||||
|
}
|
||||||
|
min={Number(
|
||||||
|
startTime
|
||||||
|
? new Date(startTime).toISOString().slice(0, 16)
|
||||||
|
: new Date().toISOString().slice(0, 16),
|
||||||
|
)}
|
||||||
|
disabled={!startTime}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const timestamp = value ? new Date(value).getTime() : 0;
|
||||||
|
if (!startTime || timestamp < startTime) return;
|
||||||
|
form.setValue(field.name, timestamp);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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('form.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
|
{t('form.confirm')}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import { createAds, deleteAds, getAdsList, updateAds } from '@/services/admin/ads';
|
||||||
|
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 { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import AdsForm from './ads-form';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('ads');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.Ads, Record<string, unknown>>
|
||||||
|
action={ref}
|
||||||
|
header={{
|
||||||
|
toolbar: (
|
||||||
|
<AdsForm<API.CreateAdsRequest>
|
||||||
|
trigger={t('create')}
|
||||||
|
title={t('createAds')}
|
||||||
|
loading={loading}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await createAds({
|
||||||
|
...values,
|
||||||
|
status: 0,
|
||||||
|
});
|
||||||
|
toast.success(t('createSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
placeholder: t('status'),
|
||||||
|
options: [
|
||||||
|
{ label: t('enabled'), value: '1' },
|
||||||
|
{ label: t('disabled'), value: '0' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filters) => {
|
||||||
|
const { data } = await getAdsList({
|
||||||
|
...pagination,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: data.data?.list || [],
|
||||||
|
total: data.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: t('status'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={row.getValue('status') === 1}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
await updateAds({
|
||||||
|
...row.original,
|
||||||
|
status: checked ? 1 : 0,
|
||||||
|
});
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'title',
|
||||||
|
header: t('title'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'type',
|
||||||
|
header: t('type'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const type = row.original.type;
|
||||||
|
return <Badge>{type}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'target_url',
|
||||||
|
header: t('targetUrl'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'description',
|
||||||
|
header: t('form.description'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'period',
|
||||||
|
header: t('validityPeriod'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { start_time, end_time } = row.original;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{formatDate(start_time)} - {formatDate(end_time)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
actions={{
|
||||||
|
render: (row) => [
|
||||||
|
<AdsForm<API.UpdateAdsRequest>
|
||||||
|
key='edit'
|
||||||
|
trigger={t('edit')}
|
||||||
|
title={t('editAds')}
|
||||||
|
loading={loading}
|
||||||
|
initialValues={row}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updateAds({ ...row, ...values });
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteAds({ id: row.id });
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+25
-13
@@ -232,16 +232,6 @@ export default function SubscribeAppForm<
|
|||||||
name: 'is_default',
|
name: 'is_default',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
placeholder: t('defaultVersion'),
|
placeholder: t('defaultVersion'),
|
||||||
calculateValue: (value) => {
|
|
||||||
const newField = field.value?.map((item) => {
|
|
||||||
if (item.is_default) {
|
|
||||||
item.is_default = false;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
form.setValue(field.name, newField);
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'url',
|
name: 'url',
|
||||||
@@ -252,10 +242,32 @@ export default function SubscribeAppForm<
|
|||||||
]}
|
]}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
form.setValue(
|
const filteredValue = value.filter((item) => item.url);
|
||||||
field.name,
|
|
||||||
value.filter((item) => item.url),
|
const newDefaultIndex = value.findIndex(
|
||||||
|
(item, idx) =>
|
||||||
|
item.is_default &&
|
||||||
|
(!field.value?.[idx] || !field.value[idx].is_default),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let finalValue = filteredValue;
|
||||||
|
|
||||||
|
if (newDefaultIndex >= 0 && filteredValue[newDefaultIndex]) {
|
||||||
|
finalValue = filteredValue.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
is_default: index === newDefaultIndex,
|
||||||
|
}));
|
||||||
|
} else if (
|
||||||
|
!filteredValue.some((item) => item.is_default) &&
|
||||||
|
filteredValue.length > 0
|
||||||
|
) {
|
||||||
|
finalValue = filteredValue.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
is_default: index === 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setValue(field.name, finalValue as any);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -5,7 +5,10 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { Label } from '@workspace/ui/components/label';
|
import { Label } from '@workspace/ui/components/label';
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { DicesIcon } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { uid } from 'radash';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -49,6 +52,89 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('showAds')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('showAdsDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.config?.show_ads}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
show_ads: checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('blockVirtualMachine')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('blockVirtualMachineDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.config?.only_real_device}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
only_real_device: checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enableSecurity')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableSecurityDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.config?.enable_security}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
enable_security: checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('communicationKey')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('communicationKeyDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
value={data?.config?.security_secret}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
security_secret: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
suffix={
|
||||||
|
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
|
||||||
|
<DicesIcon
|
||||||
|
onClick={() => {
|
||||||
|
const id = uid(32).toLowerCase();
|
||||||
|
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
security_secret: formatted,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className='cursor-pointer'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -286,31 +286,34 @@ export default function Page() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value='template'>
|
<TabsContent value='template'>
|
||||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
|
||||||
{['verify_email_template', 'expiration_email_template', 'maintenance_email_template'].map(
|
{[
|
||||||
(templateKey) => (
|
'verify_email_template',
|
||||||
<Card key={templateKey}>
|
'expiration_email_template',
|
||||||
<CardHeader>
|
'maintenance_email_template',
|
||||||
<CardTitle>{t(`${templateKey}`)}</CardTitle>
|
'traffic_exceed_email_template',
|
||||||
<CardDescription>
|
].map((templateKey) => (
|
||||||
{t(`${templateKey}Description`, { after: '{{', before: '}}' })}
|
<Card key={templateKey}>
|
||||||
</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{t(`${templateKey}`)}</CardTitle>
|
||||||
<CardContent>
|
<CardDescription>
|
||||||
<HTMLEditor
|
{t(`${templateKey}Description`, { after: '{{', before: '}}' })}
|
||||||
placeholder={t('inputPlaceholder')}
|
</CardDescription>
|
||||||
value={data?.config?.[templateKey] as string}
|
</CardHeader>
|
||||||
onBlur={(value) =>
|
<CardContent>
|
||||||
updateConfig('config', {
|
<HTMLEditor
|
||||||
...data?.config,
|
placeholder={t('inputPlaceholder')}
|
||||||
[templateKey]: value,
|
value={data?.config?.[templateKey] as string}
|
||||||
})
|
onBlur={(value) =>
|
||||||
}
|
updateConfig('config', {
|
||||||
/>
|
...data?.config,
|
||||||
</CardContent>
|
[templateKey]: value,
|
||||||
</Card>
|
})
|
||||||
),
|
}
|
||||||
)}
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ export default function CouponForm<T extends Record<string, any>>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
placeholder={t('form.countPlaceholder')}
|
placeholder={t('form.countPlaceholder')}
|
||||||
type='number'
|
type='number'
|
||||||
|
step={1}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
@@ -327,6 +328,7 @@ export default function CouponForm<T extends Record<string, any>>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
placeholder={t('form.userLimitPlaceholder')}
|
placeholder={t('form.userLimitPlaceholder')}
|
||||||
type='number'
|
type='number'
|
||||||
|
step={1}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function Page(props: any) {
|
|||||||
<ul className='grid gap-3'>
|
<ul className='grid gap-3'>
|
||||||
<li className='flex items-center justify-between'>
|
<li className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>{t('method')}</span>
|
<span className='text-muted-foreground'>{t('method')}</span>
|
||||||
<span>{t(`methods.${row.original.method}`)}</span>
|
<span>{row.original?.payment?.name || row.original?.payment?.platform}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { getAlipayF2FPaymentConfig, updateAlipayF2FPaymentConfig } from '@/services/admin/payment';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
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 { Textarea } from '@workspace/ui/components/textarea';
|
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function AlipayF2F() {
|
|
||||||
const t = useTranslations('payment');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getAlipayF2FPaymentConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getAlipayF2FPaymentConfig();
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateAlipayF2FPaymentConfig({
|
|
||||||
...data,
|
|
||||||
[key]: value,
|
|
||||||
} as API.UpdateAlipayF2fRequest);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.enable}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('enable', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('alipayf2f.sandbox')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('alipayf2f.sandboxDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.config.sandbox}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
sandbox: checked,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('showName')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.name}
|
|
||||||
onValueBlur={(value) => updateConfig('name', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('iconUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.icon_url}
|
|
||||||
onValueBlur={(value) => updateConfig('icon', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('notifyUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.domain}
|
|
||||||
onValueBlur={(value) => updateConfig('domain', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feeMode')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Select
|
|
||||||
value={String(data?.fee_mode)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateConfig('fee_mode', Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder='请选择' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
|
||||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
|
||||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
|
||||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feePercent')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
maxLength={3}
|
|
||||||
value={data?.fee_percent}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_percent', value)}
|
|
||||||
suffix='%'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('fixedFee')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
min={0}
|
|
||||||
value={data?.fee_amount}
|
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
|
||||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('alipayf2f.appId')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.app_id}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
app_id: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('alipayf2f.privateKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
defaultValue={data?.config.private_key}
|
|
||||||
onBlur={(e) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
private_key: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('alipayf2f.publicKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
defaultValue={data?.config.public_key}
|
|
||||||
onBlur={(e) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
public_key: e.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('alipayf2f.invoiceName')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('alipayf2f.invoiceNameDescription')}
|
|
||||||
value={data?.config.invoice_name}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
invoice_name: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { getEpayPaymentConfig, updateEpayPaymentConfig } from '@/services/admin/payment';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function Epay() {
|
|
||||||
const t = useTranslations('payment');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getEpayPaymentConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getEpayPaymentConfig();
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateEpayPaymentConfig({
|
|
||||||
...data,
|
|
||||||
[key]: value,
|
|
||||||
} as API.UpdateEpayRequest);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.enable}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('enable', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('showName')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.name}
|
|
||||||
onValueBlur={(value) => updateConfig('name', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('iconUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.icon}
|
|
||||||
onValueBlur={(value) => updateConfig('icon', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('notifyUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.domain}
|
|
||||||
onValueBlur={(value) => updateConfig('domain', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feeMode')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Select
|
|
||||||
value={String(data?.fee_mode)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateConfig('fee_mode', Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder='请选择' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
|
||||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
|
||||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
|
||||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feePercent')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
maxLength={3}
|
|
||||||
value={data?.fee_percent}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_percent', value)}
|
|
||||||
suffix='%'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('fixedFee')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
value={data?.fee_amount}
|
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
|
||||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('epay.url')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.url}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
url: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('epay.pid')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.pid}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
pid: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('epay.key')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.key}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
key: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,11 @@
|
|||||||
import Billing from '@/components/billing';
|
import Billing from '@/components/billing';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import PaymentTable from './payment-table';
|
||||||
import AlipayF2F from './alipayf2f';
|
|
||||||
import Epay from './epay';
|
|
||||||
import StripeAlipay from './stripe-alipay';
|
|
||||||
import StripeWeChatPay from './stripe-wechat-pay';
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs defaultValue='Epay'>
|
<PaymentTable />
|
||||||
<TabsList className='h-full flex-wrap'>
|
<div className='mt-5 flex flex-col gap-3'>
|
||||||
<TabsTrigger value='Epay'>Epay</TabsTrigger>
|
|
||||||
<TabsTrigger value='Stripe-Alipay'>Stripe(AliPay)</TabsTrigger>
|
|
||||||
<TabsTrigger value='Strip-WeChatPay'>Stripe(WeChatPay)</TabsTrigger>
|
|
||||||
<TabsTrigger value='AlipayF2F'>AlipayF2F</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value='Epay'>
|
|
||||||
<Epay />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='Stripe-Alipay'>
|
|
||||||
<StripeAlipay />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='Strip-WeChatPay'>
|
|
||||||
<StripeWeChatPay />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='AlipayF2F'>
|
|
||||||
<AlipayF2F />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
<div className='flex flex-col gap-3'>
|
|
||||||
<Billing type='payment' />
|
<Billing type='payment' />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import useGlobalStore from '@/config/use-global';
|
||||||
|
import { getPaymentPlatform } from '@/services/admin/payment';
|
||||||
|
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 { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
||||||
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@workspace/ui/components/select';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@workspace/ui/components/sheet';
|
||||||
|
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||||
|
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 { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
interface PaymentFormProps<T> {
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
loading?: boolean;
|
||||||
|
initialValues?: T;
|
||||||
|
onSubmit: (values: T) => Promise<boolean>;
|
||||||
|
isEdit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PaymentForm<T>({
|
||||||
|
trigger,
|
||||||
|
title,
|
||||||
|
loading,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
isEdit,
|
||||||
|
}: PaymentFormProps<T>) {
|
||||||
|
const t = useTranslations('payment');
|
||||||
|
const { common } = useGlobalStore();
|
||||||
|
const { currency } = common;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data: platformData } = useQuery({
|
||||||
|
queryKey: ['getPaymentPlatform'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getPaymentPlatform();
|
||||||
|
return data?.data?.list || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: t('nameRequired') }),
|
||||||
|
platform: z.string().optional(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
config: z.any(),
|
||||||
|
fee_mode: z.coerce.number().min(0).max(2),
|
||||||
|
fee_percent: z.coerce.number().optional(),
|
||||||
|
fee_amount: z.coerce.number().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
platform: '',
|
||||||
|
icon: '',
|
||||||
|
domain: '',
|
||||||
|
config: {},
|
||||||
|
fee_mode: 0,
|
||||||
|
fee_percent: 0,
|
||||||
|
fee_amount: 0,
|
||||||
|
...(initialValues as any),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const feeMode = form.watch('fee_mode');
|
||||||
|
const platformValue = form.watch('platform');
|
||||||
|
const configValues = form.watch('config');
|
||||||
|
|
||||||
|
const currentPlatform = platformData?.find((p) => p.platform === platformValue);
|
||||||
|
const currentFieldDescriptions = currentPlatform?.platform_field_description || {};
|
||||||
|
const configFields = Object.keys(currentFieldDescriptions) || [];
|
||||||
|
const platformUrl = currentPlatform?.platform_url || '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (feeMode === 0) {
|
||||||
|
form.setValue('fee_amount', 0);
|
||||||
|
form.setValue('fee_percent', 0);
|
||||||
|
} else if (feeMode === 1) {
|
||||||
|
form.setValue('fee_amount', 0);
|
||||||
|
} else if (feeMode === 2) {
|
||||||
|
form.setValue('fee_percent', 0);
|
||||||
|
}
|
||||||
|
}, [feeMode, form]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
form.reset();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
const cleanedValues = { ...values };
|
||||||
|
|
||||||
|
if (values.fee_mode === 0) {
|
||||||
|
cleanedValues.fee_amount = undefined;
|
||||||
|
cleanedValues.fee_percent = undefined;
|
||||||
|
} else if (values.fee_mode === 1) {
|
||||||
|
cleanedValues.fee_amount = undefined;
|
||||||
|
} else if (values.fee_mode === 2) {
|
||||||
|
cleanedValues.fee_percent = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await onSubmit(cleanedValues as unknown as T);
|
||||||
|
if (success) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPlatformUrl = () => {
|
||||||
|
if (platformUrl) {
|
||||||
|
window.open(platformUrl, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
||||||
|
<SheetContent className='w-[550px] max-w-full md:max-w-screen-md'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{title}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6 px-6 pt-4'>
|
||||||
|
{/* 基本信息分组 */}
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='name'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('namePlaceholder')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => form.setValue('name', value as string)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='icon'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('icon')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('iconPlaceholder')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => form.setValue('icon', value as string)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='domain'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('domain')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('domainPlaceholder', { example: 'http(s)://example.com' })}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => form.setValue('domain', value as string)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='fee_mode'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('handlingFee')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={(value) => field.onChange(parseInt(value))}
|
||||||
|
value={field.value.toString()}
|
||||||
|
className='flex flex-wrap gap-4'
|
||||||
|
>
|
||||||
|
<FormItem className='flex items-center space-x-2'>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value='0' />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className='!mt-0 cursor-pointer'>{t('noFee')}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem className='flex items-center space-x-2'>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value='1' />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className='!mt-0 cursor-pointer'>
|
||||||
|
{t('percentFee')}
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem className='flex items-center space-x-2'>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value='2' />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className='!mt-0 cursor-pointer'>{t('fixedFee')}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{feeMode === 1 && (
|
||||||
|
<div className='grid grid-cols-1 sm:w-1/2'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='fee_percent'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('feePercent')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
step='0.01'
|
||||||
|
suffix='%'
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{feeMode === 2 && (
|
||||||
|
<div className='grid grid-cols-1 sm:w-1/2'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='fee_amount'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('feeAmount')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
step='0.01'
|
||||||
|
prefix={currency.currency_symbol}
|
||||||
|
suffix={currency.currency_unit}
|
||||||
|
value={unitConversion('centsToDollars', field.value)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(unitConversion('dollarsToCents', value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{(!platformValue || platformData?.find((p) => p.platform === platformValue)) && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='platform'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('platform')}</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue('platform', value as string);
|
||||||
|
form.setValue('config', {});
|
||||||
|
}}
|
||||||
|
defaultValue={field.value}
|
||||||
|
value={field.value}
|
||||||
|
// @ts-expect-error
|
||||||
|
disabled={isEdit && Boolean(initialValues?.platform)}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t('selectPlatform')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{platformData?.map((platform) => (
|
||||||
|
<SelectItem key={platform.platform} value={platform.platform}>
|
||||||
|
{platform.platform}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{platformUrl ? (
|
||||||
|
<div className='mt-1 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
className='h-6 px-2 text-xs'
|
||||||
|
onClick={openPlatformUrl}
|
||||||
|
>
|
||||||
|
<Icon icon='tabler:external-link' className='mr-1 h-3 w-3' />
|
||||||
|
{t('applyForPayment')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='mt-1 h-6'></div>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{configFields.length > 0 && (
|
||||||
|
<div className='mt-4 space-y-4'>
|
||||||
|
{configFields.map((fieldKey) => (
|
||||||
|
<FormItem key={fieldKey}>
|
||||||
|
<FormLabel>{currentFieldDescriptions[fieldKey]}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('configPlaceholder', {
|
||||||
|
field: currentFieldDescriptions[fieldKey],
|
||||||
|
})}
|
||||||
|
value={
|
||||||
|
configValues && configValues[fieldKey] !== undefined
|
||||||
|
? configValues[fieldKey]
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newConfig = { ...configValues };
|
||||||
|
newConfig[fieldKey] = value;
|
||||||
|
form.setValue('config', newConfig);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='description'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('description')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<MarkdownEditor
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => form.setValue(field.name, value as string)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||||
|
<Button variant='outline' disabled={loading} onClick={handleClose}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
|
{t('submit')}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Display } from '@/components/display';
|
||||||
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import {
|
||||||
|
createPaymentMethod,
|
||||||
|
deletePaymentMethod,
|
||||||
|
getPaymentMethodList,
|
||||||
|
updatePaymentMethod,
|
||||||
|
} from '@/services/admin/payment';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar';
|
||||||
|
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 { useTranslations } from 'next-intl';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import PaymentForm from './payment-form';
|
||||||
|
|
||||||
|
export default function PaymentTable() {
|
||||||
|
const t = useTranslations('payment');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.PaymentConfig, { search: string }>
|
||||||
|
action={ref}
|
||||||
|
header={{
|
||||||
|
title: t('paymentManagement'),
|
||||||
|
toolbar: (
|
||||||
|
<PaymentForm<API.CreatePaymentMethodRequest>
|
||||||
|
trigger={<Button>{t('create')}</Button>}
|
||||||
|
title={t('createPayment')}
|
||||||
|
loading={loading}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await createPaymentMethod({
|
||||||
|
...values,
|
||||||
|
enable: false,
|
||||||
|
});
|
||||||
|
toast.success(t('createSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: 'enable',
|
||||||
|
header: t('enable'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(row.getValue('enable'))}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
await updatePaymentMethod({
|
||||||
|
...row.original,
|
||||||
|
enable: checked,
|
||||||
|
});
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'icon',
|
||||||
|
header: t('icon'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const icon = row.getValue('icon') as string;
|
||||||
|
return (
|
||||||
|
<Avatar className='h-8 w-8'>
|
||||||
|
{icon ? <AvatarImage src={icon} alt={row.getValue('name')} /> : null}
|
||||||
|
<AvatarFallback>
|
||||||
|
{(row.getValue('name') as string)?.charAt(0) || '?'}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('name'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'platform',
|
||||||
|
header: t('platform'),
|
||||||
|
cell: ({ row }) => <Badge>{t(row.getValue('platform'))}</Badge>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'notify_url',
|
||||||
|
header: t('notify_url'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'fee',
|
||||||
|
header: t('handlingFee'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const feeMode = row.original.fee_mode;
|
||||||
|
if (feeMode === 1) {
|
||||||
|
return <Badge>{row.original.fee_percent}%</Badge>;
|
||||||
|
} else if (feeMode === 2) {
|
||||||
|
return (
|
||||||
|
<Badge>
|
||||||
|
<Display value={row.original.fee_amount} type='currency' />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '--';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
placeholder: t('searchPlaceholder'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filter) => {
|
||||||
|
const { data } = await getPaymentMethodList({
|
||||||
|
...pagination,
|
||||||
|
...filter,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: data?.data?.list || [],
|
||||||
|
total: data?.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
actions={{
|
||||||
|
render: (row) => [
|
||||||
|
<PaymentForm<API.UpdatePaymentMethodRequest>
|
||||||
|
isEdit
|
||||||
|
key='edit'
|
||||||
|
trigger={<Button>{t('edit')}</Button>}
|
||||||
|
title={t('editPayment')}
|
||||||
|
loading={loading}
|
||||||
|
initialValues={row}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updatePaymentMethod({
|
||||||
|
...row,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deletePaymentMethod({
|
||||||
|
id: row.id,
|
||||||
|
});
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
<Button
|
||||||
|
key='copy'
|
||||||
|
variant='outline'
|
||||||
|
onClick={async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { id, ...params } = row;
|
||||||
|
await createPaymentMethod({
|
||||||
|
...params,
|
||||||
|
enable: false,
|
||||||
|
});
|
||||||
|
toast.success(t('copySuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('copy')}
|
||||||
|
</Button>,
|
||||||
|
],
|
||||||
|
batchRender(rows) {
|
||||||
|
return [
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('batchDelete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
for (const row of rows) {
|
||||||
|
await deletePaymentMethod({ id: row.id });
|
||||||
|
}
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getStripeAlipayPaymentConfig,
|
|
||||||
updateStripeAlipayPaymentConfig,
|
|
||||||
} from '@/services/admin/payment';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function Stripe() {
|
|
||||||
const t = useTranslations('payment');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getStripeAlipayPaymentConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getStripeAlipayPaymentConfig();
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateStripeAlipayPaymentConfig({
|
|
||||||
...data,
|
|
||||||
mark: 'stripe_alipay',
|
|
||||||
[key]: value,
|
|
||||||
} as any);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('aliPay')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.enable}
|
|
||||||
onCheckedChange={async (checked) => {
|
|
||||||
updateConfig('enable', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('showName')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.name}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('name', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('iconUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.icon}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('icon', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('notifyUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.domain}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('domain', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feeMode')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Select
|
|
||||||
value={String(data?.fee_mode)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateConfig('fee_mode', Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder='请选择' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
|
||||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
|
||||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
|
||||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feePercent')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={data?.fee_percent}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('fee_percent', value);
|
|
||||||
}}
|
|
||||||
suffix='%'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('fixedFee')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
value={data?.fee_amount}
|
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
|
||||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.publicKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.public_key}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
public_key: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.secretKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.secret_key}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
secret_key: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.webhookSecret')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.webhook_secret}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
webhook_secret: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getStripeWeChatPayPaymentConfig,
|
|
||||||
updateStripeWeChatPayPaymentConfig,
|
|
||||||
} from '@/services/admin/payment';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
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 { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function StripeWeChatPay() {
|
|
||||||
const t = useTranslations('payment');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getStripeWeChatPayPaymentConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getStripeWeChatPayPaymentConfig();
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateStripeWeChatPayPaymentConfig({
|
|
||||||
...data,
|
|
||||||
mark: 'stripe_wechat_pay',
|
|
||||||
[key]: value,
|
|
||||||
} as any);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('wechatPay')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.enable}
|
|
||||||
onCheckedChange={async (checked) => {
|
|
||||||
updateConfig('enable', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('showName')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('showNameDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.name}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('name', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('iconUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('iconUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.icon}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('icon', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('notifyUrl')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('notifyUrlDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.domain}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('domain', value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feeMode')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feeModeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Select
|
|
||||||
value={String(data?.fee_mode)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateConfig('fee_mode', Number(value));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder='请选择' />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value='0'>{t('feeModeItems.0')}</SelectItem>
|
|
||||||
<SelectItem value='1'>{t('feeModeItems.1')}</SelectItem>
|
|
||||||
<SelectItem value='2'>{t('feeModeItems.2')}</SelectItem>
|
|
||||||
<SelectItem value='3'>{t('feeModeItems.3')}</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('feePercent')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('feePercentDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
value={data?.fee_percent}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('fee_percent', value);
|
|
||||||
}}
|
|
||||||
suffix='%'
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('fixedFee')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('fixedFeeDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
value={data?.fee_amount}
|
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
|
||||||
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
|
||||||
onValueBlur={(value) => updateConfig('fee_amount', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.publicKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.public_key}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
public_key: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.secretKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.secret_key}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
secret_key: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('stripe.webhookSecret')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs' />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.config.webhook_secret}
|
|
||||||
onValueBlur={(value) => {
|
|
||||||
updateConfig('config', {
|
|
||||||
...data?.config,
|
|
||||||
webhook_secret: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createRuleGroup } from '@/services/admin/server';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@workspace/ui/components/dialog';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Progress } from '@workspace/ui/components/progress';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface ImportYamlRulesProps {
|
||||||
|
onImportSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleGroup {
|
||||||
|
name: string;
|
||||||
|
rules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportYamlRules({ onImportSuccess }: ImportYamlRulesProps) {
|
||||||
|
const t = useTranslations('rules');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [yamlContent, setYamlContent] = useState('');
|
||||||
|
const [importProgress, setImportProgress] = useState(0);
|
||||||
|
const [importTotal, setImportTotal] = useState(0);
|
||||||
|
const [analyzing, setAnalyzing] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const content = event.target?.result as string;
|
||||||
|
setYamlContent(content);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const processRule = (rule: string): { policyGroup: string; cleanRule: string } | null => {
|
||||||
|
const parts = rule.split(',');
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let policyGroup = 'default';
|
||||||
|
let cleanRule = rule;
|
||||||
|
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const thirdPart = parts[2]?.trim();
|
||||||
|
if (thirdPart) {
|
||||||
|
policyGroup = thirdPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanRule = parts.slice(0, 2).join(',');
|
||||||
|
|
||||||
|
return { policyGroup, cleanRule };
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRulesIntoGroups = (rules: string[]): Record<string, string[]> => {
|
||||||
|
const groups: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (!rule.trim()) continue;
|
||||||
|
|
||||||
|
const result = processRule(rule);
|
||||||
|
if (result === null) continue;
|
||||||
|
|
||||||
|
const { policyGroup, cleanRule } = result;
|
||||||
|
if (!groups[policyGroup]) {
|
||||||
|
groups[policyGroup] = [];
|
||||||
|
}
|
||||||
|
groups[policyGroup].push(cleanRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!yamlContent) {
|
||||||
|
toast.error(t('pleaseUploadFile'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setAnalyzing(true);
|
||||||
|
try {
|
||||||
|
const parsedYaml = yaml.load(yamlContent) as any;
|
||||||
|
|
||||||
|
if (!parsedYaml || !parsedYaml.rules) {
|
||||||
|
throw new Error(t('invalidYamlFormat'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let allRules: string[] = [];
|
||||||
|
if (Array.isArray(parsedYaml.rules)) {
|
||||||
|
allRules = parsedYaml.rules.filter((rule: string) => rule.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allRules.length === 0) {
|
||||||
|
throw new Error(t('noValidRules'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ruleGroups = parseRulesIntoGroups(allRules);
|
||||||
|
const groups = Object.entries(ruleGroups).map(([name, rules]) => ({
|
||||||
|
name,
|
||||||
|
rules,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setImportTotal(groups.length);
|
||||||
|
setAnalyzing(false);
|
||||||
|
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
const group = groups[i];
|
||||||
|
if (!group?.name || !group?.rules.length) continue;
|
||||||
|
await createRuleGroup({
|
||||||
|
name: group.name,
|
||||||
|
rules: group?.rules.join('\n'),
|
||||||
|
enable: false,
|
||||||
|
tags: [],
|
||||||
|
icon: '',
|
||||||
|
});
|
||||||
|
setImportProgress(i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t('importSuccess'));
|
||||||
|
setOpen(false);
|
||||||
|
setYamlContent('');
|
||||||
|
setImportProgress(0);
|
||||||
|
setImportTotal(0);
|
||||||
|
onImportSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Import error:', error);
|
||||||
|
toast.error(error instanceof Error ? error.message : t('importFailed'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setAnalyzing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type='file'
|
||||||
|
accept='.yml,.yaml'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<Button variant='default' onClick={() => fileInputRef.current?.click()}>
|
||||||
|
{t('import')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className='sm:max-w-[500px]'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('importYamlRules')}</DialogTitle>
|
||||||
|
<DialogDescription>{t('importYamlDescription')}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className='grid gap-4 py-4'>
|
||||||
|
{yamlContent && (
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label htmlFor='preview'>{t('preview')}</Label>
|
||||||
|
<Textarea
|
||||||
|
id='preview'
|
||||||
|
value={yamlContent}
|
||||||
|
readOnly
|
||||||
|
rows={10}
|
||||||
|
className='font-mono text-xs'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{importTotal > 0 && (
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span>{analyzing ? t('analyzing') : t('importing')}</span>
|
||||||
|
<span>
|
||||||
|
{importProgress} / {importTotal}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={(importProgress / importTotal) * 100} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant='outline' onClick={() => setOpen(false)} disabled={loading}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleImport} disabled={loading || !yamlContent}>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
|
{t('import')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import {
|
||||||
|
createRuleGroup,
|
||||||
|
deleteRuleGroup,
|
||||||
|
getRuleGroupList,
|
||||||
|
updateRuleGroup,
|
||||||
|
} from '@/services/admin/server';
|
||||||
|
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 Image from 'next/legacy/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import ImportYamlRules from './import-yaml-rules';
|
||||||
|
import RuleForm from './rule-form';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('rules');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.ServerRuleGroup, { query: string }>
|
||||||
|
action={ref}
|
||||||
|
header={{
|
||||||
|
toolbar: (
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Button variant='default' asChild>
|
||||||
|
<Link href='/template/rules.yml' target='_blank' download>
|
||||||
|
{t('downloadTemplate')}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<ImportYamlRules onImportSuccess={() => ref.current?.refresh()} />
|
||||||
|
<RuleForm<API.CreateRuleGroupRequest>
|
||||||
|
trigger={t('create')}
|
||||||
|
title={t('createRule')}
|
||||||
|
loading={loading}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await createRuleGroup({
|
||||||
|
name: values.name,
|
||||||
|
rules: values.rules || '',
|
||||||
|
enable: false,
|
||||||
|
tags: values.tags || [],
|
||||||
|
icon: values.icon || '',
|
||||||
|
});
|
||||||
|
toast.success(t('createSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
placeholder: t('searchRule'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filters) => {
|
||||||
|
const { data } = await getRuleGroupList({
|
||||||
|
...pagination,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: data.data?.list || [],
|
||||||
|
total: data.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: 'enable',
|
||||||
|
header: t('enable'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={row.getValue('enable')}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
await updateRuleGroup({
|
||||||
|
...row.original,
|
||||||
|
enable: checked,
|
||||||
|
} as API.UpdateRuleGroupRequest);
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'icon',
|
||||||
|
header: t('appIcon'),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.getValue('icon') ? (
|
||||||
|
<Image
|
||||||
|
src={row.getValue('icon')}
|
||||||
|
alt={row.getValue('name')}
|
||||||
|
className='h-8 w-8 rounded-md'
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='bg-muted flex h-8 w-8 items-center justify-center rounded-md'>
|
||||||
|
{row.original.name?.slice(0, 2)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('name'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'tags',
|
||||||
|
header: t('tags'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const tags = row.original.tags.filter((item) => item) || [];
|
||||||
|
if (!tags.length) return '--';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant='outline' className='mr-1'>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: t('createdAt'),
|
||||||
|
cell: ({ row }) => formatDate(row.original.created_at),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
actions={{
|
||||||
|
render: (row) => [
|
||||||
|
<RuleForm<API.UpdateRuleGroupRequest>
|
||||||
|
key='edit'
|
||||||
|
trigger={t('edit')}
|
||||||
|
title={t('editRule')}
|
||||||
|
loading={loading}
|
||||||
|
initialValues={row}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updateRuleGroup({
|
||||||
|
id: row.id,
|
||||||
|
name: values.name,
|
||||||
|
tags: values.tags,
|
||||||
|
rules: values.rules,
|
||||||
|
enable: row.enable,
|
||||||
|
icon: values.icon,
|
||||||
|
});
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteRuleGroup({ id: row.id });
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
batchRender: (rows) => [
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
for (const row of rows) {
|
||||||
|
await deleteRuleGroup({ id: row.id });
|
||||||
|
}
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.reset();
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getNodeTagList } from '@/services/admin/server';
|
||||||
|
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 { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
|
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: '请输入规则名称' }),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
rules: z.string().default(''),
|
||||||
|
icon: z.string().default(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RuleFormProps<T> {
|
||||||
|
onSubmit: (data: T) => Promise<boolean> | boolean;
|
||||||
|
initialValues?: T;
|
||||||
|
loading?: boolean;
|
||||||
|
trigger: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RuleForm<T extends Record<string, any>>({
|
||||||
|
onSubmit,
|
||||||
|
initialValues,
|
||||||
|
loading,
|
||||||
|
trigger,
|
||||||
|
title,
|
||||||
|
}: RuleFormProps<T>) {
|
||||||
|
const t = useTranslations('rules');
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
...initialValues,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialValues) {
|
||||||
|
form.reset(initialValues);
|
||||||
|
}
|
||||||
|
}, [form, initialValues]);
|
||||||
|
|
||||||
|
async function handleSubmit(data: { [x: string]: any }) {
|
||||||
|
const bool = await onSubmit(data as T);
|
||||||
|
if (bool) setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: tags } = useQuery({
|
||||||
|
queryKey: ['getNodeTagList'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getNodeTagList();
|
||||||
|
return data.data?.tags || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
form.reset();
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{title}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='icon'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('appIcon')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('enterIconUrl')}
|
||||||
|
value={field.value}
|
||||||
|
suffix={
|
||||||
|
<UploadImage
|
||||||
|
className='bg-muted h-9 rounded-none border-none px-2'
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='name'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('enterRuleName')}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='tags'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('tagsLabel')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox<string, true>
|
||||||
|
multiple
|
||||||
|
placeholder={t('selectTags')}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
options={tags?.map((item: string) => ({
|
||||||
|
value: item,
|
||||||
|
label: item,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='rules'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('rulesLabel')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t('enterRules')}
|
||||||
|
value={field.value}
|
||||||
|
rows={10}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.setValue(field.name, e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='text-muted-foreground mt-1 text-xs'>
|
||||||
|
<pre>{t('rulesFormat')}</pre>
|
||||||
|
<div className='border-muted mt-2 space-y-1 border-l-2 pl-2'>
|
||||||
|
<p className='font-mono'>DOMAIN,example.com</p>
|
||||||
|
<p className='font-mono'>DOMAIN-SUFFIX,google.com,DIRECT</p>
|
||||||
|
<p className='font-mono'>DOMAIN-KEYWORD,amazon,REJECT</p>
|
||||||
|
<p className='font-mono'>IP-CIDR,192.168.0.0/16</p>
|
||||||
|
<p className='font-mono'>IP-CIDR6,2001:db8::/32,REJECT</p>
|
||||||
|
<p className='font-mono'>SRC-IP-CIDR,192.168.1.201/32</p>
|
||||||
|
<p className='font-mono'>GEOIP,CN,DIRECT</p>
|
||||||
|
<p className='font-mono'>GEOIP,US</p>
|
||||||
|
<p className='font-mono'>DST-PORT,80,DIRECT</p>
|
||||||
|
<p className='font-mono'>SRC-PORT,7777,REJECT</p>
|
||||||
|
<p className='font-mono'>PROCESS-NAME,telegram</p>
|
||||||
|
<p className='font-mono'>RULE-SET,netflix</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -362,9 +362,10 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
{...field}
|
{...field}
|
||||||
type='number'
|
type='number'
|
||||||
placeholder='1-65535'
|
step={1}
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
|
placeholder='1-65535'
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
}}
|
}}
|
||||||
@@ -418,6 +419,9 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
{...field}
|
{...field}
|
||||||
type='number'
|
type='number'
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
placeholder='1-65535'
|
placeholder='1-65535'
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
@@ -899,6 +903,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
{
|
{
|
||||||
name: 'port',
|
name: 'port',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
step: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 65535,
|
max: 65535,
|
||||||
placeholder: t('form.relayPort'),
|
placeholder: t('form.relayPort'),
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@workspace/ui/components/accordion';
|
||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { Progress } from '@workspace/ui/components/progress';
|
import { Progress } from '@workspace/ui/components/progress';
|
||||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
@@ -11,6 +17,8 @@ import {
|
|||||||
} from '@workspace/ui/components/tooltip';
|
} from '@workspace/ui/components/tooltip';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { UserSubscribeDetail } from '../user/user-detail';
|
||||||
|
|
||||||
export function formatPercentage(value: number): string {
|
export function formatPercentage(value: number): string {
|
||||||
return `${value.toFixed(1)}%`;
|
return `${value.toFixed(1)}%`;
|
||||||
@@ -18,25 +26,19 @@ export function formatPercentage(value: number): string {
|
|||||||
|
|
||||||
export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
|
export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
|
||||||
const t = useTranslations('server.node');
|
const t = useTranslations('server.node');
|
||||||
|
const [openItem, setOpenItem] = useState<string | null>(null);
|
||||||
|
|
||||||
const {
|
const { online, cpu, mem, disk, updated_at } = status || {
|
||||||
last_at,
|
online: {},
|
||||||
online_users,
|
cpu: 0,
|
||||||
status: serverStatus,
|
mem: 0,
|
||||||
} = status || {
|
disk: 0,
|
||||||
online_users: [],
|
updated_at: 0,
|
||||||
status: {
|
|
||||||
cpu: 0,
|
|
||||||
mem: 0,
|
|
||||||
disk: 0,
|
|
||||||
updated_at: 0,
|
|
||||||
},
|
|
||||||
last_at: 0,
|
|
||||||
};
|
};
|
||||||
const isOnline = last_at > 0;
|
const isOnline = updated_at > 0;
|
||||||
const badgeVariant = isOnline ? 'default' : 'destructive';
|
const badgeVariant = isOnline ? 'default' : 'destructive';
|
||||||
const badgeText = isOnline ? t('normal') : t('abnormal');
|
const badgeText = isOnline ? t('normal') : t('abnormal');
|
||||||
const onlineCount = Array.isArray(online_users) ? online_users?.length : 0;
|
const onlineCount = (online && Object.keys(online).length) || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -52,45 +54,56 @@ export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
|
|||||||
<div className='flex flex-col space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<span>CPU</span>
|
<span>CPU</span>
|
||||||
<span>{formatPercentage(serverStatus?.cpu ?? 0)}</span>
|
<span>{formatPercentage(cpu ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={serverStatus?.cpu ?? 0} className='h-2' max={100} />
|
<Progress value={cpu ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<span>{t('memory')}</span>
|
<span>{t('memory')}</span>
|
||||||
<span>{formatPercentage(serverStatus?.mem ?? 0)}</span>
|
<span>{formatPercentage(mem ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={serverStatus?.mem ?? 0} className='h-2' max={100} />
|
<Progress value={mem ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<div className='flex justify-between'>
|
<div className='flex justify-between'>
|
||||||
<span>{t('disk')}</span>
|
<span>{t('disk')}</span>
|
||||||
<span>{formatPercentage(serverStatus?.disk ?? 0)}</span>
|
<span>{formatPercentage(disk ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={serverStatus?.disk ?? 0} className='h-2' max={100} />
|
<Progress value={disk ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
{isOnline && (
|
{isOnline && (
|
||||||
<div>
|
<div>
|
||||||
{t('lastUpdated')}: {formatDate(serverStatus?.updated_at ?? 0)}
|
{t('lastUpdated')}: {formatDate(updated_at ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{isOnline && onlineCount > 0 && (
|
{isOnline && onlineCount > 0 && (
|
||||||
<TooltipContent className='bg-muted text-foreground w-80'>
|
<TooltipContent className='bg-card text-foreground w-96'>
|
||||||
<div className='space-y-4'>
|
<ScrollArea className='h-[540px] rounded-md border px-4 py-2'>
|
||||||
<div className='space-y-2'>
|
<h4 className='py-1 text-sm font-semibold'>{t('onlineUsers')}</h4>
|
||||||
<h4 className='text-sm font-semibold'>{t('onlineUsers')}</h4>
|
<Accordion
|
||||||
<ScrollArea className='h-[400px] rounded-md border p-2'>
|
type='single'
|
||||||
{online_users.map((user, index) => (
|
collapsible
|
||||||
<div key={user.uid} className='py-1 text-xs'>
|
className='w-full'
|
||||||
{user.ip} (UID: {user.uid})
|
onValueChange={(value) => setOpenItem(value)}
|
||||||
</div>
|
>
|
||||||
))}
|
{Object.entries(online).map(([uid, ips]) => (
|
||||||
</ScrollArea>
|
<AccordionItem key={uid} value={uid}>
|
||||||
</div>
|
<AccordionTrigger>{`[UID: ${uid}] - ${ips[0]}`}</AccordionTrigger>
|
||||||
</div>
|
<AccordionContent>
|
||||||
|
<ul>
|
||||||
|
{ips.map((ip: string) => (
|
||||||
|
<li key={ip}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<UserSubscribeDetail id={Number(uid)} enabled={openItem === uid} />
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</ScrollArea>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { getTranslations } from 'next-intl/server';
|
|||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
|
||||||
import SubscribeApp from './app/table';
|
|
||||||
import GroupTable from './group/table';
|
import GroupTable from './group/table';
|
||||||
import SubscribeConfig from './subscribe-config';
|
import SubscribeConfig from './subscribe-config';
|
||||||
import SubscribeTable from './subscribe-table';
|
import SubscribeTable from './subscribe-table';
|
||||||
@@ -16,7 +15,6 @@ export default async function Page() {
|
|||||||
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
|
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
|
||||||
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
|
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
|
||||||
<TabsTrigger value='config'>{t('tabs.subscribeConfig')}</TabsTrigger>
|
<TabsTrigger value='config'>{t('tabs.subscribeConfig')}</TabsTrigger>
|
||||||
<TabsTrigger value='app'>{t('tabs.subscribeApp')}</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='subscribe'>
|
<TabsContent value='subscribe'>
|
||||||
<SubscribeTable />
|
<SubscribeTable />
|
||||||
@@ -27,9 +25,6 @@ export default async function Page() {
|
|||||||
<TabsContent value='config'>
|
<TabsContent value='config'>
|
||||||
<SubscribeConfig />
|
<SubscribeConfig />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='app'>
|
|
||||||
<SubscribeApp />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,32 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const unit_time = form.watch('unit_time');
|
const unit_time = form.watch('unit_time');
|
||||||
|
const unit_price = form.watch('unit_price');
|
||||||
|
const discounts = form.watch('discount');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!discounts?.length || !unit_price) return;
|
||||||
|
|
||||||
|
const calculatedValues = discounts.map((item: any) => {
|
||||||
|
const result = { ...item };
|
||||||
|
|
||||||
|
if (item.quantity && item.discount) {
|
||||||
|
result.price = evaluateWithPrecision(
|
||||||
|
`${unit_price || 0} * ${item.quantity} * ${item.discount} / 100`,
|
||||||
|
);
|
||||||
|
} else if (item.quantity && item.price && !item.discount) {
|
||||||
|
result.discount = evaluateWithPrecision(
|
||||||
|
`${item.price} / ${item.quantity} / ${unit_price} * 100`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (JSON.stringify(calculatedValues) !== JSON.stringify(discounts)) {
|
||||||
|
form.setValue('discount', calculatedValues);
|
||||||
|
}
|
||||||
|
}, [unit_price, discounts, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
@@ -298,6 +324,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
placeholder={t('form.noLimit')}
|
placeholder={t('form.noLimit')}
|
||||||
type='number'
|
type='number'
|
||||||
|
step={1}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
@@ -321,7 +348,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
placeholder={t('form.noLimit')}
|
placeholder={t('form.noLimit')}
|
||||||
type='number'
|
type='number'
|
||||||
value={field.value === -1 ? 0 : field.value}
|
step={1}
|
||||||
|
value={field.value}
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
@@ -343,6 +371,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
placeholder={t('form.noLimit')}
|
placeholder={t('form.noLimit')}
|
||||||
type='number'
|
type='number'
|
||||||
|
step={1}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
@@ -540,11 +569,12 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('form.discount')}</FormLabel>
|
<FormLabel>{t('form.discount')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<ArrayInput<API.SubscribeDiscount>
|
<ArrayInput<API.SubscribeDiscount & { price?: number }>
|
||||||
fields={[
|
fields={[
|
||||||
{
|
{
|
||||||
name: 'quantity',
|
name: 'quantity',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
step: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
suffix: unit_time && t(`form.${unit_time}`),
|
suffix: unit_time && t(`form.${unit_time}`),
|
||||||
},
|
},
|
||||||
@@ -555,38 +585,56 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
max: 100,
|
max: 100,
|
||||||
placeholder: t('form.discountPercent'),
|
placeholder: t('form.discountPercent'),
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
calculateValue: function (data) {
|
|
||||||
const { unit_price } = form.getValues();
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
price: evaluateWithPrecision(
|
|
||||||
`${unit_price} * ${data.quantity} * ${data.discount} / 100`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'price',
|
name: 'price',
|
||||||
placeholder: t('form.discount_price'),
|
placeholder: t('form.discount_price'),
|
||||||
type: 'number',
|
type: 'number',
|
||||||
min: 0,
|
|
||||||
formatInput: (value) => unitConversion('centsToDollars', value),
|
formatInput: (value) => unitConversion('centsToDollars', value),
|
||||||
formatOutput: (value) => unitConversion('dollarsToCents', value),
|
formatOutput: (value) => unitConversion('dollarsToCents', value),
|
||||||
internal: true,
|
|
||||||
calculateValue: (data) => {
|
|
||||||
const { unit_price } = form.getValues();
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
discount: evaluateWithPrecision(
|
|
||||||
`${data.price} / ${data.quantity} / ${unit_price} * 100`,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value) => {
|
onChange={(newValues) => {
|
||||||
form.setValue(field.name, value);
|
const oldValues = field.value || [];
|
||||||
|
const { unit_price } = form.getValues();
|
||||||
|
|
||||||
|
const calculatedValues = newValues.map((newItem, index) => {
|
||||||
|
const oldItem = oldValues[index] || {};
|
||||||
|
|
||||||
|
const result = { ...newItem };
|
||||||
|
|
||||||
|
const quantityChanged = newItem.quantity !== oldItem.quantity;
|
||||||
|
const discountChanged = newItem.discount !== oldItem.discount;
|
||||||
|
const priceChanged = newItem.price !== oldItem.price;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(quantityChanged || discountChanged) &&
|
||||||
|
!priceChanged &&
|
||||||
|
newItem.quantity &&
|
||||||
|
newItem.discount
|
||||||
|
) {
|
||||||
|
result.price = evaluateWithPrecision(
|
||||||
|
`${unit_price || 0} * ${newItem.quantity} * ${newItem.discount} / 100`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
priceChanged &&
|
||||||
|
!discountChanged &&
|
||||||
|
newItem.price &&
|
||||||
|
newItem.quantity &&
|
||||||
|
unit_price
|
||||||
|
) {
|
||||||
|
result.discount = evaluateWithPrecision(
|
||||||
|
`${newItem.price} / ${newItem.quantity} / ${unit_price} * 100`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setValue(field.name, calculatedValues);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -5,18 +5,21 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { Label } from '@workspace/ui/components/label';
|
import { Label } from '@workspace/ui/components/label';
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
import { Textarea } from '@workspace/ui/components/textarea';
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { JSONEditor } from '@workspace/ui/custom-components/editor';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Site() {
|
export default function Site() {
|
||||||
const t = useTranslations('system.site');
|
const t = useTranslations('system.site');
|
||||||
|
const ref = useRef<API.SiteConfig | undefined>(undefined);
|
||||||
const { data, refetch } = useQuery({
|
const { data, refetch } = useQuery({
|
||||||
queryKey: ['getSiteConfig'],
|
queryKey: ['getSiteConfig'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await getSiteConfig();
|
const { data } = await getSiteConfig();
|
||||||
|
ref.current = data.data;
|
||||||
return data.data;
|
return data.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -25,7 +28,7 @@ export default function Site() {
|
|||||||
if (data?.[key] === value) return;
|
if (data?.[key] === value) return;
|
||||||
try {
|
try {
|
||||||
await updateSiteConfig({
|
await updateSiteConfig({
|
||||||
...data,
|
...ref.current,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
} as API.SiteConfig);
|
} as API.SiteConfig);
|
||||||
toast.success(t('saveSuccess'));
|
toast.success(t('saveSuccess'));
|
||||||
@@ -130,6 +133,49 @@ export default function Site() {
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('customData')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('customDataDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='w-1/2 text-right'>
|
||||||
|
<JSONEditor
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: true,
|
||||||
|
properties: {
|
||||||
|
website: { type: 'string', title: 'Website' },
|
||||||
|
contacts: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Contacts',
|
||||||
|
additionalProperties: true,
|
||||||
|
properties: {
|
||||||
|
email: { type: 'string', title: 'Email' },
|
||||||
|
telephone: { type: 'string', title: 'Telephone' },
|
||||||
|
address: { type: 'string', title: 'Address' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
community: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Community',
|
||||||
|
additionalProperties: true,
|
||||||
|
properties: {
|
||||||
|
telegram: { type: 'string', title: 'Telegram' },
|
||||||
|
twitter: { type: 'string', title: 'Twitter' },
|
||||||
|
discord: { type: 'string', title: 'Discord' },
|
||||||
|
instagram: { type: 'string', title: 'Instagram' },
|
||||||
|
linkedin: { type: 'string', title: 'Linkedin' },
|
||||||
|
facebook: { type: 'string', title: 'Facebook' },
|
||||||
|
github: { type: 'string', title: 'Github' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
value={data?.custom_data}
|
||||||
|
onBlur={(value) => updateConfig('custom_data', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '@workspace/ui/components/accordion';
|
} from '@workspace/ui/components/accordion';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
@@ -67,7 +66,7 @@ export default function Page() {
|
|||||||
<CardDescription>{t('viewLogsAndManage')}</CardDescription>
|
<CardDescription>{t('viewLogsAndManage')}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-4 flex flex-col space-y-2 sm:mt-0 sm:flex-row sm:space-x-2 sm:space-y-0'>
|
<div className='mt-4 flex flex-col space-y-2 sm:mt-0 sm:flex-row sm:space-x-2 sm:space-y-0'>
|
||||||
<AlertDialog>
|
{/* <AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button>{t('systemUpgrade')}</Button>
|
<Button>{t('systemUpgrade')}</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
@@ -81,7 +80,7 @@ export default function Page() {
|
|||||||
<AlertDialogAction>{t('confirmUpgrade')}</AlertDialogAction>
|
<AlertDialogAction>{t('confirmUpgrade')}</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog> */}
|
||||||
<AlertDialog open={openRestart} onOpenChange={setOpenRestart}>
|
<AlertDialog open={openRestart} onOpenChange={setOpenRestart}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant='destructive'>{t('systemReboot')}</Button>
|
<Button variant='destructive'>{t('systemReboot')}</Button>
|
||||||
@@ -113,14 +112,14 @@ export default function Page() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<div className='flex items-center justify-between'>
|
{/* <div className='flex items-center justify-between'>
|
||||||
<div className='text-lg font-semibold'>
|
<div className='text-lg font-semibold'>
|
||||||
{t('currentVersion')} <span>V1.0.0</span>
|
{t('currentVersion')} <span>V1.0.0</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted-foreground text-sm'>
|
<div className='text-muted-foreground text-sm'>
|
||||||
{t('lastUpdated')} <span>2024-12-16 12:00:00</span>
|
{t('lastUpdated')} <span>2024-12-16 12:00:00</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
<Card className='overflow-hidden'>
|
<Card className='overflow-hidden'>
|
||||||
<CardHeader className='bg-secondary py-1'>
|
<CardHeader className='bg-secondary py-1'>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () =
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
type='number'
|
type='number'
|
||||||
|
min={0}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
prefix={currency?.currency_symbol ?? '$'}
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||||
@@ -136,6 +137,7 @@ export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () =
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
type='number'
|
type='number'
|
||||||
|
min={0}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
prefix={currency?.currency_symbol ?? '$'}
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||||
@@ -159,6 +161,7 @@ export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () =
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
type='number'
|
type='number'
|
||||||
|
min={0}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
prefix={currency?.currency_symbol ?? '$'}
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
formatInput={(value) => unitConversion('centsToDollars', value)}
|
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||||
|
|||||||
@@ -57,150 +57,154 @@ export function SubscriptionDetail({
|
|||||||
{t('onlineDevices')}
|
{t('onlineDevices')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='logs'>
|
<div className='mt-4 max-h-[60dvh] overflow-y-auto'>
|
||||||
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
|
<TabsContent value='logs'>
|
||||||
columns={[
|
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
|
||||||
{
|
columns={[
|
||||||
accessorKey: 'ip',
|
{
|
||||||
header: 'IP',
|
accessorKey: 'ip',
|
||||||
},
|
header: 'IP',
|
||||||
{
|
},
|
||||||
accessorKey: 'user_agent',
|
{
|
||||||
header: t('userAgent'),
|
accessorKey: 'user_agent',
|
||||||
},
|
header: t('userAgent'),
|
||||||
{
|
},
|
||||||
accessorKey: 'token',
|
{
|
||||||
header: t('token'),
|
accessorKey: 'token',
|
||||||
},
|
header: t('token'),
|
||||||
{
|
},
|
||||||
accessorKey: 'created_at',
|
{
|
||||||
header: t('time'),
|
accessorKey: 'created_at',
|
||||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
header: t('time'),
|
||||||
},
|
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||||
]}
|
},
|
||||||
request={async (pagination) => {
|
]}
|
||||||
const { data } = await getUserSubscribeLogs({
|
request={async (pagination) => {
|
||||||
user_id: userId,
|
const { data } = await getUserSubscribeLogs({
|
||||||
subscribe_id: subscriptionId,
|
user_id: userId,
|
||||||
...pagination,
|
subscribe_id: subscriptionId,
|
||||||
});
|
...pagination,
|
||||||
return {
|
});
|
||||||
list: data.data?.list || [],
|
return {
|
||||||
total: data.data?.total || 0,
|
list: data.data?.list || [],
|
||||||
};
|
total: data.data?.total || 0,
|
||||||
}}
|
};
|
||||||
/>
|
}}
|
||||||
</TabsContent>
|
/>
|
||||||
<TabsContent value='traffic'>
|
</TabsContent>
|
||||||
<ProTable<API.TrafficLog, Record<string, unknown>>
|
<TabsContent value='traffic'>
|
||||||
columns={[
|
<ProTable<API.TrafficLog, Record<string, unknown>>
|
||||||
{
|
columns={[
|
||||||
accessorKey: 'download',
|
{
|
||||||
header: t('download'),
|
accessorKey: 'download',
|
||||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('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: 'upload',
|
||||||
{
|
header: t('upload'),
|
||||||
accessorKey: 'timestamp',
|
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||||
header: t('time'),
|
},
|
||||||
cell: ({ row }) => formatDate(row.getValue('timestamp')),
|
{
|
||||||
},
|
accessorKey: 'timestamp',
|
||||||
]}
|
header: t('time'),
|
||||||
request={async (pagination) => {
|
cell: ({ row }) => formatDate(row.getValue('timestamp')),
|
||||||
const { data } = await getUserSubscribeTrafficLogs({
|
},
|
||||||
user_id: userId,
|
]}
|
||||||
subscribe_id: subscriptionId,
|
request={async (pagination) => {
|
||||||
...pagination,
|
const { data } = await getUserSubscribeTrafficLogs({
|
||||||
});
|
user_id: userId,
|
||||||
return {
|
subscribe_id: subscriptionId,
|
||||||
list: data.data?.list || [],
|
...pagination,
|
||||||
total: data.data?.total || 0,
|
} as API.GetUserSubscribeTrafficLogsParams);
|
||||||
};
|
return {
|
||||||
}}
|
list: data.data?.list || [],
|
||||||
/>
|
total: data.data?.total || 0,
|
||||||
</TabsContent>
|
};
|
||||||
<TabsContent value='devices'>
|
}}
|
||||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
/>
|
||||||
columns={[
|
</TabsContent>
|
||||||
{
|
<TabsContent value='devices'>
|
||||||
accessorKey: 'enabled',
|
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||||
header: t('enable'),
|
columns={[
|
||||||
cell: ({ row }) => (
|
{
|
||||||
<Switch
|
accessorKey: 'enabled',
|
||||||
checked={row.getValue('enabled')}
|
header: t('enable'),
|
||||||
onChange={(checked) => {
|
cell: ({ row }) => (
|
||||||
console.log('Switch:', checked);
|
<Switch
|
||||||
}}
|
checked={row.getValue('enabled')}
|
||||||
/>
|
onChange={(checked) => {
|
||||||
),
|
console.log('Switch:', checked);
|
||||||
},
|
}}
|
||||||
{
|
/>
|
||||||
accessorKey: 'id',
|
),
|
||||||
header: 'ID',
|
},
|
||||||
},
|
{
|
||||||
{
|
accessorKey: 'id',
|
||||||
accessorKey: 'identifier',
|
header: 'ID',
|
||||||
header: 'IMEI',
|
},
|
||||||
},
|
{
|
||||||
{
|
accessorKey: 'identifier',
|
||||||
accessorKey: 'user_agent',
|
header: 'IMEI',
|
||||||
header: t('userAgent'),
|
},
|
||||||
},
|
{
|
||||||
{
|
accessorKey: 'user_agent',
|
||||||
accessorKey: 'ip',
|
header: t('userAgent'),
|
||||||
header: 'IP',
|
},
|
||||||
},
|
{
|
||||||
{
|
accessorKey: 'ip',
|
||||||
accessorKey: 'online',
|
header: 'IP',
|
||||||
header: t('loginStatus'),
|
},
|
||||||
cell: ({ row }) => (
|
{
|
||||||
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
|
accessorKey: 'online',
|
||||||
{row.getValue('online') ? t('online') : t('offline')}
|
header: t('loginStatus'),
|
||||||
</Badge>
|
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')),
|
{
|
||||||
},
|
accessorKey: 'updated_at',
|
||||||
]}
|
header: t('lastSeen'),
|
||||||
request={async (pagination) => {
|
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||||
const { data } = await getUserSubscribeDevices({
|
},
|
||||||
user_id: userId,
|
]}
|
||||||
subscribe_id: subscriptionId,
|
request={async (pagination) => {
|
||||||
...pagination,
|
const { data } = await getUserSubscribeDevices({
|
||||||
});
|
user_id: userId,
|
||||||
return {
|
subscribe_id: subscriptionId,
|
||||||
list: data.data?.list || [],
|
...pagination,
|
||||||
total: data.data?.total || 0,
|
});
|
||||||
};
|
return {
|
||||||
}}
|
list: data.data?.list || [],
|
||||||
actions={{
|
total: data.data?.total || 0,
|
||||||
render: (row) => {
|
};
|
||||||
if (!row.identifier) return [];
|
}}
|
||||||
return [
|
actions={{
|
||||||
<ConfirmButton
|
render: (row) => {
|
||||||
key='offline'
|
if (!row.identifier) return [];
|
||||||
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
return [
|
||||||
title={t('confirmOffline')}
|
<ConfirmButton
|
||||||
description={t('kickOfflineConfirm', { ip: row.ip })}
|
key='offline'
|
||||||
onConfirm={async () => {
|
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
||||||
await kickOfflineByUserDevice({ id: row.id });
|
title={t('confirmOffline')}
|
||||||
toast.success(t('kickOfflineSuccess'));
|
description={t('kickOfflineConfirm', { ip: row.ip })}
|
||||||
}}
|
onConfirm={async () => {
|
||||||
cancelText={t('cancel')}
|
await kickOfflineByUserDevice({ id: row.id });
|
||||||
confirmText={t('confirm')}
|
toast.success(t('kickOfflineSuccess'));
|
||||||
/>,
|
}}
|
||||||
];
|
cancelText={t('cancel')}
|
||||||
},
|
confirmText={t('confirm')}
|
||||||
}}
|
/>,
|
||||||
/>
|
];
|
||||||
</TabsContent>
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { Display } from '@/components/display';
|
import { Display } from '@/components/display';
|
||||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||||
import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
|
import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
@@ -20,8 +22,19 @@ export default function Page() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const ref = useRef<ProTableActions>(null);
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
const { data: subscribeList } = useQuery({
|
||||||
|
queryKey: ['getSubscribeList', 'all'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getSubscribeList({
|
||||||
|
page: 1,
|
||||||
|
size: 9999,
|
||||||
|
});
|
||||||
|
return data.data?.list as API.SubscribeGroup[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProTable<API.User, Record<string, unknown>>
|
<ProTable<API.User, API.GetUserListParams>
|
||||||
action={ref}
|
action={ref}
|
||||||
header={{
|
header={{
|
||||||
title: t('userList'),
|
title: t('userList'),
|
||||||
@@ -58,8 +71,21 @@ export default function Page() {
|
|||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.getValue('enable')}
|
defaultChecked={row.getValue('enable')}
|
||||||
onCheckedChange={async (checked) => {
|
onCheckedChange={async (checked) => {
|
||||||
|
const {
|
||||||
|
auth_methods,
|
||||||
|
user_devices,
|
||||||
|
enable_balance_notify,
|
||||||
|
enable_login_notify,
|
||||||
|
enable_subscribe_notify,
|
||||||
|
enable_trade_notify,
|
||||||
|
updated_at,
|
||||||
|
created_at,
|
||||||
|
id,
|
||||||
|
...rest
|
||||||
|
} = row.original;
|
||||||
await updateUserBasicInfo({
|
await updateUserBasicInfo({
|
||||||
...row.original,
|
user_id: id,
|
||||||
|
...rest,
|
||||||
enable: checked,
|
enable: checked,
|
||||||
} as unknown as API.UpdateUserBasiceInfoRequest);
|
} as unknown as API.UpdateUserBasiceInfoRequest);
|
||||||
toast.success(t('updateSuccess'));
|
toast.success(t('updateSuccess'));
|
||||||
@@ -119,13 +145,38 @@ export default function Page() {
|
|||||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
request={async (pagination) => {
|
request={async (pagination, filter) => {
|
||||||
const { data } = await getUserList(pagination);
|
const { data } = await getUserList({
|
||||||
|
...pagination,
|
||||||
|
...filter,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
list: data.data?.list || [],
|
list: data.data?.list || [],
|
||||||
total: data.data?.total || 0,
|
total: data.data?.total || 0,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: 'subscribe_id',
|
||||||
|
placeholder: t('subscription'),
|
||||||
|
options: subscribeList?.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: String(item.id),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
placeholder: 'Search',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user_id',
|
||||||
|
placeholder: t('userId'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user_subscribe_id',
|
||||||
|
placeholder: t('subscriptionId'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
actions={{
|
actions={{
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,15 +1,121 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Display } from '@/components/display';
|
import { Display } from '@/components/display';
|
||||||
import { getUserDetail } from '@/services/admin/user';
|
import { getUserDetail, getUserSubscribeById } from '@/services/admin/user';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function UserSubscribeDetail({ id, enabled }: { id: number; enabled: boolean }) {
|
||||||
|
const t = useTranslations('user');
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
enabled: id !== 0 && enabled,
|
||||||
|
queryKey: ['getUserSubscribeById', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getUserSubscribeById({ id });
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!id) return '--';
|
||||||
|
|
||||||
|
const usedTraffic = data ? data.upload + data.download : 0;
|
||||||
|
const totalTraffic = data?.traffic || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div>
|
||||||
|
<h3 className='mb-2 text-sm font-medium'>{t('subscriptionInfo')}</h3>
|
||||||
|
<div className='bg-muted/30 rounded-lg p-3'>
|
||||||
|
<ul className='grid gap-3'>
|
||||||
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
|
<span className='text-muted-foreground'>{t('subscriptionId')}</span>
|
||||||
|
<span>{data?.id || '--'}</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('subscriptionName')}</span>
|
||||||
|
<span>{data?.subscribe?.name || '--'}</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('token')}</span>
|
||||||
|
<div className='font-mono text-xs' title={data?.token || ''}>
|
||||||
|
{data?.token || '--'}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('trafficUsage')}</span>
|
||||||
|
<span>
|
||||||
|
{data
|
||||||
|
? totalTraffic === 0
|
||||||
|
? `${formatBytes(usedTraffic)} / ${t('unlimited')}`
|
||||||
|
: `${formatBytes(usedTraffic)} / ${formatBytes(totalTraffic)}`
|
||||||
|
: '--'}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('startTime')}</span>
|
||||||
|
<span>{data?.start_time ? formatDate(data.start_time) : '--'}</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('expireTime')}</span>
|
||||||
|
<span>{data?.expire_time ? formatDate(data.expire_time) : '--'}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className='mb-2 text-sm font-medium'>
|
||||||
|
{t('userInfo')}
|
||||||
|
{data?.user_id && (
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
size='sm'
|
||||||
|
className='text-primary ml-2 h-auto p-0 text-xs'
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={`/dashboard/user/${data.user_id}`}>{t('viewDetails')}</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<ul className='grid gap-3'>
|
||||||
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
|
<span className='text-muted-foreground'>{t('userId')}</span>
|
||||||
|
<span>{data?.user_id}</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
|
<span className='text-muted-foreground'>{t('balance')}</span>
|
||||||
|
<span>
|
||||||
|
<Display type='currency' value={data?.user.balance} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('giftAmount')}</span>
|
||||||
|
<span>
|
||||||
|
<Display type='currency' value={data?.user?.gift_amount} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('commission')}</span>
|
||||||
|
<span>
|
||||||
|
<Display type='currency' value={data?.user?.commission} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('createdAt')}</span>
|
||||||
|
<span>{data?.user?.created_at && formatDate(data?.user?.created_at)}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function UserDetail({ id }: { id: number }) {
|
export function UserDetail({ id }: { id: number }) {
|
||||||
const t = useTranslations('user');
|
const t = useTranslations('user');
|
||||||
const [shouldFetch, setShouldFetch] = useState(false);
|
const [shouldFetch, setShouldFetch] = useState(false);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { UserSubscribeDetail } from '@/app/dashboard/user/user-detail';
|
||||||
import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console';
|
import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@workspace/ui/components/select';
|
} from '@workspace/ui/components/select';
|
||||||
|
import { Separator } from '@workspace/ui/components/separator';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { formatBytes } from '@workspace/ui/utils';
|
import { formatBytes } from '@workspace/ui/utils';
|
||||||
@@ -59,15 +61,13 @@ export default function Statistics() {
|
|||||||
users: {
|
users: {
|
||||||
today:
|
today:
|
||||||
ServerTotal?.user_traffic_ranking_today?.map((item) => ({
|
ServerTotal?.user_traffic_ranking_today?.map((item) => ({
|
||||||
name: item.user_id,
|
name: item.sid,
|
||||||
traffic: item.download + item.upload,
|
traffic: item.download + item.upload,
|
||||||
email: item.email,
|
|
||||||
})) || [],
|
})) || [],
|
||||||
yesterday:
|
yesterday:
|
||||||
ServerTotal?.user_traffic_ranking_yesterday?.map((item) => ({
|
ServerTotal?.user_traffic_ranking_yesterday?.map((item) => ({
|
||||||
name: item.user_id,
|
name: item.sid,
|
||||||
traffic: item.download + item.upload,
|
traffic: item.download + item.upload,
|
||||||
email: item.email,
|
|
||||||
})) || [],
|
})) || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -175,10 +175,6 @@ export default function Statistics() {
|
|||||||
label: t('type'),
|
label: t('type'),
|
||||||
color: 'hsl(var(--muted))',
|
color: 'hsl(var(--muted))',
|
||||||
},
|
},
|
||||||
email: {
|
|
||||||
label: t('email'),
|
|
||||||
color: 'hsl(var(--muted))',
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
color: 'hsl(var(--foreground))',
|
color: 'hsl(var(--foreground))',
|
||||||
},
|
},
|
||||||
@@ -204,13 +200,22 @@ export default function Statistics() {
|
|||||||
tickFormatter={(value, index) => String(index + 1)}
|
tickFormatter={(value, index) => String(index + 1)}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
|
trigger='hover'
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
label={true}
|
label={true}
|
||||||
labelFormatter={(label) =>
|
labelFormatter={(label, [payload]) =>
|
||||||
dataType === 'nodes'
|
dataType === 'nodes' ? (
|
||||||
? `${t('nodes')}: ${label}`
|
`${t('nodes')}: ${label}`
|
||||||
: `${t('users')}: ${label}`
|
) : (
|
||||||
|
<>
|
||||||
|
<div className='w-80'>
|
||||||
|
<UserSubscribeDetail id={payload?.payload.name} enabled={true} />
|
||||||
|
</div>
|
||||||
|
<Separator className='my-2' />
|
||||||
|
<div>{`${t('users')}: ${label}`}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
formatter={(value) => {
|
formatter={(value) => {
|
||||||
return formatBytes(Number(value) || 0);
|
return formatBytes(Number(value) || 0);
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export const navs = [
|
|||||||
icon: 'flat-color-icons:lock-portrait',
|
icon: 'flat-color-icons:lock-portrait',
|
||||||
items: AuthControl,
|
items: AuthControl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'ADS Config',
|
||||||
|
url: '/dashboard/ads',
|
||||||
|
icon: 'flat-color-icons:electrical-sensor',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Payment Config',
|
title: 'Payment Config',
|
||||||
url: '/dashboard/payment',
|
url: '/dashboard/payment',
|
||||||
@@ -76,16 +81,26 @@ export const navs = [
|
|||||||
url: '/dashboard/server',
|
url: '/dashboard/server',
|
||||||
icon: 'flat-color-icons:data-protection',
|
icon: 'flat-color-icons:data-protection',
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
title: 'Application Management',
|
||||||
{
|
url: '/dashboard/application',
|
||||||
title: 'Finance',
|
icon: 'flat-color-icons:touchscreen-smartphone',
|
||||||
items: [
|
},
|
||||||
|
{
|
||||||
|
title: 'Rule Management',
|
||||||
|
url: '/dashboard/rules',
|
||||||
|
icon: 'flat-color-icons:ruler',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Subscribe Management',
|
title: 'Subscribe Management',
|
||||||
url: '/dashboard/subscribe',
|
url: '/dashboard/subscribe',
|
||||||
icon: 'flat-color-icons:shop',
|
icon: 'flat-color-icons:shop',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Finance',
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Order Management',
|
title: 'Order Management',
|
||||||
url: '/dashboard/order',
|
url: '/dashboard/order',
|
||||||
@@ -115,7 +130,7 @@ export const navs = [
|
|||||||
{
|
{
|
||||||
title: 'Announcement Management',
|
title: 'Announcement Management',
|
||||||
url: '/dashboard/announcement',
|
url: '/dashboard/announcement',
|
||||||
icon: 'flat-color-icons:news',
|
icon: 'flat-color-icons:advertising',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Ticket Management',
|
title: 'Ticket Management',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
|||||||
site_logo: '',
|
site_logo: '',
|
||||||
keywords: '',
|
keywords: '',
|
||||||
custom_html: '',
|
custom_html: '',
|
||||||
|
custom_data: '',
|
||||||
},
|
},
|
||||||
verify: {
|
verify: {
|
||||||
turnstile_site_key: '',
|
turnstile_site_key: '',
|
||||||
@@ -63,6 +64,7 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
|||||||
verify_code_interval: 60,
|
verify_code_interval: 60,
|
||||||
},
|
},
|
||||||
oauth_methods: [],
|
oauth_methods: [],
|
||||||
|
web_ad: false,
|
||||||
},
|
},
|
||||||
user: undefined,
|
user: undefined,
|
||||||
setCommon: (common) =>
|
setCommon: (common) =>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Zrušit",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"confirmDelete": "Potvrdit smazání",
|
||||||
|
"create": "Vytvořit inzerát",
|
||||||
|
"createAds": "Vytvořit reklamu",
|
||||||
|
"createSuccess": "Úspěšně vytvořeno",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"deleteSuccess": "Úspěšně smazáno",
|
||||||
|
"deleteWarning": "Opravdu chcete smazat tento inzerát?",
|
||||||
|
"disabled": "Deaktivováno",
|
||||||
|
"edit": "Upravit",
|
||||||
|
"editAds": "Upravit reklamu",
|
||||||
|
"enabled": "Aktivováno",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Zrušit",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"content": "Obsah",
|
||||||
|
"description": "Popis",
|
||||||
|
"endTime": "Čas ukončení",
|
||||||
|
"enterContent": "Prosím zadejte obsah inzerátu",
|
||||||
|
"enterDescription": "Prosím zadejte popis inzerátu",
|
||||||
|
"enterEndTime": "Prosím vyberte čas ukončení",
|
||||||
|
"enterStartTime": "Prosím vyberte čas zahájení",
|
||||||
|
"enterTargetUrl": "Prosím zadejte cílovou URL",
|
||||||
|
"enterTitle": "Prosím zadejte název inzerátu",
|
||||||
|
"startTime": "Čas zahájení",
|
||||||
|
"targetUrl": "Cílová URL",
|
||||||
|
"title": "Název",
|
||||||
|
"type": "Typ inzerátu",
|
||||||
|
"typeImage": "Obrázek",
|
||||||
|
"typeVideo": "Video"
|
||||||
|
},
|
||||||
|
"status": "Stav",
|
||||||
|
"targetUrl": "Cílová URL",
|
||||||
|
"title": "Název",
|
||||||
|
"type": "Typ",
|
||||||
|
"updateSuccess": "Úspěšně aktualizováno",
|
||||||
|
"validityPeriod": "Doba platnosti"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Zadejte platnou e-mailovou adresu.",
|
"email": "Zadejte platnou e-mailovou adresu.",
|
||||||
"existingAccount": "Již máte účet?",
|
"existingAccount": "Již máte účet?",
|
||||||
"get": "Získat",
|
"get": "Získat",
|
||||||
"invite": "Pozvánkový kód",
|
"invite": "Pozvánkový kód (volitelné)",
|
||||||
"message": "#### Vážený uživateli,\n\nDěkujeme za váš zájem a podporu. V důsledku úpravy provozní strategie jsme uzavřeli registraci nových uživatelů. Během této doby nebude používání stávajících účtů nijak ovlivněno.\n\nSnažíme se vám poskytnout lepší služby a zážitky, a proto během uzavření registrace provedeme komplexní optimalizaci systému a aktualizaci funkcí. V budoucnu vás přivítáme s kvalitnějším obsahem a službami.\n\nSledujte naše webové stránky a sociální média pro nejnovější aktualizace a oznámení. Děkujeme za vaše pochopení a podporu.\n\nPokud máte jakékoli dotazy nebo potřebujete pomoc, neváhejte kontaktovat náš zákaznický servis.\n\n**Ještě jednou děkujeme za vaši podporu a pochopení.**",
|
"message": "#### Vážený uživateli,\n\nDěkujeme za váš zájem a podporu. V důsledku úpravy provozní strategie jsme uzavřeli registraci nových uživatelů. Během této doby nebude používání stávajících účtů nijak ovlivněno.\n\nSnažíme se vám poskytnout lepší služby a zážitky, a proto během uzavření registrace provedeme komplexní optimalizaci systému a aktualizaci funkcí. V budoucnu vás přivítáme s kvalitnějším obsahem a službami.\n\nSledujte naše webové stránky a sociální média pro nejnovější aktualizace a oznámení. Děkujeme za vaše pochopení a podporu.\n\nPokud máte jakékoli dotazy nebo potřebujete pomoc, neváhejte kontaktovat náš zákaznický servis.\n\n**Ještě jednou děkujeme za vaši podporu a pochopení.**",
|
||||||
"passwordMismatch": "Hesla se neshodují",
|
"passwordMismatch": "Hesla se neshodují",
|
||||||
"success": "Registrace úspěšná, automaticky přihlášeno!",
|
"success": "Registrace úspěšná, automaticky přihlášeno!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Blokovat virtuální stroj",
|
||||||
|
"blockVirtualMachineDescription": "Pokud je povoleno, zařízení nebudou moci běžet na virtuálních strojích nebo emulátorech",
|
||||||
|
"communicationKey": "Komunikační klíč",
|
||||||
|
"communicationKeyDescription": "Komunikační klíč se používá pro bezpečnou komunikaci mezi zařízeními a servery",
|
||||||
"enable": "Povolit",
|
"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"
|
"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",
|
||||||
|
"enableSecurity": "Povolit šifrování komunikace",
|
||||||
|
"enableSecurityDescription": "Pokud je povoleno, komunikace mezi zařízeními a servery bude šifrována",
|
||||||
|
"saveFailed": "Uložení se nezdařilo",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné",
|
||||||
|
"showAds": "Zobrazit reklamy",
|
||||||
|
"showAdsDescription": "Pokud je povoleno, na zařízeních se budou zobrazovat reklamy"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
"subject": "Předmět",
|
"subject": "Předmět",
|
||||||
"template": "Šablona",
|
"template": "Šablona",
|
||||||
|
"traffic_exceed_email_template": "Šablona oznámení o překročení provozu",
|
||||||
|
"traffic_exceed_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
|
||||||
"verify_email_template": "Šablona ověřovacího e-mailu",
|
"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.",
|
"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",
|
"whitelistSuffixes": "Povolené přípony",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "Konfigurace ADS",
|
||||||
"Announcement Management": "Správa oznámení",
|
"Announcement Management": "Správa oznámení",
|
||||||
"Apple": "Apple ID",
|
"Apple": "Apple ID",
|
||||||
|
"Application Management": "Správa aplikací",
|
||||||
"Auth Control": "Řízení ověřování",
|
"Auth Control": "Řízení ověřování",
|
||||||
"Coupon Management": "Správa kupónů",
|
"Coupon Management": "Správa kupónů",
|
||||||
"Dashboard": "Přístrojová deska",
|
"Dashboard": "Přístrojová deska",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Správa objednávek",
|
"Order Management": "Správa objednávek",
|
||||||
"Payment Config": "Konfigurace platby",
|
"Payment Config": "Konfigurace platby",
|
||||||
"Phone Number": "Telefonní číslo",
|
"Phone Number": "Telefonní číslo",
|
||||||
|
"Rule Management": "Správa pravidel",
|
||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Server Management": "Správa serveru",
|
"Server Management": "Správa serveru",
|
||||||
"Settings": "Nastavení",
|
"Settings": "Nastavení",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Platba Alipay",
|
"AlipayF2F": "Alipay Osobně",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "ID aplikace Alipay",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Název vlastního produktu",
|
"applyForPayment": "Žádat o platbu",
|
||||||
"invoiceNameDescription": "Bude zobrazeno na faktuře Alipay",
|
"balance": "Zůstatek",
|
||||||
"privateKey": "Soukromý klíč Alipay",
|
"batchDelete": "Hromadné smazání",
|
||||||
"publicKey": "Veřejný klíč Alipay",
|
"cancel": "Zrušit",
|
||||||
"sandbox": "Režim sandbox",
|
"config": "Nastavení",
|
||||||
"sandboxDescription": "Povolte režim sandbox pro testování plateb"
|
"configInfo": "Informace o nastavení",
|
||||||
},
|
"configPlaceholder": "Vyplňte prosím poskytnutou konfiguraci {field}",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"confirmDelete": "Potvrdit smazání",
|
||||||
|
"copy": "Kopírovat",
|
||||||
|
"copySuccess": "Úspěšně zkopírováno",
|
||||||
|
"create": "Přidat platební metodu",
|
||||||
|
"createPayment": "Přidat platební metodu",
|
||||||
|
"createSuccess": "Úspěšně vytvořeno",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"deleteSuccess": "Úspěšně smazáno",
|
||||||
|
"deleteWarning": "Opravdu chcete smazat tuto platební metodu? Tuto akci nelze vrátit zpět.",
|
||||||
|
"description": "Popis",
|
||||||
|
"descriptionPlaceholder": "Zadejte popis platební metody",
|
||||||
|
"domain": "Doména",
|
||||||
|
"domainPlaceholder": "volitelné, např. {example}",
|
||||||
|
"edit": "Upravit",
|
||||||
|
"editPayment": "Upravit platební metodu",
|
||||||
"enable": "Povolit",
|
"enable": "Povolit",
|
||||||
"enableDescription": "Povolit tento způsob platby",
|
"feeAmount": "Pevná částka",
|
||||||
"epay": {
|
|
||||||
"key": "KLÍČ",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Způsob účtování",
|
"feeMode": "Způsob účtování",
|
||||||
"feeModeDescription": "Způsob účtování dodatečných poplatků",
|
|
||||||
"feeModeItems": ["Bez poplatku", "Procento", "Pevná částka", "Obojí výše uvedené"],
|
|
||||||
"feePercent": "Procentuální poplatek (volitelně)",
|
"feePercent": "Procentuální poplatek (volitelně)",
|
||||||
"feePercentDescription": "Přidání poplatku k částce objednávky",
|
|
||||||
"fixedFee": "Pevný poplatek (volitelný)",
|
"fixedFee": "Pevný poplatek (volitelný)",
|
||||||
"fixedFeeDescription": "Přidání pevného poplatku k částce objednávky",
|
"handlingFee": "Poplatek za zpracování",
|
||||||
"iconUrl": "URL ikony (volitelné)",
|
"icon": "Ikona",
|
||||||
"iconUrlDescription": "Používá se pro zobrazení na frontendu",
|
"iconPlaceholder": "Zadejte URL ikony",
|
||||||
"inputPlaceholder": "Prosím zadejte",
|
"mark": "Identifikátor",
|
||||||
"key": "KLÍČ",
|
"markPlaceholder": "Zadejte identifikátor platební metody",
|
||||||
"notifyUrl": "Vlastní oznámení URL (volitelné)",
|
"name": "Název",
|
||||||
"notifyUrlDescription": "Oznámení brány budou odeslána na tuto doménu",
|
"namePlaceholder": "Zadejte název platební metody",
|
||||||
"payForRecommendations": "Zaplatit za doporučení",
|
"nameRequired": "Název je povinný",
|
||||||
"pid": "PID",
|
"noFee": "Žádný poplatek",
|
||||||
"saveSuccess": "Uložení bylo úspěšné",
|
"notify_url": "URL pro oznámení",
|
||||||
"showName": "Zobrazit název",
|
"paymentManagement": "Správa plateb",
|
||||||
"showNameDescription": "Používá se pro zobrazení na frontendu",
|
"percentFee": "Procento",
|
||||||
"stripe": {
|
"platform": "Platforma",
|
||||||
"publicKey": "VEŘEJNÝ KLÍČ",
|
"searchPlaceholder": "Zadejte hledané výrazy",
|
||||||
"secretKey": "TAJNÝ KLÍČ",
|
"selectPlatform": "Vyberte platformu",
|
||||||
"webhookSecret": "PODPIS WEBHOOK KLÍČE"
|
"selectType": "Vyberte typ platby",
|
||||||
},
|
"submit": "Odeslat",
|
||||||
"url": "URL",
|
"type": "Typ",
|
||||||
"wechatPay": "WeChat Pay"
|
"updateSuccess": "Úspěšně aktualizováno"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analyzuji...",
|
||||||
|
"appIcon": "Ikona pravidla",
|
||||||
|
"cancel": "Zrušit",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"confirmDelete": "Potvrdit smazání",
|
||||||
|
"create": "Přidat pravidlo",
|
||||||
|
"createRule": "Přidat pravidlo",
|
||||||
|
"createSuccess": "Pravidlo bylo úspěšně vytvořeno",
|
||||||
|
"createdAt": "Vytvořeno dne",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"deleteSuccess": "Pravidlo bylo úspěšně smazáno",
|
||||||
|
"deleteWarning": "Opravdu chcete smazat toto pravidlo? Tato akce není vratná.",
|
||||||
|
"description": "Popis pravidla",
|
||||||
|
"downloadTemplate": "Stáhnout šablonu",
|
||||||
|
"edit": "Upravit",
|
||||||
|
"editRule": "Upravit pravidlo",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enterIconUrl": "Zadejte nebo nahrajte ikonu",
|
||||||
|
"enterRuleName": "Zadejte název pravidla",
|
||||||
|
"enterRules": "Zadejte obsah pravidla, jedno na řádek",
|
||||||
|
"import": "Importovat",
|
||||||
|
"importFailed": "Import se nezdařil",
|
||||||
|
"importSuccess": "Import byl úspěšný",
|
||||||
|
"importYaml": "Importovat YAML",
|
||||||
|
"importYamlDescription": "Importujte pravidla ze souboru YAML, systém automaticky vytvoří skupiny pravidel na základě názvů skupin politiky",
|
||||||
|
"importYamlRules": "Importovat YAML pravidla",
|
||||||
|
"importing": "Importuji...",
|
||||||
|
"invalidYamlFormat": "Neplatný formát YAML, chybí pole pravidel",
|
||||||
|
"name": "Název pravidla",
|
||||||
|
"noValidRules": "Nebyly nalezeny žádné platné pravidla",
|
||||||
|
"pleaseUploadFile": "Prosím, nahrajte soubor YAML",
|
||||||
|
"preview": "Náhled",
|
||||||
|
"rules": "Obsah pravidla",
|
||||||
|
"rulesFormat": "Formát pravidla: typ pravidla, obsah shody, [politika], kde politika je volitelná.\nPokud politika není specifikována, bude automaticky použito aktuální jméno skupiny pravidel. Příklady:",
|
||||||
|
"rulesLabel": "Obsah pravidla",
|
||||||
|
"searchRule": "Hledat název pravidla",
|
||||||
|
"selectFile": "Vybrat soubor",
|
||||||
|
"selectTags": "Vybrat značky uzlu",
|
||||||
|
"tags": "Značky uzlu",
|
||||||
|
"tagsLabel": "Značky uzlu",
|
||||||
|
"updateSuccess": "Pravidlo bylo úspěšně aktualizováno"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Zásady ochrany osobních údajů"
|
"title": "Zásady ochrany osobních údajů"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Vlastní data",
|
||||||
|
"customDataDescription": "Vlastní data, používaná pro vlastní data webu",
|
||||||
"customHtml": "Vlastní HTML",
|
"customHtml": "Vlastní HTML",
|
||||||
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu body na stránce.",
|
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu body na stránce.",
|
||||||
"keywords": "Klíčová slova",
|
"keywords": "Klíčová slova",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Povolení účtu",
|
"accountEnable": "Povolení účtu",
|
||||||
"actions": "akce",
|
"actions": "akce",
|
||||||
|
"active": "Aktivní",
|
||||||
"add": "Přidat",
|
"add": "Přidat",
|
||||||
"administrator": "Administrátor",
|
"administrator": "Administrátor",
|
||||||
"areaCode": "Směrový kód",
|
"areaCode": "Směrový kód",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "e-mail",
|
"email": "e-mail",
|
||||||
"enable": "Povolit",
|
"enable": "Povolit",
|
||||||
"expireTime": "Čas vypršení",
|
"expireTime": "Čas vypršení",
|
||||||
|
"expired": "Vypršelo",
|
||||||
"expiredAt": "Platnost vypršela",
|
"expiredAt": "Platnost vypršela",
|
||||||
"failed": "Neúspěch",
|
"failed": "Neúspěch",
|
||||||
"giftAmount": "Částka dárku",
|
"giftAmount": "Částka dárku",
|
||||||
"giftAmountPlaceholder": "Zadejte částku dárku",
|
"giftAmountPlaceholder": "Zadejte částku dárku",
|
||||||
|
"inactive": "Neaktivní",
|
||||||
"invalidEmailFormat": "Neplatný formát e-mailu",
|
"invalidEmailFormat": "Neplatný formát e-mailu",
|
||||||
"inviteCode": "Pozvánkový kód",
|
"inviteCode": "Pozvánkový kód",
|
||||||
"inviteCodePlaceholder": "Zadejte kód pozvánky (ponechte prázdné pro vygenerování)",
|
"inviteCodePlaceholder": "Zadejte kód pozvánky (ponechte prázdné pro vygenerování)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Zadejte nové heslo (volitelné)",
|
"passwordPlaceholder": "Zadejte nové heslo (volitelné)",
|
||||||
"permanent": "Trvalý",
|
"permanent": "Trvalý",
|
||||||
"pleaseEnterEmail": "Prosím, zadejte e-mail",
|
"pleaseEnterEmail": "Prosím, zadejte e-mail",
|
||||||
|
"price": "Cena",
|
||||||
"referer": "Doporučitel",
|
"referer": "Doporučitel",
|
||||||
"refererId": "ID doporučitele",
|
"refererId": "ID doporučitele",
|
||||||
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "Vyhledat IP adresu",
|
"searchIp": "Vyhledat IP adresu",
|
||||||
"selectAuthType": "Vyberte typ ověření",
|
"selectAuthType": "Vyberte typ ověření",
|
||||||
"speedLimit": "Rychlostní limit",
|
"speedLimit": "Rychlostní limit",
|
||||||
|
"startTime": "Čas zahájení",
|
||||||
|
"status": "Stav",
|
||||||
"subscription": "Předplatné",
|
"subscription": "Předplatné",
|
||||||
"subscriptionDetails": "Podrobnosti o předplatném",
|
"subscriptionDetails": "Podrobnosti o předplatném",
|
||||||
|
"subscriptionId": "ID předplatného",
|
||||||
|
"subscriptionInfo": "Informace o předplatném",
|
||||||
"subscriptionList": "Seznam předplatných",
|
"subscriptionList": "Seznam předplatných",
|
||||||
"subscriptionLogs": "Záznamy o předplatném",
|
"subscriptionLogs": "Záznamy o předplatném",
|
||||||
"subscriptionName": "Název předplatného",
|
"subscriptionName": "Název předplatného",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Obchodní oznámení",
|
"tradeNotifications": "Obchodní oznámení",
|
||||||
"trafficLimit": "Limit provozu",
|
"trafficLimit": "Limit provozu",
|
||||||
"trafficLogs": "Protokoly provozu",
|
"trafficLogs": "Protokoly provozu",
|
||||||
|
"trafficUsage": "Využití dat",
|
||||||
|
"unknown": "Neznámé",
|
||||||
"unlimited": "Neomezený",
|
"unlimited": "Neomezený",
|
||||||
"unverified": "Neověřeno",
|
"unverified": "Neověřeno",
|
||||||
"update": "Aktualizovat",
|
"update": "Aktualizovat",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "Uživatelský agent",
|
"userAgent": "Uživatelský agent",
|
||||||
"userEmail": "Uživatelský e-mail",
|
"userEmail": "Uživatelský e-mail",
|
||||||
"userEmailPlaceholder": "Zadejte e-mail uživatele",
|
"userEmailPlaceholder": "Zadejte e-mail uživatele",
|
||||||
|
"userId": "ID uživatele",
|
||||||
|
"userInfo": "Informace o uživateli",
|
||||||
"userList": "Seznam uživatelů",
|
"userList": "Seznam uživatelů",
|
||||||
"userLogs": "Historie přihlášení",
|
"userLogs": "Historie přihlášení",
|
||||||
"userName": "Identifikátor uživatele",
|
"userName": "Identifikátor uživatele",
|
||||||
"userOrders": "Objednávky",
|
"userOrders": "Objednávky",
|
||||||
"userProfile": "Profil",
|
"userProfile": "Profil",
|
||||||
"userSubscriptions": "Předplatné",
|
"userSubscriptions": "Předplatné",
|
||||||
"verified": "Ověřeno"
|
"username": "Uživatelské jméno",
|
||||||
|
"verified": "Ověřeno",
|
||||||
|
"viewDetails": "Zobrazit podrobnosti"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"confirmDelete": "Löschen bestätigen",
|
||||||
|
"create": "Anzeige erstellen",
|
||||||
|
"createAds": "Anzeige erstellen",
|
||||||
|
"createSuccess": "Erfolgreich erstellt",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"deleteSuccess": "Erfolgreich gelöscht",
|
||||||
|
"deleteWarning": "Sind Sie sicher, dass Sie diese Anzeige löschen möchten?",
|
||||||
|
"disabled": "Deaktiviert",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"editAds": "Anzeige bearbeiten",
|
||||||
|
"enabled": "Aktiviert",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"content": "Inhalt",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"endTime": "Endzeit",
|
||||||
|
"enterContent": "Bitte geben Sie den Anzeigeninhalt ein",
|
||||||
|
"enterDescription": "Bitte geben Sie die Anzeigenbeschreibung ein",
|
||||||
|
"enterEndTime": "Bitte wählen Sie die Endzeit aus",
|
||||||
|
"enterStartTime": "Bitte wählen Sie die Startzeit aus",
|
||||||
|
"enterTargetUrl": "Bitte geben Sie die Ziel-URL ein",
|
||||||
|
"enterTitle": "Bitte geben Sie den Anzeigentitel ein",
|
||||||
|
"startTime": "Startzeit",
|
||||||
|
"targetUrl": "Ziel-URL",
|
||||||
|
"title": "Titel",
|
||||||
|
"type": "Anzeigentyp",
|
||||||
|
"typeImage": "Bild",
|
||||||
|
"typeVideo": "Video"
|
||||||
|
},
|
||||||
|
"status": "Status",
|
||||||
|
"targetUrl": "Ziel-URL",
|
||||||
|
"title": "Titel",
|
||||||
|
"type": "Typ",
|
||||||
|
"updateSuccess": "Erfolgreich aktualisiert",
|
||||||
|
"validityPeriod": "Gültigkeitsdauer"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||||
"existingAccount": "Bereits ein Konto?",
|
"existingAccount": "Bereits ein Konto?",
|
||||||
"get": "Erhalten",
|
"get": "Erhalten",
|
||||||
"invite": "Einladungscode",
|
"invite": "Einladungscode (optional)",
|
||||||
"message": "#### Sehr geehrter Nutzer,\n\nVielen Dank für Ihr Interesse und Ihre Unterstützung. Aufgrund einer Anpassung unserer Betriebsstrategie haben wir die Registrierung für neue Nutzer geschlossen. Während dieser Zeit wird die Nutzung für bestehende Nutzer nicht beeinträchtigt.\n\nWir sind bestrebt, Ihnen einen besseren Service und ein besseres Erlebnis zu bieten, und werden daher während der Schließung der Registrierung umfassende Systemoptimierungen und Funktionsupgrades durchführen. In Zukunft werden wir Sie mit hochwertigeren Inhalten und Dienstleistungen willkommen heißen.\n\nBitte folgen Sie unserer Website und unseren Social-Media-Plattformen, um die neuesten Updates und Benachrichtigungen zu erhalten. Vielen Dank für Ihr Verständnis und Ihre Unterstützung.\n\nBei Fragen oder wenn Sie Hilfe benötigen, kontaktieren Sie bitte jederzeit unser Kundenserviceteam.\n\n**Nochmals vielen Dank für Ihre Unterstützung und Ihr Verständnis.**",
|
"message": "#### Sehr geehrter Nutzer,\n\nVielen Dank für Ihr Interesse und Ihre Unterstützung. Aufgrund einer Anpassung unserer Betriebsstrategie haben wir die Registrierung für neue Nutzer geschlossen. Während dieser Zeit wird die Nutzung für bestehende Nutzer nicht beeinträchtigt.\n\nWir sind bestrebt, Ihnen einen besseren Service und ein besseres Erlebnis zu bieten, und werden daher während der Schließung der Registrierung umfassende Systemoptimierungen und Funktionsupgrades durchführen. In Zukunft werden wir Sie mit hochwertigeren Inhalten und Dienstleistungen willkommen heißen.\n\nBitte folgen Sie unserer Website und unseren Social-Media-Plattformen, um die neuesten Updates und Benachrichtigungen zu erhalten. Vielen Dank für Ihr Verständnis und Ihre Unterstützung.\n\nBei Fragen oder wenn Sie Hilfe benötigen, kontaktieren Sie bitte jederzeit unser Kundenserviceteam.\n\n**Nochmals vielen Dank für Ihre Unterstützung und Ihr Verständnis.**",
|
||||||
"passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein",
|
"passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein",
|
||||||
"success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!",
|
"success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Virtuelle Maschine blockieren",
|
||||||
|
"blockVirtualMachineDescription": "Wenn aktiviert, wird verhindert, dass Geräte auf virtuellen Maschinen oder Emulatoren ausgeführt werden.",
|
||||||
|
"communicationKey": "Kommunikationsschlüssel",
|
||||||
|
"communicationKeyDescription": "Der Kommunikationsschlüssel wird für die sichere Kommunikation zwischen Geräten und Servern verwendet.",
|
||||||
"enable": "Aktivieren",
|
"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."
|
"enableDescription": "Nach der Aktivierung werden mehrere Geräteidentifikatoren wie IMEI/IDFA/IDFV/AndroidID/Mac-Adresse für die Anmeldung und Registrierung unterstützt.",
|
||||||
|
"enableSecurity": "Kommunikationsverschlüsselung aktivieren",
|
||||||
|
"enableSecurityDescription": "Wenn aktiviert, wird die Kommunikation zwischen Geräten und Servern verschlüsselt.",
|
||||||
|
"saveFailed": "Speichern fehlgeschlagen",
|
||||||
|
"saveSuccess": "Speichern erfolgreich",
|
||||||
|
"showAds": "Werbung anzeigen",
|
||||||
|
"showAdsDescription": "Wenn aktiviert, werden auf den Geräten Anzeigen angezeigt."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"subject": "Betreff",
|
"subject": "Betreff",
|
||||||
"template": "Vorlage",
|
"template": "Vorlage",
|
||||||
|
"traffic_exceed_email_template": "Benachrichtigung über überschreitenden Verkehr",
|
||||||
|
"traffic_exceed_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
|
||||||
"verify_email_template": "E-Mail-Bestätigungsvorlage",
|
"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.",
|
"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",
|
"whitelistSuffixes": "Whitelist-Suffixe",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "ADS-Konfiguration",
|
||||||
"Announcement Management": "Ankündigungsverwaltung",
|
"Announcement Management": "Ankündigungsverwaltung",
|
||||||
"Apple": "Apple-ID",
|
"Apple": "Apple-ID",
|
||||||
|
"Application Management": "Anwendungsverwaltung",
|
||||||
"Auth Control": "Authentifizierungskontrolle",
|
"Auth Control": "Authentifizierungskontrolle",
|
||||||
"Coupon Management": "Gutscheinverwaltung",
|
"Coupon Management": "Gutscheinverwaltung",
|
||||||
"Dashboard": "Armaturenbrett",
|
"Dashboard": "Armaturenbrett",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Bestellverwaltung",
|
"Order Management": "Bestellverwaltung",
|
||||||
"Payment Config": "Zahlungskonfiguration",
|
"Payment Config": "Zahlungskonfiguration",
|
||||||
"Phone Number": "Telefonnummer",
|
"Phone Number": "Telefonnummer",
|
||||||
|
"Rule Management": "Regelverwaltung",
|
||||||
"Server": "Dienst",
|
"Server": "Dienst",
|
||||||
"Server Management": "Serververwaltung",
|
"Server Management": "Serververwaltung",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Alipay-Zahlung",
|
"AlipayF2F": "Alipay Gesicht zu Gesicht",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "Alipay Anwendungs-ID",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Benutzerdefinierter Produktname",
|
"applyForPayment": "Zahlung beantragen",
|
||||||
"invoiceNameDescription": "Wird in der Alipay-Rechnung angezeigt",
|
"balance": "Guthaben",
|
||||||
"privateKey": "Alipay Privatschlüssel",
|
"batchDelete": "Massenlöschung",
|
||||||
"publicKey": "Alipay Öffentlicher Schlüssel",
|
"cancel": "Abbrechen",
|
||||||
"sandbox": "Sandbox-Modus",
|
"config": "Konfiguration",
|
||||||
"sandboxDescription": "Aktivieren Sie den Sandbox-Modus, um Zahlungen zu testen"
|
"configInfo": "Konfigurationsinformationen",
|
||||||
},
|
"configPlaceholder": "Bitte füllen Sie die bereitgestellte {field} Konfiguration aus",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"confirmDelete": "Löschen bestätigen",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"copySuccess": "Erfolgreich kopiert",
|
||||||
|
"create": "Zahlungsmethode hinzufügen",
|
||||||
|
"createPayment": "Zahlungsmethode hinzufügen",
|
||||||
|
"createSuccess": "Erfolgreich erstellt",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"deleteSuccess": "Erfolgreich gelöscht",
|
||||||
|
"deleteWarning": "Sind Sie sicher, dass Sie diese Zahlungsmethode löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"descriptionPlaceholder": "Geben Sie die Beschreibung der Zahlungsmethode ein",
|
||||||
|
"domain": "Domain",
|
||||||
|
"domainPlaceholder": "optional, z.B. {example}",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"editPayment": "Zahlungsmethode bearbeiten",
|
||||||
"enable": "Aktivieren",
|
"enable": "Aktivieren",
|
||||||
"enableDescription": "Diese Zahlungsmethode aktivieren",
|
"feeAmount": "Fester Betrag",
|
||||||
"epay": {
|
|
||||||
"key": "SCHLÜSSEL",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Gebührenmodus",
|
"feeMode": "Gebührenmodus",
|
||||||
"feeModeDescription": "Gebührenmodus für zusätzliche Gebühren",
|
|
||||||
"feeModeItems": ["Keine Gebühr", "Prozentsatz", "Fester Betrag", "Beides"],
|
|
||||||
"feePercent": "Prozentgebühr (optional)",
|
"feePercent": "Prozentgebühr (optional)",
|
||||||
"feePercentDescription": "Gebühren werden zusätzlich zum Bestellbetrag hinzugefügt",
|
|
||||||
"fixedFee": "Feste Gebühr (optional)",
|
"fixedFee": "Feste Gebühr (optional)",
|
||||||
"fixedFeeDescription": "Feste Gebühr zusätzlich zum Bestellbetrag hinzufügen",
|
"handlingFee": "Bearbeitungsgebühr",
|
||||||
"iconUrl": "Symbol-URL (optional)",
|
"icon": "Symbol",
|
||||||
"iconUrlDescription": "Zur Anzeige im Frontend verwendet",
|
"iconPlaceholder": "Geben Sie die URL des Symbols ein",
|
||||||
"inputPlaceholder": "Bitte eingeben",
|
"mark": "Kennung",
|
||||||
"key": "SCHLÜSSEL",
|
"markPlaceholder": "Geben Sie die Kennung der Zahlungsmethode ein",
|
||||||
"notifyUrl": "Benutzerdefinierte Benachrichtigungs-URL (optional)",
|
"name": "Name",
|
||||||
"notifyUrlDescription": "Gateway-Benachrichtigungen werden an diese Domain gesendet",
|
"namePlaceholder": "Geben Sie den Namen der Zahlungsmethode ein",
|
||||||
"payForRecommendations": "Für Empfehlungen bezahlen",
|
"nameRequired": "Name ist erforderlich",
|
||||||
"pid": "PID",
|
"noFee": "Keine Gebühr",
|
||||||
"saveSuccess": "Erfolgreich gespeichert",
|
"notify_url": "Benachrichtigungs-URL",
|
||||||
"showName": "Anzeigename",
|
"paymentManagement": "Zahlungsverwaltung",
|
||||||
"showNameDescription": "Zur Anzeige im Frontend verwendet",
|
"percentFee": "Prozentsatz",
|
||||||
"stripe": {
|
"platform": "Plattform",
|
||||||
"publicKey": "ÖFFENTLICHER SCHLÜSSEL",
|
"searchPlaceholder": "Suchbegriffe eingeben",
|
||||||
"secretKey": "GEHEIMER SCHLÜSSEL",
|
"selectPlatform": "Plattform auswählen",
|
||||||
"webhookSecret": "WEBHOOK-SCHLÜSSEL-SIGNATUR"
|
"selectType": "Zahlungsart auswählen",
|
||||||
},
|
"submit": "Einreichen",
|
||||||
"url": "URL",
|
"type": "Typ",
|
||||||
"wechatPay": "WeChat Pay"
|
"updateSuccess": "Erfolgreich aktualisiert"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analysiere...",
|
||||||
|
"appIcon": "Regel-Icon",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"confirmDelete": "Löschen bestätigen",
|
||||||
|
"create": "Regel hinzufügen",
|
||||||
|
"createRule": "Regel hinzufügen",
|
||||||
|
"createSuccess": "Regel erfolgreich erstellt",
|
||||||
|
"createdAt": "Erstellt am",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"deleteSuccess": "Regel erfolgreich gelöscht",
|
||||||
|
"deleteWarning": "Sind Sie sicher, dass Sie diese Regel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"description": "Regelbeschreibung",
|
||||||
|
"downloadTemplate": "Vorlage herunterladen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"editRule": "Regel bearbeiten",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enterIconUrl": "Geben Sie eine URL für das Icon ein oder laden Sie es hoch",
|
||||||
|
"enterRuleName": "Geben Sie den Regelname ein",
|
||||||
|
"enterRules": "Geben Sie den Regelinhalt ein, jeweils eine Regel pro Zeile",
|
||||||
|
"import": "Importieren",
|
||||||
|
"importFailed": "Import fehlgeschlagen",
|
||||||
|
"importSuccess": "Import erfolgreich",
|
||||||
|
"importYaml": "YAML importieren",
|
||||||
|
"importYamlDescription": "Regeln aus einer YAML-Datei importieren, das System erstellt automatisch Regelgruppen basierend auf den Namen der Richtliniengruppen",
|
||||||
|
"importYamlRules": "YAML-Regeln importieren",
|
||||||
|
"importing": "Importiere...",
|
||||||
|
"invalidYamlFormat": "Ungültiges YAML-Format, das Feld 'Regeln' fehlt",
|
||||||
|
"name": "Regelname",
|
||||||
|
"noValidRules": "Keine gültigen Regeln gefunden",
|
||||||
|
"pleaseUploadFile": "Bitte laden Sie eine YAML-Datei hoch",
|
||||||
|
"preview": "Vorschau",
|
||||||
|
"rules": "Regelinhalt",
|
||||||
|
"rulesFormat": "Regelformat: Regeltyp, Übereinstimmungsinhalt, [Richtlinie], wobei die Richtlinie optional ist.\nWenn keine Richtlinie angegeben ist, wird automatisch der aktuelle Regelgruppenname verwendet. Beispiele:",
|
||||||
|
"rulesLabel": "Regelinhalt",
|
||||||
|
"searchRule": "Regelname suchen",
|
||||||
|
"selectFile": "Datei auswählen",
|
||||||
|
"selectTags": "Knoten-Tags auswählen",
|
||||||
|
"tags": "Knoten-Tags",
|
||||||
|
"tagsLabel": "Knoten-Tags",
|
||||||
|
"updateSuccess": "Regel erfolgreich aktualisiert"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Datenschutzrichtlinie"
|
"title": "Datenschutzrichtlinie"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Benutzerdefinierte Daten",
|
||||||
|
"customDataDescription": "Benutzerdefinierte Daten, die für benutzerdefinierte Daten der Website verwendet werden",
|
||||||
"customHtml": "Benutzerdefiniertes HTML",
|
"customHtml": "Benutzerdefiniertes HTML",
|
||||||
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des body-Tags der Seite eingefügt werden soll.",
|
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des body-Tags der Seite eingefügt werden soll.",
|
||||||
"keywords": "Schlüsselwörter",
|
"keywords": "Schlüsselwörter",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Konto aktivieren",
|
"accountEnable": "Konto aktivieren",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
|
"active": "Aktiv",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"areaCode": "Vorwahl",
|
"areaCode": "Vorwahl",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"enable": "Aktivieren",
|
"enable": "Aktivieren",
|
||||||
"expireTime": "Ablaufzeit",
|
"expireTime": "Ablaufzeit",
|
||||||
|
"expired": "Abgelaufen",
|
||||||
"expiredAt": "Abgelaufen am",
|
"expiredAt": "Abgelaufen am",
|
||||||
"failed": "Fehlgeschlagen",
|
"failed": "Fehlgeschlagen",
|
||||||
"giftAmount": "Geschenkbetrag",
|
"giftAmount": "Geschenkbetrag",
|
||||||
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
|
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
|
||||||
|
"inactive": "Inaktiv",
|
||||||
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
||||||
"inviteCode": "Einladungscode",
|
"inviteCode": "Einladungscode",
|
||||||
"inviteCodePlaceholder": "Einladungscode eingeben (leer lassen, um zu generieren)",
|
"inviteCodePlaceholder": "Einladungscode eingeben (leer lassen, um zu generieren)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Neues Passwort eingeben (optional)",
|
"passwordPlaceholder": "Neues Passwort eingeben (optional)",
|
||||||
"permanent": "Dauerhaft",
|
"permanent": "Dauerhaft",
|
||||||
"pleaseEnterEmail": "Bitte E-Mail eingeben",
|
"pleaseEnterEmail": "Bitte E-Mail eingeben",
|
||||||
|
"price": "Preis",
|
||||||
"referer": "Empfehler",
|
"referer": "Empfehler",
|
||||||
"refererId": "Referenz-ID",
|
"refererId": "Referenz-ID",
|
||||||
"refererIdPlaceholder": "Geben Sie die Referenz-ID ein",
|
"refererIdPlaceholder": "Geben Sie die Referenz-ID ein",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "IP-Adresse suchen",
|
"searchIp": "IP-Adresse suchen",
|
||||||
"selectAuthType": "Authentifizierungstyp auswählen",
|
"selectAuthType": "Authentifizierungstyp auswählen",
|
||||||
"speedLimit": "Geschwindigkeitsbegrenzung",
|
"speedLimit": "Geschwindigkeitsbegrenzung",
|
||||||
|
"startTime": "Startzeit",
|
||||||
|
"status": "Status",
|
||||||
"subscription": "Abonnement",
|
"subscription": "Abonnement",
|
||||||
"subscriptionDetails": "Abonnementdetails",
|
"subscriptionDetails": "Abonnementdetails",
|
||||||
|
"subscriptionId": "Abonnement-ID",
|
||||||
|
"subscriptionInfo": "Abonnement-Informationen",
|
||||||
"subscriptionList": "Abonnementsliste",
|
"subscriptionList": "Abonnementsliste",
|
||||||
"subscriptionLogs": "Abonnementprotokolle",
|
"subscriptionLogs": "Abonnementprotokolle",
|
||||||
"subscriptionName": "Abonnementname",
|
"subscriptionName": "Abonnementname",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Handelsbenachrichtigungen",
|
"tradeNotifications": "Handelsbenachrichtigungen",
|
||||||
"trafficLimit": "Verkehrslimit",
|
"trafficLimit": "Verkehrslimit",
|
||||||
"trafficLogs": "Verkehrsprotokolle",
|
"trafficLogs": "Verkehrsprotokolle",
|
||||||
|
"trafficUsage": "Datenverbrauch",
|
||||||
|
"unknown": "Unbekannt",
|
||||||
"unlimited": "Unbegrenzt",
|
"unlimited": "Unbegrenzt",
|
||||||
"unverified": "Unbestätigt",
|
"unverified": "Unbestätigt",
|
||||||
"update": "Aktualisieren",
|
"update": "Aktualisieren",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "Benutzeragent",
|
"userAgent": "Benutzeragent",
|
||||||
"userEmail": "Benutzer-E-Mail",
|
"userEmail": "Benutzer-E-Mail",
|
||||||
"userEmailPlaceholder": "Benutzer-E-Mail eingeben",
|
"userEmailPlaceholder": "Benutzer-E-Mail eingeben",
|
||||||
|
"userId": "Benutzer-ID",
|
||||||
|
"userInfo": "Benutzerinformationen",
|
||||||
"userList": "Benutzerliste",
|
"userList": "Benutzerliste",
|
||||||
"userLogs": "Anmeldeverlauf",
|
"userLogs": "Anmeldeverlauf",
|
||||||
"userName": "Benutzerkennung",
|
"userName": "Benutzerkennung",
|
||||||
"userOrders": "Bestellungen",
|
"userOrders": "Bestellungen",
|
||||||
"userProfile": "Profil",
|
"userProfile": "Profil",
|
||||||
"userSubscriptions": "Abonnements",
|
"userSubscriptions": "Abonnements",
|
||||||
"verified": "Verifiziert"
|
"username": "Benutzername",
|
||||||
|
"verified": "Verifiziert",
|
||||||
|
"viewDetails": "Details anzeigen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"confirmDelete": "Confirm Delete",
|
||||||
|
"create": "Create Ad",
|
||||||
|
"createAds": "Create Advertisement",
|
||||||
|
"createSuccess": "Successfully created",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteSuccess": "Successfully deleted",
|
||||||
|
"deleteWarning": "Are you sure you want to delete this ad?",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"edit": "Edit",
|
||||||
|
"editAds": "Edit Advertisement",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"content": "Content",
|
||||||
|
"description": "Description",
|
||||||
|
"endTime": "End Time",
|
||||||
|
"enterContent": "Please enter ad content",
|
||||||
|
"enterDescription": "Please enter ad description",
|
||||||
|
"enterEndTime": "Please select end time",
|
||||||
|
"enterStartTime": "Please select start time",
|
||||||
|
"enterTargetUrl": "Please enter target URL",
|
||||||
|
"enterTitle": "Please enter ad title",
|
||||||
|
"startTime": "Start Time",
|
||||||
|
"targetUrl": "Target URL",
|
||||||
|
"title": "Title",
|
||||||
|
"type": "Ad Type",
|
||||||
|
"typeImage": "Image",
|
||||||
|
"typeVideo": "Video"
|
||||||
|
},
|
||||||
|
"status": "Status",
|
||||||
|
"targetUrl": "Target URL",
|
||||||
|
"title": "Title",
|
||||||
|
"type": "Type",
|
||||||
|
"updateSuccess": "Successfully updated",
|
||||||
|
"validityPeriod": "Validity Period"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Please enter a valid email address.",
|
"email": "Please enter a valid email address.",
|
||||||
"existingAccount": "Already have an account?",
|
"existingAccount": "Already have an account?",
|
||||||
"get": "Get",
|
"get": "Get",
|
||||||
"invite": "Invitation code",
|
"invite": "Invitation code (optional)",
|
||||||
"message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**",
|
"message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**",
|
||||||
"passwordMismatch": "Passwords do not match",
|
"passwordMismatch": "Passwords do not match",
|
||||||
"success": "Registration successful, automatically logged in!",
|
"success": "Registration successful, automatically logged in!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Block Virtual Machine",
|
||||||
|
"blockVirtualMachineDescription": "When enabled, devices will be prevented from running on virtual machines or emulators",
|
||||||
|
"communicationKey": "Communication Key",
|
||||||
|
"communicationKeyDescription": "Communication key is used for secure communication between devices and servers",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"enableDescription": "After enabling, multiple device identifiers such as IMEI/IDFA/IDFV/AndroidID/Mac address are supported for login and registration"
|
"enableDescription": "After enabling, multiple device identifiers such as IMEI/IDFA/IDFV/AndroidID/Mac address are supported for login and registration",
|
||||||
|
"enableSecurity": "Enable Communication Encryption",
|
||||||
|
"enableSecurityDescription": "When enabled, the communication between devices and servers will be encrypted",
|
||||||
|
"saveFailed": "Save failed",
|
||||||
|
"saveSuccess": "Save successful",
|
||||||
|
"showAds": "Show Advertisements",
|
||||||
|
"showAdsDescription": "When enabled, advertisements will be displayed on devices"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"subject": "Subject",
|
"subject": "Subject",
|
||||||
"template": "Template",
|
"template": "Template",
|
||||||
|
"traffic_exceed_email_template": "Traffic Exceed Notice Template",
|
||||||
|
"traffic_exceed_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
|
||||||
"verify_email_template": "Verification Email 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.",
|
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
|
||||||
"whitelistSuffixes": "Whitelist Suffixes",
|
"whitelistSuffixes": "Whitelist Suffixes",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "ADS Config",
|
||||||
"Announcement Management": "Announcement Management",
|
"Announcement Management": "Announcement Management",
|
||||||
"Apple": "Apple ID",
|
"Apple": "Apple ID",
|
||||||
|
"Application Management": "Application Management",
|
||||||
"Auth Control": "Auth Control",
|
"Auth Control": "Auth Control",
|
||||||
"Coupon Management": "Coupon Management",
|
"Coupon Management": "Coupon Management",
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Order Management",
|
"Order Management": "Order Management",
|
||||||
"Payment Config": "Payment Config",
|
"Payment Config": "Payment Config",
|
||||||
"Phone Number": "Phone Number",
|
"Phone Number": "Phone Number",
|
||||||
|
"Rule Management": "Rule Management",
|
||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Server Management": "Server Management",
|
"Server Management": "Server Management",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Alipay payment",
|
"AlipayF2F": "Alipay Face to Face",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "Alipay App ID",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Custom Product Name",
|
"applyForPayment": "Apply for Payment",
|
||||||
"invoiceNameDescription": "Will be displayed on Alipay bill",
|
"balance": "Balance",
|
||||||
"privateKey": "Alipay Private Key",
|
"batchDelete": "Batch Delete",
|
||||||
"publicKey": "Alipay Public Key",
|
"cancel": "Cancel",
|
||||||
"sandbox": "Sandbox Mode",
|
"config": "Configuration",
|
||||||
"sandboxDescription": "Enable sandbox mode to test payments"
|
"configInfo": "Configuration Information",
|
||||||
},
|
"configPlaceholder": "Please fill in the provided {field} configuration",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"confirmDelete": "Confirm Delete",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copySuccess": "Copied successfully",
|
||||||
|
"create": "Add Payment Method",
|
||||||
|
"createPayment": "Add Payment Method",
|
||||||
|
"createSuccess": "Created successfully",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteSuccess": "Deleted successfully",
|
||||||
|
"deleteWarning": "Are you sure you want to delete this payment method? This action cannot be undone.",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Enter payment method description",
|
||||||
|
"domain": "Domain",
|
||||||
|
"domainPlaceholder": "optional, e.g. {example}",
|
||||||
|
"edit": "Edit",
|
||||||
|
"editPayment": "Edit Payment Method",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"enableDescription": "Enable this payment method",
|
"feeAmount": "Fixed Amount",
|
||||||
"epay": {
|
|
||||||
"key": "KEY",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Fee Mode",
|
"feeMode": "Fee Mode",
|
||||||
"feeModeDescription": "Fee charging method for additional fees",
|
"feePercent": "Fee Percentage",
|
||||||
"feeModeItems": ["No Fee", "Percentage", "Fixed Amount", "Both"],
|
"fixedFee": "Fixed Amount",
|
||||||
"feePercent": "Percentage Fee (optional)",
|
"handlingFee": "Handling Fee",
|
||||||
"feePercentDescription": "Add fee on top of order amount",
|
"icon": "Icon",
|
||||||
"fixedFee": "Fixed Fee (optional)",
|
"iconPlaceholder": "Enter icon URL",
|
||||||
"fixedFeeDescription": "Add fixed fee on top of order amount",
|
"mark": "Identifier",
|
||||||
"iconUrl": "Icon URL (optional)",
|
"markPlaceholder": "Enter payment method identifier",
|
||||||
"iconUrlDescription": "Used for frontend display",
|
"name": "Name",
|
||||||
"inputPlaceholder": "Please enter",
|
"namePlaceholder": "Enter payment method name",
|
||||||
"key": "KEY",
|
"nameRequired": "Name is required",
|
||||||
"notifyUrl": "Custom Notification Domain (optional)",
|
"noFee": "No Fee",
|
||||||
"notifyUrlDescription": "Gateway notifications will be sent to this domain",
|
"notify_url": "Notify URL",
|
||||||
"payForRecommendations": "Pay for Recommendations",
|
"paymentManagement": "Payment Management",
|
||||||
"pid": "PID",
|
"percentFee": "Percentage",
|
||||||
"saveSuccess": "Save Successful",
|
"platform": "Platform",
|
||||||
"showName": "Display Name",
|
"searchPlaceholder": "Enter search terms",
|
||||||
"showNameDescription": "Used for frontend display",
|
"selectPlatform": "Select Platform",
|
||||||
"stripe": {
|
"selectType": "Select payment type",
|
||||||
"publicKey": "PUBLIC KEY",
|
"submit": "Submit",
|
||||||
"secretKey": "SECRET KEY",
|
"type": "Type",
|
||||||
"webhookSecret": "WEBHOOK Secret Key"
|
"updateSuccess": "Updated successfully"
|
||||||
},
|
|
||||||
"url": "URL",
|
|
||||||
"wechatPay": "WeChat Pay"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analyzing...",
|
||||||
|
"appIcon": "Rule Icon",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"confirmDelete": "Confirm Deletion",
|
||||||
|
"create": "Add Rule",
|
||||||
|
"createRule": "Add Rule",
|
||||||
|
"createSuccess": "Rule created successfully",
|
||||||
|
"createdAt": "Created At",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteSuccess": "Rule deleted successfully",
|
||||||
|
"deleteWarning": "Are you sure you want to delete this rule? This action cannot be undone.",
|
||||||
|
"description": "Rule Description",
|
||||||
|
"downloadTemplate": "Download Template",
|
||||||
|
"edit": "Edit",
|
||||||
|
"editRule": "Edit Rule",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enterIconUrl": "Enter or upload an icon",
|
||||||
|
"enterRuleName": "Enter rule name",
|
||||||
|
"enterRules": "Enter rule content, one per line",
|
||||||
|
"import": "Import",
|
||||||
|
"importFailed": "Import failed",
|
||||||
|
"importSuccess": "Import successful",
|
||||||
|
"importYaml": "Import YAML",
|
||||||
|
"importYamlDescription": "Import rules from YAML file, the system will automatically create rule groups based on policy group names",
|
||||||
|
"importYamlRules": "Import YAML Rules",
|
||||||
|
"importing": "Importing...",
|
||||||
|
"invalidYamlFormat": "Invalid YAML format, missing rules field",
|
||||||
|
"name": "Rule Name",
|
||||||
|
"noValidRules": "No valid rules found",
|
||||||
|
"pleaseUploadFile": "Please upload a YAML file",
|
||||||
|
"preview": "Preview",
|
||||||
|
"rules": "Rule Content",
|
||||||
|
"rulesFormat": "Rule format: rule type,match content,[policy], where policy is optional.\nIf policy is not specified, the current rule group name will be used automatically. Examples:",
|
||||||
|
"rulesLabel": "Rule Content",
|
||||||
|
"searchRule": "Search rule name",
|
||||||
|
"selectFile": "Select File",
|
||||||
|
"selectTags": "Select node tags",
|
||||||
|
"tags": "Node Tags",
|
||||||
|
"tagsLabel": "Node Tags",
|
||||||
|
"updateSuccess": "Rule updated successfully"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Privacy Policy"
|
"title": "Privacy Policy"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Custom Data",
|
||||||
|
"customDataDescription": "Custom Data, used for custom data of the site",
|
||||||
"customHtml": "Custom HTML",
|
"customHtml": "Custom HTML",
|
||||||
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's body tag.",
|
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's body tag.",
|
||||||
"keywords": "Keywords",
|
"keywords": "Keywords",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Account Enable",
|
"accountEnable": "Account Enable",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
"active": "Active",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"areaCode": "Area Code",
|
"areaCode": "Area Code",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "Email",
|
"email": "Email",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"expireTime": "Expire Time",
|
"expireTime": "Expire Time",
|
||||||
|
"expired": "Expired",
|
||||||
"expiredAt": "Expired At",
|
"expiredAt": "Expired At",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"giftAmount": "Gift Amount",
|
"giftAmount": "Gift Amount",
|
||||||
"giftAmountPlaceholder": "Enter gift amount",
|
"giftAmountPlaceholder": "Enter gift amount",
|
||||||
|
"inactive": "Inactive",
|
||||||
"invalidEmailFormat": "Invalid email format",
|
"invalidEmailFormat": "Invalid email format",
|
||||||
"inviteCode": "Invite Code",
|
"inviteCode": "Invite Code",
|
||||||
"inviteCodePlaceholder": "Enter invite code (leave blank to generate)",
|
"inviteCodePlaceholder": "Enter invite code (leave blank to generate)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Enter new password (optional)",
|
"passwordPlaceholder": "Enter new password (optional)",
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
"pleaseEnterEmail": "Please enter email",
|
"pleaseEnterEmail": "Please enter email",
|
||||||
|
"price": "Price",
|
||||||
"referer": "Referer",
|
"referer": "Referer",
|
||||||
"refererId": "Referer ID",
|
"refererId": "Referer ID",
|
||||||
"refererIdPlaceholder": "Enter referer ID",
|
"refererIdPlaceholder": "Enter referer ID",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "Search IP address",
|
"searchIp": "Search IP address",
|
||||||
"selectAuthType": "Select auth type",
|
"selectAuthType": "Select auth type",
|
||||||
"speedLimit": "Speed Limit",
|
"speedLimit": "Speed Limit",
|
||||||
|
"startTime": "Start Time",
|
||||||
|
"status": "Status",
|
||||||
"subscription": "Subscription",
|
"subscription": "Subscription",
|
||||||
"subscriptionDetails": "Subscription Details",
|
"subscriptionDetails": "Subscription Details",
|
||||||
|
"subscriptionId": "Subscription ID",
|
||||||
|
"subscriptionInfo": "Subscription Info",
|
||||||
"subscriptionList": "Subscription List",
|
"subscriptionList": "Subscription List",
|
||||||
"subscriptionLogs": "Subscription Logs",
|
"subscriptionLogs": "Subscription Logs",
|
||||||
"subscriptionName": "Subscription Name",
|
"subscriptionName": "Subscription Name",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Trade Notifications",
|
"tradeNotifications": "Trade Notifications",
|
||||||
"trafficLimit": "Traffic Limit",
|
"trafficLimit": "Traffic Limit",
|
||||||
"trafficLogs": "Traffic Logs",
|
"trafficLogs": "Traffic Logs",
|
||||||
|
"trafficUsage": "Traffic Usage",
|
||||||
|
"unknown": "Unknown",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "User Agent",
|
"userAgent": "User Agent",
|
||||||
"userEmail": "User Email",
|
"userEmail": "User Email",
|
||||||
"userEmailPlaceholder": "Enter user email",
|
"userEmailPlaceholder": "Enter user email",
|
||||||
|
"userId": "User ID",
|
||||||
|
"userInfo": "User Info",
|
||||||
"userList": "User List",
|
"userList": "User List",
|
||||||
"userLogs": "Login History",
|
"userLogs": "Login History",
|
||||||
"userName": "User Identifier",
|
"userName": "User Identifier",
|
||||||
"userOrders": "Orders",
|
"userOrders": "Orders",
|
||||||
"userProfile": "Profile",
|
"userProfile": "Profile",
|
||||||
"userSubscriptions": "Subscriptions",
|
"userSubscriptions": "Subscriptions",
|
||||||
"verified": "Verified"
|
"username": "Username",
|
||||||
|
"verified": "Verified",
|
||||||
|
"viewDetails": "View Details"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar eliminación",
|
||||||
|
"create": "Crear anuncio",
|
||||||
|
"createAds": "Crear publicidad",
|
||||||
|
"createSuccess": "Creado con éxito",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Eliminado con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar este anuncio?",
|
||||||
|
"disabled": "Deshabilitado",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editAds": "Editar publicidad",
|
||||||
|
"enabled": "Habilitado",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"content": "Contenido",
|
||||||
|
"description": "Descripción",
|
||||||
|
"endTime": "Hora de finalización",
|
||||||
|
"enterContent": "Por favor, introduzca el contenido del anuncio",
|
||||||
|
"enterDescription": "Por favor, introduzca la descripción del anuncio",
|
||||||
|
"enterEndTime": "Por favor, seleccione la hora de finalización",
|
||||||
|
"enterStartTime": "Por favor, seleccione la hora de inicio",
|
||||||
|
"enterTargetUrl": "Por favor, introduzca la URL de destino",
|
||||||
|
"enterTitle": "Por favor, introduzca el título del anuncio",
|
||||||
|
"startTime": "Hora de inicio",
|
||||||
|
"targetUrl": "URL de destino",
|
||||||
|
"title": "Título",
|
||||||
|
"type": "Tipo de anuncio",
|
||||||
|
"typeImage": "Imagen",
|
||||||
|
"typeVideo": "Vídeo"
|
||||||
|
},
|
||||||
|
"status": "Estado",
|
||||||
|
"targetUrl": "URL de destino",
|
||||||
|
"title": "Título",
|
||||||
|
"type": "Tipo",
|
||||||
|
"updateSuccess": "Actualizado con éxito",
|
||||||
|
"validityPeriod": "Período de validez"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Por favor, introduce una dirección de correo electrónico válida.",
|
"email": "Por favor, introduce una dirección de correo electrónico válida.",
|
||||||
"existingAccount": "¿Ya tienes una cuenta?",
|
"existingAccount": "¿Ya tienes una cuenta?",
|
||||||
"get": "Obtener",
|
"get": "Obtener",
|
||||||
"invite": "Código de invitación",
|
"invite": "Código de invitación (opcional)",
|
||||||
"message": "#### ¡Estimado usuario!\n\nGracias por su interés y apoyo. Debido a un ajuste en la estrategia de operación del sitio, hemos cerrado la función de registro para nuevos usuarios. Durante este período, el uso por parte de los usuarios existentes no se verá afectado.\n\nNos comprometemos a brindarle un mejor servicio y experiencia, por lo que realizaremos una optimización completa del sistema y una actualización de funciones durante el cierre del registro. En el futuro, le daremos la bienvenida con contenido y servicios de mayor calidad.\n\nPor favor, siga nuestro sitio web y plataformas de redes sociales para obtener las últimas actualizaciones y notificaciones. Agradecemos su comprensión y apoyo.\n\nSi tiene alguna pregunta o necesita ayuda, no dude en ponerse en contacto con nuestro equipo de atención al cliente.\n\n**Nuevamente, gracias por su apoyo y comprensión.**",
|
"message": "#### ¡Estimado usuario!\n\nGracias por su interés y apoyo. Debido a un ajuste en la estrategia de operación del sitio, hemos cerrado la función de registro para nuevos usuarios. Durante este período, el uso por parte de los usuarios existentes no se verá afectado.\n\nNos comprometemos a brindarle un mejor servicio y experiencia, por lo que realizaremos una optimización completa del sistema y una actualización de funciones durante el cierre del registro. En el futuro, le daremos la bienvenida con contenido y servicios de mayor calidad.\n\nPor favor, siga nuestro sitio web y plataformas de redes sociales para obtener las últimas actualizaciones y notificaciones. Agradecemos su comprensión y apoyo.\n\nSi tiene alguna pregunta o necesita ayuda, no dude en ponerse en contacto con nuestro equipo de atención al cliente.\n\n**Nuevamente, gracias por su apoyo y comprensión.**",
|
||||||
"passwordMismatch": "Las contraseñas no coinciden",
|
"passwordMismatch": "Las contraseñas no coinciden",
|
||||||
"success": "¡Registro exitoso, sesión iniciada automáticamente!",
|
"success": "¡Registro exitoso, sesión iniciada automáticamente!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Bloquear Máquina Virtual",
|
||||||
|
"blockVirtualMachineDescription": "Cuando está habilitado, se evitará que los dispositivos se ejecuten en máquinas virtuales o emuladores",
|
||||||
|
"communicationKey": "Clave de Comunicación",
|
||||||
|
"communicationKeyDescription": "La clave de comunicación se utiliza para la comunicación segura entre dispositivos y servidores",
|
||||||
"enable": "Habilitar",
|
"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"
|
"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",
|
||||||
|
"enableSecurity": "Habilitar Cifrado de Comunicación",
|
||||||
|
"enableSecurityDescription": "Cuando está habilitado, la comunicación entre dispositivos y servidores será cifrada",
|
||||||
|
"saveFailed": "Error al guardar",
|
||||||
|
"saveSuccess": "Guardado exitoso",
|
||||||
|
"showAds": "Mostrar Anuncios",
|
||||||
|
"showAdsDescription": "Cuando está habilitado, se mostrarán anuncios en los dispositivos"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"subject": "Asunto",
|
"subject": "Asunto",
|
||||||
"template": "Plantilla",
|
"template": "Plantilla",
|
||||||
|
"traffic_exceed_email_template": "Plantilla de Notificación de Exceso de Tráfico",
|
||||||
|
"traffic_exceed_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
|
||||||
"verify_email_template": "Plantilla de Correo Electrónico de Verificación",
|
"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.",
|
"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",
|
"whitelistSuffixes": "Sufijos de la lista blanca",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "Configuración de ADS",
|
||||||
"Announcement Management": "Gestión de Anuncios",
|
"Announcement Management": "Gestión de Anuncios",
|
||||||
"Apple": "ID de Apple",
|
"Apple": "ID de Apple",
|
||||||
|
"Application Management": "Gestión de Aplicaciones",
|
||||||
"Auth Control": "Control de Autenticación",
|
"Auth Control": "Control de Autenticación",
|
||||||
"Coupon Management": "Gestión de Cupones",
|
"Coupon Management": "Gestión de Cupones",
|
||||||
"Dashboard": "Tablero",
|
"Dashboard": "Tablero",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Gestión de Pedidos",
|
"Order Management": "Gestión de Pedidos",
|
||||||
"Payment Config": "Configuración de Pago",
|
"Payment Config": "Configuración de Pago",
|
||||||
"Phone Number": "Número de Teléfono",
|
"Phone Number": "Número de Teléfono",
|
||||||
|
"Rule Management": "Gestión de Reglas",
|
||||||
"Server": "Servidor",
|
"Server": "Servidor",
|
||||||
"Server Management": "Gestión de Servidores",
|
"Server Management": "Gestión de Servidores",
|
||||||
"Settings": "Configuración",
|
"Settings": "Configuración",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Pago con Alipay",
|
"AlipayF2F": "Alipay Cara a Cara",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "ID de la aplicación de Alipay",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Nombre personalizado del producto",
|
"applyForPayment": "Solicitar Pago",
|
||||||
"invoiceNameDescription": "Se mostrará en la factura de Alipay",
|
"balance": "Saldo",
|
||||||
"privateKey": "Clave privada de Alipay",
|
"batchDelete": "Eliminar en Lote",
|
||||||
"publicKey": "Clave pública de Alipay",
|
"cancel": "Cancelar",
|
||||||
"sandbox": "Modo sandbox",
|
"config": "Configuración",
|
||||||
"sandboxDescription": "Habilitar el modo sandbox para probar el pago"
|
"configInfo": "Información de Configuración",
|
||||||
},
|
"configPlaceholder": "Por favor, complete la configuración de {field} proporcionada",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar Eliminación",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copySuccess": "Copiado con éxito",
|
||||||
|
"create": "Agregar Método de Pago",
|
||||||
|
"createPayment": "Agregar Método de Pago",
|
||||||
|
"createSuccess": "Creado con éxito",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Eliminado con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar este método de pago? Esta acción no se puede deshacer.",
|
||||||
|
"description": "Descripción",
|
||||||
|
"descriptionPlaceholder": "Ingrese la descripción del método de pago",
|
||||||
|
"domain": "Dominio",
|
||||||
|
"domainPlaceholder": "opcional, p. ej. {example}",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editPayment": "Editar Método de Pago",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"enableDescription": "Habilitar este método de pago",
|
"feeAmount": "Monto Fijo",
|
||||||
"epay": {
|
|
||||||
"key": "CLAVE",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Modo de cobro",
|
"feeMode": "Modo de cobro",
|
||||||
"feeModeDescription": "Modo de cobro de tarifas adicionales",
|
|
||||||
"feeModeItems": ["Sin tarifa", "Porcentaje", "Cantidad fija", "Ambos anteriores"],
|
|
||||||
"feePercent": "Porcentaje de tarifa (opcional)",
|
"feePercent": "Porcentaje de tarifa (opcional)",
|
||||||
"feePercentDescription": "Se añade una tarifa sobre el monto del pedido",
|
|
||||||
"fixedFee": "Tarifa fija (opcional)",
|
"fixedFee": "Tarifa fija (opcional)",
|
||||||
"fixedFeeDescription": "Se añade una tarifa fija sobre el monto del pedido",
|
"handlingFee": "Tarifa de manejo",
|
||||||
"iconUrl": "URL del icono (opcional)",
|
"icon": "Ícono",
|
||||||
"iconUrlDescription": "Usado para mostrar en el frontend",
|
"iconPlaceholder": "Ingrese la URL del ícono",
|
||||||
"inputPlaceholder": "Por favor, introduzca",
|
"mark": "Identificador",
|
||||||
"key": "CLAVE",
|
"markPlaceholder": "Ingrese el identificador del método de pago",
|
||||||
"notifyUrl": "Dominio de notificación personalizado (opcional)",
|
"name": "Nombre",
|
||||||
"notifyUrlDescription": "La notificación del gateway se enviará a este dominio",
|
"namePlaceholder": "Ingrese el nombre del método de pago",
|
||||||
"payForRecommendations": "Pagar por recomendaciones",
|
"nameRequired": "El nombre es obligatorio",
|
||||||
"pid": "PID",
|
"noFee": "Sin Tarifa",
|
||||||
"saveSuccess": "Guardado exitosamente",
|
"notify_url": "URL de notificación",
|
||||||
"showName": "Mostrar nombre",
|
"paymentManagement": "Gestión de Pagos",
|
||||||
"showNameDescription": "Usado para mostrar en el frontend",
|
"percentFee": "Porcentaje",
|
||||||
"stripe": {
|
"platform": "Plataforma",
|
||||||
"publicKey": "CLAVE PÚBLICA",
|
"searchPlaceholder": "Ingrese términos de búsqueda",
|
||||||
"secretKey": "CLAVE SECRETA",
|
"selectPlatform": "Seleccionar Plataforma",
|
||||||
"webhookSecret": "FIRMA DE CLAVE WEBHOOK"
|
"selectType": "Seleccionar tipo de pago",
|
||||||
},
|
"submit": "Enviar",
|
||||||
"url": "URL",
|
"type": "Tipo",
|
||||||
"wechatPay": "Pago con WeChat"
|
"updateSuccess": "Actualizado con éxito"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analizando...",
|
||||||
|
"appIcon": "Icono de Regla",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar Eliminación",
|
||||||
|
"create": "Agregar Regla",
|
||||||
|
"createRule": "Agregar Regla",
|
||||||
|
"createSuccess": "Regla creada con éxito",
|
||||||
|
"createdAt": "Creado En",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Regla eliminada con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar esta regla? Esta acción no se puede deshacer.",
|
||||||
|
"description": "Descripción de la Regla",
|
||||||
|
"downloadTemplate": "Descargar Plantilla",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editRule": "Editar Regla",
|
||||||
|
"enable": "Habilitar",
|
||||||
|
"enterIconUrl": "Ingrese o suba un icono",
|
||||||
|
"enterRuleName": "Ingrese el nombre de la regla",
|
||||||
|
"enterRules": "Ingrese el contenido de la regla, uno por línea",
|
||||||
|
"import": "Importar",
|
||||||
|
"importFailed": "Importación fallida",
|
||||||
|
"importSuccess": "Importación exitosa",
|
||||||
|
"importYaml": "Importar YAML",
|
||||||
|
"importYamlDescription": "Importar reglas desde un archivo YAML, el sistema creará automáticamente grupos de reglas basados en los nombres de los grupos de políticas",
|
||||||
|
"importYamlRules": "Importar Reglas YAML",
|
||||||
|
"importing": "Importando...",
|
||||||
|
"invalidYamlFormat": "Formato YAML inválido, falta el campo de reglas",
|
||||||
|
"name": "Nombre de la Regla",
|
||||||
|
"noValidRules": "No se encontraron reglas válidas",
|
||||||
|
"pleaseUploadFile": "Por favor, suba un archivo YAML",
|
||||||
|
"preview": "Vista Previa",
|
||||||
|
"rules": "Contenido de la Regla",
|
||||||
|
"rulesFormat": "Formato de regla: tipo de regla, contenido de coincidencia, [política], donde la política es opcional.\nSi no se especifica la política, se utilizará automáticamente el nombre del grupo de reglas actual. Ejemplos:",
|
||||||
|
"rulesLabel": "Contenido de la Regla",
|
||||||
|
"searchRule": "Buscar nombre de regla",
|
||||||
|
"selectFile": "Seleccionar Archivo",
|
||||||
|
"selectTags": "Seleccionar etiquetas de nodo",
|
||||||
|
"tags": "Etiquetas de Nodo",
|
||||||
|
"tagsLabel": "Etiquetas de Nodo",
|
||||||
|
"updateSuccess": "Regla actualizada con éxito"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Política de Privacidad"
|
"title": "Política de Privacidad"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Datos Personalizados",
|
||||||
|
"customDataDescription": "Datos Personalizados, utilizados para datos personalizados del sitio",
|
||||||
"customHtml": "HTML personalizado",
|
"customHtml": "HTML personalizado",
|
||||||
"customHtmlDescription": "Código HTML personalizado que se inyectará en la parte inferior de la etiqueta body del sitio.",
|
"customHtmlDescription": "Código HTML personalizado que se inyectará en la parte inferior de la etiqueta body del sitio.",
|
||||||
"keywords": "Palabras clave",
|
"keywords": "Palabras clave",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Habilitar cuenta",
|
"accountEnable": "Habilitar cuenta",
|
||||||
"actions": "acciones",
|
"actions": "acciones",
|
||||||
|
"active": "Activo",
|
||||||
"add": "Agregar",
|
"add": "Agregar",
|
||||||
"administrator": "Administrador",
|
"administrator": "Administrador",
|
||||||
"areaCode": "Código de Área",
|
"areaCode": "Código de Área",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "correo electrónico",
|
"email": "correo electrónico",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"expireTime": "Tiempo de Expiración",
|
"expireTime": "Tiempo de Expiración",
|
||||||
|
"expired": "Caducado",
|
||||||
"expiredAt": "Fecha de vencimiento",
|
"expiredAt": "Fecha de vencimiento",
|
||||||
"failed": "Fallido",
|
"failed": "Fallido",
|
||||||
"giftAmount": "Monto del Regalo",
|
"giftAmount": "Monto del Regalo",
|
||||||
"giftAmountPlaceholder": "Ingrese el monto del regalo",
|
"giftAmountPlaceholder": "Ingrese el monto del regalo",
|
||||||
|
"inactive": "Inactivo",
|
||||||
"invalidEmailFormat": "Formato de correo electrónico no válido",
|
"invalidEmailFormat": "Formato de correo electrónico no válido",
|
||||||
"inviteCode": "Código de invitación",
|
"inviteCode": "Código de invitación",
|
||||||
"inviteCodePlaceholder": "Introduce el código de invitación (déjalo en blanco para generar uno)",
|
"inviteCodePlaceholder": "Introduce el código de invitación (déjalo en blanco para generar uno)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Ingrese nueva contraseña (opcional)",
|
"passwordPlaceholder": "Ingrese nueva contraseña (opcional)",
|
||||||
"permanent": "Permanente",
|
"permanent": "Permanente",
|
||||||
"pleaseEnterEmail": "Por favor, introduce el correo electrónico",
|
"pleaseEnterEmail": "Por favor, introduce el correo electrónico",
|
||||||
|
"price": "Precio",
|
||||||
"referer": "Recomendador",
|
"referer": "Recomendador",
|
||||||
"refererId": "ID del Referente",
|
"refererId": "ID del Referente",
|
||||||
"refererIdPlaceholder": "Ingrese el ID del referente",
|
"refererIdPlaceholder": "Ingrese el ID del referente",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "Buscar dirección IP",
|
"searchIp": "Buscar dirección IP",
|
||||||
"selectAuthType": "Seleccionar tipo de autenticación",
|
"selectAuthType": "Seleccionar tipo de autenticación",
|
||||||
"speedLimit": "Límite de Velocidad",
|
"speedLimit": "Límite de Velocidad",
|
||||||
|
"startTime": "Hora de Inicio",
|
||||||
|
"status": "Estado",
|
||||||
"subscription": "Suscripción",
|
"subscription": "Suscripción",
|
||||||
"subscriptionDetails": "Detalles de la suscripción",
|
"subscriptionDetails": "Detalles de la suscripción",
|
||||||
|
"subscriptionId": "ID de Suscripción",
|
||||||
|
"subscriptionInfo": "Información de Suscripción",
|
||||||
"subscriptionList": "Lista de Suscripción",
|
"subscriptionList": "Lista de Suscripción",
|
||||||
"subscriptionLogs": "Registros de Suscripción",
|
"subscriptionLogs": "Registros de Suscripción",
|
||||||
"subscriptionName": "Nombre de la Suscripción",
|
"subscriptionName": "Nombre de la Suscripción",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Notificaciones de Comercio",
|
"tradeNotifications": "Notificaciones de Comercio",
|
||||||
"trafficLimit": "Límite de Tráfico",
|
"trafficLimit": "Límite de Tráfico",
|
||||||
"trafficLogs": "Registros de Tráfico",
|
"trafficLogs": "Registros de Tráfico",
|
||||||
|
"trafficUsage": "Uso de Tráfico",
|
||||||
|
"unknown": "Desconocido",
|
||||||
"unlimited": "Ilimitado",
|
"unlimited": "Ilimitado",
|
||||||
"unverified": "No verificado",
|
"unverified": "No verificado",
|
||||||
"update": "Actualizar",
|
"update": "Actualizar",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "Agente de Usuario",
|
"userAgent": "Agente de Usuario",
|
||||||
"userEmail": "Correo Electrónico del Usuario",
|
"userEmail": "Correo Electrónico del Usuario",
|
||||||
"userEmailPlaceholder": "Introduce el correo electrónico del usuario",
|
"userEmailPlaceholder": "Introduce el correo electrónico del usuario",
|
||||||
|
"userId": "ID de Usuario",
|
||||||
|
"userInfo": "Información de Usuario",
|
||||||
"userList": "Lista de usuarios",
|
"userList": "Lista de usuarios",
|
||||||
"userLogs": "Historial de inicio de sesión",
|
"userLogs": "Historial de inicio de sesión",
|
||||||
"userName": "Identificador de Usuario",
|
"userName": "Identificador de Usuario",
|
||||||
"userOrders": "Pedidos",
|
"userOrders": "Pedidos",
|
||||||
"userProfile": "Perfil",
|
"userProfile": "Perfil",
|
||||||
"userSubscriptions": "Suscripciones",
|
"userSubscriptions": "Suscripciones",
|
||||||
"verified": "Verificado"
|
"username": "Nombre de Usuario",
|
||||||
|
"verified": "Verificado",
|
||||||
|
"viewDetails": "Ver Detalles"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar Eliminación",
|
||||||
|
"create": "Crear Anuncio",
|
||||||
|
"createAds": "Crear Publicidad",
|
||||||
|
"createSuccess": "Creado con éxito",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Eliminado con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar este anuncio?",
|
||||||
|
"disabled": "Deshabilitado",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editAds": "Editar Publicidad",
|
||||||
|
"enabled": "Habilitado",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"content": "Contenido",
|
||||||
|
"description": "Descripción",
|
||||||
|
"endTime": "Hora de Finalización",
|
||||||
|
"enterContent": "Por favor ingrese el contenido del anuncio",
|
||||||
|
"enterDescription": "Por favor ingrese la descripción del anuncio",
|
||||||
|
"enterEndTime": "Por favor seleccione la hora de finalización",
|
||||||
|
"enterStartTime": "Por favor seleccione la hora de inicio",
|
||||||
|
"enterTargetUrl": "Por favor ingrese la URL de destino",
|
||||||
|
"enterTitle": "Por favor ingrese el título del anuncio",
|
||||||
|
"startTime": "Hora de Inicio",
|
||||||
|
"targetUrl": "URL de Destino",
|
||||||
|
"title": "Título",
|
||||||
|
"type": "Tipo de Anuncio",
|
||||||
|
"typeImage": "Imagen",
|
||||||
|
"typeVideo": "Video"
|
||||||
|
},
|
||||||
|
"status": "Estado",
|
||||||
|
"targetUrl": "URL de Destino",
|
||||||
|
"title": "Título",
|
||||||
|
"type": "Tipo",
|
||||||
|
"updateSuccess": "Actualizado con éxito",
|
||||||
|
"validityPeriod": "Período de Validez"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Por favor, ingresa una dirección de correo electrónico válida.",
|
"email": "Por favor, ingresa una dirección de correo electrónico válida.",
|
||||||
"existingAccount": "¿Ya tienes una cuenta?",
|
"existingAccount": "¿Ya tienes una cuenta?",
|
||||||
"get": "Obtener",
|
"get": "Obtener",
|
||||||
"invite": "Código de invitación",
|
"invite": "Código de invitación (opcional)",
|
||||||
"message": "#### Estimado usuario,\n\nGracias por su interés y apoyo. Debido a un ajuste en la estrategia de operación del sitio, hemos cerrado la función de registro para nuevos usuarios. Durante este período, el uso por parte de los usuarios existentes no se verá afectado.\n\nNos comprometemos a ofrecerle un mejor servicio y experiencia, por lo que realizaremos una optimización completa del sistema y una actualización de funciones durante el cierre del registro. En el futuro, le daremos la bienvenida con contenido y servicios de mayor calidad.\n\nPor favor, siga nuestro sitio web y plataformas de redes sociales para obtener las últimas actualizaciones y notificaciones. Agradecemos su comprensión y apoyo.\n\nSi tiene alguna pregunta o necesita ayuda, no dude en ponerse en contacto con nuestro equipo de atención al cliente.\n\n**Nuevamente, gracias por su apoyo y comprensión.**",
|
"message": "#### Estimado usuario,\n\nGracias por su interés y apoyo. Debido a un ajuste en la estrategia de operación del sitio, hemos cerrado la función de registro para nuevos usuarios. Durante este período, el uso por parte de los usuarios existentes no se verá afectado.\n\nNos comprometemos a ofrecerle un mejor servicio y experiencia, por lo que realizaremos una optimización completa del sistema y una actualización de funciones durante el cierre del registro. En el futuro, le daremos la bienvenida con contenido y servicios de mayor calidad.\n\nPor favor, siga nuestro sitio web y plataformas de redes sociales para obtener las últimas actualizaciones y notificaciones. Agradecemos su comprensión y apoyo.\n\nSi tiene alguna pregunta o necesita ayuda, no dude en ponerse en contacto con nuestro equipo de atención al cliente.\n\n**Nuevamente, gracias por su apoyo y comprensión.**",
|
||||||
"passwordMismatch": "Las contraseñas no coinciden",
|
"passwordMismatch": "Las contraseñas no coinciden",
|
||||||
"success": "¡Registro exitoso, has iniciado sesión automáticamente!",
|
"success": "¡Registro exitoso, has iniciado sesión automáticamente!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Bloquear Máquina Virtual",
|
||||||
|
"blockVirtualMachineDescription": "Cuando está habilitado, se evitará que los dispositivos se ejecuten en máquinas virtuales o emuladores",
|
||||||
|
"communicationKey": "Clave de Comunicación",
|
||||||
|
"communicationKeyDescription": "La clave de comunicación se utiliza para la comunicación segura entre dispositivos y servidores",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"enableDescription": "Después de habilitar, se admiten múltiples identificadores de dispositivos como IMEI/IDFA/IDFV/AndroidID/dirección Mac para iniciar sesión y registrarse"
|
"enableDescription": "Después de habilitar, se admiten múltiples identificadores de dispositivos como IMEI/IDFA/IDFV/AndroidID/dirección Mac para iniciar sesión y registrarse",
|
||||||
|
"enableSecurity": "Habilitar Cifrado de Comunicación",
|
||||||
|
"enableSecurityDescription": "Cuando está habilitado, la comunicación entre dispositivos y servidores será cifrada",
|
||||||
|
"saveFailed": "Error al guardar",
|
||||||
|
"saveSuccess": "Guardado exitoso",
|
||||||
|
"showAds": "Mostrar Anuncios",
|
||||||
|
"showAdsDescription": "Cuando está habilitado, se mostrarán anuncios en los dispositivos"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"subject": "Asunto",
|
"subject": "Asunto",
|
||||||
"template": "Plantilla",
|
"template": "Plantilla",
|
||||||
|
"traffic_exceed_email_template": "Plantilla de Notificación de Exceso de Tráfico",
|
||||||
|
"traffic_exceed_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
|
||||||
"verify_email_template": "Plantilla de Correo Electrónico de Verificación",
|
"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úrate de mantener estas variables.",
|
"verify_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
|
||||||
"whitelistSuffixes": "Sufijos de Lista Blanca",
|
"whitelistSuffixes": "Sufijos de Lista Blanca",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "Configuración de ADS",
|
||||||
"Announcement Management": "Gestión de Anuncios",
|
"Announcement Management": "Gestión de Anuncios",
|
||||||
"Apple": "ID de Apple",
|
"Apple": "ID de Apple",
|
||||||
|
"Application Management": "Gestión de Aplicaciones",
|
||||||
"Auth Control": "Control de Autenticación",
|
"Auth Control": "Control de Autenticación",
|
||||||
"Coupon Management": "Gestión de Cupones",
|
"Coupon Management": "Gestión de Cupones",
|
||||||
"Dashboard": "Tablero",
|
"Dashboard": "Tablero",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Gestión de Pedidos",
|
"Order Management": "Gestión de Pedidos",
|
||||||
"Payment Config": "Configuración de Pago",
|
"Payment Config": "Configuración de Pago",
|
||||||
"Phone Number": "Número de Teléfono",
|
"Phone Number": "Número de Teléfono",
|
||||||
|
"Rule Management": "Gestión de Reglas",
|
||||||
"Server": "Servidor",
|
"Server": "Servidor",
|
||||||
"Server Management": "Gestión de Servidores",
|
"Server Management": "Gestión de Servidores",
|
||||||
"Settings": "Configuración",
|
"Settings": "Configuración",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Pago con Alipay",
|
"AlipayF2F": "Alipay Cara a Cara",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "ID de la aplicación de Alipay",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Nombre personalizado del producto",
|
"applyForPayment": "Solicitar Pago",
|
||||||
"invoiceNameDescription": "Se mostrará en la factura de Alipay",
|
"balance": "Saldo",
|
||||||
"privateKey": "Clave privada de Alipay",
|
"batchDelete": "Eliminar en Lote",
|
||||||
"publicKey": "Clave pública de Alipay",
|
"cancel": "Cancelar",
|
||||||
"sandbox": "Modo sandbox",
|
"config": "Configuración",
|
||||||
"sandboxDescription": "Habilitar el modo sandbox para probar el pago"
|
"configInfo": "Información de Configuración",
|
||||||
},
|
"configPlaceholder": "Por favor, complete la configuración de {field} proporcionada",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar Eliminación",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copySuccess": "Copiado con éxito",
|
||||||
|
"create": "Agregar Método de Pago",
|
||||||
|
"createPayment": "Agregar Método de Pago",
|
||||||
|
"createSuccess": "Creado con éxito",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Eliminado con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar este método de pago? Esta acción no se puede deshacer.",
|
||||||
|
"description": "Descripción",
|
||||||
|
"descriptionPlaceholder": "Ingresa la descripción del método de pago",
|
||||||
|
"domain": "Dominio",
|
||||||
|
"domainPlaceholder": "opcional, p. ej. {example}",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editPayment": "Editar Método de Pago",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"enableDescription": "Habilitar este método de pago",
|
"feeAmount": "Monto Fijo",
|
||||||
"epay": {
|
|
||||||
"key": "CLAVE",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Modo de cobro",
|
"feeMode": "Modo de cobro",
|
||||||
"feeModeDescription": "Modo de cobro de tarifas adicionales",
|
|
||||||
"feeModeItems": ["Sin tarifa", "Porcentaje", "Cantidad fija", "Ambos"],
|
|
||||||
"feePercent": "Porcentaje de tarifa (opcional)",
|
"feePercent": "Porcentaje de tarifa (opcional)",
|
||||||
"feePercentDescription": "Se añade una tarifa sobre el monto del pedido",
|
|
||||||
"fixedFee": "Cuota fija (opcional)",
|
"fixedFee": "Cuota fija (opcional)",
|
||||||
"fixedFeeDescription": "Agregar una tarifa fija sobre el monto del pedido",
|
"handlingFee": "Cargo por manejo",
|
||||||
"iconUrl": "URL del ícono (opcional)",
|
"icon": "Ícono",
|
||||||
"iconUrlDescription": "Usado para mostrar en el frontend",
|
"iconPlaceholder": "Ingrese la URL del ícono",
|
||||||
"inputPlaceholder": "Por favor ingrese",
|
"mark": "Identificador",
|
||||||
"key": "LLAVE",
|
"markPlaceholder": "Ingrese el identificador del método de pago",
|
||||||
"notifyUrl": "Dominio de notificación personalizado (opcional)",
|
"name": "Nombre",
|
||||||
"notifyUrlDescription": "La notificación del gateway se enviará a este dominio",
|
"namePlaceholder": "Ingrese el nombre del método de pago",
|
||||||
"payForRecommendations": "Pagar por recomendaciones",
|
"nameRequired": "El nombre es obligatorio",
|
||||||
"pid": "PID",
|
"noFee": "Sin Tarifa",
|
||||||
"saveSuccess": "Guardado exitoso",
|
"notify_url": "URL de notificación",
|
||||||
"showName": "Mostrar nombre",
|
"paymentManagement": "Gestión de Pagos",
|
||||||
"showNameDescription": "Usado para mostrar en el frontend",
|
"percentFee": "Porcentaje",
|
||||||
"stripe": {
|
"platform": "Plataforma",
|
||||||
"publicKey": "CLAVE PÚBLICA",
|
"searchPlaceholder": "Ingrese términos de búsqueda",
|
||||||
"secretKey": "CLAVE SECRETA",
|
"selectPlatform": "Seleccionar Plataforma",
|
||||||
"webhookSecret": "FIRMA DE SECRETO DE WEBHOOK"
|
"selectType": "Seleccionar tipo de pago",
|
||||||
},
|
"submit": "Enviar",
|
||||||
"url": "URL",
|
"type": "Tipo",
|
||||||
"wechatPay": "WeChat Pay"
|
"updateSuccess": "Actualizado con éxito"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analizando...",
|
||||||
|
"appIcon": "Ícono de Regla",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"confirm": "Confirmar",
|
||||||
|
"confirmDelete": "Confirmar Eliminación",
|
||||||
|
"create": "Agregar Regla",
|
||||||
|
"createRule": "Agregar Regla",
|
||||||
|
"createSuccess": "Regla creada con éxito",
|
||||||
|
"createdAt": "Creado En",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"deleteSuccess": "Regla eliminada con éxito",
|
||||||
|
"deleteWarning": "¿Está seguro de que desea eliminar esta regla? Esta acción no se puede deshacer.",
|
||||||
|
"description": "Descripción de la Regla",
|
||||||
|
"downloadTemplate": "Descargar Plantilla",
|
||||||
|
"edit": "Editar",
|
||||||
|
"editRule": "Editar Regla",
|
||||||
|
"enable": "Habilitar",
|
||||||
|
"enterIconUrl": "Ingrese o suba un ícono",
|
||||||
|
"enterRuleName": "Ingrese el nombre de la regla",
|
||||||
|
"enterRules": "Ingrese el contenido de la regla, uno por línea",
|
||||||
|
"import": "Importar",
|
||||||
|
"importFailed": "Importación fallida",
|
||||||
|
"importSuccess": "Importación exitosa",
|
||||||
|
"importYaml": "Importar YAML",
|
||||||
|
"importYamlDescription": "Importar reglas desde un archivo YAML, el sistema creará automáticamente grupos de reglas basados en los nombres de los grupos de políticas",
|
||||||
|
"importYamlRules": "Importar Reglas YAML",
|
||||||
|
"importing": "Importando...",
|
||||||
|
"invalidYamlFormat": "Formato YAML inválido, falta el campo de reglas",
|
||||||
|
"name": "Nombre de la Regla",
|
||||||
|
"noValidRules": "No se encontraron reglas válidas",
|
||||||
|
"pleaseUploadFile": "Por favor, suba un archivo YAML",
|
||||||
|
"preview": "Vista Previa",
|
||||||
|
"rules": "Contenido de la Regla",
|
||||||
|
"rulesFormat": "Formato de regla: tipo de regla, contenido de coincidencia, [política], donde la política es opcional.\nSi no se especifica la política, se utilizará automáticamente el nombre del grupo de reglas actual. Ejemplos:",
|
||||||
|
"rulesLabel": "Contenido de la Regla",
|
||||||
|
"searchRule": "Buscar nombre de regla",
|
||||||
|
"selectFile": "Seleccionar Archivo",
|
||||||
|
"selectTags": "Seleccionar etiquetas de nodo",
|
||||||
|
"tags": "Etiquetas de Nodo",
|
||||||
|
"tagsLabel": "Etiquetas de Nodo",
|
||||||
|
"updateSuccess": "Regla actualizada con éxito"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Política de Privacidad"
|
"title": "Política de Privacidad"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Datos Personalizados",
|
||||||
|
"customDataDescription": "Datos Personalizados, utilizados para datos personalizados del sitio",
|
||||||
"customHtml": "HTML personalizado",
|
"customHtml": "HTML personalizado",
|
||||||
"customHtmlDescription": "Código HTML personalizado que se inyectará al final de la etiqueta body del sitio.",
|
"customHtmlDescription": "Código HTML personalizado que se inyectará al final de la etiqueta body del sitio.",
|
||||||
"keywords": "Palabras clave",
|
"keywords": "Palabras clave",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Habilitar Cuenta",
|
"accountEnable": "Habilitar Cuenta",
|
||||||
"actions": "acciones",
|
"actions": "acciones",
|
||||||
|
"active": "Activo",
|
||||||
"add": "Agregar",
|
"add": "Agregar",
|
||||||
"administrator": "Administrador",
|
"administrator": "Administrador",
|
||||||
"areaCode": "Código de Área",
|
"areaCode": "Código de Área",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "correo electrónico",
|
"email": "correo electrónico",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"expireTime": "Tiempo de Expiración",
|
"expireTime": "Tiempo de Expiración",
|
||||||
|
"expired": "Expirado",
|
||||||
"expiredAt": "Fecha de vencimiento",
|
"expiredAt": "Fecha de vencimiento",
|
||||||
"failed": "Fallido",
|
"failed": "Fallido",
|
||||||
"giftAmount": "Monto del Regalo",
|
"giftAmount": "Monto del Regalo",
|
||||||
"giftAmountPlaceholder": "Ingrese el monto del regalo",
|
"giftAmountPlaceholder": "Ingrese el monto del regalo",
|
||||||
|
"inactive": "Inactivo",
|
||||||
"invalidEmailFormat": "Formato de correo electrónico no válido",
|
"invalidEmailFormat": "Formato de correo electrónico no válido",
|
||||||
"inviteCode": "Código de Invitación",
|
"inviteCode": "Código de Invitación",
|
||||||
"inviteCodePlaceholder": "Ingresa el código de invitación (déjalo en blanco para generar uno)",
|
"inviteCodePlaceholder": "Ingresa el código de invitación (déjalo en blanco para generar uno)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Ingresa nueva contraseña (opcional)",
|
"passwordPlaceholder": "Ingresa nueva contraseña (opcional)",
|
||||||
"permanent": "Permanente",
|
"permanent": "Permanente",
|
||||||
"pleaseEnterEmail": "Por favor, ingresa el correo electrónico",
|
"pleaseEnterEmail": "Por favor, ingresa el correo electrónico",
|
||||||
|
"price": "Precio",
|
||||||
"referer": "Recomendador",
|
"referer": "Recomendador",
|
||||||
"refererId": "ID del Referente",
|
"refererId": "ID del Referente",
|
||||||
"refererIdPlaceholder": "Ingrese ID del referente",
|
"refererIdPlaceholder": "Ingrese ID del referente",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "Buscar dirección IP",
|
"searchIp": "Buscar dirección IP",
|
||||||
"selectAuthType": "Seleccionar tipo de autenticación",
|
"selectAuthType": "Seleccionar tipo de autenticación",
|
||||||
"speedLimit": "Límite de Velocidad",
|
"speedLimit": "Límite de Velocidad",
|
||||||
|
"startTime": "Hora de Inicio",
|
||||||
|
"status": "Estado",
|
||||||
"subscription": "Suscripción",
|
"subscription": "Suscripción",
|
||||||
"subscriptionDetails": "Detalles de la Suscripción",
|
"subscriptionDetails": "Detalles de la Suscripción",
|
||||||
|
"subscriptionId": "ID de Suscripción",
|
||||||
|
"subscriptionInfo": "Información de Suscripción",
|
||||||
"subscriptionList": "Lista de Suscripción",
|
"subscriptionList": "Lista de Suscripción",
|
||||||
"subscriptionLogs": "Registros de Suscripción",
|
"subscriptionLogs": "Registros de Suscripción",
|
||||||
"subscriptionName": "Nombre de la Suscripción",
|
"subscriptionName": "Nombre de la Suscripción",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Notificaciones de Comercio",
|
"tradeNotifications": "Notificaciones de Comercio",
|
||||||
"trafficLimit": "Límite de Tráfico",
|
"trafficLimit": "Límite de Tráfico",
|
||||||
"trafficLogs": "Registros de Tráfico",
|
"trafficLogs": "Registros de Tráfico",
|
||||||
|
"trafficUsage": "Uso de Tráfico",
|
||||||
|
"unknown": "Desconocido",
|
||||||
"unlimited": "Ilimitado",
|
"unlimited": "Ilimitado",
|
||||||
"unverified": "No verificado",
|
"unverified": "No verificado",
|
||||||
"update": "Actualizar",
|
"update": "Actualizar",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "Agente de Usuario",
|
"userAgent": "Agente de Usuario",
|
||||||
"userEmail": "Correo Electrónico del Usuario",
|
"userEmail": "Correo Electrónico del Usuario",
|
||||||
"userEmailPlaceholder": "Ingresa el correo electrónico del usuario",
|
"userEmailPlaceholder": "Ingresa el correo electrónico del usuario",
|
||||||
|
"userId": "ID de Usuario",
|
||||||
|
"userInfo": "Información de Usuario",
|
||||||
"userList": "Lista de usuarios",
|
"userList": "Lista de usuarios",
|
||||||
"userLogs": "Historial de Inicio de Sesión",
|
"userLogs": "Historial de Inicio de Sesión",
|
||||||
"userName": "Identificador de Usuario",
|
"userName": "Identificador de Usuario",
|
||||||
"userOrders": "Pedidos",
|
"userOrders": "Pedidos",
|
||||||
"userProfile": "Perfil",
|
"userProfile": "Perfil",
|
||||||
"userSubscriptions": "Suscripciones",
|
"userSubscriptions": "Suscripciones",
|
||||||
"verified": "Verificado"
|
"username": "Nombre de Usuario",
|
||||||
|
"verified": "Verificado",
|
||||||
|
"viewDetails": "Ver Detalles"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "لغو",
|
||||||
|
"confirm": "تأیید",
|
||||||
|
"confirmDelete": "تأیید حذف",
|
||||||
|
"create": "ایجاد آگهی",
|
||||||
|
"createAds": "ایجاد تبلیغ",
|
||||||
|
"createSuccess": "با موفقیت ایجاد شد",
|
||||||
|
"delete": "حذف",
|
||||||
|
"deleteSuccess": "با موفقیت حذف شد",
|
||||||
|
"deleteWarning": "آیا مطمئن هستید که میخواهید این آگهی را حذف کنید؟",
|
||||||
|
"disabled": "غیرفعال",
|
||||||
|
"edit": "ویرایش",
|
||||||
|
"editAds": "ویرایش تبلیغ",
|
||||||
|
"enabled": "فعال",
|
||||||
|
"form": {
|
||||||
|
"cancel": "لغو",
|
||||||
|
"confirm": "تأیید",
|
||||||
|
"content": "محتوا",
|
||||||
|
"description": "توضیحات",
|
||||||
|
"endTime": "زمان پایان",
|
||||||
|
"enterContent": "لطفاً محتوای آگهی را وارد کنید",
|
||||||
|
"enterDescription": "لطفاً توضیحات آگهی را وارد کنید",
|
||||||
|
"enterEndTime": "لطفاً زمان پایان را انتخاب کنید",
|
||||||
|
"enterStartTime": "لطفاً زمان شروع را انتخاب کنید",
|
||||||
|
"enterTargetUrl": "لطفاً URL هدف را وارد کنید",
|
||||||
|
"enterTitle": "لطفاً عنوان آگهی را وارد کنید",
|
||||||
|
"startTime": "زمان شروع",
|
||||||
|
"targetUrl": "URL هدف",
|
||||||
|
"title": "عنوان",
|
||||||
|
"type": "نوع آگهی",
|
||||||
|
"typeImage": "تصویر",
|
||||||
|
"typeVideo": "ویدیو"
|
||||||
|
},
|
||||||
|
"status": "وضعیت",
|
||||||
|
"targetUrl": "URL هدف",
|
||||||
|
"title": "عنوان",
|
||||||
|
"type": "نوع",
|
||||||
|
"updateSuccess": "با موفقیت بهروزرسانی شد",
|
||||||
|
"validityPeriod": "مدت اعتبار"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "لطفاً یک آدرس ایمیل معتبر وارد کنید.",
|
"email": "لطفاً یک آدرس ایمیل معتبر وارد کنید.",
|
||||||
"existingAccount": "آیا قبلاً حساب کاربری دارید؟",
|
"existingAccount": "آیا قبلاً حساب کاربری دارید؟",
|
||||||
"get": "دریافت",
|
"get": "دریافت",
|
||||||
"invite": "کد دعوت",
|
"invite": "کد دعوت (اختیاری)",
|
||||||
"message": "#### کاربر گرامی، سلام!\n\nاز توجه و حمایت شما سپاسگزاریم. به دلیل تغییرات در استراتژی عملیاتی سایت، عملکرد ثبتنام کاربران جدید را متوقف کردهایم. در این مدت، کاربران فعلی تحت تأثیر قرار نخواهند گرفت.\n\nما متعهد به ارائه خدمات و تجربه بهتر به شما هستیم، بنابراین در طول این دوره، بهینهسازی جامع سیستم و ارتقاء ویژگیها را انجام خواهیم داد. در آینده، با محتوای و خدمات بهتر از شما استقبال خواهیم کرد.\n\nلطفاً برای دریافت آخرین بهروزرسانیها و اطلاعیهها، وبسایت و پلتفرمهای اجتماعی ما را دنبال کنید. از درک و حمایت شما سپاسگزاریم.\n\nاگر سوالی دارید یا به کمک نیاز دارید، لطفاً با تیم خدمات مشتری ما تماس بگیرید.\n\n**بار دیگر از حمایت و درک شما سپاسگزاریم.**",
|
"message": "#### کاربر گرامی، سلام!\n\nاز توجه و حمایت شما سپاسگزاریم. به دلیل تغییرات در استراتژی عملیاتی سایت، عملکرد ثبتنام کاربران جدید را متوقف کردهایم. در این مدت، کاربران فعلی تحت تأثیر قرار نخواهند گرفت.\n\nما متعهد به ارائه خدمات و تجربه بهتر به شما هستیم، بنابراین در طول این دوره، بهینهسازی جامع سیستم و ارتقاء ویژگیها را انجام خواهیم داد. در آینده، با محتوای و خدمات بهتر از شما استقبال خواهیم کرد.\n\nلطفاً برای دریافت آخرین بهروزرسانیها و اطلاعیهها، وبسایت و پلتفرمهای اجتماعی ما را دنبال کنید. از درک و حمایت شما سپاسگزاریم.\n\nاگر سوالی دارید یا به کمک نیاز دارید، لطفاً با تیم خدمات مشتری ما تماس بگیرید.\n\n**بار دیگر از حمایت و درک شما سپاسگزاریم.**",
|
||||||
"passwordMismatch": "رمزهای عبور مطابقت ندارند",
|
"passwordMismatch": "رمزهای عبور مطابقت ندارند",
|
||||||
"success": "ثبتنام موفقیتآمیز بود، بهطور خودکار وارد شدید!",
|
"success": "ثبتنام موفقیتآمیز بود، بهطور خودکار وارد شدید!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "مسدود کردن ماشین مجازی",
|
||||||
|
"blockVirtualMachineDescription": "هنگامی که فعال باشد، دستگاهها از اجرای ماشینهای مجازی یا شبیهسازها جلوگیری خواهند شد",
|
||||||
|
"communicationKey": "کلید ارتباطی",
|
||||||
|
"communicationKeyDescription": "کلید ارتباطی برای ارتباط امن بین دستگاهها و سرورها استفاده میشود",
|
||||||
"enable": "فعال کردن",
|
"enable": "فعال کردن",
|
||||||
"enableDescription": "پس از فعالسازی، شناسههای دستگاههای مختلف مانند IMEI/IDFA/IDFV/AndroidID/آدرس مک برای ورود و ثبتنام پشتیبانی میشوند"
|
"enableDescription": "پس از فعالسازی، شناسههای دستگاههای مختلف مانند IMEI/IDFA/IDFV/AndroidID/آدرس مک برای ورود و ثبتنام پشتیبانی میشوند",
|
||||||
|
"enableSecurity": "فعالسازی رمزنگاری ارتباطات",
|
||||||
|
"enableSecurityDescription": "هنگامی که فعال باشد، ارتباط بین دستگاهها و سرورها رمزنگاری خواهد شد",
|
||||||
|
"saveFailed": "ذخیرهسازی ناموفق بود",
|
||||||
|
"saveSuccess": "ذخیرهسازی با موفقیت انجام شد",
|
||||||
|
"showAds": "نمایش تبلیغات",
|
||||||
|
"showAdsDescription": "هنگامی که فعال باشد، تبلیغات بر روی دستگاهها نمایش داده خواهد شد"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "وضعیت",
|
"status": "وضعیت",
|
||||||
"subject": "موضوع",
|
"subject": "موضوع",
|
||||||
"template": "الگو",
|
"template": "الگو",
|
||||||
|
"traffic_exceed_email_template": "الگوی اعلان ترافیک بیش از حد",
|
||||||
|
"traffic_exceed_email_templateDescription": "جایگزینهای {after}.variable{before} با دادههای واقعی خواهد شد. اطمینان حاصل کنید که این متغیرها را حفظ کنید.",
|
||||||
"verify_email_template": "قالب ایمیل تأیید",
|
"verify_email_template": "قالب ایمیل تأیید",
|
||||||
"verify_email_templateDescription": "جایگذاریهای {after}.variable{before} با دادههای واقعی جایگزین خواهند شد. اطمینان حاصل کنید که این متغیرها را حفظ کنید.",
|
"verify_email_templateDescription": "جایگذاریهای {after}.variable{before} با دادههای واقعی جایگزین خواهند شد. اطمینان حاصل کنید که این متغیرها را حفظ کنید.",
|
||||||
"whitelistSuffixes": "پسوندهای لیست سفید",
|
"whitelistSuffixes": "پسوندهای لیست سفید",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "تنظیمات ADS",
|
||||||
"Announcement Management": "مدیریت اطلاعیهها",
|
"Announcement Management": "مدیریت اطلاعیهها",
|
||||||
"Apple": "شناسه اپل",
|
"Apple": "شناسه اپل",
|
||||||
|
"Application Management": "مدیریت برنامه",
|
||||||
"Auth Control": "کنترل احراز هویت",
|
"Auth Control": "کنترل احراز هویت",
|
||||||
"Coupon Management": "مدیریت کوپن",
|
"Coupon Management": "مدیریت کوپن",
|
||||||
"Dashboard": "داشبورد",
|
"Dashboard": "داشبورد",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "مدیریت سفارش",
|
"Order Management": "مدیریت سفارش",
|
||||||
"Payment Config": "پیکربندی پرداخت",
|
"Payment Config": "پیکربندی پرداخت",
|
||||||
"Phone Number": "شماره تلفن",
|
"Phone Number": "شماره تلفن",
|
||||||
|
"Rule Management": "مدیریت قوانین",
|
||||||
"Server": "سرور",
|
"Server": "سرور",
|
||||||
"Server Management": "مدیریت سرور",
|
"Server Management": "مدیریت سرور",
|
||||||
"Settings": "تنظیمات",
|
"Settings": "تنظیمات",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "پرداخت علیپی",
|
"AlipayF2F": "علیپی چهره به چهره",
|
||||||
"alipayf2f": {
|
"EPay": "ایپی",
|
||||||
"appId": "شناسه برنامه Alipay",
|
"Stripe": "استرایپ",
|
||||||
"invoiceName": "نام محصول سفارشی",
|
"applyForPayment": "درخواست پرداخت",
|
||||||
"invoiceNameDescription": "در صورتحساب Alipay نمایش داده خواهد شد",
|
"balance": "موجودی",
|
||||||
"privateKey": "کلید خصوصی Alipay",
|
"batchDelete": "حذف دستهای",
|
||||||
"publicKey": "کلید عمومی Alipay",
|
"cancel": "لغو",
|
||||||
"sandbox": "حالت Sandbox",
|
"config": "پیکربندی",
|
||||||
"sandboxDescription": "حالت sandbox را برای آزمایش پرداختها فعال کنید"
|
"configInfo": "اطلاعات پیکربندی",
|
||||||
},
|
"configPlaceholder": "لطفاً پیکربندی {field} ارائه شده را پر کنید",
|
||||||
|
"confirm": "تأیید",
|
||||||
|
"confirmDelete": "تأیید حذف",
|
||||||
|
"copy": "کپی",
|
||||||
|
"copySuccess": "با موفقیت کپی شد",
|
||||||
|
"create": "افزودن روش پرداخت",
|
||||||
|
"createPayment": "افزودن روش پرداخت",
|
||||||
|
"createSuccess": "با موفقیت ایجاد شد",
|
||||||
|
"delete": "حذف",
|
||||||
|
"deleteSuccess": "با موفقیت حذف شد",
|
||||||
|
"deleteWarning": "آیا مطمئن هستید که میخواهید این روش پرداخت را حذف کنید؟ این عمل قابل بازگشت نیست.",
|
||||||
|
"description": "توضیحات",
|
||||||
|
"descriptionPlaceholder": "توضیحات روش پرداخت را وارد کنید",
|
||||||
|
"domain": "دامنه",
|
||||||
|
"domainPlaceholder": "اختیاری، به عنوان مثال {example}",
|
||||||
|
"edit": "ویرایش",
|
||||||
|
"editPayment": "ویرایش روش پرداخت",
|
||||||
"enable": "فعال کردن",
|
"enable": "فعال کردن",
|
||||||
"enableDescription": "این روش پرداخت را فعال کنید",
|
"feeAmount": "مقدار ثابت",
|
||||||
"epay": {
|
|
||||||
"key": "کلید",
|
|
||||||
"pid": "شناسه محصول",
|
|
||||||
"url": "نشانی وب"
|
|
||||||
},
|
|
||||||
"feeMode": "حالت هزینه",
|
"feeMode": "حالت هزینه",
|
||||||
"feeModeDescription": "روش دریافت هزینه برای هزینههای اضافی",
|
|
||||||
"feeModeItems": ["بدون هزینه", "درصدی", "مبلغ ثابت", "هر دو"],
|
|
||||||
"feePercent": "درصد کارمزد (اختیاری)",
|
"feePercent": "درصد کارمزد (اختیاری)",
|
||||||
"feePercentDescription": "افزودن کارمزد به مبلغ سفارش",
|
|
||||||
"fixedFee": "هزینه ثابت (اختیاری)",
|
"fixedFee": "هزینه ثابت (اختیاری)",
|
||||||
"fixedFeeDescription": "افزودن هزینه ثابت به مبلغ سفارش",
|
"handlingFee": "هزینه خدمات",
|
||||||
"iconUrl": "آدرس آیکون (اختیاری)",
|
"icon": "آیکون",
|
||||||
"iconUrlDescription": "برای نمایش در رابط کاربری استفاده میشود",
|
"iconPlaceholder": "آدرس URL آیکون را وارد کنید",
|
||||||
"inputPlaceholder": "لطفاً وارد کنید",
|
"mark": "شناسه",
|
||||||
"key": "کلید",
|
"markPlaceholder": "شناسه روش پرداخت را وارد کنید",
|
||||||
"notifyUrl": "دامنه اعلان سفارشی (اختیاری)",
|
"name": "نام",
|
||||||
"notifyUrlDescription": "اعلانهای درگاه به این دامنه ارسال خواهند شد",
|
"namePlaceholder": "نام روش پرداخت را وارد کنید",
|
||||||
"payForRecommendations": "پرداخت برای توصیهها",
|
"nameRequired": "نام الزامی است",
|
||||||
"pid": "شناسه فرآیند",
|
"noFee": "بدون هزینه",
|
||||||
"saveSuccess": "ذخیره با موفقیت انجام شد",
|
"notify_url": "آدرس اطلاعرسانی",
|
||||||
"showName": "نمایش نام",
|
"paymentManagement": "مدیریت پرداخت",
|
||||||
"showNameDescription": "برای نمایش در قسمت جلویی استفاده میشود",
|
"percentFee": "درصد",
|
||||||
"stripe": {
|
"platform": "پلتفرم",
|
||||||
"publicKey": "کلید عمومی",
|
"searchPlaceholder": "عبارات جستجو را وارد کنید",
|
||||||
"secretKey": "کلید مخفی",
|
"selectPlatform": "انتخاب پلتفرم",
|
||||||
"webhookSecret": "کلید مخفی وبهوک"
|
"selectType": "انتخاب نوع پرداخت",
|
||||||
},
|
"submit": "ارسال",
|
||||||
"url": "آدرس اینترنتی",
|
"type": "نوع",
|
||||||
"wechatPay": "ویچت پی"
|
"updateSuccess": "با موفقیت بهروزرسانی شد"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "در حال تجزیه و تحلیل...",
|
||||||
|
"appIcon": "آیکون قانون",
|
||||||
|
"cancel": "لغو",
|
||||||
|
"confirm": "تأیید",
|
||||||
|
"confirmDelete": "تأیید حذف",
|
||||||
|
"create": "افزودن قانون",
|
||||||
|
"createRule": "افزودن قانون",
|
||||||
|
"createSuccess": "قانون با موفقیت ایجاد شد",
|
||||||
|
"createdAt": "تاریخ ایجاد",
|
||||||
|
"delete": "حذف",
|
||||||
|
"deleteSuccess": "قانون با موفقیت حذف شد",
|
||||||
|
"deleteWarning": "آیا مطمئن هستید که میخواهید این قانون را حذف کنید؟ این عمل قابل بازگشت نیست.",
|
||||||
|
"description": "توضیحات قانون",
|
||||||
|
"downloadTemplate": "دانلود الگو",
|
||||||
|
"edit": "ویرایش",
|
||||||
|
"editRule": "ویرایش قانون",
|
||||||
|
"enable": "فعالسازی",
|
||||||
|
"enterIconUrl": "آدرس یا بارگذاری آیکون را وارد کنید",
|
||||||
|
"enterRuleName": "نام قانون را وارد کنید",
|
||||||
|
"enterRules": "محتوای قانون را وارد کنید، هر کدام در یک خط",
|
||||||
|
"import": "وارد کردن",
|
||||||
|
"importFailed": "وارد کردن ناموفق بود",
|
||||||
|
"importSuccess": "وارد کردن با موفقیت انجام شد",
|
||||||
|
"importYaml": "وارد کردن YAML",
|
||||||
|
"importYamlDescription": "وارد کردن قوانین از فایل YAML، سیستم به طور خودکار گروههای قانونی را بر اساس نامهای گروه سیاست ایجاد خواهد کرد",
|
||||||
|
"importYamlRules": "وارد کردن قوانین YAML",
|
||||||
|
"importing": "در حال وارد کردن...",
|
||||||
|
"invalidYamlFormat": "فرمت YAML نامعتبر است، فیلد قوانین گم شده است",
|
||||||
|
"name": "نام قانون",
|
||||||
|
"noValidRules": "هیچ قانونی یافت نشد",
|
||||||
|
"pleaseUploadFile": "لطفاً یک فایل YAML بارگذاری کنید",
|
||||||
|
"preview": "پیشنمایش",
|
||||||
|
"rules": "محتوای قانون",
|
||||||
|
"rulesFormat": "فرمت قانون: نوع قانون، محتوای تطابق، [سیاست]، که سیاست اختیاری است.\nاگر سیاست مشخص نشود، نام گروه قانونی فعلی به طور خودکار استفاده خواهد شد. مثالها:",
|
||||||
|
"rulesLabel": "محتوای قانون",
|
||||||
|
"searchRule": "جستجوی نام قانون",
|
||||||
|
"selectFile": "انتخاب فایل",
|
||||||
|
"selectTags": "انتخاب برچسبهای گره",
|
||||||
|
"tags": "برچسبهای گره",
|
||||||
|
"tagsLabel": "برچسبهای گره",
|
||||||
|
"updateSuccess": "قانون با موفقیت بهروزرسانی شد"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "سیاست حفظ حریم خصوصی"
|
"title": "سیاست حفظ حریم خصوصی"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "دادههای سفارشی",
|
||||||
|
"customDataDescription": "دادههای سفارشی، استفاده شده برای دادههای سفارشی سایت",
|
||||||
"customHtml": "HTML سفارشی",
|
"customHtml": "HTML سفارشی",
|
||||||
"customHtmlDescription": "کد HTML سفارشی که باید در انتهای تگ body سایت وارد شود.",
|
"customHtmlDescription": "کد HTML سفارشی که باید در انتهای تگ body سایت وارد شود.",
|
||||||
"keywords": "کلمات کلیدی",
|
"keywords": "کلمات کلیدی",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "فعالسازی حساب",
|
"accountEnable": "فعالسازی حساب",
|
||||||
"actions": "اقدامات",
|
"actions": "اقدامات",
|
||||||
|
"active": "فعال",
|
||||||
"add": "افزودن",
|
"add": "افزودن",
|
||||||
"administrator": "مدیر",
|
"administrator": "مدیر",
|
||||||
"areaCode": "کد منطقه",
|
"areaCode": "کد منطقه",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "ایمیل",
|
"email": "ایمیل",
|
||||||
"enable": "فعال کردن",
|
"enable": "فعال کردن",
|
||||||
"expireTime": "زمان انقضا",
|
"expireTime": "زمان انقضا",
|
||||||
|
"expired": "منقضی شده",
|
||||||
"expiredAt": "منقضی شده در",
|
"expiredAt": "منقضی شده در",
|
||||||
"failed": "ناموفق",
|
"failed": "ناموفق",
|
||||||
"giftAmount": "مقدار هدیه",
|
"giftAmount": "مقدار هدیه",
|
||||||
"giftAmountPlaceholder": "مبلغ هدیه را وارد کنید",
|
"giftAmountPlaceholder": "مبلغ هدیه را وارد کنید",
|
||||||
|
"inactive": "غیرفعال",
|
||||||
"invalidEmailFormat": "فرمت ایمیل نامعتبر است",
|
"invalidEmailFormat": "فرمت ایمیل نامعتبر است",
|
||||||
"inviteCode": "کد دعوت",
|
"inviteCode": "کد دعوت",
|
||||||
"inviteCodePlaceholder": "کد دعوت را وارد کنید (برای تولید خالی بگذارید)",
|
"inviteCodePlaceholder": "کد دعوت را وارد کنید (برای تولید خالی بگذارید)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "رمز عبور جدید را وارد کنید (اختیاری)",
|
"passwordPlaceholder": "رمز عبور جدید را وارد کنید (اختیاری)",
|
||||||
"permanent": "دائمی",
|
"permanent": "دائمی",
|
||||||
"pleaseEnterEmail": "لطفاً ایمیل خود را وارد کنید",
|
"pleaseEnterEmail": "لطفاً ایمیل خود را وارد کنید",
|
||||||
|
"price": "قیمت",
|
||||||
"referer": "ارجاعدهنده",
|
"referer": "ارجاعدهنده",
|
||||||
"refererId": "شناسه معرف",
|
"refererId": "شناسه معرف",
|
||||||
"refererIdPlaceholder": "شناسه معرف را وارد کنید",
|
"refererIdPlaceholder": "شناسه معرف را وارد کنید",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "جستجوی آدرس IP",
|
"searchIp": "جستجوی آدرس IP",
|
||||||
"selectAuthType": "نوع احراز هویت را انتخاب کنید",
|
"selectAuthType": "نوع احراز هویت را انتخاب کنید",
|
||||||
"speedLimit": "محدودیت سرعت",
|
"speedLimit": "محدودیت سرعت",
|
||||||
|
"startTime": "زمان شروع",
|
||||||
|
"status": "وضعیت",
|
||||||
"subscription": "اشتراک",
|
"subscription": "اشتراک",
|
||||||
"subscriptionDetails": "جزئیات اشتراک",
|
"subscriptionDetails": "جزئیات اشتراک",
|
||||||
|
"subscriptionId": "شناسه اشتراک",
|
||||||
|
"subscriptionInfo": "اطلاعات اشتراک",
|
||||||
"subscriptionList": "لیست اشتراک",
|
"subscriptionList": "لیست اشتراک",
|
||||||
"subscriptionLogs": "گزارشهای اشتراک",
|
"subscriptionLogs": "گزارشهای اشتراک",
|
||||||
"subscriptionName": "نام اشتراک",
|
"subscriptionName": "نام اشتراک",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "اعلانهای تجارت",
|
"tradeNotifications": "اعلانهای تجارت",
|
||||||
"trafficLimit": "محدودیت ترافیک",
|
"trafficLimit": "محدودیت ترافیک",
|
||||||
"trafficLogs": "گزارشهای ترافیک",
|
"trafficLogs": "گزارشهای ترافیک",
|
||||||
|
"trafficUsage": "استفاده از ترافیک",
|
||||||
|
"unknown": "نامشخص",
|
||||||
"unlimited": "نامحدود",
|
"unlimited": "نامحدود",
|
||||||
"unverified": "تأیید نشده",
|
"unverified": "تأیید نشده",
|
||||||
"update": "بهروزرسانی",
|
"update": "بهروزرسانی",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "عامل کاربر",
|
"userAgent": "عامل کاربر",
|
||||||
"userEmail": "ایمیل کاربر",
|
"userEmail": "ایمیل کاربر",
|
||||||
"userEmailPlaceholder": "ایمیل کاربر را وارد کنید",
|
"userEmailPlaceholder": "ایمیل کاربر را وارد کنید",
|
||||||
|
"userId": "شناسه کاربر",
|
||||||
|
"userInfo": "اطلاعات کاربر",
|
||||||
"userList": "فهرست کاربران",
|
"userList": "فهرست کاربران",
|
||||||
"userLogs": "تاریخچه ورود",
|
"userLogs": "تاریخچه ورود",
|
||||||
"userName": "شناسه کاربر",
|
"userName": "شناسه کاربر",
|
||||||
"userOrders": "سفارشها",
|
"userOrders": "سفارشها",
|
||||||
"userProfile": "پروفایل",
|
"userProfile": "پروفایل",
|
||||||
"userSubscriptions": "اشتراکها",
|
"userSubscriptions": "اشتراکها",
|
||||||
"verified": "تأیید شده"
|
"username": "نام کاربری",
|
||||||
|
"verified": "تأیید شده",
|
||||||
|
"viewDetails": "مشاهده جزئیات"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Peruuta",
|
||||||
|
"confirm": "Vahvista",
|
||||||
|
"confirmDelete": "Vahvista poisto",
|
||||||
|
"create": "Luo ilmoitus",
|
||||||
|
"createAds": "Luo mainos",
|
||||||
|
"createSuccess": "Luonti onnistui",
|
||||||
|
"delete": "Poista",
|
||||||
|
"deleteSuccess": "Poisto onnistui",
|
||||||
|
"deleteWarning": "Oletko varma, että haluat poistaa tämän ilmoituksen?",
|
||||||
|
"disabled": "Pois käytöstä",
|
||||||
|
"edit": "Muokkaa",
|
||||||
|
"editAds": "Muokkaa mainosta",
|
||||||
|
"enabled": "Käytössä",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Peruuta",
|
||||||
|
"confirm": "Vahvista",
|
||||||
|
"content": "Sisältö",
|
||||||
|
"description": "Kuvaus",
|
||||||
|
"endTime": "Loppuaika",
|
||||||
|
"enterContent": "Ole hyvä ja syötä mainoksen sisältö",
|
||||||
|
"enterDescription": "Ole hyvä ja syötä mainoksen kuvaus",
|
||||||
|
"enterEndTime": "Ole hyvä ja valitse loppuaika",
|
||||||
|
"enterStartTime": "Ole hyvä ja valitse aloitusaika",
|
||||||
|
"enterTargetUrl": "Ole hyvä ja syötä kohde-URL",
|
||||||
|
"enterTitle": "Ole hyvä ja syötä mainoksen otsikko",
|
||||||
|
"startTime": "Aloitusaika",
|
||||||
|
"targetUrl": "Kohde-URL",
|
||||||
|
"title": "Otsikko",
|
||||||
|
"type": "Mainostyyppi",
|
||||||
|
"typeImage": "Kuva",
|
||||||
|
"typeVideo": "Video"
|
||||||
|
},
|
||||||
|
"status": "Tila",
|
||||||
|
"targetUrl": "Kohde-URL",
|
||||||
|
"title": "Otsikko",
|
||||||
|
"type": "Tyyppi",
|
||||||
|
"updateSuccess": "Päivitys onnistui",
|
||||||
|
"validityPeriod": "Voimassaoloaika"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Anna kelvollinen sähköpostiosoite.",
|
"email": "Anna kelvollinen sähköpostiosoite.",
|
||||||
"existingAccount": "Onko sinulla jo tili?",
|
"existingAccount": "Onko sinulla jo tili?",
|
||||||
"get": "Hanki",
|
"get": "Hanki",
|
||||||
"invite": "Kutsukoodi",
|
"invite": "Kutsukoodi (valinnainen)",
|
||||||
"message": "#### Arvoisa käyttäjä,\n\nKiitos mielenkiinnostasi ja tuestasi. Sivuston toimintastrategian muutosten vuoksi olemme sulkeneet uusien käyttäjien rekisteröinnin. Tänä aikana nykyisten käyttäjien käyttö ei vaikutu millään tavalla.\n\nOlemme sitoutuneet tarjoamaan sinulle parempaa palvelua ja kokemusta, joten rekisteröinnin sulkemisen aikana suoritamme kattavan järjestelmän optimoinnin ja toiminnallisuuden päivityksen. Tulevaisuudessa toivotamme sinut tervetulleeksi entistä laadukkaamman sisällön ja palvelun kera.\n\nSeuraa verkkosivustoamme ja sosiaalisen median alustoja saadaksesi uusimmat päivitykset ja ilmoitukset. Kiitos ymmärryksestäsi ja tuestasi.\n\nJos sinulla on kysyttävää tai tarvitset apua, ota yhteyttä asiakaspalvelutiimiimme.\n\n**Kiitos vielä kerran tuestasi ja ymmärryksestäsi.**",
|
"message": "#### Arvoisa käyttäjä,\n\nKiitos mielenkiinnostasi ja tuestasi. Sivuston toimintastrategian muutosten vuoksi olemme sulkeneet uusien käyttäjien rekisteröinnin. Tänä aikana nykyisten käyttäjien käyttö ei vaikutu millään tavalla.\n\nOlemme sitoutuneet tarjoamaan sinulle parempaa palvelua ja kokemusta, joten rekisteröinnin sulkemisen aikana suoritamme kattavan järjestelmän optimoinnin ja toiminnallisuuden päivityksen. Tulevaisuudessa toivotamme sinut tervetulleeksi entistä laadukkaamman sisällön ja palvelun kera.\n\nSeuraa verkkosivustoamme ja sosiaalisen median alustoja saadaksesi uusimmat päivitykset ja ilmoitukset. Kiitos ymmärryksestäsi ja tuestasi.\n\nJos sinulla on kysyttävää tai tarvitset apua, ota yhteyttä asiakaspalvelutiimiimme.\n\n**Kiitos vielä kerran tuestasi ja ymmärryksestäsi.**",
|
||||||
"passwordMismatch": "Salasanat eivät täsmää",
|
"passwordMismatch": "Salasanat eivät täsmää",
|
||||||
"success": "Rekisteröinti onnistui, olet kirjautunut automaattisesti!",
|
"success": "Rekisteröinti onnistui, olet kirjautunut automaattisesti!",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Estä virtuaalikone",
|
||||||
|
"blockVirtualMachineDescription": "Kun tämä on käytössä, laitteiden käyttö virtuaalikoneissa tai emulaattoreissa estetään",
|
||||||
|
"communicationKey": "Viestintäavain",
|
||||||
|
"communicationKeyDescription": "Viestintäavain käytetään turvalliseen viestintään laitteiden ja palvelimien välillä",
|
||||||
"enable": "Ota käyttöön",
|
"enable": "Ota käyttöön",
|
||||||
"enableDescription": "Kun otat käyttöön, useita laiteidentifikaattoreita, kuten IMEI/IDFA/IDFV/AndroidID/Mac-osoite, tuetaan kirjautumiseen ja rekisteröitymiseen."
|
"enableDescription": "Kun otat käyttöön, useita laiteidentifikaattoreita, kuten IMEI/IDFA/IDFV/AndroidID/Mac-osoite, tuetaan kirjautumiseen ja rekisteröitymiseen.",
|
||||||
|
"enableSecurity": "Ota käyttöön viestinnän salaus",
|
||||||
|
"enableSecurityDescription": "Kun tämä on käytössä, laitteiden ja palvelimien välinen viestintä salataan",
|
||||||
|
"saveFailed": "Tallennus epäonnistui",
|
||||||
|
"saveSuccess": "Tallennus onnistui",
|
||||||
|
"showAds": "Näytä mainoksia",
|
||||||
|
"showAdsDescription": "Kun tämä on käytössä, mainoksia näytetään laitteilla"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Tila",
|
"status": "Tila",
|
||||||
"subject": "Aihe",
|
"subject": "Aihe",
|
||||||
"template": "Malline",
|
"template": "Malline",
|
||||||
|
"traffic_exceed_email_template": "Liikenteen Ylitysilmoitusmalli",
|
||||||
|
"traffic_exceed_email_templateDescription": "{after}.variable{before} -paikat korvataan todellisilla tiedoilla. Varmista, että säilytät nämä muuttujat.",
|
||||||
"verify_email_template": "Sähköpostin Vahvistusmalli",
|
"verify_email_template": "Sähköpostin Vahvistusmalli",
|
||||||
"verify_email_templateDescription": "Paikkamerkit {after}.variable{before} korvataan todellisilla tiedoilla. Varmista, että pidät nämä muuttujat.",
|
"verify_email_templateDescription": "Paikkamerkit {after}.variable{before} korvataan todellisilla tiedoilla. Varmista, että pidät nämä muuttujat.",
|
||||||
"whitelistSuffixes": "Sallittujen luetteloiden päätteet",
|
"whitelistSuffixes": "Sallittujen luetteloiden päätteet",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"ADS Config": "ADS-asetukset",
|
||||||
"Announcement Management": "Ilmoitusten hallinta",
|
"Announcement Management": "Ilmoitusten hallinta",
|
||||||
"Apple": "Apple ID",
|
"Apple": "Apple ID",
|
||||||
|
"Application Management": "Sovellushallinta",
|
||||||
"Auth Control": "Todennuksen hallinta",
|
"Auth Control": "Todennuksen hallinta",
|
||||||
"Coupon Management": "Kuponkien hallinta",
|
"Coupon Management": "Kuponkien hallinta",
|
||||||
"Dashboard": "Kojelauta",
|
"Dashboard": "Kojelauta",
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
"Order Management": "Tilausten hallinta",
|
"Order Management": "Tilausten hallinta",
|
||||||
"Payment Config": "Maksukonfiguraatio",
|
"Payment Config": "Maksukonfiguraatio",
|
||||||
"Phone Number": "Puhelinnumero",
|
"Phone Number": "Puhelinnumero",
|
||||||
|
"Rule Management": "Sääntöhallinta",
|
||||||
"Server": "Palvelu",
|
"Server": "Palvelu",
|
||||||
"Server Management": "Palvelimen hallinta",
|
"Server Management": "Palvelimen hallinta",
|
||||||
"Settings": "Asetukset",
|
"Settings": "Asetukset",
|
||||||
|
|||||||
@@ -1,44 +1,52 @@
|
|||||||
{
|
{
|
||||||
"aliPay": "Alipay-maksu",
|
"AlipayF2F": "Alipay Kasvokkain",
|
||||||
"alipayf2f": {
|
"EPay": "EPay",
|
||||||
"appId": "Alipay-sovelluksen ID",
|
"Stripe": "Stripe",
|
||||||
"invoiceName": "Mukautettu tuotteen nimi",
|
"applyForPayment": "Hae maksua",
|
||||||
"invoiceNameDescription": "Näkyy Alipay-laskussa",
|
"balance": "Saldo",
|
||||||
"privateKey": "Alipay-yksityinen avain",
|
"batchDelete": "Ryhmän poisto",
|
||||||
"publicKey": "Alipay-julkinen avain",
|
"cancel": "Peruuta",
|
||||||
"sandbox": "Hiekkalaatikkotila",
|
"config": "Konfiguraatio",
|
||||||
"sandboxDescription": "Ota hiekkalaatikkotila käyttöön maksujen testaamiseksi"
|
"configInfo": "Konfiguraatio tiedot",
|
||||||
},
|
"configPlaceholder": "Täytä annettu {field} konfiguraatio",
|
||||||
|
"confirm": "Vahvista",
|
||||||
|
"confirmDelete": "Vahvista poisto",
|
||||||
|
"copy": "Kopioi",
|
||||||
|
"copySuccess": "Kopiointi onnistui",
|
||||||
|
"create": "Lisää maksutapa",
|
||||||
|
"createPayment": "Lisää maksutapa",
|
||||||
|
"createSuccess": "Luonti onnistui",
|
||||||
|
"delete": "Poista",
|
||||||
|
"deleteSuccess": "Poisto onnistui",
|
||||||
|
"deleteWarning": "Oletko varma, että haluat poistaa tämän maksutavan? Tätä toimintoa ei voi peruuttaa.",
|
||||||
|
"description": "Kuvaus",
|
||||||
|
"descriptionPlaceholder": "Syötä maksutavan kuvaus",
|
||||||
|
"domain": "Verkkotunnus",
|
||||||
|
"domainPlaceholder": "valinnainen, esim. {esimerkki}",
|
||||||
|
"edit": "Muokkaa",
|
||||||
|
"editPayment": "Muokkaa maksutapaa",
|
||||||
"enable": "Ota käyttöön",
|
"enable": "Ota käyttöön",
|
||||||
"enableDescription": "Ota tämä maksutapa käyttöön",
|
"feeAmount": "Kiinteä summa",
|
||||||
"epay": {
|
|
||||||
"key": "AVAIN",
|
|
||||||
"pid": "PID",
|
|
||||||
"url": "URL"
|
|
||||||
},
|
|
||||||
"feeMode": "Maksutapa",
|
"feeMode": "Maksutapa",
|
||||||
"feeModeDescription": "Lisämaksun veloitustapa",
|
|
||||||
"feeModeItems": ["Ei käsittelymaksua", "Prosenttiosuus", "Kiinteä summa", "Molemmat yllä olevat"],
|
|
||||||
"feePercent": "Prosenttiosuusmaksu (valinnainen)",
|
"feePercent": "Prosenttiosuusmaksu (valinnainen)",
|
||||||
"feePercentDescription": "Lisätään maksu tilauksen summan päälle",
|
|
||||||
"fixedFee": "Kiinteä maksu (valinnainen)",
|
"fixedFee": "Kiinteä maksu (valinnainen)",
|
||||||
"fixedFeeDescription": "Lisää kiinteä maksu tilauksen summan päälle",
|
"handlingFee": "Käsittelymaksu",
|
||||||
"iconUrl": "Kuvakkeen URL (valinnainen)",
|
"icon": "Ikoni",
|
||||||
"iconUrlDescription": "Käytetään etunäytössä",
|
"iconPlaceholder": "Syötä ikonin URL-osoite",
|
||||||
"inputPlaceholder": "Kirjoita tähän",
|
"mark": "Tunniste",
|
||||||
"key": "AVAIN",
|
"markPlaceholder": "Syötä maksutavan tunniste",
|
||||||
"notifyUrl": "Mukautettu ilmoitusalue (valinnainen)",
|
"name": "Nimi",
|
||||||
"notifyUrlDescription": "Yhdyskäytävän ilmoitus lähetetään tähän verkkotunnukseen",
|
"namePlaceholder": "Syötä maksutavan nimi",
|
||||||
"payForRecommendations": "Maksa suosituksista",
|
"nameRequired": "Nimi on pakollinen",
|
||||||
"pid": "PID",
|
"noFee": "Ei maksua",
|
||||||
"saveSuccess": "Tallennus onnistui",
|
"notify_url": "Ilmoitus-URL",
|
||||||
"showName": "Näytä nimi",
|
"paymentManagement": "Maksujen hallinta",
|
||||||
"showNameDescription": "Näytetään käyttöliittymässä",
|
"percentFee": "Prosentti",
|
||||||
"stripe": {
|
"platform": "Alusta",
|
||||||
"publicKey": "JULKINEN AVAIN",
|
"searchPlaceholder": "Syötä hakusanat",
|
||||||
"secretKey": "SALAINEN AVAIN",
|
"selectPlatform": "Valitse alusta",
|
||||||
"webhookSecret": "WEBHOOK-SALAINEN ALLEKIRJOITUS"
|
"selectType": "Valitse maksutyyppi",
|
||||||
},
|
"submit": "Lähetä",
|
||||||
"url": "URL",
|
"type": "Tyyppi",
|
||||||
"wechatPay": "WeChat-maksu"
|
"updateSuccess": "Päivitys onnistui"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"analyzing": "Analysoidaan...",
|
||||||
|
"appIcon": "Sääntökuvake",
|
||||||
|
"cancel": "Peruuta",
|
||||||
|
"confirm": "Vahvista",
|
||||||
|
"confirmDelete": "Vahvista poisto",
|
||||||
|
"create": "Lisää sääntö",
|
||||||
|
"createRule": "Lisää sääntö",
|
||||||
|
"createSuccess": "Sääntö luotiin onnistuneesti",
|
||||||
|
"createdAt": "Luotu",
|
||||||
|
"delete": "Poista",
|
||||||
|
"deleteSuccess": "Sääntö poistettiin onnistuneesti",
|
||||||
|
"deleteWarning": "Oletko varma, että haluat poistaa tämän säännön? Tätä toimintoa ei voi peruuttaa.",
|
||||||
|
"description": "Säännön kuvaus",
|
||||||
|
"downloadTemplate": "Lataa malli",
|
||||||
|
"edit": "Muokkaa",
|
||||||
|
"editRule": "Muokkaa sääntöä",
|
||||||
|
"enable": "Ota käyttöön",
|
||||||
|
"enterIconUrl": "Syötä tai lataa kuvake",
|
||||||
|
"enterRuleName": "Syötä säännön nimi",
|
||||||
|
"enterRules": "Syötä sääntöjen sisältö, yksi per rivi",
|
||||||
|
"import": "Tuoda",
|
||||||
|
"importFailed": "Tuonti epäonnistui",
|
||||||
|
"importSuccess": "Tuonti onnistui",
|
||||||
|
"importYaml": "Tuo YAML",
|
||||||
|
"importYamlDescription": "Tuo sääntöjä YAML-tiedostosta, järjestelmä luo automaattisesti sääntöryhmiä politiikkaryhmän nimien perusteella",
|
||||||
|
"importYamlRules": "Tuo YAML-säännöt",
|
||||||
|
"importing": "Tuodaan...",
|
||||||
|
"invalidYamlFormat": "Virheellinen YAML-muoto, puuttuu sääntöjen kenttä",
|
||||||
|
"name": "Säännön nimi",
|
||||||
|
"noValidRules": "Ei voimassa olevia sääntöjä löytynyt",
|
||||||
|
"pleaseUploadFile": "Lataa YAML-tiedosto",
|
||||||
|
"preview": "Esikatselu",
|
||||||
|
"rules": "Säännön sisältö",
|
||||||
|
"rulesFormat": "Sääntömuoto: sääntötyyppi, vastaavuus, [politiikka], jossa politiikka on valinnainen.\nJos politiikkaa ei ole määritelty, nykyistä sääntöryhmän nimeä käytetään automaattisesti. Esimerkkejä:",
|
||||||
|
"rulesLabel": "Säännön sisältö",
|
||||||
|
"searchRule": "Etsi säännön nimeä",
|
||||||
|
"selectFile": "Valitse tiedosto",
|
||||||
|
"selectTags": "Valitse solmun tunnisteet",
|
||||||
|
"tags": "Solmun tunnisteet",
|
||||||
|
"tagsLabel": "Solmun tunnisteet",
|
||||||
|
"updateSuccess": "Sääntö päivitettiin onnistuneesti"
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
"title": "Tietosuojakäytäntö"
|
"title": "Tietosuojakäytäntö"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
"customData": "Mukautettu tieto",
|
||||||
|
"customDataDescription": "Mukautettu tieto, jota käytetään sivuston mukautettuun tietoon",
|
||||||
"customHtml": "Mukautettu HTML",
|
"customHtml": "Mukautettu HTML",
|
||||||
"customHtmlDescription": "Mukautettu HTML-koodi, joka lisätään sivuston body -tagin loppuun.",
|
"customHtmlDescription": "Mukautettu HTML-koodi, joka lisätään sivuston body -tagin loppuun.",
|
||||||
"keywords": "Avainsanat",
|
"keywords": "Avainsanat",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"accountEnable": "Tilin aktivointi",
|
"accountEnable": "Tilin aktivointi",
|
||||||
"actions": "toiminnot",
|
"actions": "toiminnot",
|
||||||
|
"active": "Aktiivinen",
|
||||||
"add": "Lisää",
|
"add": "Lisää",
|
||||||
"administrator": "Ylläpitäjä",
|
"administrator": "Ylläpitäjä",
|
||||||
"areaCode": "Alueen koodi",
|
"areaCode": "Alueen koodi",
|
||||||
@@ -41,10 +42,12 @@
|
|||||||
"email": "sähköposti",
|
"email": "sähköposti",
|
||||||
"enable": "Ota käyttöön",
|
"enable": "Ota käyttöön",
|
||||||
"expireTime": "Vanhentumisaika",
|
"expireTime": "Vanhentumisaika",
|
||||||
|
"expired": "Vanhentunut",
|
||||||
"expiredAt": "Vanhentunut",
|
"expiredAt": "Vanhentunut",
|
||||||
"failed": "Epäonnistui",
|
"failed": "Epäonnistui",
|
||||||
"giftAmount": "Lahjan määrä",
|
"giftAmount": "Lahjan määrä",
|
||||||
"giftAmountPlaceholder": "Syötä lahjan määrä",
|
"giftAmountPlaceholder": "Syötä lahjan määrä",
|
||||||
|
"inactive": "Epäaktiivinen",
|
||||||
"invalidEmailFormat": "Virheellinen sähköpostimuoto",
|
"invalidEmailFormat": "Virheellinen sähköpostimuoto",
|
||||||
"inviteCode": "Kutsukoodi",
|
"inviteCode": "Kutsukoodi",
|
||||||
"inviteCodePlaceholder": "Syötä kutsukoodi (jätä tyhjäksi luodaksesi uuden)",
|
"inviteCodePlaceholder": "Syötä kutsukoodi (jätä tyhjäksi luodaksesi uuden)",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"passwordPlaceholder": "Syötä uusi salasana (valinnainen)",
|
"passwordPlaceholder": "Syötä uusi salasana (valinnainen)",
|
||||||
"permanent": "Pysyvä",
|
"permanent": "Pysyvä",
|
||||||
"pleaseEnterEmail": "Ole hyvä ja syötä sähköpostiosoite",
|
"pleaseEnterEmail": "Ole hyvä ja syötä sähköpostiosoite",
|
||||||
|
"price": "Hinta",
|
||||||
"referer": "Suosittelija",
|
"referer": "Suosittelija",
|
||||||
"refererId": "Viittaajan ID",
|
"refererId": "Viittaajan ID",
|
||||||
"refererIdPlaceholder": "Syötä suosittelijan ID",
|
"refererIdPlaceholder": "Syötä suosittelijan ID",
|
||||||
@@ -74,8 +78,12 @@
|
|||||||
"searchIp": "Etsi IP-osoite",
|
"searchIp": "Etsi IP-osoite",
|
||||||
"selectAuthType": "Valitse todennustyyppi",
|
"selectAuthType": "Valitse todennustyyppi",
|
||||||
"speedLimit": "Nopeusrajoitus",
|
"speedLimit": "Nopeusrajoitus",
|
||||||
|
"startTime": "Aloitusaika",
|
||||||
|
"status": "Tila",
|
||||||
"subscription": "Tilaus",
|
"subscription": "Tilaus",
|
||||||
"subscriptionDetails": "Tilaustiedot",
|
"subscriptionDetails": "Tilaustiedot",
|
||||||
|
"subscriptionId": "Tilauksen ID",
|
||||||
|
"subscriptionInfo": "Tilauksen tiedot",
|
||||||
"subscriptionList": "Tilauksen lista",
|
"subscriptionList": "Tilauksen lista",
|
||||||
"subscriptionLogs": "Tilaushistoria",
|
"subscriptionLogs": "Tilaushistoria",
|
||||||
"subscriptionName": "Tilauksen nimi",
|
"subscriptionName": "Tilauksen nimi",
|
||||||
@@ -89,6 +97,8 @@
|
|||||||
"tradeNotifications": "Kauppailmoitukset",
|
"tradeNotifications": "Kauppailmoitukset",
|
||||||
"trafficLimit": "Liikenteen rajoitus",
|
"trafficLimit": "Liikenteen rajoitus",
|
||||||
"trafficLogs": "Liikennelokit",
|
"trafficLogs": "Liikennelokit",
|
||||||
|
"trafficUsage": "Liikenteen käyttö",
|
||||||
|
"unknown": "Tuntematon",
|
||||||
"unlimited": "Rajoittamaton",
|
"unlimited": "Rajoittamaton",
|
||||||
"unverified": "Vahvistamaton",
|
"unverified": "Vahvistamaton",
|
||||||
"update": "Päivitä",
|
"update": "Päivitä",
|
||||||
@@ -98,11 +108,15 @@
|
|||||||
"userAgent": "Käyttäjäagentti",
|
"userAgent": "Käyttäjäagentti",
|
||||||
"userEmail": "Käyttäjän sähköposti",
|
"userEmail": "Käyttäjän sähköposti",
|
||||||
"userEmailPlaceholder": "Syötä käyttäjän sähköpostiosoite",
|
"userEmailPlaceholder": "Syötä käyttäjän sähköpostiosoite",
|
||||||
|
"userId": "Käyttäjän ID",
|
||||||
|
"userInfo": "Käyttäjän tiedot",
|
||||||
"userList": "Käyttäjälista",
|
"userList": "Käyttäjälista",
|
||||||
"userLogs": "Kirjautumishistoria",
|
"userLogs": "Kirjautumishistoria",
|
||||||
"userName": "Käyttäjän tunniste",
|
"userName": "Käyttäjän tunniste",
|
||||||
"userOrders": "Tilaukset",
|
"userOrders": "Tilaukset",
|
||||||
"userProfile": "Profiili",
|
"userProfile": "Profiili",
|
||||||
"userSubscriptions": "Tilaukset",
|
"userSubscriptions": "Tilaukset",
|
||||||
"verified": "Vahvistettu"
|
"username": "Käyttäjänimi",
|
||||||
|
"verified": "Vahvistettu",
|
||||||
|
"viewDetails": "Näytä tiedot"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"confirmDelete": "Confirmer la suppression",
|
||||||
|
"create": "Créer une annonce",
|
||||||
|
"createAds": "Créer une publicité",
|
||||||
|
"createSuccess": "Créé avec succès",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"deleteSuccess": "Supprimé avec succès",
|
||||||
|
"deleteWarning": "Êtes-vous sûr de vouloir supprimer cette annonce ?",
|
||||||
|
"disabled": "Désactivé",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"editAds": "Modifier la publicité",
|
||||||
|
"enabled": "Activé",
|
||||||
|
"form": {
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"content": "Contenu",
|
||||||
|
"description": "Description",
|
||||||
|
"endTime": "Heure de fin",
|
||||||
|
"enterContent": "Veuillez entrer le contenu de l'annonce",
|
||||||
|
"enterDescription": "Veuillez entrer la description de l'annonce",
|
||||||
|
"enterEndTime": "Veuillez sélectionner l'heure de fin",
|
||||||
|
"enterStartTime": "Veuillez sélectionner l'heure de début",
|
||||||
|
"enterTargetUrl": "Veuillez entrer l'URL cible",
|
||||||
|
"enterTitle": "Veuillez entrer le titre de l'annonce",
|
||||||
|
"startTime": "Heure de début",
|
||||||
|
"targetUrl": "URL cible",
|
||||||
|
"title": "Titre",
|
||||||
|
"type": "Type d'annonce",
|
||||||
|
"typeImage": "Image",
|
||||||
|
"typeVideo": "Vidéo"
|
||||||
|
},
|
||||||
|
"status": "Statut",
|
||||||
|
"targetUrl": "URL cible",
|
||||||
|
"title": "Titre",
|
||||||
|
"type": "Type",
|
||||||
|
"updateSuccess": "Mis à jour avec succès",
|
||||||
|
"validityPeriod": "Période de validité"
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"email": "Veuillez entrer une adresse e-mail valide.",
|
"email": "Veuillez entrer une adresse e-mail valide.",
|
||||||
"existingAccount": "Vous avez déjà un compte ?",
|
"existingAccount": "Vous avez déjà un compte ?",
|
||||||
"get": "Obtenir",
|
"get": "Obtenir",
|
||||||
"invite": "Code d'invitation",
|
"invite": "Code d'invitation (facultatif)",
|
||||||
"message": "#### Cher utilisateur,\n\nMerci pour votre intérêt et votre soutien. En raison d'un ajustement de notre stratégie opérationnelle, nous avons fermé la fonction d'inscription pour les nouveaux utilisateurs. Pendant cette période, l'utilisation par les utilisateurs existants ne sera pas affectée.\n\nNous nous engageons à vous offrir un meilleur service et une meilleure expérience, c'est pourquoi nous procéderons à une optimisation complète du système et à une mise à niveau des fonctionnalités pendant la fermeture des inscriptions. À l'avenir, nous vous accueillerons avec un contenu et des services de meilleure qualité.\n\nVeuillez suivre notre site web et nos plateformes de médias sociaux pour obtenir les dernières nouvelles et notifications. Merci pour votre compréhension et votre soutien.\n\nSi vous avez des questions ou avez besoin d'aide, n'hésitez pas à contacter notre équipe de service client.\n\n**Merci encore pour votre soutien et votre compréhension.**",
|
"message": "#### Cher utilisateur,\n\nMerci pour votre intérêt et votre soutien. En raison d'un ajustement de notre stratégie opérationnelle, nous avons fermé la fonction d'inscription pour les nouveaux utilisateurs. Pendant cette période, l'utilisation par les utilisateurs existants ne sera pas affectée.\n\nNous nous engageons à vous offrir un meilleur service et une meilleure expérience, c'est pourquoi nous procéderons à une optimisation complète du système et à une mise à niveau des fonctionnalités pendant la fermeture des inscriptions. À l'avenir, nous vous accueillerons avec un contenu et des services de meilleure qualité.\n\nVeuillez suivre notre site web et nos plateformes de médias sociaux pour obtenir les dernières nouvelles et notifications. Merci pour votre compréhension et votre soutien.\n\nSi vous avez des questions ou avez besoin d'aide, n'hésitez pas à contacter notre équipe de service client.\n\n**Merci encore pour votre soutien et votre compréhension.**",
|
||||||
"passwordMismatch": "Les mots de passe saisis ne correspondent pas",
|
"passwordMismatch": "Les mots de passe saisis ne correspondent pas",
|
||||||
"success": "Inscription réussie, vous êtes maintenant connecté automatiquement !",
|
"success": "Inscription réussie, vous êtes maintenant connecté automatiquement !",
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
{
|
{
|
||||||
|
"blockVirtualMachine": "Bloquer la machine virtuelle",
|
||||||
|
"blockVirtualMachineDescription": "Lorsqu'il est activé, les appareils seront empêchés de fonctionner sur des machines virtuelles ou des émulateurs",
|
||||||
|
"communicationKey": "Clé de communication",
|
||||||
|
"communicationKeyDescription": "La clé de communication est utilisée pour une communication sécurisée entre les appareils et les serveurs",
|
||||||
"enable": "Activer",
|
"enable": "Activer",
|
||||||
"enableDescription": "Après activation, plusieurs identifiants de périphériques tels que IMEI/IDFA/IDFV/AndroidID/adresse Mac sont pris en charge pour la connexion et l'enregistrement"
|
"enableDescription": "Après activation, plusieurs identifiants de périphériques tels que IMEI/IDFA/IDFV/AndroidID/adresse Mac sont pris en charge pour la connexion et l'enregistrement",
|
||||||
|
"enableSecurity": "Activer le chiffrement de la communication",
|
||||||
|
"enableSecurityDescription": "Lorsqu'il est activé, la communication entre les appareils et les serveurs sera chiffrée",
|
||||||
|
"saveFailed": "Échec de l'enregistrement",
|
||||||
|
"saveSuccess": "Enregistrement réussi",
|
||||||
|
"showAds": "Afficher les publicités",
|
||||||
|
"showAdsDescription": "Lorsqu'il est activé, des publicités seront affichées sur les appareils"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"subject": "Sujet",
|
"subject": "Sujet",
|
||||||
"template": "Modèle",
|
"template": "Modèle",
|
||||||
|
"traffic_exceed_email_template": "Modèle de notification de dépassement de trafic",
|
||||||
|
"traffic_exceed_email_templateDescription": "Les espaces réservés {after}.variable{before} seront remplacés par des données réelles. Assurez-vous de conserver ces variables.",
|
||||||
"verify_email_template": "Modèle d'Email de Vérification",
|
"verify_email_template": "Modèle d'Email de Vérification",
|
||||||
"verify_email_templateDescription": "Les espaces réservés {after}.variable{before} seront remplacés par des données réelles. Assurez-vous de conserver ces variables.",
|
"verify_email_templateDescription": "Les espaces réservés {after}.variable{before} seront remplacés par des données réelles. Assurez-vous de conserver ces variables.",
|
||||||
"whitelistSuffixes": "Suffixes de la liste blanche",
|
"whitelistSuffixes": "Suffixes de la liste blanche",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user