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

@@ -33,6 +33,14 @@ interface ApiEndpoint {
authentication: "API Key" | "OAuth 2.0" | "None"
}
function openInNewTab(url: string) {
try {
window.open(url, "_blank", "noopener,noreferrer")
} catch (e) {
console.error(e)
}
}
export function ApiDocumentation() {
const { toast } = useToast()
const [activeTab, setActiveTab] = useState("user-data")
@@ -616,15 +624,19 @@ export function ApiDocumentation() {
</div>
<div className="flex justify-between items-center mt-4">
<Button variant="outline" className="gap-2">
<Button
variant="outline"
className="gap-2 bg-transparent"
onClick={() => openInNewTab("/api/openapi?download=1")}
>
<FileJson className="h-4 w-4" />
OpenAPI规范
</Button>
<Button variant="outline" className="gap-2">
<Button variant="outline" className="gap-2 bg-transparent" onClick={() => openInNewTab("/api/openapi")}>
<Code className="h-4 w-4" />
SDK
</Button>
<Button className="gap-2">
<Button className="gap-2" onClick={() => openInNewTab("/api/ingest")}>
<Play className="h-4 w-4" />
API测试工具
</Button>

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>

View File

@@ -0,0 +1,43 @@
"use client"
import Link from "next/link"
import { Home, Database, Users, TrendingUp, Bot } from "lucide-react"
import { cn } from "@/lib/utils"
import { usePathname } from "next/navigation"
export default function BottomTabs() {
const pathname = usePathname()
const tabs = [
{ href: "/", label: "首页", icon: Home },
{ href: "/data-platform", label: "数据中台", icon: Database },
{ href: "/user-portrait", label: "用户池", icon: Users }, // 改名为用户池
{ href: "/user-assets", label: "用户资产", icon: TrendingUp },
{ href: "/ai-assistant", label: "AI助手", icon: Bot },
]
return (
<nav className="md:hidden fixed bottom-0 inset-x-0 z-40 border-t bg-white/90 backdrop-blur">
<ul className="mx-auto grid max-w-full grid-cols-5">
{tabs.map((t) => {
const active = pathname === t.href || (t.href !== "/" && pathname.startsWith(t.href))
const Icon = t.icon
return (
<li key={t.href}>
<Link
href={t.href}
className={cn(
"flex flex-col items-center justify-center py-2 text-xs",
active ? "text-slate-900" : "text-slate-600",
)}
>
<Icon className="h-4 w-4" />
<span className="mt-0.5 text-[10px]">{t.label}</span>
</Link>
</li>
)
})}
</ul>
</nav>
)
}

View File

@@ -0,0 +1,44 @@
"use client"
import Link from "next/link"
import { useMemo } from "react"
import { cn } from "@/lib/utils"
export default function TopNav({ activePath }: { activePath?: string }) {
const items = useMemo(
() => [
{ href: "/", label: "首页" },
{ href: "/data-platform", label: "数据中台" },
{ href: "/user-portrait", label: "用户画像" },
{ href: "/ai-assistant", label: "AI 助手" },
{ href: "/user-valuation", label: "用户资产估值" },
],
[],
)
return (
<nav className="flex h-14 items-center gap-2">
<div className="mr-4 text-lg font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
</div>
<ul className="flex items-center gap-1">
{items.map((it) => {
const active = activePath === it.href || (it.href !== "/" && activePath?.startsWith(it.href))
return (
<li key={it.href}>
<Link
href={it.href}
className={cn(
"px-3 py-1.5 rounded-md text-sm hover:bg-slate-100",
active && "bg-slate-900 text-white hover:bg-slate-900",
)}
>
{it.label}
</Link>
</li>
)
})}
</ul>
</nav>
)
}