'use client'; import {useId, useMemo} from 'react'; import {Link2, Minus, Plus} from 'lucide-react'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select'; import type {TlsCertificateItem} from '@/lib/services/openflare'; import {WebsiteService} from '@/lib/services/openflare'; import {validateDomain} from '../../components/helpers'; import type {DomainListRow} from '../helpers'; function ensureRows(rows: DomainListRow[]) { return rows.length > 0 ? rows : [{ domain: '', certificateId: '' }]; } function buildSuggestionSources(domains: string[]) { const values = new Set(); for (const domain of domains) { const normalized = domain.trim().toLowerCase().replace(/^\*\./, ''); if (!normalized) { continue; } values.add(normalized); const segments = normalized.split('.'); for (let index = 1; index < segments.length - 1; index += 1) { values.add(segments.slice(index).join('.')); } } return Array.from(values); } function buildDomainSuggestions(input: string, sources: string[], rows: DomainListRow[]) { const normalizedInput = input.trim().toLowerCase(); if (!normalizedInput) { return []; } const existingDomains = new Set( rows .map((row) => row.domain.trim().toLowerCase()) .filter((row) => row && row !== normalizedInput), ); const suggestions: string[] = []; for (const source of sources) { if (source.startsWith(normalizedInput) && source !== normalizedInput) { suggestions.push(source); } const separatorIndex = normalizedInput.lastIndexOf('.'); if (separatorIndex <= 0) { continue; } const prefix = normalizedInput.slice(0, separatorIndex); const suffixInput = normalizedInput.slice(separatorIndex + 1); if (!suffixInput || source.startsWith(suffixInput)) { suggestions.push(`${prefix}.${source}`); } } return suggestions.filter((suggestion, index) => { return ( suggestion !== normalizedInput && !existingDomains.has(suggestion) && suggestions.indexOf(suggestion) === index ); }); } interface DomainListInputProps { rows: DomainListRow[]; onChange: (rows: DomainListRow[]) => void; onBlur?: () => void; suggestionSources?: string[]; certificates?: TlsCertificateItem[]; domainPlaceholder?: string; } export function DomainListInput({ rows, onChange, onBlur, suggestionSources = [], certificates = [], domainPlaceholder = 'app.example.com', }: DomainListInputProps) { const listId = useId(); const safeRows = ensureRows(rows); const normalizedSources = useMemo( () => buildSuggestionSources(suggestionSources), [suggestionSources], ); const updateRows = (nextRows: DomainListRow[]) => { onChange(ensureRows(nextRows)); }; const applyManagedDomainCertificate = async (index: number, domain: string) => { const normalized = domain.trim().toLowerCase(); if (!normalized || validateDomain(normalized)) { return; } try { const result = await WebsiteService.match(normalized); if (!result.matched || !result.candidate?.certificate_id) { return; } const nextRows = safeRows.slice(); if (!nextRows[index].certificateId) { nextRows[index] = { ...nextRows[index], certificateId: String(result.candidate.certificate_id), }; updateRows(nextRows); } } catch { // Ignore match failures; user can still pick a certificate manually. } }; return (
{safeRows.map((row, index) => { const suggestions = buildDomainSuggestions(row.domain, normalizedSources, safeRows).slice( 0, 4, ); return (
{ onBlur?.(); void applyManagedDomainCertificate(index, row.domain); }} onChange={(event) => { const nextRows = safeRows.slice(); nextRows[index] = { ...nextRows[index], domain: event.target.value, }; updateRows(nextRows); }} /> {suggestions.map((suggestion) => ( {suggestions.length > 0 ? (
{suggestions.map((suggestion) => ( ))}
) : null}
); })}
输入域名后将自动匹配托管域名证书
); }