🐛 fix: Add DynamicMultiplier component for managing node multipliers and update ServersPage layout

This commit is contained in:
web 2025-09-24 03:00:37 -07:00
parent 47f66030db
commit bb6671c14f
6 changed files with 163 additions and 116 deletions

View File

@ -1,57 +1,52 @@
<a name="readme-top"></a> <a name="readme-top"></a>
# Changelog # Changelog
## [1.4.8](https://github.com/perfect-panel/ppanel-web/compare/v1.4.7...v1.4.8) (2025-09-23) ## [1.4.8](https://github.com/perfect-panel/ppanel-web/compare/v1.4.7...v1.4.8) (2025-09-23)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* Rename 'server_id' to 'protocol' in NodesPage and clean up unused imports and code in ServerConfig ([70b3484](https://github.com/perfect-panel/ppanel-web/commit/70b3484)) - Rename 'server_id' to 'protocol' in NodesPage and clean up unused imports and code in ServerConfig ([70b3484](https://github.com/perfect-panel/ppanel-web/commit/70b3484))
* Update announcement page to display timeline of announcements with Markdown content ([3c036eb](https://github.com/perfect-panel/ppanel-web/commit/3c036eb)) - Update announcement page to display timeline of announcements with Markdown content ([3c036eb](https://github.com/perfect-panel/ppanel-web/commit/3c036eb))
* Update Empty component to support border prop and adjust usage in various pages ([ce9ab89](https://github.com/perfect-panel/ppanel-web/commit/ce9ab89)) - Update Empty component to support border prop and adjust usage in various pages ([ce9ab89](https://github.com/perfect-panel/ppanel-web/commit/ce9ab89))
## [1.4.7](https://github.com/perfect-panel/ppanel-web/compare/v1.4.6...v1.4.7) (2025-09-23) ## [1.4.7](https://github.com/perfect-panel/ppanel-web/compare/v1.4.6...v1.4.7) (2025-09-23)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* Add unique key to ProTable for improved rendering with user ID filters ([2bff15f](https://github.com/perfect-panel/ppanel-web/commit/2bff15f)) - Add unique key to ProTable for improved rendering with user ID filters ([2bff15f](https://github.com/perfect-panel/ppanel-web/commit/2bff15f))
* Adjust layout spacing and chart aspect ratio in ServerConfig component ([05a61d8](https://github.com/perfect-panel/ppanel-web/commit/05a61d8)) - Adjust layout spacing and chart aspect ratio in ServerConfig component ([05a61d8](https://github.com/perfect-panel/ppanel-web/commit/05a61d8))
* Refactor server ID cell rendering for improved readability and consistency ([0345b7c](https://github.com/perfect-panel/ppanel-web/commit/0345b7c)) - Refactor server ID cell rendering for improved readability and consistency ([0345b7c](https://github.com/perfect-panel/ppanel-web/commit/0345b7c))
* Update announcement page to format creation date and enhance content display ([8445e30](https://github.com/perfect-panel/ppanel-web/commit/8445e30)) - Update announcement page to format creation date and enhance content display ([8445e30](https://github.com/perfect-panel/ppanel-web/commit/8445e30))
* Update OnlineUsersCell to display user count with icon instead of badge ([7a4ebdf](https://github.com/perfect-panel/ppanel-web/commit/7a4ebdf)) - Update OnlineUsersCell to display user count with icon instead of badge ([7a4ebdf](https://github.com/perfect-panel/ppanel-web/commit/7a4ebdf))
* Update subscribe name fallback to return '--' instead of 'Unknown' ([0a07d25](https://github.com/perfect-panel/ppanel-web/commit/0a07d25)) - Update subscribe name fallback to return '--' instead of 'Unknown' ([0a07d25](https://github.com/perfect-panel/ppanel-web/commit/0a07d25))
## [1.4.6](https://github.com/perfect-panel/ppanel-web/compare/v1.4.5...v1.4.6) (2025-09-17) ## [1.4.6](https://github.com/perfect-panel/ppanel-web/compare/v1.4.5...v1.4.6) (2025-09-17)
### 🎫 Chores ### 🎫 Chores
* Merge branch 'main' into develop ([41f06bf](https://github.com/perfect-panel/ppanel-web/commit/41f06bf)) - Merge branch 'main' into develop ([41f06bf](https://github.com/perfect-panel/ppanel-web/commit/41f06bf))
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* Add loaded state to node, server, and subscribe stores for better loading management ([13dce0c](https://github.com/perfect-panel/ppanel-web/commit/13dce0c)) - Add loaded state to node, server, and subscribe stores for better loading management ([13dce0c](https://github.com/perfect-panel/ppanel-web/commit/13dce0c))
* Removed node metadata fields in subscription schema to simplify structure ([0cadd83](https://github.com/perfect-panel/ppanel-web/commit/0cadd83)) - Removed node metadata fields in subscription schema to simplify structure ([0cadd83](https://github.com/perfect-panel/ppanel-web/commit/0cadd83))
## [1.4.5](https://github.com/perfect-panel/ppanel-web/compare/v1.4.4...v1.4.5) (2025-09-17) ## [1.4.5](https://github.com/perfect-panel/ppanel-web/compare/v1.4.4...v1.4.5) (2025-09-17)
### ♻ Code Refactoring ### ♻ Code Refactoring
* Replace useQuery with Zustand store for subscription and node data management ([c6dd0b6](https://github.com/perfect-panel/ppanel-web/commit/c6dd0b6)) - Replace useQuery with Zustand store for subscription and node data management ([c6dd0b6](https://github.com/perfect-panel/ppanel-web/commit/c6dd0b6))
* Simplify TemplatePreview component structure by consolidating Sheet and Button elements ([1b715c5](https://github.com/perfect-panel/ppanel-web/commit/1b715c5)) - Simplify TemplatePreview component structure by consolidating Sheet and Button elements ([1b715c5](https://github.com/perfect-panel/ppanel-web/commit/1b715c5))
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* Add showLineNumbers prop handling in MonacoEditor for improved placeholder positioning ([bd67ece](https://github.com/perfect-panel/ppanel-web/commit/bd67ece)) - Add showLineNumbers prop handling in MonacoEditor for improved placeholder positioning ([bd67ece](https://github.com/perfect-panel/ppanel-web/commit/bd67ece))
* Add fetchTags method to NodesPage and ensure tags are fetched alongside nodes ([a3c5e31](https://github.com/perfect-panel/ppanel-web/commit/a3c5e31)) - Add fetchTags method to NodesPage and ensure tags are fetched alongside nodes ([a3c5e31](https://github.com/perfect-panel/ppanel-web/commit/a3c5e31))
* Add NEXT_PUBLIC_HIDDEN_TUTORIAL_DOCUMENT to control tutorial visibility and update page query accordingly ([e94405d](https://github.com/perfect-panel/ppanel-web/commit/e94405d)) - Add NEXT_PUBLIC_HIDDEN_TUTORIAL_DOCUMENT to control tutorial visibility and update page query accordingly ([e94405d](https://github.com/perfect-panel/ppanel-web/commit/e94405d))
* Add subscribeSchema for subscription management with detailed proxy and user information ([49b3dcc](https://github.com/perfect-panel/ppanel-web/commit/49b3dcc)) - Add subscribeSchema for subscription management with detailed proxy and user information ([49b3dcc](https://github.com/perfect-panel/ppanel-web/commit/49b3dcc))
* Enhance server ID display in ServerTrafficLogPage with badges and server ratio ([6dfac27](https://github.com/perfect-panel/ppanel-web/commit/6dfac27)) - Enhance server ID display in ServerTrafficLogPage with badges and server ratio ([6dfac27](https://github.com/perfect-panel/ppanel-web/commit/6dfac27))
* Update platform handling in Content component to ensure available platforms are correctly filtered and displayed ([1dde708](https://github.com/perfect-panel/ppanel-web/commit/1dde708)) - Update platform handling in Content component to ensure available platforms are correctly filtered and displayed ([1dde708](https://github.com/perfect-panel/ppanel-web/commit/1dde708))
<a name="readme-top"></a> <a name="readme-top"></a>

View File

@ -0,0 +1,115 @@
'use client';
import { getNodeMultiplier, setNodeMultiplier } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent } from '@workspace/ui/components/card';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
export default function DynamicMultiplier() {
const t = useTranslations('servers');
const [open, setOpen] = useState(false);
const [timeSlots, setTimeSlots] = useState<API.TimePeriod[]>([]);
const { data: periodsResp, refetch: refetchPeriods } = useQuery({
queryKey: ['getNodeMultiplier'],
queryFn: async () => {
const { data } = await getNodeMultiplier();
return (data.data?.periods || []) as API.TimePeriod[];
},
enabled: open,
});
useEffect(() => {
if (periodsResp) {
setTimeSlots(periodsResp);
}
}, [periodsResp]);
async function savePeriods() {
try {
await setNodeMultiplier({ periods: timeSlots });
await refetchPeriods();
toast.success(t('config.saveSuccess'));
setOpen(false);
} catch (error) {
toast.error(t('config.saveError'));
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Card>
<CardContent className='p-4'>
<div className='flex cursor-pointer items-center justify-between'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:clock-time-eight' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('config.dynamicMultiplier')}</p>
<p className='text-muted-foreground truncate text-sm'>
{t('config.dynamicMultiplierDescription')}
</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</CardContent>
</Card>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('config.dynamicMultiplier')}</SheetTitle>
<SheetDescription>{t('config.dynamicMultiplierDescription')}</SheetDescription>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-60px-env(safe-area-inset-top))] px-6'>
<div className='space-y-4 pt-4'>
<ArrayInput<API.TimePeriod>
fields={[
{ name: 'start_time', prefix: t('config.startTime'), type: 'time' },
{ name: 'end_time', prefix: t('config.endTime'), type: 'time' },
{
name: 'multiplier',
prefix: t('config.multiplier'),
type: 'number',
placeholder: '0',
},
]}
value={timeSlots}
onChange={setTimeSlots}
/>
</div>
</ScrollArea>
<SheetFooter className='flex-row justify-between pt-3'>
<Button variant='outline' onClick={() => setTimeSlots(periodsResp || [])}>
{t('config.reset')}
</Button>
<div className='flex gap-2'>
<Button variant='outline' onClick={() => setOpen(false)}>
{t('config.actions.cancel')}
</Button>
<Button onClick={savePeriods}>{t('config.actions.save')}</Button>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -15,12 +15,12 @@ import { useServer } from '@/store/server';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge'; import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Card, CardContent } from '@workspace/ui/components/card';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { cn } from '@workspace/ui/lib/utils'; import { cn } from '@workspace/ui/lib/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 DynamicMultiplier from './dynamic-multiplier';
import OnlineUsersCell from './online-users-cell'; import OnlineUsersCell from './online-users-cell';
import ServerConfig from './server-config'; import ServerConfig from './server-config';
import ServerForm from './server-form'; import ServerForm from './server-form';
@ -95,11 +95,10 @@ export default function ServersPage() {
return ( return (
<div className='space-y-4'> <div className='space-y-4'>
<Card> <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<CardContent className='p-4'> <DynamicMultiplier />
<ServerConfig /> <ServerConfig />
</CardContent> </div>
</Card>
<ProTable<API.Server, { search: string }> <ProTable<API.Server, { search: string }>
action={ref} action={ref}
header={{ header={{

View File

@ -1,15 +1,10 @@
'use client'; 'use client';
import { import { getNodeConfig, updateNodeConfig } from '@/services/admin/system';
getNodeConfig,
getNodeMultiplier,
setNodeMultiplier,
updateNodeConfig,
} from '@/services/admin/system';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
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 } from '@workspace/ui/components/card';
import { import {
Form, Form,
FormControl, FormControl,
@ -19,7 +14,6 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@workspace/ui/components/form'; } from '@workspace/ui/components/form';
import { Label } from '@workspace/ui/components/label';
import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { import {
Sheet, Sheet,
@ -29,7 +23,6 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from '@workspace/ui/components/sheet'; } from '@workspace/ui/components/sheet';
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 { Icon } from '@workspace/ui/custom-components/icon';
import { DicesIcon } from 'lucide-react'; import { DicesIcon } from 'lucide-react';
@ -51,7 +44,6 @@ export default function ServerConfig() {
const t = useTranslations('servers'); const t = useTranslations('servers');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [timeSlots, setTimeSlots] = useState<API.TimePeriod[]>([]);
const { data: cfgResp, refetch: refetchCfg } = useQuery({ const { data: cfgResp, refetch: refetchCfg } = useQuery({
queryKey: ['getNodeConfig'], queryKey: ['getNodeConfig'],
@ -62,15 +54,6 @@ export default function ServerConfig() {
enabled: open, enabled: open,
}); });
const { data: periodsResp, refetch: refetchPeriods } = useQuery({
queryKey: ['getNodeMultiplier'],
queryFn: async () => {
const { data } = await getNodeMultiplier();
return (data.data?.periods || []) as API.TimePeriod[];
},
enabled: open,
});
const form = useForm<NodeConfigFormData>({ const form = useForm<NodeConfigFormData>({
resolver: zodResolver(nodeConfigSchema), resolver: zodResolver(nodeConfigSchema),
defaultValues: { defaultValues: {
@ -90,12 +73,6 @@ export default function ServerConfig() {
} }
}, [cfgResp, form]); }, [cfgResp, form]);
useEffect(() => {
if (periodsResp) {
setTimeSlots(periodsResp);
}
}, [periodsResp]);
async function onSubmit(values: NodeConfigFormData) { async function onSubmit(values: NodeConfigFormData) {
setSaving(true); setSaving(true);
try { try {
@ -108,32 +85,32 @@ export default function ServerConfig() {
} }
} }
async function savePeriods() {
await setNodeMultiplier({ periods: timeSlots });
await refetchPeriods();
toast.success(t('config.saveSuccess'));
}
return ( return (
<Sheet open={open} onOpenChange={setOpen}> <Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between'> <Card>
<div className='flex items-center gap-3'> <CardContent className='p-4'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'> <div className='flex cursor-pointer items-center justify-between'>
<Icon icon='mdi:resistor-nodes' className='text-primary h-5 w-5' /> <div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:resistor-nodes' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('config.title')}</p>
<p className='text-muted-foreground truncate text-sm'>
{t('config.description')}
</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div> </div>
<div className='flex-1'> </CardContent>
<p className='font-medium'>{t('config.title')}</p> </Card>
<p className='text-muted-foreground text-sm'>{t('config.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger> </SheetTrigger>
<SheetContent className='w-[720px] max-w-full md:max-w-screen-md'> <SheetContent className='w-[720px] max-w-full md:max-w-screen-md'>
<SheetHeader> <SheetHeader>
<SheetTitle>{t('config.title')}</SheetTitle> <SheetTitle></SheetTitle>
</SheetHeader> </SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'> <ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
@ -218,45 +195,6 @@ export default function ServerConfig() {
</FormItem> </FormItem>
)} )}
/> />
<div className='mt-6 space-y-3'>
<Label className='text-base'>{t('config.dynamicMultiplier')}</Label>
<p className='text-muted-foreground text-sm'>
{t('config.dynamicMultiplierDescription')}
</p>
<div className='flex flex-col gap-2'>
<div className='w-full'>
<ArrayInput<API.TimePeriod>
fields={[
{ name: 'start_time', prefix: t('config.startTime'), type: 'time' },
{ name: 'end_time', prefix: t('config.endTime'), type: 'time' },
{
name: 'multiplier',
prefix: t('config.multiplier'),
type: 'number',
placeholder: '0',
},
]}
value={timeSlots}
onChange={setTimeSlots}
/>
<div className='mt-3 flex gap-2'>
<Button
type='button'
size='sm'
variant='outline'
onClick={() => setTimeSlots(periodsResp || [])}
>
{t('config.reset')}
</Button>
<Button size='sm' type='button' onClick={savePeriods}>
{t('config.save')}
</Button>
</div>
</div>
</div>
</div>
</form> </form>
</Form> </Form>
</ScrollArea> </ScrollArea>

View File

@ -8,7 +8,7 @@
"city": "City", "city": "City",
"config": { "config": {
"title": "Node configuration", "title": "Node configuration",
"description": "Manage node communication keys, pull/push intervals, and dynamic multipliers.", "description": "Manage node communication keys, pull/push intervals.",
"saveSuccess": "Saved successfully", "saveSuccess": "Saved successfully",
"communicationKey": "Communication key", "communicationKey": "Communication key",
"inputPlaceholder": "Please enter", "inputPlaceholder": "Please enter",

View File

@ -13,7 +13,7 @@
}, },
"communicationKey": "通信密钥", "communicationKey": "通信密钥",
"communicationKeyDescription": "用于节点鉴权。", "communicationKeyDescription": "用于节点鉴权。",
"description": "管理节点通信密钥、拉取/推送间隔与动态倍率。", "description": "管理节点通信密钥、拉取/推送间隔。",
"dynamicMultiplier": "动态倍率", "dynamicMultiplier": "动态倍率",
"dynamicMultiplierDescription": "按时间段设置倍率,用于调节流量或计费。", "dynamicMultiplierDescription": "按时间段设置倍率,用于调节流量或计费。",
"endTime": "结束时间", "endTime": "结束时间",