webadmin端

批量删除用户
模板上传图片数量限制改成30
小飞侠接口配置
This commit is contained in:
2026-07-06 18:04:02 +08:00
parent 4e18763968
commit f0b99ea9da
17 changed files with 1172 additions and 49 deletions
+244 -1
View File
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import {
Alert,
Button,
Checkbox,
Descriptions,
Drawer,
Form,
@@ -37,6 +38,28 @@ type UserDetail = AdminUserRow & {
addressCount?: number;
};
type BatchDeletePreviewItem = {
id: string;
userNo: string;
nickname: string | null;
phone: string | null;
hasRisk: boolean;
unfinishedOrders: UserOrderRow[];
redeemRecords: Array<{
id: string;
redeemNo: string;
amount: number;
createdAt: string;
payoutStatus: string | null;
}>;
};
type BatchDeletePreview = {
items: BatchDeletePreviewItem[];
hasRisk: boolean;
total: number;
};
export default function UsersPage() {
const navigate = useNavigate();
const [form] = Form.useForm();
@@ -49,6 +72,13 @@ export default function UsersPage() {
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState('');
const [deleting, setDeleting] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
const [batchDeleteStep, setBatchDeleteStep] = useState<1 | 2>(1);
const [batchPreview, setBatchPreview] = useState<BatchDeletePreview | null>(null);
const [batchPreviewLoading, setBatchPreviewLoading] = useState(false);
const [batchDeleting, setBatchDeleting] = useState(false);
const [batchRiskAck, setBatchRiskAck] = useState(false);
const load = useCallback(async () => {
setLoading(true);
@@ -99,6 +129,7 @@ export default function UsersPage() {
setDeleteOpen(false);
setDrawerOpen(false);
setDetail(null);
setSelectedRowKeys((keys) => keys.filter((k) => k !== detail.id));
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败');
@@ -107,6 +138,60 @@ export default function UsersPage() {
}
}
async function openBatchDeleteModal() {
if (!selectedRowKeys.length) return;
setBatchDeleteStep(1);
setBatchRiskAck(false);
setBatchPreview(null);
setBatchDeleteOpen(true);
setBatchPreviewLoading(true);
try {
const res = await request<BatchDeletePreview>('/admin/users/batch-delete/preview', {
method: 'POST',
body: JSON.stringify({ ids: selectedRowKeys }),
});
setBatchPreview(res);
} catch (e) {
message.error(e instanceof Error ? e.message : '预检失败');
setBatchDeleteOpen(false);
} finally {
setBatchPreviewLoading(false);
}
}
async function confirmBatchDelete(confirmRisk: boolean) {
if (!selectedRowKeys.length) return;
setBatchDeleting(true);
try {
const res = await request<{ deleted: number; message: string }>('/admin/users/batch-delete', {
method: 'POST',
body: JSON.stringify({ ids: selectedRowKeys, confirmRisk }),
});
message.success(res.message || `已删除 ${res.deleted} 名用户`);
setBatchDeleteOpen(false);
setBatchPreview(null);
if (detail && selectedRowKeys.includes(detail.id)) {
setDrawerOpen(false);
setDetail(null);
}
setSelectedRowKeys([]);
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '批量删除失败');
} finally {
setBatchDeleting(false);
}
}
function handleBatchDeleteOk() {
if (!batchPreview) return;
if (batchPreview.hasRisk && batchDeleteStep === 1) {
setBatchDeleteStep(2);
return;
}
void confirmBatchDelete(batchPreview.hasRisk);
}
const orderColumns: ColumnsType<UserOrderRow> = [
{ title: '订单号', dataIndex: 'orderNo', width: 170 },
{
@@ -177,7 +262,16 @@ export default function UsersPage() {
return (
<div>
<Typography.Title level={4}></Typography.Title>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Button
danger
disabled={!selectedRowKeys.length}
onClick={() => void openBatchDeleteModal()}
>
{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
</Button>
</Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
<Form.Item name="phone" label="手机号">
<Input placeholder="模糊搜索" allowClear />
@@ -214,6 +308,11 @@ export default function UsersPage() {
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1200 }}
rowSelection={{
selectedRowKeys,
preserveSelectedRowKeys: true,
onChange: (keys) => setSelectedRowKeys(keys as string[]),
}}
pagination={{
current: page,
pageSize,
@@ -338,6 +437,150 @@ export default function UsersPage() {
</>
)}
</Modal>
<Modal
title={batchDeleteStep === 1 ? `确认批量删除(${selectedRowKeys.length} 人)` : '二次确认:删除关联业务数据'}
open={batchDeleteOpen}
okText={batchPreview?.hasRisk && batchDeleteStep === 1 ? '下一步' : '确认删除'}
okButtonProps={{
danger: batchDeleteStep === 2 || !batchPreview?.hasRisk,
loading: batchPreviewLoading || batchDeleting,
disabled: batchDeleteStep === 2 && !batchRiskAck,
}}
cancelText={batchDeleteStep === 2 ? '上一步' : '取消'}
onOk={() => handleBatchDeleteOk()}
onCancel={() => {
if (batchDeleteStep === 2) {
setBatchDeleteStep(1);
setBatchRiskAck(false);
return;
}
setBatchDeleteOpen(false);
}}
width={800}
destroyOnClose
>
{batchPreviewLoading && (
<Typography.Text type="secondary"></Typography.Text>
)}
{!batchPreviewLoading && batchPreview && batchDeleteStep === 1 && (
<>
<Alert
type={batchPreview.hasRisk ? 'warning' : 'error'}
showIcon
style={{ marginBottom: 16 }}
message={batchPreview.hasRisk ? '部分用户存在未完成订单或核销记录' : '此操作不可恢复'}
description={batchPreview.hasRisk
? '标有「需关注」的用户名下有未完成订单和/或核销记录。继续后将进入二次确认,确认后将一并删除相关订单、核销记录及权益等业务数据。用户行为日志将保留。'
: `将删除 ${batchPreview.total} 名用户及其地址、订单、权益券等业务数据。用户行为日志将保留。`}
/>
<Table
size="small"
rowKey="id"
pagination={false}
scroll={{ x: 640, y: 320 }}
dataSource={batchPreview.items}
expandable={{
rowExpandable: (row) => row.hasRisk,
expandedRowRender: (row) => (
<div style={{ padding: '0 8px 8px' }}>
{row.unfinishedOrders.length > 0 && (
<>
<Typography.Text strong>{row.unfinishedOrders.length}</Typography.Text>
<Table
size="small"
style={{ marginTop: 8, marginBottom: 12 }}
rowKey="id"
pagination={false}
dataSource={row.unfinishedOrders}
columns={orderColumns}
/>
</>
)}
{row.redeemRecords.length > 0 && (
<>
<Typography.Text strong>{row.redeemRecords.length}</Typography.Text>
<Table
size="small"
style={{ marginTop: 8 }}
rowKey="id"
pagination={false}
dataSource={row.redeemRecords}
columns={[
{ title: '核销单号', dataIndex: 'redeemNo', width: 160 },
{ title: '金额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
{
title: '打款状态',
dataIndex: 'payoutStatus',
width: 100,
render: (v) => (v === 'PENDING' ? <Tag color="orange"></Tag> : v === 'PAID' ? <Tag color="green"></Tag> : '—'),
},
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
]}
/>
</>
)}
</div>
),
}}
columns={[
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
{ title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => v || '—' },
{ title: '手机', dataIndex: 'phone', width: 120, render: (v) => v || '—' },
{
title: '风险',
width: 200,
render: (_, row) => (row.hasRisk ? (
<Space size={4} wrap>
<Tag color="warning"></Tag>
{row.unfinishedOrders.length > 0 && (
<Tag color="orange"> {row.unfinishedOrders.length}</Tag>
)}
{row.redeemRecords.length > 0 && (
<Tag color="red"> {row.redeemRecords.length}</Tag>
)}
</Space>
) : (
<Tag color="default"></Tag>
)),
},
]}
/>
</>
)}
{!batchPreviewLoading && batchPreview && batchDeleteStep === 2 && (
<>
<Alert
type="error"
showIcon
style={{ marginBottom: 16 }}
message="即将删除未完成订单与核销记录"
description={(
<>
<strong>{batchPreview.items.filter((i) => i.hasRisk).length}</strong>
<strong></strong>
</>
)}
/>
<Table
size="small"
style={{ marginBottom: 16 }}
rowKey="id"
pagination={false}
scroll={{ y: 200 }}
dataSource={batchPreview.items.filter((i) => i.hasRisk)}
columns={[
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
{ title: '未完成订单', width: 110, render: (_, row) => row.unfinishedOrders.length },
{ title: '核销记录', width: 90, render: (_, row) => row.redeemRecords.length },
]}
/>
<Checkbox checked={batchRiskAck} onChange={(e) => setBatchRiskAck(e.target.checked)}>
</Checkbox>
</>
)}
</Modal>
</div>
);
}