Files
shaguabijia-admin-web/src/app/(main)/users/page.tsx
T
marco 3c757aa838 feat(debug-trace): 用户列表加「开/关调试链接」开关
- UserListItem 加 debug_trace_enabled
- 操作列加开关,调 POST /admin/api/users/{id}/debug-trace(operator 守卫、二次确认,仿封禁)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:31:27 +08:00

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>
);
}