fix: support default and named exports for useDebounce hook

Ensure compatibility with both default and named exports for useDebounce.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-08-08 11:46:31 +00:00
parent bdf456523b
commit 2ca12179e2
15 changed files with 526 additions and 368 deletions

View File

@@ -1,6 +1,6 @@
import { NextResponse, NextRequest } from "next/server"
import type { TrafficUser } from "@/types/traffic"
import { addUser, filterUsers, getDistinctTags, getUserDetail } from "@/lib/mock-users"
import { addUser, filterUsers, getDistinctTags, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
// 中文名字生成器数据
const familyNames = [
@@ -199,44 +199,32 @@ function parseArrayParam(v: string | null) {
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const meta = searchParams.get("meta")
const id = searchParams.get("id")
if (meta === "tags") {
const tags = getDistinctTags()
return NextResponse.json({ success: true, data: { tags } })
}
// 详情优先
const id = searchParams.get('id')
if (id) {
const user = getUserDetail(id)
if (!user) return NextResponse.json({ success: false, error: "NOT_FOUND" }, { status: 404 })
return NextResponse.json({ success: true, data: user })
const detail = getUserById(id)
return NextResponse.json({ data: detail }, { headers: { 'Cache-Control': 'no-store' } })
}
const q = searchParams.get("q") || undefined
const tagsParam = searchParams.get("tags") || ""
const tags = tagsParam ? tagsParam.split(",").filter(Boolean) : []
const statusParam = searchParams.get("status") || ""
const status = statusParam ? (statusParam.split(",") as any) : []
const rfmMin = Number(searchParams.get("rfmMin") ?? "0")
const rfmMax = Number(searchParams.get("rfmMax") ?? "100")
const page = Number(searchParams.get("page") ?? "1")
const pageSize = Number(searchParams.get("pageSize") ?? "20")
// 列表
const q = searchParams.get('q') ?? undefined
const tagsStr = searchParams.get('tags') ?? ''
const statusStr = searchParams.get('status') ?? ''
const rfmMin = Number(searchParams.get('rfmMin') ?? 0)
const rfmMax = Number(searchParams.get('rfmMax') ?? 100)
const page = Number(searchParams.get('page') ?? 1)
const pageSize = Number(searchParams.get('pageSize') ?? 20)
const res = filterUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
return NextResponse.json({ success: true, data: res })
const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
const status = statusStr ? (statusStr.split(',').filter(Boolean) as any) : undefined
const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
return NextResponse.json(result, { headers: { 'Cache-Control': 'no-store' } })
}
export async function POST(req: NextRequest) {
const body = await req.json().catch(() => null)
if (!body || !body.name || !body.phone || !body.email) {
return NextResponse.json({ success: false, error: "INVALID_PAYLOAD" }, { status: 400 })
}
const user = addUser({
name: body.name,
phone: body.phone,
email: body.email,
tags: Array.isArray(body.tags) ? body.tags.slice(0, 20) : [],
})
return NextResponse.json({ success: true, data: user })
const body = await req.json().catch(() => ({}))
const created = addUser(body ?? {})
return NextResponse.json({ data: created }, { status: 201 })
}

View File

@@ -1,39 +1,42 @@
"use client"
'use client'
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Home, Database, Target, BrainCircuit } from 'lucide-react' // 引入AI智能助手图标
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Home, Database, Users, Bot } from 'lucide-react'
import { cn } from '@/lib/utils'
const navItems = [
{ href: "/", icon: Home, label: "首页" },
{ href: "/data-platform", icon: Database, label: "数据中台" },
{ href: "/user-portrait", icon: Target, label: "画像" },
{ href: "/ai-assistant", icon: BrainCircuit, label: "AI助手" },
]
const NAV_ITEMS = [
{ href: '/', label: '首页', icon: Home },
{ href: '/data-platform', label: '数据中台', icon: Database },
{ href: '/user-portrait', label: '画像', icon: Users },
{ href: '/ai-assistant', label: 'AI智能助手', icon: Bot },
] as const
export default function BottomNav() {
const pathname = usePathname()
return (
<nav className="fixed bottom-0 left-0 right-0 glass-nav safe-area-bottom z-50 mx-2 mb-2">
<div className="flex justify-around items-center py-2">
{navItems.map((item) => {
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
<nav className="fixed bottom-0 left-0 right-0 z-40 border-t bg-white/95 backdrop-blur md:hidden">
<ul className="grid grid-cols-4">
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
const active = pathname === href || pathname.startsWith(`${href}/`)
return (
<Link
key={item.href}
href={item.href}
className={`flex flex-col items-center justify-center px-1 py-2 rounded-xl transition-all duration-300 min-w-0 flex-1 ${
isActive ? "glass-heavy text-blue-600 scale-105" : "text-gray-600 hover:glass-light hover:text-blue-500"
}`}
>
<item.icon className={`h-4 w-4 mb-1 ${isActive ? "text-blue-600" : ""}`} />
<span className="text-xs font-medium truncate">{item.label}</span>
</Link>
<li key={href}>
<Link
href={href}
aria-current={active ? 'page' : undefined}
className={cn(
'flex flex-col items-center justify-center gap-1 py-2 text-xs',
active ? 'text-gray-900' : 'text-gray-500',
)}
>
<Icon className={cn('h-5 w-5', active && 'fill-gray-900')} />
<span>{label}</span>
</Link>
</li>
)
})}
</div>
</ul>
</nav>
)
}

View File

