Files
OpenFlare/frontend/app/(main)/proxy-routes/detail/components/domain-section.tsx
T
ryan 5a8722ff07 feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
2026-08-16 16:49:16 +08:00

248 lines
7.5 KiB
TypeScript

'use client';
import { useEffect, useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import type { ProxyRouteItem } from '@/lib/services/openflare';
import { zoneQueryKey, ZoneService } from '@/lib/services/openflare';
import { useTranslations } from 'next-intl';
import { listAllZoneDomains } from '../../components/helpers';
import { ZoneDomainSelector } from '../../components/zone-domain-selector';
import { proxyRouteFormIds } from '../helpers';
import { useRouteSectionSave } from '../hooks/use-route-section-save';
import { SectionShell } from './section-shell';
type DomainSettingsValues = {
site_name: string;
zone_domain_ids: number[];
enabled: boolean;
redirect_http: boolean;
};
interface DomainSectionProps {
route: ProxyRouteItem;
onRouteUpdate: (route: ProxyRouteItem) => void;
onSavingChange?: (saving: boolean) => void;
}
export function DomainSection({
route,
onRouteUpdate,
onSavingChange,
}: DomainSectionProps) {
const t = useTranslations('proxyRoutes');
const domainSettingsSchema = z.object({
site_name: z
.string()
.trim()
.min(1, t('validation.enterSiteId'))
.max(255, t('validation.siteNameTooLong')),
zone_domain_ids: z
.array(z.number().int().positive())
.min(1, t('validation.selectAtLeastOneDomain')),
enabled: z.boolean(),
redirect_http: z.boolean(),
});
const { saving, save } = useRouteSectionSave(
route,
onRouteUpdate,
onSavingChange,
);
const zonesQuery = useQuery({
queryKey: zoneQueryKey,
queryFn: () => ZoneService.list(),
});
const domainsQuery = useQuery({
queryKey: [...zoneQueryKey, 'all-domains'],
queryFn: () => listAllZoneDomains(),
});
const form = useForm<DomainSettingsValues>({
resolver: zodResolver(domainSettingsSchema),
defaultValues: {
site_name: route.site_name,
zone_domain_ids: route.zone_domain_ids ?? [],
enabled: route.enabled,
redirect_http: route.redirect_http,
},
});
useEffect(() => {
form.reset({
site_name: route.site_name,
zone_domain_ids: route.zone_domain_ids ?? [],
enabled: route.enabled,
redirect_http: route.redirect_http,
});
}, [form, route]);
const selectedIDs = form.watch('zone_domain_ids');
const selectedDomains = useMemo(() => {
const fromApi = domainsQuery.data ?? [];
const byId = new Map(fromApi.map((domain) => [domain.id, domain]));
// Prefer live catalog; fall back to route-bound domains for display before catalog loads.
return selectedIDs
.map((id) => {
const catalog = byId.get(id);
if (catalog) {
return catalog;
}
const bound = (route.zone_domains ?? []).find((item) => item.id === id);
if (!bound) {
return null;
}
return {
id: bound.id,
zone_id: bound.zone_id,
proxy_route_id: route.id,
domain: bound.domain,
cert_id: bound.cert_id,
created_at: '',
updated_at: '',
};
})
.filter((item): item is NonNullable<typeof item> => item != null);
}, [domainsQuery.data, route.id, route.zone_domains, selectedIDs]);
const hasCertificate = selectedDomains.some(
(domain) => domain.cert_id != null,
);
return (
<SectionShell
title={t('domainSettings')}
description={t('domainSettingsDesc')}
formId={proxyRouteFormIds.domains}
saving={saving}
>
<Form {...form}>
<form
id={proxyRouteFormIds.domains}
className='space-y-5'
onSubmit={form.handleSubmit(async (values) => {
if (values.redirect_http && !hasCertificate) {
form.setError('redirect_http', {
message: t('redirectNeedsCert'),
});
return;
}
await save(
{
site_name: values.site_name.trim(),
zone_domain_ids: values.zone_domain_ids,
enabled: values.enabled,
enable_https: hasCertificate,
redirect_http: hasCertificate ? values.redirect_http : false,
},
t('domainSettingsSaved'),
);
})}
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
<div className='space-y-0.5'>
<FormLabel>{t('enableSite')}</FormLabel>
<FormDescription>{t('enableSiteDesc')}</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='site_name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('siteId')}</FormLabel>
<FormControl>
<Input placeholder='marketing-site' {...field} />
</FormControl>
<FormDescription>{t('siteIdStableDesc')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='zone_domain_ids'
render={({ field }) => (
<FormItem>
<FormLabel>{t('bindDomains')}</FormLabel>
<FormControl>
<ZoneDomainSelector
value={field.value}
onChange={field.onChange}
domains={domainsQuery.data ?? []}
zones={zonesQuery.data ?? []}
currentRouteId={route.id}
disabled={domainsQuery.isLoading}
onDomainCreated={async () => {
await domainsQuery.refetch();
}}
/>
</FormControl>
<FormDescription>{t('bindDomainsHint')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='redirect_http'
render={({ field }) => (
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
<div className='space-y-0.5'>
<FormLabel>{t('redirectHttp')}</FormLabel>
<FormDescription>
{hasCertificate
? t('redirectHttpEnabledHint')
: t('redirectHttpNeedsCert')}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
disabled={!hasCertificate}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SectionShell>
);
}