a393b5c0e3
- 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用 - message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19 "Static function can not consume context" 告警;providers 用 <App> 容器包裹全站 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #12 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
'use client';
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { App, Button, Popconfirm, Space, Table } from 'antd';
|
|
import { api, errMsg } from '@/lib/api';
|
|
import { canDo } from '@/lib/auth';
|
|
import { formatUtcTime } from '@/lib/format';
|
|
import { useCursorList } from '@/lib/useCursorList';
|
|
import type { DeviceOnboardingItem } from '@/lib/types';
|
|
|
|
// completed_at 为 UTC 口径(server_default now()),按北京显示(勿用 new Date().toLocaleString)
|
|
const dt = (v: string) => formatUtcTime(v);
|
|
const EMPTY: Record<string, unknown> = {};
|
|
|
|
export default function DevicesPage() {
|
|
const { message, modal } = App.useApp();
|
|
// 后端按设备聚合、全量返回(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>
|
|
);
|
|
}
|