Files
users/components/home/user-list.tsx
v0 afc77439bb feat: enhance user profile with detailed tags and asset evaluation
Optimize user detail page for asset assessment and tag info.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-08-21 05:32:37 +00:00

155 lines
5.0 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 Pagination = { page: number; pageSize: number; total: number; totalPages: number }
// The API may return different shapes; normalize them here.
function normalizeResponse(json: any): { items: Row[]; total: number; pagination?: Pagination } {
// v2 shape: { success, data: { items, pagination, totalValue } }
if (json && json.data && Array.isArray(json.data.items)) {
const items = json.data.items as Row[]
const total = json.data?.pagination?.total ?? json.total ?? items.length
return { items, total, pagination: json.data.pagination }
}
// v1 shape: { data: Row[], pagination }
if (json && Array.isArray(json.data)) {
const items = json.data as Row[]
const total = json.pagination?.total ?? json.total ?? items.length
return { items, total, pagination: json.pagination }
}
// shape: { items: Row[], pagination }
if (json && Array.isArray(json.items)) {
const items = json.items as Row[]
const total = json.pagination?.total ?? json.total ?? items.length
return { items, total, pagination: json.pagination }
}
// raw array
if (Array.isArray(json)) {
return { items: json as Row[], total: (json as Row[]).length }
}
// unknown shape
return { items: [], total: 0 }
}
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 = await res.json()
const { items, total } = normalizeResponse(json)
if (!aborted) {
setData(Array.isArray(items) ? items : [])
setTotal(typeof total === "number" ? 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(() => (Array.isArray(data) ? 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
}
}