refactor: streamline mobile interface for data analysis focus
Remove settings, optimize mobile layout, highlight data analysis features Use generic company names, simulate today's timeline, responsive design Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { Home, Users, BarChart3, Database, Settings } from "lucide-react"
|
import { Home, Users, Target, Database } from "lucide-react"
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", icon: Home, label: "首页" },
|
{ href: "/", icon: Home, label: "概览" },
|
||||||
|
{ href: "/data-integration", icon: Database, label: "数据集成" },
|
||||||
{ href: "/user-portrait", icon: Users, label: "用户画像" },
|
{ href: "/user-portrait", icon: Users, label: "用户画像" },
|
||||||
{ href: "/user-value", icon: BarChart3, label: "用户估值" },
|
{ href: "/user-value", icon: Target, label: "价值评估" },
|
||||||
{ href: "/data-platform", icon: Database, label: "数据中台" },
|
|
||||||
{ href: "/settings", icon: Settings, label: "设置" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function BottomNav() {
|
export default function BottomNav() {
|
||||||
@@ -25,11 +24,11 @@ export default function BottomNav() {
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex flex-col items-center justify-center px-3 py-2 rounded-xl transition-all duration-300 min-w-0 flex-1 ${
|
className={`flex flex-col items-center justify-center px-2 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"
|
isActive ? "glass-heavy text-blue-600 scale-105" : "text-gray-600 hover:glass-light hover:text-blue-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className={`h-5 w-5 mb-1 ${isActive ? "text-blue-600" : ""}`} />
|
<item.icon className={`h-4 w-4 mb-1 ${isActive ? "text-blue-600" : ""}`} />
|
||||||
<span className="text-xs font-medium truncate">{item.label}</span>
|
<span className="text-xs font-medium truncate">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { cn } from "@/lib/utils"
|
|||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Settings,
|
|
||||||
Users,
|
Users,
|
||||||
Target,
|
Target,
|
||||||
X,
|
X,
|
||||||
@@ -28,7 +27,7 @@ interface MobileSidebarProps {
|
|||||||
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const [expandedSections, setExpandedSections] = useState({
|
const [expandedSections, setExpandedSections] = useState({
|
||||||
"user-portrait": true,
|
"user-portrait": false,
|
||||||
"user-value": false,
|
"user-value": false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,55 +52,63 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
|||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
title: "概览",
|
title: "数据概览",
|
||||||
href: "/",
|
href: "/",
|
||||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
icon: <LayoutDashboard className="h-4 w-4" />,
|
||||||
primary: true,
|
primary: true,
|
||||||
|
description: "平台整体数据分析",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "数据集成",
|
title: "数据集成",
|
||||||
href: "/data-integration",
|
href: "/data-integration",
|
||||||
icon: <Database className="h-5 w-5" />,
|
icon: <Database className="h-4 w-4" />,
|
||||||
primary: true,
|
primary: true,
|
||||||
tag: "核心",
|
tag: "核心",
|
||||||
|
description: "多源数据整合平台",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "用户画像",
|
title: "用户画像",
|
||||||
href: "/user-portrait",
|
href: "/user-portrait",
|
||||||
icon: <Users className="h-5 w-5" />,
|
icon: <Users className="h-4 w-4" />,
|
||||||
primary: true,
|
primary: true,
|
||||||
expandable: true,
|
expandable: true,
|
||||||
section: "user-portrait",
|
section: "user-portrait",
|
||||||
|
description: "用户标签与分群分析",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "用户词",
|
title: "流量关键词",
|
||||||
href: "/user-portrait/user-keywords",
|
href: "/user-portrait/keywords",
|
||||||
icon: <Tag className="h-4 w-4" />,
|
icon: <Tag className="h-3 w-3" />,
|
||||||
|
description: "关键词价值评估",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "标签管理",
|
title: "标签管理",
|
||||||
href: "/user-portrait/tags",
|
href: "/user-portrait/tags",
|
||||||
icon: <Tag className="h-4 w-4" />,
|
icon: <Tag className="h-3 w-3" />,
|
||||||
|
description: "用户标签分类",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "用户估值",
|
title: "价值评估",
|
||||||
href: "/user-value",
|
href: "/user-value",
|
||||||
icon: <Target className="h-5 w-5" />,
|
icon: <Target className="h-4 w-4" />,
|
||||||
primary: true,
|
primary: true,
|
||||||
expandable: true,
|
expandable: true,
|
||||||
section: "user-value",
|
section: "user-value",
|
||||||
|
description: "用户价值分析模型",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: "估值模型",
|
title: "估值模型",
|
||||||
href: "/user-value/model",
|
href: "/user-value/model",
|
||||||
icon: <BarChart3 className="h-4 w-4" />,
|
icon: <BarChart3 className="h-3 w-3" />,
|
||||||
|
description: "RFM价值模型",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "用户升级路径",
|
title: "升级路径",
|
||||||
href: "/user-value/upgrade-paths",
|
href: "/user-value/upgrade-paths",
|
||||||
icon: <TrendingUp className="h-4 w-4" />,
|
icon: <TrendingUp className="h-3 w-3" />,
|
||||||
|
description: "用户价值提升",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -110,9 +117,9 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
|||||||
const getTagColor = (tag: string) => {
|
const getTagColor = (tag: string) => {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case "核心":
|
case "核心":
|
||||||
return "glass-light text-orange-700 border-orange-200"
|
return "bg-blue-100 text-blue-700 border-blue-200"
|
||||||
default:
|
default:
|
||||||
return "glass-light text-gray-700 border-gray-200"
|
return "bg-gray-100 text-gray-700 border-gray-200"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,56 +137,63 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
|||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-0 top-0 h-full w-80 glass-nav safe-area-top safe-area-left z-50 transition-transform duration-300 ease-out",
|
"fixed left-0 top-0 h-full w-72 glass-nav safe-area-top safe-area-left z-50 transition-transform duration-300 ease-out",
|
||||||
isOpen ? "translate-x-0" : "-translate-x-full",
|
isOpen ? "translate-x-0" : "-translate-x-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/20">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/20">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-8 h-8 rounded-lg glass-light flex items-center justify-center">
|
<div className="w-6 h-6 rounded-lg glass-light flex items-center justify-center">
|
||||||
<Database className="h-5 w-5 text-blue-600" />
|
<Database className="h-4 w-4 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-lg font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
<h1 className="text-sm font-semibold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
用户数字资产中台
|
数据资产中台
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-xl">
|
<Button variant="ghost" size="icon" onClick={onClose} className="glass-light rounded-lg h-8 w-8">
|
||||||
<X className="h-5 w-5" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导航菜单 */}
|
{/* 导航菜单 */}
|
||||||
<div className="flex-1 overflow-y-auto py-4">
|
<div className="flex-1 overflow-y-auto py-2">
|
||||||
<nav className="space-y-2 px-4">
|
<nav className="space-y-1 px-3">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<div key={item.href}>
|
<div key={item.href}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-300 group",
|
"flex items-center px-3 py-2 text-xs font-medium rounded-lg transition-all duration-300 group",
|
||||||
pathname === item.href
|
pathname === item.href
|
||||||
? "glass-heavy text-blue-700 shadow-glass"
|
? "glass-heavy text-blue-700 shadow-glass"
|
||||||
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600",
|
: "glass-light text-gray-700 hover:glass-heavy hover:text-blue-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={item.href} onClick={onClose} className="flex items-center flex-1">
|
<Link href={item.href} onClick={onClose} className="flex items-center flex-1 min-w-0">
|
||||||
<div className="transition-colors duration-300">{item.icon}</div>
|
<div className="transition-colors duration-300 flex-shrink-0">{item.icon}</div>
|
||||||
<div className="flex-1 flex items-center justify-between ml-3">
|
<div className="flex-1 ml-2 min-w-0">
|
||||||
<span className="font-medium">{item.title}</span>
|
<div className="flex items-center justify-between">
|
||||||
{item.tag && (
|
<span className="font-medium truncate">{item.title}</span>
|
||||||
<Badge className={cn("text-xs px-2 py-1 rounded-lg", getTagColor(item.tag))}>{item.tag}</Badge>
|
{item.tag && (
|
||||||
)}
|
<Badge
|
||||||
|
className={cn("text-xs px-1.5 py-0.5 rounded ml-1 flex-shrink-0", getTagColor(item.tag))}
|
||||||
|
>
|
||||||
|
{item.tag}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 truncate mt-0.5">{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{item.expandable && (
|
{item.expandable && (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSection(item.section!)}
|
onClick={() => toggleSection(item.section!)}
|
||||||
className="ml-2 p-1 hover:bg-white/20 rounded-lg transition-colors duration-200"
|
className="ml-1 p-1 hover:bg-white/20 rounded transition-colors duration-200 flex-shrink-0"
|
||||||
>
|
>
|
||||||
{expandedSections[item.section!] ? (
|
{expandedSections[item.section!] ? (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -187,21 +201,26 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
|||||||
|
|
||||||
{/* 子菜单 */}
|
{/* 子菜单 */}
|
||||||
{item.children && expandedSections[item.section!] && (
|
{item.children && expandedSections[item.section!] && (
|
||||||
<div className="ml-6 mt-2 space-y-1">
|
<div className="ml-4 mt-1 space-y-1">
|
||||||
{item.children.map((subItem) => (
|
{item.children.map((subItem) => (
|
||||||
<Link
|
<Link
|
||||||
key={subItem.href}
|
key={subItem.href}
|
||||||
href={subItem.href}
|
href={subItem.href}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center px-3 py-2 text-sm rounded-lg transition-all duration-300",
|
"flex items-center px-2 py-1.5 text-xs rounded transition-all duration-300",
|
||||||
pathname === subItem.href
|
pathname === subItem.href
|
||||||
? "glass-light text-blue-600 shadow-glass-sm"
|
? "glass-light text-blue-600 shadow-glass-sm"
|
||||||
: "text-gray-600 hover:glass-light hover:text-blue-500",
|
: "text-gray-600 hover:glass-light hover:text-blue-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mr-3 text-gray-400 transition-colors duration-300">{subItem.icon}</div>
|
<div className="mr-2 text-gray-400 transition-colors duration-300 flex-shrink-0">
|
||||||
<span>{subItem.title}</span>
|
{subItem.icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium truncate">{subItem.title}</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">{subItem.description}</p>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,21 +230,21 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部设置 */}
|
{/* 底部信息 */}
|
||||||
<div className="border-t border-white/20 p-4">
|
<div className="border-t border-white/20 p-3">
|
||||||
<Link
|
<div className="glass-light rounded-lg p-2">
|
||||||
href="/settings"
|
<div className="flex items-center justify-between text-xs">
|
||||||
onClick={onClose}
|
<span className="text-gray-600">数据同步</span>
|
||||||
className={cn(
|
<div className="flex items-center">
|
||||||
"flex items-center px-4 py-2 text-xs font-medium rounded-lg transition-all duration-300 w-full",
|
<div className="w-2 h-2 bg-green-500 rounded-full mr-1"></div>
|
||||||
pathname === "/settings"
|
<span className="text-green-600">正常</span>
|
||||||
? "glass-heavy text-blue-700 shadow-glass"
|
</div>
|
||||||
: "glass-light text-gray-600 hover:glass-heavy hover:text-blue-500",
|
</div>
|
||||||
)}
|
<div className="flex items-center justify-between text-xs mt-1">
|
||||||
>
|
<span className="text-gray-600">最后更新</span>
|
||||||
<Settings className="h-4 w-4 transition-colors duration-300" />
|
<span className="text-gray-500">2分钟前</span>
|
||||||
<span className="ml-2">设置</span>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -38,48 +38,48 @@ export default function UserPortrait() {
|
|||||||
userList: true,
|
userList: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 使用真实的用户数据
|
// 使用通用公司名称和当天时间线的用户数据
|
||||||
const mockUsers = [
|
const mockUsers = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "李明华",
|
name: "张明华",
|
||||||
company: "阿里巴巴集团",
|
company: "科技创新有限公司",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"],
|
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"],
|
||||||
category: "企业服务",
|
category: "企业服务",
|
||||||
pool: "私域池",
|
pool: "私域池",
|
||||||
lastActive: "2024-01-18",
|
lastActive: "今天 14:30",
|
||||||
value: 28500,
|
value: 28500,
|
||||||
rfmScore: 95,
|
rfmScore: 95,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
{ id: "wx1", name: "李明华", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
|
{ id: "wx1", name: "张明华", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
|
||||||
{ id: "wx2", name: "李明华工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] },
|
{ id: "wx2", name: "张明华工作号", avatar: "/placeholder.svg?height=40&width=40", tags: ["工作账号"] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "张雨婷",
|
name: "李雨婷",
|
||||||
company: "腾讯科技",
|
company: "数字营销公司",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["中高价值", "产品经理", "内容创作者", "Android用户"],
|
tags: ["中高价值", "产品经理", "内容创作者", "Android用户"],
|
||||||
category: "内容营销",
|
category: "内容营销",
|
||||||
pool: "公域池",
|
pool: "公域池",
|
||||||
lastActive: "2024-01-17",
|
lastActive: "今天 13:45",
|
||||||
value: 15800,
|
value: 15800,
|
||||||
rfmScore: 78,
|
rfmScore: 78,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
{ id: "wx3", name: "张雨婷", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
|
{ id: "wx3", name: "李雨婷", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号"] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
name: "王建国",
|
name: "王建国",
|
||||||
company: "字节跳动",
|
company: "软件开发工作室",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"],
|
tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"],
|
||||||
category: "技术社区",
|
category: "技术社区",
|
||||||
pool: "私域池",
|
pool: "私域池",
|
||||||
lastActive: "2024-01-16",
|
lastActive: "今天 12:20",
|
||||||
value: 32400,
|
value: 32400,
|
||||||
rfmScore: 89,
|
rfmScore: 89,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -89,12 +89,12 @@ export default function UserPortrait() {
|
|||||||
{
|
{
|
||||||
id: "4",
|
id: "4",
|
||||||
name: "刘思琪",
|
name: "刘思琪",
|
||||||
company: "美团点评",
|
company: "数据分析咨询",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["中价值", "运营专家", "数据分析师", "Windows用户"],
|
tags: ["中价值", "运营专家", "数据分析师", "Windows用户"],
|
||||||
category: "数据分析",
|
category: "数据分析",
|
||||||
pool: "私域池",
|
pool: "私域池",
|
||||||
lastActive: "2024-01-15",
|
lastActive: "今天 11:15",
|
||||||
value: 18900,
|
value: 18900,
|
||||||
rfmScore: 72,
|
rfmScore: 72,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -104,12 +104,12 @@ export default function UserPortrait() {
|
|||||||
{
|
{
|
||||||
id: "5",
|
id: "5",
|
||||||
name: "陈浩然",
|
name: "陈浩然",
|
||||||
company: "京东集团",
|
company: "云计算服务商",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["高价值", "架构师", "技术领导", "Linux用户"],
|
tags: ["高价值", "架构师", "技术领导", "Linux用户"],
|
||||||
category: "技术架构",
|
category: "技术架构",
|
||||||
pool: "私域池",
|
pool: "私域池",
|
||||||
lastActive: "2024-01-14",
|
lastActive: "今天 10:30",
|
||||||
value: 45600,
|
value: 45600,
|
||||||
rfmScore: 93,
|
rfmScore: 93,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -120,12 +120,12 @@ export default function UserPortrait() {
|
|||||||
{
|
{
|
||||||
id: "6",
|
id: "6",
|
||||||
name: "周欣妍",
|
name: "周欣妍",
|
||||||
company: "滴滴出行",
|
company: "用户体验设计",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["中价值", "UI设计师", "用户体验", "iOS用户"],
|
tags: ["中价值", "UI设计师", "用户体验", "iOS用户"],
|
||||||
category: "设计创意",
|
category: "设计创意",
|
||||||
pool: "公域池",
|
pool: "公域池",
|
||||||
lastActive: "2024-01-13",
|
lastActive: "今天 09:45",
|
||||||
value: 12300,
|
value: 12300,
|
||||||
rfmScore: 65,
|
rfmScore: 65,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -135,12 +135,12 @@ export default function UserPortrait() {
|
|||||||
{
|
{
|
||||||
id: "7",
|
id: "7",
|
||||||
name: "马志强",
|
name: "马志强",
|
||||||
company: "华为技术",
|
company: "企业解决方案",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["高价值", "解决方案专家", "B2B销售", "Android用户"],
|
tags: ["高价值", "解决方案专家", "B2B销售", "Android用户"],
|
||||||
category: "企业解决方案",
|
category: "企业解决方案",
|
||||||
pool: "私域池",
|
pool: "私域池",
|
||||||
lastActive: "2024-01-12",
|
lastActive: "今天 08:20",
|
||||||
value: 38700,
|
value: 38700,
|
||||||
rfmScore: 87,
|
rfmScore: 87,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -150,12 +150,12 @@ export default function UserPortrait() {
|
|||||||
{
|
{
|
||||||
id: "8",
|
id: "8",
|
||||||
name: "赵丽娟",
|
name: "赵丽娟",
|
||||||
company: "小米科技",
|
company: "品牌营销策划",
|
||||||
avatar: "/placeholder.svg?height=40&width=40",
|
avatar: "/placeholder.svg?height=40&width=40",
|
||||||
tags: ["中高价值", "市场营销", "品牌推广", "iOS用户"],
|
tags: ["中高价值", "市场营销", "品牌推广", "iOS用户"],
|
||||||
category: "品牌营销",
|
category: "品牌营销",
|
||||||
pool: "公域池",
|
pool: "公域池",
|
||||||
lastActive: "2024-01-11",
|
lastActive: "今天 07:30",
|
||||||
value: 21500,
|
value: 21500,
|
||||||
rfmScore: 76,
|
rfmScore: 76,
|
||||||
wechatAccounts: [
|
wechatAccounts: [
|
||||||
@@ -167,17 +167,17 @@ export default function UserPortrait() {
|
|||||||
|
|
||||||
const [users, setUsers] = useState(mockUsers)
|
const [users, setUsers] = useState(mockUsers)
|
||||||
|
|
||||||
// 用户词统计
|
// 流量关键词统计
|
||||||
const userKeywordStats = {
|
const keywordStats = {
|
||||||
totalKeywords: 156,
|
totalKeywords: 156,
|
||||||
activeKeywords: 124,
|
activeKeywords: 124,
|
||||||
coverage: 89.4,
|
coverage: 89.4,
|
||||||
topKeywords: [
|
topKeywords: [
|
||||||
{ keyword: "技术", count: 45, trend: "+12%" },
|
{ keyword: "技术解决方案", count: 45, trend: "+12%", value: "高" },
|
||||||
{ keyword: "产品", count: 38, trend: "+8%" },
|
{ keyword: "数据分析", count: 38, trend: "+8%", value: "中高" },
|
||||||
{ keyword: "运营", count: 32, trend: "+15%" },
|
{ keyword: "用户体验", count: 32, trend: "+15%", value: "中" },
|
||||||
{ keyword: "设计", count: 28, trend: "+6%" },
|
{ keyword: "品牌营销", count: 28, trend: "+6%", value: "中高" },
|
||||||
{ keyword: "市场", count: 25, trend: "+10%" },
|
{ keyword: "企业服务", count: 25, trend: "+10%", value: "高" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,81 +237,98 @@ export default function UserPortrait() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValueColor = (value: string) => {
|
||||||
|
switch (value) {
|
||||||
|
case "高":
|
||||||
|
return "bg-green-100 text-green-800"
|
||||||
|
case "中高":
|
||||||
|
return "bg-blue-100 text-blue-800"
|
||||||
|
case "中":
|
||||||
|
return "bg-yellow-100 text-yellow-800"
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6 space-y-6">
|
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">用户画像</h1>
|
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">用户画像</h1>
|
||||||
<p className="text-muted-foreground">全面的用户数据分析和管理</p>
|
<p className="text-sm md:text-base text-muted-foreground">数据驱动的用户分析平台</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button>
|
<Button size="sm" className="flex-1 md:flex-none">
|
||||||
<UserPlus className="mr-2 h-4 w-4" />
|
<UserPlus className="mr-2 h-4 w-4" />
|
||||||
导入用户
|
导入用户
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline">
|
<Button variant="outline" size="sm" className="flex-1 md:flex-none bg-transparent">
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
导出数据
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 用户词管理 */}
|
{/* 流量关键词管理 */}
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={expandedSections.userKeywords}
|
open={expandedSections.userKeywords}
|
||||||
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userKeywords: open }))}
|
onOpenChange={(open) => setExpandedSections((prev) => ({ ...prev, userKeywords: open }))}
|
||||||
>
|
>
|
||||||
<Card className="border shadow-sm">
|
<Card className="border shadow-sm">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer">
|
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Tag className="h-5 w-5 text-blue-600" />
|
<Tag className="h-4 w-4 md:h-5 md:w-5 text-blue-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle>用户词管理</CardTitle>
|
<CardTitle className="text-base md:text-lg">流量关键词</CardTitle>
|
||||||
<CardDescription>用户关键词分析和管理</CardDescription>
|
<CardDescription className="text-xs md:text-sm">关键词价值评估与分类</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.userKeywords ? (
|
{expandedSections.userKeywords ? (
|
||||||
<ChevronDown className="h-5 w-5" />
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-5 w-5" />
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0 px-4 md:px-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4">
|
||||||
<div className="bg-blue-50 p-3 rounded-lg">
|
<div className="bg-blue-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-blue-600">总关键词</p>
|
<p className="text-xs md:text-sm font-medium text-blue-600">总关键词</p>
|
||||||
<p className="text-2xl font-bold text-blue-900">{userKeywordStats.totalKeywords}</p>
|
<p className="text-lg md:text-2xl font-bold text-blue-900">{keywordStats.totalKeywords}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 p-3 rounded-lg">
|
<div className="bg-green-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-green-600">活跃关键词</p>
|
<p className="text-xs md:text-sm font-medium text-green-600">活跃关键词</p>
|
||||||
<p className="text-2xl font-bold text-green-900">{userKeywordStats.activeKeywords}</p>
|
<p className="text-lg md:text-2xl font-bold text-green-900">{keywordStats.activeKeywords}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-50 p-3 rounded-lg">
|
<div className="bg-purple-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-purple-600">覆盖率</p>
|
<p className="text-xs md:text-sm font-medium text-purple-600">覆盖率</p>
|
||||||
<p className="text-2xl font-bold text-purple-900">{userKeywordStats.coverage}%</p>
|
<p className="text-lg md:text-2xl font-bold text-purple-900">{keywordStats.coverage}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-orange-50 p-3 rounded-lg">
|
<div className="bg-orange-50 p-2 md:p-3 rounded-lg">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full h-full bg-transparent"
|
size="sm"
|
||||||
onClick={() => router.push("/user-portrait/user-keywords")}
|
className="w-full h-full bg-transparent text-xs md:text-sm"
|
||||||
|
onClick={() => router.push("/user-portrait/keywords")}
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
|
||||||
管理关键词
|
管理
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
<div className="space-y-2">
|
||||||
{userKeywordStats.topKeywords.map((item, index) => (
|
{keywordStats.topKeywords.map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
<div key={index} className="flex items-center justify-between p-2 md:p-3 bg-gray-50 rounded">
|
||||||
<span className="text-sm font-medium">{item.keyword}</span>
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<div className="text-right">
|
<span className="text-xs md:text-sm font-medium truncate">{item.keyword}</span>
|
||||||
<p className="text-sm font-bold">{item.count}</p>
|
<Badge className={`text-xs ${getValueColor(item.value)}`}>{item.value}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-right flex-shrink-0">
|
||||||
|
<p className="text-xs md:text-sm font-bold">{item.count}</p>
|
||||||
<p className="text-xs text-green-600">{item.trend}</p>
|
<p className="text-xs text-green-600">{item.trend}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -329,54 +346,55 @@ export default function UserPortrait() {
|
|||||||
>
|
>
|
||||||
<Card className="border shadow-sm">
|
<Card className="border shadow-sm">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer">
|
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Tag className="h-5 w-5 text-purple-600" />
|
<Tag className="h-4 w-4 md:h-5 md:w-5 text-purple-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle>标签管理</CardTitle>
|
<CardTitle className="text-base md:text-lg">标签管理</CardTitle>
|
||||||
<CardDescription>用户标签分类和管理</CardDescription>
|
<CardDescription className="text-xs md:text-sm">用户标签分类与管理</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.tagManagement ? (
|
{expandedSections.tagManagement ? (
|
||||||
<ChevronDown className="h-5 w-5" />
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-5 w-5" />
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0 px-4 md:px-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4">
|
||||||
<div className="bg-purple-50 p-3 rounded-lg">
|
<div className="bg-purple-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-purple-600">总标签数</p>
|
<p className="text-xs md:text-sm font-medium text-purple-600">总标签数</p>
|
||||||
<p className="text-2xl font-bold text-purple-900">{tagStats.totalTags}</p>
|
<p className="text-lg md:text-2xl font-bold text-purple-900">{tagStats.totalTags}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 p-3 rounded-lg">
|
<div className="bg-blue-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-blue-600">活跃标签</p>
|
<p className="text-xs md:text-sm font-medium text-blue-600">活跃标签</p>
|
||||||
<p className="text-2xl font-bold text-blue-900">{tagStats.activeTags}</p>
|
<p className="text-lg md:text-2xl font-bold text-blue-900">{tagStats.activeTags}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 p-3 rounded-lg">
|
<div className="bg-green-50 p-2 md:p-3 rounded-lg">
|
||||||
<p className="text-sm font-medium text-green-600">覆盖率</p>
|
<p className="text-xs md:text-sm font-medium text-green-600">覆盖率</p>
|
||||||
<p className="text-2xl font-bold text-green-900">{tagStats.coverage}%</p>
|
<p className="text-lg md:text-2xl font-bold text-green-900">{tagStats.coverage}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-orange-50 p-3 rounded-lg">
|
<div className="bg-orange-50 p-2 md:p-3 rounded-lg">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full h-full bg-transparent"
|
size="sm"
|
||||||
|
className="w-full h-full bg-transparent text-xs md:text-sm"
|
||||||
onClick={() => router.push("/user-portrait/tags")}
|
onClick={() => router.push("/user-portrait/tags")}
|
||||||
>
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
|
||||||
管理标签
|
管理
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
||||||
{tagStats.topTags.map((item, index) => (
|
{tagStats.topTags.map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
<div key={index} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||||
<Badge className={item.color}>{item.tag}</Badge>
|
<Badge className={`${item.color} text-xs`}>{item.tag}</Badge>
|
||||||
<span className="text-sm font-bold">{item.count}</span>
|
<span className="text-xs font-bold ml-2">{item.count}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -392,35 +410,41 @@ export default function UserPortrait() {
|
|||||||
>
|
>
|
||||||
<Card className="border shadow-sm">
|
<Card className="border shadow-sm">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer">
|
<CardHeader className="hover:bg-gray-50 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Users className="h-5 w-5 text-green-600" />
|
<Users className="h-4 w-4 md:h-5 md:w-5 text-green-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle>用户列表</CardTitle>
|
<CardTitle className="text-base md:text-lg">用户列表</CardTitle>
|
||||||
<CardDescription>查看和管理所有用户 ({filteredUsers.length})</CardDescription>
|
<CardDescription className="text-xs md:text-sm">
|
||||||
|
用户数据管理 ({filteredUsers.length})
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.userList ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
{expandedSections.userList ? (
|
||||||
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0 px-4 md:px-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4 mb-4 md:mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="搜索用户、公司或标签..."
|
placeholder="搜索用户、公司或标签..."
|
||||||
className="pl-8"
|
className="pl-8 text-sm"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="text-sm">
|
||||||
<SelectValue placeholder="选择场景分类" />
|
<SelectValue placeholder="选择场景分类" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -433,7 +457,7 @@ export default function UserPortrait() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select value={selectedPool} onValueChange={setSelectedPool}>
|
<Select value={selectedPool} onValueChange={setSelectedPool}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="text-sm">
|
||||||
<SelectValue placeholder="选择流量池" />
|
<SelectValue placeholder="选择流量池" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -446,7 +470,66 @@ export default function UserPortrait() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="space-y-3 md:hidden">
|
||||||
|
{/* 移动端卡片布局 */}
|
||||||
|
{filteredUsers.map((user) => (
|
||||||
|
<div key={user.id} className="bg-white border rounded-lg p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<img src={user.avatar || "/placeholder.svg"} alt={user.name} />
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm">{user.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{user.company}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
user.rfmScore >= 90
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: user.rfmScore >= 80
|
||||||
|
? "bg-blue-100 text-blue-800"
|
||||||
|
: user.rfmScore >= 70
|
||||||
|
? "bg-yellow-100 text-yellow-800"
|
||||||
|
: "bg-gray-100 text-gray-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{user.rfmScore}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-500">{user.lastActive}</span>
|
||||||
|
<span className="font-medium">¥{user.value.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{user.tags.slice(0, 3).map((tag, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{user.tags.length > 3 && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
+{user.tags.length - 3}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => handleWechatClick(user.id)} className="text-xs">
|
||||||
|
<MessageCircle className="h-3 w-3 mr-1" />
|
||||||
|
{user.wechatAccounts.length}个账号
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleUserClick(user.id)} className="text-xs">
|
||||||
|
<Eye className="h-3 w-3 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 桌面端表格布局 */}
|
||||||
|
<div className="hidden md:block overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-50">
|
<tr className="bg-gray-50">
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import {
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||||
|
|
||||||
// 真实用户数据
|
// 使用通用公司名称和当天时间线的用户数据
|
||||||
const mockUsers = [
|
const mockUsers = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "李明华",
|
name: "张明华",
|
||||||
company: "阿里巴巴集团",
|
company: "科技创新有限公司",
|
||||||
recency: 1,
|
recency: 1,
|
||||||
frequency: 28,
|
frequency: 28,
|
||||||
monetary: 45600,
|
monetary: 45600,
|
||||||
@@ -42,11 +42,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 45,
|
upgradeFromDays: 45,
|
||||||
nextUpgrade: "VIP客户",
|
nextUpgrade: "VIP客户",
|
||||||
upgradeProbability: 85,
|
upgradeProbability: 85,
|
||||||
|
lastActive: "今天 14:30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "张雨婷",
|
name: "李雨婷",
|
||||||
company: "腾讯科技",
|
company: "数字营销公司",
|
||||||
recency: 3,
|
recency: 3,
|
||||||
frequency: 22,
|
frequency: 22,
|
||||||
monetary: 32800,
|
monetary: 32800,
|
||||||
@@ -56,11 +57,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 28,
|
upgradeFromDays: 28,
|
||||||
nextUpgrade: "白金会员",
|
nextUpgrade: "白金会员",
|
||||||
upgradeProbability: 78,
|
upgradeProbability: 78,
|
||||||
|
lastActive: "今天 13:45",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
name: "王建国",
|
name: "王建国",
|
||||||
company: "字节跳动",
|
company: "软件开发工作室",
|
||||||
recency: 2,
|
recency: 2,
|
||||||
frequency: 25,
|
frequency: 25,
|
||||||
monetary: 38900,
|
monetary: 38900,
|
||||||
@@ -70,11 +72,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 38,
|
upgradeFromDays: 38,
|
||||||
nextUpgrade: "企业VIP",
|
nextUpgrade: "企业VIP",
|
||||||
upgradeProbability: 82,
|
upgradeProbability: 82,
|
||||||
|
lastActive: "今天 12:20",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "4",
|
id: "4",
|
||||||
name: "刘思琪",
|
name: "刘思琪",
|
||||||
company: "美团点评",
|
company: "数据分析咨询",
|
||||||
recency: 7,
|
recency: 7,
|
||||||
frequency: 18,
|
frequency: 18,
|
||||||
monetary: 25400,
|
monetary: 25400,
|
||||||
@@ -84,11 +87,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 12,
|
upgradeFromDays: 12,
|
||||||
nextUpgrade: "高价值用户",
|
nextUpgrade: "高价值用户",
|
||||||
upgradeProbability: 68,
|
upgradeProbability: 68,
|
||||||
|
lastActive: "今天 11:15",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "5",
|
id: "5",
|
||||||
name: "陈浩然",
|
name: "陈浩然",
|
||||||
company: "京东集团",
|
company: "云计算服务商",
|
||||||
recency: 4,
|
recency: 4,
|
||||||
frequency: 20,
|
frequency: 20,
|
||||||
monetary: 28700,
|
monetary: 28700,
|
||||||
@@ -98,11 +102,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 22,
|
upgradeFromDays: 22,
|
||||||
nextUpgrade: "高价值用户",
|
nextUpgrade: "高价值用户",
|
||||||
upgradeProbability: 72,
|
upgradeProbability: 72,
|
||||||
|
lastActive: "今天 10:30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "6",
|
id: "6",
|
||||||
name: "周欣妍",
|
name: "周欣妍",
|
||||||
company: "滴滴出行",
|
company: "用户体验设计",
|
||||||
recency: 12,
|
recency: 12,
|
||||||
frequency: 15,
|
frequency: 15,
|
||||||
monetary: 18600,
|
monetary: 18600,
|
||||||
@@ -112,11 +117,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 8,
|
upgradeFromDays: 8,
|
||||||
nextUpgrade: "中高价值用户",
|
nextUpgrade: "中高价值用户",
|
||||||
upgradeProbability: 55,
|
upgradeProbability: 55,
|
||||||
|
lastActive: "今天 09:45",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "7",
|
id: "7",
|
||||||
name: "马志强",
|
name: "马志强",
|
||||||
company: "华为技术",
|
company: "企业解决方案",
|
||||||
recency: 6,
|
recency: 6,
|
||||||
frequency: 16,
|
frequency: 16,
|
||||||
monetary: 22100,
|
monetary: 22100,
|
||||||
@@ -126,11 +132,12 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 15,
|
upgradeFromDays: 15,
|
||||||
nextUpgrade: "中高价值用户",
|
nextUpgrade: "中高价值用户",
|
||||||
upgradeProbability: 61,
|
upgradeProbability: 61,
|
||||||
|
lastActive: "今天 08:20",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "8",
|
id: "8",
|
||||||
name: "赵丽娟",
|
name: "赵丽娟",
|
||||||
company: "小米科技",
|
company: "品牌营销策划",
|
||||||
recency: 15,
|
recency: 15,
|
||||||
frequency: 12,
|
frequency: 12,
|
||||||
monetary: 15800,
|
monetary: 15800,
|
||||||
@@ -140,6 +147,7 @@ const mockUsers = [
|
|||||||
upgradeFromDays: 5,
|
upgradeFromDays: 5,
|
||||||
nextUpgrade: "中高价值用户",
|
nextUpgrade: "中高价值用户",
|
||||||
upgradeProbability: 45,
|
upgradeProbability: 45,
|
||||||
|
lastActive: "今天 07:30",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -186,33 +194,33 @@ export default function UserValuePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-6">
|
<div className="container mx-auto p-4 space-y-4 md:space-y-6">
|
||||||
{/* 页面标题和工具栏 */}
|
{/* 页面标题和工具栏 */}
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">用户估值</h1>
|
<h1 className="text-2xl md:text-3xl font-bold">价值评估</h1>
|
||||||
<p className="text-muted-foreground mt-1">用户价值评估与升级路径分析</p>
|
<p className="text-sm md:text-base text-muted-foreground mt-1">用户价值分析与升级路径</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[120px] md:w-[140px] text-sm">
|
||||||
<SelectValue placeholder="选择时间范围" />
|
<SelectValue placeholder="时间范围" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="7days">最近7天</SelectItem>
|
<SelectItem value="7days">近7天</SelectItem>
|
||||||
<SelectItem value="30days">最近30天</SelectItem>
|
<SelectItem value="30days">近30天</SelectItem>
|
||||||
<SelectItem value="90days">最近90天</SelectItem>
|
<SelectItem value="90days">近90天</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
|
||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline">
|
<Button variant="outline" size="sm" className="hidden md:flex bg-transparent">
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
导出报告
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,118 +232,122 @@ export default function UserValuePage() {
|
|||||||
>
|
>
|
||||||
<Card className="border-none shadow-md">
|
<Card className="border-none shadow-md">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b hover:from-green-100 hover:to-emerald-100 transition-colors cursor-pointer">
|
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 border-b hover:from-green-100 hover:to-emerald-100 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Target className="h-5 w-5 text-green-600" />
|
<Target className="h-4 w-4 md:h-5 md:w-5 text-green-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle>用户估值概览</CardTitle>
|
<CardTitle className="text-base md:text-lg">价值概览</CardTitle>
|
||||||
<CardDescription>用户价值评估整体情况</CardDescription>
|
<CardDescription className="text-xs md:text-sm">用户价值评估整体情况</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.overview ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
{expandedSections.overview ? (
|
||||||
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4">
|
||||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 border-none shadow-sm">
|
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 border-none shadow-sm">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-3 md:p-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-green-600">平均用户估值</div>
|
<div className="text-xs md:text-sm font-medium text-green-600">平均估值</div>
|
||||||
<div className="text-2xl font-bold mt-2">¥3,248</div>
|
<div className="text-lg md:text-2xl font-bold mt-1 md:mt-2">¥3,248</div>
|
||||||
<div className="text-sm text-green-600 mt-1 flex items-center">
|
<div className="text-xs text-green-600 mt-1 flex items-center">
|
||||||
<ArrowUpRight className="h-4 w-4 mr-1" />
|
<ArrowUpRight className="h-3 w-3 md:h-4 md:w-4 mr-1" />
|
||||||
较上月增长 8.2%
|
+8.2%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-100 p-2 rounded-full">
|
<div className="bg-green-100 p-1.5 md:p-2 rounded-full">
|
||||||
<Target className="h-5 w-5 text-green-600" />
|
<Target className="h-3 w-3 md:h-5 md:w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2 md:mt-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span>预期目标</span>
|
<span>目标</span>
|
||||||
<span>94%</span>
|
<span>94%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={94} className="h-1.5 bg-green-100" />
|
<Progress value={94} className="h-1 md:h-1.5 bg-green-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-none shadow-sm">
|
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 border-none shadow-sm">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-3 md:p-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-purple-600">高价值用户</div>
|
<div className="text-xs md:text-sm font-medium text-purple-600">高价值用户</div>
|
||||||
<div className="text-2xl font-bold mt-2">45,678</div>
|
<div className="text-lg md:text-2xl font-bold mt-1 md:mt-2">45,678</div>
|
||||||
<div className="text-sm text-green-600 mt-1 flex items-center">
|
<div className="text-xs text-green-600 mt-1 flex items-center">
|
||||||
<ArrowUpRight className="h-4 w-4 mr-1" />
|
<ArrowUpRight className="h-3 w-3 md:h-4 md:w-4 mr-1" />
|
||||||
较上月增长 12.3%
|
+12.3%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-100 p-2 rounded-full">
|
<div className="bg-purple-100 p-1.5 md:p-2 rounded-full">
|
||||||
<TrendingUp className="h-5 w-5 text-purple-600" />
|
<TrendingUp className="h-3 w-3 md:h-5 md:w-5 text-purple-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2 md:mt-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span>占总用户比例</span>
|
<span>占比</span>
|
||||||
<span>28.6%</span>
|
<span>28.6%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={28.6} className="h-1.5 bg-purple-100" />
|
<Progress value={28.6} className="h-1 md:h-1.5 bg-purple-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-none shadow-sm">
|
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-none shadow-sm">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-3 md:p-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-blue-600">用户价值增长率</div>
|
<div className="text-xs md:text-sm font-medium text-blue-600">价值增长率</div>
|
||||||
<div className="text-2xl font-bold mt-2">15.8%</div>
|
<div className="text-lg md:text-2xl font-bold mt-1 md:mt-2">15.8%</div>
|
||||||
<div className="text-sm text-green-600 mt-1 flex items-center">
|
<div className="text-xs text-green-600 mt-1 flex items-center">
|
||||||
<ArrowUpRight className="h-4 w-4 mr-1" />
|
<ArrowUpRight className="h-3 w-3 md:h-4 md:w-4 mr-1" />
|
||||||
较上月提升 2.1%
|
+2.1%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-100 p-2 rounded-full">
|
<div className="bg-blue-100 p-1.5 md:p-2 rounded-full">
|
||||||
<BarChart3 className="h-5 w-5 text-blue-600" />
|
<BarChart3 className="h-3 w-3 md:h-5 md:w-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2 md:mt-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span>行业平均</span>
|
<span>行业平均</span>
|
||||||
<span>11.2%</span>
|
<span>11.2%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={141} className="h-1.5 bg-blue-100" />
|
<Progress value={141} className="h-1 md:h-1.5 bg-blue-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-gradient-to-br from-amber-50 to-yellow-50 border-none shadow-sm">
|
<Card className="bg-gradient-to-br from-amber-50 to-yellow-50 border-none shadow-sm">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-3 md:p-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-amber-600">用户升级率</div>
|
<div className="text-xs md:text-sm font-medium text-amber-600">升级率</div>
|
||||||
<div className="text-2xl font-bold mt-2">12.5%</div>
|
<div className="text-lg md:text-2xl font-bold mt-1 md:mt-2">12.5%</div>
|
||||||
<div className="text-sm text-green-600 mt-1 flex items-center">
|
<div className="text-xs text-green-600 mt-1 flex items-center">
|
||||||
<ArrowUpRight className="h-4 w-4 mr-1" />
|
<ArrowUpRight className="h-3 w-3 md:h-4 md:w-4 mr-1" />
|
||||||
较上月提升 1.8%
|
+1.8%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-amber-100 p-2 rounded-full">
|
<div className="bg-amber-100 p-1.5 md:p-2 rounded-full">
|
||||||
<Users className="h-5 w-5 text-amber-600" />
|
<Users className="h-3 w-3 md:h-5 md:w-5 text-amber-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-2 md:mt-3">
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1">
|
||||||
<span>目标</span>
|
<span>目标</span>
|
||||||
<span>15%</span>
|
<span>15%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={83} className="h-1.5 bg-amber-100" />
|
<Progress value={83} className="h-1 md:h-1.5 bg-amber-100" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -352,13 +364,13 @@ export default function UserValuePage() {
|
|||||||
>
|
>
|
||||||
<Card className="border-none shadow-md">
|
<Card className="border-none shadow-md">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b hover:from-blue-100 hover:to-indigo-100 transition-colors cursor-pointer">
|
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b hover:from-blue-100 hover:to-indigo-100 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<BarChart3 className="h-5 w-5 text-blue-600" />
|
<BarChart3 className="h-4 w-4 md:h-5 md:w-5 text-blue-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||||||
RFM用户价值模型
|
RFM估值模型
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -366,44 +378,48 @@ export default function UserValuePage() {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowModelDialog(true)
|
setShowModelDialog(true)
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 p-0"
|
className="h-5 w-5 md:h-6 md:w-6 p-0"
|
||||||
>
|
>
|
||||||
<HelpCircle className="h-4 w-4" />
|
<HelpCircle className="h-3 w-3 md:h-4 md:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>基于用户行为的价值评估模型</CardDescription>
|
<CardDescription className="text-xs md:text-sm">基于用户行为的价值评估</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.model ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
{expandedSections.model ? (
|
||||||
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="font-semibold flex items-center">
|
<h3 className="font-semibold flex items-center text-sm md:text-base">
|
||||||
<span className="bg-red-100 text-red-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
|
<span className="bg-red-100 text-red-800 w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center mr-2 text-xs md:text-sm">
|
||||||
R
|
R
|
||||||
</span>
|
</span>
|
||||||
Recency (最近消费时间)
|
最近消费时间
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">用户最近一次交易的时间间隔,时间越短价值越高。</p>
|
<p className="text-xs md:text-sm text-muted-foreground">时间间隔越短价值越高</p>
|
||||||
<div className="bg-gray-50 p-3 rounded-md">
|
<div className="bg-gray-50 p-2 md:p-3 rounded-md">
|
||||||
<div className="text-sm">
|
<div className="text-xs md:text-sm space-y-1">
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>1-3天</span>
|
<span>1-3天</span>
|
||||||
<span className="font-medium">5分</span>
|
<span className="font-medium">5分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>4-7天</span>
|
<span>4-7天</span>
|
||||||
<span className="font-medium">4分</span>
|
<span className="font-medium">4分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>8-14天</span>
|
<span>8-14天</span>
|
||||||
<span className="font-medium">3分</span>
|
<span className="font-medium">3分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>15-30天</span>
|
<span>15-30天</span>
|
||||||
<span className="font-medium">2分</span>
|
<span className="font-medium">2分</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,28 +432,28 @@ export default function UserValuePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="font-semibold flex items-center">
|
<h3 className="font-semibold flex items-center text-sm md:text-base">
|
||||||
<span className="bg-green-100 text-green-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
|
<span className="bg-green-100 text-green-800 w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center mr-2 text-xs md:text-sm">
|
||||||
F
|
F
|
||||||
</span>
|
</span>
|
||||||
Frequency (消费频率)
|
消费频率
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">用户在一段时间内的交易次数,频率越高价值越高。</p>
|
<p className="text-xs md:text-sm text-muted-foreground">频率越高价值越高</p>
|
||||||
<div className="bg-gray-50 p-3 rounded-md">
|
<div className="bg-gray-50 p-2 md:p-3 rounded-md">
|
||||||
<div className="text-sm">
|
<div className="text-xs md:text-sm space-y-1">
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>25次以上/月</span>
|
<span>25次以上/月</span>
|
||||||
<span className="font-medium">5分</span>
|
<span className="font-medium">5分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>20-24次/月</span>
|
<span>20-24次/月</span>
|
||||||
<span className="font-medium">4分</span>
|
<span className="font-medium">4分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>15-19次/月</span>
|
<span>15-19次/月</span>
|
||||||
<span className="font-medium">3分</span>
|
<span className="font-medium">3分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>10-14次/月</span>
|
<span>10-14次/月</span>
|
||||||
<span className="font-medium">2分</span>
|
<span className="font-medium">2分</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -450,28 +466,28 @@ export default function UserValuePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="font-semibold flex items-center">
|
<h3 className="font-semibold flex items-center text-sm md:text-base">
|
||||||
<span className="bg-blue-100 text-blue-800 w-6 h-6 rounded-full flex items-center justify-center mr-2">
|
<span className="bg-blue-100 text-blue-800 w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center mr-2 text-xs md:text-sm">
|
||||||
M
|
M
|
||||||
</span>
|
</span>
|
||||||
Monetary (消费金额)
|
消费金额
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">用户在一段时间内的消费金额,金额越高价值越高。</p>
|
<p className="text-xs md:text-sm text-muted-foreground">金额越高价值越高</p>
|
||||||
<div className="bg-gray-50 p-3 rounded-md">
|
<div className="bg-gray-50 p-2 md:p-3 rounded-md">
|
||||||
<div className="text-sm">
|
<div className="text-xs md:text-sm space-y-1">
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>30000元以上</span>
|
<span>30000元以上</span>
|
||||||
<span className="font-medium">5分</span>
|
<span className="font-medium">5分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>20000-29999元</span>
|
<span>20000-29999元</span>
|
||||||
<span className="font-medium">4分</span>
|
<span className="font-medium">4分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>10000-19999元</span>
|
<span>10000-19999元</span>
|
||||||
<span className="font-medium">3分</span>
|
<span className="font-medium">3分</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between">
|
||||||
<span>5000-9999元</span>
|
<span>5000-9999元</span>
|
||||||
<span className="font-medium">2分</span>
|
<span className="font-medium">2分</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -484,28 +500,28 @@ export default function UserValuePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-4 md:mt-6">
|
||||||
<h3 className="font-semibold mb-3">RFM评分等级</h3>
|
<h3 className="font-semibold mb-3 text-sm md:text-base">RFM评分等级</h3>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 md:gap-3">
|
||||||
<div className="bg-green-100 p-3 rounded-md text-center">
|
<div className="bg-green-100 p-2 md:p-3 rounded-md text-center">
|
||||||
<p className="font-medium text-green-800">高价值用户</p>
|
<p className="font-medium text-green-800 text-xs md:text-sm">高价值用户</p>
|
||||||
<p className="text-sm text-green-700">85-100分</p>
|
<p className="text-xs text-green-700">85-100分</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-100 p-3 rounded-md text-center">
|
<div className="bg-blue-100 p-2 md:p-3 rounded-md text-center">
|
||||||
<p className="font-medium text-blue-800">中高价值用户</p>
|
<p className="font-medium text-blue-800 text-xs md:text-sm">中高价值用户</p>
|
||||||
<p className="text-sm text-blue-700">70-84分</p>
|
<p className="text-xs text-blue-700">70-84分</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-yellow-100 p-3 rounded-md text-center">
|
<div className="bg-yellow-100 p-2 md:p-3 rounded-md text-center">
|
||||||
<p className="font-medium text-yellow-800">中等价值用户</p>
|
<p className="font-medium text-yellow-800 text-xs md:text-sm">中等价值用户</p>
|
||||||
<p className="text-sm text-yellow-700">55-69分</p>
|
<p className="text-xs text-yellow-700">55-69分</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 p-3 rounded-md text-center">
|
<div className="bg-gray-100 p-2 md:p-3 rounded-md text-center">
|
||||||
<p className="font-medium text-gray-800">低价值用户</p>
|
<p className="font-medium text-gray-800 text-xs md:text-sm">低价值用户</p>
|
||||||
<p className="text-sm text-gray-700">40-54分</p>
|
<p className="text-xs text-gray-700">40-54分</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-red-100 p-3 rounded-md text-center">
|
<div className="bg-red-100 p-2 md:p-3 rounded-md text-center">
|
||||||
<p className="font-medium text-red-800">流失风险用户</p>
|
<p className="font-medium text-red-800 text-xs md:text-sm">流失风险用户</p>
|
||||||
<p className="text-sm text-red-700">0-39分</p>
|
<p className="text-xs text-red-700">0-39分</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -521,35 +537,75 @@ export default function UserValuePage() {
|
|||||||
>
|
>
|
||||||
<Card className="border-none shadow-md">
|
<Card className="border-none shadow-md">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 border-b hover:from-purple-100 hover:to-pink-100 transition-colors cursor-pointer">
|
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 border-b hover:from-purple-100 hover:to-pink-100 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Users className="h-5 w-5 text-purple-600" />
|
<Users className="h-4 w-4 md:h-5 md:w-5 text-purple-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle>用户价值分析</CardTitle>
|
<CardTitle className="text-base md:text-lg">价值分析</CardTitle>
|
||||||
<CardDescription>用户价值分布与详细数据 ({filteredUsers.length})</CardDescription>
|
<CardDescription className="text-xs md:text-sm">
|
||||||
|
用户价值详细数据 ({filteredUsers.length})
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.analysis ? <ChevronDown className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
{expandedSections.analysis ? (
|
||||||
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="relative w-64">
|
<div className="relative w-full md:w-64">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="搜索用户或公司..."
|
placeholder="搜索用户或公司..."
|
||||||
className="pl-8"
|
className="pl-8 text-sm"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
{/* 移动端卡片布局 */}
|
||||||
|
<div className="space-y-3 md:hidden">
|
||||||
|
{filteredUsers.map((user) => (
|
||||||
|
<div key={user.id} className="bg-white border rounded-lg p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm">{user.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{user.company}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={`${getRFMLevelColor(user.level)} text-xs`}>{user.rfmScore}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-500">R值</p>
|
||||||
|
<p className="font-medium">{user.recency}天</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-500">F值</p>
|
||||||
|
<p className="font-medium">{user.frequency}次</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-500">M值</p>
|
||||||
|
<p className="font-medium">¥{(user.monetary / 1000).toFixed(0)}K</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-500">{user.lastActive}</span>
|
||||||
|
<span className="text-xs text-green-600">{user.growth}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 桌面端表格布局 */}
|
||||||
|
<div className="hidden md:block rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -593,13 +649,13 @@ export default function UserValuePage() {
|
|||||||
>
|
>
|
||||||
<Card className="border-none shadow-md">
|
<Card className="border-none shadow-md">
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 border-b hover:from-orange-100 hover:to-red-100 transition-colors cursor-pointer">
|
<CardHeader className="bg-gradient-to-r from-orange-50 to-red-50 border-b hover:from-orange-100 hover:to-red-100 transition-colors cursor-pointer py-3 md:py-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<TrendingUp className="h-5 w-5 text-orange-600" />
|
<TrendingUp className="h-4 w-4 md:h-5 md:w-5 text-orange-600" />
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||||||
用户升级路径分析
|
升级路径分析
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -607,45 +663,45 @@ export default function UserValuePage() {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowUpgradeDialog(true)
|
setShowUpgradeDialog(true)
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 p-0"
|
className="h-5 w-5 md:h-6 md:w-6 p-0"
|
||||||
>
|
>
|
||||||
<HelpCircle className="h-4 w-4" />
|
<HelpCircle className="h-3 w-3 md:h-4 md:w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>用户价值提升路径与策略</CardDescription>
|
<CardDescription className="text-xs md:text-sm">用户价值提升路径与策略</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expandedSections.upgradePaths ? (
|
{expandedSections.upgradePaths ? (
|
||||||
<ChevronDown className="h-5 w-5" />
|
<ChevronDown className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-5 w-5" />
|
<ChevronRight className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4 mb-4 md:mb-6">
|
||||||
<Card className="bg-white shadow-sm">
|
<Card className="bg-white shadow-sm">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-lg flex items-center">
|
<CardTitle className="text-sm md:text-lg flex items-center">
|
||||||
<Target className="h-5 w-5 mr-2 text-green-600" />
|
<Target className="h-4 w-4 md:h-5 md:w-5 mr-2 text-green-600" />
|
||||||
升级潜力分析
|
升级潜力
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="text-xs md:text-sm">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1 md:space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">高潜力用户</span>
|
<span>高潜力用户</span>
|
||||||
<span className="text-sm font-medium">2,345</span>
|
<span className="font-medium">2,345</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">中潜力用户</span>
|
<span>中潜力用户</span>
|
||||||
<span className="text-sm font-medium">5,678</span>
|
<span className="font-medium">5,678</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">低潜力用户</span>
|
<span>低潜力用户</span>
|
||||||
<span className="text-sm font-medium">1,234</span>
|
<span className="font-medium">1,234</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -653,24 +709,24 @@ export default function UserValuePage() {
|
|||||||
|
|
||||||
<Card className="bg-white shadow-sm">
|
<Card className="bg-white shadow-sm">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-lg flex items-center">
|
<CardTitle className="text-sm md:text-lg flex items-center">
|
||||||
<Layers className="h-5 w-5 mr-2 text-blue-600" />
|
<Layers className="h-4 w-4 md:h-5 md:w-5 mr-2 text-blue-600" />
|
||||||
升级成功率
|
升级成功率
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="text-xs md:text-sm">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1 md:space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">本月升级率</span>
|
<span>本月升级率</span>
|
||||||
<span className="text-sm font-medium">12.5%</span>
|
<span className="font-medium">12.5%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">平均升级周期</span>
|
<span>平均升级周期</span>
|
||||||
<span className="text-sm font-medium">28天</span>
|
<span className="font-medium">28天</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">预期升级用户</span>
|
<span>预期升级用户</span>
|
||||||
<span className="text-sm font-medium">1,856</span>
|
<span className="font-medium">1,856</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -678,54 +734,54 @@ export default function UserValuePage() {
|
|||||||
|
|
||||||
<Card className="bg-white shadow-sm">
|
<Card className="bg-white shadow-sm">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-lg flex items-center">
|
<CardTitle className="text-sm md:text-lg flex items-center">
|
||||||
<Tag className="h-5 w-5 mr-2 text-purple-600" />
|
<Tag className="h-4 w-4 md:h-5 md:w-5 mr-2 text-purple-600" />
|
||||||
升级价值贡献
|
价值贡献
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="text-xs md:text-sm">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1 md:space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">升级价值增长</span>
|
<span>升级价值增长</span>
|
||||||
<span className="text-sm font-medium">¥2.3M</span>
|
<span className="font-medium">¥2.3M</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">平均用户价值提升</span>
|
<span>平均价值提升</span>
|
||||||
<span className="text-sm font-medium">¥1,245</span>
|
<span className="font-medium">¥1,245</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm">ROI提升</span>
|
<span>ROI提升</span>
|
||||||
<span className="text-sm font-medium">35.8%</span>
|
<span className="font-medium">35.8%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 md:space-y-4">
|
||||||
<h3 className="text-lg font-medium">个人升级路径推荐</h3>
|
<h3 className="text-sm md:text-lg font-medium">个人升级路径推荐</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2 md:space-y-3">
|
||||||
{filteredUsers.slice(0, 5).map((user) => (
|
{filteredUsers.slice(0, 5).map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50"
|
className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 border rounded-lg hover:bg-gray-50 space-y-2 md:space-y-0"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{user.name}</p>
|
<p className="font-medium text-sm md:text-base">{user.name}</p>
|
||||||
<p className="text-sm text-gray-500">{user.company}</p>
|
<p className="text-xs md:text-sm text-gray-500">{user.company}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={getRFMLevelColor(user.level)}>{user.level}</Badge>
|
<Badge className={`${getRFMLevelColor(user.level)} text-xs`}>{user.level}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-2 md:gap-4">
|
||||||
<div className="text-right">
|
<div className="text-left md:text-right">
|
||||||
<p className="text-sm font-medium">下一级别: {user.nextUpgrade}</p>
|
<p className="text-xs md:text-sm font-medium">下一级别: {user.nextUpgrade}</p>
|
||||||
<p className="text-sm text-gray-500">升级概率: {user.upgradeProbability}%</p>
|
<p className="text-xs text-gray-500">升级概率: {user.upgradeProbability}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-24">
|
<div className="w-full md:w-24">
|
||||||
<Progress value={user.upgradeProbability} className="h-2" />
|
<Progress value={user.upgradeProbability} className="h-1.5 md:h-2" />
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm" className="text-xs bg-transparent">
|
||||||
查看策略
|
查看策略
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -9,269 +9,555 @@ import { Textarea } from "@/components/ui/textarea"
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { AlertTriangle, Check } from "lucide-react"
|
import { AlertCircle, Plus, X, Save, TestTube } from "lucide-react"
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||||
|
|
||||||
interface RuleEditorProps {
|
interface TagRule {
|
||||||
ruleId: string | null
|
id?: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
conditions: RuleCondition[]
|
||||||
|
actions: RuleAction[]
|
||||||
|
priority: number
|
||||||
|
enabled: boolean
|
||||||
|
schedule?: RuleSchedule
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RuleEditor({ ruleId }: RuleEditorProps) {
|
interface RuleCondition {
|
||||||
const [rule, setRule] = useState({
|
field: string
|
||||||
|
operator: string
|
||||||
|
value: string
|
||||||
|
logicalOperator?: "AND" | "OR"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleAction {
|
||||||
|
type: "add_tag" | "remove_tag" | "update_field"
|
||||||
|
target: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleSchedule {
|
||||||
|
type: "immediate" | "scheduled" | "recurring"
|
||||||
|
startTime?: string
|
||||||
|
interval?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleEditorProps {
|
||||||
|
ruleId?: string
|
||||||
|
onSave: (rule: TagRule) => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RuleEditor({ ruleId, onSave, onCancel }: RuleEditorProps) {
|
||||||
|
const [rule, setRule] = useState<TagRule>({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
targetTag: "",
|
conditions: [{ field: "", operator: "", value: "" }],
|
||||||
condition: "",
|
actions: [{ type: "add_tag", target: "", value: "" }],
|
||||||
priority: "2",
|
priority: 1,
|
||||||
status: "draft",
|
enabled: true,
|
||||||
sql: "",
|
|
||||||
})
|
})
|
||||||
const [activeTab, setActiveTab] = useState("visual")
|
|
||||||
const [validationResult, setValidationResult] = useState<{
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
isValid: boolean
|
const [testResults, setTestResults] = useState<any>(null)
|
||||||
message: string
|
const [validationErrors, setValidationErrors] = useState<string[]>([])
|
||||||
} | null>(null)
|
|
||||||
|
const fieldOptions = [
|
||||||
|
{ value: "user_type", label: "用户类型" },
|
||||||
|
{ value: "registration_date", label: "注册日期" },
|
||||||
|
{ value: "last_login", label: "最后登录" },
|
||||||
|
{ value: "purchase_amount", label: "购买金额" },
|
||||||
|
{ value: "activity_score", label: "活跃度评分" },
|
||||||
|
{ value: "device_type", label: "设备类型" },
|
||||||
|
{ value: "location", label: "地理位置" },
|
||||||
|
{ value: "age", label: "年龄" },
|
||||||
|
{ value: "gender", label: "性别" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const operatorOptions = [
|
||||||
|
{ value: "equals", label: "等于" },
|
||||||
|
{ value: "not_equals", label: "不等于" },
|
||||||
|
{ value: "greater_than", label: "大于" },
|
||||||
|
{ value: "less_than", label: "小于" },
|
||||||
|
{ value: "contains", label: "包含" },
|
||||||
|
{ value: "not_contains", label: "不包含" },
|
||||||
|
{ value: "starts_with", label: "开始于" },
|
||||||
|
{ value: "ends_with", label: "结束于" },
|
||||||
|
{ value: "in_range", label: "在范围内" },
|
||||||
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ruleId) {
|
if (ruleId) {
|
||||||
// 这里应该是获取规则详情的逻辑
|
loadRule(ruleId)
|
||||||
// 模拟从服务器获取数据
|
|
||||||
setTimeout(() => {
|
|
||||||
setRule({
|
|
||||||
name: "高价值用户识别",
|
|
||||||
description: "根据用户消费金额和频次识别高价值用户",
|
|
||||||
targetTag: "高价值用户",
|
|
||||||
condition: "消费金额 > 5000 AND 消费频次 > 10",
|
|
||||||
priority: "1",
|
|
||||||
status: "active",
|
|
||||||
sql: "SELECT user_id FROM user_behavior WHERE total_amount > 5000 AND purchase_count > 10",
|
|
||||||
})
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
}, [ruleId])
|
}, [ruleId])
|
||||||
|
|
||||||
const handleValidate = () => {
|
const loadRule = async (id: string) => {
|
||||||
// 模拟验证逻辑
|
setIsLoading(true)
|
||||||
if (rule.condition.trim() === "") {
|
try {
|
||||||
setValidationResult({
|
// 模拟API调用
|
||||||
isValid: false,
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
message: "规则条件不能为空",
|
// 这里应该从API加载规则数据
|
||||||
|
setRule({
|
||||||
|
id,
|
||||||
|
name: "示例规则",
|
||||||
|
description: "这是一个示例规则",
|
||||||
|
conditions: [{ field: "user_type", operator: "equals", value: "premium" }],
|
||||||
|
actions: [{ type: "add_tag", target: "tag", value: "高价值用户" }],
|
||||||
|
priority: 1,
|
||||||
|
enabled: true,
|
||||||
})
|
})
|
||||||
return
|
} catch (error) {
|
||||||
|
console.error("加载规则失败:", error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRule = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
if (!rule.name.trim()) {
|
||||||
|
errors.push("规则名称不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTab === "sql" && rule.sql.trim() === "") {
|
if (rule.conditions.length === 0) {
|
||||||
setValidationResult({
|
errors.push("至少需要一个条件")
|
||||||
isValid: false,
|
|
||||||
message: "SQL 语句不能为空",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟验证成功
|
rule.conditions.forEach((condition, index) => {
|
||||||
setValidationResult({
|
if (!condition.field || !condition.operator || !condition.value) {
|
||||||
isValid: true,
|
errors.push(`条件 ${index + 1} 不完整`)
|
||||||
message: "规则验证通过,可以保存或执行",
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (rule.actions.length === 0) {
|
||||||
|
errors.push("至少需要一个动作")
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.actions.forEach((action, index) => {
|
||||||
|
if (!action.target || !action.value) {
|
||||||
|
errors.push(`动作 ${index + 1} 不完整`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
// 这里应该是保存规则的逻辑
|
const errors = validateRule()
|
||||||
console.log("保存规则:", rule)
|
setValidationErrors(errors)
|
||||||
// 模拟保存成功
|
|
||||||
alert("规则保存成功")
|
if (errors.length === 0) {
|
||||||
|
onSave(rule)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExecute = () => {
|
const handleTest = async () => {
|
||||||
// 这里应该是执行规则的逻辑
|
setIsLoading(true)
|
||||||
console.log("执行规则:", rule)
|
try {
|
||||||
// 模拟执行成功
|
// 模拟测试规则
|
||||||
alert("规则执行成功,影响用户 1250 人")
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
setTestResults({
|
||||||
|
matchedUsers: 156,
|
||||||
|
estimatedImpact: "将为156个用户添加标签",
|
||||||
|
executionTime: "预计执行时间: 2分钟",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("测试规则失败:", error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCondition = () => {
|
||||||
|
setRule({
|
||||||
|
...rule,
|
||||||
|
conditions: [...rule.conditions, { field: "", operator: "", value: "" }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCondition = (index: number) => {
|
||||||
|
const newConditions = rule.conditions.filter((_, i) => i !== index)
|
||||||
|
setRule({ ...rule, conditions: newConditions })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCondition = (index: number, field: keyof RuleCondition, value: string) => {
|
||||||
|
const newConditions = [...rule.conditions]
|
||||||
|
newConditions[index] = { ...newConditions[index], [field]: value }
|
||||||
|
setRule({ ...rule, conditions: newConditions })
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAction = () => {
|
||||||
|
setRule({
|
||||||
|
...rule,
|
||||||
|
actions: [...rule.actions, { type: "add_tag", target: "", value: "" }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAction = (index: number) => {
|
||||||
|
const newActions = rule.actions.filter((_, i) => i !== index)
|
||||||
|
setRule({ ...rule, actions: newActions })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAction = (index: number, field: keyof RuleAction, value: string) => {
|
||||||
|
const newActions = [...rule.actions]
|
||||||
|
newActions[index] = { ...newActions[index], [field]: value }
|
||||||
|
setRule({ ...rule, actions: newActions })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading && !rule.name) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p>加载规则中...</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{ruleId ? "编辑规则" : "创建新规则"}</CardTitle>
|
<CardTitle>{ruleId ? "编辑规则" : "创建新规则"}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{ruleId ? "修改现有标签规则的配置" : "创建新的自动化标签规则"}</CardDescription>
|
||||||
{ruleId ? "修改现有标签规则" : "创建新的标签生成规则"}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<Tabs defaultValue="basic" className="w-full">
|
||||||
<div className="space-y-4">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<div>
|
<TabsTrigger value="basic">基本信息</TabsTrigger>
|
||||||
<Label htmlFor="rule-name">规则名称</Label>
|
<TabsTrigger value="conditions">条件设置</TabsTrigger>
|
||||||
<Input
|
<TabsTrigger value="actions">动作设置</TabsTrigger>
|
||||||
id="rule-name"
|
<TabsTrigger value="schedule">执行计划</TabsTrigger>
|
||||||
value={rule.name}
|
</TabsList>
|
||||||
onChange={(e) => setRule({ ...rule, name: e.target.value })}
|
|
||||||
className="mt-1"
|
<TabsContent value="basic" className="space-y-4">
|
||||||
/>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">规则名称</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={rule.name}
|
||||||
|
onChange={(e) => setRule({ ...rule, name: e.target.value })}
|
||||||
|
placeholder="输入规则名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="priority">优先级</Label>
|
||||||
|
<Select
|
||||||
|
value={rule.priority.toString()}
|
||||||
|
onValueChange={(value) => setRule({ ...rule, priority: Number.parseInt(value) })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">高 (1)</SelectItem>
|
||||||
|
<SelectItem value="2">中 (2)</SelectItem>
|
||||||
|
<SelectItem value="3">低 (3)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Label htmlFor="rule-description">规则描述</Label>
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">规则描述</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="rule-description"
|
id="description"
|
||||||
value={rule.description}
|
value={rule.description}
|
||||||
onChange={(e) => setRule({ ...rule, description: e.target.value })}
|
onChange={(e) => setRule({ ...rule, description: e.target.value })}
|
||||||
className="mt-1"
|
placeholder="描述这个规则的用途和逻辑"
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Label htmlFor="target-tag">目标标签</Label>
|
|
||||||
<Input
|
|
||||||
id="target-tag"
|
|
||||||
value={rule.targetTag}
|
|
||||||
onChange={(e) => setRule({ ...rule, targetTag: e.target.value })}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="rule-priority">优先级</Label>
|
|
||||||
<Select
|
|
||||||
value={rule.priority}
|
|
||||||
onValueChange={(value) => setRule({ ...rule, priority: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="mt-1">
|
|
||||||
<SelectValue placeholder="选择优先级" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1">1 - 高</SelectItem>
|
|
||||||
<SelectItem value="2">2 - 中</SelectItem>
|
|
||||||
<SelectItem value="3">3 - 低</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="rule-status">规则状态</Label>
|
|
||||||
<div className="flex items-center space-x-2 mt-1">
|
|
||||||
<Switch
|
|
||||||
id="rule-status"
|
|
||||||
checked={rule.status === "active"}
|
|
||||||
onCheckedChange={(checked) => setRule({ ...rule, status: checked ? "active" : "draft" })}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="rule-status">
|
|
||||||
{rule.status === "active" ? "启用" : "草稿"}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="flex items-center space-x-2">
|
||||||
<Label>规则定义</Label>
|
<Switch
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
id="enabled"
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
checked={rule.enabled}
|
||||||
<TabsTrigger value="visual">可视化编辑</TabsTrigger>
|
onCheckedChange={(checked) => setRule({ ...rule, enabled: checked })}
|
||||||
<TabsTrigger value="sql">SQL 编辑</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="visual" className="space-y-4 mt-4">
|
|
||||||
<Textarea
|
|
||||||
value={rule.condition}
|
|
||||||
onChange={(e) => setRule({ ...rule, condition: e.target.value })}
|
|
||||||
placeholder="例如: 消费金额 > 5000 AND 消费频次 > 10"
|
|
||||||
className="min-h-[200px]"
|
|
||||||
/>
|
/>
|
||||||
<div className="bg-gray-50 p-4 rounded-md">
|
<Label htmlFor="enabled">启用规则</Label>
|
||||||
<h4 className="text-sm font-medium mb-2">可用字段</h4>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
</TabsContent>
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 消费金额" })}>
|
|
||||||
消费金额
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 消费频次" })}>
|
|
||||||
消费频次
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 最近登录时间" })}>
|
|
||||||
最近登录时间
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 活跃度" })}>
|
|
||||||
活跃度
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 注册时间" })}>
|
|
||||||
注册时间
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " 年龄" })}>
|
|
||||||
年龄
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-sm font-medium mt-4 mb-2">操作符</h4>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " >" })}>
|
|
||||||
大于 (>)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " <" })}>
|
|
||||||
小于 (<)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " =" })}>
|
|
||||||
等于 (=)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " !=" })}>
|
|
||||||
不等于 (!=)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " AND" })}>
|
|
||||||
与 (AND)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " OR" })}>
|
|
||||||
或 (OR)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " NOT" })}>
|
|
||||||
非 (NOT)
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setRule({ ...rule, condition: rule.condition + " IN" })}>
|
|
||||||
包含 (IN)
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="sql" className="space-y-4 mt-4">
|
|
||||||
<Textarea
|
|
||||||
value={rule.sql}
|
|
||||||
onChange={(e) => setRule({ ...rule, sql: e.target.value })}
|
|
||||||
placeholder="输入 SQL 查询语句"
|
|
||||||
className="min-h-[200px] font-mono"
|
|
||||||
/>
|
|
||||||
<div className="bg-gray-50 p-4 rounded-md">
|
|
||||||
<h4 className="text-sm font-medium mb-2">SQL 模板</h4>
|
|
||||||
<div className="text-xs font-mono bg-gray-100 p-2 rounded">
|
|
||||||
SELECT user_id FROM user_behavior WHERE total_amount > 5000 AND purchase_count > 10
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
SQL 查询必须返回 user_id 字段,用于标识需要打标签的用户。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{validationResult && (
|
<TabsContent value="conditions" className="space-y-4">
|
||||||
<Alert variant={validationResult.isValid ? "default" : "destructive"}>
|
<div className="flex items-center justify-between">
|
||||||
{validationResult.isValid ? (
|
<h3 className="text-lg font-medium">触发条件</h3>
|
||||||
<Check className="h-4 w-4" />
|
<Button onClick={addCondition} size="sm">
|
||||||
) : (
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
<AlertTriangle className="h-4 w-4" />
|
添加条件
|
||||||
)}
|
</Button>
|
||||||
<AlertTitle>{validationResult.isValid ? "验证通过" : "验证失败"}</AlertTitle>
|
</div>
|
||||||
<AlertDescription>{validationResult.message}</AlertDescription>
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{rule.conditions.map((condition, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-medium">条件 {index + 1}</h4>
|
||||||
|
{rule.conditions.length > 1 && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => removeCondition(index)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>字段</Label>
|
||||||
|
<Select value={condition.field} onValueChange={(value) => updateCondition(index, "field", value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择字段" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{fieldOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>操作符</Label>
|
||||||
|
<Select
|
||||||
|
value={condition.operator}
|
||||||
|
onValueChange={(value) => updateCondition(index, "operator", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择操作符" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{operatorOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>值</Label>
|
||||||
|
<Input
|
||||||
|
value={condition.value}
|
||||||
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
||||||
|
placeholder="输入比较值"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < rule.conditions.length - 1 && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label>逻辑关系:</Label>
|
||||||
|
<Select
|
||||||
|
value={condition.logicalOperator || "AND"}
|
||||||
|
onValueChange={(value) => updateCondition(index, "logicalOperator", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="AND">AND</SelectItem>
|
||||||
|
<SelectItem value="OR">OR</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="actions" className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium">执行动作</h3>
|
||||||
|
<Button onClick={addAction} size="sm">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
添加动作
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{rule.actions.map((action, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-medium">动作 {index + 1}</h4>
|
||||||
|
{rule.actions.length > 1 && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => removeAction(index)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>动作类型</Label>
|
||||||
|
<Select value={action.type} onValueChange={(value) => updateAction(index, "type", value)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="add_tag">添加标签</SelectItem>
|
||||||
|
<SelectItem value="remove_tag">移除标签</SelectItem>
|
||||||
|
<SelectItem value="update_field">更新字段</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>目标</Label>
|
||||||
|
<Input
|
||||||
|
value={action.target}
|
||||||
|
onChange={(e) => updateAction(index, "target", e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
action.type === "add_tag" || action.type === "remove_tag" ? "标签名称" : "字段名称"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>值</Label>
|
||||||
|
<Input
|
||||||
|
value={action.value}
|
||||||
|
onChange={(e) => updateAction(index, "value", e.target.value)}
|
||||||
|
placeholder="输入值"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="schedule" className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">执行计划</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>执行类型</Label>
|
||||||
|
<Select
|
||||||
|
value={rule.schedule?.type || "immediate"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setRule({
|
||||||
|
...rule,
|
||||||
|
schedule: { ...rule.schedule, type: value as any },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="immediate">立即执行</SelectItem>
|
||||||
|
<SelectItem value="scheduled">定时执行</SelectItem>
|
||||||
|
<SelectItem value="recurring">周期执行</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rule.schedule?.type === "scheduled" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>执行时间</Label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={rule.schedule.startTime || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setRule({
|
||||||
|
...rule,
|
||||||
|
schedule: { ...rule.schedule, startTime: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{rule.schedule?.type === "recurring" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>执行间隔</Label>
|
||||||
|
<Select
|
||||||
|
value={rule.schedule.interval || "daily"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setRule({
|
||||||
|
...rule,
|
||||||
|
schedule: { ...rule.schedule, interval: value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="hourly">每小时</SelectItem>
|
||||||
|
<SelectItem value="daily">每天</SelectItem>
|
||||||
|
<SelectItem value="weekly">每周</SelectItem>
|
||||||
|
<SelectItem value="monthly">每月</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Separator className="my-6" />
|
||||||
|
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<Alert className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
{validationErrors.map((error, index) => (
|
||||||
|
<li key={index}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{testResults && (
|
||||||
|
<Alert className="mb-4">
|
||||||
|
<TestTube className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>
|
||||||
|
<strong>匹配用户:</strong> {testResults.matchedUsers}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>预期影响:</strong> {testResults.estimatedImpact}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>执行时间:</strong> {testResults.executionTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button variant="outline" onClick={handleTest} disabled={isLoading}>
|
||||||
|
<TestTube className="h-4 w-4 mr-2" />
|
||||||
|
{isLoading ? "测试中..." : "测试规则"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button variant="outline" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={isLoading}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
保存规则
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline" onClick={handleValidate}>
|
|
||||||
验证规则
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button variant="outline">取消</Button>
|
|
||||||
<Button onClick={handleSave}>保存规则</Button>
|
|
||||||
<Button variant="default" className="bg-green-600 hover:bg-green-700" onClick={handleExecute}>
|
|
||||||
执行规则
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
3
dependencies.ts
Normal file
3
dependencies.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// 强制安装缺失的依赖包
|
||||||
|
import "cmdk"
|
||||||
|
import "@radix-ui/react-separator"
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"@radix-ui/react-radio-group": "latest",
|
"@radix-ui/react-radio-group": "latest",
|
||||||
"@radix-ui/react-scroll-area": "latest",
|
"@radix-ui/react-scroll-area": "latest",
|
||||||
"@radix-ui/react-select": "latest",
|
"@radix-ui/react-select": "latest",
|
||||||
|
"@radix-ui/react-separator": "latest",
|
||||||
"@radix-ui/react-slider": "latest",
|
"@radix-ui/react-slider": "latest",
|
||||||
"@radix-ui/react-slot": "latest",
|
"@radix-ui/react-slot": "latest",
|
||||||
"@radix-ui/react-switch": "latest",
|
"@radix-ui/react-switch": "latest",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"@tanstack/react-virtual": "latest",
|
"@tanstack/react-virtual": "latest",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "latest",
|
||||||
"d3": "latest",
|
"d3": "latest",
|
||||||
"date-fns": "latest",
|
"date-fns": "latest",
|
||||||
"docx": "latest",
|
"docx": "latest",
|
||||||
|
|||||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
|||||||
'@radix-ui/react-select':
|
'@radix-ui/react-select':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 2.2.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
version: 2.2.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
|
'@radix-ui/react-separator':
|
||||||
|
specifier: latest
|
||||||
|
version: 1.1.7(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
'@radix-ui/react-slider':
|
'@radix-ui/react-slider':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 1.3.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
version: 1.3.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
@@ -80,6 +83,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cmdk:
|
||||||
|
specifier: latest
|
||||||
|
version: 1.1.1(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
d3:
|
d3:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 7.9.0
|
version: 7.9.0
|
||||||
@@ -732,6 +738,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-separator@1.1.7':
|
||||||
|
resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slider@1.3.5':
|
'@radix-ui/react-slider@1.3.5':
|
||||||
resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==}
|
resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1352,6 +1371,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
cmdk@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -3770,6 +3795,15 @@ snapshots:
|
|||||||
'@types/react': 18.0.0
|
'@types/react': 18.0.0
|
||||||
'@types/react-dom': 18.0.0
|
'@types/react-dom': 18.0.0
|
||||||
|
|
||||||
|
'@radix-ui/react-separator@1.1.7(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
|
react: 18.0.0
|
||||||
|
react-dom: 18.0.0(react@18.0.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.0.0
|
||||||
|
'@types/react-dom': 18.0.0
|
||||||
|
|
||||||
'@radix-ui/react-slider@1.3.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)':
|
'@radix-ui/react-slider@1.3.5(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.1
|
'@radix-ui/number': 1.1.1
|
||||||
@@ -4373,6 +4407,18 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
cmdk@1.1.1(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0):
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.0.0)(react@18.0.0)
|
||||||
|
'@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@18.0.0)(react@18.0.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)
|
||||||
|
react: 18.0.0
|
||||||
|
react-dom: 18.0.0(react@18.0.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- '@types/react-dom'
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|||||||
Reference in New Issue
Block a user