import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, message, Modal, Space, Table, Tag, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined, ReloadOutlined, EditOutlined, DeleteOutlined, InboxOutlined, } from '@ant-design/icons'; import { HttpUtil, ClipboardManager } from '@/utils'; import CustomGeoFormModal from './CustomGeoFormModal'; import type { CustomGeoRecord } from './CustomGeoFormModal'; import './CustomGeoSection.css'; interface CustomGeoSectionProps { active: boolean; } interface CustomGeoListRecord extends CustomGeoRecord { lastUpdatedAt?: number; } function formatTime(ts?: number): string { if (!ts) return ''; const d = new Date(ts * 1000); if (isNaN(d.getTime())) return String(ts); const pad = (n: number) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function relativeTime(ts?: number): string { if (!ts) return ''; const diff = Math.floor(Date.now() / 1000) - ts; if (diff < 60) return 'just now'; if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; if (diff < 86400) return `${Math.floor(diff / 3600)} h ago`; if (diff < 2592000) return `${Math.floor(diff / 86400)} d ago`; return formatTime(ts); } function extDisplay(record: CustomGeoListRecord): string { const fn = record.type === 'geoip' ? `geoip_${record.alias}.dat` : `geosite_${record.alias}.dat`; return `ext:${fn}:tag`; } export default function CustomGeoSection({ active }: CustomGeoSectionProps) { const { t } = useTranslation(); const [modal, modalContextHolder] = Modal.useModal(); const [messageApi, messageContextHolder] = message.useMessage(); const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [updatingAll, setUpdatingAll] = useState(false); const [actionId, setActionId] = useState(null); const [formOpen, setFormOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const loadList = useCallback(async () => { setLoading(true); try { const msg = await HttpUtil.get('/panel/api/custom-geo/list'); if (msg?.success && Array.isArray(msg.obj)) setList(msg.obj); } finally { setLoading(false); } }, []); useEffect(() => { if (active) loadList(); }, [active, loadList]); function openAdd() { setEditingRecord(null); setFormOpen(true); } function openEdit(record: CustomGeoListRecord) { setEditingRecord(record); setFormOpen(true); } async function copyExt(record: CustomGeoListRecord) { const text = extDisplay(record); const ok = await ClipboardManager.copyText(text); if (ok) messageApi.success(`${t('copied')}: ${text}`); } function confirmDelete(record: CustomGeoListRecord) { modal.confirm({ title: t('pages.index.customGeoDelete'), content: t('pages.index.customGeoDeleteConfirm'), okText: t('delete'), okType: 'danger', cancelText: t('cancel'), onOk: async () => { const msg = await HttpUtil.post(`/panel/api/custom-geo/delete/${record.id}`); if (msg?.success) await loadList(); }, }); } async function downloadOne(id: number) { setActionId(id); try { const msg = await HttpUtil.post(`/panel/api/custom-geo/download/${id}`); if (msg?.success) await loadList(); } finally { setActionId(null); } } async function updateAll() { setUpdatingAll(true); try { const msg = await HttpUtil.post('/panel/api/custom-geo/update-all'); const ok = msg?.obj?.succeeded?.length || 0; const failed = msg?.obj?.failed?.length || 0; if (msg?.success || ok > 0) { await loadList(); if (failed > 0) messageApi.warning(`Updated ${ok}, failed ${failed}`); } } finally { setUpdatingAll(false); } } const columns = useMemo>( () => [ { title: t('pages.index.customGeoAlias'), key: 'alias', width: 200, render: (_v, record) => (
{record.type} {record.alias}
), }, { title: t('pages.index.customGeoUrl'), key: 'url', ellipsis: true, render: (_v, record) => ( {record.url} ), }, { title: t('pages.index.customGeoExtColumn'), key: 'extDat', width: 220, render: (_v, record) => ( copyExt(record)} > {extDisplay(record)} ), }, { title: t('pages.index.customGeoLastUpdated'), key: 'lastUpdatedAt', width: 140, render: (_v, record) => record.lastUpdatedAt ? ( {relativeTime(record.lastUpdatedAt)} ) : ( ), }, { title: t('pages.index.customGeoActions'), key: 'action', width: 120, render: (_v, record) => ( {list.length > 0 && {list.length}} r.id} loading={loading} size="small" scroll={{ x: 760 }} locale={{ emptyText: (
{t('pages.index.customGeoEmpty')}
), }} /> setFormOpen(false)} onSaved={loadList} /> ); }