feat(onboarding): 删「按用户强制引导」UI + 新增设备管理页
- users 页删 force_onboarding 操作 + types 字段 - 新增设备管理页(/devices): 按设备列表 + 行内「重走引导」+「全部重设」危险操作 + 菜单项 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Button, Modal, Popconfirm, Space, Table, message } from 'antd';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { canDo } from '@/lib/auth';
|
||||
import { useCursorList } from '@/lib/useCursorList';
|
||||
import type { DeviceOnboardingItem } from '@/lib/types';
|
||||
|
||||
const dt = (v: string) => new Date(v).toLocaleString('zh-CN');
|
||||
const EMPTY: Record<string, unknown> = {};
|
||||
|
||||
export default function DevicesPage() {
|
||||
// 后端按设备聚合、全量返回(next_cursor 恒 null),故无筛选/加载更多。
|
||||
const { items, loading, reload } = useCursorList<DeviceOnboardingItem>(
|
||||
'/admin/api/onboarding/devices',
|
||||
EMPTY,
|
||||
);
|
||||
const canReset = canDo(['operator']);
|
||||
|
||||
const resetDevice = async (d: DeviceOnboardingItem) => {
|
||||
try {
|
||||
await api.post(`/admin/api/onboarding/devices/${encodeURIComponent(d.device_id)}/reset`);
|
||||
message.success('已重置:该设备所有账号下次打开 App 重走引导');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
Modal.confirm({
|
||||
title: '确认全部重设新手引导?',
|
||||
content:
|
||||
'清空所有设备的引导完成记录,库里所有用户下次打开 App 都会重走一遍新手引导。此操作不可逆。',
|
||||
okText: '确认全部重设',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await api.post('/admin/api/onboarding/reset-all', { confirm: true });
|
||||
message.success('已全部重设:所有用户下次打开 App 会重走引导');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<DeviceOnboardingItem> = [
|
||||
{ title: '设备(ANDROID_ID)', dataIndex: 'device_id' },
|
||||
{ title: '走过引导的账号数', dataIndex: 'account_count', width: 160 },
|
||||
{ title: '最近完成引导', dataIndex: 'last_completed_at', render: dt, width: 200 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'op',
|
||||
width: 120,
|
||||
render: (_: unknown, d: DeviceOnboardingItem) =>
|
||||
canReset ? (
|
||||
<Popconfirm
|
||||
title="重走引导?"
|
||||
description="该设备上所有账号下次打开 App 会重看一次。"
|
||||
onConfirm={() => resetDevice(d)}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a>重走引导</a>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<span style={{ color: '#ccc' }}>-</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>设备管理</h2>
|
||||
<p style={{ color: '#888', marginBottom: 16 }}>
|
||||
列出走过新手引导的设备(按硬件 ANDROID_ID 聚合)。「重走引导」删除该设备的引导完成记录,
|
||||
使其上所有账号下次打开 App 重走一遍。没走过引导的设备本就会引导,不在此列。
|
||||
</p>
|
||||
{canReset && (
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" danger onClick={resetAll}>
|
||||
全部重设新手引导
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
rowKey="device_id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FundProjectionScreenOutlined,
|
||||
LogoutOutlined,
|
||||
MessageOutlined,
|
||||
MobileOutlined,
|
||||
MoneyCollectOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
@@ -23,6 +24,7 @@ const { Sider, Header, Content } = Layout;
|
||||
const MENU = [
|
||||
{ key: '/dashboard', icon: <DashboardOutlined />, label: '数据大盘' },
|
||||
{ key: '/users', icon: <UserOutlined />, label: '用户管理' },
|
||||
{ key: '/devices', icon: <MobileOutlined />, label: '设备管理' },
|
||||
{ key: '/withdraws', icon: <MoneyCollectOutlined />, label: '提现管理' },
|
||||
{ key: '/price-reports', icon: <FlagOutlined />, label: '上报审核' },
|
||||
{ key: '/feedbacks', icon: <MessageOutlined />, label: '反馈工单' },
|
||||
|
||||
@@ -123,28 +123,6 @@ export default function UsersPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// 一次性操作:点一下把该用户标记为「未看过引导」,下次打开 App 重看一遍,看完后端自动恢复(只触发这一次)。
|
||||
// 用户尚未重看前可「撤销」(把 force_onboarding 置回 false),纠正误点。
|
||||
const setForceOnboarding = (u: UserListItem, enable: boolean) => {
|
||||
Modal.confirm({
|
||||
title: enable
|
||||
? `确认让用户 ${u.phone} 重看一次新手引导?`
|
||||
: `撤销用户 ${u.phone} 的「重看新手引导」?`,
|
||||
content: enable
|
||||
? '点一下即把该用户标记为「未看过引导」:Ta 下次打开 App 会重走一遍新手引导,看完自动恢复——只触发这一次。仅对已支持该功能的 App 版本生效。'
|
||||
: '该用户尚未重看,撤销后下次打开 App 不再触发。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await api.post(`/admin/api/users/${u.id}/force-onboarding`, { enabled: enable });
|
||||
message.success(enable ? '已开启:该用户下次打开 App 会重看一次' : '已撤销');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<UserListItem> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '手机号', dataIndex: 'phone' },
|
||||
@@ -177,15 +155,6 @@ export default function UsersPage() {
|
||||
{u.debug_trace_enabled ? '关调试链接' : '开调试链接'}
|
||||
</a>
|
||||
)}
|
||||
{canStatus &&
|
||||
u.status !== 'deleted' &&
|
||||
(u.force_onboarding ? (
|
||||
<span style={{ color: '#fa8c16' }}>
|
||||
待重看 <a onClick={() => setForceOnboarding(u, false)}>撤销</a>
|
||||
</span>
|
||||
) : (
|
||||
<a onClick={() => setForceOnboarding(u, true)}>开启新手引导</a>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
+7
-2
@@ -29,13 +29,18 @@ export interface UserListItem {
|
||||
status: string;
|
||||
// 调试链接权限:开了的用户在 App 比价结果页/记录页可复制 price.shaguabijia.com 的 trace 调试链接
|
||||
debug_trace_enabled: boolean;
|
||||
// 运营「一键开启新手引导」:true = 该用户下次打开 App 会被强制重走新手引导,走完自动取消
|
||||
force_onboarding: boolean;
|
||||
wechat_openid: string | null;
|
||||
created_at: string;
|
||||
last_login_at: string;
|
||||
}
|
||||
|
||||
// 设备维度新手引导:一台设备(ANDROID_ID)聚合,走过引导的账号数 + 最近完成时间。
|
||||
export interface DeviceOnboardingItem {
|
||||
device_id: string;
|
||||
account_count: number;
|
||||
last_completed_at: string;
|
||||
}
|
||||
|
||||
export interface UserOverview {
|
||||
user: UserListItem;
|
||||
coin_balance: number;
|
||||
|
||||
Reference in New Issue
Block a user