短信验证调试成功
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Descriptions, Drawer, Form, Input, Segmented, Space, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { USER_LOG_CATEGORY_OPTIONS, resolveUserLogCategory, USER_LOG_CATEGORY_LABELS, type UserLogCategory } from '../lib/user-log';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { request } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
userNo: string | null;
|
||||
phone: string | null;
|
||||
nickname: string | null;
|
||||
category: UserLogCategory | null;
|
||||
eventName: string;
|
||||
clientApp: string | null;
|
||||
refType: string | null;
|
||||
refId: string | null;
|
||||
extraJson: Record<string, unknown> | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS = USER_LOG_CATEGORY_LABELS;
|
||||
|
||||
function summarizeExtra(json: Record<string, unknown> | null) {
|
||||
if (!json) return '—';
|
||||
const text = JSON.stringify(json);
|
||||
return text.length > 80 ? `${text.slice(0, 80)}…` : text;
|
||||
}
|
||||
|
||||
export default function UserLogsPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [form] = Form.useForm();
|
||||
const [category, setCategory] = useState(searchParams.get('category') ?? '');
|
||||
const [filters, setFilters] = useState<Record<string, string>>(() => ({
|
||||
userId: searchParams.get('userId') ?? '',
|
||||
phone: searchParams.get('phone') ?? '',
|
||||
userNo: searchParams.get('userNo') ?? '',
|
||||
eventName: searchParams.get('eventName') ?? '',
|
||||
}));
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||
'/admin/logs/users',
|
||||
() => {
|
||||
const qs = new URLSearchParams();
|
||||
if (filters.userId) qs.set('userId', filters.userId);
|
||||
if (filters.phone) qs.set('phone', filters.phone);
|
||||
if (filters.userNo) qs.set('userNo', filters.userNo);
|
||||
if (filters.eventName) qs.set('eventName', filters.eventName);
|
||||
if (category) qs.set('category', category);
|
||||
return qs;
|
||||
},
|
||||
[filters, category],
|
||||
);
|
||||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(filters);
|
||||
}, [form, filters]);
|
||||
|
||||
const columns: ColumnsType<Row> = [
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
title: '用户', width: 160,
|
||||
render: (_, r) => (
|
||||
<div>
|
||||
<div>{r.nickname || r.userNo || '—'}</div>
|
||||
<div style={{ color: '#999', fontSize: 12 }}>{r.phone || r.userId || '—'}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分类', dataIndex: 'category', width: 100,
|
||||
render: (v: UserLogCategory | null, r) => (
|
||||
<Tag>{CATEGORY_LABELS[v ?? ''] || resolveUserLogCategory(r.eventName) || '其他'}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '事件', dataIndex: 'eventName', width: 160 },
|
||||
{
|
||||
title: '关联', width: 120,
|
||||
render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'),
|
||||
},
|
||||
{
|
||||
title: '摘要', ellipsis: true,
|
||||
render: (_, r) => summarizeExtra(r.extraJson),
|
||||
},
|
||||
{
|
||||
title: '操作', width: 80,
|
||||
render: (_, row) => (
|
||||
<Button type="link" size="small" onClick={async () => {
|
||||
setDetail(await request(`/admin/logs/users/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}>详情</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>用户日志</Typography.Title>
|
||||
<Segmented
|
||||
style={{ marginBottom: 16 }}
|
||||
options={USER_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
|
||||
value={category}
|
||||
onChange={(v) => {
|
||||
setCategory(String(v));
|
||||
setPage(1);
|
||||
const next = new URLSearchParams(searchParams);
|
||||
if (v) next.set('category', String(v));
|
||||
else next.delete('category');
|
||||
setSearchParams(next);
|
||||
}}
|
||||
/>
|
||||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => {
|
||||
setFilters(v);
|
||||
setPage(1);
|
||||
const next = new URLSearchParams(searchParams);
|
||||
for (const key of ['userId', 'phone', 'userNo', 'eventName'] as const) {
|
||||
if (v[key]) next.set(key, v[key]);
|
||||
else next.delete(key);
|
||||
}
|
||||
setSearchParams(next);
|
||||
}}>
|
||||
<Form.Item name="userId" label="用户ID"><Input allowClear style={{ width: 120 }} /></Form.Item>
|
||||
<Form.Item name="phone" label="手机号"><Input allowClear style={{ width: 130 }} /></Form.Item>
|
||||
<Form.Item name="userNo" label="用户编号"><Input allowClear style={{ width: 120 }} /></Form.Item>
|
||||
<Form.Item name="eventName" label="事件名"><Input allowClear style={{ width: 160 }} placeholder="pay_success" /></Form.Item>
|
||||
<Form.Item><Button type="primary" htmlType="submit">筛选</Button></Form.Item>
|
||||
<Form.Item><Button onClick={() => { form.resetFields(); setFilters({}); setCategory(''); setSearchParams({}); setPage(1); }}>重置</Button></Form.Item>
|
||||
</Form>
|
||||
<Table rowKey="id" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1000 }}
|
||||
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||||
<Drawer title="日志详情" width={520} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||
{detail && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户">{String(detail.nickname || detail.userNo || detail.userId || '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机">{String(detail.phone || '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">{CATEGORY_LABELS[String(detail.category ?? '')] || String(detail.category || '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="事件">{String(detail.eventName)}</Descriptions.Item>
|
||||
<Descriptions.Item label="客户端">{String(detail.clientApp || '—')}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联">{detail.refType ? `${String(detail.refType)}#${String(detail.refId)}` : '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
|
||||
<Descriptions.Item label="参数">
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||
{JSON.stringify(detail.extraJson ?? {}, null, 2)}
|
||||
</pre>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user