diff --git a/CHANGELOG.md b/CHANGELOG.md index 76358c3..7213637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ + # Changelog ## [1.4.4](https://github.com/perfect-panel/ppanel-web/compare/v1.4.3...v1.4.4) (2025-09-16) - ### 🐛 Bug Fixes -* Add minimum value constraint for count and user limit inputs in CouponForm ([6991b69](https://github.com/perfect-panel/ppanel-web/commit/6991b69)) -* Add protocol-related constants, default configurations, field definitions, and validation modes ([d685407](https://github.com/perfect-panel/ppanel-web/commit/d685407)) -* Added the enabled field in the protocol configuration, updated the related type definition and default configuration ([2b0cf9a](https://github.com/perfect-panel/ppanel-web/commit/2b0cf9a)) -* Filter available protocols to exclude disabled ones in NodeForm ([982d288](https://github.com/perfect-panel/ppanel-web/commit/982d288)) -* Refactor key generation logic and update dependencies for ML-KEM-768 integration ([b8f630f](https://github.com/perfect-panel/ppanel-web/commit/b8f630f)) +- Add minimum value constraint for count and user limit inputs in CouponForm ([6991b69](https://github.com/perfect-panel/ppanel-web/commit/6991b69)) +- Add protocol-related constants, default configurations, field definitions, and validation modes ([d685407](https://github.com/perfect-panel/ppanel-web/commit/d685407)) +- Added the enabled field in the protocol configuration, updated the related type definition and default configuration ([2b0cf9a](https://github.com/perfect-panel/ppanel-web/commit/2b0cf9a)) +- Filter available protocols to exclude disabled ones in NodeForm ([982d288](https://github.com/perfect-panel/ppanel-web/commit/982d288)) +- Refactor key generation logic and update dependencies for ML-KEM-768 integration ([b8f630f](https://github.com/perfect-panel/ppanel-web/commit/b8f630f)) diff --git a/apps/admin/app/dashboard/coupon/coupon-form.tsx b/apps/admin/app/dashboard/coupon/coupon-form.tsx index 5c5194b..d6f0ea4 100644 --- a/apps/admin/app/dashboard/coupon/coupon-form.tsx +++ b/apps/admin/app/dashboard/coupon/coupon-form.tsx @@ -1,8 +1,7 @@ 'use client'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Form, @@ -81,16 +80,7 @@ export default function CouponForm>({ const type = form.watch('type'); - const { data: subscribe } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 9999, - }); - return data.data?.list as API.Subscribe[]; - }, - }); + const { subscribes } = useSubscribe(); return ( @@ -247,9 +237,9 @@ export default function CouponForm>({ onChange={(value) => { form.setValue(field.name, value); }} - options={subscribe?.map((item: API.Subscribe) => ({ - value: item.id, - label: item.name, + options={subscribes?.map((item) => ({ + value: item.id!, + label: item.name!, }))} /> diff --git a/apps/admin/app/dashboard/coupon/page.tsx b/apps/admin/app/dashboard/coupon/page.tsx index dc99748..eb71f36 100644 --- a/apps/admin/app/dashboard/coupon/page.tsx +++ b/apps/admin/app/dashboard/coupon/page.tsx @@ -9,9 +9,8 @@ import { getCouponList, updateCoupon, } from '@/services/admin/coupon'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { formatDate } from '@/utils/common'; -import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; import { Switch } from '@workspace/ui/components/switch'; @@ -24,16 +23,7 @@ import CouponForm from './coupon-form'; export default function Page() { const t = useTranslations('coupon'); const [loading, setLoading] = useState(false); - const { data } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 9999, - }); - return data.data?.list as API.SubscribeGroup[]; - }, - }); + const { subscribes } = useSubscribe(); const ref = useRef(null); return ( @@ -67,8 +57,8 @@ export default function Page() { { key: 'subscribe', placeholder: t('subscribe'), - options: data?.map((item) => ({ - label: item.name, + options: subscribes?.map((item) => ({ + label: item.name!, value: String(item.id), })), }, diff --git a/apps/admin/app/dashboard/log/server-traffic/page.tsx b/apps/admin/app/dashboard/log/server-traffic/page.tsx index 11ee1c7..992e238 100644 --- a/apps/admin/app/dashboard/log/server-traffic/page.tsx +++ b/apps/admin/app/dashboard/log/server-traffic/page.tsx @@ -2,8 +2,7 @@ import { ProTable } from '@/components/pro-table'; import { filterServerTrafficLog } from '@/services/admin/log'; -import { filterServerList } from '@/services/admin/server'; -import { useQuery } from '@tanstack/react-query'; +import { useServer } from '@/store/server'; import { Button } from '@workspace/ui/components/button'; import { formatBytes } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; @@ -13,20 +12,10 @@ import { useSearchParams } from 'next/navigation'; export default function ServerTrafficLogPage() { const t = useTranslations('log'); const sp = useSearchParams(); + const { getServerName } = useServer(); const today = new Date().toISOString().split('T')[0]; - const { data: servers = [] } = useQuery({ - queryKey: ['filterServerListAll'], - queryFn: async () => { - const { data } = await filterServerList({ page: 1, size: 999999999 }); - return data?.data?.list || []; - }, - }); - - const getServerName = (id?: number) => - id ? (servers.find((s) => s.id === id)?.name ?? `Server ${id}`) : 'Unknown'; - const initialFilters = { date: sp.get('date') || today, server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined, diff --git a/apps/admin/app/dashboard/log/traffic-details/page.tsx b/apps/admin/app/dashboard/log/traffic-details/page.tsx index 7b403e1..95c8f3c 100644 --- a/apps/admin/app/dashboard/log/traffic-details/page.tsx +++ b/apps/admin/app/dashboard/log/traffic-details/page.tsx @@ -3,9 +3,8 @@ import { UserDetail, UserSubscribeDetail } from '@/app/dashboard/user/user-detail'; import { ProTable } from '@/components/pro-table'; import { filterTrafficLogDetails } from '@/services/admin/log'; -import { filterServerList } from '@/services/admin/server'; +import { useServer } from '@/store/server'; import { formatDate } from '@/utils/common'; -import { useQuery } from '@tanstack/react-query'; import { formatBytes } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; @@ -13,20 +12,10 @@ import { useSearchParams } from 'next/navigation'; export default function TrafficDetailsPage() { const t = useTranslations('log'); const sp = useSearchParams(); + const { getServerName } = useServer(); const today = new Date().toISOString().split('T')[0]; - const { data: servers = [] } = useQuery({ - queryKey: ['filterServerListAll'], - queryFn: async () => { - const { data } = await filterServerList({ page: 1, size: 999999999 }); - return data?.data?.list || []; - }, - }); - - const getServerName = (id?: number) => - id ? (servers.find((s) => s.id === id)?.name ?? `Server ${id}`) : 'Unknown'; - const initialFilters = { date: sp.get('date') || today, server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined, diff --git a/apps/admin/app/dashboard/marketing/quota/broadcast-form.tsx b/apps/admin/app/dashboard/marketing/quota/broadcast-form.tsx index ca29b2e..97ea242 100644 --- a/apps/admin/app/dashboard/marketing/quota/broadcast-form.tsx +++ b/apps/admin/app/dashboard/marketing/quota/broadcast-form.tsx @@ -2,9 +2,8 @@ import { Display } from '@/components/display'; import { createQuotaTask, queryQuotaTaskPreCount } from '@/services/admin/marketing'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Form, @@ -73,17 +72,7 @@ export default function QuotaBroadcastForm() { const [isSubmitting, setIsSubmitting] = useState(false); const [open, setOpen] = useState(false); - // Get subscribe list - const { data: subscribeList } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 999999999, - }); - return data.data?.list as API.SubscribeItem[]; - }, - }); + const { subscribes } = useSubscribe(); // Calculate recipient count const calculateRecipients = async () => { @@ -217,21 +206,19 @@ export default function QuotaBroadcastForm() { value={field.value || []} onChange={field.onChange} placeholder={t('pleaseSelectSubscribers')} - options={ - subscribeList?.map((subscribe) => ({ - value: subscribe.id!, - label: subscribe.name!, - children: ( -
-
{subscribe.name}
-
- /{' '} - -
+ options={subscribes?.map((subscribe) => ({ + value: subscribe.id!, + label: subscribe.name!, + children: ( +
+
{subscribe.name}
+
+ /{' '} +
- ), - })) || [] - } +
+ ), + }))} /> diff --git a/apps/admin/app/dashboard/marketing/quota/task-manager.tsx b/apps/admin/app/dashboard/marketing/quota/task-manager.tsx index a485835..82a4502 100644 --- a/apps/admin/app/dashboard/marketing/quota/task-manager.tsx +++ b/apps/admin/app/dashboard/marketing/quota/task-manager.tsx @@ -3,9 +3,8 @@ import { Display } from '@/components/display'; import { ProTable } from '@/components/pro-table'; import { queryQuotaTaskList } from '@/services/admin/marketing'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { formatDate } from '@/utils/common'; -import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { @@ -23,21 +22,9 @@ export default function QuotaTaskManager() { const t = useTranslations('marketing'); const [open, setOpen] = useState(false); - // Get subscribe list to show subscription names - const { data: subscribeList } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 999999999, - }); - return data.data?.list as API.SubscribeItem[]; - }, - }); - - // Create a map for quick lookup of subscription names + const { subscribes } = useSubscribe(); const subscribeMap = - subscribeList?.reduce( + subscribes?.reduce( (acc, subscribe) => { acc[subscribe.id!] = subscribe.name!; return acc; diff --git a/apps/admin/app/dashboard/nodes/node-form.tsx b/apps/admin/app/dashboard/nodes/node-form.tsx index 3d022d6..4a8064d 100644 --- a/apps/admin/app/dashboard/nodes/node-form.tsx +++ b/apps/admin/app/dashboard/nodes/node-form.tsx @@ -1,8 +1,8 @@ 'use client'; -import { filterServerList, queryNodeTag } from '@/services/admin/server'; +import { useNode } from '@/store/node'; +import { useServer } from '@/store/server'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Form, @@ -40,8 +40,6 @@ export type ProtocolName = | 'tuic' | 'anytls'; -type ServerRow = API.Server; - const buildSchema = (t: ReturnType) => z.object({ name: z.string().trim().min(1, t('errors.nameRequired')), @@ -103,38 +101,12 @@ export default function NodeForm(props: { const serverId = form.watch('server_id'); - const { data } = useQuery({ - enabled: open, - queryKey: ['filterServerListAll'], - queryFn: async () => { - const { data } = await filterServerList({ page: 1, size: 999999999 }); - return data?.data?.list || []; - }, - }); - const servers: ServerRow[] = data as ServerRow[]; + const { servers, getAvailableProtocols } = useServer(); + const { tags } = useNode(); - const { data: tagsData } = useQuery({ - enabled: open, - queryKey: ['queryNodeTag'], - queryFn: async () => { - const { data } = await queryNodeTag(); - return data?.data?.tags || []; - }, - }); - const existingTags: string[] = tagsData as string[]; + const existingTags: string[] = tags || []; - const currentServer = useMemo(() => servers?.find((s) => s.id === serverId), [servers, serverId]); - - const availableProtocols = useMemo(() => { - if (!currentServer?.protocols) return []; - - return currentServer.protocols - .filter((p) => p.enable !== false) - .map((p) => ({ - protocol: p.type, - port: p.port, - })); - }, [currentServer]); + const availableProtocols = getAvailableProtocols(serverId); useEffect(() => { if (initialValues) { @@ -176,12 +148,11 @@ export default function NodeForm(props: { fieldsToFill.push('address'); } - const protocols = - (selectedServer.protocols as Array<{ type: ProtocolName; port?: number }>) || []; + const protocols = getAvailableProtocols(id); const firstProtocol = protocols[0]; if (firstProtocol && (!currentValues.protocol || autoFilledFields.has('protocol'))) { - form.setValue('protocol', firstProtocol.type, { shouldDirty: false }); + form.setValue('protocol', firstProtocol.protocol, { shouldDirty: false }); fieldsToFill.push('protocol'); if (!currentValues.port || currentValues.port === 0 || autoFilledFields.has('port')) { @@ -203,7 +174,7 @@ export default function NodeForm(props: { const protocol = (nextProto || '') as ProtocolName | ''; form.setValue('protocol', protocol); - if (!protocol || !currentServer) { + if (!protocol || !serverId) { removeAutoFilledField('protocol'); return; } @@ -214,9 +185,7 @@ export default function NodeForm(props: { removeAutoFilledField('protocol'); if (!currentValues.port || currentValues.port === 0 || isPortAutoFilled) { - const protocolData = ( - currentServer.protocols as Array<{ type: ProtocolName; port?: number }> - )?.find((p) => p.type === protocol); + const protocolData = availableProtocols.find((p) => p.protocol === protocol); if (protocolData) { const port = protocolData.port || 0; diff --git a/apps/admin/app/dashboard/nodes/page.tsx b/apps/admin/app/dashboard/nodes/page.tsx index e4026c7..c88bc24 100644 --- a/apps/admin/app/dashboard/nodes/page.tsx +++ b/apps/admin/app/dashboard/nodes/page.tsx @@ -5,12 +5,12 @@ import { createNode, deleteNode, filterNodeList, - filterServerList, resetSortWithNode, toggleNodeStatus, updateNode, } from '@/services/admin/server'; -import { useQuery } from '@tanstack/react-query'; +import { useNode } from '@/store/node'; +import { useServer } from '@/store/server'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; import { Switch } from '@workspace/ui/components/switch'; @@ -25,24 +25,9 @@ export default function NodesPage() { const ref = useRef(null); const [loading, setLoading] = useState(false); - const { data: servers = [] } = useQuery({ - queryKey: ['filterServerListAll'], - queryFn: async () => { - const { data } = await filterServerList({ page: 1, size: 999999999 }); - return data?.data?.list || []; - }, - }); - - const getServerName = (id?: number) => - id ? (servers.find((s) => s.id === id)?.name ?? `#${id}`) : '—'; - const getServerOriginAddr = (id?: number) => - id ? (servers.find((s) => s.id === id)?.address ?? '—') : '—'; - const getProtocolOriginPort = (id?: number, proto?: string) => { - if (!id || !proto) return '—'; - const hit = servers.find((s) => s.id === id)?.protocols?.find((p) => (p as any).type === proto); - const p = (hit as any)?.port as number | undefined; - return typeof p === 'number' ? String(p) : '—'; - }; + // Use our zustand store for server data + const { getServerName, getServerAddress, getProtocolPort } = useServer(); + const { fetchNodes } = useNode(); return ( @@ -69,6 +54,7 @@ export default function NodesPage() { await createNode(body); toast.success(t('created')); ref.current?.refresh(); + fetchNodes(); setLoading(false); return true; } catch (e) { @@ -90,6 +76,7 @@ export default function NodesPage() { await toggleNodeStatus({ id: row.original.id, enable: v }); toast.success(v ? t('enabled_on') : t('enabled_off')); ref.current?.refresh(); + fetchNodes(); }} /> ), @@ -106,14 +93,14 @@ export default function NodesPage() { id: 'server_id', header: t('server'), cell: ({ row }) => ( -
+
- {getServerName(row.original.server_id)} ·{' '} - {getServerOriginAddr(row.original.server_id)} + {getServerName(row.original.server_id)} : {getServerAddress(row.original.server_id)} +
- {row.original.protocol || '—'} ·{' '} - {getProtocolOriginPort(row.original.server_id, row.original.protocol)} + {row.original.protocol || '—'} :{' '} + {getProtocolPort(row.original.server_id, row.original.protocol)}
), @@ -163,6 +150,7 @@ export default function NodesPage() { await updateNode(body); toast.success(t('updated')); ref.current?.refresh(); + fetchNodes(); setLoading(false); return true; } catch (e) { @@ -180,6 +168,7 @@ export default function NodesPage() { await deleteNode({ id: row.id } as any); toast.success(t('deleted')); ref.current?.refresh(); + fetchNodes(); }} cancelText={t('cancel')} confirmText={t('confirm')} @@ -195,6 +184,7 @@ export default function NodesPage() { }); toast.success(t('copied')); ref.current?.refresh(); + fetchNodes(); }} > {t('copy')} @@ -211,6 +201,7 @@ export default function NodesPage() { await Promise.all(rows.map((r) => deleteNode({ id: r.id } as any))); toast.success(t('deleted')); ref.current?.refresh(); + fetchNodes(); }} cancelText={t('cancel')} confirmText={t('confirm')} diff --git a/apps/admin/app/dashboard/order/page.tsx b/apps/admin/app/dashboard/order/page.tsx index c05b79b..7480937 100644 --- a/apps/admin/app/dashboard/order/page.tsx +++ b/apps/admin/app/dashboard/order/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useQuery } from '@tanstack/react-query'; import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; import { useRef } from 'react'; @@ -8,7 +7,7 @@ import { useRef } from 'react'; import { Display } from '@/components/display'; import { ProTable, ProTableActions } from '@/components/pro-table'; import { getOrderList, updateOrderStatus } from '@/services/admin/order'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { formatDate } from '@/utils/common'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; @@ -32,16 +31,7 @@ export default function Page() { const ref = useRef(null); - const { data: subscribeList } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 999999999, - }); - return data.data?.list as API.SubscribeGroup[]; - }, - }); + const { subscribes, getSubscribeName } = useSubscribe(); const initialFilters = { search: sp.get('search') || undefined, @@ -68,9 +58,7 @@ export default function Page() { accessorKey: 'subscribe_id', header: t('subscribe'), cell: ({ row }) => { - const name = subscribeList?.find( - (item) => item.id === row.getValue('subscribe_id'), - )?.name; + const name = getSubscribeName(row.getValue('subscribe_id')); const quantity = row.original.quantity; return name ? `${name} × ${quantity}` : ''; }, @@ -186,8 +174,8 @@ export default function Page() { { key: 'subscribe_id', placeholder: `${t('subscribe')}`, - options: subscribeList?.map((item) => ({ - label: item.name, + options: subscribes?.map((item) => ({ + label: item.name!, value: String(item.id), })), }, diff --git a/apps/admin/app/dashboard/product/page.tsx b/apps/admin/app/dashboard/product/page.tsx index de2cbf2..cd87214 100644 --- a/apps/admin/app/dashboard/product/page.tsx +++ b/apps/admin/app/dashboard/product/page.tsx @@ -1,9 +1,5 @@ -import { getTranslations } from 'next-intl/server'; - import SubscribeTable from './subscribe-table'; export default async function Page() { - const t = await getTranslations('product'); - return ; } diff --git a/apps/admin/app/dashboard/product/subscribe-form.tsx b/apps/admin/app/dashboard/product/subscribe-form.tsx index d114528..68182e7 100644 --- a/apps/admin/app/dashboard/product/subscribe-form.tsx +++ b/apps/admin/app/dashboard/product/subscribe-form.tsx @@ -1,8 +1,7 @@ 'use client'; -import { filterNodeList, queryNodeTag } from '@/services/admin/server'; +import { useNode } from '@/store/node'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; import { Accordion, AccordionContent, @@ -229,35 +228,9 @@ export default function SubscribeForm>({ if (bool) setOpen(false); } - const { data: nodes } = useQuery({ - queryKey: ['filterNodeListAll'], - queryFn: async () => { - const { data } = await filterNodeList({ page: 1, size: 999999999 }); - return (data.data?.list || []) as API.Node[]; - }, - }); + const { nodes, getAllAvailableTags, getNodesByTag, getNodesWithoutTags } = useNode(); - const { data: allTagsData } = useQuery({ - queryKey: ['queryNodeTag'], - queryFn: async () => { - const { data } = await queryNodeTag(); - return data?.data?.tags || []; - }, - }); - - const nodeExtractedTags = Array.from( - new Set( - ((nodes as API.Node[]) || []) - .flatMap((n) => (Array.isArray(n.tags) ? n.tags : [])) - .filter(Boolean), - ), - ) as string[]; - - const allAvailableTags = (allTagsData as string[]) || []; - - const tagGroups = Array.from(new Set([...allAvailableTags, ...nodeExtractedTags])).filter( - Boolean, - ); + const tagGroups = getAllAvailableTags(); const unit_time = form.watch('unit_time'); @@ -806,10 +779,7 @@ export default function SubscribeForm>({ {tagGroups.map((tag) => { const value = field.value || []; const tagId = tag; - const nodesWithTag = - (nodes as API.Node[])?.filter((n) => - (n.tags || []).includes(tag), - ) || []; + const nodesWithTag = getNodesByTag(tag); return ( @@ -836,22 +806,20 @@ export default function SubscribeForm>({
    - {(nodes as API.Node[]) - ?.filter((n) => (n.tags || []).includes(tag)) - ?.map((node) => ( -
  • - {node.name} - - {node.address}:{node.port} - - - {node.protocol} - -
  • - ))} + {getNodesByTag(tag).map((node) => ( +
  • + {node.name} + + {node.address}:{node.port} + + + {node.protocol} + +
  • + ))}
@@ -872,34 +840,32 @@ export default function SubscribeForm>({ {t('form.node')}
- {(nodes as API.Node[]) - ?.filter((item) => (item.tags || []).length === 0) - ?.map((item) => { - const value = field.value || []; + {getNodesWithoutTags().map((item) => { + const value = field.value || []; - return ( -
- { - return checked - ? form.setValue(field.name, [...value, item.id]) - : form.setValue( - field.name, - value.filter((value: number) => value !== item.id), - ); - }} - /> - -
- ); - })} + return ( +
+ { + return checked + ? form.setValue(field.name, [...value, item.id]) + : form.setValue( + field.name, + value.filter((value: number) => value !== item.id), + ); + }} + /> + +
+ ); + })}
diff --git a/apps/admin/app/dashboard/product/subscribe-table.tsx b/apps/admin/app/dashboard/product/subscribe-table.tsx index b360ac1..b0a868b 100644 --- a/apps/admin/app/dashboard/product/subscribe-table.tsx +++ b/apps/admin/app/dashboard/product/subscribe-table.tsx @@ -10,6 +10,7 @@ import { subscribeSort, updateSubscribe, } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; import { Switch } from '@workspace/ui/components/switch'; @@ -23,6 +24,7 @@ export default function SubscribeTable() { const t = useTranslations('product'); const [loading, setLoading] = useState(false); const ref = useRef(null); + const { fetchSubscribes } = useSubscribe(); return ( action={ref} @@ -42,6 +44,7 @@ export default function SubscribeTable() { }); toast.success(t('createSuccess')); ref.current?.refresh(); + fetchSubscribes(); setLoading(false); return true; @@ -83,6 +86,7 @@ export default function SubscribeTable() { show: checked, } as API.UpdateSubscribeRequest); ref.current?.refresh(); + fetchSubscribes(); }} /> ); @@ -101,6 +105,7 @@ export default function SubscribeTable() { sell: checked, } as API.UpdateSubscribeRequest); ref.current?.refresh(); + fetchSubscribes(); }} /> ); @@ -186,6 +191,7 @@ export default function SubscribeTable() { } as API.UpdateSubscribeRequest); toast.success(t('updateSuccess')); ref.current?.refresh(); + fetchSubscribes(); setLoading(false); return true; } catch (error) { @@ -206,6 +212,7 @@ export default function SubscribeTable() { }); toast.success(t('deleteSuccess')); ref.current?.refresh(); + fetchSubscribes(); }} cancelText={t('cancel')} confirmText={t('confirm')} @@ -224,6 +231,7 @@ export default function SubscribeTable() { } as API.CreateSubscribeRequest); toast.success(t('copySuccess')); ref.current?.refresh(); + fetchSubscribes(); setLoading(false); return true; } catch (error) { @@ -248,6 +256,7 @@ export default function SubscribeTable() { toast.success(t('deleteSuccess')); ref.current?.reset(); + fetchSubscribes(); }} cancelText={t('cancel')} confirmText={t('confirm')} diff --git a/apps/admin/app/dashboard/servers/form-schema/defaults.ts b/apps/admin/app/dashboard/servers/form-schema/defaults.ts index 2560e57..9c38969 100644 --- a/apps/admin/app/dashboard/servers/form-schema/defaults.ts +++ b/apps/admin/app/dashboard/servers/form-schema/defaults.ts @@ -91,6 +91,7 @@ export function getProtocolDefaultConfig(proto: ProtocolType) { case 'mieru': return { type: 'mieru', + enable: false, port: null, multiplex: 'none', transport: 'tcp', @@ -98,6 +99,7 @@ export function getProtocolDefaultConfig(proto: ProtocolType) { case 'anytls': return { type: 'anytls', + enable: false, port: null, security: 'tls', padding_scheme: null, diff --git a/apps/admin/app/dashboard/servers/form-schema/index.ts b/apps/admin/app/dashboard/servers/form-schema/index.ts index e7ba0f0..803acc3 100644 --- a/apps/admin/app/dashboard/servers/form-schema/index.ts +++ b/apps/admin/app/dashboard/servers/form-schema/index.ts @@ -18,7 +18,7 @@ export { } from './constants'; // Re-export all types -export type { FieldConfig, ProtocolType, ServerFormValues } from './types'; +export type { FieldConfig, ProtocolType } from './types'; // Re-export all schemas export { formSchema, protocolApiScheme } from './schemas'; diff --git a/apps/admin/app/dashboard/servers/form-schema/types.ts b/apps/admin/app/dashboard/servers/form-schema/types.ts index 0a2459d..36911ad 100644 --- a/apps/admin/app/dashboard/servers/form-schema/types.ts +++ b/apps/admin/app/dashboard/servers/form-schema/types.ts @@ -1,6 +1,4 @@ -import { z } from 'zod'; import { protocols } from './constants'; -import { formSchema } from './schemas'; export type FieldConfig = { name: string; @@ -22,6 +20,4 @@ export type FieldConfig = { gridSpan?: 1 | 2; }; -export type ServerFormValues = z.infer; - export type ProtocolType = (typeof protocols)[number]; diff --git a/apps/admin/app/dashboard/servers/page.tsx b/apps/admin/app/dashboard/servers/page.tsx index a810882..182bc1f 100644 --- a/apps/admin/app/dashboard/servers/page.tsx +++ b/apps/admin/app/dashboard/servers/page.tsx @@ -1,5 +1,5 @@ 'use client'; -// Online users detail moved to separate component + import { ProTable, ProTableActions } from '@/components/pro-table'; import { createServer, @@ -10,6 +10,8 @@ import { resetSortWithServer, updateServer, } from '@/services/admin/server'; +import { useNode } from '@/store/node'; +import { useServer } from '@/store/server'; import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; @@ -23,18 +25,6 @@ import OnlineUsersCell from './online-users-cell'; import ServerConfig from './server-config'; import ServerForm from './server-form'; -type ProtocolName = 'shadowsocks' | 'vmess' | 'vless' | 'trojan' | 'hysteria2' | 'tuic' | 'anytls'; - -const PROTOCOL_COLORS: Record = { - shadowsocks: 'bg-green-500', - vmess: 'bg-rose-500', - vless: 'bg-blue-500', - trojan: 'bg-yellow-500', - hysteria2: 'bg-purple-500', - tuic: 'bg-cyan-500', - anytls: 'bg-gray-500', -}; - function PctBar({ value }: { value: number }) { const v = value.toFixed(2); return ( @@ -69,6 +59,8 @@ function RegionIpCell({ export default function ServersPage() { const t = useTranslations('servers'); + const { isServerReferencedByNodes } = useNode(); + const { fetchServers } = useServer(); const [loading, setLoading] = useState(false); const [migrating, setMigrating] = useState(false); @@ -129,6 +121,7 @@ export default function ServersPage() { await createServer(values as unknown as API.CreateServerRequest); toast.success(t('created')); ref.current?.refresh(); + fetchServers(); setLoading(false); return true; } catch (e) { @@ -163,23 +156,16 @@ export default function ServersPage() { accessorKey: 'protocols', header: t('protocols'), cell: ({ row }) => { - const list = (row.original.protocols || []) as API.Protocol[]; - if (!list.length) return t('noData'); + const list = row.original.protocols.filter( + (p) => p.enable !== false, + ) as API.Protocol[]; + if (!list.length) return '—'; return (
{list.map((p, idx) => { - const proto = ((p as any)?.type || '') as ProtocolName | ''; - if (!proto) return null; - const color = PROTOCOL_COLORS[proto as ProtocolName]; - const port = (p as any)?.port as number | undefined; - const label = `${proto}${port ? ` (${port})` : ''}`; return ( - - {label} + + {p.type} ({p.port}) ); })} @@ -272,6 +258,7 @@ export default function ServersPage() { }); toast.success(t('updated')); ref.current?.refresh(); + fetchServers(); setLoading(false); return true; } catch (e) { @@ -282,13 +269,18 @@ export default function ServersPage() { />, {t('delete')}} + trigger={ + + } title={t('confirmDeleteTitle')} description={t('confirmDeleteDesc')} onConfirm={async () => { await deleteServer({ id: row.id } as any); toast.success(t('deleted')); ref.current?.refresh(); + fetchServers(); }} cancelText={t('cancel')} confirmText={t('confirm')} @@ -311,6 +303,7 @@ export default function ServersPage() { await createServer(body); toast.success(t('copied')); ref.current?.refresh(); + fetchServers(); setLoading(false); }} > @@ -318,16 +311,22 @@ export default function ServersPage() { , ], batchRender(rows) { + const hasReferencedServers = rows.some((row) => isServerReferencedByNodes(row.id)); return [ {t('delete')}} + trigger={ + + } title={t('confirmDeleteTitle')} description={t('confirmDeleteDesc')} onConfirm={async () => { await Promise.all(rows.map((r) => deleteServer({ id: r.id }))); toast.success(t('deleted')); ref.current?.refresh(); + fetchServers(); }} cancelText={t('cancel')} confirmText={t('confirm')} diff --git a/apps/admin/app/dashboard/servers/server-form.tsx b/apps/admin/app/dashboard/servers/server-form.tsx index a78c7a4..788672e 100644 --- a/apps/admin/app/dashboard/servers/server-form.tsx +++ b/apps/admin/app/dashboard/servers/server-form.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useNode } from '@/store/node'; import { zodResolver } from '@hookform/resolvers/zod'; import { Accordion, @@ -48,7 +49,6 @@ import { getProtocolDefaultConfig, PROTOCOL_FIELDS, protocols as PROTOCOLS, - ServerFormValues, } from './form-schema'; function DynamicField({ @@ -321,14 +321,16 @@ export default function ServerForm(props: { trigger: string; title: string; loading?: boolean; - initialValues?: Partial; - onSubmit: (values: ServerFormValues) => Promise | boolean; + initialValues?: Partial; + onSubmit: (values: Partial) => Promise | boolean; }) { const { trigger, title, loading, initialValues, onSubmit } = props; const t = useTranslations('servers'); const [open, setOpen] = useState(false); const [accordionValue, setAccordionValue] = useState(); + const { isProtocolUsedInNodes } = useNode(); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -337,7 +339,7 @@ export default function ServerForm(props: { country: '', city: '', ratio: 1, - protocols: [], + protocols: [] as any[], ...initialValues, }, }); @@ -515,6 +517,7 @@ export default function ServerForm(props: { PROTOCOLS.findIndex((t) => t === type), ); const current = (protocolsValues[i] || {}) as Record; + const isEnabled = current?.enable !== false; const fields = PROTOCOL_FIELDS[type] || []; return ( @@ -539,16 +542,20 @@ export default function ServerForm(props: { - {current.enable ? t('enabled') : t('disabled')} + {isEnabled ? t('enabled') : t('disabled')}
{ form.setValue(`protocols.${i}.enable`, checked); }} diff --git a/apps/admin/app/dashboard/system/user-security/register-form.tsx b/apps/admin/app/dashboard/system/user-security/register-form.tsx index 3201c89..8c8c6c1 100644 --- a/apps/admin/app/dashboard/system/user-security/register-form.tsx +++ b/apps/admin/app/dashboard/system/user-security/register-form.tsx @@ -1,7 +1,7 @@ 'use client'; -import { getSubscribeList } from '@/services/admin/subscribe'; import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system'; +import { useSubscribe } from '@/store/subscribe'; import { zodResolver } from '@hookform/resolvers/zod'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; @@ -61,17 +61,7 @@ export default function RegisterConfig() { enabled: open, }); - const { data: subscribe } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 9999, - }); - return data.data?.list as API.Subscribe[]; - }, - enabled: open, - }); + const { subscribes } = useSubscribe(); const form = useForm({ resolver: zodResolver(registerSchema), @@ -268,12 +258,10 @@ export default function RegisterConfig() { field.onChange(value); } }} - options={ - subscribe?.map((item) => ({ - label: item.name, - value: item.id, - })) || [] - } + options={subscribes?.map((item) => ({ + label: item.name!, + value: item.id!, + }))} className='bg-secondary w-32 rounded-r-none' /> )} diff --git a/apps/admin/app/dashboard/user/page.tsx b/apps/admin/app/dashboard/user/page.tsx index fe2eb31..0183a66 100644 --- a/apps/admin/app/dashboard/user/page.tsx +++ b/apps/admin/app/dashboard/user/page.tsx @@ -2,7 +2,6 @@ import { Display } from '@/components/display'; import { ProTable, ProTableActions } from '@/components/pro-table'; -import { getSubscribeList } from '@/services/admin/subscribe'; import { createUser, deleteUser, @@ -10,6 +9,7 @@ import { getUserList, updateUserBasicInfo, } from '@/services/admin/user'; +import { useSubscribe } from '@/store/subscribe'; import { formatDate } from '@/utils/common'; import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; @@ -49,16 +49,7 @@ export default function Page() { const ref = useRef(null); const sp = useSearchParams(); - const { data: subscribeList } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 9999, - }); - return data.data?.list as API.SubscribeGroup[]; - }, - }); + const { subscribes } = useSubscribe(); const initialFilters = { search: sp.get('search') || undefined, @@ -194,9 +185,9 @@ export default function Page() { { key: 'subscribe_id', placeholder: t('subscription'), - options: subscribeList?.map((item) => ({ - label: item.name, - value: String(item.id), + options: subscribes?.map((item) => ({ + label: item.name!, + value: String(item.id!), })), }, { diff --git a/apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx b/apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx index 25349ac..2c98134 100644 --- a/apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx +++ b/apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx @@ -1,8 +1,7 @@ 'use client'; -import { getSubscribeList } from '@/services/admin/subscribe'; +import { useSubscribe } from '@/store/subscribe'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Form, @@ -74,16 +73,7 @@ export function SubscriptionForm({ trigger, title, loading, initialData, onSubmi } }; - const { data: subscribe } = useQuery({ - queryKey: ['getSubscribeList', 'all'], - queryFn: async () => { - const { data } = await getSubscribeList({ - page: 1, - size: 9999, - }); - return data.data?.list as API.Subscribe[]; - }, - }); + const { subscribes } = useSubscribe(); return ( @@ -117,9 +107,9 @@ export function SubscriptionForm({ trigger, title, loading, initialData, onSubmi onChange={(value) => { form.setValue(field.name, value); }} - options={subscribe?.map((item: API.Subscribe) => ({ - value: item.id, - label: item.name, + options={subscribes?.map((item) => ({ + value: item.id!, + label: item.name!, }))} /> diff --git a/apps/admin/services/admin/index.ts b/apps/admin/services/admin/index.ts index bdf189b..4ebecd4 100644 --- a/apps/admin/services/admin/index.ts +++ b/apps/admin/services/admin/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as ads from './ads'; diff --git a/apps/admin/services/common/index.ts b/apps/admin/services/common/index.ts index 61ba129..73b3bda 100644 --- a/apps/admin/services/common/index.ts +++ b/apps/admin/services/common/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as auth from './auth'; diff --git a/apps/admin/store/node.ts b/apps/admin/store/node.ts new file mode 100644 index 0000000..a629aa3 --- /dev/null +++ b/apps/admin/store/node.ts @@ -0,0 +1,113 @@ +import { filterNodeList, queryNodeTag } from '@/services/admin/server'; +import { create } from 'zustand'; + +interface NodeState { + // Data + nodes: API.Node[]; + tags: string[]; + + // Actions + fetchNodes: () => Promise; + fetchTags: () => Promise; + + // Getters + getNodeById: (nodeId: number) => API.Node | undefined; + isProtocolUsedInNodes: (serverId: number, protocolType: string) => boolean; + isServerReferencedByNodes: (serverId: number) => boolean; + getNodesByTag: (tag: string) => API.Node[]; + getNodesWithoutTags: () => API.Node[]; + getNodeTags: () => string[]; + getAllAvailableTags: () => string[]; +} + +export const useNodeStore = create((set, get) => ({ + // Initial state + nodes: [], + tags: [], + + // Actions + fetchNodes: async () => { + try { + const { data } = await filterNodeList({ page: 1, size: 999999999 }); + set({ nodes: data?.data?.list || [] }); + } catch (error) { + // Handle error silently + } + }, + + fetchTags: async () => { + try { + const { data } = await queryNodeTag(); + set({ tags: data?.data?.tags || [] }); + } catch (error) { + // Handle error silently + } + }, + + // Getters + getNodeById: (nodeId: number) => { + return get().nodes.find((n) => n.id === nodeId); + }, + + isProtocolUsedInNodes: (serverId: number, protocolType: string) => { + return get().nodes.some( + (node) => node.server_id === serverId && node.protocol === protocolType, + ); + }, + + isServerReferencedByNodes: (serverId: number) => { + return get().nodes.some((node) => node.server_id === serverId); + }, + + getNodesByTag: (tag: string) => { + return get().nodes.filter((node) => (node.tags || []).includes(tag)); + }, + + getNodesWithoutTags: () => { + return get().nodes.filter((node) => (node.tags || []).length === 0); + }, + + getNodeTags: () => { + return Array.from( + new Set( + get() + .nodes.flatMap((node) => (Array.isArray(node.tags) ? node.tags : [])) + .filter(Boolean), + ), + ) as string[]; + }, + + getAllAvailableTags: () => { + const nodeExtractedTags = get().getNodeTags(); + const allApiTags = get().tags; + return Array.from(new Set([...allApiTags, ...nodeExtractedTags])).filter(Boolean); + }, +})); + +export const useNode = () => { + const store = useNodeStore(); + + // Auto-fetch nodes and tags + if (store.nodes.length === 0) { + store.fetchNodes(); + } + if (store.tags.length === 0) { + store.fetchTags(); + } + + return { + nodes: store.nodes, + tags: store.tags, + fetchNodes: store.fetchNodes, + fetchTags: store.fetchTags, + getNodeById: store.getNodeById, + isProtocolUsedInNodes: store.isProtocolUsedInNodes, + isServerReferencedByNodes: store.isServerReferencedByNodes, + getNodesByTag: store.getNodesByTag, + getNodesWithoutTags: store.getNodesWithoutTags, + getNodeTags: store.getNodeTags, + getAllAvailableTags: store.getAllAvailableTags, + }; +}; + +export default useNodeStore; diff --git a/apps/admin/store/server.ts b/apps/admin/store/server.ts new file mode 100644 index 0000000..87b635e --- /dev/null +++ b/apps/admin/store/server.ts @@ -0,0 +1,94 @@ +import { filterServerList } from '@/services/admin/server'; +import { create } from 'zustand'; + +interface ServerState { + // Data + servers: API.Server[]; + + // Actions + fetchServers: () => Promise; + + // Getters + getServerById: (serverId: number) => API.Server | undefined; + getServerName: (serverId?: number) => string; + getServerAddress: (serverId?: number) => string; + getServerEnabledProtocols: (serverId: number) => API.Protocol[]; + getProtocolPort: (serverId?: number, protocol?: string) => string; + getAvailableProtocols: (serverId?: number) => Array<{ protocol: string; port: number }>; +} + +export const useServerStore = create((set, get) => ({ + // Initial state + servers: [], + + // Actions + fetchServers: async () => { + try { + const { data } = await filterServerList({ page: 1, size: 999999999 }); + set({ servers: data?.data?.list || [] }); + } catch (error) { + // Handle error silently + } + }, + + // Getters + getServerById: (serverId: number) => { + return get().servers.find((s) => s.id === serverId); + }, + + getServerName: (serverId?: number) => { + if (!serverId) return '—'; + const server = get().servers.find((s) => s.id === serverId); + return server?.name ?? `#${serverId}`; + }, + + getServerAddress: (serverId?: number) => { + if (!serverId) return '—'; + const server = get().servers.find((s) => s.id === serverId); + return server?.address ?? '—'; + }, + + getServerEnabledProtocols: (serverId: number) => { + const server = get().servers.find((s) => s.id === serverId); + return server?.protocols?.filter((p) => p.enable !== false) || []; + }, + + getProtocolPort: (serverId?: number, protocol?: string) => { + if (!serverId || !protocol) return '—'; + const enabledProtocols = get().getServerEnabledProtocols(serverId); + const protocolConfig = enabledProtocols.find((p) => p.type === protocol); + return protocolConfig?.port ? String(protocolConfig.port) : '—'; + }, + + getAvailableProtocols: (serverId?: number) => { + if (!serverId) return []; + return get() + .getServerEnabledProtocols(serverId) + .map((p) => ({ + protocol: p.type, + port: p.port, + })); + }, +})); + +export const useServer = () => { + const store = useServerStore(); + + // Auto-fetch servers + if (store.servers.length === 0) { + store.fetchServers(); + } + + return { + servers: store.servers, + fetchServers: store.fetchServers, + getServerById: store.getServerById, + getServerName: store.getServerName, + getServerAddress: store.getServerAddress, + getServerEnabledProtocols: store.getServerEnabledProtocols, + getProtocolPort: store.getProtocolPort, + getAvailableProtocols: store.getAvailableProtocols, + }; +}; + +export default useServerStore; diff --git a/apps/admin/store/subscribe.ts b/apps/admin/store/subscribe.ts new file mode 100644 index 0000000..776414a --- /dev/null +++ b/apps/admin/store/subscribe.ts @@ -0,0 +1,58 @@ +import { getSubscribeList } from '@/services/admin/subscribe'; +import { create } from 'zustand'; + +interface SubscribeState { + // Data + subscribes: API.SubscribeItem[]; + + // Actions + fetchSubscribes: () => Promise; + + // Getters + getSubscribeName: (subscribeId?: number) => string; + getSubscribeById: (subscribeId: number) => API.SubscribeItem | undefined; +} + +export const useSubscribeStore = create((set, get) => ({ + // Initial state + subscribes: [], + + // Actions + fetchSubscribes: async () => { + try { + const { data } = await getSubscribeList({ page: 1, size: 999999999 }); + set({ subscribes: data?.data?.list || [] }); + } catch (error) { + // Handle error silently + } + }, + + // Getters + getSubscribeName: (subscribeId?: number) => { + if (!subscribeId) return 'Unknown'; + const subscribe = get().subscribes.find((s) => s.id === subscribeId); + return subscribe?.name ?? `Subscribe ${subscribeId}`; + }, + + getSubscribeById: (subscribeId: number) => { + return get().subscribes.find((s) => s.id === subscribeId); + }, +})); + +export const useSubscribe = () => { + const store = useSubscribeStore(); + + // Auto-fetch subscribes + if (store.subscribes.length === 0) { + store.fetchSubscribes(); + } + + return { + subscribes: store.subscribes, + fetchSubscribes: store.fetchSubscribes, + getSubscribeName: store.getSubscribeName, + getSubscribeById: store.getSubscribeById, + }; +}; + +export default useSubscribeStore; diff --git a/apps/user/services/common/index.ts b/apps/user/services/common/index.ts index 61ba129..73b3bda 100644 --- a/apps/user/services/common/index.ts +++ b/apps/user/services/common/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as auth from './auth'; diff --git a/apps/user/services/user/index.ts b/apps/user/services/user/index.ts index 12fe8d0..f988131 100644 --- a/apps/user/services/user/index.ts +++ b/apps/user/services/user/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as announcement from './announcement';