import { useEffect, useState } from 'react'
import { Button, Badge, Select } from '@/components/atoms'
import { Modal } from '@/components/common/Modal'
import { DataTable, type DataColumn } from '@/components/common/DataTable'
import { showToast } from '@/stores/toastStore'
import {
getDnsRules, addDnsRule, updateDnsRule, deleteDnsRule,
getDnsBlacklist, getDnsBlockStats, addDnsBlacklist, deleteDnsBlacklist,
type DnsRule, type DnsBlacklistItem, type DnsBlockCategory,
} from '@/lib/api'
const MATCH_LABELS = ['精确', '通配', '正则']
const MATCH_OPTIONS = [
{ value: '0', label: '精确匹配' },
{ value: '1', label: '通配匹配' },
{ value: '2', label: '正则匹配' },
]
export function DnsPage() {
const [rules, setRules] = useState<DnsRule[]>([])
const [blacklist, setBlacklist] = useState<DnsBlacklistItem[]>([])
const [blockStats, setBlockStats] = useState<DnsBlockCategory[]>([])
const [loading, setLoading] = useState(true)
const [tab, setTab] = useState<'rules' | 'blacklist' | 'stats'>('rules')
const [showAddRule, setShowAddRule] = useState(false)
const [showAddBlack, setShowAddBlack] = useState(false)
const [ruleForm, setRuleForm] = useState({ domain: '', ip: '', kind: 0 })
const [blackForm, setBlackForm] = useState({ domain: '', category: '' })
const fetchData = async () => {
try {
const [r, b, s] = await Promise.all([getDnsRules(), getDnsBlacklist(), getDnsBlockStats()])
setRules(r); setBlacklist(b); setBlockStats(s)
} catch { /* 静默 */ }
setLoading(false)
}
useEffect(() => { fetchData() }, [])
const ruleColumns: DataColumn<DnsRule>[] = [
{ key: 'domain', label: '域名', sortable: true, className: 'font-mono text-xs' },
{ key: 'ip', label: '目标 IP', sortable: true },
{
key: 'kind', label: '匹配', sortable: true,
render: (r) => <span className="text-xs text-[var(--color-text-tertiary)]">{MATCH_LABELS[r.kind] ?? r.kind}</span>,
},
{
key: 'enable', label: '状态', sortable: true,
render: (r) => <Badge variant={r.enable ? 'success' : 'default'}>{r.enable ? '启用' : '禁用'}</Badge>,
},
{
key: 'actions', label: '操作',
render: (r) => (
<div className="flex gap-1">
<Button variant="ghost" size="sm" onClick={async () => {
try { await updateDnsRule(r.id, { kind: r.kind, domain: r.domain, ip: r.ip }); await fetchData() }
catch { showToast('error', '操作失败') }
}}>{r.enable ? '禁用' : '启用'}</Button>
<Button variant="ghost" size="sm" onClick={async () => {
try { await deleteDnsRule(r.id); showToast('success', '已删除'); await fetchData() }
catch { showToast('error', '删除失败') }
}}>删除</Button>
</div>
),
},
]
const blackColumns: DataColumn<DnsBlacklistItem>[] = [
{ key: 'domain', label: '域名', sortable: true, className: 'font-mono text-xs' },
{ key: 'category', label: '分类', render: (b) => <Badge variant="warning">{b.category}</Badge> },
{ key: 'source', label: '来源', render: (b) => <span className="text-xs text-[var(--color-text-tertiary)]">{b.source}</span> },
{
key: 'actions', label: '操作',
render: (b) => (
<Button variant="ghost" size="sm" onClick={async () => {
try { await deleteDnsBlacklist(b.id); showToast('success', '已删除'); await fetchData() }
catch { showToast('error', '删除失败') }
}}>删除</Button>
),
},
]
return (
<div className="p-6">
<h1 className="text-lg font-bold text-[var(--color-text-primary)] mb-4">DNS 管控</h1>
{/* Tab 切换 */}
<div className="flex gap-1 mb-4 bg-[var(--color-surface-2)] rounded-lg p-1 w-fit">
{(['rules', 'blacklist', 'stats'] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
tab === t
? 'bg-[var(--color-surface-0)] text-[var(--color-text-primary)] shadow-sm'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
}`}
>
{t === 'rules' ? '域名规则' : t === 'blacklist' ? '黑名单' : '拦截统计'}
</button>
))}
</div>
{tab === 'rules' && (
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">域名规则</h2>
<Button size="sm" variant="soft" onClick={() => setShowAddRule(true)}>添加规则</Button>
</div>
<DataTable data={rules} columns={ruleColumns} rowKey={(r) => r.id} searchFields={['domain']} searchPlaceholder="搜索域名…" loading={loading} emptyText="暂无规则" />
</div>
)}
{tab === 'blacklist' && (
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">DNS 黑名单</h2>
<Button size="sm" variant="soft" onClick={() => setShowAddBlack(true)}>添加域名</Button>
</div>
<DataTable data={blacklist} columns={blackColumns} rowKey={(b) => b.id} searchFields={['domain', 'category']} searchPlaceholder="搜索域名 / 分类…" loading={loading} emptyText="暂无黑名单" />
</div>
)}
{tab === 'stats' && (
<div className="glass-panel p-5">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] mb-4">拦截统计</h2>
{blockStats.length === 0 ? (
<p className="text-sm text-[var(--color-text-tertiary)] text-center py-8">暂无拦截数据</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
{blockStats.map((s) => (
<div key={s.category} className="p-3 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)] text-center">
<p className="text-2xl font-bold text-[var(--color-brand-500)]">{s.count}</p>
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">{s.category}</p>
</div>
))}
</div>
)}
</div>
)}
{/* 添加规则弹窗 */}
<Modal open={showAddRule} onClose={() => setShowAddRule(false)} maxWidth="max-w-sm">
<div className="p-6">
<h3 className="text-sm font-semibold mb-4 text-[var(--color-text-primary)]">添加 DNS 规则</h3>
<div className="space-y-3">
<InputField label="域名 *" value={ruleForm.domain} onChange={(v) => setRuleForm((f) => ({ ...f, domain: v }))} placeholder="example.com" />
<InputField label="目标 IP *" value={ruleForm.ip} onChange={(v) => setRuleForm((f) => ({ ...f, ip: v }))} placeholder="192.168.1.1" />
<div>
<label className="text-xs text-[var(--color-text-secondary)]">匹配模式</label>
<Select options={MATCH_OPTIONS} value={String(ruleForm.kind)} onChange={(v) => setRuleForm((f) => ({ ...f, kind: Number(v) }))} className="mt-1" />
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="secondary" size="sm" onClick={() => setShowAddRule(false)}>取消</Button>
<Button variant="primary" size="sm" onClick={async () => {
if (!ruleForm.domain || !ruleForm.ip) { showToast('warning', '域名和 IP 为必填'); return }
try { await addDnsRule(ruleForm); showToast('success', '规则已添加'); setShowAddRule(false); setRuleForm({ domain: '', ip: '', kind: 0 }); await fetchData() }
catch { showToast('error', '添加失败') }
}}>确定</Button>
</div>
</div>
</Modal>
{/* 添加黑名单弹窗 */}
<Modal open={showAddBlack} onClose={() => setShowAddBlack(false)} maxWidth="max-w-sm">
<div className="p-6">
<h3 className="text-sm font-semibold mb-4 text-[var(--color-text-primary)]">添加黑名单域名</h3>
<div className="space-y-3">
<InputField label="域名 *" value={blackForm.domain} onChange={(v) => setBlackForm((f) => ({ ...f, domain: v }))} placeholder="example.com" />
<InputField label="分类" value={blackForm.category} onChange={(v) => setBlackForm((f) => ({ ...f, category: v }))} placeholder="如 广告" />
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="secondary" size="sm" onClick={() => setShowAddBlack(false)}>取消</Button>
<Button variant="primary" size="sm" onClick={async () => {
if (!blackForm.domain) { showToast('warning', '域名为必填'); return }
try { await addDnsBlacklist(blackForm); showToast('success', '已添加'); setShowAddBlack(false); setBlackForm({ domain: '', category: '' }); await fetchData() }
catch { showToast('error', '添加失败') }
}}>确定</Button>
</div>
</div>
</Modal>
</div>
)
}
function InputField({ label, value, onChange, placeholder }: {
label: string; value: string; onChange: (v: string) => void; placeholder?: string
}) {
return (
<label className="block">
<span className="text-xs text-[var(--color-text-secondary)]">{label}</span>
<input
value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder}
className="mt-1 w-full px-3 py-2 text-sm rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-0)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-brand-500)]/40"
/>
</label>
)
}
|