+
- {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')}