3c757aa838
- UserListItem 加 debug_trace_enabled
- 操作列加开关,调 POST /admin/api/users/{id}/debug-trace(operator 守卫、二次确认,仿封禁)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import {
|
|
Button,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Modal,
|
|
Select,
|
|
Space,
|
|
Table,
|
|
Tag,
|
|
message,
|
|
} from 'antd';
|
|
import { api, errMsg } from '@/lib/api';
|
|
import { canDo } from '@/lib/auth';
|
|
import { useCursorList } from '@/lib/useCursorList';
|
|
import type { UserListItem } from '@/lib/types';
|
|
|
|
const STATUS_COLOR: Record<string, string> = { active: 'green', disabled: 'red', deleted: 'default' };
|
|
|
|
export default function UsersPage() {
|
|
const router = useRouter();
|
|
const [phone, setPhone] = useState('');
|
|
const [status, setStatus] = useState<string | undefined>();
|
|
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
|
const { items, nextCursor, loading, loadMore, reload } = useCursorList<UserListItem>(
|
|
'/admin/api/users',
|
|
filters,
|
|
);
|
|
|
|
const canCoins = canDo(['finance']);
|
|
const canStatus = canDo(['operator']);
|
|
|
|
const [coinUser, setCoinUser] = useState<UserListItem | null>(null);
|
|
const [coinForm] = Form.useForm();
|
|
|
|
const search = () => setFilters({ phone: phone || undefined, status });
|
|
|
|
const submitCoins = async () => {
|
|
const v = await coinForm.validateFields();
|
|
try {
|
|
await api.post(`/admin/api/users/${coinUser!.id}/coins`, v);
|
|
message.success('已调整金币');
|
|
setCoinUser(null);
|
|
coinForm.resetFields();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
};
|
|
|
|
const toggleStatus = (u: UserListItem) => {
|
|
const next = u.status === 'active' ? 'disabled' : 'active';
|
|
Modal.confirm({
|
|
title: `确认${next === 'disabled' ? '封禁' : '解封'}用户 ${u.phone}?`,
|
|
onOk: async () => {
|
|
try {
|
|
await api.post(`/admin/api/users/${u.id}/status`, { status: next });
|
|
message.success('已更新状态');
|
|
reload();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const toggleDebugTrace = (u: UserListItem) => {
|
|
const next = !u.debug_trace_enabled;
|
|
Modal.confirm({
|
|
title: `确认${next ? '开启' : '关闭'}用户 ${u.phone} 的调试链接权限?`,
|
|
content: next
|
|
? '开启后,该用户在 App 比价结果页/记录页可复制 trace 调试链接发给开发排障'
|
|
: undefined,
|
|
onOk: async () => {
|
|
try {
|
|
await api.post(`/admin/api/users/${u.id}/debug-trace`, { enabled: next });
|
|
message.success('已更新调试链接权限');
|
|
reload();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const columns: ColumnsType<UserListItem> = [
|
|
{ title: 'ID', dataIndex: 'id', width: 70 },
|
|
{ title: '手机号', dataIndex: 'phone' },
|
|
{ title: '昵称', dataIndex: 'nickname', render: (v: string | null) => v || '-' },
|
|
{ title: '渠道', dataIndex: 'register_channel', width: 90 },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 90,
|
|
render: (s: string) => <Tag color={STATUS_COLOR[s]}>{s}</Tag>,
|
|
},
|
|
{
|
|
title: '注册时间',
|
|
dataIndex: 'created_at',
|
|
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'op',
|
|
render: (_: unknown, u: UserListItem) => (
|
|
<Space>
|
|
<a onClick={() => router.push(`/users/${u.id}`)}>详情</a>
|
|
{canCoins && <a onClick={() => setCoinUser(u)}>调金币</a>}
|
|
{canStatus && u.status !== 'deleted' && (
|
|
<a onClick={() => toggleStatus(u)}>{u.status === 'active' ? '封禁' : '解封'}</a>
|
|
)}
|
|
{canStatus && u.status !== 'deleted' && (
|
|
<a onClick={() => toggleDebugTrace(u)}>
|
|
{u.debug_trace_enabled ? '关调试链接' : '开调试链接'}
|
|
</a>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<h2>用户管理</h2>
|
|
<Space style={{ marginBottom: 16 }} wrap>
|
|
<Input
|
|
placeholder="手机号前缀"
|
|
value={phone}
|
|
onChange={(e) => setPhone(e.target.value)}
|
|
onPressEnter={search}
|
|
allowClear
|
|
style={{ width: 160 }}
|
|
/>
|
|
<Select
|
|
placeholder="状态"
|
|
value={status}
|
|
onChange={setStatus}
|
|
allowClear
|
|
style={{ width: 120 }}
|
|
options={[
|
|
{ value: 'active', label: 'active' },
|
|
{ value: 'disabled', label: 'disabled' },
|
|
{ value: 'deleted', label: 'deleted' },
|
|
]}
|
|
/>
|
|
<Button type="primary" onClick={search}>
|
|
查询
|
|
</Button>
|
|
</Space>
|
|
<Table rowKey="id" columns={columns} dataSource={items} loading={loading} pagination={false} />
|
|
<div style={{ marginTop: 16, textAlign: 'center' }}>
|
|
<Button onClick={loadMore} disabled={!nextCursor} loading={loading}>
|
|
{nextCursor ? '加载更多' : '没有更多了'}
|
|
</Button>
|
|
</div>
|
|
|
|
<Modal
|
|
title={`调整金币 - ${coinUser?.phone ?? ''}`}
|
|
open={!!coinUser}
|
|
onOk={submitCoins}
|
|
onCancel={() => setCoinUser(null)}
|
|
destroyOnClose
|
|
>
|
|
<Form form={coinForm} layout="vertical">
|
|
<Form.Item name="amount" label="金币变动(正=增加,负=扣减)" rules={[{ required: true }]}>
|
|
<InputNumber style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="reason" label="原因(入审计)" rules={[{ required: true }]}>
|
|
<Input.TextArea rows={2} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|