fix: connect remaining sms pages to real backend
This commit is contained in:
@@ -1,33 +1,20 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
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 TenantAccount, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import {
|
||||
formatCurrency,
|
||||
getEnterpriseRecords,
|
||||
saveEnterpriseRecords,
|
||||
statusOptions,
|
||||
toggleEnterpriseStatus,
|
||||
type EnterpriseRecord,
|
||||
} from './adminEnterpriseMock';
|
||||
|
||||
type AdminCustomersPageProps = {
|
||||
basePath?: string;
|
||||
};
|
||||
|
||||
type CustomerRow = TenantOption & {
|
||||
account?: TenantAccount;
|
||||
};
|
||||
|
||||
function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCancel: () => void; onConfirm: () => void }) {
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onCancel} variant="ghost">取消</Button>
|
||||
<Button onClick={onConfirm}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onCancel}
|
||||
open
|
||||
title="操作确认"
|
||||
>
|
||||
<Modal footer={<><Button onClick={onCancel} variant="ghost">取消</Button><Button onClick={onConfirm}>确认</Button></>} onClose={onCancel} open title="操作确认">
|
||||
<p className="admin-confirm-text">{message}</p>
|
||||
</Modal>
|
||||
);
|
||||
@@ -35,21 +22,32 @@ function ConfirmModal({ message, onCancel, onConfirm }: { message: string; onCan
|
||||
|
||||
export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCustomersPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const [records, setRecords] = useState<EnterpriseRecord[]>(() => getEnterpriseRecords());
|
||||
const [records, setRecords] = useState<CustomerRow[]>([]);
|
||||
const [queryId, setQueryId] = useState('');
|
||||
const [queryName, setQueryName] = useState('');
|
||||
const [queryStatus, setQueryStatus] = useState('all');
|
||||
const [filters, setFilters] = useState({ id: '', name: '', status: 'all' });
|
||||
const [confirmAction, setConfirmAction] = useState<{ type: 'toggle' | 'delete'; record: EnterpriseRecord } | null>(null);
|
||||
const [confirmAction, setConfirmAction] = useState<{ type: 'toggle' | 'delete'; record: CustomerRow } | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function deleteEnterprise(id: string) {
|
||||
const nextRecords = records.filter((record) => record.id !== id);
|
||||
saveEnterpriseRecords(nextRecords);
|
||||
setRecords(nextRecords);
|
||||
function loadData() {
|
||||
Promise.all([adminApi.listTenants(), adminApi.listAccounts()])
|
||||
.then(([tenants, accounts]) => {
|
||||
setRecords(tenants.filter((tenant) => tenant.status !== 'deleted').map((tenant) => ({
|
||||
...tenant,
|
||||
account: accounts.find((account) => account.tenantId === tenant.id),
|
||||
})));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业列表加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const filteredRecords = useMemo(() => records.filter((record) => {
|
||||
const matchId = filters.id ? record.id.includes(filters.id) : true;
|
||||
const matchId = filters.id ? record.id.includes(filters.id) || record.code.includes(filters.id) : true;
|
||||
const matchName = filters.name ? record.name.includes(filters.name) : true;
|
||||
const matchStatus = filters.status === 'all' ? true : record.status === filters.status;
|
||||
return matchId && matchName && matchStatus;
|
||||
@@ -57,180 +55,81 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
|
||||
const activeCount = records.filter((record) => record.status === 'active').length;
|
||||
const disabledCount = records.filter((record) => record.status === 'disabled').length;
|
||||
const todaySpend = records.reduce((sum, record) => sum + record.todaySpend, 0);
|
||||
const totalBalance = records.reduce((sum, record) => sum + (record.account?.balanceCents ?? 0), 0);
|
||||
|
||||
const columns: Array<TableColumn<EnterpriseRecord>> = [
|
||||
{ key: 'id', title: '企业ID', width: '90px', render: (record) => record.id },
|
||||
const columns: Array<TableColumn<CustomerRow>> = [
|
||||
{ key: 'id', title: '企业ID', width: '220px', render: (record) => record.id },
|
||||
{ key: 'name', title: '企业名称', render: (record) => <strong>{record.name}</strong> },
|
||||
{
|
||||
key: 'balance',
|
||||
title: '当前余额',
|
||||
align: 'right',
|
||||
render: (record) => (
|
||||
<span className={record.balance < 0 ? 'status-danger' : ''}>
|
||||
¥{formatCurrency(record.balance)}
|
||||
{record.balance < 0 ? <Tag tone="danger" className="enterprise-inline-tag">欠费</Tag> : null}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ key: 'overdraftLimit', title: '透支限额', align: 'right', render: (record) => `¥${formatCurrency(record.overdraftLimit)}` },
|
||||
{ key: 'todaySpend', title: '今日消费', align: 'right', render: (record) => `¥${formatCurrency(record.todaySpend)}` },
|
||||
{
|
||||
key: 'status',
|
||||
title: '企业状态',
|
||||
render: (record) => (
|
||||
<Tag tone={record.status === 'active' ? 'success' : 'warning'}>
|
||||
{record.status === 'active' ? '正常' : '已禁用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ key: 'code', title: '企业编码', render: (record) => record.code },
|
||||
{ key: 'balance', title: '现金余额', align: 'right', render: (record) => `¥${((record.account?.balanceCents ?? 0) / 100).toLocaleString('zh-CN')}` },
|
||||
{ key: 'smsUnits', title: '短信余量', align: 'right', render: (record) => `${(record.account?.smsUnits ?? 0).toLocaleString('zh-CN')} 条` },
|
||||
{ key: 'status', title: '企业状态', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
render: (record) => (
|
||||
<div className="table-actions">
|
||||
<Button
|
||||
onClick={() => navigate(`${basePath}/${record.id}/edit`)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setConfirmAction({ type: 'toggle', record })}
|
||||
size="sm"
|
||||
variant={record.status === 'active' ? 'danger' : 'secondary'}
|
||||
>
|
||||
<Button onClick={() => navigate(`${basePath}/${record.id}`)} size="sm" variant="ghost">详情</Button>
|
||||
<Button onClick={() => navigate(`${basePath}/${record.id}/edit`)} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => setConfirmAction({ type: 'toggle', record })} size="sm" variant={record.status === 'active' ? 'warning' : 'success'}>
|
||||
{record.status === 'active' ? '禁用' : '启用'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Trash2 size={15} />}
|
||||
onClick={() => setConfirmAction({ type: 'delete', record })}
|
||||
size="sm"
|
||||
variant="danger"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button icon={<Trash2 size={15} />} onClick={() => setConfirmAction({ type: 'delete', record })} size="sm" variant="danger">删除</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function submitConfirmAction() {
|
||||
if (!confirmAction) return;
|
||||
const action = confirmAction.type === 'delete'
|
||||
? adminApi.deleteTenant(confirmAction.record.id)
|
||||
: adminApi.changeTenantStatus(confirmAction.record.id, confirmAction.record.status === 'active' ? 'disabled' : 'active');
|
||||
action.then(() => {
|
||||
setConfirmAction(null);
|
||||
loadData();
|
||||
}).catch((failure: Error) => setError(failure.message || '企业状态更新失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['企业管理']} />
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={() => navigate(`${basePath}/new`)}>
|
||||
添加企业
|
||||
</Button>
|
||||
<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>¥{formatCurrency(todaySpend)}</strong>
|
||||
<small>列表内企业消费汇总。</small>
|
||||
</div>
|
||||
</div>
|
||||
<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>¥{(totalBalance / 100).toLocaleString('zh-CN')}</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="企业ID"
|
||||
onChange={(event) => setQueryId(event.target.value)}
|
||||
placeholder="请输入企业ID"
|
||||
value={queryId}
|
||||
/>
|
||||
<Input
|
||||
label="企业名称"
|
||||
onChange={(event) => setQueryName(event.target.value)}
|
||||
placeholder="请输入企业名称"
|
||||
value={queryName}
|
||||
/>
|
||||
<Select
|
||||
label="企业状态"
|
||||
onChange={(event) => setQueryStatus(event.target.value)}
|
||||
options={statusOptions}
|
||||
value={queryStatus}
|
||||
/>
|
||||
<Input label="企业ID/编码" onChange={(event) => setQueryId(event.target.value)} placeholder="请输入企业ID或编码" value={queryId} />
|
||||
<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({ id: queryId, name: queryName, status: queryStatus })}
|
||||
variant="secondary"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setQueryId('');
|
||||
setQueryName('');
|
||||
setQueryStatus('all');
|
||||
setFilters({ id: '', name: '', status: 'all' });
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button onClick={() => setFilters({ id: queryId, name: queryName, status: queryStatus })} variant="secondary">查询</Button>
|
||||
<Button onClick={() => { setQueryId(''); setQueryName(''); setQueryStatus('all'); setFilters({ id: '', name: '', status: 'all' }); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="surface section-stack">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<h2>企业列表</h2>
|
||||
<p className="muted">纯前端 mock 数据,支持新增、编辑、启用和禁用。</p>
|
||||
</div>
|
||||
<Tag tone="info">{filteredRecords.length} 条</Tag>
|
||||
</div>
|
||||
<Table columns={columns} data={filteredRecords} rowKey="id" />
|
||||
<div className="section-heading"><div><h2>企业列表</h2><p className="muted">数据来自租户、账户真实接口。</p></div><Tag tone="info">{filteredRecords.length} 条</Tag></div>
|
||||
<Table columns={columns} data={filteredRecords} emptyText="暂无企业" rowKey="id" />
|
||||
</div>
|
||||
|
||||
{confirmAction ? (
|
||||
<ConfirmModal
|
||||
message={confirmAction.type === 'delete'
|
||||
? `确认删除企业“${confirmAction.record.name}”吗?`
|
||||
: `确认${confirmAction.record.status === 'active' ? '禁用' : '启用'}企业“${confirmAction.record.name}”吗?`}
|
||||
message={confirmAction.type === 'delete' ? `确认删除企业“${confirmAction.record.name}”吗?` : `确认${confirmAction.record.status === 'active' ? '禁用' : '启用'}企业“${confirmAction.record.name}”吗?`}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
onConfirm={() => {
|
||||
if (confirmAction.type === 'delete') {
|
||||
deleteEnterprise(confirmAction.record.id);
|
||||
} else {
|
||||
setRecords(toggleEnterpriseStatus(confirmAction.record.id));
|
||||
}
|
||||
setConfirmAction(null);
|
||||
}}
|
||||
onConfirm={submitConfirmAction}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user