114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Plus, Search, Trash2 } from 'lucide-react';
|
|
import { Breadcrumb, Button, Input, Modal, Table, Textarea, type TableColumn } from '@/components/ui';
|
|
import { adminApi, type DictionaryItem } from '@/api/adminApi';
|
|
|
|
type GlobalBlacklistItem = DictionaryItem & {
|
|
phoneNumber?: string;
|
|
reason?: string | null;
|
|
};
|
|
|
|
export function AdminGlobalBlacklistPage() {
|
|
const [items, setItems] = useState<GlobalBlacklistItem[]>([]);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [phone, setPhone] = useState('');
|
|
const [reason, setReason] = useState('');
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
function loadData() {
|
|
adminApi.listGlobalBlacklist({ keyword })
|
|
.then((data) => {
|
|
setItems(data as GlobalBlacklistItem[]);
|
|
setError('');
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '全局黑名单加载失败'));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const filteredItems = useMemo(() => items.filter((item) => {
|
|
const text = [item.phoneNumber, item.reason, item.status].join(' ');
|
|
return !keyword || text.includes(keyword);
|
|
}), [items, keyword]);
|
|
|
|
const columns = useMemo<Array<TableColumn<GlobalBlacklistItem>>>(() => [
|
|
{ key: 'phone', title: '手机号码', width: '180px', render: (record) => <strong>{record.phoneNumber}</strong> },
|
|
{ key: 'createdAt', title: '入库时间', width: '190px', render: (record) => record.createdAt ?? '-' },
|
|
{ key: 'reason', title: '入库原因', render: (record) => record.reason ?? '-' },
|
|
{ key: 'status', title: '状态', width: '120px', render: (record) => record.status ?? '-' },
|
|
{
|
|
key: 'actions',
|
|
title: '操作',
|
|
width: '130px',
|
|
align: 'right',
|
|
render: (record) => (
|
|
<Button icon={<Trash2 size={15} />} onClick={() => adminApi.deleteGlobalBlacklist(record.id).then(loadData).catch((failure: Error) => setError(failure.message))} size="sm" variant="danger">
|
|
删除
|
|
</Button>
|
|
),
|
|
},
|
|
], []);
|
|
|
|
function addItem() {
|
|
adminApi.createGlobalBlacklist({ phoneNumber: phone, reason, status: 'active' })
|
|
.then(() => {
|
|
setPhone('');
|
|
setReason('');
|
|
setModalOpen(false);
|
|
loadData();
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '添加全局黑名单失败'));
|
|
}
|
|
|
|
return (
|
|
<section className="page-stack admin-security-page">
|
|
<div className="page-heading">
|
|
<div>
|
|
<Breadcrumb items={['安全控制', '全局黑名单']} />
|
|
<h1>全局黑名单</h1>
|
|
</div>
|
|
<Button icon={<Plus size={16} />} onClick={() => setModalOpen(true)}>添加黑名单</Button>
|
|
</div>
|
|
{error ? <p className="form-error">{error}</p> : null}
|
|
|
|
<div className="surface admin-security-filter">
|
|
<Input
|
|
label="搜索"
|
|
onChange={(event) => setKeyword(event.target.value)}
|
|
placeholder="搜索手机号、原因或状态"
|
|
prefix={<Search size={16} />}
|
|
value={keyword}
|
|
/>
|
|
<div className="admin-security-filter__actions">
|
|
<Button icon={<Search size={16} />} onClick={loadData}>查询</Button>
|
|
<Button onClick={() => setKeyword('')} variant="ghost">重置</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface admin-security-table-card">
|
|
<Table columns={columns} data={filteredItems} emptyText="暂无全局黑名单记录" rowKey="id" />
|
|
</div>
|
|
|
|
<Modal
|
|
footer={(
|
|
<>
|
|
<Button onClick={() => setModalOpen(false)} variant="ghost">取消</Button>
|
|
<Button disabled={!phone} onClick={addItem}>确认添加</Button>
|
|
</>
|
|
)}
|
|
onClose={() => setModalOpen(false)}
|
|
open={modalOpen}
|
|
title="添加全局黑名单"
|
|
>
|
|
<div className="admin-security-form">
|
|
<Input label="手机号码" onChange={(event) => setPhone(event.target.value)} placeholder="请输入手机号码" value={phone} />
|
|
<Textarea label="入库原因" onChange={(event) => setReason(event.target.value)} placeholder="请输入入库原因" rows={3} value={reason} />
|
|
</div>
|
|
</Modal>
|
|
</section>
|
|
);
|
|
}
|