小飞侠接口调通测试
This commit is contained in:
@@ -33,7 +33,16 @@ export async function request<T>(path: string, options: RequestInit = {}): Promi
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||
const json = await res.json();
|
||||
const text = await res.text();
|
||||
if (!text.trim()) {
|
||||
throw new Error(`接口空响应(HTTP ${res.status} ${res.statusText || ''})`);
|
||||
}
|
||||
let json: { code: number; message?: string; data?: T };
|
||||
try {
|
||||
json = JSON.parse(text) as { code: number; message?: string; data?: T };
|
||||
} catch {
|
||||
throw new Error(`接口返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
||||
}
|
||||
if (json.code === 401) {
|
||||
clearAuth();
|
||||
window.location.href = '/login';
|
||||
@@ -70,6 +79,8 @@ export type AdminUserRow = {
|
||||
phone: string | null;
|
||||
phoneVerifiedAt: string | null;
|
||||
mergedIntoUserId: string | null;
|
||||
wxOpenId: string | null;
|
||||
wechatVerified: boolean;
|
||||
nickname: string | null;
|
||||
status: number;
|
||||
createdAt: string;
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { request, type AdminUserRow, type Paginated } from '../lib/api';
|
||||
import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
|
||||
type UserOrderRow = {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
status: string;
|
||||
payAmount: number;
|
||||
payStatus?: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type UserDetail = AdminUserRow & {
|
||||
wxUnionId?: string | null;
|
||||
cityPref?: Record<string, unknown> | null;
|
||||
mergedInto?: { id: string; userNo: string; phone: string | null; nickname: string | null } | null;
|
||||
orders?: Array<{ id: string; orderNo: string; status: string; payAmount: number; createdAt: string }>;
|
||||
orders?: UserOrderRow[];
|
||||
mergedFromCount?: number;
|
||||
orderCount?: number;
|
||||
addressCount?: number;
|
||||
};
|
||||
|
||||
@@ -33,6 +46,9 @@ export default function UsersPage() {
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [detail, setDetail] = useState<UserDetail | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState('');
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -65,6 +81,44 @@ export default function UsersPage() {
|
||||
setDrawerOpen(true);
|
||||
}
|
||||
|
||||
function openDeleteModal() {
|
||||
setDeleteConfirm('');
|
||||
setDeleteOpen(true);
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!detail) return;
|
||||
if (deleteConfirm !== detail.userNo) {
|
||||
message.error('请输入正确的用户编号以确认删除');
|
||||
return;
|
||||
}
|
||||
setDeleting(true);
|
||||
try {
|
||||
await request(`/admin/users/${detail.id}`, { method: 'DELETE' });
|
||||
message.success('用户已删除(行为日志已保留)');
|
||||
setDeleteOpen(false);
|
||||
setDrawerOpen(false);
|
||||
setDetail(null);
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const orderColumns: ColumnsType<UserOrderRow> = [
|
||||
{ title: '订单号', dataIndex: 'orderNo', width: 170 },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (s) => <Tag>{ORDER_STATUS_LABELS[s] || s}</Tag>,
|
||||
},
|
||||
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` },
|
||||
{ title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
];
|
||||
|
||||
const columns: ColumnsType<AdminUserRow> = [
|
||||
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
|
||||
{ title: '昵称', dataIndex: 'nickname', width: 100 },
|
||||
@@ -80,6 +134,12 @@ export default function UsersPage() {
|
||||
width: 90,
|
||||
render: (v) => (v ? <Tag color="green">已验证</Tag> : <Tag color="orange">访客</Tag>),
|
||||
},
|
||||
{
|
||||
title: '微信',
|
||||
dataIndex: 'wechatVerified',
|
||||
width: 100,
|
||||
render: (v) => (v ? <Tag color="blue">微信已验证</Tag> : <Tag>未绑定</Tag>),
|
||||
},
|
||||
{
|
||||
title: 'deviceKey',
|
||||
dataIndex: 'deviceKey',
|
||||
@@ -153,7 +213,7 @@ export default function UsersPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 1100 }}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
@@ -166,7 +226,15 @@ export default function UsersPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer title="用户详情" width={560} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
<Drawer
|
||||
title="用户详情"
|
||||
width={640}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
extra={detail && (
|
||||
<Button danger onClick={openDeleteModal}>删除用户</Button>
|
||||
)}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small">
|
||||
@@ -177,6 +245,11 @@ export default function UsersPage() {
|
||||
<Descriptions.Item label="验手机时间">
|
||||
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="微信验证">
|
||||
{detail.wechatVerified ? <Tag color="blue">微信已验证</Tag> : <Tag>未绑定</Tag>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="wxOpenId">{detail.wxOpenId || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="wxUnionId">{detail.wxUnionId || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="合并至">
|
||||
{detail.mergedInto
|
||||
@@ -189,22 +262,20 @@ export default function UsersPage() {
|
||||
{new Date(detail.createdAt).toLocaleString('zh-CN')}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.orders && detail.orders.length > 0 && (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>最近订单</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={detail.orders}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '金额', dataIndex: 'payAmount', render: (v) => `¥${v}` },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
全部订单({detail.orders?.length ?? 0})
|
||||
</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: 520, y: 240 }}
|
||||
dataSource={detail.orders ?? []}
|
||||
columns={orderColumns}
|
||||
locale={{ emptyText: '暂无订单' }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginTop: 16 }}
|
||||
@@ -215,6 +286,58 @@ export default function UsersPage() {
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="确认删除用户"
|
||||
open={deleteOpen}
|
||||
okText="确认删除"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: !detail || deleteConfirm !== detail.userNo,
|
||||
loading: deleting,
|
||||
}}
|
||||
onOk={() => void confirmDelete()}
|
||||
onCancel={() => setDeleteOpen(false)}
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="此操作不可恢复"
|
||||
description={(
|
||||
<>
|
||||
将删除用户 <strong>{detail.userNo}</strong> 及其地址、订单、权益券、核销记录等业务数据。
|
||||
<br />
|
||||
用户行为日志(埋点)与第三方调用日志将<strong>保留</strong>,不随用户删除。
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Typography.Text strong>关联订单({detail.orders?.length ?? 0} 笔)</Typography.Text>
|
||||
<Table
|
||||
size="small"
|
||||
style={{ marginTop: 8, marginBottom: 16 }}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: 520, y: 200 }}
|
||||
dataSource={detail.orders ?? []}
|
||||
columns={orderColumns}
|
||||
locale={{ emptyText: '无订单' }}
|
||||
/>
|
||||
<Typography.Paragraph type="secondary">
|
||||
请输入用户编号 <Typography.Text code>{detail.userNo}</Typography.Text> 以确认删除:
|
||||
</Typography.Paragraph>
|
||||
<Input
|
||||
value={deleteConfirm}
|
||||
placeholder={detail.userNo}
|
||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user