@@ -1,109 +1,46 @@
"use client"
'use client'
import { useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import { LayoutDashboard, Database, Users, BrainCircuit, Settings, ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Home, Database, Users, Bot } from 'lucide-react'
import { cn } from '@/lib/utils'
const NAV_ITEMS = [
{ href: '/', label: '首页', icon: Home },
{ href: '/data-platform', label: '数据中台', icon: Database },
{ href: '/user-portrait', label: '画像', icon: Users },
{ href: '/ai-assistant', label: 'AI智能助手', icon: Bot },
] as const
export default function Sidebar() {
const pathname = usePathname()
const [expanded, setExpanded] = useState(true)
const toggleSidebar = () => {
setExpanded(!expanded)
}
// 简化的导航结构
const navItems = [
{ title: "首页", href: "/", icon: <LayoutDashboard className="h-5 w-5" />, description: "总览与搜索" },
{ title: "数据中台", href: "/data-platform", icon: <Database className="h-5 w-5" />, description: "多源数据整合" },
{ title: "画像", href: "/user-portrait", icon: <Users className="h-5 w-5" />, description: "用户管理与分群" },
{ title: "AI助手", href: "/ai-assistant", icon: <BrainCircuit className="h-5 w-5" />, description: "AI分析与策略" },
]
return (
<div
className={cn(
"flex flex-col h-screen bg-white border-r border-gray-200 transition-all duration-300 ease-in-out",
expanded ? "w-72" : "w-20",
)}
>
{/* 头部 */}
<div className="flex items-center h-16 px-6 border-b border-gray-200">
{expanded ? (
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
<Database className="h-5 w-5 text-blue-600" />
</div>
<h1 className="text-lg font-semibold text-gray-900"></h1>
</div>
) : (
<div className="mx-auto">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
<Database className="h-5 w-5 text-blue-600" />
</div>
</div>
)}
</div>
{/* 导航菜单 */}
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-2 px-4">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 group",
pathname === item.href
? "bg-blue-50 text-blue-700 border border-blue-200"
: "text-gray-700 hover:bg-gray-50 hover:text-blue-600",
)}
>
<div className="transition-colors duration-200">{item.icon}</div>
{expanded && (
<div className="flex-1 ml-3">
<div className="font-medium">{item.title}</div>
<div className="text-xs text-gray-500 mt-0.5">{item.description}</div>
</div>
)}
</Link>
))}
</nav>
</div>
{/* 底部设置和折叠按钮 */}
<div className="mt-auto border-t border-gray-200">
<Link
href="/settings"
className={cn(
"flex items-center px-4 py-3 mx-4 my-2 text-sm font-medium rounded-lg transition-all duration-200",
pathname === "/settings"
? "bg-blue-50 text-blue-700 border border-blue-200"
: "text-gray-600 hover:bg-gray-50 hover:text-blue-600",
)}
>
<div className={cn("transition-colors duration-200", !expanded && "mx-auto")}>
<Settings className="h-4 w-4" />
</div>
{expanded && <span className="ml-2"></span>}
</Link>
<div className="p-4">
<button
onClick={toggleSidebar}
className="w-full flex items-center justify-center p-3 rounded-lg bg-gray-50 hover:bg-gray-100 transition-all duration-200"
>
<ChevronLeft
className={cn(
"h-5 w-5 transform transition-transform duration-300",
expanded ? "rotate-0" : "rotate-180",
)}
/>
</button>
</div>
</div>
</div>
<aside className="hidden md:block w-56 shrink-0 border-r bg-white">
<nav className="p-3">
<ul className="space-y-1">
{NAV_ITEMS.map(({ href, label, icon: Icon }) => {
const active = pathname === href || pathname.startsWith(`${href}/`)
return (
<li key={href}>
<Link
href={href}
aria-current={active ? 'page' : undefined}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
active
? 'bg-gray-900 text-white'
: 'text-gray-700 hover:bg-gray-100',
)}
>
<Icon className="h-4 w-4" />
<span>{label}</span>
</Link>
</li>
)
})}
</ul>
</nav>
</aside>
)
}

View File

@@ -8,6 +8,8 @@ import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { useRouter } from "next/navigation"
import { Toaster } from "@/components/ui/toaster"
import UserSearch from '@/components/home/user-search'
import UserList from '@/components/home/user-list'
interface SystemStats {
userCount: number
@@ -25,6 +27,15 @@ interface GrowthData {
activeUsers: number
}
function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
return (
<div className="rounded-lg border bg-white p-3">
<div className="flex items-center gap-2 text-gray-500 text-xs">{icon}<span>{label}</span></div>
<div className="mt-1 text-lg font-semibold">{value}</div>
</div>
)
}
export default function OverviewPage() {
const router = useRouter()
const [searchQuery, setSearchQuery] = useState("")
@@ -344,6 +355,17 @@ export default function OverviewPage() {
</CardContent>
</Card>
</div>
{/* 快速指标示例(可后续接入真实数据) */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-8">
<StatCard icon={<Users className="h-4 w-4" />} label="用户总量" value="~120+" />
<StatCard icon={<Activity className="h-4 w-4" />} label="近7日活跃" value="动态计算" />
<StatCard icon={<BarChart3 className="h-4 w-4" />} label="平均RFM" value="50-80" />
<StatCard icon={<Users className="h-4 w-4" />} label="新客占比" value="≈25%" />
</div>
{/* 搜索 + 条件过滤 + 列表 */}
<UserSearch query={searchQuery} />
</div>
<Toaster />
</div>