From 6e19bfa661ae3499b0ac2454847b5816e755b137 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 5 Apr 2026 20:31:56 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=85=B3=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(dashboard)/wx-users/page.tsx | 8 +- app/api/wx-users/level-progress/route.ts | 45 ++++++ components/wx-users/wx-user-detail-dialog.tsx | 143 ++++++++++++++---- 3 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 app/api/wx-users/level-progress/route.ts diff --git a/app/(dashboard)/wx-users/page.tsx b/app/(dashboard)/wx-users/page.tsx index c8138f2..aff3d33 100644 --- a/app/(dashboard)/wx-users/page.tsx +++ b/app/(dashboard)/wx-users/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Header } from '@/components/layout/header' @@ -20,6 +20,7 @@ export default function WxUsersPage() { const [search, setSearch] = useState('') const [selectedUser, setSelectedUser] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) + const queryClient = useQueryClient() const { data, isLoading, error } = useQuery({ queryKey: ['wx-users', search], @@ -52,6 +53,10 @@ export default function WxUsersPage() { } } + const handleDeleted = () => { + queryClient.invalidateQueries({ queryKey: ['wx-users'] }) + } + const formatDate = (date: Date | string) => { return new Date(date).toLocaleDateString('zh-CN', { year: 'numeric', @@ -189,6 +194,7 @@ export default function WxUsersPage() { open={isDialogOpen} onOpenChange={handleDialogOpenChange} user={userDetails} + onDeleted={handleDeleted} /> ) diff --git a/app/api/wx-users/level-progress/route.ts b/app/api/wx-users/level-progress/route.ts new file mode 100644 index 0000000..3f9b81a --- /dev/null +++ b/app/api/wx-users/level-progress/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { auth } from '@/lib/auth' + +export const dynamic = 'force-dynamic' + +// DELETE /api/wx-users/level-progress - Batch delete level progress +export async function DELETE( + request: NextRequest, +) { + try { + const session = await auth.api.getSession({ + headers: request.headers, + }) + + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { ids } = await request.json() + + if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string')) { + return NextResponse.json( + { error: 'ids must be a non-empty array of strings' }, + { status: 400 } + ) + } + + const result = await prisma.wxUserLevelProgress.deleteMany({ + where: { + id: { + in: ids, + }, + }, + }) + + return NextResponse.json({ deleted: result.count }) + } catch (error) { + console.error('Error deleting level progress:', error) + return NextResponse.json( + { error: 'Failed to delete level progress' }, + { status: 500 } + ) + } +} diff --git a/components/wx-users/wx-user-detail-dialog.tsx b/components/wx-users/wx-user-detail-dialog.tsx index 7ef675c..0a3452e 100644 --- a/components/wx-users/wx-user-detail-dialog.tsx +++ b/components/wx-users/wx-user-detail-dialog.tsx @@ -1,5 +1,7 @@ 'use client' +import { useState, useEffect } from 'react' +import Image from 'next/image' import { WxUserWithProgress } from '@/types' import { Dialog, @@ -8,38 +10,91 @@ import { 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>(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 ( - + 用户详情 - - 完整信息及关卡进度 - + 完整信息及关卡进度
-
+
{user.avatarUrl ? ( - {user.nickname ) : (
@@ -77,28 +132,60 @@ export function WxUserDetailDialog({ {user.levelProgress.length > 0 && (
-

关卡进度

-
- {user.levelProgress.map((progress) => ( -
+

关卡进度

+ {selectedIds.size > 0 && ( +
- ))} + {isDeleting ? '删除中...' : `删除已选 (${selectedIds.size})`} + + )} +
+
+ + + + + + + + + + + {user.levelProgress.map((progress) => ( + + + + + + + ))} + +
+ handleSelectAll(e.target.checked)} + className="rounded border-gray-300" + /> + 关卡ID关卡答案通关时间
+ handleSelectOne(progress.id, e.target.checked)} + className="rounded border-gray-300" + /> + + {progress.levelId} + + {progress.level?.answer || '-'} + + {new Date(progress.completedAt).toLocaleDateString('zh-CN')} +
)}