Files
users/components/home/user-list.tsx
v0 2ca12179e2 fix: support default and named exports for useDebounce hook
Ensure compatibility with both default and named exports for useDebounce.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-08-08 11:46:31 +00:00

117 lines
3.7 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 { useEffect, useMemo, useState } from 'react'
type Row = {
id: string
name: string
email: string
phone: string
rfmScore: number
lastActiveAt: string
tags: string[]
}
type ApiResp = {
data: Row[]
pagination: { page: number; pageSize: number; total: number; totalPages: number }
}
export default function UserList({ queryString }: { queryString: string }) {
const [data, setData] = useState<Row[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let aborted = false
const controller = new AbortController()
async function load() {
setLoading(true)
setError(null)
try {
const res = await fetch(`/api/users?${queryString}`, { signal: controller.signal, cache: 'no-store' })
if (!res.ok) throw new Error(`请求失败: ${res.status}`)
const json: ApiResp = await res.json()
if (!aborted) {
setData(json.data || [])
setTotal(json.pagination?.total || 0)
}
} catch (e: any) {
if (!aborted) setError(e?.message || '未知错误')
} finally {
if (!aborted) setLoading(false)
}
}
load()
return () => {
aborted = true
controller.abort()
}
}, [queryString])
const rows = useMemo(() => data, [data])
if (loading) {
return <div className="rounded-lg border bg-white p-4">...</div>
}
if (error) {
return <div className="rounded-lg border bg-white p-4 text-red-600">{error}</div>
}
return (
<div className="rounded-lg border bg-white">
<div className="flex items-center justify-between p-3 border-b">
<div className="text-sm text-gray-600"> {total} </div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-50">
<tr className="text-left text-gray-500">
<th className="px-3 py-2 font-medium"></th>
<th className="px-3 py-2 font-medium"></th>
<th className="px-3 py-2 font-medium"></th>
<th className="px-3 py-2 font-medium">RFM</th>
<th className="px-3 py-2 font-medium"></th>
<th className="px-3 py-2 font-medium"></th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id} className="border-t">
<td className="px-3 py-2">{r.name}</td>
<td className="px-3 py-2">{r.email}</td>
<td className="px-3 py-2">{r.phone}</td>
<td className="px-3 py-2">{r.rfmScore}</td>
<td className="px-3 py-2">{formatDateTime(r.lastActiveAt)}</td>
<td className="px-3 py-2">
<div className="flex flex-wrap gap-1">
{(r.tags || []).slice(0, 3).map((t) => (
<span key={t} className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">{t}</span>
))}
{(r.tags || []).length > 3 && <span className="px-2 py-0.5 rounded-full border">+{r.tags.length - 3}</span>}
</div>
</td>
</tr>
))}
{!rows.length && (
<tr>
<td className="px-3 py-8 text-center text-gray-500" colSpan={6}></td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
function formatDateTime(iso: string) {
try {
const d = new Date(iso)
return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`
} catch {
return iso
}
}