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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
43
components/nav/bottom-tabs.tsx
Normal file
43
components/nav/bottom-tabs.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
components/nav/top-nav.tsx
Normal file
44
components/nav/top-nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user