209 lines
9.4 KiB
TypeScript
209 lines
9.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Building2, DollarSign, Plus, Trash2, TrendingDown, TrendingUp } from 'lucide-react';
|
|
import { adminApi, type TenantManagementRow } from '@/api/adminApi';
|
|
import { Breadcrumb, Button, Input, ManualRechargeDialog, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
|
import { formatCents } from '@/utils/currency';
|
|
|
|
type AdminCustomersPageProps = {
|
|
basePath?: string;
|
|
};
|
|
|
|
type CustomerRow = TenantManagementRow;
|
|
|
|
function ConfirmModal({
|
|
error,
|
|
isSubmitting,
|
|
message,
|
|
onCancel,
|
|
onConfirm,
|
|
type,
|
|
}: {
|
|
error: string;
|
|
isSubmitting: boolean;
|
|
message: string;
|
|
onCancel: () => void;
|
|
onConfirm: () => void;
|
|
type: 'toggle' | 'delete';
|
|
}) {
|
|
return (
|
|
<Modal
|
|
footer={<><Button disabled={isSubmitting} onClick={onCancel} variant="ghost">取消</Button><Button disabled={isSubmitting} onClick={onConfirm} variant={type === 'delete' ? 'danger' : 'primary'}>{isSubmitting ? '处理中...' : '确认'}</Button></>}
|
|
onClose={() => { if (!isSubmitting) onCancel(); }}
|
|
open
|
|
title={type === 'delete' ? '删除企业' : '变更企业状态'}
|
|
>
|
|
<p className="admin-confirm-text">{message}</p>
|
|
{error ? <p className="form-error" role="alert">{error}</p> : null}
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCustomersPageProps) {
|
|
const navigate = useNavigate();
|
|
const [records, setRecords] = useState<CustomerRow[]>([]);
|
|
const [queryName, setQueryName] = useState('');
|
|
const [queryStatus, setQueryStatus] = useState('all');
|
|
const [filters, setFilters] = useState({ name: '', status: 'all' });
|
|
const [confirmAction, setConfirmAction] = useState<{ type: 'toggle' | 'delete'; record: CustomerRow } | null>(null);
|
|
const [confirmError, setConfirmError] = useState('');
|
|
const [confirming, setConfirming] = useState(false);
|
|
const [rechargeTarget, setRechargeTarget] = useState<CustomerRow | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
function loadData() {
|
|
adminApi.listTenantManagementRows()
|
|
.then((items) => {
|
|
setRecords(items);
|
|
setError('');
|
|
})
|
|
.catch((failure: Error) => setError(failure.message || '企业列表加载失败'));
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const filteredRecords = useMemo(() => records.filter((record) => {
|
|
const matchName = filters.name ? record.name.includes(filters.name) : true;
|
|
const matchStatus = filters.status === 'all' ? true : record.status === filters.status;
|
|
return matchName && matchStatus;
|
|
}), [filters, records]);
|
|
|
|
const activeCount = records.filter((record) => record.status === 'active').length;
|
|
const disabledCount = records.filter((record) => record.status === 'disabled').length;
|
|
const totalBalance = records.reduce((sum, record) => sum + (record.account?.balanceCents ?? 0), 0);
|
|
|
|
const columns: Array<TableColumn<CustomerRow>> = [
|
|
{ key: 'name', title: '企业名称', width: '260px', render: (record) => <strong className="table-strong-text">{record.name}</strong> },
|
|
{
|
|
key: 'balance',
|
|
title: '当前余额',
|
|
width: '150px',
|
|
align: 'right',
|
|
render: (record) => {
|
|
const balance = record.account?.balanceCents ?? 0;
|
|
return (
|
|
<span className={balance < 0 ? 'status-danger' : ''}>
|
|
¥{formatCents(balance)}
|
|
{balance < 0 ? <Tag tone="danger" className="enterprise-inline-tag">欠费</Tag> : null}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{ key: 'creditLimit', title: '授信额度', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.account?.creditCents ?? 0)}` },
|
|
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todaySpendCents)}` },
|
|
{ key: 'todayRefund', title: '今日返还', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todayRefundCents)}` },
|
|
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
|
{
|
|
key: 'actions',
|
|
title: '操作',
|
|
align: 'right',
|
|
width: '300px',
|
|
render: (record) => (
|
|
<div className="table-actions">
|
|
<Button icon={<DollarSign size={15} />} onClick={() => openRechargeModal(record)} size="sm" variant="ghost">充值</Button>
|
|
<Button onClick={() => navigate(`${basePath}/${record.id}/edit`)} size="sm" variant="ghost">编辑</Button>
|
|
<Button onClick={() => openConfirmAction({ type: 'toggle', record })} size="sm" variant={record.status === 'active' ? 'warning' : 'success'}>
|
|
{record.status === 'active' ? '禁用' : '启用'}
|
|
</Button>
|
|
<Button icon={<Trash2 size={15} />} onClick={() => openConfirmAction({ type: 'delete', record })} size="sm" variant="danger">删除</Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
function openRechargeModal(record: CustomerRow) {
|
|
setRechargeTarget(record);
|
|
}
|
|
|
|
function openConfirmAction(action: { type: 'toggle' | 'delete'; record: CustomerRow }) {
|
|
setConfirmError('');
|
|
setConfirmAction(action);
|
|
}
|
|
|
|
async function submitConfirmAction() {
|
|
if (!confirmAction) return;
|
|
setConfirming(true);
|
|
setConfirmError('');
|
|
try {
|
|
if (confirmAction.type === 'delete') {
|
|
await adminApi.deleteTenant(confirmAction.record.id);
|
|
} else {
|
|
await adminApi.changeTenantStatus(confirmAction.record.id, confirmAction.record.status === 'active' ? 'disabled' : 'active');
|
|
}
|
|
} catch (failure) {
|
|
const detail = failure instanceof Error ? failure.message : '企业操作失败';
|
|
setConfirmError(`${confirmAction.type === 'delete' ? '删除' : '状态变更'}失败:${detail}`);
|
|
setConfirming(false);
|
|
return;
|
|
}
|
|
setConfirmAction(null);
|
|
try {
|
|
await loadData();
|
|
} catch (failure) {
|
|
setError(failure instanceof Error ? failure.message : '企业列表刷新失败');
|
|
} finally {
|
|
setConfirming(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="page-stack">
|
|
<div className="page-heading">
|
|
<div><Breadcrumb items={['企业管理']} /></div>
|
|
<Button icon={<Plus size={16} />} onClick={() => navigate(`${basePath}/new`)}>添加企业</Button>
|
|
</div>
|
|
{error ? <p className="form-error">{error}</p> : null}
|
|
|
|
<div className="dashboard-grid enterprise-summary-grid">
|
|
<div className="surface mini-status-card"><Building2 size={22} /><div><span>企业总数</span><strong>{records.length}</strong><small>真实租户数量。</small></div></div>
|
|
<div className="surface mini-status-card"><TrendingUp size={22} /><div><span>正常运营</span><strong>{activeCount}</strong><small>可正常提交发送任务。</small></div></div>
|
|
<div className="surface mini-status-card"><TrendingDown size={22} /><div><span>已禁用</span><strong>{disabledCount}</strong><small>已暂停发送能力。</small></div></div>
|
|
<div className="surface mini-status-card"><DollarSign size={22} /><div><span>账户余额</span><strong>¥{formatCents(totalBalance)}</strong><small>企业账户余额汇总。</small></div></div>
|
|
</div>
|
|
|
|
<div className="surface ui-query-panel">
|
|
<h2>查询条件</h2>
|
|
<div className="ui-query-panel__grid enterprise-query-grid">
|
|
<Input label="企业名称" onChange={(event) => setQueryName(event.target.value)} placeholder="请输入企业名称" value={queryName} />
|
|
<Select label="企业状态" onChange={(event) => setQueryStatus(event.target.value)} options={[{ label: '全部状态', value: 'all' }, { label: '正常', value: 'active' }, { label: '禁用', value: 'disabled' }]} value={queryStatus} />
|
|
<div className="enterprise-query-actions">
|
|
<Button onClick={() => setFilters({ name: queryName, status: queryStatus })} variant="secondary">查询</Button>
|
|
<Button onClick={() => { setQueryName(''); setQueryStatus('all'); setFilters({ name: '', status: 'all' }); }} variant="ghost">重置</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="surface section-stack">
|
|
<div className="section-heading"><div><h2>企业列表</h2></div><Tag tone="info">{filteredRecords.length} 条</Tag></div>
|
|
<Table columns={columns} data={filteredRecords} emptyText="暂无企业" rowKey="id" />
|
|
</div>
|
|
|
|
{confirmAction ? (
|
|
<ConfirmModal
|
|
error={confirmError}
|
|
isSubmitting={confirming}
|
|
message={confirmAction.type === 'delete' ? `确认删除企业“${confirmAction.record.name}”吗?` : `确认${confirmAction.record.status === 'active' ? '禁用' : '启用'}企业“${confirmAction.record.name}”吗?`}
|
|
onCancel={() => { setConfirmAction(null); setConfirmError(''); }}
|
|
onConfirm={() => void submitConfirmAction()}
|
|
type={confirmAction.type}
|
|
/>
|
|
) : null}
|
|
<ManualRechargeDialog
|
|
initialTargetId={rechargeTarget?.id}
|
|
lockTarget
|
|
onClose={() => setRechargeTarget(null)}
|
|
onCompleted={loadData}
|
|
open={Boolean(rechargeTarget)}
|
|
targets={rechargeTarget ? [{
|
|
id: rechargeTarget.id,
|
|
name: rechargeTarget.name,
|
|
code: rechargeTarget.code,
|
|
balanceCents: rechargeTarget.account?.balanceCents ?? 0,
|
|
}] : []}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|