197 lines
6.8 KiB
TypeScript
197 lines
6.8 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import Image from 'next/image'
|
||
import { WxUserWithProgress } from '@/types'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
} from '@/components/ui/dialog'
|
||
import { Button } from '@/components/ui/button'
|
||
|
||
interface WxUserDetailDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
user: WxUserWithProgress | null | undefined
|
||
onDeleted?: () => void
|
||
}
|
||
|
||
export function WxUserDetailDialog({
|
||
open,
|
||
onOpenChange,
|
||
user,
|
||
onDeleted,
|
||
}: WxUserDetailDialogProps) {
|
||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||
const [isDeleting, setIsDeleting] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
setSelectedIds(new Set())
|
||
}
|
||
}, [open])
|
||
|
||
if (!user) return null
|
||
|
||
const handleSelectAll = (checked: boolean) => {
|
||
if (checked) {
|
||
setSelectedIds(new Set(user.levelProgress.map((p) => p.id)))
|
||
} else {
|
||
setSelectedIds(new Set())
|
||
}
|
||
}
|
||
|
||
const handleSelectOne = (id: string, checked: boolean) => {
|
||
const newSelected = new Set(selectedIds)
|
||
if (checked) {
|
||
newSelected.add(id)
|
||
} else {
|
||
newSelected.delete(id)
|
||
}
|
||
setSelectedIds(newSelected)
|
||
}
|
||
|
||
const handleBatchDelete = async () => {
|
||
if (selectedIds.size === 0) return
|
||
|
||
setIsDeleting(true)
|
||
try {
|
||
const res = await fetch('/api/wx-users/level-progress', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ids: Array.from(selectedIds) }),
|
||
})
|
||
|
||
if (res.ok) {
|
||
setSelectedIds(new Set())
|
||
onDeleted?.()
|
||
}
|
||
} finally {
|
||
setIsDeleting(false)
|
||
}
|
||
}
|
||
|
||
const allSelected =
|
||
user.levelProgress.length > 0 &&
|
||
selectedIds.size === user.levelProgress.length
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>用户详情</DialogTitle>
|
||
<DialogDescription>完整信息及关卡进度</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-16 h-16 flex-shrink-0 relative">
|
||
{user.avatarUrl ? (
|
||
<Image
|
||
src={user.avatarUrl}
|
||
alt={user.nickname || 'User'}
|
||
fill
|
||
className="rounded-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center text-xl font-medium text-gray-600">
|
||
{user.nickname?.[0]?.toUpperCase() || 'U'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-gray-900">
|
||
{user.nickname || '匿名用户'}
|
||
</h2>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
注册时间:{new Date(user.createdAt).toLocaleDateString('zh-CN')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">积分</p>
|
||
<p className="text-2xl font-bold text-orange-600">{user.points}</p>
|
||
</div>
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">已通关关卡</p>
|
||
<p className="text-2xl font-bold text-green-600">{user.levelProgress.length}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-2">OpenID</p>
|
||
<code className="block bg-gray-100 text-gray-800 text-xs p-3 rounded-lg break-all">
|
||
{user.openid}
|
||
</code>
|
||
</div>
|
||
|
||
{user.levelProgress.length > 0 && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<p className="text-xs text-gray-500 uppercase tracking-wider">关卡进度</p>
|
||
{selectedIds.size > 0 && (
|
||
<Button
|
||
variant="destructive"
|
||
size="sm"
|
||
onClick={handleBatchDelete}
|
||
disabled={isDeleting}
|
||
>
|
||
{isDeleting ? '删除中...' : `删除已选 (${selectedIds.size})`}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
<div className="border rounded-lg overflow-hidden">
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-gray-50 border-b">
|
||
<tr>
|
||
<th className="w-10 px-3 py-2 text-left">
|
||
<input
|
||
type="checkbox"
|
||
checked={allSelected}
|
||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||
className="rounded border-gray-300"
|
||
/>
|
||
</th>
|
||
<th className="px-3 py-2 text-left text-xs text-gray-500 uppercase">关卡ID</th>
|
||
<th className="px-3 py-2 text-left text-xs text-gray-500 uppercase">关卡答案</th>
|
||
<th className="px-3 py-2 text-left text-xs text-gray-500 uppercase">通关时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{user.levelProgress.map((progress) => (
|
||
<tr key={progress.id} className="hover:bg-gray-50">
|
||
<td className="px-3 py-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedIds.has(progress.id)}
|
||
onChange={(e) => handleSelectOne(progress.id, e.target.checked)}
|
||
className="rounded border-gray-300"
|
||
/>
|
||
</td>
|
||
<td className="px-3 py-2 font-mono text-xs text-gray-400">
|
||
{progress.levelId}
|
||
</td>
|
||
<td className="px-3 py-2 font-medium text-gray-900">
|
||
{progress.level?.answer || '-'}
|
||
</td>
|
||
<td className="px-3 py-2 text-gray-500">
|
||
{new Date(progress.completedAt).toLocaleDateString('zh-CN')}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|