From 1ef3a55359ded2dd5193e9cadcb7a98a67837007 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Thu, 18 Jun 2026 20:08:06 +0800 Subject: [PATCH] feat(users): add user device management and registration ID display Add UserDevice interface and API endpoint for fetching user devices. Implement device dialog in the users page with copy-to-clipboard functionality for push tokens and registration IDs. --- src/api/index.ts | 2 +- src/api/users.ts | 18 +++++++ src/pages/Users.tsx | 129 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 806d1ac..f3e84ee 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,7 @@ export { default as apiClient } from './client' export { authApi } from './auth' export { usersApi } from './users' -export type { UserQueryParams, UserDetail } from './users' +export type { UserQueryParams, UserDetail, UserDevice } from './users' export { rolesApi } from './roles' export type { Permission, RoleInfo, RoleDetail } from './roles' export { postsApi } from './posts' diff --git a/src/api/users.ts b/src/api/users.ts index 39929fd..9765925 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -17,6 +17,20 @@ export interface UserDetail extends User { following_count: number } +// 用户设备(含 JPush RegistrationID) +export interface UserDevice { + id: number + user_id: string + device_id: string + device_type: string // ios | android | web + push_token?: string // JPush RegistrationID + is_active: boolean + device_name?: string + last_used_at?: string + created_at: string + updated_at: string +} + // 用户管理API export const usersApi = { // 获取用户列表(分页、搜索、筛选) @@ -27,6 +41,10 @@ export const usersApi = { getUserById: (id: string) => apiClient.get>(`/admin/users/${id}`), + // 获取用户设备列表(含 registration_id / push_token) + getUserDevices: (id: string) => + apiClient.get>(`/admin/users/${id}/devices`), + // 更新用户状态(封禁/解封) updateUserStatus: (id: string, status: 'active' | 'banned') => apiClient.put>(`/admin/users/${id}/status`, { status }), diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index a7cf62a..e5bc48e 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import type { User, Role, PaginatedResponse } from '@/types' -import { usersApi, type UserQueryParams, type UserDetail as UserDetailType } from '@/api' +import { usersApi, type UserQueryParams, type UserDetail as UserDetailType, type UserDevice } from '@/api' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -31,7 +31,7 @@ import { import { PaginationNavigator } from '@/components/ui/pagination' import UserDetail from '@/components/UserDetail' import UserRoleDialog from '@/components/UserRoleDialog' -import { Search, RefreshCw, Eye, UserPlus, Ban, Unlock, Loader2 } from 'lucide-react' +import { Search, RefreshCw, Eye, UserPlus, Ban, Unlock, Smartphone, Loader2, Copy, Check } from 'lucide-react' // 状态颜色映射 const statusColors: Record = { @@ -75,6 +75,13 @@ export default function Users() { const [roleDialogUser, setRoleDialogUser] = useState(null) const [allRoles, setAllRoles] = useState([]) + // 设备对话框 + const [deviceDialogOpen, setDeviceDialogOpen] = useState(false) + const [deviceUser, setDeviceUser] = useState(null) + const [devices, setDevices] = useState([]) + const [devicesLoading, setDevicesLoading] = useState(false) + const [copiedToken, setCopiedToken] = useState(null) + // 加载用户列表 const loadUsers = useCallback(async () => { setLoading(true) @@ -149,6 +156,35 @@ export default function Users() { setRoleDialogOpen(true) } + // 打开设备对话框(查询 registration_id) + const handleOpenDeviceDialog = async (user: User) => { + setDeviceUser(user) + setDeviceDialogOpen(true) + setDevicesLoading(true) + setDevices([]) + try { + const response = await usersApi.getUserDevices(user.id) + setDevices(response.data.data || []) + } catch (error) { + console.error('Failed to load user devices:', error) + setDevices([]) + } finally { + setDevicesLoading(false) + } + } + + // 复制 registration_id + const handleCopyToken = async (token: string) => { + if (!token) return + try { + await navigator.clipboard.writeText(token) + setCopiedToken(token) + window.setTimeout(() => setCopiedToken((cur) => (cur === token ? null : cur)), 1500) + } catch (error) { + console.error('Failed to copy:', error) + } + } + // 分配角色 const handleAssignRole = async (userId: string, role: string) => { await usersApi.assignRole(userId, role) @@ -365,6 +401,14 @@ export default function Users() { > + {user.status === 'active' ? (