Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f60d40c06d | |||
| 1b6befac87 | |||
| 3cb0629d64 | |||
| 1d8c765c14 | |||
| 39e89bfef5 | |||
| 52c1d1fc60 | |||
| 3d92902f1b | |||
| 331bbea45f | |||
| a604f28fef | |||
| 69fc670ba3 | |||
| c53ac61501 | |||
| 795476209a | |||
| 9227411d74 | |||
| 0aa5d5b0a9 | |||
| 40a6f7ca81 | |||
| 8346c85109 | |||
| aa6dda894b | |||
| 5ed04c0a59 | |||
| 652e0323fd | |||
| d1f5a9b825 | |||
| 88b3504735 | |||
| 28f8c78963 | |||
| 9140b8ad1e | |||
| 6227ba91d0 | |||
| 18ee600836 | |||
| cefcb310d6 | |||
| 4ea6e4a2a3 | |||
| 4983c33bb3 | |||
| 3e8912e7bb | |||
| b6da51b618 | |||
| 9d8b814212 | |||
| 5e5e4edd2e | |||
| d6fbc38795 | |||
| b0aa3645f8 | |||
| 30ae781f05 | |||
| f3204b757f | |||
| 1d0d911741 | |||
| 5d078fdaca | |||
| 8775fb620e | |||
| 770932e957 | |||
| 1cdb7e7872 | |||
| 72396851ff | |||
| 7b6bb7bc69 | |||
| 57eaa5532d | |||
| 4acf7b1837 | |||
| 9b8258ce5a | |||
| 585b99c2cc | |||
| 39a9ce60de | |||
| 207bc247f4 | |||
| 938363b1eb | |||
| 7f0f5ce9db | |||
| f90d4d2ce6 | |||
| 18b07c750d | |||
| 19619fdbe0 | |||
| f408fdffae | |||
| f26f1c24ae | |||
| c59742a4f3 | |||
| dd23279e3e | |||
| c984c0d9c0 | |||
| f275f01f25 | |||
| c2af06052c | |||
| 8f8a12a510 | |||
| 77edf1d6ec | |||
| f8b6d82780 | |||
| b01a5bc83b | |||
| 25da429da6 | |||
| 12026b02cd | |||
| 6301409531 | |||
| b13c77e9b7 | |||
| a4e9d5da59 | |||
| 70497af0aa | |||
| 46ae1665be | |||
| 3547bb1596 | |||
| 10d322f1ba | |||
| 1d917383eb | |||
| 36c76674b6 | |||
| 5766376590 | |||
| 6d0d069ce7 | |||
| 6de29d51c4 | |||
| e8a72d5579 | |||
| eeea165f7e | |||
| 25ce37ea05 | |||
| b2a2f42e53 | |||
| cc834caa30 | |||
| 4aea4e811f | |||
| fb0c510b93 | |||
| d28a10b6df | |||
| f99c6048ad | |||
| 136287d85a | |||
| 5bc7905a52 | |||
| 701cdee6de | |||
| 3065c3a0df | |||
| 68bc18f584 | |||
| cd59d44435 | |||
| 3cc94773ef | |||
| c44ad47f3c | |||
| 837157cc42 | |||
| fec80f592d | |||
| a93db4eb9f |
+149
-2
@@ -1,12 +1,159 @@
|
|||||||
<a name="readme-top"></a>
|
<a name="readme-top"></a>
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
# [1.0.0-beta.3](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-12-26)
|
# [1.0.0-beta.7](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2025-01-28)
|
||||||
|
|
||||||
|
|
||||||
### ♻ Code Refactoring
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
* **config**: Simplify environment variable handling and improve build script ([cf54d0f](https://github.com/perfect-panel/ppanel-web/commit/cf54d0f))
|
* **sbscribe**: Rename and reorganize components for better structure and clarity ([5e5e4ed](https://github.com/perfect-panel/ppanel-web/commit/5e5e4ed))
|
||||||
|
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
* **auth**: Add email and SMS code sending functionality with localization updates ([57eaa55](https://github.com/perfect-panel/ppanel-web/commit/57eaa55))
|
||||||
|
* **auth**: Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple ([18ee600](https://github.com/perfect-panel/ppanel-web/commit/18ee600))
|
||||||
|
* **auth**: Add SMS and email configuration options to global store and update localization ([4acf7b1](https://github.com/perfect-panel/ppanel-web/commit/4acf7b1))
|
||||||
|
* **auth**: Enhance user registration with invite handling and logo display ([207bc24](https://github.com/perfect-panel/ppanel-web/commit/207bc24))
|
||||||
|
* **auth**: Redirect user after OAuth login and add logos icon collection ([aa6dda8](https://github.com/perfect-panel/ppanel-web/commit/aa6dda8))
|
||||||
|
* **config**: Add application selection and encryption settings to configuration form ([88b3504](https://github.com/perfect-panel/ppanel-web/commit/88b3504))
|
||||||
|
* **config**: Update encryption fields in configuration form and refactor OAuth callback parameters ([652e032](https://github.com/perfect-panel/ppanel-web/commit/652e032))
|
||||||
|
* **global**: Add SMS configuration options to global store ([39a9ce6](https://github.com/perfect-panel/ppanel-web/commit/39a9ce6))
|
||||||
|
* **locales**: Add area code and telephone fields to user forms in multiple languages ([9b8258c](https://github.com/perfect-panel/ppanel-web/commit/9b8258c))
|
||||||
|
* **locales**: Add description information of communication keys and encryption methods to enhance client configuration capabilities ([d1f5a9b](https://github.com/perfect-panel/ppanel-web/commit/d1f5a9b))
|
||||||
|
* **node**: Add tags ([f408fdf](https://github.com/perfect-panel/ppanel-web/commit/f408fdf))
|
||||||
|
* **node**: Move the node configuration to the server module ([7f0f5ce](https://github.com/perfect-panel/ppanel-web/commit/7f0f5ce))
|
||||||
|
* **oauth**: Add certification component for handling OAuth login callbacks and improve user authentication flow ([5ed04c0](https://github.com/perfect-panel/ppanel-web/commit/5ed04c0))
|
||||||
|
* **oauth**: Implement OAuth token retrieval and refactor login callback handling ([40a6f7c](https://github.com/perfect-panel/ppanel-web/commit/40a6f7c))
|
||||||
|
* **oauth**: Refactor platform parameter handling and improve logout redirection logic ([8346c85](https://github.com/perfect-panel/ppanel-web/commit/8346c85))
|
||||||
|
* **oauth**: Update OAuth login handling to use callback parameter and improve URL parameter retrieval ([9227411](https://github.com/perfect-panel/ppanel-web/commit/9227411))
|
||||||
|
* **sms**: Update locales ([938363b](https://github.com/perfect-panel/ppanel-web/commit/938363b))
|
||||||
|
* **subscribe**: Add 'sold' column to SubscribeTable and update inventory terminology ([19619fd](https://github.com/perfect-panel/ppanel-web/commit/19619fd))
|
||||||
|
* **subscribe**: Move subscription configuration and application to subscription module ([f90d4d2](https://github.com/perfect-panel/ppanel-web/commit/f90d4d2))
|
||||||
|
* **subscribe**: Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting ([f26f1c2](https://github.com/perfect-panel/ppanel-web/commit/f26f1c2))
|
||||||
|
* **tutorial**: Fetch the latest tutorial version from GitHub API for dynamic URL generation ([28f8c78](https://github.com/perfect-panel/ppanel-web/commit/28f8c78))
|
||||||
|
* **user**: Add telephone input with area code selection and update localization ([585b99c](https://github.com/perfect-panel/ppanel-web/commit/585b99c))
|
||||||
|
* Update Auth Control ([c59742a](https://github.com/perfect-panel/ppanel-web/commit/c59742a))
|
||||||
|
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* **api**: Remove redundant requestType parameter in appleLoginCallback ([0aa5d5b](https://github.com/perfect-panel/ppanel-web/commit/0aa5d5b))
|
||||||
|
* **api**: Rename app-related functions and types to application for consistency ([9d8b814](https://github.com/perfect-panel/ppanel-web/commit/9d8b814))
|
||||||
|
* **api**: Update subscription_protocol to subscribe_type for consistency across services ([b6da51b](https://github.com/perfect-panel/ppanel-web/commit/b6da51b))
|
||||||
|
* **auth**: Change Textarea value to defaultValue for client_secret in Apple auth page ([69fc670](https://github.com/perfect-panel/ppanel-web/commit/69fc670))
|
||||||
|
* **auth**: Remove unused telephone code login function and update typings for telephone login requests ([7239685](https://github.com/perfect-panel/ppanel-web/commit/7239685))
|
||||||
|
* **auth**: Require minimum length for invite string when forced invite is enabled ([a604f28](https://github.com/perfect-panel/ppanel-web/commit/a604f28))
|
||||||
|
* **auth**: Update user authentication flow to include email and phone code verification ([5d078fd](https://github.com/perfect-panel/ppanel-web/commit/5d078fd))
|
||||||
|
* **dashboard**: Improve URL encoding for subscription links and enhance success message handling ([4983c33](https://github.com/perfect-panel/ppanel-web/commit/4983c33))
|
||||||
|
* **dashboard**: Update icon imports for platform consistency and adjust icon size ([3e8912e](https://github.com/perfect-panel/ppanel-web/commit/3e8912e))
|
||||||
|
* **dashboard**: Update platform detection logic and improve layout responsiveness ([b0aa364](https://github.com/perfect-panel/ppanel-web/commit/b0aa364))
|
||||||
|
* **deps**: Remove outdated @iconify/react dependency and add iconify-json packages ([d6fbc38](https://github.com/perfect-panel/ppanel-web/commit/d6fbc38))
|
||||||
|
* **locales**: Add error message for incorrect user information ([52c1d1f](https://github.com/perfect-panel/ppanel-web/commit/52c1d1f))
|
||||||
|
* **locales**: Add error message for incorrect user information ([3d92902](https://github.com/perfect-panel/ppanel-web/commit/3d92902))
|
||||||
|
* **locales**: Add logout message to authentication localization files ([1d0d911](https://github.com/perfect-panel/ppanel-web/commit/1d0d911))
|
||||||
|
* **nav**: Comment out unused social login options to simplify navigation configuration ([cefcb31](https://github.com/perfect-panel/ppanel-web/commit/cefcb31))
|
||||||
|
* **node-config**: Add null checks for time slots and ensure proper handling of undefined values ([1cdb7e7](https://github.com/perfect-panel/ppanel-web/commit/1cdb7e7))
|
||||||
|
* **node**: Add country and city fields to the form schema and localization files ([8775fb6](https://github.com/perfect-panel/ppanel-web/commit/8775fb6))
|
||||||
|
* **oauth**: Refactor OAuth configuration types and update related API methods ([6227ba9](https://github.com/perfect-panel/ppanel-web/commit/6227ba9))
|
||||||
|
* **oauth**: Remove redundant checks when updating configuration to simplify logic ([9140b8a](https://github.com/perfect-panel/ppanel-web/commit/9140b8a))
|
||||||
|
* **payment**: Replace window.open with window.location.href for checkout links ([1d8c765](https://github.com/perfect-panel/ppanel-web/commit/1d8c765))
|
||||||
|
* **phone**: Update SMS expiration time field to use 'sms_expire_time' with default value of 300 ([18b07c7](https://github.com/perfect-panel/ppanel-web/commit/18b07c7))
|
||||||
|
* **redirect**: Simplify redirect URL logic by removing unnecessary condition for sessionStorage ([c53ac61](https://github.com/perfect-panel/ppanel-web/commit/c53ac61))
|
||||||
|
* **redirect**: Update redirect URL logic to ensure proper handling of OAuth and auth paths ([7954762](https://github.com/perfect-panel/ppanel-web/commit/7954762))
|
||||||
|
* **site**: Add image upload functionality for site logo configuration ([4ea6e4a](https://github.com/perfect-panel/ppanel-web/commit/4ea6e4a))
|
||||||
|
* **sort**: Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance ([331bbea](https://github.com/perfect-panel/ppanel-web/commit/331bbea))
|
||||||
|
* **subscription**: Add reset functionality for user subscription token ([39e89bf](https://github.com/perfect-panel/ppanel-web/commit/39e89bf))
|
||||||
|
* **type**: Fix ts type check error ([3cb0629](https://github.com/perfect-panel/ppanel-web/commit/3cb0629))
|
||||||
|
* **user-nav**: Update user avatar and label to display telephone if email is not available ([7b6bb7b](https://github.com/perfect-panel/ppanel-web/commit/7b6bb7b))
|
||||||
|
* **user**: Update user identifier field and localizations ([1b6befa](https://github.com/perfect-panel/ppanel-web/commit/1b6befa))
|
||||||
|
|
||||||
|
|
||||||
|
### 💄 Styles
|
||||||
|
|
||||||
|
* **dashboard**: Adjust grid layout and update image dimensions in application display ([f3204b7](https://github.com/perfect-panel/ppanel-web/commit/f3204b7))
|
||||||
|
* **globals**: Refactor delete confirmation button and update badge styles in node and subscribe tables ([30ae781](https://github.com/perfect-panel/ppanel-web/commit/30ae781))
|
||||||
|
* Update node secret UI and add telephone code field to authentication form ([770932e](https://github.com/perfect-panel/ppanel-web/commit/770932e))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.6](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2025-01-10)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **auth**: Update UserCheckForm to use setInitialValues and modify onSubmit type ([c984c0d](https://github.com/perfect-panel/ppanel-web/commit/c984c0d))
|
||||||
|
|
||||||
|
# [1.0.0-beta.5](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2025-01-09)
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **locales**: Replace 'nodeGroupId' with 'groupId' in multiple language files for consistency ([a4e9d5d](https://github.com/perfect-panel/ppanel-web/commit/a4e9d5d))
|
||||||
|
- **locales**: Update 'deductBalance' to 'giftAmount' across multiple languages and fix newline in announcement.json ([70497af](https://github.com/perfect-panel/ppanel-web/commit/70497af))
|
||||||
|
- **stats**: Replace dynamic stat fetching with environment constants for user, server, and location counts ([46ae166](https://github.com/perfect-panel/ppanel-web/commit/46ae166))
|
||||||
|
- **subscribe**: Update suffix from 'MB' to 'Mbps' and enhance speed limit display logic ([3547bb1](https://github.com/perfect-panel/ppanel-web/commit/3547bb1))
|
||||||
|
- **user**: Add 'gift_amount' field and update related references in user services and components ([b13c77e](https://github.com/perfect-panel/ppanel-web/commit/b13c77e))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- **deps**: Update package dependencies across multiple projects for improved stability and performance ([b01a5bc](https://github.com/perfect-panel/ppanel-web/commit/b01a5bc))
|
||||||
|
- **ui**: Update package dependencies for improved stability and performance ([25da429](https://github.com/perfect-panel/ppanel-web/commit/25da429))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **api**: Replace 'deduction' with 'gift_amount' and add 'commission' field in type definitions ([77edf1d](https://github.com/perfect-panel/ppanel-web/commit/77edf1d))
|
||||||
|
- **api**: Update API type definitions to replace 'deduction' with 'gift_amount' and make 'commission' field optional ([c2af060](https://github.com/perfect-panel/ppanel-web/commit/c2af060))
|
||||||
|
- **auth**: Refactor user authentication forms to remove global store dependency and improve type handling ([12026b0](https://github.com/perfect-panel/ppanel-web/commit/12026b0))
|
||||||
|
- **coupon**: Rename 'server' field to 'subscribe' in coupon form and update coupon update request type ([f8b6d82](https://github.com/perfect-panel/ppanel-web/commit/f8b6d82))
|
||||||
|
- **types**: Add 'gift_amount' field to API type definitions ([8f8a12a](https://github.com/perfect-panel/ppanel-web/commit/8f8a12a))
|
||||||
|
- **user**: Add the 'gift_amount' field to the user service's type definition ([6301409](https://github.com/perfect-panel/ppanel-web/commit/6301409))
|
||||||
|
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
# [1.0.0-beta.4](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2025-01-07)
|
||||||
|
|
||||||
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
|
- **auth**: Refactor user authorization handling and improve error logging ([68bc18f](https://github.com/perfect-panel/ppanel-web/commit/68bc18f))
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **affiliate**: Add Affiliate component with commission display and invite link functionality ([4aea4e8](https://github.com/perfect-panel/ppanel-web/commit/4aea4e8))
|
||||||
|
- **affiliate**: Update affiliate component to display total commission and improve data fetching ([cc834ca](https://github.com/perfect-panel/ppanel-web/commit/cc834ca))
|
||||||
|
- **api**: Add new subscription properties and locale support for deduction ratios and reset cycles ([fec80f5](https://github.com/perfect-panel/ppanel-web/commit/fec80f5))
|
||||||
|
- **api**: Add Time Period Configuration ([837157c](https://github.com/perfect-panel/ppanel-web/commit/837157c))
|
||||||
|
- **favicon**: Update SVG favicon design for admin and user interfaces ([1d91738](https://github.com/perfect-panel/ppanel-web/commit/1d91738))
|
||||||
|
- **node**: Add serverKey ([25ce37e](https://github.com/perfect-panel/ppanel-web/commit/25ce37e))
|
||||||
|
- **relay**: Add relay mode configuration and update related schemas ([3cc9477](https://github.com/perfect-panel/ppanel-web/commit/3cc9477))
|
||||||
|
- **schema**: Add security field to hysteria2 and tuic schemas ([cd59d44](https://github.com/perfect-panel/ppanel-web/commit/cd59d44))
|
||||||
|
- **subscribe**: Add reset_time to API typings and update unsubscribe logic ([eeea165](https://github.com/perfect-panel/ppanel-web/commit/eeea165))
|
||||||
|
- **subscribe**: Add subscribe_discount type ([f99c604](https://github.com/perfect-panel/ppanel-web/commit/f99c604))
|
||||||
|
- **subscribe**: Add subscription credits ([5bc7905](https://github.com/perfect-panel/ppanel-web/commit/5bc7905))
|
||||||
|
- **subscribe**: Add unsubscribe functionality with confirmation messages and localized strings ([b2a2f42](https://github.com/perfect-panel/ppanel-web/commit/b2a2f42))
|
||||||
|
- **subscribe**: Improve error handling in subscription forms and update component props ([d28a10b](https://github.com/perfect-panel/ppanel-web/commit/d28a10b))
|
||||||
|
- **subscribe**: Improve layout and styling in subscription components ([5766376](https://github.com/perfect-panel/ppanel-web/commit/5766376))
|
||||||
|
- **subscription**: Add localized messages for existing subscriptions and deletion restrictions ([e8a72d5](https://github.com/perfect-panel/ppanel-web/commit/e8a72d5))
|
||||||
|
|
||||||
|
### 🎫 Chores
|
||||||
|
|
||||||
|
- Update changelog, enhance prepare script, and add openapi command ([a93db4e](https://github.com/perfect-panel/ppanel-web/commit/a93db4e))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **dashboard**: Correct progress value calculations and update groupId accessor ([36c7667](https://github.com/perfect-panel/ppanel-web/commit/36c7667))
|
||||||
|
- **layout**: Remove unnecessary cookie initialization in Logout function ([3065c3a](https://github.com/perfect-panel/ppanel-web/commit/3065c3a))
|
||||||
|
- **locales**: Update Hong Kong ([6d0d069](https://github.com/perfect-panel/ppanel-web/commit/6d0d069))
|
||||||
|
- **subscribe**: Update value validation to check for number type in subscribe form ([6de29d5](https://github.com/perfect-panel/ppanel-web/commit/6de29d5))
|
||||||
|
|
||||||
|
### 💄 Styles
|
||||||
|
|
||||||
|
- **locales**: Remove unused subscription labels from multiple locale files ([fb0c510](https://github.com/perfect-panel/ppanel-web/commit/fb0c510))
|
||||||
|
- **locales**: Update server.json to reorganize relay mode options and improve labels ([701cdee](https://github.com/perfect-panel/ppanel-web/commit/701cdee))
|
||||||
|
- **node**: Improve layout and spacing in NodeStatusCell component ([136287d](https://github.com/perfect-panel/ppanel-web/commit/136287d))
|
||||||
|
- **time-slot**: Add chart display ([c44ad47](https://github.com/perfect-panel/ppanel-web/commit/c44ad47))
|
||||||
|
|
||||||
<a name="readme-top"></a>
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
|||||||
+19
-68
@@ -1,27 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { resetPassword, userLogin, userRegister } from '@/services/common/auth';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { ReactNode, useState, useTransition } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NEXT_PUBLIC_DEFAULT_USER_EMAIL,
|
NEXT_PUBLIC_DEFAULT_USER_EMAIL,
|
||||||
NEXT_PUBLIC_DEFAULT_USER_PASSWORD,
|
NEXT_PUBLIC_DEFAULT_USER_PASSWORD,
|
||||||
} from '@/config/constants';
|
} from '@/config/constants';
|
||||||
import useGlobalStore from '@/config/use-global';
|
|
||||||
import { checkUser, resetPassword, userLogin, userRegister } from '@/services/common/auth';
|
|
||||||
import { getRedirectUrl, setAuthorization } from '@/utils/common';
|
import { getRedirectUrl, setAuthorization } from '@/utils/common';
|
||||||
import { useTranslations } from 'next-intl';
|
import LoginForm from './login-form';
|
||||||
import { useRouter } from 'next/navigation';
|
import RegisterForm from './register-form';
|
||||||
import { ReactNode, useState, useTransition } from 'react';
|
import ResetForm from './reset-form';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import UserCheckForm from './user-check-form';
|
|
||||||
import UserLoginForm from './user-login-form';
|
|
||||||
import UserRegisterForm from './user-register-form';
|
|
||||||
import UserResetForm from './user-reset-form';
|
|
||||||
|
|
||||||
export default function UserAuthForm() {
|
export default function EmailAuthForm() {
|
||||||
const t = useTranslations('auth');
|
const t = useTranslations('auth');
|
||||||
const { common } = useGlobalStore();
|
|
||||||
const { register } = common;
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [type, setType] = useState<'login' | 'register' | 'reset'>();
|
const [type, setType] = useState<'login' | 'register' | 'reset'>('login');
|
||||||
const [loading, startTransition] = useTransition();
|
const [loading, startTransition] = useTransition();
|
||||||
const [initialValues, setInitialValues] = useState<{
|
const [initialValues, setInitialValues] = useState<{
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -41,44 +38,35 @@ export default function UserAuthForm() {
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'login':
|
case 'login': {
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const login = await userLogin(params);
|
const login = await userLogin(params);
|
||||||
toast.success(t('login.success'));
|
toast.success(t('login.success'));
|
||||||
onLogin(login.data.data?.token);
|
onLogin(login.data.data?.token);
|
||||||
break;
|
break;
|
||||||
case 'register':
|
}
|
||||||
// eslint-disable-next-line no-case-declarations
|
case 'register': {
|
||||||
const create = await userRegister(params);
|
const create = await userRegister(params);
|
||||||
toast.success(t('register.success'));
|
toast.success(t('register.success'));
|
||||||
onLogin(create.data.data?.token);
|
onLogin(create.data.data?.token);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'reset':
|
case 'reset':
|
||||||
await resetPassword(params);
|
await resetPassword(params);
|
||||||
toast.success(t('reset.success'));
|
toast.success(t('reset.success'));
|
||||||
setType('login');
|
setType('login');
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
if (type === 'reset') break;
|
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const response = await checkUser(params);
|
|
||||||
setInitialValues({
|
|
||||||
...initialValues,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
setType(response.data.data?.exist ? 'login' : 'register');
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let UserForm: ReactNode = null;
|
let UserForm: ReactNode = null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'login':
|
case 'login':
|
||||||
UserForm = (
|
UserForm = (
|
||||||
<UserLoginForm
|
<LoginForm
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
@@ -89,7 +77,7 @@ export default function UserAuthForm() {
|
|||||||
break;
|
break;
|
||||||
case 'register':
|
case 'register':
|
||||||
UserForm = (
|
UserForm = (
|
||||||
<UserRegisterForm
|
<RegisterForm
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
@@ -100,7 +88,7 @@ export default function UserAuthForm() {
|
|||||||
break;
|
break;
|
||||||
case 'reset':
|
case 'reset':
|
||||||
UserForm = (
|
UserForm = (
|
||||||
<UserResetForm
|
<ResetForm
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
@@ -109,15 +97,6 @@ export default function UserAuthForm() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
UserForm = (
|
|
||||||
<UserCheckForm
|
|
||||||
loading={loading}
|
|
||||||
onSubmit={handleFormSubmit}
|
|
||||||
initialValues={initialValues}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -128,34 +107,6 @@ export default function UserAuthForm() {
|
|||||||
{t(`${type || 'check'}.description`)}
|
{t(`${type || 'check'}.description`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* {!((type === 'register' && register.stop_register) || type === 'reset') && (
|
|
||||||
<>
|
|
||||||
<div className='mb-3 flex flex-wrap items-center justify-center gap-3 font-bold'>
|
|
||||||
<Button type='button' variant='outline'>
|
|
||||||
<Icon icon='uil:telegram' className='mr-2 size-5' />
|
|
||||||
Telegram
|
|
||||||
</Button>
|
|
||||||
<Button type='button' variant='outline'>
|
|
||||||
<Icon icon='uil:google' className='mr-2 size-5' />
|
|
||||||
Google
|
|
||||||
</Button>
|
|
||||||
<Button type='button' variant='outline'>
|
|
||||||
<Icon icon='uil:apple' className='mr-2 size-5' />
|
|
||||||
Apple
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'my-14 flex h-0 items-center text-center',
|
|
||||||
'before:mr-4 before:block before:w-1/2 before:border-b-[1px]',
|
|
||||||
'after:ml-4 after:w-1/2 after:border-b-[1px]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className='text-muted-foreground w-[125px] text-sm'>{t('orWithEmail')}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{UserForm}
|
{UserForm}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
+9
-14
@@ -1,16 +1,16 @@
|
|||||||
import useGlobalStore from '@/config/use-global';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
||||||
import { Input } from '@workspace/ui/components/input';
|
import { Input } from '@workspace/ui/components/input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CloudFlareTurnstile from './turnstile';
|
import CloudFlareTurnstile from '../turnstile';
|
||||||
|
|
||||||
export default function UserLoginForm({
|
export default function LoginForm({
|
||||||
loading,
|
loading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -21,14 +21,14 @@ export default function UserLoginForm({
|
|||||||
onSubmit: (data: any) => void;
|
onSubmit: (data: any) => void;
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
setInitialValues: Dispatch<SetStateAction<any>>;
|
setInitialValues: Dispatch<SetStateAction<any>>;
|
||||||
onSwitchForm: (type?: 'register' | 'reset') => void;
|
onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations('auth.login');
|
const t = useTranslations('auth.login');
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify } = common;
|
const { verify } = common;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string().email(t('email')),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_login_verify && verify.turnstile_site_key ? z.string() : z.string().optional(),
|
verify.enable_login_verify && verify.turnstile_site_key ? z.string() : z.string().optional(),
|
||||||
@@ -48,7 +48,7 @@ export default function UserLoginForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input disabled placeholder='Enter your email...' type='email' {...field} />
|
<Input placeholder='Enter your email...' type='email' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -60,12 +60,7 @@ export default function UserLoginForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='Enter your password...' type='password' {...field} />
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter your password...'
|
|
||||||
type='password'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -100,10 +95,10 @@ export default function UserLoginForm({
|
|||||||
className='p-0'
|
className='p-0'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInitialValues(undefined);
|
setInitialValues(undefined);
|
||||||
onSwitchForm(undefined);
|
onSwitchForm('register');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('switchAccount')}
|
{t('registerAccount')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
+42
-46
@@ -1,19 +1,18 @@
|
|||||||
import useGlobalStore from '@/config/use-global';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { sendEmailCode } from '@/services/common/common';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
||||||
import { Input } from '@workspace/ui/components/input';
|
import { Input } from '@workspace/ui/components/input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
||||||
import { useCountDown } from 'ahooks';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CloudFlareTurnstile from './turnstile';
|
import SendCode from '../send-code';
|
||||||
|
import CloudFlareTurnstile from '../turnstile';
|
||||||
|
|
||||||
export default function UserRegisterForm({
|
export default function RegisterForm({
|
||||||
loading,
|
loading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -24,33 +23,36 @@ export default function UserRegisterForm({
|
|||||||
onSubmit: (data: any) => void;
|
onSubmit: (data: any) => void;
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
setInitialValues: Dispatch<SetStateAction<any>>;
|
setInitialValues: Dispatch<SetStateAction<any>>;
|
||||||
onSwitchForm: (type?: 'register' | 'reset') => void;
|
onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations('auth.register');
|
const t = useTranslations('auth.register');
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, register, invite } = common;
|
const { verify, auth, invite } = common;
|
||||||
|
|
||||||
const [targetDate, setTargetDate] = useState<number>();
|
const handleCheckUser = async (email: string) => {
|
||||||
const [, { seconds }] = useCountDown({
|
try {
|
||||||
targetDate,
|
const domain = email.split('@')[1];
|
||||||
onEnd: () => {
|
const isValid =
|
||||||
setTargetDate(undefined);
|
!auth.email.email_enable_verify ||
|
||||||
},
|
auth.email?.email_domain_suffix_list.split('\n').includes(domain || '');
|
||||||
});
|
return !isValid;
|
||||||
const handleSendCode = async () => {
|
} catch (error) {
|
||||||
await sendEmailCode({
|
console.log('Error checking user:', error);
|
||||||
email: initialValues.email,
|
return false;
|
||||||
type: 1,
|
}
|
||||||
});
|
|
||||||
setTargetDate(Date.now() + 60000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
email: z.string(),
|
email: z
|
||||||
|
.string()
|
||||||
|
.email(t('email'))
|
||||||
|
.refine(handleCheckUser, {
|
||||||
|
message: t('whitelist'),
|
||||||
|
}),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
repeat_password: z.string(),
|
repeat_password: z.string(),
|
||||||
code: register.enable_email_verify ? z.string() : z.string().nullish(),
|
code: auth.email.email_enable_verify ? z.string() : z.string().nullish(),
|
||||||
invite: invite.forced_invite ? z.string() : z.string().nullish(),
|
invite: invite.forced_invite ? z.string() : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
verify.enable_register_verify && verify.turnstile_site_key
|
||||||
@@ -66,17 +68,18 @@ export default function UserRegisterForm({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...initialValues,
|
...initialValues,
|
||||||
invite: sessionStorage.getItem('invite'),
|
invite: localStorage.getItem('invite') || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{register.stop_register ? (
|
{auth.register.stop_register ? (
|
||||||
<Markdown>{t('message')}</Markdown>
|
<Markdown>{t('message')}</Markdown>
|
||||||
) : (
|
) : (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -87,7 +90,7 @@ export default function UserRegisterForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input disabled placeholder='Enter your email...' type='email' {...field} />
|
<Input placeholder='Enter your email...' type='email' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -99,12 +102,7 @@ export default function UserRegisterForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='Enter your password...' type='password' {...field} />
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter your password...'
|
|
||||||
type='password'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -116,18 +114,13 @@ export default function UserRegisterForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='Enter password again...' type='password' {...field} />
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter password again...'
|
|
||||||
type='password'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{register.enable_email_verify && (
|
{auth.email.email_enable_verify && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='code'
|
name='code'
|
||||||
@@ -136,15 +129,18 @@ export default function UserRegisterForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter code...'
|
placeholder='Enter code...'
|
||||||
type='text'
|
type='text'
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
/>
|
/>
|
||||||
<Button type='button' onClick={handleSendCode} disabled={seconds > 0}>
|
<SendCode
|
||||||
{seconds > 0 ? `${seconds}s` : t('get')}
|
type='email'
|
||||||
</Button>
|
params={{
|
||||||
|
...form.getValues(),
|
||||||
|
type: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -159,7 +155,7 @@ export default function UserRegisterForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading || !!localStorage.getItem('invite')}
|
||||||
placeholder={t('invite')}
|
placeholder={t('invite')}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value || ''}
|
value={field.value || ''}
|
||||||
@@ -191,13 +187,13 @@ export default function UserRegisterForm({
|
|||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
<div className='mt-4 text-right text-sm'>
|
<div className='mt-4 text-right text-sm'>
|
||||||
{t('existingAccount')}
|
{t('existingAccount')}
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant='link'
|
||||||
className='p-0'
|
className='p-0'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInitialValues(undefined);
|
setInitialValues(undefined);
|
||||||
onSwitchForm(undefined);
|
onSwitchForm('login');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('switchToLogin')}
|
{t('switchToLogin')}
|
||||||
+21
-39
@@ -1,18 +1,17 @@
|
|||||||
import useGlobalStore from '@/config/use-global';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { sendEmailCode } from '@/services/common/common';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react/dist/iconify.js';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
||||||
import { Input } from '@workspace/ui/components/input';
|
import { Input } from '@workspace/ui/components/input';
|
||||||
import { useCountDown } from 'ahooks';
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import CloudFlareTurnstile from './turnstile';
|
import SendCode from '../send-code';
|
||||||
|
import CloudFlareTurnstile from '../turnstile';
|
||||||
|
|
||||||
export default function UserResetForm({
|
export default function ResetForm({
|
||||||
loading,
|
loading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -23,32 +22,17 @@ export default function UserResetForm({
|
|||||||
onSubmit: (data: any) => void;
|
onSubmit: (data: any) => void;
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
setInitialValues: Dispatch<SetStateAction<any>>;
|
setInitialValues: Dispatch<SetStateAction<any>>;
|
||||||
onSwitchForm: (type?: 'register' | 'reset') => void;
|
onSwitchForm: Dispatch<SetStateAction<'register' | 'reset' | 'login'>>;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations('auth.reset');
|
const t = useTranslations('auth.reset');
|
||||||
|
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, register } = common;
|
const { verify, auth } = common;
|
||||||
|
|
||||||
const [targetDate, setTargetDate] = useState<number>();
|
|
||||||
const [, { seconds }] = useCountDown({
|
|
||||||
targetDate,
|
|
||||||
onEnd: () => {
|
|
||||||
setTargetDate(undefined);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const handleSendCode = async () => {
|
|
||||||
await sendEmailCode({
|
|
||||||
email: initialValues.email,
|
|
||||||
type: 2,
|
|
||||||
});
|
|
||||||
setTargetDate(Date.now() + 60000); // 60秒倒计时
|
|
||||||
};
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string().email(t('email')),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
code: register.enable_email_verify ? z.string() : z.string().nullish(),
|
code: auth?.email?.email_enable_verify ? z.string() : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
verify.enable_register_verify && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
@@ -69,7 +53,7 @@ export default function UserResetForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input disabled placeholder='Enter your email...' type='email' {...field} />
|
<Input placeholder='Enter your email...' type='email' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -81,19 +65,14 @@ export default function UserResetForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder='Enter your password...' type='password' {...field} />
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter your password...'
|
|
||||||
type='password'
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{register.enable_email_verify && (
|
{auth?.email?.email_enable_verify && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='code'
|
name='code'
|
||||||
@@ -102,15 +81,18 @@ export default function UserResetForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter code...'
|
placeholder='Enter code...'
|
||||||
type='text'
|
type='text'
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
/>
|
/>
|
||||||
<Button type='button' onClick={handleSendCode} disabled={seconds > 0}>
|
<SendCode
|
||||||
{seconds > 0 ? `${seconds}s` : t('get')}
|
type='email'
|
||||||
</Button>
|
params={{
|
||||||
|
...form.getValues(),
|
||||||
|
type: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -139,13 +121,13 @@ export default function UserResetForm({
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
<div className='mt-4 text-right text-sm'>
|
<div className='mt-4 text-right text-sm'>
|
||||||
{t('existingAccount')}
|
{t('existingAccount')}
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant='link'
|
||||||
className='p-0'
|
className='p-0'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInitialValues(undefined);
|
setInitialValues(undefined);
|
||||||
onSwitchForm(undefined);
|
onSwitchForm('login');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('switchToLogin')}
|
{t('switchToLogin')}
|
||||||
@@ -8,7 +8,7 @@ import LoginLottie from '@workspace/ui/lotties/login.json';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/legacy/image';
|
import Image from 'next/legacy/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import UserAuthForm from './user-auth-form';
|
import EmailAuthForm from './email/auth-form';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const t = useTranslations('auth');
|
const t = useTranslations('auth');
|
||||||
@@ -45,7 +45,7 @@ export default function Page() {
|
|||||||
<div className='lg:bg-background flex flex-col items-center rounded-2xl md:w-[600px] lg:flex-auto lg:p-10 lg:shadow'>
|
<div className='lg:bg-background flex flex-col items-center rounded-2xl md:w-[600px] lg:flex-auto lg:p-10 lg:shadow'>
|
||||||
<div className='flex flex-col items-stretch justify-center md:w-[400px] lg:h-full'>
|
<div className='flex flex-col items-stretch justify-center md:w-[400px] lg:h-full'>
|
||||||
<div className='flex flex-col justify-center pb-14 lg:flex-auto lg:pb-20'>
|
<div className='flex flex-col justify-center pb-14 lg:flex-auto lg:pb-20'>
|
||||||
<UserAuthForm />
|
<EmailAuthForm />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-end'>
|
<div className='flex items-center justify-end'>
|
||||||
{/* <div className='text-primary flex gap-5 text-sm font-semibold'>
|
{/* <div className='text-primary flex gap-5 text-sm font-semibold'>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { sendEmailCode, sendSmsCode } from '@/services/common/common';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { useCountDown } from 'ahooks';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface SendCodeProps {
|
||||||
|
type: 'email' | 'phone';
|
||||||
|
params: {
|
||||||
|
email?: string;
|
||||||
|
type?: 1 | 2;
|
||||||
|
telephone_area_code?: string;
|
||||||
|
telephone?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export default function SendCode({ type, params }: SendCodeProps) {
|
||||||
|
const t = useTranslations('auth');
|
||||||
|
const [targetDate, setTargetDate] = useState<number>();
|
||||||
|
|
||||||
|
const [, { seconds }] = useCountDown({
|
||||||
|
targetDate,
|
||||||
|
onEnd: () => {
|
||||||
|
setTargetDate(undefined);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEmailCode = async () => {
|
||||||
|
if (params.email && params.type) {
|
||||||
|
await sendEmailCode({
|
||||||
|
email: params.email,
|
||||||
|
type: params.type,
|
||||||
|
});
|
||||||
|
setTargetDate(Date.now() + 60000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPhoneCode = async () => {
|
||||||
|
if (params.telephone && params.telephone_area_code) {
|
||||||
|
await sendSmsCode({
|
||||||
|
telephone: params.telephone,
|
||||||
|
telephone_area_code: params.telephone_area_code,
|
||||||
|
});
|
||||||
|
setTargetDate(Date.now() + 60000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (type === 'email') {
|
||||||
|
getEmailCode();
|
||||||
|
} else {
|
||||||
|
getPhoneCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const disabled =
|
||||||
|
seconds > 0 ||
|
||||||
|
(type === 'email' ? !params.email : !params.telephone || !params.telephone_area_code);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button type='button' onClick={handleSendCode} disabled={disabled}>
|
||||||
|
{seconds > 0 ? `${seconds}s` : t('get')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form';
|
|
||||||
import { Input } from '@workspace/ui/components/input';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export default function UserCheckForm({
|
|
||||||
loading,
|
|
||||||
onSubmit,
|
|
||||||
initialValues,
|
|
||||||
}: {
|
|
||||||
loading?: boolean;
|
|
||||||
onSubmit: (data: any) => void;
|
|
||||||
initialValues: any;
|
|
||||||
}) {
|
|
||||||
const t = useTranslations('auth.check');
|
|
||||||
// const { common } = useGlobalStore();
|
|
||||||
// const { register } = common;
|
|
||||||
const formSchema = z.object({
|
|
||||||
email: z.string().email(t('email')),
|
|
||||||
// .refine(
|
|
||||||
// (email) => {
|
|
||||||
// if (!register.enable_email_domain_suffix) return true;
|
|
||||||
// const domain = email.split('@')[1] as string;
|
|
||||||
// return register.email_domain_suffix_list.split('\n').includes(domain);
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// message: t('whitelist'),
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: initialValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
const handleEmailChange = (value: string) => {
|
|
||||||
form.setValue('email', value);
|
|
||||||
|
|
||||||
if (typingTimeoutRef.current) {
|
|
||||||
clearTimeout(typingTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
typingTimeoutRef.current = setTimeout(async () => {
|
|
||||||
const isValid = await form.trigger('email');
|
|
||||||
if (isValid) {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
form.handleSubmit(onSubmit)();
|
|
||||||
} else {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialValues && initialValues.email) {
|
|
||||||
handleEmailChange(initialValues.email);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (typingTimeoutRef.current) {
|
|
||||||
clearTimeout(typingTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='email'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
disabled={loading}
|
|
||||||
placeholder='Enter your email...'
|
|
||||||
type='email'
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => handleEmailChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button type='submit' disabled={!isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Icon icon='mdi:loading' className='mr-2 size-5 animate-spin' />
|
|
||||||
{t('checking')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t('continue')
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('apple');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getOAuthByPlatform', 'apple'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getOAuthByPlatform({
|
||||||
|
platform: 'apple',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||||
|
try {
|
||||||
|
await updateOAuthConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.UpdateOAuthConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('teamId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('teamIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='ABCDE1FGHI'
|
||||||
|
value={data?.config?.team_id}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
team_id: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('keyId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('keyIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='ABC1234567'
|
||||||
|
value={data?.config?.key_id}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
key_id: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='com.your.app.service'
|
||||||
|
value={data?.config?.client_id}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_id: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('clientSecret')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Textarea
|
||||||
|
className='h-20'
|
||||||
|
placeholder={`-----BEGIN PRIVATE KEY-----\nMIGTAgEA...\n-----END PRIVATE KEY-----`}
|
||||||
|
defaultValue={data?.config?.client_secret}
|
||||||
|
onBlur={(e) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_secret: e.target.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('redirectUri')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='https://your-domain.com/v1/auth/oauth/callback/apple'
|
||||||
|
value={data?.redirect}
|
||||||
|
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@workspace/ui/components/card';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('email');
|
||||||
|
const ref = useRef<Partial<API.EmailSmtpConfig>>({});
|
||||||
|
const [email, setEmail] = useState<string>();
|
||||||
|
|
||||||
|
const { data, refetch, isFetching } = useQuery({
|
||||||
|
queryKey: ['getEmailSmtpConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getEmailSmtpConfig();
|
||||||
|
ref.current = data.data as API.EmailSmtpConfig;
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: string, value: unknown) {
|
||||||
|
if (data?.[key] === value) return;
|
||||||
|
try {
|
||||||
|
await updateEmailSmtpConfig({
|
||||||
|
...ref.current,
|
||||||
|
[key]: value,
|
||||||
|
} as API.EmailSmtpConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue='basic'>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='basic'>{t('emailBasicConfig')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='template'>{t('emailTemplate')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='logs'>{t('emailLogs')}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='basic'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
key: 'email_enabled',
|
||||||
|
label: t('enable'),
|
||||||
|
description: t('enableDescription'),
|
||||||
|
component: 'switch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enable_email_verify',
|
||||||
|
label: t('emailVerification'),
|
||||||
|
description: t('emailVerificationDescription'),
|
||||||
|
component: 'switch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enable_email_domain_suffix',
|
||||||
|
label: t('emailSuffixWhitelist'),
|
||||||
|
description: t('emailSuffixWhitelistDescription'),
|
||||||
|
component: 'switch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_domain_suffix_list',
|
||||||
|
label: t('whitelistSuffixes'),
|
||||||
|
description: t('whitelistSuffixesDescription'),
|
||||||
|
component: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_host',
|
||||||
|
label: t('smtpServerAddress'),
|
||||||
|
description: t('smtpServerAddressDescription'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_port',
|
||||||
|
label: t('smtpServerPort'),
|
||||||
|
description: t('smtpServerPortDescription'),
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_ssl',
|
||||||
|
label: t('smtpEncryptionMethod'),
|
||||||
|
description: t('smtpEncryptionMethodDescription'),
|
||||||
|
component: 'switch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_user',
|
||||||
|
label: t('smtpAccount'),
|
||||||
|
description: t('smtpAccountDescription'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_pass',
|
||||||
|
label: t('smtpPassword'),
|
||||||
|
description: t('smtpPasswordDescription'),
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'email_smtp_from',
|
||||||
|
label: t('senderAddress'),
|
||||||
|
description: t('senderAddressDescription'),
|
||||||
|
},
|
||||||
|
].map(({ key, label, description, type = 'text', component = 'input' }) => (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell className={component === 'textarea' ? 'align-top' : undefined}>
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{description}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
{component === 'input' ? (
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
value={data?.[key]}
|
||||||
|
type={type}
|
||||||
|
onValueBlur={(value) => updateConfig(key, value)}
|
||||||
|
/>
|
||||||
|
) : component === 'switch' ? (
|
||||||
|
<Switch
|
||||||
|
checked={data?.[key]}
|
||||||
|
onCheckedChange={(checked) => updateConfig(key, checked)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Textarea
|
||||||
|
className='h-32'
|
||||||
|
placeholder={t('whitelistSuffixesPlaceholder')}
|
||||||
|
defaultValue={data?.[key]}
|
||||||
|
onBlur={(e) => updateConfig(key, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('sendTestEmail')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='flex items-center gap-2 text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='test@example.com'
|
||||||
|
value={email}
|
||||||
|
type='email'
|
||||||
|
onValueChange={(value) => setEmail(value as string)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!email || isFetching}
|
||||||
|
onClick={async () => {
|
||||||
|
if (!email) return;
|
||||||
|
try {
|
||||||
|
await testEmailSmtp({ email });
|
||||||
|
toast.success(t('sendSuccess'));
|
||||||
|
} catch {
|
||||||
|
toast.error(t('sendFailure'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('sendTestEmail')}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value='template'>
|
||||||
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||||
|
{['verify_email_template', 'expiration_email_template', 'maintenance_email_template'].map(
|
||||||
|
(templateKey) => (
|
||||||
|
<Card key={templateKey}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t(`${templateKey}`)}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(`${templateKey}Description`, { after: '{{', before: '}}' })}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<HTMLEditor
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
value={data?.[templateKey as keyof API.EmailSmtpConfig] as string}
|
||||||
|
onBlur={(value) => updateConfig(templateKey, value)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value='logs'>Logs</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('facebook');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getOAuthByPlatform', 'facebook'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getOAuthByPlatform({
|
||||||
|
platform: 'facebook',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||||
|
try {
|
||||||
|
await updateOAuthConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.UpdateOAuthConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='1234567890123456'
|
||||||
|
value={data?.config?.client_id}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_id: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('clientSecret')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='1234567890abcdef1234567890abcdef'
|
||||||
|
value={data?.config?.client_secret}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_secret: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('redirectUri')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='https://your-domain.com/v1/auth/oauth/callback/facebook'
|
||||||
|
value={data?.redirect}
|
||||||
|
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
+2
-47
@@ -5,13 +5,12 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { Label } from '@workspace/ui/components/label';
|
import { Label } from '@workspace/ui/components/label';
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
import { Textarea } from '@workspace/ui/components/textarea';
|
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Page() {
|
||||||
const t = useTranslations('system.register');
|
const t = useTranslations('auth-control');
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
const { data, refetch } = useQuery({
|
||||||
queryKey: ['getRegisterConfig'],
|
queryKey: ['getRegisterConfig'],
|
||||||
@@ -55,50 +54,6 @@ export default function Register() {
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('emailVerification')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.enable_email_verify}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('enable_email_verify', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('emailSuffixWhitelist')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('emailSuffixWhitelistDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.enable_email_domain_suffix}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('enable_email_domain_suffix', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className='align-top'>
|
|
||||||
<Label>{t('whitelistSuffixes')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('whitelistSuffixesDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Textarea
|
|
||||||
className='h-52'
|
|
||||||
placeholder={t('whitelistSuffixesPlaceholder')}
|
|
||||||
defaultValue={data?.email_domain_suffix_list}
|
|
||||||
onBlur={(e) => {
|
|
||||||
updateConfig('email_domain_suffix_list', e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Label>{t('ipRegistrationLimit')}</Label>
|
<Label>{t('ipRegistrationLimit')}</Label>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('github');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getOAuthByPlatform', 'github'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getOAuthByPlatform({
|
||||||
|
platform: 'github',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||||
|
try {
|
||||||
|
await updateOAuthConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.UpdateOAuthConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='e.g., Iv1.1234567890abcdef'
|
||||||
|
value={data?.config?.client_id}
|
||||||
|
onValueBlur={(value) =>
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_id: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('clientSecret')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='e.g., 1234567890abcdef1234567890abcdef12345678'
|
||||||
|
value={data?.config?.client_secret}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_secret: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('redirectUri')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='https://your-domain.com/v1/auth/oauth/callback/github'
|
||||||
|
value={data?.redirect}
|
||||||
|
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('google');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getOAuthByPlatform', 'google'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getOAuthByPlatform({
|
||||||
|
platform: 'google',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||||
|
try {
|
||||||
|
await updateOAuthConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.UpdateOAuthConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='123456789-abc123def456.apps.googleusercontent.com'
|
||||||
|
value={data?.config?.client_id}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_id: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('clientSecret')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
|
value={data?.config?.client_secret}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
client_secret: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('redirectUri')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='https://your-domain.com/v1/auth/oauth/callback/google'
|
||||||
|
value={data?.redirect}
|
||||||
|
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { AuthControl } from '@/config/navs';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
interface AuthControlLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuthControlLayout({ children }: Readonly<AuthControlLayoutProps>) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const t = useTranslations('menu');
|
||||||
|
if (!pathname) return null;
|
||||||
|
return (
|
||||||
|
<Tabs value={pathname}>
|
||||||
|
<TabsList className='h-full flex-wrap'>
|
||||||
|
{AuthControl.map((item) => (
|
||||||
|
<TabsTrigger key={item.url} value={item.url} asChild>
|
||||||
|
<Link href={item.url}>{t(item.title)}</Link>
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value={pathname}>{children}</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return redirect('/dashboard/auth-control/general');
|
||||||
|
}
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import { getSmsList } from '@/services/admin/sms';
|
||||||
|
import {
|
||||||
|
getSmsConfig,
|
||||||
|
getSmsPlatform,
|
||||||
|
testSmsSend,
|
||||||
|
updateSmsConfig,
|
||||||
|
} from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@workspace/ui/components/select';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('phone');
|
||||||
|
const { data, refetch, isFetching } = useQuery({
|
||||||
|
queryKey: ['getSmsConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getSmsConfig();
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: platforms } = useQuery({
|
||||||
|
queryKey: ['getSmsPlatform'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getSmsPlatform();
|
||||||
|
return data.data?.list;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPlatform = platforms?.find((platform) => platform.platform === data?.sms_platform);
|
||||||
|
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
|
||||||
|
|
||||||
|
async function updateConfig(key: string, value: unknown) {
|
||||||
|
if (data?.[key] === value) return;
|
||||||
|
try {
|
||||||
|
await updateSmsConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.SmsConfig);
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [params, setParams] = useState<API.SendSmsRequest>({
|
||||||
|
telephone: '',
|
||||||
|
content: t('testSmsContent'),
|
||||||
|
area_code: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue='settings' className='w-full'>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value='settings'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.sms_enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('sms_enabled', checked)}
|
||||||
|
disabled={isFetching}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('expireTime')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('expireTimeTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={data?.sms_expire_time ?? 300}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_expire_time', value)}
|
||||||
|
suffix='S'
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('placeholders.expireTime')}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('interval')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('intervalTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
value={data?.sms_interval ?? 60}
|
||||||
|
min={0}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_interval', value)}
|
||||||
|
suffix='S'
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('placeholders.interval')}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('limit')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('limitTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
value={data?.sms_limit ?? 20}
|
||||||
|
min={0}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_limit', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('placeholders.limit')}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('platform')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('platformTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='flex items-center gap-1 text-right'>
|
||||||
|
<Select
|
||||||
|
value={data?.sms_platform}
|
||||||
|
onValueChange={(value) => updateConfig('sms_platform', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{platforms?.map((item) => (
|
||||||
|
<SelectItem key={item.platform} value={item.platform}>
|
||||||
|
{item.platform}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{platform_url && (
|
||||||
|
<Button size='sm' asChild>
|
||||||
|
<Link href={platform_url} target='_blank'>
|
||||||
|
{t('applyPlatform')}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>Key</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('platformConfigTip', { key: platformConfig?.sms_key })}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
value={data?.sms_key ?? ''}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_key', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('platformConfigTip', { key: platformConfig?.sms_key })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>Secret</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('platformConfigTip', { key: platformConfig?.sms_secret })}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
value={data?.sms_secret ?? ''}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_secret', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
type='password'
|
||||||
|
placeholder={t('platformConfigTip', { key: platformConfig?.sms_secret })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{platformConfig?.sms_template_code && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('templateCode')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
value={data?.sms_template_code ?? ''}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_template_code', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{platformConfig?.sms_template_param && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('templateParam')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('platformConfigTip', { key: platformConfig?.sms_template_param })}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
value={data?.sms_template_param ?? 'code'}
|
||||||
|
onValueBlur={(value) => updateConfig('sms_template_param', value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('platformConfigTip', {
|
||||||
|
key: platformConfig?.sms_template_param,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('template')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('templateTip', { code: platformConfig?.sms_template })}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Textarea
|
||||||
|
defaultValue={data?.sms_template ?? ''}
|
||||||
|
onBlur={(e) => updateConfig('sms_template', e.target.value)}
|
||||||
|
disabled={isFetching}
|
||||||
|
placeholder={t('placeholders.template', { code: platformConfig?.sms_template })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('testSms')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('testSmsTip')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='flex items-center gap-2 text-right'>
|
||||||
|
<AreaCodeSelect
|
||||||
|
value={params.area_code}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value.phone) {
|
||||||
|
setParams((prev) => ({ ...prev, area_code: value.phone! }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('testSmsPhone')}
|
||||||
|
value={params.telephone}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setParams((prev) => ({ ...prev, telephone: value as string }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!params.telephone || !params.area_code}
|
||||||
|
onClick={async () => {
|
||||||
|
if (isFetching || !params.telephone || !params.area_code) return;
|
||||||
|
try {
|
||||||
|
await testSmsSend(params);
|
||||||
|
toast.success(t('sendSuccess'));
|
||||||
|
} catch {
|
||||||
|
toast.error(t('sendFailed'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('testSms')}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value='logs'>
|
||||||
|
<LogsTable />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogsTable() {
|
||||||
|
const t = useTranslations('phone');
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.Sms, { telephone: string }>
|
||||||
|
action={ref}
|
||||||
|
header={{
|
||||||
|
title: t('SmsList'),
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: 'platform',
|
||||||
|
header: t('platform'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'areaCode',
|
||||||
|
header: t('areaCode'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'telephone',
|
||||||
|
header: t('telephone'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'content',
|
||||||
|
header: t('content'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: t('status'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue('status');
|
||||||
|
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
|
||||||
|
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: t('createdAt'),
|
||||||
|
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: 'telephone',
|
||||||
|
placeholder: t('search'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filter) => {
|
||||||
|
const { data } = await getSmsList({ ...pagination, ...filter });
|
||||||
|
return {
|
||||||
|
list: data.data?.list || [],
|
||||||
|
total: data.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const t = useTranslations('telegram');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getOAuthByPlatform', 'telegram'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getOAuthByPlatform({
|
||||||
|
platform: 'telegram',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) {
|
||||||
|
try {
|
||||||
|
await updateOAuthConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.UpdateOAuthConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t('saveFailed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('enable')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.enabled}
|
||||||
|
onCheckedChange={(checked) => updateConfig('enabled', checked)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientId')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='6123456789'
|
||||||
|
value={data?.config?.bot}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
bot: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('clientSecret')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='6123456789:AAHn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
|
value={data?.config?.bot_token}
|
||||||
|
onValueBlur={(value) => {
|
||||||
|
updateConfig('config', {
|
||||||
|
...data?.config,
|
||||||
|
bot_token: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('redirectUri')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder='https://your-domain.com/v1/auth/oauth/callback/telegram'
|
||||||
|
value={data?.redirect}
|
||||||
|
onValueBlur={(value) => updateConfig('redirect', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||||
import { DatePicker } from '@workspace/ui/custom-components/date-picker';
|
import { DatePicker } from '@workspace/ui/custom-components/date-picker';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
import { unitConversion } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -235,7 +235,7 @@ export default function CouponForm<T extends Record<string, any>>({
|
|||||||
)}
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='server'
|
name='subscribe'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('form.specifiedServer')}</FormLabel>
|
<FormLabel>{t('form.specifiedServer')}</FormLabel>
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ export default function Page() {
|
|||||||
defaultChecked={row.getValue('enable')}
|
defaultChecked={row.getValue('enable')}
|
||||||
onCheckedChange={async (checked) => {
|
onCheckedChange={async (checked) => {
|
||||||
await updateCoupon({
|
await updateCoupon({
|
||||||
...row,
|
...row.original,
|
||||||
enable: checked,
|
enable: checked,
|
||||||
} as any);
|
} as API.UpdateCouponRequest);
|
||||||
ref.current?.refresh();
|
ref.current?.refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { TagInput } from '@workspace/ui/custom-components/tag-input';
|
import { TagInput } from '@workspace/ui/custom-components/tag-input';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const baseProtocolSchema = z.object({
|
|||||||
const shadowsocksSchema = z.object({
|
const shadowsocksSchema = z.object({
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
|
server_key: nullableString,
|
||||||
});
|
});
|
||||||
|
|
||||||
const vmessSchema = baseProtocolSchema;
|
const vmessSchema = baseProtocolSchema;
|
||||||
@@ -51,11 +52,13 @@ const hysteria2Schema = z.object({
|
|||||||
hop_ports: nullableString,
|
hop_ports: nullableString,
|
||||||
hop_interval: z.number().nullish(),
|
hop_interval: z.number().nullish(),
|
||||||
obfs_password: nullableString,
|
obfs_password: nullableString,
|
||||||
|
security: z.string(),
|
||||||
security_config: securityConfigSchema,
|
security_config: securityConfigSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tuicSchema = z.object({
|
const tuicSchema = z.object({
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
|
security: z.string(),
|
||||||
security_config: securityConfigSchema,
|
security_config: securityConfigSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,13 +91,24 @@ const protocolConfigSchema = z.discriminatedUnion('protocol', [
|
|||||||
|
|
||||||
const baseFormSchema = z.object({
|
const baseFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
tags: z.array(z.string()).nullish().default([]),
|
||||||
|
country: z.string().nullish(),
|
||||||
|
city: z.string().nullish(),
|
||||||
server_addr: z.string(),
|
server_addr: z.string(),
|
||||||
speed_limit: z.number().nullish(),
|
speed_limit: z.number().nullish(),
|
||||||
traffic_ratio: z.number().default(1),
|
traffic_ratio: z.number().default(1),
|
||||||
group_id: z.number().nullish(),
|
group_id: z.number().nullish(),
|
||||||
enable_relay: z.boolean().nullish().default(false),
|
relay_mode: z.string().nullish().default('none'),
|
||||||
relay_host: nullableString,
|
relay_node: z
|
||||||
relay_port: portSchema,
|
.array(
|
||||||
|
z.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: portSchema,
|
||||||
|
prefix: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.nullish()
|
||||||
|
.default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const formSchema = z.intersection(baseFormSchema, protocolConfigSchema);
|
export const formSchema = z.intersection(baseFormSchema, protocolConfigSchema);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getNodeConfig,
|
||||||
|
getNodeMultiplier,
|
||||||
|
setNodeMultiplier,
|
||||||
|
updateNodeConfig,
|
||||||
|
} from '@/services/admin/system';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { ChartContainer, ChartTooltip } from '@workspace/ui/components/chart';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { DicesIcon } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { uid } from 'radash';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Cell, Legend, Pie, PieChart } from 'recharts';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'hsl(var(--chart-1))',
|
||||||
|
'hsl(var(--chart-2))',
|
||||||
|
'hsl(var(--chart-3))',
|
||||||
|
'hsl(var(--chart-4))',
|
||||||
|
'hsl(var(--chart-5))',
|
||||||
|
];
|
||||||
|
|
||||||
|
const MINUTES_IN_DAY = 1440; // 24 * 60
|
||||||
|
|
||||||
|
function getTimeRangeData(slots: API.TimePeriod[]) {
|
||||||
|
const timePoints = slots
|
||||||
|
.filter((slot) => slot.start_time && slot.end_time)
|
||||||
|
.flatMap((slot) => {
|
||||||
|
const [startH = 0, startM = 0] = slot.start_time.split(':').map(Number);
|
||||||
|
const [endH = 0, endM = 0] = slot?.end_time?.split(':').map(Number);
|
||||||
|
const start = startH * 60 + startM;
|
||||||
|
let end = endH * 60 + endM;
|
||||||
|
if (end < start) end += MINUTES_IN_DAY;
|
||||||
|
return { start, end, multiplier: slot.multiplier };
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let currentMinute = 0;
|
||||||
|
|
||||||
|
timePoints.forEach((point) => {
|
||||||
|
if (point.start > currentMinute) {
|
||||||
|
result.push({
|
||||||
|
name: `${Math.floor(currentMinute / 60)}:${String(currentMinute % 60).padStart(2, '0')} - ${Math.floor(point.start / 60)}:${String(point.start % 60).padStart(2, '0')}`,
|
||||||
|
value: point.start - currentMinute,
|
||||||
|
multiplier: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
name: `${Math.floor(point.start / 60)}:${String(point.start % 60).padStart(2, '0')} - ${Math.floor((point.end / 60) % 24)}:${String(point.end % 60).padStart(2, '0')}`,
|
||||||
|
value: point.end - point.start,
|
||||||
|
multiplier: point.multiplier,
|
||||||
|
});
|
||||||
|
currentMinute = point.end % MINUTES_IN_DAY;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentMinute < MINUTES_IN_DAY) {
|
||||||
|
result.push({
|
||||||
|
name: `${Math.floor(currentMinute / 60)}:${String(currentMinute % 60).padStart(2, '0')} - 24:00`,
|
||||||
|
value: MINUTES_IN_DAY - currentMinute,
|
||||||
|
multiplier: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeConfig() {
|
||||||
|
const t = useTranslations('server.config');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getNodeConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getNodeConfig();
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: string, value: unknown) {
|
||||||
|
if (data?.[key] === value) return;
|
||||||
|
try {
|
||||||
|
await updateNodeConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.NodeConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [timeSlots, setTimeSlots] = useState<API.TimePeriod[]>([]);
|
||||||
|
|
||||||
|
const { data: NodeMultiplier, refetch: refetchNodeMultiplier } = useQuery({
|
||||||
|
queryKey: ['getNodeMultiplier'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getNodeMultiplier();
|
||||||
|
if (timeSlots.length === 0) {
|
||||||
|
setTimeSlots(data.data?.periods || []);
|
||||||
|
}
|
||||||
|
return data.data?.periods || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartTimeSlots = useMemo(() => {
|
||||||
|
return getTimeRangeData(timeSlots);
|
||||||
|
}, [timeSlots]);
|
||||||
|
|
||||||
|
const chartConfig = useMemo(() => {
|
||||||
|
return chartTimeSlots.reduce(
|
||||||
|
(acc, item, index) => {
|
||||||
|
acc[item.name] = {
|
||||||
|
label: item.name,
|
||||||
|
color: COLORS[index % COLORS.length] || 'hsl(var(--default-chart-color))',
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, { label: string; color: string }>,
|
||||||
|
);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('communicationKey')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('communicationKeyDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
value={data?.node_secret}
|
||||||
|
onValueBlur={(value) => updateConfig('node_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('node_secret', formatted);
|
||||||
|
}}
|
||||||
|
className='cursor-pointer'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('nodePullInterval')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('nodePullIntervalDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
onValueBlur={(value) => updateConfig('node_pull_interval', value)}
|
||||||
|
suffix='S'
|
||||||
|
value={data?.node_pull_interval}
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('nodePushInterval')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('nodePushIntervalDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
value={data?.node_push_interval}
|
||||||
|
onValueBlur={(value) => updateConfig('node_push_interval', value)}
|
||||||
|
placeholder={t('inputPlaceholder')}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('dynamicMultiplier')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('dynamicMultiplierDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='flex justify-end gap-2'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => {
|
||||||
|
setTimeSlots(NodeMultiplier || []);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('reset')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={() => {
|
||||||
|
setNodeMultiplier({
|
||||||
|
periods: timeSlots,
|
||||||
|
}).then(async () => {
|
||||||
|
const result = await refetchNodeMultiplier();
|
||||||
|
if (result.data) setTimeSlots(result.data);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('save')}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className='flex flex-col-reverse gap-8 px-4 pt-6 md:flex-row md:items-start'>
|
||||||
|
<div className='w-full md:w-1/2'>
|
||||||
|
<ChartContainer config={chartConfig} className='mx-auto aspect-[4/3] max-w-[400px]'>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={chartTimeSlots}
|
||||||
|
cx='50%'
|
||||||
|
cy='50%'
|
||||||
|
labelLine={false}
|
||||||
|
outerRadius='80%'
|
||||||
|
fill='#8884d8'
|
||||||
|
dataKey='value'
|
||||||
|
label={({ name, percent, multiplier }) =>
|
||||||
|
`${(multiplier || 0)?.toFixed(2)}x (${(percent * 100).toFixed(0)}%)`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{chartTimeSlots.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<ChartTooltip
|
||||||
|
content={({ payload }) => {
|
||||||
|
if (payload && payload.length) {
|
||||||
|
const data = payload[0]?.payload;
|
||||||
|
return (
|
||||||
|
<div className='bg-background rounded-lg border p-2 shadow-sm'>
|
||||||
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-muted-foreground text-[0.70rem] uppercase'>
|
||||||
|
{t('timeSlot')}
|
||||||
|
</span>
|
||||||
|
<span className='text-muted-foreground font-bold'>
|
||||||
|
{data.name || '其他'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-muted-foreground text-[0.70rem] uppercase'>
|
||||||
|
{t('multiplier')}
|
||||||
|
</span>
|
||||||
|
<span className='font-bold'>{data.multiplier.toFixed(2)}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
<div className='w-full md:w-1/2'>
|
||||||
|
<ArrayInput<API.TimePeriod>
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
name: 'start_time',
|
||||||
|
prefix: t('startTime'),
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
{ name: 'end_time', prefix: t('endTime'), type: 'time' },
|
||||||
|
{ name: 'multiplier', prefix: t('multiplier'), type: 'number', placeholder: '0' },
|
||||||
|
]}
|
||||||
|
value={timeSlots}
|
||||||
|
onChange={setTimeSlots}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { getNodeGroupList } from '@/services/admin/server';
|
import { getNodeGroupList } from '@/services/admin/server';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||||
@@ -33,14 +32,17 @@ import {
|
|||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||||
|
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
|
import TagInput from '@workspace/ui/custom-components/tag-input';
|
||||||
import { cn } from '@workspace/ui/lib/utils';
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
import { unitConversion } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { formSchema, protocols } from './form-schema';
|
import { formSchema, protocols } from './form-schema';
|
||||||
|
|
||||||
interface NodeFormProps<T> {
|
interface NodeFormProps<T> {
|
||||||
onSubmit: (data: T) => Promise<boolean> | boolean;
|
onSubmit: (data: T) => Promise<boolean> | boolean;
|
||||||
initialValues?: T;
|
initialValues?: T;
|
||||||
@@ -55,13 +57,14 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
loading,
|
loading,
|
||||||
trigger,
|
trigger,
|
||||||
title,
|
title,
|
||||||
}: NodeFormProps<T>) {
|
}: Readonly<NodeFormProps<T>>) {
|
||||||
const t = useTranslations('server.node');
|
const t = useTranslations('server.node');
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
tags: [],
|
||||||
traffic_ratio: 1,
|
traffic_ratio: 1,
|
||||||
protocol: 'shadowsocks',
|
protocol: 'shadowsocks',
|
||||||
...initialValues,
|
...initialValues,
|
||||||
@@ -75,6 +78,8 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
const protocol = form.watch('protocol');
|
const protocol = form.watch('protocol');
|
||||||
const transport = form.watch('config.transport');
|
const transport = form.watch('config.transport');
|
||||||
const security = form.watch('config.security');
|
const security = form.watch('config.security');
|
||||||
|
const relayMode = form.watch('relay_mode');
|
||||||
|
const method = form.watch('config.method');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form?.reset(initialValues);
|
form?.reset(initialValues);
|
||||||
@@ -105,7 +110,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
{trigger}
|
{trigger}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
|
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
@@ -136,7 +141,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
name='group_id'
|
name='group_id'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('form.nodeGroupId')}</FormLabel>
|
<FormLabel>{t('form.groupId')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Combobox<number, false>
|
<Combobox<number, false>
|
||||||
placeholder={t('form.selectNodeGroup')}
|
placeholder={t('form.selectNodeGroup')}
|
||||||
@@ -145,6 +150,64 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.name,
|
label: item.name,
|
||||||
}))}
|
}))}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value || 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-5 gap-2'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='tags'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className='col-span-3'>
|
||||||
|
<FormLabel>{t('form.tags')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<TagInput
|
||||||
|
placeholder={t('form.tagsPlaceholder')}
|
||||||
|
value={field.value || []}
|
||||||
|
onChange={(value) => form.setValue(field.name, value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='country'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.country')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='city'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.city')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -187,7 +250,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
}}
|
}}
|
||||||
suffix='MB'
|
suffix='Mbps'
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -214,65 +277,6 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='enable_relay'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.enableRelay')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className='pt-2'>
|
|
||||||
<Switch
|
|
||||||
checked={!!field.value}
|
|
||||||
onCheckedChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='relay_host'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.relayHost')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
{...field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='relay_port'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.relayPort')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
{...field}
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
max={65535}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -358,6 +362,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
{...field}
|
{...field}
|
||||||
type='number'
|
type='number'
|
||||||
|
placeholder='1-65535'
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -369,6 +374,30 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{[
|
||||||
|
'2022-blake3-aes-128-gcm',
|
||||||
|
'2022-blake3-aes-256-gcm',
|
||||||
|
'2022-blake3-chacha20-poly1305',
|
||||||
|
].includes(method) && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='config.server_key'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.serverKey')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -389,6 +418,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
{...field}
|
{...field}
|
||||||
type='number'
|
type='number'
|
||||||
|
placeholder='1-65535'
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
}}
|
}}
|
||||||
@@ -499,7 +529,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name='config.transport'
|
name='config.transport'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className='min-w-32'>
|
<FormItem className='!mt-0 min-w-32'>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@@ -603,7 +633,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name='config.security'
|
name='config.security'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className='min-w-32'>
|
<FormItem className='!mt-0 min-w-32'>
|
||||||
<Select
|
<Select
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -814,6 +844,84 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className='flex flex-row items-center justify-between p-3'>
|
||||||
|
<CardTitle>{t('form.relayMode')}</CardTitle>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='relay_mode'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className='!mt-0 min-w-32'>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t('form.selectRelayMode')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='none'>
|
||||||
|
{t('form.relayModeOptions.none')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value='all'>{t('form.relayModeOptions.all')}</SelectItem>
|
||||||
|
<SelectItem value='random'>
|
||||||
|
{t('form.relayModeOptions.random')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
{relayMode !== 'none' && (
|
||||||
|
<CardContent className='w-full space-y-3 px-3'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='relay_node'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<ArrayInput
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
name: 'host',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: t('form.relayHost'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'port',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
placeholder: t('form.relayPort'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'prefix',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: t('form.relayPrefix'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -830,8 +938,12 @@ export default function NodeForm<T extends { [x: string]: any }>({
|
|||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={form.handleSubmit(handleSubmit, (errors) => {
|
onClick={form.handleSubmit(handleSubmit, (errors) => {
|
||||||
console.log(errors);
|
const keys = Object.keys(errors);
|
||||||
return errors;
|
for (const key of keys) {
|
||||||
|
const formattedKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
toast.error(`${t(`form.${formattedKey}`)} is ${errors[key]?.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
|
||||||
|
|||||||
@@ -42,43 +42,44 @@ export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className='flex items-center space-x-2 rounded-md'>
|
<div className='flex items-center gap-2 text-xs *:flex-1'>
|
||||||
|
<div className='flex items-center space-x-1'>
|
||||||
<Badge variant={badgeVariant}>{badgeText}</Badge>
|
<Badge variant={badgeVariant}>{badgeText}</Badge>
|
||||||
<span className='text-sm font-medium'>
|
<span className='font-medium'>
|
||||||
{t('onlineCount')}: {onlineCount}
|
{t('onlineCount')}: {onlineCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
<div className='flex flex-col space-y-1'>
|
||||||
<TooltipContent className='bg-muted text-foreground w-80'>
|
<div className='flex justify-between'>
|
||||||
<div className='space-y-4'>
|
<span>CPU</span>
|
||||||
<>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<div className='flex justify-between text-xs'>
|
|
||||||
<span className='font-medium'>CPU</span>
|
|
||||||
<span>{formatPercentage(serverStatus?.cpu ?? 0)}</span>
|
<span>{formatPercentage(serverStatus?.cpu ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={(serverStatus?.cpu ?? 0) * 100} className='h-2' />
|
<Progress value={serverStatus?.cpu ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<div className='flex justify-between text-xs'>
|
<div className='flex justify-between'>
|
||||||
<span className='font-medium'>{t('memory')}</span>
|
<span>{t('memory')}</span>
|
||||||
<span>{formatPercentage(serverStatus?.mem ?? 0)}</span>
|
<span>{formatPercentage(serverStatus?.mem ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={(serverStatus?.mem ?? 0) * 100} className='h-2' />
|
<Progress value={serverStatus?.mem ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<div className='flex justify-between text-xs'>
|
<div className='flex justify-between'>
|
||||||
<span className='font-medium'>{t('disk')}</span>
|
<span>{t('disk')}</span>
|
||||||
<span>{formatPercentage(serverStatus?.disk ?? 0)}</span>
|
<span>{formatPercentage(serverStatus?.disk ?? 0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={(serverStatus?.disk ?? 0) * 100} className='h-2' />
|
<Progress value={serverStatus?.disk ?? 0} className='h-2' max={100} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{isOnline && (
|
||||||
<div className='text-xs'>
|
<div>
|
||||||
{t('lastUpdated')}: {formatDate(serverStatus?.updated_at ?? 0)}
|
{t('lastUpdated')}: {formatDate(serverStatus?.updated_at ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
{isOnline && onlineCount > 0 && (
|
{isOnline && onlineCount > 0 && (
|
||||||
|
<TooltipContent className='bg-muted text-foreground w-80'>
|
||||||
|
<div className='space-y-4'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<h4 className='text-sm font-semibold'>{t('onlineUsers')}</h4>
|
<h4 className='text-sm font-semibold'>{t('onlineUsers')}</h4>
|
||||||
<ScrollArea className='h-[400px] rounded-md border p-2'>
|
<ScrollArea className='h-[400px] rounded-md border p-2'>
|
||||||
@@ -89,10 +90,9 @@ export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
|
|||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -116,6 +116,24 @@ export default function NodeTable() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('name'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'server_addr',
|
||||||
|
header: t('serverAddr'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<Badge variant='outline'>
|
||||||
|
{row.original.country} - {row.original.city}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant='outline'>{row.getValue('server_addr')}</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: t('status'),
|
header: t('status'),
|
||||||
@@ -123,19 +141,11 @@ export default function NodeTable() {
|
|||||||
return <NodeStatusCell status={row.original?.status} />;
|
return <NodeStatusCell status={row.original?.status} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'name',
|
|
||||||
header: t('name'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'server_addr',
|
|
||||||
header: t('serverAddr'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'speed_limit',
|
accessorKey: 'speed_limit',
|
||||||
header: t('speedLimit'),
|
header: t('speedLimit'),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Display type='traffic' value={row.getValue('speed_limit')} unlimited />
|
<Display type='trafficSpeed' value={row.getValue('speed_limit')} unlimited />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -145,13 +155,31 @@ export default function NodeTable() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
accessorKey: 'groupId',
|
accessorKey: 'group_id',
|
||||||
header: t('nodeGroup'),
|
header: t('nodeGroup'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const name = groups?.find((group) => group.id === row.getValue('groupId'))?.name;
|
const name = groups?.find((group) => group.id === row.getValue('group_id'))?.name;
|
||||||
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'tags',
|
||||||
|
header: t('tags'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const tags = (row.getValue('tags') as string[]) || [];
|
||||||
|
return tags.length > 0 ? (
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant='outline'>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'--'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
params={[
|
params={[
|
||||||
{
|
{
|
||||||
@@ -200,9 +228,24 @@ export default function NodeTable() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteNode({
|
||||||
|
id: row.id,
|
||||||
|
});
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
<Button
|
<Button
|
||||||
key='copy'
|
key='copy'
|
||||||
variant='secondary'
|
variant='outline'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -223,21 +266,6 @@ export default function NodeTable() {
|
|||||||
>
|
>
|
||||||
{t('copy')}
|
{t('copy')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<ConfirmButton
|
|
||||||
key='delete'
|
|
||||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
|
||||||
title={t('confirmDelete')}
|
|
||||||
description={t('deleteWarning')}
|
|
||||||
onConfirm={async () => {
|
|
||||||
await deleteNode({
|
|
||||||
id: row.id,
|
|
||||||
});
|
|
||||||
toast.success(t('deleteSuccess'));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
confirmText={t('confirm')}
|
|
||||||
/>,
|
|
||||||
],
|
],
|
||||||
batchRender(rows) {
|
batchRender(rows) {
|
||||||
return [
|
return [
|
||||||
@@ -263,20 +291,20 @@ export default function NodeTable() {
|
|||||||
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
||||||
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
||||||
|
|
||||||
const originalSortMap = new Map(items.map((item) => [item.id, item.sort || item.id]));
|
const originalSorts = items.map((item) => item.sort);
|
||||||
|
|
||||||
const [movedItem] = items.splice(sourceIndex, 1);
|
const [movedItem] = items.splice(sourceIndex, 1);
|
||||||
items.splice(targetIndex, 0, movedItem!);
|
items.splice(targetIndex, 0, movedItem!);
|
||||||
|
|
||||||
const updatedItems = items.map((item, index) => {
|
const updatedItems = items.map((item, index) => {
|
||||||
const originalSort = originalSortMap.get(item.id);
|
const originalSort = originalSorts[index];
|
||||||
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
||||||
return { ...item, sort: newSort };
|
return { ...item, sort: newSort };
|
||||||
});
|
});
|
||||||
|
|
||||||
const changedItems = updatedItems.filter(
|
const changedItems = updatedItems.filter((item, index) => {
|
||||||
(item) => originalSortMap.get(item.id) !== item.sort,
|
return item.sort !== items[index]?.sort;
|
||||||
);
|
});
|
||||||
|
|
||||||
if (changedItems.length > 0) {
|
if (changedItems.length > 0) {
|
||||||
nodeSort({
|
nodeSort({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
|
|||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import GroupTable from './group-table';
|
import GroupTable from './group-table';
|
||||||
|
import NodeConfig from './node-config';
|
||||||
import NodeTable from './node-table';
|
import NodeTable from './node-table';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
@@ -12,6 +13,7 @@ export default async function Page() {
|
|||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
|
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
|
||||||
<TabsTrigger value='group'>{t('tabs.nodeGroup')}</TabsTrigger>
|
<TabsTrigger value='group'>{t('tabs.nodeGroup')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='config'>{t('tabs.nodeConfig')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='node'>
|
<TabsContent value='node'>
|
||||||
<NodeTable />
|
<NodeTable />
|
||||||
@@ -19,6 +21,9 @@ export default async function Page() {
|
|||||||
<TabsContent value='group'>
|
<TabsContent value='group'>
|
||||||
<GroupTable />
|
<GroupTable />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value='config'>
|
||||||
|
<NodeConfig />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getApplication,
|
||||||
|
getApplicationConfig,
|
||||||
|
updateApplicationConfig,
|
||||||
|
} from '@/services/admin/system';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@workspace/ui/components/form';
|
||||||
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@workspace/ui/components/sheet';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||||
|
import { DicesIcon } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { uid } from 'radash';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
app_id: z.number().optional(),
|
||||||
|
encryption_key: z.string().optional(),
|
||||||
|
encryption_method: z.string().optional(),
|
||||||
|
startup_picture: z.string().optional(),
|
||||||
|
startup_picture_skip_time: z.number().optional(),
|
||||||
|
domains: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export default function ConfigForm() {
|
||||||
|
const t = useTranslations('subscribe.app');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<FormSchema>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
app_id: 0,
|
||||||
|
encryption_key: '',
|
||||||
|
encryption_method: '',
|
||||||
|
startup_picture: '',
|
||||||
|
startup_picture_skip_time: 0,
|
||||||
|
domains: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getApplicationConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getApplicationConfig();
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: applications } = useQuery({
|
||||||
|
queryKey: ['getApplication'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getApplication();
|
||||||
|
return data.data?.applications || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset(data);
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
async function onSubmit(values: FormSchema) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updateApplicationConfig({
|
||||||
|
...values,
|
||||||
|
domains: values.domains?.filter((domain) => domain),
|
||||||
|
} as API.ApplicationConfig);
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
refetch();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
/* empty */
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant='outline'>
|
||||||
|
<Icon icon='mdi:cog' className='mr-2' />
|
||||||
|
{t('config')}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{t('configApp')}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<ScrollArea className='h-[calc(100dvh-48px-36px-36px)]'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form className='space-y-4 py-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='app_id'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('selectApp')}</FormLabel>
|
||||||
|
<FormDescription>{t('selectAppDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
{...field}
|
||||||
|
options={
|
||||||
|
applications?.map((app) => ({
|
||||||
|
label: app.name,
|
||||||
|
value: app.id,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => form.setValue(field.name, value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='encryption_key'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('communicationKey')}</FormLabel>
|
||||||
|
<FormDescription>{t('communicationKeyDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => form.setValue(field.name, value as string)}
|
||||||
|
suffix={
|
||||||
|
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
|
||||||
|
<DicesIcon
|
||||||
|
onClick={() => {
|
||||||
|
const id = uid(32).toLowerCase();
|
||||||
|
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
||||||
|
form.setValue(field.name, formatted);
|
||||||
|
}}
|
||||||
|
className='cursor-pointer'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='encryption_method'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('encryption')}</FormLabel>
|
||||||
|
<FormDescription>{t('encryptionDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox
|
||||||
|
options={[
|
||||||
|
{ label: 'none', value: 'none' },
|
||||||
|
{ label: 'AES', value: 'aes' },
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => form.setValue(field.name, value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='startup_picture'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('startupPicture')}</FormLabel>
|
||||||
|
<FormDescription>{t('startupPictureDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => form.setValue(field.name, value as string)}
|
||||||
|
suffix={
|
||||||
|
<UploadImage
|
||||||
|
className='bg-muted h-9 rounded-none border-none px-2'
|
||||||
|
onChange={(value) => form.setValue('startup_picture', value as string)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='startup_picture_skip_time'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('startupPictureSkip')}</FormLabel>
|
||||||
|
<FormDescription>{t('startupPictureSkipDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
{...field}
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
suffix='S'
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
form.setValue('startup_picture_skip_time', Number(value))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='domains'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className='px-1'>
|
||||||
|
<FormLabel>{t('backupDomains')}</FormLabel>
|
||||||
|
<FormDescription>{t('backupDomainsDescription')}</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
className='h-28'
|
||||||
|
placeholder='example.com'
|
||||||
|
value={field.value?.join('\n')}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.setValue(field.name, e.target.value.split('\n'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</ScrollArea>
|
||||||
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||||
|
<Button variant='outline' onClick={() => setOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={form.handleSubmit(onSubmit)} disabled={loading}>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
|
{t('confirm')}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getSubscribeType } from '@/services/admin/system';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@workspace/ui/components/form';
|
||||||
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@workspace/ui/components/select';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@workspace/ui/components/sheet';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { assign, shake } from 'radash';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const platforms = ['windows', 'macos', 'linux', 'android', 'ios', 'harmony'];
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
subscribe_type: 'Clash',
|
||||||
|
name: '',
|
||||||
|
icon: '',
|
||||||
|
url: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionSchema = z.object({
|
||||||
|
url: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
is_default: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
icon: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
subscribe_type: z.string(),
|
||||||
|
platform: z.object({
|
||||||
|
windows: z.array(versionSchema).optional(),
|
||||||
|
macos: z.array(versionSchema).optional(),
|
||||||
|
linux: z.array(versionSchema).optional(),
|
||||||
|
android: z.array(versionSchema).optional(),
|
||||||
|
ios: z.array(versionSchema).optional(),
|
||||||
|
harmony: z.array(versionSchema).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FormProps<T> {
|
||||||
|
trigger: React.ReactNode | string;
|
||||||
|
title: string;
|
||||||
|
initialValues?: Partial<T>;
|
||||||
|
onSubmit: (values: T) => Promise<boolean>;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SubscribeAppForm<
|
||||||
|
T extends API.CreateApplicationRequest | API.UpdateApplicationRequest,
|
||||||
|
>({ trigger, title, loading, initialValues, onSubmit }: FormProps<T>) {
|
||||||
|
const t = useTranslations('subscribe.app');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
type FormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const form = useForm<FormSchema>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: assign(
|
||||||
|
defaultValues,
|
||||||
|
shake(initialValues, (value) => value === null),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset(
|
||||||
|
assign(
|
||||||
|
defaultValues,
|
||||||
|
shake(initialValues, (value) => value === null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [form, initialValues]);
|
||||||
|
|
||||||
|
const { data: subscribe_types } = useQuery<string[]>({
|
||||||
|
queryKey: ['getSubscribeType'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getSubscribeType();
|
||||||
|
return data.data?.subscribe_types || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
{typeof trigger === 'string' ? <Button>{trigger}</Button> : trigger}
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>{title}</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<ScrollArea className='h-[calc(100dvh-48px-36px-36px)]'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form className='space-y-4 py-4'>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='icon'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className='col-span-2'>
|
||||||
|
<FormLabel>{t('appIcon')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
required
|
||||||
|
suffix={
|
||||||
|
<UploadImage
|
||||||
|
className='bg-muted h-9 rounded-none border-none px-2'
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='name'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('appName')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
required
|
||||||
|
type='text'
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='subscribe_type'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('subscriptionProtocol')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t('subscriptionProtocol')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{subscribe_types?.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('platform')}</FormLabel>
|
||||||
|
<Tabs defaultValue={platforms[0]}>
|
||||||
|
<TabsList>
|
||||||
|
{platforms.map((platform) => (
|
||||||
|
<TabsTrigger key={platform} value={platform} className='uppercase'>
|
||||||
|
{platform}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{platforms.map((platform) => (
|
||||||
|
<TabsContent key={platform} value={platform}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`platform.${platform as keyof FormSchema['platform']}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<ArrayInput
|
||||||
|
isReverse
|
||||||
|
className='grid grid-cols-3 gap-4'
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: t('version'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: t('description'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'is_default',
|
||||||
|
type: 'boolean',
|
||||||
|
placeholder: t('defaultVersion'),
|
||||||
|
calculateValue: (value) => {
|
||||||
|
const newField = field.value?.map((item) => {
|
||||||
|
if (item.is_default) {
|
||||||
|
item.is_default = false;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
form.setValue(field.name, newField);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: t('downloadLink'),
|
||||||
|
className: 'col-span-3',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</FormItem>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</ScrollArea>
|
||||||
|
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||||
|
<Button variant='outline' onClick={() => setOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={form.handleSubmit(async (values) => {
|
||||||
|
const success = await onSubmit(values as unknown as T);
|
||||||
|
if (success) setOpen(false);
|
||||||
|
})}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
|
{t('confirm')}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
|
import {
|
||||||
|
createApplication,
|
||||||
|
deleteApplication,
|
||||||
|
getApplication,
|
||||||
|
updateApplication,
|
||||||
|
} from '@/services/admin/system';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/legacy/image';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import ConfigForm from './config';
|
||||||
|
import SubscribeAppForm from './form';
|
||||||
|
|
||||||
|
export default function SubscribeApp() {
|
||||||
|
const t = useTranslations('subscribe.app');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.ApplicationResponseInfo, Record<string, unknown>>
|
||||||
|
action={ref}
|
||||||
|
header={{
|
||||||
|
title: t('appList'),
|
||||||
|
toolbar: (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<ConfigForm />
|
||||||
|
<SubscribeAppForm<API.CreateApplicationRequest>
|
||||||
|
trigger={t('create')}
|
||||||
|
title={t('createApp')}
|
||||||
|
loading={loading}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await createApplication(values);
|
||||||
|
toast.success(t('createSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
request={async (_pagination, filters) => {
|
||||||
|
const { data } = await getApplication();
|
||||||
|
return {
|
||||||
|
list: data.data?.applications || [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: 'icon',
|
||||||
|
header: t('appIcon'),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Image
|
||||||
|
src={row.getValue('icon')}
|
||||||
|
alt={row.getValue('name')}
|
||||||
|
className='h-8 w-8 rounded-md'
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('appName'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'subscribe_type',
|
||||||
|
header: t('subscriptionProtocol'),
|
||||||
|
cell: ({ row }) => row.getValue('subscribe_type'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
actions={{
|
||||||
|
render: (row) => [
|
||||||
|
<SubscribeAppForm<API.UpdateApplicationRequest>
|
||||||
|
key='edit'
|
||||||
|
trigger={<Button>{t('edit')}</Button>}
|
||||||
|
title={t('editApp')}
|
||||||
|
loading={loading}
|
||||||
|
initialValues={{
|
||||||
|
...row,
|
||||||
|
}}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updateApplication({
|
||||||
|
...values,
|
||||||
|
id: row.id,
|
||||||
|
});
|
||||||
|
toast.success(t('updateSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
setLoading(false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteApplication({ id: row.id! });
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
batchRender: (rows) => [
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('batchDelete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await Promise.all(rows.map((row) => deleteApplication({ id: row.id! })));
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.reset();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
+1
-1
@@ -14,7 +14,7 @@ import { formatDate } from '@workspace/ui/utils';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import GroupForm from './group-form';
|
import GroupForm from './form';
|
||||||
|
|
||||||
const GroupTable = () => {
|
const GroupTable = () => {
|
||||||
const t = useTranslations('subscribe');
|
const t = useTranslations('subscribe');
|
||||||
@@ -2,7 +2,9 @@ import { getTranslations } from 'next-intl/server';
|
|||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
|
||||||
import GroupTable from './group-table';
|
import SubscribeApp from './app/table';
|
||||||
|
import GroupTable from './group/table';
|
||||||
|
import SubscribeConfig from './subscribe-config';
|
||||||
import SubscribeTable from './subscribe-table';
|
import SubscribeTable from './subscribe-table';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
@@ -13,6 +15,8 @@ export default async function Page() {
|
|||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
|
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
|
||||||
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
|
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='config'>{t('tabs.subscribeConfig')}</TabsTrigger>
|
||||||
|
<TabsTrigger value='app'>{t('tabs.subscribeApp')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='subscribe'>
|
<TabsContent value='subscribe'>
|
||||||
<SubscribeTable />
|
<SubscribeTable />
|
||||||
@@ -20,6 +24,12 @@ export default async function Page() {
|
|||||||
<TabsContent value='group'>
|
<TabsContent value='group'>
|
||||||
<GroupTable />
|
<GroupTable />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value='config'>
|
||||||
|
<SubscribeConfig />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='app'>
|
||||||
|
<SubscribeApp />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { getSubscribeConfig, updateSubscribeConfig } from '@/services/admin/system';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
|
||||||
|
export default function SubscribeConfig() {
|
||||||
|
const t = useTranslations('subscribe.config');
|
||||||
|
|
||||||
|
const { data, refetch } = useQuery({
|
||||||
|
queryKey: ['getSubscribeConfig'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await getSubscribeConfig();
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateConfig(key: string, value: unknown) {
|
||||||
|
if (data?.[key] === value) return;
|
||||||
|
try {
|
||||||
|
await updateSubscribeConfig({
|
||||||
|
...data,
|
||||||
|
[key]: value,
|
||||||
|
} as API.SubscribeConfig);
|
||||||
|
toast.success(t('saveSuccess'));
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('singleSubscriptionMode')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>
|
||||||
|
{t('singleSubscriptionModeDescription')}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.single_model}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig('single_model', checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('wildcardResolution')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('wildcardResolutionDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Switch
|
||||||
|
checked={data?.pan_domain}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
updateConfig('pan_domain', checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Label>{t('subscriptionPath')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('subscriptionPathDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='flex items-center gap-2 text-right'>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('subscriptionPathPlaceholder')}
|
||||||
|
value={data?.subscribe_path}
|
||||||
|
onValueBlur={(value) => updateConfig('subscribe_path', value)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='align-top'>
|
||||||
|
<Label>{t('subscriptionDomain')}</Label>
|
||||||
|
<p className='text-muted-foreground text-xs'>{t('subscriptionDomainDescription')}</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='text-right'>
|
||||||
|
<Textarea
|
||||||
|
className='h-52'
|
||||||
|
placeholder={t('subscriptionDomainPlaceholder')}
|
||||||
|
defaultValue={data?.subscribe_domain}
|
||||||
|
onBlur={(e) => {
|
||||||
|
updateConfig('subscribe_domain', e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import { getNodeGroupList, getNodeList } from '@/services/admin/server';
|
import { getNodeGroupList, getNodeList } from '@/services/admin/server';
|
||||||
import { getSubscribeGroupList } from '@/services/admin/subscribe';
|
import { getSubscribeGroupList } from '@/services/admin/subscribe';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -30,15 +31,20 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||||
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
|
||||||
import { JSONEditor } from '@workspace/ui/custom-components/editor';
|
import { JSONEditor } from '@workspace/ui/custom-components/editor';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { evaluateWithPrecision, unitConversion } from '@workspace/ui/utils';
|
import { evaluateWithPrecision, unitConversion } from '@workspace/ui/utils';
|
||||||
|
import { CreditCard, Server, Settings } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { assign, shake } from 'radash';
|
import { assign, shake } from 'radash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface SubscribeFormProps<T> {
|
interface SubscribeFormProps<T> {
|
||||||
@@ -50,7 +56,7 @@ interface SubscribeFormProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
inventory: -1,
|
inventory: 0,
|
||||||
speed_limit: 0,
|
speed_limit: 0,
|
||||||
device_limit: 0,
|
device_limit: 0,
|
||||||
traffic: 0,
|
traffic: 0,
|
||||||
@@ -59,6 +65,11 @@ const defaultValues = {
|
|||||||
server_group: [],
|
server_group: [],
|
||||||
server: [],
|
server: [],
|
||||||
unit_time: 'Month',
|
unit_time: 'Month',
|
||||||
|
deduction_ratio: 0,
|
||||||
|
purchase_with_discount: false,
|
||||||
|
reset_cycle: 0,
|
||||||
|
renewal_reset: false,
|
||||||
|
deduction_mode: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SubscribeForm<T extends Record<string, any>>({
|
export default function SubscribeForm<T extends Record<string, any>>({
|
||||||
@@ -67,9 +78,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
loading,
|
loading,
|
||||||
trigger,
|
trigger,
|
||||||
title,
|
title,
|
||||||
}: SubscribeFormProps<T>) {
|
}: Readonly<SubscribeFormProps<T>>) {
|
||||||
const t = useTranslations('subscribe');
|
const t = useTranslations('subscribe');
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -86,14 +96,18 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
inventory: z.number().optional(),
|
inventory: z.number().optional().default(-1),
|
||||||
speed_limit: z.number().optional(),
|
speed_limit: z.number().optional().default(0),
|
||||||
device_limit: z.number().optional(),
|
device_limit: z.number().optional().default(0),
|
||||||
traffic: z.number().optional(),
|
traffic: z.number().optional().default(0),
|
||||||
quota: z.number().optional(),
|
quota: z.number().optional().default(0),
|
||||||
group_id: z.number().optional().nullish(),
|
group_id: z.number().optional().nullish(),
|
||||||
server_group: z.array(z.number()).optional(),
|
server_group: z.array(z.number()).optional().default([]),
|
||||||
server: z.array(z.number()).optional(),
|
server: z.array(z.number()).optional().default([]),
|
||||||
|
deduction_ratio: z.number().optional().default(0),
|
||||||
|
purchase_with_discount: z.boolean().optional().default(false),
|
||||||
|
reset_cycle: z.number().optional().default(0),
|
||||||
|
renewal_reset: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@@ -103,6 +117,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
shake(initialValues, (value) => value === null) as Record<string, any>,
|
shake(initialValues, (value) => value === null) as Record<string, any>,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form?.reset(
|
form?.reset(
|
||||||
assign(defaultValues, shake(initialValues, (value) => value === null) as Record<string, any>),
|
assign(defaultValues, shake(initialValues, (value) => value === null) as Record<string, any>),
|
||||||
@@ -111,9 +126,9 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
|
|
||||||
async function handleSubmit(data: { [x: string]: any }) {
|
async function handleSubmit(data: { [x: string]: any }) {
|
||||||
const bool = await onSubmit(data as T);
|
const bool = await onSubmit(data as T);
|
||||||
|
|
||||||
if (bool) setOpen(false);
|
if (bool) setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: group } = useQuery({
|
const { data: group } = useQuery({
|
||||||
queryKey: ['getSubscribeGroupList'],
|
queryKey: ['getSubscribeGroupList'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -142,6 +157,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const unit_time = form.watch('unit_time');
|
const unit_time = form.watch('unit_time');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
@@ -154,17 +170,32 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
{trigger}
|
{trigger}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className='w-screen max-w-full md:max-w-full'>
|
<SheetContent className='w-[800px] max-w-full md:max-w-screen-md'>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
<ScrollArea className='h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form onSubmit={form.handleSubmit(handleSubmit)} className='pt-4'>
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
<Tabs defaultValue='basic' className='w-full'>
|
||||||
className='grid gap-4 px-6 pt-4 lg:grid-cols-3'
|
<TabsList className='mb-6 grid w-full grid-cols-3'>
|
||||||
>
|
<TabsTrigger value='basic' className='flex items-center gap-2'>
|
||||||
<div className='flex flex-col gap-4'>
|
<Settings className='h-4 w-4' />
|
||||||
|
{t('form.basic')}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value='pricing' className='flex items-center gap-2'>
|
||||||
|
<CreditCard className='h-4 w-4' />
|
||||||
|
{t('form.pricing')}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value='servers' className='flex items-center gap-2'>
|
||||||
|
<Server className='h-4 w-4' />
|
||||||
|
{t('form.servers')}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value='basic' className='space-y-4'>
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='name'
|
name='name'
|
||||||
@@ -183,6 +214,146 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='group_id'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.groupId')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Combobox<number, false>
|
||||||
|
placeholder={t('form.selectSubscribeGroup')}
|
||||||
|
{...field}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value || 0);
|
||||||
|
}}
|
||||||
|
options={group?.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-3 gap-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='traffic'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.traffic')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.noLimit')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||||
|
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||||
|
suffix='GB'
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='speed_limit'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.speedLimit')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.noLimit')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) => unitConversion('bitsToMb', value)}
|
||||||
|
formatOutput={(value) => unitConversion('mbToBits', value)}
|
||||||
|
suffix='Mbps'
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='device_limit'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.deviceLimit')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.noLimit')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-3 gap-4'>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='inventory'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.inventory')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.noLimit')}
|
||||||
|
type='number'
|
||||||
|
value={field.value === -1 ? 0 : field.value}
|
||||||
|
min={0}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='quota'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.quota')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder={t('form.noLimit')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='description'
|
name='description'
|
||||||
@@ -227,7 +398,6 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
'uil:shield-exclamation',
|
'uil:shield-exclamation',
|
||||||
'uil:database',
|
'uil:database',
|
||||||
'uil:server',
|
'uil:server',
|
||||||
'Visit https://icon-sets.iconify.design to browse available icons.',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
@@ -255,8 +425,11 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-4'>
|
</TabsContent>
|
||||||
<div className='grid grid-cols-3 gap-4'>
|
|
||||||
|
<TabsContent value='pricing' className='space-y-4'>
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<div className='grid grid-cols-4 gap-4'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='unit_price'
|
name='unit_price'
|
||||||
@@ -279,6 +452,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='unit_time'
|
name='unit_time'
|
||||||
@@ -295,6 +469,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
|
{ label: t('form.NoLimit'), value: 'NoLimit' },
|
||||||
{ label: t('form.Year'), value: 'Year' },
|
{ label: t('form.Year'), value: 'Year' },
|
||||||
{ label: t('form.Month'), value: 'Month' },
|
{ label: t('form.Month'), value: 'Month' },
|
||||||
{ label: t('form.Day'), value: 'Day' },
|
{ label: t('form.Day'), value: 'Day' },
|
||||||
@@ -307,6 +482,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='replacement'
|
name='replacement'
|
||||||
@@ -329,138 +505,34 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='traffic'
|
name='reset_cycle'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t('form.traffic')}</FormLabel>
|
<FormLabel>{t('form.resetCycle')}</FormLabel>
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('form.noLimit')}
|
|
||||||
type='number'
|
|
||||||
{...field}
|
|
||||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
|
||||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
|
||||||
suffix='GB'
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='speed_limit'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.speedLimit')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('form.noLimit')}
|
|
||||||
type='number'
|
|
||||||
{...field}
|
|
||||||
formatInput={(value) => unitConversion('bitsToMb', value)}
|
|
||||||
formatOutput={(value) => unitConversion('mbToBits', value)}
|
|
||||||
suffix='MB'
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='device_limit'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.deviceLimit')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('form.noLimit')}
|
|
||||||
type='number'
|
|
||||||
{...field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='inventory'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.inventory')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={`-1 ${t('form.noLimit')}`}
|
|
||||||
type='number'
|
|
||||||
{...field}
|
|
||||||
min={-1}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='quota'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.quota')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('form.noLimit')}
|
|
||||||
type='number'
|
|
||||||
{...field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name='group_id'
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('form.subscribeGroup')}</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Combobox<number, false>
|
<Combobox<number, false>
|
||||||
placeholder={t('form.selectSubscribeGroup')}
|
placeholder={t('form.selectResetCycle')}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
options={group?.map((item) => ({
|
options={[
|
||||||
label: item.name,
|
{ label: t('form.noReset'), value: 0 },
|
||||||
value: item.id,
|
{ label: t('form.resetOn1st'), value: 1 },
|
||||||
}))}
|
{ label: t('form.monthlyReset'), value: 2 },
|
||||||
|
{ label: t('form.annualReset'), value: 3 },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='col-span-3'>
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='discount'
|
name='discount'
|
||||||
@@ -501,7 +573,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
formatInput: (value) => unitConversion('centsToDollars', value),
|
formatInput: (value) => unitConversion('centsToDollars', value),
|
||||||
formatOutput: (value) => unitConversion('dollarsToCents', value),
|
formatOutput: (value) => unitConversion('dollarsToCents', value),
|
||||||
internal: true,
|
internal: true,
|
||||||
calculateValue: function (data) {
|
calculateValue: (data) => {
|
||||||
const { unit_price } = form.getValues();
|
const { unit_price } = form.getValues();
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
@@ -523,10 +595,78 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='deduction_ratio'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.deductionRatio')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
placeholder='Auto'
|
||||||
|
suffix='%'
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>{t('form.deductionRatioDescription')}</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='renewal_reset'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='space-y-0.5'>
|
||||||
|
<FormLabel>{t('form.renewalReset')}</FormLabel>
|
||||||
|
<FormDescription>{t('form.renewalResetDescription')}</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='purchase_with_discount'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='space-y-0.5'>
|
||||||
|
<FormLabel>{t('form.purchaseWithDiscount')}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t('form.purchaseWithDiscountDescription')}
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-4'>
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={!!field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value='servers' className='space-y-4'>
|
||||||
|
<div className='space-y-6'>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='server_group'
|
name='server_group'
|
||||||
@@ -549,7 +689,9 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
? form.setValue(field.name, [...value, group.id])
|
? form.setValue(field.name, [...value, group.id])
|
||||||
: form.setValue(
|
: form.setValue(
|
||||||
field.name,
|
field.name,
|
||||||
value.filter((value: number) => value !== group.id),
|
value.filter(
|
||||||
|
(value: number) => value !== group.id,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -559,7 +701,9 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ul className='list-disc [&>li]:mt-2'>
|
<ul className='list-disc [&>li]:mt-2'>
|
||||||
{server
|
{server
|
||||||
?.filter((server: API.Server) => server.group_id === group.id)
|
?.filter(
|
||||||
|
(server: API.Server) => server.group_id === group.id,
|
||||||
|
)
|
||||||
?.map((node: API.Server) => {
|
?.map((node: API.Server) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@@ -583,6 +727,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='server'
|
name='server'
|
||||||
@@ -624,6 +769,8 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -637,8 +784,18 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
>
|
>
|
||||||
{t('form.cancel')}
|
{t('form.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
<Button
|
||||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
|
disabled={loading}
|
||||||
|
onClick={form.handleSubmit(handleSubmit, (errors) => {
|
||||||
|
const keys = Object.keys(errors);
|
||||||
|
for (const key of keys) {
|
||||||
|
const formattedKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
toast.error(`${t(`form.${formattedKey}`)} is ${errors[key]?.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||||
{t('form.confirm')}
|
{t('form.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function SubscribeTable() {
|
|||||||
});
|
});
|
||||||
const ref = useRef<ProTableActions>(null);
|
const ref = useRef<ProTableActions>(null);
|
||||||
return (
|
return (
|
||||||
<ProTable<API.Subscribe, { group_id: number; query: string }>
|
<ProTable<API.SubscribeItem, { group_id: number; query: string }>
|
||||||
action={ref}
|
action={ref}
|
||||||
header={{
|
header={{
|
||||||
toolbar: (
|
toolbar: (
|
||||||
@@ -133,7 +133,14 @@ export default function SubscribeTable() {
|
|||||||
{
|
{
|
||||||
accessorKey: 'unit_price',
|
accessorKey: 'unit_price',
|
||||||
header: t('unitPrice'),
|
header: t('unitPrice'),
|
||||||
cell: ({ row }) => <Display type='currency' value={row.getValue('unit_price')} />,
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Display type='currency' value={row.getValue('unit_price')} />/
|
||||||
|
{t(row.original.unit_time ? `form.${row.original.unit_time}` : 'form.Month')}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'replacement',
|
accessorKey: 'replacement',
|
||||||
@@ -176,10 +183,15 @@ export default function SubscribeTable() {
|
|||||||
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sold',
|
||||||
|
header: t('sold'),
|
||||||
|
cell: ({ row }) => <Badge variant='outline'>{row.getValue('sold')}</Badge>,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
actions={{
|
actions={{
|
||||||
render: (row) => [
|
render: (row) => [
|
||||||
<SubscribeForm<API.Subscribe>
|
<SubscribeForm<API.SubscribeItem>
|
||||||
key='edit'
|
key='edit'
|
||||||
trigger={t('edit')}
|
trigger={t('edit')}
|
||||||
title={t('editSubscribe')}
|
title={t('editSubscribe')}
|
||||||
@@ -203,6 +215,21 @@ export default function SubscribeTable() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
<ConfirmButton
|
||||||
|
key='delete'
|
||||||
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
|
title={t('confirmDelete')}
|
||||||
|
description={t('deleteWarning')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteSubscribe({
|
||||||
|
id: row.id!,
|
||||||
|
});
|
||||||
|
toast.success(t('deleteSuccess'));
|
||||||
|
ref.current?.refresh();
|
||||||
|
}}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
confirmText={t('confirm')}
|
||||||
|
/>,
|
||||||
<Button
|
<Button
|
||||||
key='copy'
|
key='copy'
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
@@ -214,7 +241,7 @@ export default function SubscribeTable() {
|
|||||||
...params,
|
...params,
|
||||||
show: false,
|
show: false,
|
||||||
sell: false,
|
sell: false,
|
||||||
});
|
} as API.CreateSubscribeRequest);
|
||||||
toast.success(t('copySuccess'));
|
toast.success(t('copySuccess'));
|
||||||
ref.current?.refresh();
|
ref.current?.refresh();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -227,21 +254,6 @@ export default function SubscribeTable() {
|
|||||||
>
|
>
|
||||||
{t('copy')}
|
{t('copy')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<ConfirmButton
|
|
||||||
key='delete'
|
|
||||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
|
||||||
title={t('confirmDelete')}
|
|
||||||
description={t('deleteWarning')}
|
|
||||||
onConfirm={async () => {
|
|
||||||
await deleteSubscribe({
|
|
||||||
id: row.id,
|
|
||||||
});
|
|
||||||
toast.success(t('deleteSuccess'));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
cancelText={t('cancel')}
|
|
||||||
confirmText={t('confirm')}
|
|
||||||
/>,
|
|
||||||
],
|
],
|
||||||
batchRender: (rows) => [
|
batchRender: (rows) => [
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
@@ -251,7 +263,7 @@ export default function SubscribeTable() {
|
|||||||
description={t('deleteWarning')}
|
description={t('deleteWarning')}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
await batchDeleteSubscribe({
|
await batchDeleteSubscribe({
|
||||||
ids: rows.map((item) => item.id),
|
ids: rows.map((item) => item.id) as number[],
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success(t('deleteSuccess'));
|
toast.success(t('deleteSuccess'));
|
||||||
@@ -266,24 +278,24 @@ export default function SubscribeTable() {
|
|||||||
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
const sourceIndex = items.findIndex((item) => String(item.id) === source);
|
||||||
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
const targetIndex = items.findIndex((item) => String(item.id) === target);
|
||||||
|
|
||||||
const originalSortMap = new Map(items.map((item) => [item.id, item.sort || item.id]));
|
const originalSorts = items.map((item) => item.sort);
|
||||||
|
|
||||||
const [movedItem] = items.splice(sourceIndex, 1);
|
const [movedItem] = items.splice(sourceIndex, 1);
|
||||||
items.splice(targetIndex, 0, movedItem!);
|
items.splice(targetIndex, 0, movedItem!);
|
||||||
|
|
||||||
const updatedItems = items.map((item, index) => {
|
const updatedItems = items.map((item, index) => {
|
||||||
const originalSort = originalSortMap.get(item.id);
|
const originalSort = originalSorts[index];
|
||||||
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
const newSort = originalSort !== undefined ? originalSort : item.sort;
|
||||||
return { ...item, sort: newSort };
|
return { ...item, sort: newSort };
|
||||||
});
|
});
|
||||||
|
|
||||||
const changedItems = updatedItems.filter(
|
const changedItems = updatedItems.filter((item, index) => {
|
||||||
(item) => originalSortMap.get(item.id) !== item.sort,
|
return item.sort !== items[index]?.sort;
|
||||||
);
|
});
|
||||||
|
|
||||||
if (changedItems.length > 0) {
|
if (changedItems.length > 0) {
|
||||||
subscribeSort({
|
subscribeSort({
|
||||||
sort: changedItems.map((item) => ({ id: item.id, sort: item.sort })),
|
sort: changedItems.map((item) => ({ id: item.id, sort: item.sort })) as API.SortItem[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
|
||||||
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
|
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function Email() {
|
|
||||||
const t = useTranslations('system.email');
|
|
||||||
|
|
||||||
const { data, refetch, isFetching } = useQuery({
|
|
||||||
queryKey: ['getEmailSmtpConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getEmailSmtpConfig();
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [email, setEmail] = useState<string>();
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateEmailSmtpConfig({
|
|
||||||
...data,
|
|
||||||
[key]: value,
|
|
||||||
} as API.EmailSmtpConfig);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table>
|
|
||||||
<TableBody>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
key: 'email_smtp_host',
|
|
||||||
label: t('smtpServerAddress'),
|
|
||||||
description: t('smtpServerAddressDescription'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email_smtp_port',
|
|
||||||
label: t('smtpServerPort'),
|
|
||||||
description: t('smtpServerPortDescription'),
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email_smtp_ssl',
|
|
||||||
label: t('smtpEncryptionMethod'),
|
|
||||||
description: t('smtpEncryptionMethodDescription'),
|
|
||||||
component: 'switch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email_smtp_user',
|
|
||||||
label: t('smtpAccount'),
|
|
||||||
description: t('smtpAccountDescription'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email_smtp_pass',
|
|
||||||
label: t('smtpPassword'),
|
|
||||||
description: t('smtpPasswordDescription'),
|
|
||||||
type: 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'email_smtp_from',
|
|
||||||
label: t('senderAddress'),
|
|
||||||
description: t('senderAddressDescription'),
|
|
||||||
},
|
|
||||||
].map(({ key, label, description, type = 'text', component = 'input' }) => (
|
|
||||||
<TableRow key={key}>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{label}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{description}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
{component === 'input' ? (
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.[key]}
|
|
||||||
type={type}
|
|
||||||
onValueBlur={(value) => updateConfig(key, value)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Switch
|
|
||||||
checked={data?.[key]}
|
|
||||||
onCheckedChange={(checked) => updateConfig(key, checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('sendTestEmail')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='flex items-center gap-2 text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={email}
|
|
||||||
onValueChange={(value) => setEmail(value as string)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={!email}
|
|
||||||
onClick={async () => {
|
|
||||||
if (isFetching || !email) return;
|
|
||||||
try {
|
|
||||||
await testEmailSmtp({ email });
|
|
||||||
toast.success(t('sendSuccess'));
|
|
||||||
} catch {
|
|
||||||
toast.error(t('sendFailure'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('sendTestEmail')}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<div className='grid grid-cols-1 gap-4 py-4 md:grid-cols-3'>
|
|
||||||
{['verify_email_template', 'expiration_email_template', 'maintenance_email_template'].map(
|
|
||||||
(templateKey) => (
|
|
||||||
<HTMLEditor
|
|
||||||
key={templateKey}
|
|
||||||
title={t(`${templateKey}`)}
|
|
||||||
description={t(`${templateKey}Description`, { after: '{{', before: '}}' })}
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.[templateKey]}
|
|
||||||
onBlur={(value) => {
|
|
||||||
updateConfig(templateKey, value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { getNodeConfig, updateNodeConfig } from '@/services/admin/system';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export default function Node() {
|
|
||||||
const t = useTranslations('system.node');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getNodeConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getNodeConfig();
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateNodeConfig({
|
|
||||||
...data,
|
|
||||||
[key]: value,
|
|
||||||
} as API.NodeConfig);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('communicationKey')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('communicationKeyDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
value={data?.node_secret}
|
|
||||||
onValueBlur={(value) => updateConfig('node_secret', value)}
|
|
||||||
suffix={
|
|
||||||
<Icon
|
|
||||||
icon='uil:arrow-random'
|
|
||||||
onClick={() => {
|
|
||||||
updateConfig('node_secret', nanoid());
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('nodePullInterval')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('nodePullIntervalDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
onValueBlur={(value) => updateConfig('node_pull_interval', value)}
|
|
||||||
suffix='S'
|
|
||||||
value={data?.node_pull_interval}
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('nodePushInterval')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('nodePushIntervalDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
value={data?.node_push_interval}
|
|
||||||
onValueBlur={(value) => updateConfig('node_push_interval', value)}
|
|
||||||
placeholder={t('inputPlaceholder')}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
|
|||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import Currency from './currency';
|
import Currency from './currency';
|
||||||
import Email from './email';
|
|
||||||
import Invite from './invite';
|
import Invite from './invite';
|
||||||
import Node from './node';
|
|
||||||
import Register from './register';
|
|
||||||
import Site from './site';
|
import Site from './site';
|
||||||
import Subscription from './subscription';
|
|
||||||
import Telegram from './telegram';
|
import Telegram from './telegram';
|
||||||
import Tos from './tos';
|
import Tos from './tos';
|
||||||
import Verify from './verify';
|
import Verify from './verify';
|
||||||
@@ -20,11 +16,7 @@ export default async function Page() {
|
|||||||
<TabsList className='h-full flex-wrap'>
|
<TabsList className='h-full flex-wrap'>
|
||||||
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
|
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
|
||||||
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
|
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
|
||||||
<TabsTrigger value='subscription'>{t('tabs.subscription')}</TabsTrigger>
|
|
||||||
<TabsTrigger value='register'>{t('tabs.register')}</TabsTrigger>
|
|
||||||
<TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger>
|
<TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger>
|
||||||
<TabsTrigger value='email'>{t('tabs.email')}</TabsTrigger>
|
|
||||||
<TabsTrigger value='node'>{t('tabs.node')}</TabsTrigger>
|
|
||||||
<TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger>
|
<TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger>
|
||||||
<TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger>
|
<TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger>
|
||||||
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
|
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
|
||||||
@@ -35,21 +27,9 @@ export default async function Page() {
|
|||||||
<TabsContent value='currency'>
|
<TabsContent value='currency'>
|
||||||
<Currency />
|
<Currency />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='subscription'>
|
|
||||||
<Subscription />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='register'>
|
|
||||||
<Register />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='verify'>
|
<TabsContent value='verify'>
|
||||||
<Verify />
|
<Verify />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value='email'>
|
|
||||||
<Email />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='node'>
|
|
||||||
<Node />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='invite'>
|
<TabsContent value='invite'>
|
||||||
<Invite />
|
<Invite />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Label } from '@workspace/ui/components/label';
|
|||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||||
import { Textarea } from '@workspace/ui/components/textarea';
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -47,6 +48,14 @@ export default function Site() {
|
|||||||
placeholder={t('logoPlaceholder')}
|
placeholder={t('logoPlaceholder')}
|
||||||
value={data?.site_logo}
|
value={data?.site_logo}
|
||||||
onValueBlur={(value) => updateConfig('site_logo', value)}
|
onValueBlur={(value) => updateConfig('site_logo', value)}
|
||||||
|
suffix={
|
||||||
|
<UploadImage
|
||||||
|
className='bg-muted h-9 rounded-none border-none px-2'
|
||||||
|
onChange={(value) => {
|
||||||
|
updateConfig('site_logo', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,340 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createApplication,
|
|
||||||
deleteApplication,
|
|
||||||
getApplication,
|
|
||||||
getSubscribeConfig,
|
|
||||||
getSubscribeType,
|
|
||||||
updateApplication,
|
|
||||||
updateSubscribeConfig,
|
|
||||||
} from '@/services/admin/system';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Label } from '@workspace/ui/components/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@workspace/ui/components/select';
|
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
|
||||||
import { Textarea } from '@workspace/ui/components/textarea';
|
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
|
||||||
|
|
||||||
function compareData(
|
|
||||||
originalData: API.ApplicationResponse,
|
|
||||||
modifiedData: API.ApplicationResponse,
|
|
||||||
): {
|
|
||||||
added: API.Application[];
|
|
||||||
deleted: API.Application[];
|
|
||||||
updated: API.Application[];
|
|
||||||
} {
|
|
||||||
const added: API.Application[] = [];
|
|
||||||
const deleted: API.Application[] = [];
|
|
||||||
const updated: API.Application[] = [];
|
|
||||||
|
|
||||||
const findById = (array: API.Application[], id: number): API.Application | undefined => {
|
|
||||||
return array.find((item) => item.id === id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isUpdated = (original: API.Application, modified: API.Application): boolean => {
|
|
||||||
return (
|
|
||||||
original.name !== modified.name ||
|
|
||||||
original.platform !== modified.platform ||
|
|
||||||
original.subscribe_type !== modified.subscribe_type ||
|
|
||||||
original.url !== modified.url ||
|
|
||||||
original.icon !== modified.icon
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.values(originalData).forEach((platformData) => {
|
|
||||||
platformData.forEach((originalApp) => {
|
|
||||||
const modifiedApp = findById(
|
|
||||||
modifiedData[originalApp.platform as API.CreateApplicationRequest['platform']],
|
|
||||||
originalApp.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!modifiedApp) {
|
|
||||||
deleted.push(originalApp);
|
|
||||||
} else if (isUpdated(originalApp, modifiedApp)) {
|
|
||||||
updated.push(modifiedApp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.values(modifiedData).forEach((platformData) => {
|
|
||||||
platformData.forEach((modifiedApp) => {
|
|
||||||
if (
|
|
||||||
!findById(
|
|
||||||
originalData[modifiedApp.platform as API.CreateApplicationRequest['platform']],
|
|
||||||
modifiedApp.id,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
added.push(modifiedApp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { added, deleted, updated };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Subscription() {
|
|
||||||
const t = useTranslations('system.subscription');
|
|
||||||
|
|
||||||
const { data, refetch } = useQuery({
|
|
||||||
queryKey: ['getSubscribeConfig'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getSubscribeConfig();
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: apps, refetch: appsRefetch } = useQuery({
|
|
||||||
queryKey: ['getApplication'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getApplication();
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updateConfig(key: string, value: unknown) {
|
|
||||||
if (data?.[key] === value) return;
|
|
||||||
try {
|
|
||||||
await updateSubscribeConfig({
|
|
||||||
...data,
|
|
||||||
[key]: value,
|
|
||||||
} as API.SubscribeConfig);
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: subscribe_types } = useQuery<string[]>({
|
|
||||||
queryKey: ['getSubscribeType'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getSubscribeType();
|
|
||||||
|
|
||||||
return data.data?.subscribe_types || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [app, setApp] = useState<API.ApplicationResponse>();
|
|
||||||
const appTypes = Object.keys(apps || {}) as (keyof API.ApplicationResponse)[];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!app) setApp(apps);
|
|
||||||
}, [app, apps]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('singleSubscriptionMode')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>
|
|
||||||
{t('singleSubscriptionModeDescription')}
|
|
||||||
</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.single_model}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('single_model', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('wildcardResolution')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('wildcardResolutionDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Switch
|
|
||||||
checked={data?.pan_domain}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
updateConfig('pan_domain', checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('subscriptionPath')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('subscriptionPathDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='flex items-center gap-2 text-right'>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('subscriptionPathPlaceholder')}
|
|
||||||
value={data?.subscribe_path}
|
|
||||||
onValueBlur={(value) => updateConfig('subscribe_path', value)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className='align-top'>
|
|
||||||
<Label>{t('subscriptionDomain')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('subscriptionDomainDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='text-right'>
|
|
||||||
<Textarea
|
|
||||||
className='h-52'
|
|
||||||
placeholder={t('subscriptionDomainPlaceholder')}
|
|
||||||
defaultValue={data?.subscribe_domain}
|
|
||||||
onBlur={(e) => {
|
|
||||||
updateConfig('subscribe_domain', e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Label>{t('app')}</Label>
|
|
||||||
<p className='text-muted-foreground text-xs'>{t('appDescription')}</p>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='flex justify-end gap-2'>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
setApp(apps);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('reset')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
onClick={() => {
|
|
||||||
const { added, deleted, updated } = compareData(apps!, app!);
|
|
||||||
|
|
||||||
added.forEach(async (item) => {
|
|
||||||
await createApplication(item as API.CreateApplicationRequest);
|
|
||||||
});
|
|
||||||
deleted.forEach(async (item) => {
|
|
||||||
await deleteApplication({
|
|
||||||
id: item.id,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
updated.forEach(async (item) => {
|
|
||||||
await updateApplication(item);
|
|
||||||
});
|
|
||||||
toast.success(t('saveSuccess'));
|
|
||||||
appsRefetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<Tabs defaultValue='windows'>
|
|
||||||
<TabsList className='h-full flex-wrap'>
|
|
||||||
{appTypes.map((type) => {
|
|
||||||
return (
|
|
||||||
<TabsTrigger value={type} key={type} className='uppercase'>
|
|
||||||
{type}
|
|
||||||
</TabsTrigger>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabsList>
|
|
||||||
{appTypes.map((type) => {
|
|
||||||
const list = (app?.[type] || []) as API.Application[];
|
|
||||||
const updatedList = (key: string, value: string, index: number) => {
|
|
||||||
const newList = list.map((item, i) => (i === index ? { ...item, [key]: value } : item));
|
|
||||||
|
|
||||||
setApp({
|
|
||||||
...app,
|
|
||||||
[type]: newList,
|
|
||||||
} as API.ApplicationResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabsContent value={type} key={type} className='mt-4 space-y-4'>
|
|
||||||
{list.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center gap-2 lg:flex-row' key={index}>
|
|
||||||
<Select
|
|
||||||
value={item.subscribe_type}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updatedList('subscribe_type', value, index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={t('subscriptionProtocol')} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{subscribe_types?.map((item) => (
|
|
||||||
<SelectItem key={item} value={item}>
|
|
||||||
{item}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('appName')}
|
|
||||||
value={item.name}
|
|
||||||
onValueChange={(value) => updatedList('name', value as string, index)}
|
|
||||||
/>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('appIcon')}
|
|
||||||
value={item.icon}
|
|
||||||
onValueChange={(value) => updatedList('icon', value as string, index)}
|
|
||||||
/>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder={t('appDownloadURL')}
|
|
||||||
value={item.url}
|
|
||||||
onValueChange={(value) => updatedList('url', value as string, index)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant='destructive'
|
|
||||||
size='sm'
|
|
||||||
onClick={() => {
|
|
||||||
setApp({
|
|
||||||
...app,
|
|
||||||
[type]: list.filter((l, i) => i !== index),
|
|
||||||
} as API.ApplicationResponse);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('delete')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
className='w-full'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
setApp({
|
|
||||||
...app,
|
|
||||||
[type]: [
|
|
||||||
...list,
|
|
||||||
{
|
|
||||||
platform: type,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as API.ApplicationResponse);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('add')}
|
|
||||||
</Button>
|
|
||||||
</TabsContent>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tabs>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
getTicketList,
|
getTicketList,
|
||||||
updateTicketStatus,
|
updateTicketStatus,
|
||||||
} from '@/services/admin/ticket';
|
} from '@/services/admin/ticket';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +21,7 @@ import { Input } from '@workspace/ui/components/input';
|
|||||||
import { Label } from '@workspace/ui/components/label';
|
import { Label } from '@workspace/ui/components/label';
|
||||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { cn } from '@workspace/ui/lib/utils';
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { getSystemLog, restartSystem } from '@/services/admin/tool';
|
import { getSystemLog, restartSystem } from '@/services/admin/tool';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -30,6 +29,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@workspace/ui/components/card';
|
} from '@workspace/ui/components/card';
|
||||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Display } from '@/components/display';
|
||||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
|
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
import { formatDate, unitConversion } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -71,13 +72,29 @@ export default function Page() {
|
|||||||
header: 'ID',
|
header: 'ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'email',
|
accessorKey: 'auth_methods',
|
||||||
header: t('userName'),
|
header: t('userName'),
|
||||||
|
cell: ({ row }) => row.original.auth_methods?.[0]?.auth_identifier || '--',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'balance',
|
accessorKey: 'balance',
|
||||||
header: t('balance'),
|
header: t('balance'),
|
||||||
cell: ({ row }) => unitConversion('centsToDollars', row.getValue('balance')),
|
cell: ({ row }) => <Display type='currency' value={row.getValue('balance')} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'gift_amount',
|
||||||
|
header: t('giftAmount'),
|
||||||
|
cell: ({ row }) => <Display type='currency' value={row.getValue('gift_amount')} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'commission',
|
||||||
|
header: t('commission'),
|
||||||
|
cell: ({ row }) => <Display type='currency' value={row.getValue('commission')} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'refer_code',
|
||||||
|
header: t('inviteCode'),
|
||||||
|
cell: ({ row }) => row.getValue('refer_code') || '--',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'referer_id',
|
accessorKey: 'referer_id',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Display } from '@/components/display';
|
||||||
import { getUserDetail } from '@/services/admin/user';
|
import { getUserDetail } from '@/services/admin/user';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
|
||||||
import { formatDate, unitConversion } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -43,7 +44,21 @@ export function UserDetail({ id }: { id: number }) {
|
|||||||
</li>
|
</li>
|
||||||
<li className='flex items-center justify-between font-semibold'>
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
<span className='text-muted-foreground'>{t('balance')}</span>
|
<span className='text-muted-foreground'>{t('balance')}</span>
|
||||||
<span>{unitConversion('centsToDollars', data?.balance)}</span>
|
<span>
|
||||||
|
<Display type='currency' value={data?.balance} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('giftAmount')}</span>
|
||||||
|
<span>
|
||||||
|
<Display type='currency' value={data?.gift_amount} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>{t('commission')}</span>
|
||||||
|
<span>
|
||||||
|
<Display type='currency' value={data?.commission} />
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className='flex items-center justify-between'>
|
<li className='flex items-center justify-between'>
|
||||||
<span className='text-muted-foreground'>{t('createdAt')}</span>
|
<span className='text-muted-foreground'>{t('createdAt')}</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@workspace/ui/components/sheet';
|
} from '@workspace/ui/components/sheet';
|
||||||
import { Switch } from '@workspace/ui/components/switch';
|
import { Switch } from '@workspace/ui/components/switch';
|
||||||
|
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { unitConversion } from '@workspace/ui/utils';
|
import { unitConversion } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -43,17 +45,23 @@ export default function UserForm<T extends Record<string, any>>({
|
|||||||
loading,
|
loading,
|
||||||
trigger,
|
trigger,
|
||||||
title,
|
title,
|
||||||
}: UserFormProps<T>) {
|
}: Readonly<UserFormProps<T>>) {
|
||||||
const t = useTranslations('user');
|
const t = useTranslations('user');
|
||||||
|
const { common } = useGlobalStore();
|
||||||
|
const { currency } = common;
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.string().email(t('form.invalidEmailFormat')),
|
email: z.string().email(t('form.invalidEmailFormat')),
|
||||||
|
telephone_area_code: z.string().optional(),
|
||||||
|
telephone: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
referer_id: z.number().optional(),
|
referer_id: z.number().optional(),
|
||||||
refer_code: z.string().optional(),
|
refer_code: z.string().optional(),
|
||||||
is_admin: z.boolean().optional(),
|
is_admin: z.boolean().optional(),
|
||||||
balance: z.number().optional(),
|
balance: z.number().optional(),
|
||||||
|
gift_amount: z.number().optional(),
|
||||||
|
commission: z.number().optional(),
|
||||||
});
|
});
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -110,6 +118,48 @@ export default function UserForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='telephone'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.telephone')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
prefix={
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='telephone_area_code'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<AreaCodeSelect
|
||||||
|
className='w-32 rounded-none border-y-0 border-l-0'
|
||||||
|
simple
|
||||||
|
placeholder={t('form.areaCodePlaceholder')}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setValue(field.name, value.phone);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={t('form.telephonePlaceholder')}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='password'
|
name='password'
|
||||||
@@ -176,7 +226,7 @@ export default function UserForm<T extends Record<string, any>>({
|
|||||||
<FormLabel>{t('form.balance')}</FormLabel>
|
<FormLabel>{t('form.balance')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
prefix='¥'
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
placeholder={t('form.balancePlaceholder')}
|
placeholder={t('form.balancePlaceholder')}
|
||||||
type='number'
|
type='number'
|
||||||
{...field}
|
{...field}
|
||||||
@@ -192,6 +242,54 @@ export default function UserForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='gift_amount'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.giftAmount')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
|
placeholder={t('form.giftAmountPlaceholder')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||||
|
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name='commission'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('form.commission')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
prefix={currency?.currency_symbol ?? '$'}
|
||||||
|
placeholder={t('form.commissionPlaceholder')}
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) => unitConversion('centsToDollars', value)}
|
||||||
|
formatOutput={(value) => unitConversion('dollarsToCents', value)}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='is_admin'
|
name='is_admin'
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const Authorization = (await cookies()).get('Authorization')?.value;
|
||||||
|
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
const messages = await getMessages();
|
const messages = await getMessages();
|
||||||
|
|
||||||
@@ -86,14 +88,21 @@ export default async function RootLayout({
|
|||||||
console.log('Error fetching global config:', error);
|
console.log('Error fetching global config:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Authorization) {
|
||||||
try {
|
try {
|
||||||
user = await currentUser({
|
user = await currentUser({
|
||||||
skipErrorHandler: true,
|
skipErrorHandler: true,
|
||||||
Authorization: (await cookies()).get('Authorization')?.value,
|
Authorization,
|
||||||
}).then((res) => res.data.data);
|
}).then((res) => {
|
||||||
|
if (res.data.data?.is_admin) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error fetching current user:', error);
|
console.log('Error fetching current user:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html suppressHydrationWarning lang={locale} dir={getLangDir(locale)}>
|
<html suppressHydrationWarning lang={locale} dir={getLangDir(locale)}>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Card, CardDescription, CardHeader, CardTitle } from '@workspace/ui/comp
|
|||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const BASE_URL = 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets/billing/index.json';
|
|
||||||
|
|
||||||
interface BillingProps {
|
interface BillingProps {
|
||||||
type: 'dashboard' | 'payment';
|
type: 'dashboard' | 'payment';
|
||||||
}
|
}
|
||||||
@@ -17,23 +15,48 @@ interface ItemType {
|
|||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBillingURL() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.github.com/repos/perfect-panel/ppanel-assets/commits',
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
const version = json[0]?.sha;
|
||||||
|
const url = new URL('https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets');
|
||||||
|
url.pathname += `@${version}/billing/index.json`;
|
||||||
|
return url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return 'https://cdn.jsdelivr.net/gh/perfect-panel/ppanel-assets/billing/index.json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function Billing({ type }: BillingProps) {
|
export default async function Billing({ type }: BillingProps) {
|
||||||
const t = await getTranslations('common.billing');
|
const t = await getTranslations('common.billing');
|
||||||
let list: ItemType[] = [];
|
let list: ItemType[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(BASE_URL, { cache: 'no-store' });
|
const url = await getBillingURL();
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const now = new Date().getTime();
|
const now = Date.now();
|
||||||
list = data[type].filter((item: { expiryDate: string }) => {
|
|
||||||
|
list = Array.isArray(data[type])
|
||||||
|
? data[type].filter((item: { expiryDate: string }) => {
|
||||||
const expiryDate = Date.parse(item.expiryDate);
|
const expiryDate = Date.parse(item.expiryDate);
|
||||||
return !isNaN(expiryDate) && expiryDate > now;
|
return !isNaN(expiryDate) && expiryDate > now;
|
||||||
});
|
})
|
||||||
|
: [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error fetching billing data:', error);
|
console.log(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (list && list.length === 0) return null;
|
|
||||||
|
if (!list?.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className='text mt-2 font-bold'>
|
<h1 className='text mt-2 font-bold'>
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ export function RevenueStatisticsCard() {
|
|||||||
y={viewBox.cy}
|
y={viewBox.cy}
|
||||||
className='fill-foreground text-2xl font-bold'
|
className='fill-foreground text-2xl font-bold'
|
||||||
>
|
>
|
||||||
{RevenueStatistics?.today.amount_total}
|
{unitConversion(
|
||||||
|
'centsToDollars',
|
||||||
|
RevenueStatistics?.today.amount_total,
|
||||||
|
)}
|
||||||
</tspan>
|
</tspan>
|
||||||
</text>
|
</text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console';
|
import { queryServerTotalData, queryTicketWaitReply } from '@/services/admin/console';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@workspace/ui/components/chart';
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@workspace/ui/components/chart';
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@workspace/ui/components/select';
|
} from '@workspace/ui/components/select';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { formatBytes } from '@workspace/ui/utils';
|
import { formatBytes } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import useGlobalStore from '@/config/use-global';
|
|||||||
import { formatBytes, unitConversion } from '@workspace/ui/utils';
|
import { formatBytes, unitConversion } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
type DisplayType = 'currency' | 'traffic' | 'number';
|
type DisplayType = 'currency' | 'traffic' | 'number' | 'trafficSpeed';
|
||||||
|
|
||||||
interface DisplayProps<T> {
|
interface DisplayProps<T> {
|
||||||
value?: T;
|
value?: T;
|
||||||
@@ -26,12 +26,16 @@ export function Display<T extends number | undefined | null>({
|
|||||||
return formattedValue;
|
return formattedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['traffic', 'number'].includes(type) && unlimited && !value) {
|
if (['traffic', 'trafficSpeed', 'number'].includes(type) && unlimited && !value) {
|
||||||
return t('unlimited');
|
return t('unlimited');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'traffic') {
|
if (type === 'traffic') {
|
||||||
return value ? formatBytes(value as number) : '0';
|
return value ? formatBytes(value) : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'trafficSpeed') {
|
||||||
|
return value ? formatBytes(value).replace('B', 'b') + 'ps' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { locales } from '@/config/constants';
|
import { locales } from '@/config/constants';
|
||||||
import { setLocale } from '@/utils/common';
|
import { setLocale } from '@/utils/common';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@workspace/ui/components/select';
|
} from '@workspace/ui/components/select';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { getCountry } from '@workspace/ui/utils';
|
import { getCountry } from '@workspace/ui/utils';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function Providers({
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 5 * 1000,
|
staleTime: 5 * 1000,
|
||||||
|
retry: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { navs } from '@/config/navs';
|
import { navs } from '@/config/navs';
|
||||||
import useGlobalStore from '@/config/use-global';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@workspace/ui/components/sidebar';
|
} from '@workspace/ui/components/sidebar';
|
||||||
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/legacy/image';
|
import Image from 'next/legacy/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -61,7 +61,11 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
tooltip={t(item.title)}
|
tooltip={t(item.title)}
|
||||||
isActive={item.url === pathname}
|
isActive={
|
||||||
|
item.url === '/dashboard'
|
||||||
|
? pathname === item.url
|
||||||
|
: pathname.startsWith(item.url)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
{item.icon && <Icon icon={item.icon} />}
|
{item.icon && <Icon icon={item.icon} />}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function UserNav() {
|
|||||||
<Avatar className='size-8'>
|
<Avatar className='size-8'>
|
||||||
<AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} />
|
<AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} />
|
||||||
<AvatarFallback className='rounded-none bg-transparent'>
|
<AvatarFallback className='rounded-none bg-transparent'>
|
||||||
{user?.email?.[0]?.toUpperCase()}
|
{user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -35,7 +35,9 @@ export function UserNav() {
|
|||||||
<DropdownMenuContent forceMount align='end' className='w-56'>
|
<DropdownMenuContent forceMount align='end' className='w-56'>
|
||||||
<DropdownMenuLabel className='font-normal'>
|
<DropdownMenuLabel className='font-normal'>
|
||||||
<div className='flex flex-col space-y-1'>
|
<div className='flex flex-col space-y-1'>
|
||||||
<p className='text-sm font-medium leading-none'>{user?.email}</p>
|
<p className='text-sm font-medium leading-none'>
|
||||||
|
{user?.auth_methods?.[0]?.auth_identifier}
|
||||||
|
</p>
|
||||||
{/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */}
|
{/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import packageJSON from '../package.json';
|
|||||||
export const locales = packageJSON.i18n.outputLocales;
|
export const locales = packageJSON.i18n.outputLocales;
|
||||||
export const defaultLocale = packageJSON.i18n.entry;
|
export const defaultLocale = packageJSON.i18n.entry;
|
||||||
|
|
||||||
export const NEXT_PUBLIC_DEFAULT_LANGUAGE = env('NEXT_PUBLIC_DEFAULT_LANGUAGE') || defaultLocale;
|
export const NEXT_PUBLIC_DEFAULT_LANGUAGE =
|
||||||
|
env('NEXT_PUBLIC_DEFAULT_LANGUAGE') ?? process.env.NEXT_PUBLIC_DEFAULT_LANGUAGE ?? defaultLocale;
|
||||||
|
|
||||||
export const NEXT_PUBLIC_SITE_URL = env('NEXT_PUBLIC_SITE_URL');
|
export const NEXT_PUBLIC_SITE_URL = env('NEXT_PUBLIC_SITE_URL') ?? process.env.NEXT_PUBLIC_SITE_URL;
|
||||||
export const NEXT_PUBLIC_API_URL = env('NEXT_PUBLIC_API_URL');
|
export const NEXT_PUBLIC_API_URL = env('NEXT_PUBLIC_API_URL') ?? process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
export const NEXT_PUBLIC_DEFAULT_USER_EMAIL = env('NEXT_PUBLIC_DEFAULT_USER_EMAIL');
|
export const NEXT_PUBLIC_DEFAULT_USER_EMAIL =
|
||||||
export const NEXT_PUBLIC_DEFAULT_USER_PASSWORD = env('NEXT_PUBLIC_DEFAULT_USER_PASSWORD');
|
env('NEXT_PUBLIC_DEFAULT_USER_EMAIL') ?? process.env.NEXT_PUBLIC_DEFAULT_USER_EMAIL;
|
||||||
|
export const NEXT_PUBLIC_DEFAULT_USER_PASSWORD =
|
||||||
|
env('NEXT_PUBLIC_DEFAULT_USER_PASSWORD') ?? process.env.NEXT_PUBLIC_DEFAULT_USER_PASSWORD;
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
export const AuthControl = [
|
||||||
|
{
|
||||||
|
title: 'General',
|
||||||
|
url: '/dashboard/auth-control/general',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Email',
|
||||||
|
url: '/dashboard/auth-control/email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Phone Number',
|
||||||
|
url: '/dashboard/auth-control/phone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Telegram',
|
||||||
|
url: '/dashboard/auth-control/telegram',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Apple',
|
||||||
|
url: '/dashboard/auth-control/apple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Google',
|
||||||
|
url: '/dashboard/auth-control/google',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Facebook',
|
||||||
|
url: '/dashboard/auth-control/facebook',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'Twitter',
|
||||||
|
// url: '/dashboard/auth-control/twitter',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: 'GitHub',
|
||||||
|
url: '/dashboard/auth-control/github',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const navs = [
|
export const navs = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
@@ -12,6 +51,12 @@ export const navs = [
|
|||||||
url: '/dashboard/system',
|
url: '/dashboard/system',
|
||||||
icon: 'flat-color-icons:services',
|
icon: 'flat-color-icons:services',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Auth Control',
|
||||||
|
url: '/dashboard/auth-control',
|
||||||
|
icon: 'flat-color-icons:lock-portrait',
|
||||||
|
items: AuthControl,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Payment Config',
|
title: 'Payment Config',
|
||||||
url: '/dashboard/payment',
|
url: '/dashboard/payment',
|
||||||
@@ -82,16 +127,20 @@ export const navs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function findNavByUrl(url: string) {
|
export function findNavByUrl(url: string) {
|
||||||
for (const nav of navs) {
|
function findNav(items: any[], url: string, path: any[] = []): any[] {
|
||||||
if (nav.url && nav.url === url) {
|
for (const item of items) {
|
||||||
return [nav];
|
if (item.url === url) {
|
||||||
|
return [...path, item];
|
||||||
}
|
}
|
||||||
if (nav.items) {
|
if (item.items) {
|
||||||
const current = nav.items.find((item) => item.url === url);
|
const result = findNav(item.items, url, [...path, item]);
|
||||||
if (current) {
|
if (result.length) {
|
||||||
return [nav, current];
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return findNav(navs, url);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
|||||||
enable_reset_password_verify: false,
|
enable_reset_password_verify: false,
|
||||||
turnstile_site_key: '',
|
turnstile_site_key: '',
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
sms: {
|
||||||
|
sms_enabled: false,
|
||||||
|
sms_limit: 0,
|
||||||
|
sms_interval: 0,
|
||||||
|
sms_expire_time: 0,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
email_enabled: false,
|
||||||
|
email_enable_verify: false,
|
||||||
|
email_enable_domain_suffix: false,
|
||||||
|
email_domain_suffix_list: '',
|
||||||
|
},
|
||||||
|
register: {
|
||||||
|
stop_register: false,
|
||||||
|
enable_trial: false,
|
||||||
|
enable_ip_register_limit: false,
|
||||||
|
ip_register_limit: 0,
|
||||||
|
ip_register_limit_duration: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
register: {
|
register: {
|
||||||
stop_register: false,
|
stop_register: false,
|
||||||
enable_email_verify: false,
|
enable_email_verify: false,
|
||||||
@@ -30,6 +51,12 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
|||||||
enable_ip_register_limit: false,
|
enable_ip_register_limit: false,
|
||||||
ip_register_limit: 0,
|
ip_register_limit: 0,
|
||||||
ip_register_limit_duration: 0,
|
ip_register_limit_duration: 0,
|
||||||
|
sms: {
|
||||||
|
sms_enabled: false,
|
||||||
|
sms_limit: 0,
|
||||||
|
sms_interval: 0,
|
||||||
|
sms_expire_time: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
invite: {
|
invite: {
|
||||||
forced_invite: false,
|
forced_invite: false,
|
||||||
@@ -47,6 +74,7 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
|||||||
subscribe_domain: '',
|
subscribe_domain: '',
|
||||||
pan_domain: false,
|
pan_domain: false,
|
||||||
},
|
},
|
||||||
|
oauth_methods: [],
|
||||||
},
|
},
|
||||||
user: undefined,
|
user: undefined,
|
||||||
setCommon: (common) =>
|
setCommon: (common) =>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"clientId": "ID služby",
|
||||||
|
"clientIdDescription": "Apple Service ID, který můžete získat z Apple Developer Portálu",
|
||||||
|
"clientSecret": "Soukromý klíč",
|
||||||
|
"clientSecretDescription": "Obsah soukromého klíče (.p8 soubor) používaný pro autentizaci s Apple",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID",
|
||||||
|
"keyId": "ID klíče",
|
||||||
|
"keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu",
|
||||||
|
"redirectUri": "Návratová URL",
|
||||||
|
"redirectUriDescription": "URL, na kterou Apple přesměruje po úspěšném ověření",
|
||||||
|
"saveFailed": "Uložení se nezdařilo",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné",
|
||||||
|
"teamId": "ID týmu",
|
||||||
|
"teamIdDescription": "ID týmu Apple Developer"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ipRegistrationLimit": "Limit registrace IP",
|
||||||
|
"ipRegistrationLimitDescription": "Když je povoleno, IP adresy, které splňují požadavky pravidla, budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo frontendovým proxy serverům",
|
||||||
|
"penaltyTime": "Čas trestu (minuty)",
|
||||||
|
"penaltyTimeDescription": "Uživatelé musí počkat, až vyprší doba penalizace, než se mohou znovu zaregistrovat",
|
||||||
|
"registrationLimitCount": "Počet registrací",
|
||||||
|
"registrationLimitCountDescription": "Povolit sankci po dosažení limitu registrací",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné",
|
||||||
|
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
|
||||||
|
"stopNewUserRegistrationDescription": "Když je povoleno, nikdo se nemůže registrovat",
|
||||||
|
"trialRegistration": "Registrace do zkušební verze",
|
||||||
|
"trialRegistrationDescription": "Povolit registraci na zkušební verzi; nejprve upravte zkušební balíček a dobu trvání"
|
||||||
|
}
|
||||||
@@ -4,20 +4,28 @@
|
|||||||
"continue": "Pokračovat",
|
"continue": "Pokračovat",
|
||||||
"description": "Zadejte svůj e-mail pro pokračování v přihlášení nebo registraci.",
|
"description": "Zadejte svůj e-mail pro pokračování v přihlášení nebo registraci.",
|
||||||
"email": "Zadejte platnou e-mailovou adresu.",
|
"email": "Zadejte platnou e-mailovou adresu.",
|
||||||
"title": "Přihlášení/Registrace",
|
"title": "Přihlášení/Registrace"
|
||||||
"whitelist": "Doména e-mailu není na povoleném seznamu."
|
|
||||||
},
|
},
|
||||||
|
"get": "Získat",
|
||||||
"login": {
|
"login": {
|
||||||
|
"codeLogin": "Přihlášení pomocí ověřovacího kódu",
|
||||||
"description": "Zadejte prosím své přihlašovací údaje pro přihlášení.",
|
"description": "Zadejte prosím své přihlašovací údaje pro přihlášení.",
|
||||||
|
"email": "Zadejte platnou e-mailovou adresu.",
|
||||||
"forgotPassword": "Zapomněli jste heslo?",
|
"forgotPassword": "Zapomněli jste heslo?",
|
||||||
|
"passwordLogin": "Přihlášení pomocí hesla",
|
||||||
|
"registerAccount": "Registrace účtu",
|
||||||
"success": "Přihlášení bylo úspěšné!",
|
"success": "Přihlášení bylo úspěšné!",
|
||||||
"switchAccount": "Registrace/Přepnout účet",
|
|
||||||
"title": "Přihlášení"
|
"title": "Přihlášení"
|
||||||
},
|
},
|
||||||
"logout": "Odhlásit se",
|
"logout": "Odhlásit se",
|
||||||
|
"methods": {
|
||||||
|
"email": "E-mail",
|
||||||
|
"sms": "Telefon"
|
||||||
|
},
|
||||||
"orWithEmail": "nebo pomocí e-mailu",
|
"orWithEmail": "nebo pomocí e-mailu",
|
||||||
"register": {
|
"register": {
|
||||||
"description": "Vytvořte si nový účet, vyplňte své údaje pro registraci.",
|
"description": "Vytvořte si nový účet, vyplňte své údaje pro registraci.",
|
||||||
|
"email": "Zadejte platnou e-mailovou adresu.",
|
||||||
"existingAccount": "Již máte účet?",
|
"existingAccount": "Již máte účet?",
|
||||||
"get": "Získat",
|
"get": "Získat",
|
||||||
"invite": "Pozvánkový kód",
|
"invite": "Pozvánkový kód",
|
||||||
@@ -25,15 +33,19 @@
|
|||||||
"passwordMismatch": "Hesla se neshodují",
|
"passwordMismatch": "Hesla se neshodují",
|
||||||
"success": "Registrace úspěšná, automaticky přihlášeno!",
|
"success": "Registrace úspěšná, automaticky přihlášeno!",
|
||||||
"switchToLogin": "Přihlásit se/Obnovit e-mail",
|
"switchToLogin": "Přihlásit se/Obnovit e-mail",
|
||||||
"title": "Registrace"
|
"title": "Registrace",
|
||||||
|
"whitelist": "Doména e-mailu není na povoleném seznamu."
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"description": "Zadejte svou e-mailovou adresu pro obnovení hesla.",
|
"description": "Zadejte svou e-mailovou adresu pro obnovení hesla.",
|
||||||
|
"email": "Zadejte prosím platnou e-mailovou adresu.",
|
||||||
"existingAccount": "Máte již účet?",
|
"existingAccount": "Máte již účet?",
|
||||||
"get": "Získat",
|
"get": "Získat",
|
||||||
"success": "Obnovení hesla bylo úspěšné, automaticky přepnuto na přihlášení!",
|
"success": "Obnovení hesla bylo úspěšné, automaticky přepnuto na přihlášení!",
|
||||||
"switchToLogin": "Přihlášení/Registrace",
|
"switchToLogin": "Přihlášení/Registrace",
|
||||||
"title": "Obnovení hesla"
|
"title": "Obnovení hesla"
|
||||||
},
|
},
|
||||||
"tos": "Podmínky služby"
|
"tos": "Podmínky služby",
|
||||||
|
"verifyAccount": "Ověření účtu",
|
||||||
|
"verifyAccountDesc": "Prosím, ověřte svůj účet, abyste mohli pokračovat."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"20004": "Tento uživatel byl deaktivován, pokud máte dotazy, kontaktujte zákaznický servis.",
|
"20004": "Tento uživatel byl deaktivován, pokud máte dotazy, kontaktujte zákaznický servis.",
|
||||||
"20005": "Nedostatečný zůstatek, prosím dobijte a zkuste to znovu.",
|
"20005": "Nedostatečný zůstatek, prosím dobijte a zkuste to znovu.",
|
||||||
"20006": "Funkce registrace není momentálně dostupná, zkuste to prosím později.",
|
"20006": "Funkce registrace není momentálně dostupná, zkuste to prosím později.",
|
||||||
|
"20008": "Informace o uživateli jsou nesprávné, zkontrolujte je a zkuste to znovu.",
|
||||||
"30001": "Tento uzel již existuje, nepřidávejte ho znovu.",
|
"30001": "Tento uzel již existuje, nepřidávejte ho znovu.",
|
||||||
"30002": "Příslušný uzel nebyl nalezen, zkontrolujte a zkuste to znovu.",
|
"30002": "Příslušný uzel nebyl nalezen, zkontrolujte a zkuste to znovu.",
|
||||||
"30003": "Skupina již existuje, zkuste použít jiný název.",
|
"30003": "Skupina již existuje, zkuste použít jiný název.",
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
"50002": "Tento kupón již byl použit, nelze ho použít znovu.",
|
"50002": "Tento kupón již byl použit, nelze ho použít znovu.",
|
||||||
"60001": "Předplatné vypršelo, obnovte ho prosím a použijte znovu.",
|
"60001": "Předplatné vypršelo, obnovte ho prosím a použijte znovu.",
|
||||||
"60002": "Toto předplatné nelze momentálně použít, zkuste to prosím později.",
|
"60002": "Toto předplatné nelze momentálně použít, zkuste to prosím později.",
|
||||||
|
"60003": "Bylo zjištěno existující předplatné. Zrušte ho, než budete pokračovat.",
|
||||||
|
"60004": "Nelze momentálně odstranit, protože předplatné má aktivní uživatele.",
|
||||||
"70001": "Ověřovací kód je nesprávný, zadejte ho prosím znovu.",
|
"70001": "Ověřovací kód je nesprávný, zadejte ho prosím znovu.",
|
||||||
"80001": "Úkol nebyl úspěšně přidán do fronty, zkuste to prosím později.",
|
"80001": "Úkol nebyl úspěšně přidán do fronty, zkuste to prosím později.",
|
||||||
"90001": "Vypněte prosím režim DEBUG a zkuste to znovu.",
|
"90001": "Vypněte prosím režim DEBUG a zkuste to znovu.",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"emailBasicConfig": "Základní konfigurace",
|
||||||
|
"emailBasicConfigDescription": "Nakonfigurujte nastavení SMTP serveru a možnosti ověření e-mailu",
|
||||||
|
"emailLogs": "Záznamy e-mailů",
|
||||||
|
"emailLogsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav",
|
||||||
|
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
|
||||||
|
"emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu",
|
||||||
|
"emailTemplate": "Šablona e-mailu",
|
||||||
|
"emailVerification": "Ověření e-mailu",
|
||||||
|
"emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
|
||||||
|
"expiration_email_template": "Šablona oznámení o vypršení platnosti",
|
||||||
|
"expiration_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
|
||||||
|
"failed": "Neúspěch",
|
||||||
|
"inputPlaceholder": "Zadejte hodnotu...",
|
||||||
|
"maintenance_email_template": "Šablona oznámení o údržbě",
|
||||||
|
"maintenance_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
|
||||||
|
"recipient": "Příjemce",
|
||||||
|
"saveFailed": "Nepodařilo se uložit konfiguraci",
|
||||||
|
"saveSuccess": "Konfigurace byla úspěšně uložena.",
|
||||||
|
"sendFailure": "Nepodařilo se odeslat testovací e-mail, zkontrolujte prosím konfiguraci.",
|
||||||
|
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
|
||||||
|
"sendTestEmail": "Odeslat testovací e-mail",
|
||||||
|
"sendTestEmailDescription": "Odeslat testovací e-mail pro ověření konfigurace.",
|
||||||
|
"senderAddress": "Adresa odesílatele",
|
||||||
|
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
|
||||||
|
"sent": "Odesláno",
|
||||||
|
"sentAt": "Odesláno",
|
||||||
|
"smtpAccount": "SMTP účet",
|
||||||
|
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
|
||||||
|
"smtpEncryptionMethod": "Metoda šifrování SMTP",
|
||||||
|
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
|
||||||
|
"smtpPassword": "SMTP heslo",
|
||||||
|
"smtpPasswordDescription": "Heslo pro účet SMTP.",
|
||||||
|
"smtpServerAddress": "Adresa SMTP serveru",
|
||||||
|
"smtpServerAddressDescription": "Zadejte adresu serveru používanou pro odesílání e-mailů.",
|
||||||
|
"smtpServerPort": "Port SMTP serveru",
|
||||||
|
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
|
||||||
|
"status": "Stav",
|
||||||
|
"subject": "Předmět",
|
||||||
|
"verify_email_template": "Šablona ověřovacího e-mailu",
|
||||||
|
"verify_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
|
||||||
|
"whitelistSuffixes": "Povolené přípony",
|
||||||
|
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu během registrace; jeden na řádek",
|
||||||
|
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, každou na samostatný řádek"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "ID klienta",
|
||||||
|
"clientIdDescription": "ID aplikace Facebook z konzole pro vývojáře Facebooku",
|
||||||
|
"clientSecret": "Klientský tajný klíč",
|
||||||
|
"clientSecretDescription": "Facebook App Secret z Facebook Developers Console",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku",
|
||||||
|
"redirectUri": "Autorizovaná přesměrovací URI",
|
||||||
|
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření na Facebooku",
|
||||||
|
"saveFailed": "Uložení se nezdařilo",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "ID klienta GitHub",
|
||||||
|
"clientIdDescription": "ID klienta z nastavení vaší GitHub OAuth aplikace",
|
||||||
|
"clientSecret": "GitHub klientský tajný klíč",
|
||||||
|
"clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace",
|
||||||
|
"enable": "Povolit ověřování GitHub",
|
||||||
|
"enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu",
|
||||||
|
"redirectUri": "URL pro zpětné volání autorizace",
|
||||||
|
"redirectUriDescription": "URL ve vaší aplikaci, kam budou uživatelé přesměrováni po autentizaci na GitHubu",
|
||||||
|
"saveFailed": "Nepodařilo se uložit nastavení GitHubu",
|
||||||
|
"saveSuccess": "Nastavení GitHubu bylo úspěšně uloženo"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "ID klienta",
|
||||||
|
"clientIdDescription": "ID klienta Google OAuth 2.0 z Google Cloud Console",
|
||||||
|
"clientSecret": "Klientský tajný klíč",
|
||||||
|
"clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google",
|
||||||
|
"redirectUri": "Autorizovaná přesměrovací URI",
|
||||||
|
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření Googlem",
|
||||||
|
"saveFailed": "Uložení se nezdařilo",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné"
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
{
|
{
|
||||||
"Announcement Management": "Správa oznámení",
|
"Announcement Management": "Správa oznámení",
|
||||||
|
"Apple": "Apple ID",
|
||||||
|
"Auth Control": "Řízení ověřování",
|
||||||
"Coupon Management": "Správa kupónů",
|
"Coupon Management": "Správa kupónů",
|
||||||
"Dashboard": "Přístrojová deska",
|
"Dashboard": "Přístrojová deska",
|
||||||
"Document Management": "Správa dokumentů",
|
"Document Management": "Správa dokumentů",
|
||||||
|
"Email": "E-mail",
|
||||||
|
"Facebook": "Facebook",
|
||||||
"Finance": "Finance",
|
"Finance": "Finance",
|
||||||
|
"General": "Obecné",
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Google": "Google",
|
||||||
"Order Management": "Správa objednávek",
|
"Order Management": "Správa objednávek",
|
||||||
"Payment Config": "Konfigurace platby",
|
"Payment Config": "Konfigurace platby",
|
||||||
|
"Phone Number": "Telefonní číslo",
|
||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Server Management": "Správa serveru",
|
"Server Management": "Správa serveru",
|
||||||
"Settings": "Nastavení",
|
"Settings": "Nastavení",
|
||||||
"Subscribe Management": "Správa předplatného",
|
"Subscribe Management": "Správa předplatného",
|
||||||
"System Config": "Systémová konfigurace",
|
"System Config": "Systémová konfigurace",
|
||||||
"System Tool": "Systémový nástroj",
|
"System Tool": "Systémový nástroj",
|
||||||
|
"Telegram": "Telegram",
|
||||||
"Ticket Management": "Správa lístků",
|
"Ticket Management": "Správa lístků",
|
||||||
|
"Twitter": "Twitter",
|
||||||
"User": "Uživatel",
|
"User": "Uživatel",
|
||||||
"User Management": "Správa uživatelů"
|
"User Management": "Správa uživatelů"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"SmsList": "Seznam SMS záznamů",
|
||||||
|
"applyPlatform": "Použít platformu",
|
||||||
|
"areaCode": "Směrový kód",
|
||||||
|
"content": "Obsah",
|
||||||
|
"createdAt": "Čas odeslání",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
|
||||||
|
"expireTime": "Čas vypršení",
|
||||||
|
"expireTimeTip": "Doba platnosti ověřovacího kódu SMS (v sekundách)",
|
||||||
|
"interval": "Interval",
|
||||||
|
"intervalTip": "Interval odesílání SMS ověřovacího kódu pro stejné telefonní číslo (v sekundách), 0 znamená bez omezení",
|
||||||
|
"limit": "Denní limit",
|
||||||
|
"limitTip": "Denní limit pro odesílání ověřovacích SMS kódů na stejné telefonní číslo, 0 znamená bez omezení",
|
||||||
|
"logs": "Protokoly",
|
||||||
|
"placeholders": {
|
||||||
|
"expireTime": "Zadejte dobu vypršení, výchozí je 300",
|
||||||
|
"interval": "Zadejte interval, výchozí je 60",
|
||||||
|
"limit": "Zadejte denní limit, výchozí je 20",
|
||||||
|
"template": "Váš ověřovací kód je {code}, platný po dobu 5 minut",
|
||||||
|
"templateCode": "Zadejte kód šablony"
|
||||||
|
},
|
||||||
|
"platform": "SMS platforma",
|
||||||
|
"platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}",
|
||||||
|
"platformTip": "Vyberte prosím platformu pro SMS",
|
||||||
|
"search": "Vyhledat telefonní číslo",
|
||||||
|
"sendFailed": "Odeslání se nezdařilo",
|
||||||
|
"sendSuccess": "Úspěšně odesláno",
|
||||||
|
"settings": "Nastavení",
|
||||||
|
"status": "Stav",
|
||||||
|
"telephone": "Telefonní číslo",
|
||||||
|
"template": "Šablona SMS",
|
||||||
|
"templateCode": "Kód šablony",
|
||||||
|
"templateParam": "Parametr šablony",
|
||||||
|
"templateTip": "Vyplňte prosím šablonu SMS, ponechte {code} uprostřed, jinak funkce SMS nebude fungovat",
|
||||||
|
"testSms": "Odeslat testovací SMS",
|
||||||
|
"testSmsContent": "Toto je testovací zpráva",
|
||||||
|
"testSmsPhone": "Zadejte telefonní číslo",
|
||||||
|
"testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace",
|
||||||
|
"updateSuccess": "Aktualizace úspěšná"
|
||||||
|
}
|
||||||
@@ -1,4 +1,24 @@
|
|||||||
{
|
{
|
||||||
|
"config": {
|
||||||
|
"addTimeSlot": "Přidat časový úsek",
|
||||||
|
"communicationKey": "Komunikační klíč",
|
||||||
|
"communicationKeyDescription": "Klíč pro komunikaci uzlů k zajištění bezpečnosti dat",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"dynamicMultiplier": "Dynamický multiplikátor",
|
||||||
|
"dynamicMultiplierDescription": "Dynamický multiplikátor označuje zpracování posílaných dat s různými multiplikátory pro různé uzly během různých časových období.",
|
||||||
|
"endTime": "Konec",
|
||||||
|
"inputPlaceholder": "Zadejte",
|
||||||
|
"multiplier": "Multiplikátor",
|
||||||
|
"nodePullInterval": "Interval stahování uzlu",
|
||||||
|
"nodePullIntervalDescription": "Frekvence (v sekundách) pro stahování dat uzly z panelu",
|
||||||
|
"nodePushInterval": "Interval odesílání uzlu",
|
||||||
|
"nodePushIntervalDescription": "Frekvence odesílání dat uzly do panelu",
|
||||||
|
"reset": "Resetovat",
|
||||||
|
"save": "Uložit",
|
||||||
|
"saveSuccess": "Úspěšně uloženo",
|
||||||
|
"startTime": "Začátek",
|
||||||
|
"timeSlot": "Časový úsek"
|
||||||
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"actions": "Akce",
|
"actions": "Akce",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
@@ -46,18 +66,19 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"allowInsecure": "Povolit nezabezpečené",
|
"allowInsecure": "Povolit nezabezpečené",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
|
"city": "Město",
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
|
"country": "Země",
|
||||||
"edit": "Upravit",
|
"edit": "Upravit",
|
||||||
"editSecurity": "Upravit nastavení zabezpečení",
|
"editSecurity": "Upravit nastavení zabezpečení",
|
||||||
"enableRelay": "Povolit přenos",
|
|
||||||
"enableTLS": "Povolit TLS",
|
"enableTLS": "Povolit TLS",
|
||||||
"encryptionMethod": "Metoda šifrování",
|
"encryptionMethod": "Metoda šifrování",
|
||||||
"flow": "Algoritmus řízení toku",
|
"flow": "Algoritmus řízení toku",
|
||||||
|
"groupId": "Skupina uzlů",
|
||||||
"hopInterval": "Interval skoků",
|
"hopInterval": "Interval skoků",
|
||||||
"hopPorts": "Porty skoků",
|
"hopPorts": "Porty skoků",
|
||||||
"hopPortsPlaceholder": "Více portů oddělených čárkou",
|
"hopPortsPlaceholder": "Více portů oddělených čárkou",
|
||||||
"name": "Název",
|
"name": "Název",
|
||||||
"nodeGroupId": "ID skupiny uzlů",
|
|
||||||
"obfsPassword": "Heslo pro zmatení",
|
"obfsPassword": "Heslo pro zmatení",
|
||||||
"obfsPasswordPlaceholder": "Nechte prázdné, pokud nechcete zmást",
|
"obfsPasswordPlaceholder": "Nechte prázdné, pokud nechcete zmást",
|
||||||
"path": "Cesta",
|
"path": "Cesta",
|
||||||
@@ -65,7 +86,14 @@
|
|||||||
"port": "Port služby",
|
"port": "Port služby",
|
||||||
"protocol": "Protokol",
|
"protocol": "Protokol",
|
||||||
"relayHost": "Adresa přenosu",
|
"relayHost": "Adresa přenosu",
|
||||||
|
"relayMode": "Režim relé",
|
||||||
|
"relayModeOptions": {
|
||||||
|
"all": "Vše",
|
||||||
|
"none": "Žádný",
|
||||||
|
"random": "Náhodný"
|
||||||
|
},
|
||||||
"relayPort": "Port přenosu",
|
"relayPort": "Port přenosu",
|
||||||
|
"relayPrefix": "Předpona relé",
|
||||||
"remarks": "Poznámky",
|
"remarks": "Poznámky",
|
||||||
"security": "Zabezpečení",
|
"security": "Zabezpečení",
|
||||||
"securityConfig": "Nastavení zabezpečení",
|
"securityConfig": "Nastavení zabezpečení",
|
||||||
@@ -88,10 +116,14 @@
|
|||||||
"selectEncryptionMethod": "Vyberte metodu šifrování",
|
"selectEncryptionMethod": "Vyberte metodu šifrování",
|
||||||
"selectNodeGroup": "Vyberte skupinu uzlů",
|
"selectNodeGroup": "Vyberte skupinu uzlů",
|
||||||
"selectProtocol": "Vyberte protokol",
|
"selectProtocol": "Vyberte protokol",
|
||||||
|
"selectRelayMode": "Vyberte režim relé",
|
||||||
"serverAddr": "Adresa serveru",
|
"serverAddr": "Adresa serveru",
|
||||||
|
"serverKey": "Klíč serveru",
|
||||||
"serverName": "Název služby",
|
"serverName": "Název služby",
|
||||||
"speedLimit": "Omezení rychlosti",
|
"speedLimit": "Omezení rychlosti",
|
||||||
"speedLimitPlaceholder": "Bez omezení",
|
"speedLimitPlaceholder": "Bez omezení",
|
||||||
|
"tags": "Štítky",
|
||||||
|
"tagsPlaceholder": "Použijte Enter nebo čárku (,) pro zadání více štítků",
|
||||||
"trafficRatio": "Poměr provozu",
|
"trafficRatio": "Poměr provozu",
|
||||||
"transport": "Přenosový protokol",
|
"transport": "Přenosový protokol",
|
||||||
"transportConfig": "Nastavení přenosového protokolu",
|
"transportConfig": "Nastavení přenosového protokolu",
|
||||||
@@ -112,6 +144,7 @@
|
|||||||
"serverAddr": "Adresa serveru",
|
"serverAddr": "Adresa serveru",
|
||||||
"speedLimit": "Omezení rychlosti",
|
"speedLimit": "Omezení rychlosti",
|
||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
|
"tags": "Štítky",
|
||||||
"trafficRatio": "Poměr provozu",
|
"trafficRatio": "Poměr provozu",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"updateSuccess": "Úspěšně aktualizováno",
|
"updateSuccess": "Úspěšně aktualizováno",
|
||||||
@@ -119,6 +152,7 @@
|
|||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"node": "Uzlu",
|
"node": "Uzlu",
|
||||||
|
"nodeConfig": "Konfigurace uzlu",
|
||||||
"nodeGroup": "Skupina uzlů"
|
"nodeGroup": "Skupina uzlů"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,59 @@
|
|||||||
{
|
{
|
||||||
"actions": "akce",
|
"actions": "akce",
|
||||||
|
"app": {
|
||||||
|
"appDownloadURL": "URL pro stažení aplikace",
|
||||||
|
"appIcon": "Ikona aplikace",
|
||||||
|
"appList": "Seznam aplikací",
|
||||||
|
"appName": "Název aplikace",
|
||||||
|
"backupDomains": "Seznam záložních domén",
|
||||||
|
"backupDomainsDescription": "Seznam záložních domén pro řešení domén, jedna doména na řádek",
|
||||||
|
"batchDelete": "Hromadné smazání",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
|
"communicationKey": "Komunikační klíč",
|
||||||
|
"communicationKeyDescription": "Klíč používaný pro komunikaci klienta",
|
||||||
|
"config": "Konfigurace",
|
||||||
|
"configApp": "Konfigurace aplikace",
|
||||||
|
"confirm": "Potvrdit",
|
||||||
|
"confirmDelete": "Potvrdit smazání",
|
||||||
|
"create": "Vytvořit",
|
||||||
|
"createApp": "Vytvořit aplikaci",
|
||||||
|
"createSuccess": "Úspěšně vytvořeno",
|
||||||
|
"defaultVersion": "Výchozí",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"deleteWarning": "Tuto akci nelze vrátit zpět",
|
||||||
|
"describeDescription": "Používá se k popisu aplikace, zobrazuje se v seznamu aplikací",
|
||||||
|
"description": "Popis",
|
||||||
|
"downloadLink": "Odkaz ke stažení",
|
||||||
|
"edit": "Upravit",
|
||||||
|
"editApp": "Upravit aplikaci",
|
||||||
|
"encryption": "Metoda šifrování",
|
||||||
|
"encryptionDescription": "Vyberte metodu šifrování pro komunikaci klienta. Pokud je vybrána, klient použije tuto metodu ke komunikaci se serverem",
|
||||||
|
"nameDescription": "Název aplikace, zobrazuje se v seznamu aplikací",
|
||||||
|
"platform": "Platforma",
|
||||||
|
"selectApp": "Vybrat aplikaci",
|
||||||
|
"selectAppDescription": "Vyberte aplikaci k nastavení, všechna nastavení se použijí na vybranou aplikaci",
|
||||||
|
"startupPicture": "Úvodní obrázek",
|
||||||
|
"startupPictureDescription": "Úvodní obrázek, podporuje síťové a lokální obrázky. Pro síťové obrázky zadejte úplnou URL adresu obrázku",
|
||||||
|
"startupPicturePreview": "Náhled úvodního obrázku",
|
||||||
|
"startupPictureSkip": "Čas přeskočení úvodního obrázku",
|
||||||
|
"startupPictureSkipDescription": "Doba zobrazení úvodního obrázku v sekundách, zadejte 0 pro nezobrazení",
|
||||||
|
"subscriptionProtocol": "Protokol předplatného",
|
||||||
|
"updateSuccess": "Úspěšně aktualizováno",
|
||||||
|
"version": "Verze"
|
||||||
|
},
|
||||||
|
"cancel": "Zrušit",
|
||||||
|
"config": {
|
||||||
|
"singleSubscriptionMode": "Režim jednorázového předplatného",
|
||||||
|
"singleSubscriptionModeDescription": "Když je povoleno, všechna uživatelská předplatná budou převedena na zůstatek",
|
||||||
|
"subscriptionDomain": "Doména předplatného",
|
||||||
|
"subscriptionDomainDescription": "Používá se pro předplatné; nechte prázdné pro použití domény webu",
|
||||||
|
"subscriptionDomainPlaceholder": "Zadejte doménu předplatného, jednu na řádek",
|
||||||
|
"subscriptionPath": "Cesta předplatného",
|
||||||
|
"subscriptionPathDescription": "Používá se pro předplatné; po úpravě nezapomeňte restartovat systém pro optimální výkon",
|
||||||
|
"subscriptionPathPlaceholder": "Zadejte",
|
||||||
|
"wildcardResolution": "Řešení zástupných znaků",
|
||||||
|
"wildcardResolutionDescription": "Používá se pro předplatné"
|
||||||
|
},
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
"confirmDelete": "Opravdu chcete smazat?",
|
"confirmDelete": "Opravdu chcete smazat?",
|
||||||
"copy": "Kopírovat",
|
"copy": "Kopírovat",
|
||||||
@@ -19,9 +72,14 @@
|
|||||||
"Hour": "Hodina",
|
"Hour": "Hodina",
|
||||||
"Minute": "Minuta",
|
"Minute": "Minuta",
|
||||||
"Month": "Měsíc",
|
"Month": "Měsíc",
|
||||||
|
"NoLimit": "Bez omezení",
|
||||||
"Year": "Rok",
|
"Year": "Rok",
|
||||||
|
"annualReset": "Roční Reset",
|
||||||
|
"basic": "Základní",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
|
"deductionRatio": "Automatická/Manuální Konfigurace Odpočtu",
|
||||||
|
"deductionRatioDescription": "Používá se pro odpočet. Systém standardně používá automatický výpočetní algoritmus. Když je poskytnut manuální poměr, systém vypočítá proporce na základě časového a dopravního poměru, přičemž celkový součet činí 100%.",
|
||||||
"description": "Popis",
|
"description": "Popis",
|
||||||
"deviceLimit": "Omezení zařízení",
|
"deviceLimit": "Omezení zařízení",
|
||||||
"discount": "Sleva",
|
"discount": "Sleva",
|
||||||
@@ -30,17 +88,28 @@
|
|||||||
"discountPercent": "Procento slevy",
|
"discountPercent": "Procento slevy",
|
||||||
"discount_price": "Cena po slevě",
|
"discount_price": "Cena po slevě",
|
||||||
"duration": "Doba trvání (měsíce)",
|
"duration": "Doba trvání (měsíce)",
|
||||||
"inventory": "Sklad",
|
"groupId": "Skupina předplatného",
|
||||||
|
"inventory": "Limit předplatného",
|
||||||
|
"monthlyReset": "Měsíční Reset",
|
||||||
"name": "Název",
|
"name": "Název",
|
||||||
"noLimit": "Bez omezení",
|
"noLimit": "Bez omezení",
|
||||||
|
"noReset": "Žádný Reset",
|
||||||
|
"pricing": "Cenotvorba",
|
||||||
|
"purchaseWithDiscount": "Povolit Odpočet",
|
||||||
|
"purchaseWithDiscountDescription": "Povolit nebo zakázat funkci odhlášení. Po aktivaci systém provede zpracování odpočtu podle nakonfigurovaných pravidel a poměrů a zbývající hodnota bude vrácena na zůstatek.",
|
||||||
"quota": "Limit nákupu",
|
"quota": "Limit nákupu",
|
||||||
|
"renewalReset": "Reset při Obnovení",
|
||||||
|
"renewalResetDescription": "Resetovací cyklus při obnovení",
|
||||||
"replacement": "Cena za reset (za každý)",
|
"replacement": "Cena za reset (za každý)",
|
||||||
|
"resetCycle": "Resetovací Cyklus",
|
||||||
|
"resetOn1st": "Reset 1. dne",
|
||||||
|
"selectResetCycle": "Vyberte prosím resetovací cyklus",
|
||||||
"selectSubscribeGroup": "Vyberte prosím skupinu předplatného",
|
"selectSubscribeGroup": "Vyberte prosím skupinu předplatného",
|
||||||
"selectUnitTime": "Vyberte jednotku času",
|
"selectUnitTime": "Vyberte jednotku času",
|
||||||
"server": "Server",
|
"server": "Server",
|
||||||
"serverGroup": "Skupina serverů",
|
"serverGroup": "Skupina serverů",
|
||||||
"speedLimit": "Omezení rychlosti (Mbps)",
|
"servers": "Servery",
|
||||||
"subscribeGroup": "Skupina předplatného",
|
"speedLimit": "Omezení rychlosti ",
|
||||||
"traffic": "Přenos dat",
|
"traffic": "Přenos dat",
|
||||||
"unitPrice": "Jednotková cena",
|
"unitPrice": "Jednotková cena",
|
||||||
"unitTime": "Jednotka času"
|
"unitTime": "Jednotka času"
|
||||||
@@ -70,19 +139,22 @@
|
|||||||
"updateSuccess": "Úspěšně aktualizováno",
|
"updateSuccess": "Úspěšně aktualizováno",
|
||||||
"updatedAt": "Čas aktualizace"
|
"updatedAt": "Čas aktualizace"
|
||||||
},
|
},
|
||||||
"inventory": "inventář",
|
"inventory": "Limit předplatného",
|
||||||
"name": "název",
|
"name": "název",
|
||||||
"quota": "Limit na nákup/čas",
|
"quota": "Limit na nákup/čas",
|
||||||
"replacement": "Obnovení ceny/krát",
|
"replacement": "Obnovení ceny/krát",
|
||||||
"sell": "Prodej",
|
"sell": "Prodej",
|
||||||
"show": "Zobrazit na hlavní stránce",
|
"show": "Zobrazit na hlavní stránce",
|
||||||
|
"sold": "Počet předplatných",
|
||||||
"subscribe": "Předplatit",
|
"subscribe": "Předplatit",
|
||||||
"subscribeGroup": "Přihlásit se ke skupině",
|
"subscribeGroup": "Přihlásit se ke skupině",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"subscribe": "Předplatit",
|
"subscribe": "Předplatit",
|
||||||
|
"subscribeApp": "Konfigurace aplikace",
|
||||||
|
"subscribeConfig": "Konfigurace předplatného",
|
||||||
"subscribeGroup": "Skupina předplatných"
|
"subscribeGroup": "Skupina předplatných"
|
||||||
},
|
},
|
||||||
"traffic": "provoz",
|
"traffic": "provoz",
|
||||||
"unitPrice": "Cena/měsíc",
|
"unitPrice": "Jednotková cena",
|
||||||
"updateSuccess": "Aktualizace byla úspěšná"
|
"updateSuccess": "Aktualizace byla úspěšná"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,6 @@
|
|||||||
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
|
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
|
||||||
"saveSuccess": "Úspěšně uloženo"
|
"saveSuccess": "Úspěšně uloženo"
|
||||||
},
|
},
|
||||||
"email": {
|
|
||||||
"expiration_email_template": "Šablona oznámení o vypršení platnosti",
|
|
||||||
"expiration_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte.",
|
|
||||||
"inputPlaceholder": "Zadejte obsah...",
|
|
||||||
"maintenance_email_template": "Šablona oznámení o údržbě",
|
|
||||||
"maintenance_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte.",
|
|
||||||
"saveSuccess": "Konfigurace byla úspěšně uložena.",
|
|
||||||
"sendFailure": "Odeslání testovacího e-mailu selhalo, zkontrolujte konfiguraci.",
|
|
||||||
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
|
|
||||||
"sendTestEmail": "Odeslat testovací e-mail",
|
|
||||||
"sendTestEmailDescription": "Odešlete testovací e-mail pro ověření správnosti konfigurace.",
|
|
||||||
"senderAddress": "Adresa odesílatele",
|
|
||||||
"senderAddressDescription": "Výchozí e-mailová adresa použitá k odesílání e-mailů.",
|
|
||||||
"smtpAccount": "Účet SMTP",
|
|
||||||
"smtpAccountDescription": "E-mailový účet pro ověření identity.",
|
|
||||||
"smtpEncryptionMethod": "Metoda šifrování SMTP",
|
|
||||||
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
|
|
||||||
"smtpPassword": "Heslo SMTP",
|
|
||||||
"smtpPasswordDescription": "Heslo k účtu SMTP.",
|
|
||||||
"smtpServerAddress": "Adresa serveru SMTP",
|
|
||||||
"smtpServerAddressDescription": "Zadejte adresu serveru pro odesílání e-mailů.",
|
|
||||||
"smtpServerPort": "Port serveru SMTP",
|
|
||||||
"smtpServerPortDescription": "Zadejte číslo portu pro připojení k serveru SMTP.",
|
|
||||||
"verify_email_template": "Šablona ověřovacího e-mailu",
|
|
||||||
"verify_email_templateDescription": "Proměnné {after}.názevProměnné{before} v šabloně budou nahrazeny skutečnými daty, ujistěte se, že tyto proměnné zachováte."
|
|
||||||
},
|
|
||||||
"invite": {
|
"invite": {
|
||||||
"commissionFirstTimeOnly": "Provize pouze při prvním nákupu",
|
"commissionFirstTimeOnly": "Provize pouze při prvním nákupu",
|
||||||
"commissionFirstTimeOnlyDescription": "Po aktivaci se provize generuje pouze při první platbě pozvaného uživatele. Můžete konfigurovat jednotlivé uživatele ve správě uživatelů.",
|
"commissionFirstTimeOnlyDescription": "Po aktivaci se provize generuje pouze při první platbě pozvaného uživatele. Můžete konfigurovat jednotlivé uživatele ve správě uživatelů.",
|
||||||
@@ -44,36 +18,6 @@
|
|||||||
"inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.",
|
"inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.",
|
||||||
"saveSuccess": "Úspěšně uloženo"
|
"saveSuccess": "Úspěšně uloženo"
|
||||||
},
|
},
|
||||||
"node": {
|
|
||||||
"communicationKey": "Komunikační klíč",
|
|
||||||
"communicationKeyDescription": "Klíč pro komunikaci uzlu, aby se zajistilo, že data nebudou zachycena jinými osobami",
|
|
||||||
"inputPlaceholder": "Prosím zadejte",
|
|
||||||
"nodePullInterval": "Interval stahování uzlu",
|
|
||||||
"nodePullIntervalDescription": "Frekvence, s jakou uzel získává data z panelu (v sekundách)",
|
|
||||||
"nodePushInterval": "Interval odesílání uzlu",
|
|
||||||
"nodePushIntervalDescription": "Frekvence, s jakou uzel odesílá data do panelu",
|
|
||||||
"saveSuccess": "Úspěšně uloženo"
|
|
||||||
},
|
|
||||||
"register": {
|
|
||||||
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
|
|
||||||
"emailSuffixWhitelistDescription": "Po aktivaci se mohou registrovat pouze e-maily s příponami uvedenými v seznamu",
|
|
||||||
"emailVerification": "Ověření e-mailu",
|
|
||||||
"emailVerificationDescription": "Po aktivaci budou uživatelé muset ověřit svůj e-mail",
|
|
||||||
"ipRegistrationLimit": "Omezení registrace podle IP",
|
|
||||||
"ipRegistrationLimitDescription": "Po aktivaci budou IP adresy, které splňují pravidla, omezeny v registraci. Upozorňujeme, že kvůli CDN nebo front-end proxy může dojít k problémům s určením IP adresy",
|
|
||||||
"penaltyTime": "Doba trestu (minuty)",
|
|
||||||
"penaltyTimeDescription": "Uživatelé musí počkat, až uplyne doba trestu, než se budou moci znovu registrovat",
|
|
||||||
"registrationLimitCount": "Počet omezení registrace",
|
|
||||||
"registrationLimitCountDescription": "Po dosažení limitu registrace se aktivuje trest",
|
|
||||||
"saveSuccess": "Úspěšně uloženo",
|
|
||||||
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
|
|
||||||
"stopNewUserRegistrationDescription": "Po aktivaci se nikdo nebude moci registrovat",
|
|
||||||
"trialRegistration": "Zkušební registrace",
|
|
||||||
"trialRegistrationDescription": "Aktivujte zkušební registraci, nejprve upravte zkušební balíček a dobu trvání",
|
|
||||||
"whitelistSuffixes": "Povolené přípony",
|
|
||||||
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu při registraci, jedna přípona na řádek",
|
|
||||||
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, jednu na řádek"
|
|
||||||
},
|
|
||||||
"site": {
|
"site": {
|
||||||
"logo": "LOGO",
|
"logo": "LOGO",
|
||||||
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
|
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
|
||||||
@@ -89,37 +33,10 @@
|
|||||||
"siteNameDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit název webu",
|
"siteNameDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit název webu",
|
||||||
"siteNamePlaceholder": "Zadejte název webu"
|
"siteNamePlaceholder": "Zadejte název webu"
|
||||||
},
|
},
|
||||||
"subscription": {
|
|
||||||
"add": "Přidat",
|
|
||||||
"app": "Aplikace",
|
|
||||||
"appDescription": "Nastavte adresu pro stažení aplikace a pravidla importu, nezapomeňte po úpravách uložit",
|
|
||||||
"appDownloadURL": "Adresa pro stažení aplikace",
|
|
||||||
"appIcon": "Ikona aplikace",
|
|
||||||
"appName": "Název aplikace",
|
|
||||||
"delete": "Smazat",
|
|
||||||
"reset": "Resetovat",
|
|
||||||
"save": "Uložit",
|
|
||||||
"saveSuccess": "Úspěšně uloženo",
|
|
||||||
"singleSubscriptionMode": "Režim jednorázového předplatného",
|
|
||||||
"singleSubscriptionModeDescription": "Po aktivaci se všechny uživatelské balíčky převedou na zůstatek",
|
|
||||||
"subscriptionDomain": "Doména předplatného",
|
|
||||||
"subscriptionDomainDescription": "Používá se pro předplatné, pokud je prázdné, použije se doména webu",
|
|
||||||
"subscriptionDomainPlaceholder": "Zadejte doménu předplatného, pro více domén použijte jeden řádek pro každou",
|
|
||||||
"subscriptionPath": "Cesta předplatného",
|
|
||||||
"subscriptionPathDescription": "Používá se pro předplatné, po úpravě je nutné restartovat systém pro zajištění nejlepšího výkonu",
|
|
||||||
"subscriptionPathPlaceholder": "Zadejte",
|
|
||||||
"subscriptionProtocol": "Protokol předplatného",
|
|
||||||
"wildcardResolution": "Řešení zástupných znaků",
|
|
||||||
"wildcardResolutionDescription": "Používá se pro předplatné"
|
|
||||||
},
|
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"currency": "Měna",
|
"currency": "Měna",
|
||||||
"email": "E-mail",
|
|
||||||
"invite": "Pozvat",
|
"invite": "Pozvat",
|
||||||
"node": "Uzlový bod",
|
|
||||||
"register": "Registrace",
|
|
||||||
"site": "Stránka",
|
"site": "Stránka",
|
||||||
"subscription": "Předplatné",
|
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"tos": "Podmínky služby",
|
"tos": "Podmínky služby",
|
||||||
"verify": "Ověřit"
|
"verify": "Ověřit"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "ID robota",
|
||||||
|
"clientIdDescription": "ID Telegram bota, které můžete získat od @BotFather",
|
||||||
|
"clientSecret": "Token robota",
|
||||||
|
"clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather",
|
||||||
|
"enable": "Povolit",
|
||||||
|
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
|
||||||
|
"redirectUri": "Přesměrovací URL",
|
||||||
|
"redirectUriDescription": "URL, na kterou Telegram přesměruje po úspěšném ověření",
|
||||||
|
"saveFailed": "Uložení se nezdařilo",
|
||||||
|
"saveSuccess": "Uložení bylo úspěšné"
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"actions": "akce",
|
"actions": "akce",
|
||||||
"balance": "Zůstatek",
|
"balance": "Zůstatek",
|
||||||
"cancel": "zrušit",
|
"cancel": "zrušit",
|
||||||
|
"commission": "Provize",
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
"confirmDelete": "Opravdu chcete smazat?",
|
"confirmDelete": "Opravdu chcete smazat?",
|
||||||
"create": "Vytvořit",
|
"create": "Vytvořit",
|
||||||
@@ -16,10 +17,16 @@
|
|||||||
"email": "e-mail",
|
"email": "e-mail",
|
||||||
"enable": "Povolit",
|
"enable": "Povolit",
|
||||||
"form": {
|
"form": {
|
||||||
|
"areaCode": "Směrové číslo",
|
||||||
|
"areaCodePlaceholder": "Směrové číslo",
|
||||||
"balance": "Zůstatek",
|
"balance": "Zůstatek",
|
||||||
"balancePlaceholder": "Zůstatek",
|
"balancePlaceholder": "Zůstatek",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
|
"commission": "Provize",
|
||||||
|
"commissionPlaceholder": "Zadejte provizi",
|
||||||
"confirm": "Potvrdit",
|
"confirm": "Potvrdit",
|
||||||
|
"giftAmount": "Částka daru",
|
||||||
|
"giftAmountPlaceholder": "Zadejte částku daru",
|
||||||
"invalidEmailFormat": "Neplatný formát e-mailu",
|
"invalidEmailFormat": "Neplatný formát e-mailu",
|
||||||
"inviteCode": "Pozvánkový kód",
|
"inviteCode": "Pozvánkový kód",
|
||||||
"inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)",
|
"inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)",
|
||||||
@@ -28,11 +35,16 @@
|
|||||||
"passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)",
|
"passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)",
|
||||||
"refererId": "ID doporučitele",
|
"refererId": "ID doporučitele",
|
||||||
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
"refererIdPlaceholder": "Zadejte ID doporučitele",
|
||||||
|
"telephone": "Telefonní číslo",
|
||||||
|
"telephonePlaceholder": "Zadejte telefonní číslo",
|
||||||
"userEmail": "E-mail uživatele",
|
"userEmail": "E-mail uživatele",
|
||||||
"userEmailPlaceholder": "Zadejte e-mail uživatele"
|
"userEmailPlaceholder": "Zadejte e-mail uživatele"
|
||||||
},
|
},
|
||||||
|
"giftAmount": "Částka dárku",
|
||||||
|
"inviteCode": "Pozvánkový kód",
|
||||||
"referer": "Doporučitel",
|
"referer": "Doporučitel",
|
||||||
|
"telephone": "Telefonní číslo",
|
||||||
"updateSuccess": "Aktualizace byla úspěšná",
|
"updateSuccess": "Aktualizace byla úspěšná",
|
||||||
"userList": "Seznam uživatelů",
|
"userList": "Seznam uživatelů",
|
||||||
"userName": "Uživatelský e-mail"
|
"userName": "Identifikátor uživatele"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Dienst-ID",
|
||||||
|
"clientIdDescription": "Apple-Dienst-ID, die Sie im Apple Developer Portal erhalten können",
|
||||||
|
"clientSecret": "Privater Schlüssel",
|
||||||
|
"clientSecretDescription": "Der private Schlüsselinhalt (.p8-Datei), der für die Authentifizierung bei Apple verwendet wird",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden",
|
||||||
|
"keyId": "Schlüssel-ID",
|
||||||
|
"keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal",
|
||||||
|
"redirectUri": "Rückkehr-URL",
|
||||||
|
"redirectUriDescription": "Die URL, zu der Apple nach erfolgreicher Authentifizierung weiterleitet",
|
||||||
|
"saveFailed": "Speichern fehlgeschlagen",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert",
|
||||||
|
"teamId": "Team-ID",
|
||||||
|
"teamIdDescription": "Apple Developer-Team-ID"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ipRegistrationLimit": "IP-Registrierungsgrenze",
|
||||||
|
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die Bestimmung der IPs aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann",
|
||||||
|
"penaltyTime": "Strafzeit (Minuten)",
|
||||||
|
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren können",
|
||||||
|
"registrationLimitCount": "Anzahl der Anmeldungen",
|
||||||
|
"registrationLimitCountDescription": "Strafe aktivieren, nachdem das Registrierungslimit erreicht wurde",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert",
|
||||||
|
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
|
||||||
|
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
|
||||||
|
"trialRegistration": "Studienregistrierung",
|
||||||
|
"trialRegistrationDescription": "Testregistrierung aktivieren; zuerst Testpaket und -dauer anpassen"
|
||||||
|
}
|
||||||
@@ -4,20 +4,28 @@
|
|||||||
"continue": "Fortfahren",
|
"continue": "Fortfahren",
|
||||||
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden oder zu registrieren.",
|
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden oder zu registrieren.",
|
||||||
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||||
"title": "Anmelden/Registrieren",
|
"title": "Anmelden/Registrieren"
|
||||||
"whitelist": "Die E-Mail-Domain befindet sich nicht auf der zugelassenen Whitelist."
|
|
||||||
},
|
},
|
||||||
|
"get": "Holen",
|
||||||
"login": {
|
"login": {
|
||||||
|
"codeLogin": "Anmeldung mit Bestätigungscode",
|
||||||
"description": "Bitte geben Sie Ihre Kontoinformationen ein, um sich anzumelden.",
|
"description": "Bitte geben Sie Ihre Kontoinformationen ein, um sich anzumelden.",
|
||||||
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||||
"forgotPassword": "Passwort vergessen?",
|
"forgotPassword": "Passwort vergessen?",
|
||||||
|
"passwordLogin": "Anmeldung mit Passwort",
|
||||||
|
"registerAccount": "Konto registrieren",
|
||||||
"success": "Erfolgreich eingeloggt!",
|
"success": "Erfolgreich eingeloggt!",
|
||||||
"switchAccount": "Registrieren/Konto wechseln",
|
|
||||||
"title": "Anmeldung"
|
"title": "Anmeldung"
|
||||||
},
|
},
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
|
"methods": {
|
||||||
|
"email": "E-Mail",
|
||||||
|
"sms": "Telefon"
|
||||||
|
},
|
||||||
"orWithEmail": "oder mit E-Mail",
|
"orWithEmail": "oder mit E-Mail",
|
||||||
"register": {
|
"register": {
|
||||||
"description": "Erstellen Sie ein neues Konto, indem Sie Ihre Informationen ausfüllen, um sich zu registrieren.",
|
"description": "Erstellen Sie ein neues Konto, indem Sie Ihre Informationen ausfüllen, um sich zu registrieren.",
|
||||||
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||||
"existingAccount": "Bereits ein Konto?",
|
"existingAccount": "Bereits ein Konto?",
|
||||||
"get": "Erhalten",
|
"get": "Erhalten",
|
||||||
"invite": "Einladungscode",
|
"invite": "Einladungscode",
|
||||||
@@ -25,15 +33,19 @@
|
|||||||
"passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein",
|
"passwordMismatch": "Die eingegebenen Passwörter stimmen nicht überein",
|
||||||
"success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!",
|
"success": "Registrierung erfolgreich, Sie sind automatisch eingeloggt!",
|
||||||
"switchToLogin": "Anmelden/E-Mail zurücksetzen",
|
"switchToLogin": "Anmelden/E-Mail zurücksetzen",
|
||||||
"title": "Registrieren"
|
"title": "Registrieren",
|
||||||
|
"whitelist": "Die E-Mail-Domain befindet sich nicht in der erlaubten Whitelist."
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um das Passwort zurückzusetzen.",
|
"description": "Bitte geben Sie Ihre E-Mail-Adresse ein, um das Passwort zurückzusetzen.",
|
||||||
|
"email": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||||
"existingAccount": "Bereits ein Konto?",
|
"existingAccount": "Bereits ein Konto?",
|
||||||
"get": "Erhalten",
|
"get": "Erhalten",
|
||||||
"success": "Passwort erfolgreich zurückgesetzt, automatisch zum Login gewechselt!",
|
"success": "Passwort erfolgreich zurückgesetzt, automatisch zum Login gewechselt!",
|
||||||
"switchToLogin": "Anmelden/Registrieren",
|
"switchToLogin": "Anmelden/Registrieren",
|
||||||
"title": "Passwort zurücksetzen"
|
"title": "Passwort zurücksetzen"
|
||||||
},
|
},
|
||||||
"tos": "Nutzungsbedingungen"
|
"tos": "Nutzungsbedingungen",
|
||||||
|
"verifyAccount": "Kontoverifizierung",
|
||||||
|
"verifyAccountDesc": "Bitte verifizieren Sie Ihr Konto, um fortzufahren."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"20004": "Dieser Benutzer wurde deaktiviert, bei Fragen wenden Sie sich bitte an den Kundenservice.",
|
"20004": "Dieser Benutzer wurde deaktiviert, bei Fragen wenden Sie sich bitte an den Kundenservice.",
|
||||||
"20005": "Unzureichendes Guthaben, bitte aufladen und erneut versuchen.",
|
"20005": "Unzureichendes Guthaben, bitte aufladen und erneut versuchen.",
|
||||||
"20006": "Die Registrierungsfunktion ist derzeit nicht verfügbar, bitte versuchen Sie es später erneut.",
|
"20006": "Die Registrierungsfunktion ist derzeit nicht verfügbar, bitte versuchen Sie es später erneut.",
|
||||||
|
"20008": "Die Benutzerinformationen sind falsch, bitte überprüfen Sie sie und versuchen Sie es erneut.",
|
||||||
"30001": "Dieser Knoten existiert bereits, bitte nicht erneut hinzufügen.",
|
"30001": "Dieser Knoten existiert bereits, bitte nicht erneut hinzufügen.",
|
||||||
"30002": "Kein relevanter Knoten gefunden, bitte überprüfen und erneut versuchen.",
|
"30002": "Kein relevanter Knoten gefunden, bitte überprüfen und erneut versuchen.",
|
||||||
"30003": "Die Gruppe existiert bereits, bitte versuchen Sie einen anderen Namen zu verwenden.",
|
"30003": "Die Gruppe existiert bereits, bitte versuchen Sie einen anderen Namen zu verwenden.",
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
"50002": "Dieser Gutschein wurde bereits verwendet und kann nicht erneut verwendet werden.",
|
"50002": "Dieser Gutschein wurde bereits verwendet und kann nicht erneut verwendet werden.",
|
||||||
"60001": "Das Abonnement ist abgelaufen, bitte erneuern und dann verwenden.",
|
"60001": "Das Abonnement ist abgelaufen, bitte erneuern und dann verwenden.",
|
||||||
"60002": "Das Abonnement kann derzeit nicht verwendet werden, bitte versuchen Sie es später erneut.",
|
"60002": "Das Abonnement kann derzeit nicht verwendet werden, bitte versuchen Sie es später erneut.",
|
||||||
|
"60003": "Ein bestehendes Abonnement wurde erkannt. Bitte kündigen Sie es, bevor Sie fortfahren.",
|
||||||
|
"60004": "Kann derzeit nicht gelöscht werden, da das Abonnement aktive Benutzer hat.",
|
||||||
"70001": "Der Bestätigungscode ist falsch, bitte erneut eingeben.",
|
"70001": "Der Bestätigungscode ist falsch, bitte erneut eingeben.",
|
||||||
"80001": "Die Aufgabe wurde nicht erfolgreich in die Warteschlange aufgenommen, bitte versuchen Sie es später erneut.",
|
"80001": "Die Aufgabe wurde nicht erfolgreich in die Warteschlange aufgenommen, bitte versuchen Sie es später erneut.",
|
||||||
"90001": "Bitte deaktivieren Sie den DEBUG-Modus und versuchen Sie es erneut.",
|
"90001": "Bitte deaktivieren Sie den DEBUG-Modus und versuchen Sie es erneut.",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"emailBasicConfig": "Grundkonfiguration",
|
||||||
|
"emailBasicConfigDescription": "Konfigurieren Sie die SMTP-Servereinstellungen und E-Mail-Verifizierungsoptionen",
|
||||||
|
"emailLogs": "E-Mail-Protokolle",
|
||||||
|
"emailLogsDescription": "Sehen Sie sich den Verlauf gesendeter E-Mails und deren Status an",
|
||||||
|
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
|
||||||
|
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren",
|
||||||
|
"emailTemplate": "E-Mail-Vorlage",
|
||||||
|
"emailVerification": "E-Mail-Verifizierung",
|
||||||
|
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
|
||||||
|
"expiration_email_template": "Vorlage für Ablaufbenachrichtigung",
|
||||||
|
"expiration_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"inputPlaceholder": "Wert eingeben...",
|
||||||
|
"maintenance_email_template": "Wartungshinweis Vorlage",
|
||||||
|
"maintenance_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
|
||||||
|
"recipient": "Empfänger",
|
||||||
|
"saveFailed": "Konfiguration konnte nicht gespeichert werden",
|
||||||
|
"saveSuccess": "Konfiguration erfolgreich gespeichert.",
|
||||||
|
"sendFailure": "Senden der Test-E-Mail fehlgeschlagen, bitte überprüfen Sie die Konfiguration.",
|
||||||
|
"sendSuccess": "Test-E-Mail erfolgreich gesendet.",
|
||||||
|
"sendTestEmail": "Test-E-Mail senden",
|
||||||
|
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um die Konfiguration zu überprüfen.",
|
||||||
|
"senderAddress": "Absenderadresse",
|
||||||
|
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
|
||||||
|
"sent": "Gesendet",
|
||||||
|
"sentAt": "Gesendet am",
|
||||||
|
"smtpAccount": "SMTP-Konto",
|
||||||
|
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
|
||||||
|
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
|
||||||
|
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
|
||||||
|
"smtpPassword": "SMTP-Passwort",
|
||||||
|
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
|
||||||
|
"smtpServerAddress": "SMTP-Server-Adresse",
|
||||||
|
"smtpServerAddressDescription": "Geben Sie die Serveradresse an, die zum Senden von E-Mails verwendet wird.",
|
||||||
|
"smtpServerPort": "SMTP-Server-Port",
|
||||||
|
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
|
||||||
|
"status": "Status",
|
||||||
|
"subject": "Betreff",
|
||||||
|
"verify_email_template": "E-Mail-Bestätigungsvorlage",
|
||||||
|
"verify_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass diese Variablen beibehalten werden.",
|
||||||
|
"whitelistSuffixes": "Whitelist-Suffixe",
|
||||||
|
"whitelistSuffixesDescription": "Wird zur E-Mail-Verifizierung während der Registrierung verwendet; eine pro Zeile",
|
||||||
|
"whitelistSuffixesPlaceholder": "Geben Sie E-Mail-Suffixe ein, eines pro Zeile"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Kunden-ID",
|
||||||
|
"clientIdDescription": "Facebook-App-ID aus der Facebook-Entwicklerkonsole",
|
||||||
|
"clientSecret": "Client-Geheimnis",
|
||||||
|
"clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden",
|
||||||
|
"redirectUri": "Autorisierte Umleitungs-URI",
|
||||||
|
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Facebook-Authentifizierung weitergeleitet wird",
|
||||||
|
"saveFailed": "Speichern fehlgeschlagen",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "GitHub-Client-ID",
|
||||||
|
"clientIdDescription": "Die Client-ID aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
|
||||||
|
"clientSecret": "GitHub-Clientgeheimnis",
|
||||||
|
"clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
|
||||||
|
"enable": "GitHub-Authentifizierung aktivieren",
|
||||||
|
"enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden",
|
||||||
|
"redirectUri": "Autorisierungs-Callback-URL",
|
||||||
|
"redirectUriDescription": "Die URL in Ihrer Anwendung, zu der Benutzer nach der GitHub-Authentifizierung weitergeleitet werden",
|
||||||
|
"saveFailed": "Speichern der GitHub-Einstellungen fehlgeschlagen",
|
||||||
|
"saveSuccess": "GitHub-Einstellungen erfolgreich gespeichert"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Kunden-ID",
|
||||||
|
"clientIdDescription": "Google OAuth 2.0-Client-ID aus der Google Cloud Console",
|
||||||
|
"clientSecret": "Client-Geheimnis",
|
||||||
|
"clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden",
|
||||||
|
"redirectUri": "Autorisierte Umleitungs-URI",
|
||||||
|
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Google-Authentifizierung weitergeleitet wird",
|
||||||
|
"saveFailed": "Speichern fehlgeschlagen",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert"
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
{
|
{
|
||||||
"Announcement Management": "Ankündigungsverwaltung",
|
"Announcement Management": "Ankündigungsverwaltung",
|
||||||
|
"Apple": "Apple-ID",
|
||||||
|
"Auth Control": "Authentifizierungskontrolle",
|
||||||
"Coupon Management": "Gutscheinverwaltung",
|
"Coupon Management": "Gutscheinverwaltung",
|
||||||
"Dashboard": "Armaturenbrett",
|
"Dashboard": "Armaturenbrett",
|
||||||
"Document Management": "Dokumentenverwaltung",
|
"Document Management": "Dokumentenverwaltung",
|
||||||
|
"Email": "E-Mail",
|
||||||
|
"Facebook": "Facebook",
|
||||||
"Finance": "Finanzen",
|
"Finance": "Finanzen",
|
||||||
|
"General": "Allgemein",
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Google": "Google",
|
||||||
"Order Management": "Bestellverwaltung",
|
"Order Management": "Bestellverwaltung",
|
||||||
"Payment Config": "Zahlungskonfiguration",
|
"Payment Config": "Zahlungskonfiguration",
|
||||||
|
"Phone Number": "Telefonnummer",
|
||||||
"Server": "Dienst",
|
"Server": "Dienst",
|
||||||
"Server Management": "Serververwaltung",
|
"Server Management": "Serververwaltung",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
"Subscribe Management": "Abonnementverwaltung",
|
"Subscribe Management": "Abonnementverwaltung",
|
||||||
"System Config": "Systemkonfiguration",
|
"System Config": "Systemkonfiguration",
|
||||||
"System Tool": "Systemwerkzeug",
|
"System Tool": "Systemwerkzeug",
|
||||||
|
"Telegram": "Telegram",
|
||||||
"Ticket Management": "Ticketverwaltung",
|
"Ticket Management": "Ticketverwaltung",
|
||||||
|
"Twitter": "Twitter",
|
||||||
"User": "Benutzer",
|
"User": "Benutzer",
|
||||||
"User Management": "Benutzerverwaltung"
|
"User Management": "Benutzerverwaltung"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"SmsList": "SMS-Protokollliste",
|
||||||
|
"applyPlatform": "Plattform anwenden",
|
||||||
|
"areaCode": "Vorwahl",
|
||||||
|
"content": "Inhalt",
|
||||||
|
"createdAt": "Sendezeit",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
|
||||||
|
"expireTime": "Ablaufzeit",
|
||||||
|
"expireTimeTip": "Gültigkeitsdauer des SMS-Bestätigungscodes (Sekunden)",
|
||||||
|
"interval": "Intervall",
|
||||||
|
"intervalTip": "SMS-Bestätigungscode-Sendeintervall für dieselbe Telefonnummer (Sekunden), 0 bedeutet keine Begrenzung",
|
||||||
|
"limit": "Tageslimit",
|
||||||
|
"limitTip": "Tägliches Limit für das Senden von SMS-Verifizierungscodes an dieselbe Telefonnummer, 0 bedeutet kein Limit",
|
||||||
|
"logs": "Protokolle",
|
||||||
|
"placeholders": {
|
||||||
|
"expireTime": "Geben Sie die Ablaufzeit ein, Standard ist 300",
|
||||||
|
"interval": "Geben Sie das Intervall ein, Standard ist 60",
|
||||||
|
"limit": "Geben Sie das tägliche Limit ein, Standard ist 20",
|
||||||
|
"template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten",
|
||||||
|
"templateCode": "Geben Sie den Vorlagen-Code ein"
|
||||||
|
},
|
||||||
|
"platform": "SMS-Plattform",
|
||||||
|
"platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus",
|
||||||
|
"platformTip": "Bitte wählen Sie die SMS-Plattform aus",
|
||||||
|
"search": "Telefonnummer suchen",
|
||||||
|
"sendFailed": "Senden fehlgeschlagen",
|
||||||
|
"sendSuccess": "Erfolgreich gesendet",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"status": "Status",
|
||||||
|
"telephone": "Telefonnummer",
|
||||||
|
"template": "SMS-Vorlage",
|
||||||
|
"templateCode": "Vorlagen-Code",
|
||||||
|
"templateParam": "Vorlagenparameter",
|
||||||
|
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus und lassen Sie {code} in der Mitte stehen, da sonst die SMS-Funktion nicht funktioniert.",
|
||||||
|
"testSms": "Test-SMS senden",
|
||||||
|
"testSmsContent": "Dies ist eine Testnachricht",
|
||||||
|
"testSmsPhone": "Telefonnummer eingeben",
|
||||||
|
"testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen",
|
||||||
|
"updateSuccess": "Aktualisierung erfolgreich"
|
||||||
|
}
|
||||||
@@ -1,4 +1,24 @@
|
|||||||
{
|
{
|
||||||
|
"config": {
|
||||||
|
"addTimeSlot": "Zeitfenster hinzufügen",
|
||||||
|
"communicationKey": "Kommunikationsschlüssel",
|
||||||
|
"communicationKeyDescription": "Schlüssel für die Knotenkommunikation zur Gewährleistung der Datensicherheit",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"dynamicMultiplier": "Dynamischer Multiplikator",
|
||||||
|
"dynamicMultiplierDescription": "Dynamischer Multiplikator bezieht sich auf die Verarbeitung von übertragenen Daten mit unterschiedlichen Multiplikatoren für verschiedene Knoten während unterschiedlicher Zeiträume.",
|
||||||
|
"endTime": "Endzeit",
|
||||||
|
"inputPlaceholder": "Eingeben",
|
||||||
|
"multiplier": "Multiplikator",
|
||||||
|
"nodePullInterval": "Knotenabfrageintervall",
|
||||||
|
"nodePullIntervalDescription": "Häufigkeit (in Sekunden), mit der Knoten Daten vom Panel abrufen",
|
||||||
|
"nodePushInterval": "Knotenübertragungsintervall",
|
||||||
|
"nodePushIntervalDescription": "Häufigkeit, mit der Knoten Daten an das Panel übertragen",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
|
"save": "Speichern",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert",
|
||||||
|
"startTime": "Startzeit",
|
||||||
|
"timeSlot": "Zeitfenster"
|
||||||
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
@@ -46,18 +66,19 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"allowInsecure": "Unsicher zulassen",
|
"allowInsecure": "Unsicher zulassen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"city": "Stadt",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
|
"country": "Land",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"editSecurity": "Sicherheitskonfiguration bearbeiten",
|
"editSecurity": "Sicherheitskonfiguration bearbeiten",
|
||||||
"enableRelay": "Relay aktivieren",
|
|
||||||
"enableTLS": "TLS aktivieren",
|
"enableTLS": "TLS aktivieren",
|
||||||
"encryptionMethod": "Verschlüsselungsmethode",
|
"encryptionMethod": "Verschlüsselungsmethode",
|
||||||
"flow": "Flusskontrollalgorithmus",
|
"flow": "Flusskontrollalgorithmus",
|
||||||
|
"groupId": "Knotengruppe",
|
||||||
"hopInterval": "Sprungintervall",
|
"hopInterval": "Sprungintervall",
|
||||||
"hopPorts": "Sprungports",
|
"hopPorts": "Sprungports",
|
||||||
"hopPortsPlaceholder": "Mehrere Ports durch Kommas trennen",
|
"hopPortsPlaceholder": "Mehrere Ports durch Kommas trennen",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"nodeGroupId": "Knotengruppen-ID",
|
|
||||||
"obfsPassword": "Verschleierungspasswort",
|
"obfsPassword": "Verschleierungspasswort",
|
||||||
"obfsPasswordPlaceholder": "Leer lassen für keine Verschleierung",
|
"obfsPasswordPlaceholder": "Leer lassen für keine Verschleierung",
|
||||||
"path": "Pfad",
|
"path": "Pfad",
|
||||||
@@ -65,7 +86,14 @@
|
|||||||
"port": "Serverport",
|
"port": "Serverport",
|
||||||
"protocol": "Protokoll",
|
"protocol": "Protokoll",
|
||||||
"relayHost": "Relay-Adresse",
|
"relayHost": "Relay-Adresse",
|
||||||
|
"relayMode": "Relaismodus",
|
||||||
|
"relayModeOptions": {
|
||||||
|
"all": "Alle",
|
||||||
|
"none": "Keine",
|
||||||
|
"random": "Zufällig"
|
||||||
|
},
|
||||||
"relayPort": "Relay-Port",
|
"relayPort": "Relay-Port",
|
||||||
|
"relayPrefix": "Relaispräfix",
|
||||||
"remarks": "Bemerkungen",
|
"remarks": "Bemerkungen",
|
||||||
"security": "Sicherheit",
|
"security": "Sicherheit",
|
||||||
"securityConfig": "Sicherheitskonfiguration",
|
"securityConfig": "Sicherheitskonfiguration",
|
||||||
@@ -88,10 +116,14 @@
|
|||||||
"selectEncryptionMethod": "Verschlüsselungsmethode auswählen",
|
"selectEncryptionMethod": "Verschlüsselungsmethode auswählen",
|
||||||
"selectNodeGroup": "Knotengruppe auswählen",
|
"selectNodeGroup": "Knotengruppe auswählen",
|
||||||
"selectProtocol": "Protokoll auswählen",
|
"selectProtocol": "Protokoll auswählen",
|
||||||
|
"selectRelayMode": "Relaismodus auswählen",
|
||||||
"serverAddr": "Serveradresse",
|
"serverAddr": "Serveradresse",
|
||||||
|
"serverKey": "Server-Schlüssel",
|
||||||
"serverName": "Dienstname",
|
"serverName": "Dienstname",
|
||||||
"speedLimit": "Geschwindigkeitsbegrenzung",
|
"speedLimit": "Geschwindigkeitsbegrenzung",
|
||||||
"speedLimitPlaceholder": "Keine Begrenzung",
|
"speedLimitPlaceholder": "Keine Begrenzung",
|
||||||
|
"tags": "Tags",
|
||||||
|
"tagsPlaceholder": "Verwenden Sie die Eingabetaste oder das Komma (,), um mehrere Tags einzugeben",
|
||||||
"trafficRatio": "Verkehrsrate",
|
"trafficRatio": "Verkehrsrate",
|
||||||
"transport": "Transportprotokoll",
|
"transport": "Transportprotokoll",
|
||||||
"transportConfig": "Transportprotokollkonfiguration",
|
"transportConfig": "Transportprotokollkonfiguration",
|
||||||
@@ -112,6 +144,7 @@
|
|||||||
"serverAddr": "Serveradresse",
|
"serverAddr": "Serveradresse",
|
||||||
"speedLimit": "Geschwindigkeitsbegrenzung",
|
"speedLimit": "Geschwindigkeitsbegrenzung",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
"tags": "Stichwörter",
|
||||||
"trafficRatio": "Verkehrsrate",
|
"trafficRatio": "Verkehrsrate",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"updateSuccess": "Erfolgreich aktualisiert",
|
"updateSuccess": "Erfolgreich aktualisiert",
|
||||||
@@ -119,6 +152,7 @@
|
|||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"node": "Knoten",
|
"node": "Knoten",
|
||||||
|
"nodeConfig": "Knoten-Konfiguration",
|
||||||
"nodeGroup": "Knotengruppe"
|
"nodeGroup": "Knotengruppe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,59 @@
|
|||||||
{
|
{
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
|
"app": {
|
||||||
|
"appDownloadURL": "App-Download-URL",
|
||||||
|
"appIcon": "App-Symbol",
|
||||||
|
"appList": "App-Liste",
|
||||||
|
"appName": "App-Name",
|
||||||
|
"backupDomains": "Liste der Backup-Domains",
|
||||||
|
"backupDomainsDescription": "Liste der Backup-Domains für die Domain-Auflösung, eine Domain pro Zeile",
|
||||||
|
"batchDelete": "Stapel löschen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"communicationKey": "Kommunikationsschlüssel",
|
||||||
|
"communicationKeyDescription": "Schlüssel, der für die Kommunikation mit dem Client verwendet wird",
|
||||||
|
"config": "Konfiguration",
|
||||||
|
"configApp": "App-Konfiguration",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"confirmDelete": "Löschen bestätigen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"createApp": "App erstellen",
|
||||||
|
"createSuccess": "Erfolgreich erstellt",
|
||||||
|
"defaultVersion": "Standard",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"deleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden",
|
||||||
|
"describeDescription": "Wird verwendet, um die Anwendung zu beschreiben, wird in der App-Liste angezeigt",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"downloadLink": "Download-Link",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"editApp": "App bearbeiten",
|
||||||
|
"encryption": "Verschlüsselungsmethode",
|
||||||
|
"encryptionDescription": "Wählen Sie die Verschlüsselungsmethode für die Kommunikation mit dem Client. Wenn ausgewählt, wird der Client diese Methode verwenden, um mit dem Server zu kommunizieren.",
|
||||||
|
"nameDescription": "Anwendungsname, wird in der App-Liste angezeigt",
|
||||||
|
"platform": "Plattform",
|
||||||
|
"selectApp": "App auswählen",
|
||||||
|
"selectAppDescription": "Wählen Sie die App aus, die konfiguriert werden soll. Alle Einstellungen gelten für die ausgewählte App.",
|
||||||
|
"startupPicture": "Startbild",
|
||||||
|
"startupPictureDescription": "Startbild, unterstützt Netzwerk- und lokale Bilder. Für Netzwerkbilder bitte die vollständige Bild-URL eingeben",
|
||||||
|
"startupPicturePreview": "Vorschau des Startbildes",
|
||||||
|
"startupPictureSkip": "Überspringzeit des Startbildes",
|
||||||
|
"startupPictureSkipDescription": "Anzeigedauer des Startbildes in Sekunden, 0 eingeben, um nicht anzuzeigen",
|
||||||
|
"subscriptionProtocol": "Abonnementprotokoll",
|
||||||
|
"updateSuccess": "Erfolgreich aktualisiert",
|
||||||
|
"version": "Version"
|
||||||
|
},
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"config": {
|
||||||
|
"singleSubscriptionMode": "Einzelabonnement-Modus",
|
||||||
|
"singleSubscriptionModeDescription": "Wenn aktiviert, werden alle Benutzerabonnements in Guthaben umgewandelt",
|
||||||
|
"subscriptionDomain": "Abonnement-Domain",
|
||||||
|
"subscriptionDomainDescription": "Wird für Abonnements verwendet; leer lassen, um die Site-Domain zu verwenden",
|
||||||
|
"subscriptionDomainPlaceholder": "Abonnement-Domain eingeben, eine pro Zeile",
|
||||||
|
"subscriptionPath": "Abonnement-Pfad",
|
||||||
|
"subscriptionPathDescription": "Wird für Abonnements verwendet; starten Sie das System nach der Änderung neu, um die optimale Leistung zu gewährleisten",
|
||||||
|
"subscriptionPathPlaceholder": "Eingeben",
|
||||||
|
"wildcardResolution": "Wildcard-Auflösung",
|
||||||
|
"wildcardResolutionDescription": "Wird für Abonnements verwendet"
|
||||||
|
},
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
"confirmDelete": "Möchten Sie wirklich löschen?",
|
"confirmDelete": "Möchten Sie wirklich löschen?",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
@@ -19,9 +72,14 @@
|
|||||||
"Hour": "Stunde",
|
"Hour": "Stunde",
|
||||||
"Minute": "Minute",
|
"Minute": "Minute",
|
||||||
"Month": "Monat",
|
"Month": "Monat",
|
||||||
|
"NoLimit": "Kein Limit",
|
||||||
"Year": "Jahr",
|
"Year": "Jahr",
|
||||||
|
"annualReset": "Jährliches Zurücksetzen",
|
||||||
|
"basic": "Grundlegend",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
|
"deductionRatio": "Automatische/Manuelle Abzugskonfiguration",
|
||||||
|
"deductionRatioDescription": "Wird für Abzüge verwendet. Standardmäßig verwendet das System einen automatischen Berechnungsalgorithmus. Wenn ein manueller Prozentsatz angegeben wird, berechnet das System die Anteile basierend auf dem Zeit- und Verkehrsverhältnis, wobei sichergestellt wird, dass die Summe 100% ergibt.",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"deviceLimit": "Gerätebeschränkung",
|
"deviceLimit": "Gerätebeschränkung",
|
||||||
"discount": "Rabatt",
|
"discount": "Rabatt",
|
||||||
@@ -30,17 +88,28 @@
|
|||||||
"discountPercent": "Rabattprozentsatz",
|
"discountPercent": "Rabattprozentsatz",
|
||||||
"discount_price": "Rabattpreis",
|
"discount_price": "Rabattpreis",
|
||||||
"duration": "Dauer (Monate)",
|
"duration": "Dauer (Monate)",
|
||||||
"inventory": "Bestand",
|
"groupId": "Abonnementgruppe",
|
||||||
|
"inventory": "Abonnementslimit",
|
||||||
|
"monthlyReset": "Monatliches Zurücksetzen",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"noLimit": "Keine Begrenzung",
|
"noLimit": "Keine Begrenzung",
|
||||||
|
"noReset": "Kein Zurücksetzen",
|
||||||
|
"pricing": "Preisgestaltung",
|
||||||
|
"purchaseWithDiscount": "Abzug erlauben",
|
||||||
|
"purchaseWithDiscountDescription": "Aktivieren oder deaktivieren Sie die Abmeldefunktion. Nach der Aktivierung führt das System die Abzugsverarbeitung gemäß den konfigurierten Regeln und Anteilen durch, und der verbleibende Wert wird dem Guthaben gutgeschrieben.",
|
||||||
"quota": "Kaufbeschränkung",
|
"quota": "Kaufbeschränkung",
|
||||||
|
"renewalReset": "Zurücksetzen bei Erneuerung",
|
||||||
|
"renewalResetDescription": "Zurücksetzungszyklus bei Erneuerung",
|
||||||
"replacement": "Ersatzpreis (pro Mal)",
|
"replacement": "Ersatzpreis (pro Mal)",
|
||||||
|
"resetCycle": "Zurücksetzungszyklus",
|
||||||
|
"resetOn1st": "Zurücksetzen am 1.",
|
||||||
|
"selectResetCycle": "Bitte wählen Sie einen Zurücksetzungszyklus",
|
||||||
"selectSubscribeGroup": "Bitte Abonnementgruppe auswählen",
|
"selectSubscribeGroup": "Bitte Abonnementgruppe auswählen",
|
||||||
"selectUnitTime": "Bitte wählen Sie eine Zeiteinheit",
|
"selectUnitTime": "Bitte wählen Sie eine Zeiteinheit",
|
||||||
"server": "Dienst",
|
"server": "Dienst",
|
||||||
"serverGroup": "Dienstgruppe",
|
"serverGroup": "Dienstgruppe",
|
||||||
"speedLimit": "Geschwindigkeitsbegrenzung (Mbps)",
|
"servers": "Server",
|
||||||
"subscribeGroup": "Abonnementgruppe",
|
"speedLimit": "Geschwindigkeitsbegrenzung ",
|
||||||
"traffic": "Datenvolumen",
|
"traffic": "Datenvolumen",
|
||||||
"unitPrice": "Einheitspreis",
|
"unitPrice": "Einheitspreis",
|
||||||
"unitTime": "Zeiteinheit"
|
"unitTime": "Zeiteinheit"
|
||||||
@@ -70,19 +139,22 @@
|
|||||||
"updateSuccess": "Erfolgreich aktualisiert",
|
"updateSuccess": "Erfolgreich aktualisiert",
|
||||||
"updatedAt": "Aktualisierungszeit"
|
"updatedAt": "Aktualisierungszeit"
|
||||||
},
|
},
|
||||||
"inventory": "Inventar",
|
"inventory": "Abonnementslimit",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"quota": "Kaufbeschränkung/pro Einkauf",
|
"quota": "Kaufbeschränkung/pro Einkauf",
|
||||||
"replacement": "Preis pro Zurücksetzung",
|
"replacement": "Preis pro Zurücksetzung",
|
||||||
"sell": "Verkauf",
|
"sell": "Verkauf",
|
||||||
"show": "Startseite anzeigen",
|
"show": "Startseite anzeigen",
|
||||||
|
"sold": "Abonnentenzahl",
|
||||||
"subscribe": "Abonnieren",
|
"subscribe": "Abonnieren",
|
||||||
"subscribeGroup": "Gruppe abonnieren",
|
"subscribeGroup": "Gruppe abonnieren",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"subscribe": "Abonnieren",
|
"subscribe": "Abonnieren",
|
||||||
|
"subscribeApp": "App-Konfiguration",
|
||||||
|
"subscribeConfig": "Abonnement-Konfiguration",
|
||||||
"subscribeGroup": "Abonnieren Gruppe"
|
"subscribeGroup": "Abonnieren Gruppe"
|
||||||
},
|
},
|
||||||
"traffic": "Verkehr",
|
"traffic": "Verkehr",
|
||||||
"unitPrice": "Preis/Monat",
|
"unitPrice": "Stückpreis",
|
||||||
"updateSuccess": "Aktualisierung erfolgreich"
|
"updateSuccess": "Aktualisierung erfolgreich"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,6 @@
|
|||||||
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
|
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
|
||||||
"saveSuccess": "Erfolgreich gespeichert"
|
"saveSuccess": "Erfolgreich gespeichert"
|
||||||
},
|
},
|
||||||
"email": {
|
|
||||||
"expiration_email_template": "Ablaufbenachrichtigungsvorlage",
|
|
||||||
"expiration_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben.",
|
|
||||||
"inputPlaceholder": "Bitte Inhalt eingeben...",
|
|
||||||
"maintenance_email_template": "Wartungsbenachrichtigungsvorlage",
|
|
||||||
"maintenance_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben.",
|
|
||||||
"saveSuccess": "Die Konfiguration wurde erfolgreich gespeichert.",
|
|
||||||
"sendFailure": "Der Versand der Test-E-Mail ist fehlgeschlagen. Bitte überprüfen Sie die Konfiguration.",
|
|
||||||
"sendSuccess": "Die Test-E-Mail wurde erfolgreich gesendet.",
|
|
||||||
"sendTestEmail": "Test-E-Mail senden",
|
|
||||||
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um zu überprüfen, ob die Konfiguration korrekt ist.",
|
|
||||||
"senderAddress": "Absenderadresse",
|
|
||||||
"senderAddressDescription": "Standard-E-Mail-Adresse, die zum Senden von E-Mails verwendet wird.",
|
|
||||||
"smtpAccount": "SMTP-Konto",
|
|
||||||
"smtpAccountDescription": "E-Mail-Konto zur Authentifizierung.",
|
|
||||||
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
|
|
||||||
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
|
|
||||||
"smtpPassword": "SMTP-Passwort",
|
|
||||||
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
|
|
||||||
"smtpServerAddress": "SMTP-Serveradresse",
|
|
||||||
"smtpServerAddressDescription": "Geben Sie die Serveradresse zum Senden von E-Mails an.",
|
|
||||||
"smtpServerPort": "SMTP-Serverport",
|
|
||||||
"smtpServerPortDescription": "Geben Sie die Portnummer für die Verbindung zum SMTP-Server an.",
|
|
||||||
"verify_email_template": "Verifizierungsmailvorlage",
|
|
||||||
"verify_email_templateDescription": "Die Variablennamen {after}.Variable{before} im Template werden durch tatsächliche Daten ersetzt. Bitte stellen Sie sicher, dass diese Variablen erhalten bleiben."
|
|
||||||
},
|
|
||||||
"invite": {
|
"invite": {
|
||||||
"commissionFirstTimeOnly": "Nur Provision beim ersten Kauf",
|
"commissionFirstTimeOnly": "Nur Provision beim ersten Kauf",
|
||||||
"commissionFirstTimeOnlyDescription": "Nach Aktivierung wird die Provision nur bei der ersten Zahlung des Eingeladenen generiert. Sie können in der Benutzerverwaltung einen individuellen Benutzer konfigurieren.",
|
"commissionFirstTimeOnlyDescription": "Nach Aktivierung wird die Provision nur bei der ersten Zahlung des Eingeladenen generiert. Sie können in der Benutzerverwaltung einen individuellen Benutzer konfigurieren.",
|
||||||
@@ -44,36 +18,6 @@
|
|||||||
"inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.",
|
"inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.",
|
||||||
"saveSuccess": "Erfolgreich gespeichert"
|
"saveSuccess": "Erfolgreich gespeichert"
|
||||||
},
|
},
|
||||||
"node": {
|
|
||||||
"communicationKey": "Kommunikationsschlüssel",
|
|
||||||
"communicationKeyDescription": "Schlüssel für die Kommunikation des Knotens, um sicherzustellen, dass Daten nicht von anderen abgefangen werden",
|
|
||||||
"inputPlaceholder": "Bitte eingeben",
|
|
||||||
"nodePullInterval": "Knotenabfrageintervall",
|
|
||||||
"nodePullIntervalDescription": "Häufigkeit, mit der der Knoten Daten vom Panel abruft (Sekunden)",
|
|
||||||
"nodePushInterval": "Knotenübertragungsintervall",
|
|
||||||
"nodePushIntervalDescription": "Häufigkeit, mit der der Knoten Daten an das Panel überträgt",
|
|
||||||
"saveSuccess": "Erfolgreich gespeichert"
|
|
||||||
},
|
|
||||||
"register": {
|
|
||||||
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
|
|
||||||
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit den in der Liste enthaltenen Suffixen registrieren",
|
|
||||||
"emailVerification": "E-Mail-Verifizierung",
|
|
||||||
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail verifizieren",
|
|
||||||
"ipRegistrationLimit": "IP-Registrierungsbeschränkung",
|
|
||||||
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die den Regelanforderungen entsprechen, in der Registrierung eingeschränkt. Bitte beachten Sie, dass die Bestimmung der IP aufgrund von CDN oder Frontend-Proxy zu Problemen führen kann",
|
|
||||||
"penaltyTime": "Strafzeit (Minuten)",
|
|
||||||
"penaltyTimeDescription": "Benutzer müssen die Strafzeit abwarten, bevor sie sich erneut registrieren können",
|
|
||||||
"registrationLimitCount": "Registrierungsbeschränkungsanzahl",
|
|
||||||
"registrationLimitCountDescription": "Nach Erreichen der Registrierungsbeschränkung wird die Strafe aktiviert",
|
|
||||||
"saveSuccess": "Erfolgreich gespeichert",
|
|
||||||
"stopNewUserRegistration": "Registrierung neuer Benutzer stoppen",
|
|
||||||
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
|
|
||||||
"trialRegistration": "Testregistrierung",
|
|
||||||
"trialRegistrationDescription": "Testregistrierung aktivieren, bitte zuerst Testpaket und -dauer ändern",
|
|
||||||
"whitelistSuffixes": "Whitelist-Suffixe",
|
|
||||||
"whitelistSuffixesDescription": "Für die E-Mail-Verifizierung bei der Registrierung, ein Suffix pro Zeile",
|
|
||||||
"whitelistSuffixesPlaceholder": "Bitte geben Sie E-Mail-Suffixe ein, eines pro Zeile"
|
|
||||||
},
|
|
||||||
"site": {
|
"site": {
|
||||||
"logo": "LOGO",
|
"logo": "LOGO",
|
||||||
"logoDescription": "Position zur Anzeige des LOGOs",
|
"logoDescription": "Position zur Anzeige des LOGOs",
|
||||||
@@ -89,37 +33,10 @@
|
|||||||
"siteNameDescription": "Position zur Anzeige des Seitennamens",
|
"siteNameDescription": "Position zur Anzeige des Seitennamens",
|
||||||
"siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein"
|
"siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein"
|
||||||
},
|
},
|
||||||
"subscription": {
|
|
||||||
"add": "Hinzufügen",
|
|
||||||
"app": "Anwendung",
|
|
||||||
"appDescription": "Konfigurieren Sie die Download-Adresse und Importregeln der App. Vergessen Sie nicht, nach Änderungen zu speichern.",
|
|
||||||
"appDownloadURL": "App-Download-Adresse",
|
|
||||||
"appIcon": "App-Symbol",
|
|
||||||
"appName": "App-Name",
|
|
||||||
"delete": "Löschen",
|
|
||||||
"reset": "Zurücksetzen",
|
|
||||||
"save": "Speichern",
|
|
||||||
"saveSuccess": "Erfolgreich gespeichert",
|
|
||||||
"singleSubscriptionMode": "Einzelabonnement-Modus",
|
|
||||||
"singleSubscriptionModeDescription": "Wenn aktiviert, werden alle Benutzerpakete in Guthaben umgewandelt.",
|
|
||||||
"subscriptionDomain": "Abonnement-Domain",
|
|
||||||
"subscriptionDomainDescription": "Für Abonnements, leer lassen, um die Site-Domain zu verwenden",
|
|
||||||
"subscriptionDomainPlaceholder": "Bitte geben Sie die Abonnement-Domain ein, bei mehreren Domains bitte eine pro Zeile",
|
|
||||||
"subscriptionPath": "Abonnement-Pfad",
|
|
||||||
"subscriptionPathDescription": "Für Abonnements, bitte starten Sie das System nach Änderungen neu, um die beste Leistung zu gewährleisten",
|
|
||||||
"subscriptionPathPlaceholder": "Bitte eingeben",
|
|
||||||
"subscriptionProtocol": "Abonnement-Protokoll",
|
|
||||||
"wildcardResolution": "Wildcard-Auflösung",
|
|
||||||
"wildcardResolutionDescription": "Für Abonnements"
|
|
||||||
},
|
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"currency": "Währung",
|
"currency": "Währung",
|
||||||
"email": "E-Mail",
|
|
||||||
"invite": "Einladen",
|
"invite": "Einladen",
|
||||||
"node": "Knoten",
|
|
||||||
"register": "Registrieren",
|
|
||||||
"site": "Seite",
|
"site": "Seite",
|
||||||
"subscription": "Abonnement",
|
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"tos": "Nutzungsbedingungen",
|
"tos": "Nutzungsbedingungen",
|
||||||
"verify": "Verifizieren"
|
"verify": "Verifizieren"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Bot-ID",
|
||||||
|
"clientIdDescription": "Telegram-Bot-ID, die Sie von @BotFather erhalten können",
|
||||||
|
"clientSecret": "Bot-Token",
|
||||||
|
"clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können",
|
||||||
|
"enable": "Aktivieren",
|
||||||
|
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
|
||||||
|
"redirectUri": "Weiterleitungs-URL",
|
||||||
|
"redirectUriDescription": "Die URL, zu der Telegram nach erfolgreicher Authentifizierung weiterleitet",
|
||||||
|
"saveFailed": "Speichern fehlgeschlagen",
|
||||||
|
"saveSuccess": "Erfolgreich gespeichert"
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"balance": "Kontostand",
|
"balance": "Kontostand",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"commission": "Provision",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
"confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?",
|
"confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
@@ -16,10 +17,16 @@
|
|||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"enable": "Aktivieren",
|
"enable": "Aktivieren",
|
||||||
"form": {
|
"form": {
|
||||||
|
"areaCode": "Vorwahl",
|
||||||
|
"areaCodePlaceholder": "Vorwahl eingeben",
|
||||||
"balance": "Kontostand",
|
"balance": "Kontostand",
|
||||||
"balancePlaceholder": "Kontostand",
|
"balancePlaceholder": "Kontostand",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
|
"commission": "Provision",
|
||||||
|
"commissionPlaceholder": "Geben Sie die Provision ein",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
|
"giftAmount": "Geschenkbetrag",
|
||||||
|
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
|
||||||
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
"invalidEmailFormat": "Ungültiges E-Mail-Format",
|
||||||
"inviteCode": "Einladungscode",
|
"inviteCode": "Einladungscode",
|
||||||
"inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)",
|
"inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)",
|
||||||
@@ -28,11 +35,16 @@
|
|||||||
"passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)",
|
"passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)",
|
||||||
"refererId": "Empfehler-ID",
|
"refererId": "Empfehler-ID",
|
||||||
"refererIdPlaceholder": "Bitte Empfehler-ID eingeben",
|
"refererIdPlaceholder": "Bitte Empfehler-ID eingeben",
|
||||||
|
"telephone": "Telefonnummer",
|
||||||
|
"telephonePlaceholder": "Telefonnummer eingeben",
|
||||||
"userEmail": "Benutzer-E-Mail",
|
"userEmail": "Benutzer-E-Mail",
|
||||||
"userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben"
|
"userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben"
|
||||||
},
|
},
|
||||||
|
"giftAmount": "Geschenkbetrag",
|
||||||
|
"inviteCode": "Einladungscode",
|
||||||
"referer": "Empfehler",
|
"referer": "Empfehler",
|
||||||
|
"telephone": "Telefonnummer",
|
||||||
"updateSuccess": "Aktualisierung erfolgreich",
|
"updateSuccess": "Aktualisierung erfolgreich",
|
||||||
"userList": "Benutzerliste",
|
"userList": "Benutzerliste",
|
||||||
"userName": "Benutzer-E-Mail"
|
"userName": "Benutzerkennung"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Service ID",
|
||||||
|
"clientIdDescription": "Apple Service ID, you can get it from Apple Developer Portal",
|
||||||
|
"clientSecret": "Private Key",
|
||||||
|
"clientSecretDescription": "The private key content (.p8 file) used for authentication with Apple",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enableDescription": "After enabling, users can sign in with their Apple ID",
|
||||||
|
"keyId": "Key ID",
|
||||||
|
"keyIdDescription": "The ID of your private key from Apple Developer Portal",
|
||||||
|
"redirectUri": "Return URL",
|
||||||
|
"redirectUriDescription": "The URL that Apple will redirect to after successful authentication",
|
||||||
|
"saveFailed": "Save failed",
|
||||||
|
"saveSuccess": "Save successful",
|
||||||
|
"teamId": "Team ID",
|
||||||
|
"teamIdDescription": "Apple Developer Team ID"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"ipRegistrationLimit": "IP Registration Limit",
|
||||||
|
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies",
|
||||||
|
"penaltyTime": "Penalty Time (minutes)",
|
||||||
|
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
|
||||||
|
"registrationLimitCount": "Registration Limit Count",
|
||||||
|
"registrationLimitCountDescription": "Enable penalty after reaching registration limit",
|
||||||
|
"saveSuccess": "Save Successful",
|
||||||
|
"stopNewUserRegistration": "Stop New User Registration",
|
||||||
|
"stopNewUserRegistrationDescription": "When enabled, no one can register",
|
||||||
|
"trialRegistration": "Trial Registration",
|
||||||
|
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first"
|
||||||
|
}
|
||||||
@@ -4,36 +4,48 @@
|
|||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"description": "Please enter your email to continue logging in or registering.",
|
"description": "Please enter your email to continue logging in or registering.",
|
||||||
"email": "Please enter a valid email address.",
|
"email": "Please enter a valid email address.",
|
||||||
"title": "Login/Register",
|
"title": "Login/Register"
|
||||||
"whitelist": "The email domain is not in the allowed whitelist."
|
|
||||||
},
|
},
|
||||||
|
"get": "Get",
|
||||||
"login": {
|
"login": {
|
||||||
|
"codeLogin": "Verification Code Login",
|
||||||
"description": "Please enter your account information to log in.",
|
"description": "Please enter your account information to log in.",
|
||||||
|
"email": "Please enter a valid email address.",
|
||||||
"forgotPassword": "Forgot Password?",
|
"forgotPassword": "Forgot Password?",
|
||||||
|
"passwordLogin": "Password Login",
|
||||||
|
"registerAccount": "Register Account",
|
||||||
"success": "Login successful!",
|
"success": "Login successful!",
|
||||||
"switchAccount": "Register/Switch Account",
|
|
||||||
"title": "Login"
|
"title": "Login"
|
||||||
},
|
},
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
|
"methods": {
|
||||||
|
"email": "Email",
|
||||||
|
"sms": "Telephone"
|
||||||
|
},
|
||||||
"orWithEmail": "Or with Email",
|
"orWithEmail": "Or with Email",
|
||||||
"register": {
|
"register": {
|
||||||
"description": "Create a new account, fill in your information to register.",
|
"description": "Create a new account, fill in your information to register.",
|
||||||
|
"email": "Please enter a valid email address.",
|
||||||
"existingAccount": "Already have an account?",
|
"existingAccount": "Already have an account?",
|
||||||
"get": "Get",
|
"get": "Get",
|
||||||
"invite": "Invitation code",
|
"invite": "Invitation code",
|
||||||
"message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**",
|
"message": "#### Dear User, Hello!\n\nThank you for your attention and support. Due to adjustments in site operation strategy, we have closed the new user registration function. During this period, existing users will not be affected.\n\nWe are committed to providing you with better service and experience, so we will conduct comprehensive system optimization and feature upgrades during the registration closure. In the future, we will welcome you with better content and services.\n\nPlease follow our website and social media platforms to get the latest updates and notifications. Thank you for your understanding and support.\n\nIf you have any questions or need assistance, please feel free to contact our customer service team.\n\n**Thank you again for your support and understanding.**",
|
||||||
"passwordMismatch": "Passwords do not match",
|
"passwordMismatch": "Passwords do not match",
|
||||||
"success": "Registration successful, automatically logged in!",
|
"success": "Registration successful, automatically logged in!",
|
||||||
"switchToLogin": "Login/Reset Email",
|
"switchToLogin": "Login",
|
||||||
"title": "Register"
|
"title": "Register",
|
||||||
|
"whitelist": "The email domain is not in the allowed whitelist."
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
"description": "Please enter your email address to reset your password.",
|
"description": "Please enter your email address to reset your password.",
|
||||||
|
"email": "Please enter a valid email address.",
|
||||||
"existingAccount": "Already have an account?",
|
"existingAccount": "Already have an account?",
|
||||||
"get": "Get",
|
"get": "Get",
|
||||||
"success": "Password reset successful, automatically switched to login!",
|
"success": "Password reset successful, automatically switched to login!",
|
||||||
"switchToLogin": "Login/Register",
|
"switchToLogin": "Login/Register",
|
||||||
"title": "Reset Password"
|
"title": "Reset Password"
|
||||||
},
|
},
|
||||||
"tos": "Terms of Service"
|
"tos": "Terms of Service",
|
||||||
|
"verifyAccount": "Account Verification",
|
||||||
|
"verifyAccountDesc": " Please verify your account to continue."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"20004": "The user is disabled, please contact customer service if you have questions.",
|
"20004": "The user is disabled, please contact customer service if you have questions.",
|
||||||
"20005": "Insufficient balance, please recharge and try again.",
|
"20005": "Insufficient balance, please recharge and try again.",
|
||||||
"20006": "The registration function is temporarily unavailable, please try again later.",
|
"20006": "The registration function is temporarily unavailable, please try again later.",
|
||||||
|
"20008": "User information is incorrect, please check and try again.",
|
||||||
"30001": "The node already exists, please do not add it again.",
|
"30001": "The node already exists, please do not add it again.",
|
||||||
"30002": "Related node not found, please check and try again.",
|
"30002": "Related node not found, please check and try again.",
|
||||||
"30003": "Group already exists, please try using a different name.",
|
"30003": "Group already exists, please try using a different name.",
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
"50002": "The coupon has been used, cannot be used again.",
|
"50002": "The coupon has been used, cannot be used again.",
|
||||||
"60001": "Subscription has expired, please renew before using.",
|
"60001": "Subscription has expired, please renew before using.",
|
||||||
"60002": "Unable to use the subscription at the moment, please try again later.",
|
"60002": "Unable to use the subscription at the moment, please try again later.",
|
||||||
|
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
||||||
|
"60004": "Unable to delete at the moment as the subscription has active users.",
|
||||||
"70001": "Incorrect verification code, please re-enter.",
|
"70001": "Incorrect verification code, please re-enter.",
|
||||||
"80001": "Task was not successfully queued, please try again later.",
|
"80001": "Task was not successfully queued, please try again later.",
|
||||||
"90001": "Please disable DEBUG mode and try again.",
|
"90001": "Please disable DEBUG mode and try again.",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"emailBasicConfig": "Basic Configuration",
|
||||||
|
"emailBasicConfigDescription": "Configure SMTP server settings and email verification options",
|
||||||
|
"emailLogs": "Email Logs",
|
||||||
|
"emailLogsDescription": "View the history of sent emails and their status",
|
||||||
|
"emailSuffixWhitelist": "Email Suffix Whitelist",
|
||||||
|
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
|
||||||
|
"emailTemplate": "Email Template",
|
||||||
|
"emailVerification": "Email Verification",
|
||||||
|
"emailVerificationDescription": "When enabled, users will need to verify their email",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enableDescription": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
|
||||||
|
"expiration_email_template": "Expiration Notice Template",
|
||||||
|
"expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
|
||||||
|
"failed": "Failed",
|
||||||
|
"inputPlaceholder": "Enter value...",
|
||||||
|
"maintenance_email_template": "Maintenance Notice Template",
|
||||||
|
"maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
|
||||||
|
"recipient": "Recipient",
|
||||||
|
"saveFailed": "Failed to save configuration",
|
||||||
|
"saveSuccess": "Configuration saved successfully.",
|
||||||
|
"sendFailure": "Failed to send test email, please check the configuration.",
|
||||||
|
"sendSuccess": "Test email sent successfully.",
|
||||||
|
"sendTestEmail": "Send Test Email",
|
||||||
|
"sendTestEmailDescription": "Send a test email to verify the configuration.",
|
||||||
|
"senderAddress": "Sender Address",
|
||||||
|
"senderAddressDescription": "The default email address used for sending emails.",
|
||||||
|
"sent": "Sent",
|
||||||
|
"sentAt": "Sent At",
|
||||||
|
"smtpAccount": "SMTP Account",
|
||||||
|
"smtpAccountDescription": "The email account used for authentication.",
|
||||||
|
"smtpEncryptionMethod": "SMTP Encryption Method",
|
||||||
|
"smtpEncryptionMethodDescription": "Choose whether to enable SSL/TLS encryption.",
|
||||||
|
"smtpPassword": "SMTP Password",
|
||||||
|
"smtpPasswordDescription": "The password for the SMTP account.",
|
||||||
|
"smtpServerAddress": "SMTP Server Address",
|
||||||
|
"smtpServerAddressDescription": "Specify the server address used for sending emails.",
|
||||||
|
"smtpServerPort": "SMTP Server Port",
|
||||||
|
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
|
||||||
|
"status": "Status",
|
||||||
|
"subject": "Subject",
|
||||||
|
"verify_email_template": "Verification Email Template",
|
||||||
|
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
|
||||||
|
"whitelistSuffixes": "Whitelist Suffixes",
|
||||||
|
"whitelistSuffixesDescription": "Used for email verification during registration; one per line",
|
||||||
|
"whitelistSuffixesPlaceholder": "Enter email suffixes, one per line"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Client ID",
|
||||||
|
"clientIdDescription": "Facebook App ID from Facebook Developers Console",
|
||||||
|
"clientSecret": "Client Secret",
|
||||||
|
"clientSecretDescription": "Facebook App Secret from Facebook Developers Console",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enableDescription": "After enabling, users can sign in with their Facebook account",
|
||||||
|
"redirectUri": "Authorized redirect URI",
|
||||||
|
"redirectUriDescription": "The URL to which the user will be redirected after Facebook authentication",
|
||||||
|
"saveFailed": "Save failed",
|
||||||
|
"saveSuccess": "Save successful"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "GitHub Client ID",
|
||||||
|
"clientIdDescription": "The client ID from your GitHub OAuth application settings",
|
||||||
|
"clientSecret": "GitHub Client Secret",
|
||||||
|
"clientSecretDescription": "The client secret from your GitHub OAuth application settings",
|
||||||
|
"enable": "Enable GitHub Authentication",
|
||||||
|
"enableDescription": "Allow users to sign in with their GitHub accounts",
|
||||||
|
"redirectUri": "Authorization callback URL",
|
||||||
|
"redirectUriDescription": "The URL in your application where users will be redirected after GitHub authentication",
|
||||||
|
"saveFailed": "Failed to save GitHub settings",
|
||||||
|
"saveSuccess": "GitHub settings saved successfully"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clientId": "Client ID",
|
||||||
|
"clientIdDescription": "Google OAuth 2.0 Client ID from Google Cloud Console",
|
||||||
|
"clientSecret": "Client Secret",
|
||||||
|
"clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enableDescription": "After enabling, users can sign in with their Google account",
|
||||||
|
"redirectUri": "Authorized redirect URI",
|
||||||
|
"redirectUriDescription": "The URL to which the user will be redirected after Google authentication",
|
||||||
|
"saveFailed": "Save failed",
|
||||||
|
"saveSuccess": "Save successful"
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
{
|
{
|
||||||
"Announcement Management": "Announcement Management",
|
"Announcement Management": "Announcement Management",
|
||||||
|
"Apple": "Apple ID",
|
||||||
|
"Auth Control": "Auth Control",
|
||||||
"Coupon Management": "Coupon Management",
|
"Coupon Management": "Coupon Management",
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
"Document Management": "Document Management",
|
"Document Management": "Document Management",
|
||||||
|
"Email": "Email",
|
||||||
|
"Facebook": "Facebook",
|
||||||
"Finance": "Finance",
|
"Finance": "Finance",
|
||||||
|
"General": "General",
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Google": "Google",
|
||||||
"Order Management": "Order Management",
|
"Order Management": "Order Management",
|
||||||
"Payment Config": "Payment Config",
|
"Payment Config": "Payment Config",
|
||||||
|
"Phone Number": "Phone Number",
|
||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Server Management": "Server Management",
|
"Server Management": "Server Management",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Subscribe Management": "Subscribe Management",
|
"Subscribe Management": "Subscribe Management",
|
||||||
"System Config": "System Config",
|
"System Config": "System Config",
|
||||||
"System Tool": "System Tool",
|
"System Tool": "System Tool",
|
||||||
|
"Telegram": "Telegram",
|
||||||
"Ticket Management": "Ticket Management",
|
"Ticket Management": "Ticket Management",
|
||||||
|
"Twitter": "Twitter",
|
||||||
"User": "User",
|
"User": "User",
|
||||||
"User Management": "User Management"
|
"User Management": "User Management"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"SmsList": "SMS Log List",
|
||||||
|
"applyPlatform": "Apply Platform",
|
||||||
|
"areaCode": "Area Code",
|
||||||
|
"content": "Content",
|
||||||
|
"createdAt": "Send Time",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enableTip": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
|
||||||
|
"expireTime": "Expire Time",
|
||||||
|
"expireTimeTip": "SMS verification code validity period (seconds)",
|
||||||
|
"interval": "Interval",
|
||||||
|
"intervalTip": "SMS verification code sending interval for the same phone number (seconds), 0 means no limit",
|
||||||
|
"limit": "Daily Limit",
|
||||||
|
"limitTip": "Daily SMS verification code sending limit for the same phone number, 0 means no limit",
|
||||||
|
"logs": "Logs",
|
||||||
|
"placeholders": {
|
||||||
|
"expireTime": "Enter expire time, default is 300",
|
||||||
|
"interval": "Enter interval time, default is 60",
|
||||||
|
"limit": "Enter daily limit, default is 20",
|
||||||
|
"template": "Your verification code is {code}, valid for 5 minutes",
|
||||||
|
"templateCode": "Enter template code"
|
||||||
|
},
|
||||||
|
"platform": "SMS Platform",
|
||||||
|
"platformConfigTip": "Please fill in the provided {key} configuration",
|
||||||
|
"platformTip": "Please select SMS platform",
|
||||||
|
"search": "Search phone number",
|
||||||
|
"sendFailed": "Send Failed",
|
||||||
|
"sendSuccess": "Send Success",
|
||||||
|
"settings": "Settings",
|
||||||
|
"status": "Status",
|
||||||
|
"telephone": "Phone Number",
|
||||||
|
"template": "SMS Template",
|
||||||
|
"templateCode": "Template Code",
|
||||||
|
"templateParam": "Template Parameter",
|
||||||
|
"templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work",
|
||||||
|
"testSms": "Send Test SMS",
|
||||||
|
"testSmsContent": "This is a test message",
|
||||||
|
"testSmsPhone": "Enter phone number",
|
||||||
|
"testSmsTip": "Send a test SMS to verify your configuration",
|
||||||
|
"updateSuccess": "Update Success"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user