'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { AppModal } from '@/components/ui/app-modal'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { createManagedDomain, deleteManagedDomain, getManagedDomainCertificates, getManagedDomains, updateManagedDomain, } from '@/features/managed-domains/api/managed-domains'; import type { ManagedDomainCertificateOption, ManagedDomainItem, ManagedDomainMutationPayload, } from '@/features/managed-domains/types'; import { DangerButton, PrimaryButton, ResourceField, ResourceInput, ResourceSelect, SecondaryButton, ToggleField, } from '@/features/shared/components/resource-primitives'; import { formatDateTime } from '@/lib/utils/date'; const managedDomainsQueryKey = ['managed-domains'] as const; const managedDomainCertificatesQueryKey = ['managed-domains', 'certificates'] as const; const managedDomainSchema = z.object({ domain: z .string() .trim() .min(1, '请输入域名') .max(255, '域名不能超过 255 个字符') .refine((value) => !value.includes('://') && !value.includes('/'), '域名格式不合法') .refine( (value) => /^(?:\*\.)?(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/.test(value), '域名格式不合法', ) .refine( (value) => !value.includes('*') || (value.startsWith('*.') && value.indexOf('*', 1) === -1), '通配符域名仅支持 *.example.com 格式', ), cert_id: z.string(), enabled: z.boolean(), remark: z.string().max(255, '备注不能超过 255 个字符'), }); type ManagedDomainFormValues = z.infer; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; const defaultValues: ManagedDomainFormValues = { domain: '', cert_id: '', enabled: true, remark: '', }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function getMatchTypeMeta(domain: string) { return domain.startsWith('*.') ? { label: '通配符', variant: 'warning' as const } : { label: '精确匹配', variant: 'info' as const }; } function buildCertificateLabel(certificate: ManagedDomainCertificateOption) { return certificate.not_after ? `${certificate.name}(到期:${formatDateTime(certificate.not_after)})` : certificate.name; } function toPayload(values: ManagedDomainFormValues): ManagedDomainMutationPayload { return { domain: values.domain.trim().toLowerCase(), cert_id: values.cert_id ? Number(values.cert_id) : null, enabled: values.enabled, remark: values.remark.trim(), }; } function toFormValues(domain: ManagedDomainItem): ManagedDomainFormValues { return { domain: domain.domain, cert_id: domain.cert_id ? String(domain.cert_id) : '', enabled: domain.enabled, remark: domain.remark || '', }; } export function ManagedDomainsPage() { const queryClient = useQueryClient(); const [editingDomainId, setEditingDomainId] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [feedback, setFeedback] = useState(null); const form = useForm({ resolver: zodResolver(managedDomainSchema), defaultValues, }); const watchedDomain = useWatch({ control: form.control, name: 'domain' }); const watchedCertId = useWatch({ control: form.control, name: 'cert_id' }); const watchedEnabled = useWatch({ control: form.control, name: 'enabled' }); const managedDomainsQuery = useQuery({ queryKey: managedDomainsQueryKey, queryFn: getManagedDomains, }); const certificatesQuery = useQuery({ queryKey: managedDomainCertificatesQueryKey, queryFn: getManagedDomainCertificates, }); const saveMutation = useMutation({ mutationFn: async (values: ManagedDomainFormValues) => { const payload = toPayload(values); return editingDomainId ? updateManagedDomain(editingDomainId, payload) : createManagedDomain(payload); }, onSuccess: async () => { setFeedback({ tone: 'success', message: editingDomainId ? '域名规则已更新。' : '域名规则已创建。', }); setEditingDomainId(null); setIsEditorOpen(false); form.reset(defaultValues); await queryClient.invalidateQueries({ queryKey: managedDomainsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const deleteMutation = useMutation({ mutationFn: deleteManagedDomain, onSuccess: async () => { setFeedback({ tone: 'success', message: '域名规则已删除。' }); await queryClient.invalidateQueries({ queryKey: managedDomainsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const certificates = useMemo(() => certificatesQuery.data ?? [], [certificatesQuery.data]); const domains = useMemo(() => managedDomainsQuery.data ?? [], [managedDomainsQuery.data]); const certificateMap = useMemo( () => new Map(certificates.map((item) => [item.id, item])), [certificates], ); const currentCertificate = watchedCertId ? certificateMap.get(Number(watchedCertId)) : null; const handleReset = () => { setEditingDomainId(null); setIsEditorOpen(false); setFeedback(null); form.reset(defaultValues); }; const handleCreate = () => { setEditingDomainId(null); setFeedback(null); form.reset(defaultValues); setIsEditorOpen(true); }; const handleEdit = (domain: ManagedDomainItem) => { setEditingDomainId(domain.id); setFeedback(null); form.reset(toFormValues(domain)); setIsEditorOpen(true); }; const handleDelete = (domain: ManagedDomainItem) => { if (!window.confirm(`确认删除域名规则 ${domain.domain} 吗?`)) { return; } setFeedback(null); deleteMutation.mutate(domain.id); }; const handleSubmit = form.handleSubmit((values) => { setFeedback(null); saveMutation.mutate(values); }); return ( <>
新增域名 } /> {feedback ? : null} void queryClient.invalidateQueries({ queryKey: managedDomainsQueryKey })} > 刷新 } > {managedDomainsQuery.isLoading ? ( ) : managedDomainsQuery.isError ? ( ) : domains.length === 0 ? ( ) : (
{domains.map((domain) => { const certificate = domain.cert_id ? certificateMap.get(domain.cert_id) : null; const matchType = getMatchTypeMeta(domain.domain); return ( ); })}
域名 类型 绑定证书 状态 备注 更新时间 操作
{domain.domain} {certificate ? certificate.name : '未绑定'} {domain.remark || '—'} {formatDateTime(domain.updated_at)}
handleEdit(domain)} className='px-3 py-2 text-xs' > 编辑 handleDelete(domain)} disabled={deleteMutation.isPending} className='px-3 py-2 text-xs' > 删除
)}
取消 {saveMutation.isPending ? '保存中...' : editingDomainId ? '保存修改' : '新增域名'} } >
form.setValue('cert_id', event.target.value, { shouldDirty: true, shouldValidate: true, }) } > {certificates.map((certificate) => ( ))}
form.setValue('enabled', checked, { shouldDirty: true, shouldValidate: true, }) } />

当前输入: {watchedDomain.trim() || '未填写域名'}

{watchedDomain.trim() ? ( ) : ( )}

建议只为真正需要默认推荐的域名绑定证书;如果同一 hostname 同时命中精确和通配符规则, 系统会优先使用精确匹配。

); }