♻️ refactor: Replace useQuery with Zustand store for subscription and node data management

This commit is contained in:
web
2025-09-17 02:20:54 -07:00
parent bdd53b1551
commit c6dd0b63f2
28 changed files with 448 additions and 359 deletions
@@ -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,
@@ -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';
@@ -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<typeof formSchema>;
export type ProtocolType = (typeof protocols)[number];
+27 -28
View File
@@ -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<ProtocolName, string> = {
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 (
<div className='flex flex-wrap gap-1'>
{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 (
<Badge
key={idx}
variant='outline'
className={cn('text-primary-foreground', color)}
>
{label}
<Badge key={idx} variant='outline'>
{p.type} ({p.port})
</Badge>
);
})}
@@ -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() {
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
trigger={
<Button variant='destructive' disabled={isServerReferencedByNodes(row.id)}>
{t('delete')}
</Button>
}
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() {
</Button>,
],
batchRender(rows) {
const hasReferencedServers = rows.some((row) => isServerReferencedByNodes(row.id));
return [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
trigger={
<Button variant='destructive' disabled={hasReferencedServers}>
{t('delete')}
</Button>
}
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')}
@@ -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<ServerFormValues>;
onSubmit: (values: ServerFormValues) => Promise<boolean> | boolean;
initialValues?: Partial<API.Server>;
onSubmit: (values: Partial<API.Server>) => Promise<boolean> | boolean;
}) {
const { trigger, title, loading, initialValues, onSubmit } = props;
const t = useTranslations('servers');
const [open, setOpen] = useState(false);
const [accordionValue, setAccordionValue] = useState<string>();
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<string, any>;
const isEnabled = current?.enable !== false;
const fields = PROTOCOL_FIELDS[type] || [];
return (
<AccordionItem key={type} value={type} className='mb-2 rounded-lg border'>
@@ -539,16 +542,20 @@ export default function ServerForm(props: {
<span
className={cn(
'text-xs',
current.enable ? 'text-green-500' : 'text-muted-foreground',
isEnabled ? 'text-green-500' : 'text-muted-foreground',
)}
>
{current.enable ? t('enabled') : t('disabled')}
{isEnabled ? t('enabled') : t('disabled')}
</span>
</div>
</div>
<Switch
className='mr-2'
checked={!!current.enable}
checked={!!isEnabled}
disabled={Boolean(
initialValues?.id &&
isProtocolUsedInNodes(initialValues?.id || 0, type),
)}
onCheckedChange={(checked) => {
form.setValue(`protocols.${i}.enable`, checked);
}}