fix: support default and named export for useDebounce hook

Ensure compatibility with both default and named imports for hook.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-09-23 07:09:47 +00:00
parent 7bb91b447e
commit ce0a716d02
30 changed files with 1682 additions and 2474 deletions

View File

@@ -1,311 +1,96 @@
"use client"
import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ArrowLeft, User, Tag, TrendingUp, DollarSign, Calendar, Phone, Mail, MapPin } from "lucide-react"
import { useParams } from "next/navigation"
import MobileHeader from "@/app/components/MobileHeader"
import BottomNav from "@/app/components/BottomNav"
import Section from "@/components/user-portrait/mobile/section"
import ProfileHeader from "@/components/user-portrait/mobile/profile-header"
import MetricsRFM from "@/components/user-portrait/mobile/metrics-rfm"
import InteractionsList from "@/components/user-portrait/mobile/interactions-list"
import PurchaseHistory from "@/components/user-portrait/mobile/purchase-history"
import WechatAccounts from "@/components/user-portrait/mobile/wechat-accounts"
type UserDetail = {
type Detail = {
id: string
name: string
avatar?: string
email: string
phone: string
city: string
store: string
project: string
team: string
tags: string[]
persona: string[]
source: string
recency: number
frequency: number
monetary: number
rfmScore: number
assetValue: number
riskLevel: "低" | "中" | "高"
lastActivity: string
interactions: { id: string; type: string; time: string; note?: string }[]
purchaseHistory: { id: string; amount: number; time: string; item: string }[]
wechatAccounts: { id: string; nickname: string; avatar?: string }[]
status: "活跃" | "沉睡" | "流失风险"
detailedTags: {
category: string
tags: { name: string; confidence: number; source: string }[]
}[]
assetBreakdown: {
category: string
value: number
percentage: number
}[]
}
export default function UserDetailPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const [data, setData] = useState<UserDetail | null>(null)
const [data, setData] = useState<Detail | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!params?.id) return
setLoading(true)
setTimeout(() => {
setData({
id: params.id,
name: "张三",
email: "zhangsan@example.com",
phone: "138****8888",
city: "厦门",
store: "思明店",
project: "贷款业务",
team: "A组",
tags: ["高价值客户", "活跃用户", "信用良好"],
persona: ["高净值人群", "投资偏好保守"],
source: "微信推广",
recency: 85,
frequency: 92,
monetary: 88,
rfmScore: 88,
assetValue: 1250000,
riskLevel: "低",
lastActivity: "2024-01-15 14:30:00",
status: "活跃",
detailedTags: [
{
category: "行为标签",
tags: [
{ name: "高频访问", confidence: 95, source: "系统分析" },
{ name: "深度浏览", confidence: 88, source: "行为追踪" },
{ name: "主动咨询", confidence: 92, source: "客服记录" },
],
},
{
category: "偏好标签",
tags: [
{ name: "理财产品", confidence: 90, source: "购买记录" },
{ name: "保险产品", confidence: 75, source: "浏览记录" },
{ name: "投资咨询", confidence: 85, source: "咨询记录" },
],
},
{
category: "风险标签",
tags: [
{ name: "信用优良", confidence: 98, source: "征信报告" },
{ name: "收入稳定", confidence: 92, source: "银行流水" },
{ name: "资产充足", confidence: 88, source: "资产证明" },
],
},
],
assetBreakdown: [
{ category: "客户生命周期价值", value: 450000, percentage: 36 },
{ category: "交叉销售潜力", value: 320000, percentage: 26 },
{ category: "推荐价值", value: 280000, percentage: 22 },
{ category: "品牌忠诚度价值", value: 200000, percentage: 16 },
],
})
setLoading(false)
}, 500)
fetch(`/api/users?id=${params.id}`)
.then((r) => r.json())
.then((res) => setData(res?.data ?? null))
.finally(() => setLoading(false))
}, [params?.id])
if (loading) {
return (
<div className="min-h-screen bg-slate-50 p-4">
<div className="animate-pulse space-y-4">
<div className="h-12 bg-slate-200 rounded"></div>
<div className="h-32 bg-slate-200 rounded"></div>
<div className="h-48 bg-slate-200 rounded"></div>
</div>
</div>
)
}
if (!data) {
return (
<div className="min-h-screen bg-slate-50 p-4 flex items-center justify-center">
<div className="text-center">
<p className="text-slate-600 mb-4"></p>
<Button onClick={() => router.back()}></Button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-slate-50">
<div className="bg-white border-b px-4 py-3 flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="font-semibold"></h1>
</div>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
<MobileHeader onMenuToggle={() => {}} title="用户详情" />
<main className="container mx-auto px-4 pb-24 space-y-4">
{loading ? (
<div className="rounded-xl bg-white/60 backdrop-blur p-6 text-sm text-muted-foreground">
</div>
) : data ? (
<div className="space-y-4">
<section className="rounded-2xl bg-white/60 backdrop-blur-md p-4 shadow-sm border">
<ProfileHeader
name={data.name}
avatar={data.avatar}
email={data.email}
phone={data.phone}
tags={data.tags}
/>
</section>
<div className="container mx-auto px-4 py-4 space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">{data.name}</h2>
<div className="flex items-center gap-4 text-sm text-slate-600 mt-1">
<span className="flex items-center gap-1">
<Phone className="h-3 w-3" />
{data.phone}
</span>
<span className="flex items-center gap-1">
<Mail className="h-3 w-3" />
{data.email}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-slate-600 mt-1">
<span className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{data.city} · {data.store}
</span>
<span>
{data.project} · {data.team}
</span>
</div>
</div>
<Badge variant={data.status === "活跃" ? "default" : "secondary"}>{data.status}</Badge>
</div>
<Section title="RFM 指标">
<MetricsRFM
recency={data.recency}
frequency={data.frequency}
monetary={data.monetary}
rfmScore={data.rfmScore}
/>
</Section>
<div className="flex flex-wrap gap-2">
{data.persona.map((p) => (
<Badge key={p} variant="outline" className="text-xs">
{p}
</Badge>
))}
</div>
</CardContent>
</Card>
<Section title="互动记录">
<InteractionsList items={data.interactions} />
</Section>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">¥{data.assetValue.toLocaleString()}</div>
<div className="text-xs text-slate-600"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold">{data.rfmScore}</div>
<div className="text-xs text-slate-600">RFM评分</div>
</div>
<div className="text-center">
<div
className={`text-2xl font-bold ${data.riskLevel === "低" ? "text-green-600" : data.riskLevel === "中" ? "text-yellow-600" : "text-red-600"}`}
>
{data.riskLevel}
</div>
<div className="text-xs text-slate-600"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{data.source}</div>
<div className="text-xs text-slate-600"></div>
</div>
</div>
<Section title="购买历史">
<PurchaseHistory items={data.purchaseHistory} />
</Section>
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
{data.assetBreakdown.map((item) => (
<div key={item.category} className="flex items-center justify-between">
<span className="text-sm">{item.category}</span>
<div className="flex items-center gap-2">
<div className="w-20 bg-slate-200 rounded-full h-2">
<div className="bg-blue-500 h-2 rounded-full" style={{ width: `${item.percentage}%` }}></div>
</div>
<span className="text-sm font-medium">¥{item.value.toLocaleString()}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{data.detailedTags.map((category) => (
<div key={category.category} className="space-y-2">
<h4 className="font-medium text-sm text-slate-700">{category.category}</h4>
<div className="grid gap-2">
{category.tags.map((tag) => (
<div key={tag.name} className="flex items-center justify-between p-2 bg-slate-50 rounded-md">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
{tag.name}
</Badge>
<span className="text-xs text-slate-600">: {tag.source}</span>
</div>
<div className="flex items-center gap-1">
<div className="w-12 bg-slate-200 rounded-full h-1">
<div className="bg-green-500 h-1 rounded-full" style={{ width: `${tag.confidence}%` }}></div>
</div>
<span className="text-xs font-medium">{tag.confidence}%</span>
</div>
</div>
))}
</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
RFM详细指标
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-xl font-bold">{data.recency}</div>
<div className="text-xs text-slate-600"> (R)</div>
<div className="w-full bg-slate-200 rounded-full h-2 mt-1">
<div className="bg-blue-500 h-2 rounded-full" style={{ width: `${data.recency}%` }}></div>
</div>
</div>
<div className="text-center">
<div className="text-xl font-bold">{data.frequency}</div>
<div className="text-xs text-slate-600"> (F)</div>
<div className="w-full bg-slate-200 rounded-full h-2 mt-1">
<div className="bg-green-500 h-2 rounded-full" style={{ width: `${data.frequency}%` }}></div>
</div>
</div>
<div className="text-center">
<div className="text-xl font-bold">{data.monetary}</div>
<div className="text-xs text-slate-600"> (M)</div>
<div className="w-full bg-slate-200 rounded-full h-2 mt-1">
<div className="bg-purple-500 h-2 rounded-full" style={{ width: `${data.monetary}%` }}></div>
</div>
</div>
</div>
<div className="mt-4 text-center">
<div className="text-sm text-slate-600"></div>
<div className="flex items-center justify-center gap-1 mt-1">
<Calendar className="h-3 w-3" />
<span className="text-sm">{data.lastActivity}</span>
</div>
</div>
</CardContent>
</Card>
</div>
<Section title="绑定微信账号">
<WechatAccounts accounts={data.wechatAccounts.map((w) => ({ id: w.id, nickname: w.nickname, avatar: w.avatar }))} />
</Section>
</div>
) : (
<div className="rounded-xl bg-white/60 backdrop-blur p-6 text-sm text-red-600">
</div>
)}
</main>
<BottomNav />
</div>
)
}

View File

@@ -1,303 +1,239 @@
"use client"
import type React from "react"
import { useRouter } from "next/navigation"
import { useEffect, useMemo, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link"
import MobileHeader from "@/app/components/MobileHeader"
import BottomNav from "@/app/components/BottomNav"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Slider } from "@/components/ui/slider"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Search, Filter, Users, TrendingUp } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Search, Filter, Plus } from 'lucide-react'
import FilterDrawer, { type FilterValues } from "@/components/user-portrait/filter-drawer"
type ListItem = {
type User = {
id: string
name: string
phone: string
email: string
tags: string[]
rfmScore: number
lastActiveAt: string
city: string
store: string
project: string
team: string
persona: string[]
source: string
lastActivity: string
status: "活跃" | "沉睡" | "已封禁"
}
type Meta = {
tags: string[]
cities: string[]
personas: string[]
sources: string[]
}
type UsersResponse = { success: true; data: { items: User[]; total: number; page: number; pageSize: number } }
export default function UserPoolPage() {
const router = useRouter()
const [q, setQ] = useState("")
const [rfmRange, setRfmRange] = useState<[number, number]>([0, 100])
const [items, setItems] = useState<ListItem[]>([])
const [meta, setMeta] = useState<Meta>({ tags: [], cities: [], personas: [], sources: [] })
const [filters, setFilters] = useState({
tags: [] as string[],
status: [] as string[],
city: [] as string[],
persona: [] as string[],
source: [] as string[],
})
const [totalValue, setTotalValue] = useState(0)
export default function UserPortraitPage() {
const [users, setUsers] = useState<User[]>([])
const [total, setTotal] = useState(0)
useEffect(() => {
fetch("/api/users?meta=tags")
.then((r) => r.json())
.then((res) => setMeta(res.data))
}, [])
const [searchQuery, setSearchQuery] = useState("")
const [isAddingUser, setIsAddingUser] = useState(false)
const [newUser, setNewUser] = useState({ name: "", phone: "", email: "", tags: [] as string[] })
const [filterOpen, setFilterOpen] = useState(false)
const [allTags, setAllTags] = useState<string[]>([])
const [filters, setFilters] = useState<FilterValues>({ tags: [], status: [], rfm: [0, 100] })
const queryString = useMemo(() => {
const p = new URLSearchParams()
if (q) p.set("q", q)
if (searchQuery) p.set("q", searchQuery)
if (filters.tags.length) p.set("tags", filters.tags.join(","))
if (filters.status.length) p.set("status", filters.status.join(","))
if (filters.city.length) p.set("city", filters.city.join(","))
if (filters.persona.length) p.set("persona", filters.persona.join(","))
if (filters.source.length) p.set("source", filters.source.join(","))
p.set("rfmMin", String(rfmRange[0]))
p.set("rfmMax", String(rfmRange[1]))
p.set("rfmMin", String(filters.rfm[0]))
p.set("rfmMax", String(filters.rfm[1]))
p.set("page", "1")
p.set("pageSize", "50")
return p.toString()
}, [q, filters, rfmRange])
}, [searchQuery, filters])
useEffect(() => {
fetch(`/api/users?${queryString}`)
.then((r) => r.json())
.then((res) => {
setItems(res.data.items)
setTotalValue(res.data.totalValue)
.then((res: UsersResponse) => {
if (res?.success) {
setUsers(res.data.items)
setTotal(res.data.total)
}
})
.catch(() => {})
}, [queryString])
const toggle = (key: keyof typeof filters, v: string) =>
setFilters((prev) => ({
...prev,
[key]: prev[key].includes(v) ? prev[key].filter((x) => x !== v) : [...prev[key], v],
}))
useEffect(() => {
fetch("/api/users?meta=tags")
.then((r) => r.json())
.then((res: any) => setAllTags(res?.data?.tags ?? []))
.catch(() => {})
}, [])
const handleUserClick = (userId: string) => {
router.push(`/user-portrait/${userId}`)
const handleAddUser = async () => {
const resp = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
})
const data = await resp.json()
if (data?.success) {
setIsAddingUser(false)
setNewUser({ name: "", phone: "", email: "", tags: [] })
// 触发刷新
fetch(`/api/users?${queryString}`)
.then((r) => r.json())
.then((res: UsersResponse) => {
if (res?.success) {
setUsers(res.data.items)
setTotal(res.data.total)
}
})
}
}
return (
<div className="container mx-auto px-4 py-5 space-y-4">
{/* 核心板块:用户估值概览 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xl flex items-center gap-2">
<Users className="h-5 w-5" />
·
</CardTitle>
<p className="text-sm text-muted-foreground"> RFM </p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Stat label="当前筛选用户数" value={`${items.length}`} />
<Stat label="估值总额(≈)" value={`¥ ${totalValue.toLocaleString("zh-CN")}`} />
<Stat label="RFM 范围" value={`${rfmRange[0]} - ${rfmRange[1]}`} />
<Stat label="已选标签数" value={`${filters.tags.length}`} />
</div>
</CardContent>
</Card>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
<MobileHeader onMenuToggle={() => {}} title="用户画像" />
{/* 筛选区:清晰多维度 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<Filter className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-col md:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-slate-400" />
<Input
placeholder="搜索姓名/手机/邮箱/标签/画像标签"
className="pl-8"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
<main className="container mx-auto px-4 pb-24 space-y-4">
<div className="rounded-2xl bg-white/60 backdrop-blur-md p-4 shadow-sm border">
<div className="flex items-baseline justify-between">
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
<FilterRow title="RFM" right={<span className="text-xs text-muted-foreground">0~100</span>}>
<Slider
value={rfmRange}
onValueChange={(v) => setRfmRange(v as [number, number])}
min={0}
max={100}
step={1}
/>
</FilterRow>
<FilterRow title="城市">
<Chips options={meta.cities} actives={filters.city} onToggle={(v) => toggle("city", v)} />
</FilterRow>
<FilterRow title="标签">
<Chips options={meta.tags} actives={filters.tags} onToggle={(v) => toggle("tags", v)} />
</FilterRow>
<FilterRow title="画像( Persona )">
<Chips options={meta.personas} actives={filters.persona} onToggle={(v) => toggle("persona", v)} />
</FilterRow>
<FilterRow title="来源">
<Chips options={meta.sources} actives={filters.source} onToggle={(v) => toggle("source", v)} />
</FilterRow>
</CardContent>
</Card>
{/* 列表 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center justify-between">
<span></span>
<div className="flex gap-2">
<button className="text-xs px-3 py-1 bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100">
</button>
<button className="text-xs px-3 py-1 bg-green-50 text-green-600 rounded-md hover:bg-green-100">
</button>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="hidden md:block rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead>/</TableHead>
<TableHead>Persona</TableHead>
<TableHead></TableHead>
<TableHead className="text-center">RFM</TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((u) => (
<TableRow
key={u.id}
className="cursor-pointer hover:bg-slate-50"
onClick={() => handleUserClick(u.id)}
>
<TableCell className="font-medium">
{u.name}
<div className="text-xs text-muted-foreground">{u.email}</div>
</TableCell>
<TableCell>
{u.city} / {u.store}
</TableCell>
<TableCell>
{u.project} / {u.team}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{u.persona.map((p) => (
<Badge key={p} variant="secondary" className="text-[10px]">
{p}
</Badge>
))}
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{u.tags.slice(0, 3).map((t) => (
<Badge key={t} className="text-[10px]">
{t}
</Badge>
))}
</div>
</TableCell>
<TableCell className="text-center">{u.rfmScore}</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<TrendingUp className="h-3 w-3 text-green-500" />
<span className="text-xs text-green-600"></span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="text-sm text-muted-foreground"> {total} </div>
</div>
{/* 移动端卡片 */}
<div className="md:hidden space-y-2">
{items.map((u) => (
<div
key={u.id}
className="border rounded-md p-3 cursor-pointer hover:bg-slate-50"
onClick={() => handleUserClick(u.id)}
>
<div className="font-medium">{u.name}</div>
<div className="text-xs text-muted-foreground">
{u.city} · {u.store} · RFM {u.rfmScore}
<div className="mt-4">
<Tabs defaultValue="users" className="w-full">
<TabsList className="grid grid-cols-2 w-full">
<TabsTrigger value="users" className="data-[state=active]:bg-white"></TabsTrigger>
<TabsTrigger value="tags" className="data-[state=active]:bg-white"></TabsTrigger>
</TabsList>
<TabsContent value="users" className="space-y-4">
<div className="flex items-center gap-2 mt-3">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
<Input className="pl-8" placeholder="搜索用户…" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
</div>
<Button variant="outline" onClick={() => setFilterOpen(true)}>
<Filter className="h-4 w-4 mr-1" />
</Button>
<Button onClick={() => setIsAddingUser(true)}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="mt-1 flex flex-wrap gap-1">
{u.persona.map((p) => (
<Badge key={p} variant="secondary" className="text-[10px]">
{p}
</Badge>
<div className="grid gap-3">
{users.map((u) => (
<Card key={u.id} className="border bg-white/70 backdrop-blur-md shadow-sm">
<CardContent className="p-4">
<div className="grid grid-cols-12 gap-3 items-center">
<div className="col-span-5">
<Link href={`/user-portrait/${u.id}`} className="font-medium hover:underline">
{u.name}
</Link>
<p className="text-xs text-muted-foreground mt-1">
{new Date(u.lastActivity).toLocaleDateString("zh-CN")}
</p>
</div>
<div className="col-span-3">
<div className="text-sm">{u.phone}</div>
<div className="text-xs text-muted-foreground">{u.email}</div>
</div>
<div className="col-span-2">
<div className="flex flex-wrap gap-1">
{u.tags.slice(0, 2).map((t) => (
<Badge key={t} variant="secondary" className="text-xs">{t}</Badge>
))}
{u.tags.length > 2 && (
<Badge variant="outline" className="text-xs">+{u.tags.length - 2}</Badge>
)}
</div>
</div>
<div className="col-span-1 text-sm font-semibold">{u.rfmScore}</div>
<div className="col-span-1">
<span className={`text-xs px-2 py-1 rounded-full ${
u.status === "活跃" ? "bg-green-100 text-green-700" :
u.status === "沉睡" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-700"
}`}>
{u.status}
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
<div className="mt-1 flex items-center gap-1">
<TrendingUp className="h-3 w-3 text-green-500" />
<span className="text-xs text-green-600"></span>
</TabsContent>
<TabsContent value="tags">
<div className="text-sm text-muted-foreground py-6 text-center"></div>
</TabsContent>
</Tabs>
</div>
</div>
</main>
<BottomNav />
{/* 筛选抽屉 */}
<FilterDrawer
open={filterOpen}
onOpenChange={setFilterOpen}
allTags={allTags}
value={filters}
onApply={(v) => setFilters(v)}
/>
{/* 添加用户 */}
<Dialog open={isAddingUser} onOpenChange={setIsAddingUser}>
<DialogContent>
<DialogHeader><DialogTitle></DialogTitle></DialogHeader>
<div className="space-y-3 py-2">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" value={newUser.name} onChange={(e) => setNewUser((p) => ({ ...p, name: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input id="phone" value={newUser.phone} onChange={(e) => setNewUser((p) => ({ ...p, phone: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input id="email" type="email" value={newUser.email} onChange={(e) => setNewUser((p) => ({ ...p, email: e.target.value }))} />
</div>
{!!allTags.length && (
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-auto">
{allTags.map((t) => (
<label key={t} className="flex items-center gap-2 text-sm">
<Checkbox
checked={newUser.tags.includes(t)}
onCheckedChange={(ck) =>
setNewUser((p) => ({ ...p, tags: ck ? [...p.tags, t] : p.tags.filter((x) => x !== t) }))
}
/>
<span className="truncate">{t}</span>
</label>
))}
</div>
</div>
))}
)}
</div>
</CardContent>
</Card>
</div>
)
}
function Stat({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-md border p-3">
<div className="text-xs text-muted-foreground">{label}</div>
<div className="text-lg font-semibold">{value}</div>
</div>
)
}
function FilterRow({ title, children, right }: { title: string; children: React.ReactNode; right?: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground">{title}</div>
{right}
</div>
{children}
</div>
)
}
function Chips({
options,
actives,
onToggle,
}: { options: string[]; actives: string[]; onToggle: (v: string) => void }) {
return (
<div className="flex flex-wrap gap-2">
{options.map((o) => (
<button
key={o}
onClick={() => onToggle(o)}
className={`px-2 py-1 rounded-full text-xs border ${actives.includes(o) ? "bg-slate-900 text-white" : "bg-white hover:bg-slate-50"}`}
>
{o}
</button>
))}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAddingUser(false)}></Button>
<Button onClick={handleAddUser}></Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}