'use client' import { useState } from 'react' import Image from 'next/image' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Header } from '@/components/layout/header' import { Spinner } from '@/components/ui/spinner' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { WxUserDetailDialog } from '@/components/wx-users/wx-user-detail-dialog' import { apiFetch } from '@/lib/api' import { WxUser, WxUserDetailResponse } from '@/types' import { Search } from 'lucide-react' interface UsersResponse { users: WxUser[] meta: { total: number } } export default function WxUsersPage() { const queryClient = useQueryClient() const [search, setSearch] = useState('') const [selectedUser, setSelectedUser] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const { data, isLoading, error } = useQuery({ queryKey: ['wx-users', search], queryFn: async () => { const res = await apiFetch(`/api/wx-users?search=${encodeURIComponent(search)}`) if (!res.ok) { const payload = await res.json().catch(() => null) throw new Error(payload?.error || 'Failed to fetch wx users') } return res.json() }, }) const { data: userDetails, isLoading: isDetailLoading } = useQuery({ queryKey: ['wx-user-detail', selectedUser?.id], queryFn: async () => { const res = await apiFetch(`/api/wx-users/${selectedUser?.id}`) if (!res.ok) { const payload = await res.json().catch(() => null) throw new Error(payload?.error || 'Failed to fetch wx user details') } return res.json() }, enabled: !!selectedUser && isDialogOpen, }) const syncProgressMutation = useMutation({ mutationFn: async ({ userId, levelIds }: { userId: string; levelIds: string[] }) => { const res = await apiFetch('/api/wx-users/level-progress', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, levelIds }), }) if (!res.ok) { const payload = await res.json().catch(() => null) throw new Error(payload?.error || 'Failed to update level progress') } return res.json() }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ['wx-users'] }), queryClient.invalidateQueries({ queryKey: ['wx-user-detail', selectedUser?.id] }), ]) }, }) const clearProgressMutation = useMutation({ mutationFn: async (userId: string) => { const res = await apiFetch('/api/wx-users/level-progress', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }), }) if (!res.ok) { const payload = await res.json().catch(() => null) throw new Error(payload?.error || 'Failed to clear level progress') } return res.json() }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ['wx-users'] }), queryClient.invalidateQueries({ queryKey: ['wx-user-detail', selectedUser?.id] }), ]) }, }) const handleOpenDetail = (user: WxUser) => { setSelectedUser(user) setIsDialogOpen(true) } const handleDialogChange = (open: boolean) => { setIsDialogOpen(open) if (!open) { setSelectedUser(null) } } const handleSaveProgress = async (levelIds: string[]) => { if (!selectedUser) return await syncProgressMutation.mutateAsync({ userId: selectedUser.id, levelIds }) } const handleClearAll = async () => { if (!selectedUser) return await clearProgressMutation.mutateAsync(selectedUser.id) } const formatDate = (date: Date | string) => { return new Date(date).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) } if (isLoading) { return (
) } if (error) { return (

{error.message || '加载失败'}

) } return (

微信小游戏用户

展示用户列表,并维护每个用户的通关关卡配置。当前共 {data?.meta.total || 0} 个用户。

setSearch(e.target.value)} className="border-slate-200 bg-white pl-10" />
{data?.users.map((user) => ( ))} {data?.users.length === 0 && ( )}
用户 OpenID 已通关关卡数 注册时间 操作
{user.avatarUrl ? (
{user.nickname
) : (
{user.nickname?.[0]?.toUpperCase() || 'U'}
)}
{user.nickname || '匿名用户'}
体力 {user.stamina}
{user.openid} {user.completedLevelCount} 关 {formatDate(user.createdAt)}
暂无用户数据
{isDialogOpen && selectedUser && isDetailLoading && !userDetails ? (
) : null}
) }