Files
users/components/home/user-list.tsx
v0 22e725887a refactor: restructure project into 5 core modules
Organize project by 5 core modules based on requirement docs.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:18:24 +00:00

179 lines
5.8 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
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 && e.name !== "AbortError") {
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 className="space-y-3">
<Skeleton className="h-4 w-32" />
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
</div>
</div>
)
}
if (error) {
return (
<div className="rounded-lg border bg-white p-4">
<div className="text-red-600 text-center py-8">
<div className="text-lg font-medium mb-2"></div>
<div className="text-sm">{error}</div>
</div>
</div>
)
}
return (
<div className="rounded-lg border bg-white">
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
<div className="text-sm text-gray-600">
<span className="font-medium text-gray-900">{total}</span>
</div>
{total > 0 && <div className="text-xs text-gray-500"> {Math.min(20, total)} </div>}
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-50 border-b">
<tr className="text-left text-gray-600">
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium">RFM分数</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{rows.map((r) => (
<tr key={r.id} className="hover:bg-gray-50 transition-colors">
<td className="px-4 py-3 font-medium text-gray-900">{r.name}</td>
<td className="px-4 py-3 text-gray-600">{r.email}</td>
<td className="px-4 py-3 text-gray-600 font-mono">{r.phone}</td>
<td className="px-4 py-3">
<Badge
variant={r.rfmScore >= 70 ? "default" : r.rfmScore >= 40 ? "secondary" : "outline"}
className="font-medium"
>
{r.rfmScore}
</Badge>
</td>
<td className="px-4 py-3 text-gray-600 text-xs">{formatDateTime(r.lastActiveAt)}</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{(r.tags || []).slice(0, 3).map((t) => (
<Badge key={t} variant="outline" className="text-xs px-2 py-0.5">
{t}
</Badge>
))}
{(r.tags || []).length > 3 && (
<Badge variant="outline" className="text-xs px-2 py-0.5">
+{r.tags.length - 3}
</Badge>
)}
</div>
</td>
</tr>
))}
{!rows.length && (
<tr>
<td className="px-4 py-12 text-center text-gray-500" colSpan={6}>
<div className="flex flex-col items-center gap-2">
<div className="text-lg"></div>
<div className="text-sm"></div>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
function formatDateTime(iso: string) {
try {
const d = new Date(iso)
const now = new Date()
const diffMs = now.getTime() - d.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
return "今天 " + d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
} else if (diffDays === 1) {
return "昨天 " + d.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })
} else if (diffDays < 7) {
return `${diffDays}天前`
} else {
return d.toLocaleDateString("zh-CN", { month: "short", day: "numeric" })
}
} catch {
return iso.slice(0, 10)
}
}