🐛 fix: Remove unnecessary migration function code and add device configuration options

This commit is contained in:
web 2025-10-21 04:33:29 -07:00
parent a46657d5ef
commit 521a7a97fb
5 changed files with 128 additions and 125 deletions

View File

@ -5,14 +5,11 @@ import {
createServer, createServer,
deleteServer, deleteServer,
filterServerList, filterServerList,
hasMigrateSeverNode,
migrateServerNode,
resetSortWithServer, resetSortWithServer,
updateServer, updateServer,
} from '@/services/admin/server'; } from '@/services/admin/server';
import { useNode } from '@/store/node'; import { useNode } from '@/store/node';
import { useServer } from '@/store/server'; import { useServer } from '@/store/server';
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 { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
@ -66,33 +63,6 @@ export default function ServersPage() {
const [migrating, setMigrating] = useState(false); const [migrating, setMigrating] = useState(false);
const ref = useRef<ProTableActions>(null); const ref = useRef<ProTableActions>(null);
const { data: hasMigrate, refetch: refetchHasMigrate } = useQuery({
queryKey: ['hasMigrateSeverNode'],
queryFn: async () => {
const { data } = await hasMigrateSeverNode();
return data.data?.has_migrate;
},
});
const handleMigrate = async () => {
setMigrating(true);
try {
const { data } = await migrateServerNode();
const fail = data.data?.fail || 0;
if (fail > 0) {
toast.error(data.data?.message);
} else {
toast.success(t('migrated'));
}
refetchHasMigrate();
ref.current?.refresh();
} catch (error) {
toast.error(t('migrateFailed'));
} finally {
setMigrating(false);
}
};
return ( return (
<div className='space-y-4'> <div className='space-y-4'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'> <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
@ -105,11 +75,6 @@ export default function ServersPage() {
title: t('pageTitle'), title: t('pageTitle'),
toolbar: ( toolbar: (
<div className='flex gap-2'> <div className='flex gap-2'>
{hasMigrate && (
<Button variant='outline' onClick={handleMigrate} disabled={migrating}>
{migrating ? t('migrating') : t('migrate')}
</Button>
)}
<ServerForm <ServerForm
trigger={t('create')} trigger={t('create')}
title={t('drawerCreateTitle')} title={t('drawerCreateTitle')}

View File

@ -43,6 +43,7 @@ import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import { SS_CIPHERS } from './form-schema';
const dnsConfigSchema = z.object({ const dnsConfigSchema = z.object({
proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']), proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
@ -55,6 +56,7 @@ const outboundConfigSchema = z.object({
protocol: z.string(), protocol: z.string(),
address: z.string(), address: z.string(),
port: z.number(), port: z.number(),
cipher: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
rules: z.array(z.string()).optional(), rules: z.array(z.string()).optional(),
}); });
@ -92,7 +94,7 @@ export default function ServerConfig() {
node_pull_interval: undefined, node_pull_interval: undefined,
node_push_interval: undefined, node_push_interval: undefined,
traffic_report_threshold: undefined, traffic_report_threshold: undefined,
ip_strategy: undefined, ip_strategy: 'prefer_ipv4',
dns: [], dns: [],
block: [], block: [],
outbound: [], outbound: [],
@ -106,7 +108,8 @@ export default function ServerConfig() {
node_pull_interval: cfgResp.node_pull_interval as number | undefined, node_pull_interval: cfgResp.node_pull_interval as number | undefined,
node_push_interval: cfgResp.node_push_interval as number | undefined, node_push_interval: cfgResp.node_push_interval as number | undefined,
traffic_report_threshold: cfgResp.traffic_report_threshold as number | undefined, traffic_report_threshold: cfgResp.traffic_report_threshold as number | undefined,
ip_strategy: cfgResp.ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined, ip_strategy:
(cfgResp.ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined) || 'prefer_ipv4',
dns: cfgResp.dns || [], dns: cfgResp.dns || [],
block: cfgResp.block || [], block: cfgResp.block || [],
outbound: cfgResp.outbound || [], outbound: cfgResp.outbound || [],
@ -364,7 +367,8 @@ export default function ServerConfig() {
<FormField <FormField
control={form.control} control={form.control}
name='outbound' name='outbound'
render={({ field }) => ( render={({ field }) => {
return (
<FormItem> <FormItem>
<FormControl> <FormControl>
<ArrayInput <ArrayInput
@ -400,10 +404,22 @@ export default function ServerConfig() {
{ label: 'Reject', value: 'reject' }, { label: 'Reject', value: 'reject' },
], ],
}, },
{
name: 'cipher',
type: 'select',
options: SS_CIPHERS.map((cipher) => ({
label: cipher,
value: cipher,
})),
visible: (item: Record<string, any>) =>
item.protocol === 'shadowsocks',
},
{ {
name: 'address', name: 'address',
type: 'text', type: 'text',
placeholder: t('server_config.fields.outbound_address_placeholder'), placeholder: t(
'server_config.fields.outbound_address_placeholder',
),
}, },
{ {
name: 'port', name: 'port',
@ -434,6 +450,7 @@ export default function ServerConfig() {
protocol: item.protocol, protocol: item.protocol,
address: item.address, address: item.address,
port: item.port, port: item.port,
cipher: item.cipher,
password: item.password, password: item.password,
rules: rules:
typeof item.rules === 'string' typeof item.rules === 'string'
@ -446,7 +463,8 @@ export default function ServerConfig() {
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} );
}}
/> />
</TabsContent> </TabsContent>

View File

@ -45,6 +45,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
ip_register_limit: 0, ip_register_limit: 0,
ip_register_limit_duration: 0, ip_register_limit_duration: 0,
}, },
device: {
enable: false,
show_ads: false,
enable_security: false,
only_real_device: false,
},
}, },
invite: { invite: {
forced_invite: false, forced_invite: false,

View File

@ -48,6 +48,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
ip_register_limit: 0, ip_register_limit: 0,
ip_register_limit_duration: 0, ip_register_limit_duration: 0,
}, },
device: {
enable: false,
show_ads: false,
enable_security: false,
only_real_device: false,
},
}, },
invite: { invite: {
forced_invite: false, forced_invite: false,

View File

@ -12,6 +12,8 @@ interface FieldConfig extends Omit<EnhancedInputProps, 'type'> {
name: string; name: string;
type: 'text' | 'number' | 'select' | 'time' | 'boolean' | 'textarea'; type: 'text' | 'number' | 'select' | 'time' | 'boolean' | 'textarea';
options?: { label: string; value: string }[]; options?: { label: string; value: string }[];
// optional per-item visibility function: returns true to show the field for the given item
visible?: (item: Record<string, any>) => boolean;
} }
interface ObjectInputProps<T> { interface ObjectInputProps<T> {
@ -40,6 +42,8 @@ export function ObjectInput<T extends Record<string, any>>({
onChange(updatedInternalState); onChange(updatedInternalState);
}; };
const renderField = (field: FieldConfig) => { const renderField = (field: FieldConfig) => {
// if visible callback exists and returns false for current item, don't render
if (field.visible && !field.visible(internalState)) return null;
switch (field.type) { switch (field.type) {
case 'select': case 'select':
return ( return (
@ -86,11 +90,15 @@ export function ObjectInput<T extends Record<string, any>>({
}; };
return ( return (
<div className={cn('flex flex-1 flex-wrap gap-4', className)}> <div className={cn('flex flex-1 flex-wrap gap-4', className)}>
{fields.map((field) => ( {fields.map((field) => {
const node = renderField(field);
if (node === null) return null; // don't render wrapper if field hidden
return (
<div key={field.name} className={cn('flex-1', field.className)}> <div key={field.name} className={cn('flex-1', field.className)}>
{renderField(field)} {node}
</div> </div>
))} );
})}
</div> </div>
); );
} }