import React, { useEffect, useState } from 'react'; import { Button, Dropdown, Form, Header, Icon, Input, Label, Segment, Table, TextArea, } from 'semantic-ui-react'; import { API, showError, showSuccess, formatDateTime } from '../../helpers'; const emptyHeader = () => ({ key: '', value: '' }); const parseCustomHeaders = (rawValue) => { if (Array.isArray(rawValue)) { return rawValue; } if (!rawValue) { return []; } try { const parsed = JSON.parse(rawValue); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } }; const initialForm = { domain: '', origin_url: '', enabled: true, enable_https: false, cert_id: '', redirect_http: false, custom_headers: [emptyHeader()], remark: '', }; const ProxyRoute = () => { const [routes, setRoutes] = useState([]); const [certificates, setCertificates] = useState([]); const [loading, setLoading] = useState(false); const [publishing, setPublishing] = useState(false); const [matching, setMatching] = useState(false); const [matchResult, setMatchResult] = useState(null); const [form, setForm] = useState(initialForm); const [editingId, setEditingId] = useState(null); const loadRoutes = async () => { setLoading(true); const res = await API.get('/api/proxy-routes/'); const { success, message, data } = res.data; if (success) { setRoutes(data || []); } else { showError(message); } setLoading(false); }; const loadCertificates = async () => { const res = await API.get('/api/tls-certificates/'); const { success, message, data } = res.data; if (success) { setCertificates(data || []); } else { showError(message); } }; useEffect(() => { loadRoutes().then(); loadCertificates().then(); }, []); useEffect(() => { if (!form.enable_https) { setMatchResult(null); return undefined; } const domain = form.domain.trim().toLowerCase(); if (!domain) { setMatchResult(null); return undefined; } const timer = setTimeout(async () => { setMatching(true); const res = await API.get('/api/managed-domains/match', { params: { domain }, }); const { success, message, data } = res.data; if (success) { setMatchResult(data || null); if (data?.matched && data?.candidate?.certificate_id) { setForm((current) => { if (!current.enable_https || current.domain.trim().toLowerCase() !== domain || current.cert_id) { return current; } return { ...current, cert_id: data.candidate.certificate_id }; }); } } else { setMatchResult(null); showError(message); } setMatching(false); }, 400); return () => clearTimeout(timer); }, [form.domain, form.enable_https]); const resetForm = () => { setForm(initialForm); setEditingId(null); setMatchResult(null); }; const submitRoute = async () => { const payload = { ...form, domain: form.domain.trim(), origin_url: form.origin_url.trim(), cert_id: form.enable_https && form.cert_id ? Number(form.cert_id) : null, custom_headers: (form.custom_headers || []).filter((item) => item.key.trim() || item.value.trim()), remark: form.remark.trim(), }; const res = editingId ? await API.put(`/api/proxy-routes/${editingId}`, payload) : await API.post('/api/proxy-routes/', payload); const { success, message } = res.data; if (success) { showSuccess(editingId ? '规则已更新' : '规则已创建'); resetForm(); await loadRoutes(); } else { showError(message); } }; const deleteRoute = async (id) => { const res = await API.delete(`/api/proxy-routes/${id}`); const { success, message } = res.data; if (success) { showSuccess('规则已删除'); await loadRoutes(); } else { showError(message); } }; const publishConfig = async () => { setPublishing(true); const res = await API.post('/api/config-versions/publish'); const { success, message, data } = res.data; if (success) { showSuccess(`发布成功,版本 ${data.version}`); } else { showError(message); } setPublishing(false); }; const beginEdit = (route) => { const customHeaders = parseCustomHeaders(route.custom_headers); setEditingId(route.id); setForm({ domain: route.domain, origin_url: route.origin_url, enabled: route.enabled, enable_https: route.enable_https || false, cert_id: route.cert_id || '', redirect_http: route.redirect_http || false, custom_headers: customHeaders.length > 0 ? customHeaders : [emptyHeader()], remark: route.remark || '', }); setMatchResult(null); }; const updateCustomHeader = (index, field, value) => { setForm((current) => ({ ...current, custom_headers: current.custom_headers.map((item, itemIndex) => ( itemIndex === index ? { ...item, [field]: value } : item )), })); }; const addCustomHeader = () => { setForm((current) => ({ ...current, custom_headers: [...(current.custom_headers || []), emptyHeader()], })); }; const removeCustomHeader = (index) => { setForm((current) => { const nextHeaders = (current.custom_headers || []).filter((item, itemIndex) => itemIndex !== index); return { ...current, custom_headers: nextHeaders.length > 0 ? nextHeaders : [emptyHeader()], }; }); }; const certificateOptions = certificates.map((certificate) => ({ key: certificate.id, text: `${certificate.name} (${certificate.not_after ? formatDateTime(certificate.not_after) : 'unknown'})`, value: certificate.id, })); return (
反代规则

维护 Host 到 Origin 的映射,并可直接触发发布。

setForm({ ...form, domain: value })} /> setForm({ ...form, origin_url: value })} /> setForm({ ...form, enable_https: checked, cert_id: checked ? form.cert_id : '', redirect_http: checked ? form.redirect_http : false, }) } style={{ alignSelf: 'flex-end', marginBottom: '1rem' }} /> setForm({ ...form, cert_id: value || '' })} /> {form.enable_https ? (

{matching ? '正在按域名自动匹配证书...' : matchResult?.matched ? `已匹配${matchResult.candidate?.match_type === 'exact' ? '精确' : '通配符'}规则 ${matchResult.candidate?.domain},推荐证书:${matchResult.candidate?.certificate_name}` : form.domain.trim() ? '未找到匹配证书,可继续手动选择。' : '输入域名后会自动匹配托管证书。'}

) : null} setForm({ ...form, remark: value })} /> setForm({ ...form, enabled: checked })} style={{ alignSelf: 'flex-end', marginBottom: '1rem' }} /> setForm({ ...form, redirect_http: checked })} style={{ alignSelf: 'flex-end', marginBottom: '1rem' }} /> {(form.custom_headers || []).map((header, index) => ( updateCustomHeader(index, 'key', value)} /> updateCustomHeader(index, 'value', value)} /> {editingId ? ( ) : null} 域名 源站地址 HTTPS 自定义头 状态 备注 更新时间 操作 {routes.map((route) => ( {route.domain} {route.origin_url} {route.enable_https ? ( ) : ( )} {parseCustomHeaders(route.custom_headers).length > 0 ? ( ) : ( )} {route.enabled ? : } {route.remark || '无'} {formatDateTime(route.updated_at)} ))}
); }; export default ProxyRoute;