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>
This commit is contained in:
v0
2025-08-21 05:32:37 +00:00
parent 9bb0ee2758
commit afc77439bb
25 changed files with 2421 additions and 1645 deletions

View File

@@ -1,6 +1,6 @@
'use client'
"use client"
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from "react"
type Row = {
id: string
@@ -12,9 +12,38 @@ type Row = {
tags: string[]
}
type ApiResp = {
data: Row[]
pagination: { page: number; pageSize: number; total: number; totalPages: number }
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 }) {
@@ -26,23 +55,26 @@ export default function UserList({ queryString }: { queryString: string }) {
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' })
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()
const json = await res.json()
const { items, total } = normalizeResponse(json)
if (!aborted) {
setData(json.data || [])
setTotal(json.pagination?.total || 0)
setData(Array.isArray(items) ? items : [])
setTotal(typeof total === "number" ? total : 0)
}
} catch (e: any) {
if (!aborted) setError(e?.message || '未知错误')
if (!aborted) setError(e?.message || "未知错误")
} finally {
if (!aborted) setLoading(false)
}
}
load()
return () => {
aborted = true
@@ -50,7 +82,7 @@ export default function UserList({ queryString }: { queryString: string }) {
}
}, [queryString])
const rows = useMemo(() => data, [data])
const rows = useMemo(() => (Array.isArray(data) ? data : []), [data])
if (loading) {
return <div className="rounded-lg border bg-white p-4">...</div>
@@ -87,16 +119,22 @@ export default function UserList({ queryString }: { queryString: string }) {
<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>
<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>}
{(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>
<td className="px-3 py-8 text-center text-gray-500" colSpan={6}>
</td>
</tr>
)}
</tbody>