Files
MemeStudio/components/wx-users/wx-user-detail-dialog.tsx
2026-04-05 20:31:56 +08:00

197 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}