-
{item.title}
- {item.tag && (
-
{item.tag}
- )}
+
+
{item.icon}
+
+
+ {item.title}
+ {item.tag && (
+
+ {item.tag}
+
+ )}
+
+
{item.description}
{item.expandable && (
)}
@@ -187,21 +201,26 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
{/* 子菜单 */}
{item.children && expandedSections[item.section!] && (
-
+
{item.children.map((subItem) => (
-
{subItem.icon}
-
{subItem.title}
+
+ {subItem.icon}
+
+
+
{subItem.title}
+
{subItem.description}
+
))}
@@ -211,21 +230,21 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
- {/* 底部设置 */}
-
-
-
-
设置
-
+ {/* 底部信息 */}
+
>
diff --git a/app/user-portrait/page.tsx b/app/user-portrait/page.tsx
index 735ad64..71f1b6e 100644
--- a/app/user-portrait/page.tsx
+++ b/app/user-portrait/page.tsx
@@ -38,48 +38,48 @@ export default function UserPortrait() {
userList: true,
})
- // 使用真实的用户数据
+ // 使用通用公司名称和当天时间线的用户数据
const mockUsers = [
{
id: "1",
- name: "李明华",
- company: "阿里巴巴集团",
+ name: "张明华",
+ company: "科技创新有限公司",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术决策者", "活跃用户", "iOS用户", "企业级客户"],
category: "企业服务",
pool: "私域池",
- lastActive: "2024-01-18",
+ lastActive: "今天 14:30",
value: 28500,
rfmScore: 95,
wechatAccounts: [
- { id: "wx1", name: "李明华", avatar: "/placeholder.svg?height=40&width=40", tags: ["主要账号", "活跃"] },
- { id: "wx2", 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: "2",
- name: "张雨婷",
- company: "腾讯科技",
+ name: "李雨婷",
+ company: "数字营销公司",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中高价值", "产品经理", "内容创作者", "Android用户"],
category: "内容营销",
pool: "公域池",
- lastActive: "2024-01-17",
+ lastActive: "今天 13:45",
value: 15800,
rfmScore: 78,
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",
name: "王建国",
- company: "字节跳动",
+ company: "软件开发工作室",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "技术专家", "开源贡献者", "MacOS用户"],
category: "技术社区",
pool: "私域池",
- lastActive: "2024-01-16",
+ lastActive: "今天 12:20",
value: 32400,
rfmScore: 89,
wechatAccounts: [
@@ -89,12 +89,12 @@ export default function UserPortrait() {
{
id: "4",
name: "刘思琪",
- company: "美团点评",
+ company: "数据分析咨询",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中价值", "运营专家", "数据分析师", "Windows用户"],
category: "数据分析",
pool: "私域池",
- lastActive: "2024-01-15",
+ lastActive: "今天 11:15",
value: 18900,
rfmScore: 72,
wechatAccounts: [
@@ -104,12 +104,12 @@ export default function UserPortrait() {
{
id: "5",
name: "陈浩然",
- company: "京东集团",
+ company: "云计算服务商",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "架构师", "技术领导", "Linux用户"],
category: "技术架构",
pool: "私域池",
- lastActive: "2024-01-14",
+ lastActive: "今天 10:30",
value: 45600,
rfmScore: 93,
wechatAccounts: [
@@ -120,12 +120,12 @@ export default function UserPortrait() {
{
id: "6",
name: "周欣妍",
- company: "滴滴出行",
+ company: "用户体验设计",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中价值", "UI设计师", "用户体验", "iOS用户"],
category: "设计创意",
pool: "公域池",
- lastActive: "2024-01-13",
+ lastActive: "今天 09:45",
value: 12300,
rfmScore: 65,
wechatAccounts: [
@@ -135,12 +135,12 @@ export default function UserPortrait() {
{
id: "7",
name: "马志强",
- company: "华为技术",
+ company: "企业解决方案",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["高价值", "解决方案专家", "B2B销售", "Android用户"],
category: "企业解决方案",
pool: "私域池",
- lastActive: "2024-01-12",
+ lastActive: "今天 08:20",
value: 38700,
rfmScore: 87,
wechatAccounts: [
@@ -150,12 +150,12 @@ export default function UserPortrait() {
{
id: "8",
name: "赵丽娟",
- company: "小米科技",
+ company: "品牌营销策划",
avatar: "/placeholder.svg?height=40&width=40",
tags: ["中高价值", "市场营销", "品牌推广", "iOS用户"],
category: "品牌营销",
pool: "公域池",
- lastActive: "2024-01-11",
+ lastActive: "今天 07:30",
value: 21500,
rfmScore: 76,
wechatAccounts: [
@@ -167,17 +167,17 @@ export default function UserPortrait() {
const [users, setUsers] = useState(mockUsers)
- // 用户词统计
- const userKeywordStats = {
+ // 流量关键词统计
+ const keywordStats = {
totalKeywords: 156,
activeKeywords: 124,
coverage: 89.4,
topKeywords: [
- { keyword: "技术", count: 45, trend: "+12%" },
- { keyword: "产品", count: 38, trend: "+8%" },
- { keyword: "运营", count: 32, trend: "+15%" },
- { keyword: "设计", count: 28, trend: "+6%" },
- { keyword: "市场", count: 25, trend: "+10%" },
+ { keyword: "技术解决方案", count: 45, trend: "+12%", value: "高" },
+ { keyword: "数据分析", count: 38, trend: "+8%", value: "中高" },
+ { keyword: "用户体验", count: 32, trend: "+15%", value: "中" },
+ { keyword: "品牌营销", count: 28, trend: "+6%", value: "中高" },
+ { 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 (
-
+
-
用户画像
-
全面的用户数据分析和管理
+
用户画像
+
数据驱动的用户分析平台
-
- {/* 用户词管理 */}
+ {/* 流量关键词管理 */}
setExpandedSections((prev) => ({ ...prev, userKeywords: open }))}
>
-
+
-
+
- 用户词管理
- 用户关键词分析和管理
+ 流量关键词
+ 关键词价值评估与分类
{expandedSections.userKeywords ? (
-
+
) : (
-
+
)}
-
-
-
-
总关键词
-
{userKeywordStats.totalKeywords}
+
+
+
+
总关键词
+
{keywordStats.totalKeywords}
-
-
活跃关键词
-
{userKeywordStats.activeKeywords}
+
+
活跃关键词
+
{keywordStats.activeKeywords}
-
-
覆盖率
-
{userKeywordStats.coverage}%
+
+
覆盖率
+
{keywordStats.coverage}%
-
+
router.push("/user-portrait/user-keywords")}
+ size="sm"
+ className="w-full h-full bg-transparent text-xs md:text-sm"
+ onClick={() => router.push("/user-portrait/keywords")}
>
-
- 管理关键词
+
+ 管理
-
- {userKeywordStats.topKeywords.map((item, index) => (
-
-
{item.keyword}
-
-
{item.count}
+
+ {keywordStats.topKeywords.map((item, index) => (
+
+
+ {item.keyword}
+ {item.value}
+
+
+
{item.count}
{item.trend}
@@ -329,54 +346,55 @@ export default function UserPortrait() {
>
-
+
-
+
- 标签管理
- 用户标签分类和管理
+ 标签管理
+ 用户标签分类与管理
{expandedSections.tagManagement ? (
-
+
) : (
-
+
)}
-
-
-
-
总标签数
-
{tagStats.totalTags}
+
+
+
+
总标签数
+
{tagStats.totalTags}
-
-
活跃标签
-
{tagStats.activeTags}
+
+
活跃标签
+
{tagStats.activeTags}
-
-
覆盖率
-
{tagStats.coverage}%
+
+
覆盖率
+
{tagStats.coverage}%
-
+
router.push("/user-portrait/tags")}
>
-
- 管理标签
+
+ 管理
{tagStats.topTags.map((item, index) => (
- {item.tag}
- {item.count}
+ {item.tag}
+ {item.count}
))}
@@ -392,35 +410,41 @@ export default function UserPortrait() {
>
-
+
-
+
- 用户列表
- 查看和管理所有用户 ({filteredUsers.length})
+ 用户列表
+
+ 用户数据管理 ({filteredUsers.length})
+
- {expandedSections.userList ?
:
}
+ {expandedSections.userList ? (
+
+ ) : (
+
+ )}
-
-
+
+
-
+
+ {/* 移动端卡片布局 */}
+ {filteredUsers.map((user) => (
+
+
+
+
+
+
+
+
{user.name}
+
{user.company}
+
+
+
= 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}
+
+
+
+ {user.lastActive}
+ ¥{user.value.toLocaleString()}
+
+
+ {user.tags.slice(0, 3).map((tag, index) => (
+
+ {tag}
+
+ ))}
+ {user.tags.length > 3 && (
+
+ +{user.tags.length - 3}
+
+ )}
+
+
+ handleWechatClick(user.id)} className="text-xs">
+
+ {user.wechatAccounts.length}个账号
+
+ handleUserClick(user.id)} className="text-xs">
+
+ 查看
+
+
+
+ ))}
+
+
+ {/* 桌面端表格布局 */}
+
diff --git a/app/user-value/page.tsx b/app/user-value/page.tsx
index c9ac019..36c98a3 100644
--- a/app/user-value/page.tsx
+++ b/app/user-value/page.tsx
@@ -27,12 +27,12 @@ import {
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
-// 真实用户数据
+// 使用通用公司名称和当天时间线的用户数据
const mockUsers = [
{
id: "1",
- name: "李明华",
- company: "阿里巴巴集团",
+ name: "张明华",
+ company: "科技创新有限公司",
recency: 1,
frequency: 28,
monetary: 45600,
@@ -42,11 +42,12 @@ const mockUsers = [
upgradeFromDays: 45,
nextUpgrade: "VIP客户",
upgradeProbability: 85,
+ lastActive: "今天 14:30",
},
{
id: "2",
- name: "张雨婷",
- company: "腾讯科技",
+ name: "李雨婷",
+ company: "数字营销公司",
recency: 3,
frequency: 22,
monetary: 32800,
@@ -56,11 +57,12 @@ const mockUsers = [
upgradeFromDays: 28,
nextUpgrade: "白金会员",
upgradeProbability: 78,
+ lastActive: "今天 13:45",
},
{
id: "3",
name: "王建国",
- company: "字节跳动",
+ company: "软件开发工作室",
recency: 2,
frequency: 25,
monetary: 38900,
@@ -70,11 +72,12 @@ const mockUsers = [
upgradeFromDays: 38,
nextUpgrade: "企业VIP",
upgradeProbability: 82,
+ lastActive: "今天 12:20",
},
{
id: "4",
name: "刘思琪",
- company: "美团点评",
+ company: "数据分析咨询",
recency: 7,
frequency: 18,
monetary: 25400,
@@ -84,11 +87,12 @@ const mockUsers = [
upgradeFromDays: 12,
nextUpgrade: "高价值用户",
upgradeProbability: 68,
+ lastActive: "今天 11:15",
},
{
id: "5",
name: "陈浩然",
- company: "京东集团",
+ company: "云计算服务商",
recency: 4,
frequency: 20,
monetary: 28700,
@@ -98,11 +102,12 @@ const mockUsers = [
upgradeFromDays: 22,
nextUpgrade: "高价值用户",
upgradeProbability: 72,
+ lastActive: "今天 10:30",
},
{
id: "6",
name: "周欣妍",
- company: "滴滴出行",
+ company: "用户体验设计",
recency: 12,
frequency: 15,
monetary: 18600,
@@ -112,11 +117,12 @@ const mockUsers = [
upgradeFromDays: 8,
nextUpgrade: "中高价值用户",
upgradeProbability: 55,
+ lastActive: "今天 09:45",
},
{
id: "7",
name: "马志强",
- company: "华为技术",
+ company: "企业解决方案",
recency: 6,
frequency: 16,
monetary: 22100,
@@ -126,11 +132,12 @@ const mockUsers = [
upgradeFromDays: 15,
nextUpgrade: "中高价值用户",
upgradeProbability: 61,
+ lastActive: "今天 08:20",
},
{
id: "8",
name: "赵丽娟",
- company: "小米科技",
+ company: "品牌营销策划",
recency: 15,
frequency: 12,
monetary: 15800,
@@ -140,6 +147,7 @@ const mockUsers = [
upgradeFromDays: 5,
nextUpgrade: "中高价值用户",
upgradeProbability: 45,
+ lastActive: "今天 07:30",
},
]
@@ -186,33 +194,33 @@ export default function UserValuePage() {
}
return (
-
+
{/* 页面标题和工具栏 */}
-
用户估值
-
用户价值评估与升级路径分析
+
价值评估
+
用户价值分析与升级路径
-
+
-
+
-
+
- 导出报告
+ 导出
@@ -224,118 +232,122 @@ export default function UserValuePage() {
>
-
+
-
+
- 用户估值概览
- 用户价值评估整体情况
+ 价值概览
+ 用户价值评估整体情况
- {expandedSections.overview ?
:
}
+ {expandedSections.overview ? (
+
+ ) : (
+
+ )}
-
+
-
+
-
平均用户估值
-
¥3,248
-
-
- 较上月增长 8.2%
+
平均估值
+
¥3,248
+
-
-
+
-
+
-
高价值用户
-
45,678
-
-
- 较上月增长 12.3%
+
高价值用户
+
45,678
+
-
-
+
-
+
-
用户价值增长率
-
15.8%
-
-
- 较上月提升 2.1%
+
价值增长率
+
15.8%
+
-
-
+
-
+
-
用户升级率
-
12.5%
-
-
- 较上月提升 1.8%
+
升级率
+
12.5%
+
-
-
+
@@ -352,13 +364,13 @@ export default function UserValuePage() {
>
-
+
-
+
-
- RFM用户价值模型
+
+ RFM估值模型
-
+
- 基于用户行为的价值评估模型
+ 基于用户行为的价值评估
- {expandedSections.model ?
:
}
+ {expandedSections.model ? (
+
+ ) : (
+
+ )}
-
+
-
-
+
+
R
- Recency (最近消费时间)
+ 最近消费时间
- 用户最近一次交易的时间间隔,时间越短价值越高。
-
-
-
+
时间间隔越短价值越高
+
+
+
1-3天
5分
-
+
4-7天
4分
-
+
8-14天
3分
-
+
15-30天
2分
@@ -416,28 +432,28 @@ export default function UserValuePage() {
-
-
+
+
F
- Frequency (消费频率)
+ 消费频率
- 用户在一段时间内的交易次数,频率越高价值越高。
-
-
-
+
频率越高价值越高
+
+
+
25次以上/月
5分
-
+
20-24次/月
4分
-
+
15-19次/月
3分
-
+
10-14次/月
2分
@@ -450,28 +466,28 @@ export default function UserValuePage() {
-
-
+
+
M
- Monetary (消费金额)
+ 消费金额
- 用户在一段时间内的消费金额,金额越高价值越高。
-
-
-
+
金额越高价值越高
+
+
+
30000元以上
5分
-
+
20000-29999元
4分
-
+
10000-19999元
3分
-
+
5000-9999元
2分
@@ -484,28 +500,28 @@ export default function UserValuePage() {
-
-
RFM评分等级
-
-
-
高价值用户
-
85-100分
+
+
RFM评分等级
+
+
-
-
中高价值用户
-
70-84分
+
-
-
中等价值用户
-
55-69分
+
-
@@ -521,35 +537,75 @@ export default function UserValuePage() {
>
-
+
-
+
- 用户价值分析
- 用户价值分布与详细数据 ({filteredUsers.length})
+ 价值分析
+
+ 用户价值详细数据 ({filteredUsers.length})
+
- {expandedSections.analysis ?
:
}
+ {expandedSections.analysis ? (
+
+ ) : (
+
+ )}
-
-
+ {/* 移动端卡片布局 */}
+
+ {filteredUsers.map((user) => (
+
+
+
+
{user.name}
+
{user.company}
+
+
{user.rfmScore}
+
+
+
+
+
F值
+
{user.frequency}次
+
+
+
M值
+
¥{(user.monetary / 1000).toFixed(0)}K
+
+
+
+ {user.lastActive}
+ {user.growth}
+
+
+ ))}
+
+
+ {/* 桌面端表格布局 */}
+
@@ -593,13 +649,13 @@ export default function UserValuePage() {
>
-
+
-
+
-
- 用户升级路径分析
+
+ 升级路径分析
-
+
- 用户价值提升路径与策略
+ 用户价值提升路径与策略
{expandedSections.upgradePaths ? (
-
+
) : (
-
+
)}
-
+
-
-
- 升级潜力分析
+
+
+ 升级潜力
-
-
+
+
- 高潜力用户
- 2,345
+ 高潜力用户
+ 2,345
- 中潜力用户
- 5,678
+ 中潜力用户
+ 5,678
- 低潜力用户
- 1,234
+ 低潜力用户
+ 1,234
@@ -653,24 +709,24 @@ export default function UserValuePage() {
-
-
+
+
升级成功率
-
-
+
+
- 本月升级率
- 12.5%
+ 本月升级率
+ 12.5%
- 平均升级周期
- 28天
+ 平均升级周期
+ 28天
- 预期升级用户
- 1,856
+ 预期升级用户
+ 1,856
@@ -678,54 +734,54 @@ export default function UserValuePage() {
-
-
- 升级价值贡献
+
+
+ 价值贡献
-
-
+
+
- 升级价值增长
- ¥2.3M
+ 升级价值增长
+ ¥2.3M
- 平均用户价值提升
- ¥1,245
+ 平均价值提升
+ ¥1,245
- ROI提升
- 35.8%
+ ROI提升
+ 35.8%
-
-
个人升级路径推荐
-
+
+
个人升级路径推荐
+
{filteredUsers.slice(0, 5).map((user) => (
-
+
-
{user.name}
-
{user.company}
+
{user.name}
+
{user.company}
-
{user.level}
+
{user.level}
-
-
-
下一级别: {user.nextUpgrade}
-
升级概率: {user.upgradeProbability}%
+
+
+
下一级别: {user.nextUpgrade}
+
升级概率: {user.upgradeProbability}%
-
diff --git a/components/tag-rules/rule-editor.tsx b/components/tag-rules/rule-editor.tsx
index f7e31a4..f8dcc6f 100644
--- a/components/tag-rules/rule-editor.tsx
+++ b/components/tag-rules/rule-editor.tsx
@@ -1,7 +1,7 @@
"use client"
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 { Input } from "@/components/ui/input"
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 { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
-import { AlertTriangle, Check } from "lucide-react"
+import { Separator } from "@/components/ui/separator"
+import { AlertCircle, Plus, X, Save, TestTube } from "lucide-react"
+import { Alert, AlertDescription } from "@/components/ui/alert"
-interface RuleEditorProps {
- ruleId: string | null
+interface TagRule {
+ id?: string
+ name: string
+ description: string
+ conditions: RuleCondition[]
+ actions: RuleAction[]
+ priority: number
+ enabled: boolean
+ schedule?: RuleSchedule
}
-export function RuleEditor({ ruleId }: RuleEditorProps) {
- const [rule, setRule] = useState({
+interface RuleCondition {
+ 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
({
name: "",
description: "",
- targetTag: "",
- condition: "",
- priority: "2",
- status: "draft",
- sql: "",
+ conditions: [{ field: "", operator: "", value: "" }],
+ actions: [{ type: "add_tag", target: "", value: "" }],
+ priority: 1,
+ enabled: true,
})
- const [activeTab, setActiveTab] = useState("visual")
- const [validationResult, setValidationResult] = useState<{
- isValid: boolean
- message: string
- } | null>(null)
+
+ const [isLoading, setIsLoading] = useState(false)
+ const [testResults, setTestResults] = useState(null)
+ const [validationErrors, setValidationErrors] = useState([])
+
+ 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(() => {
if (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)
+ loadRule(ruleId)
}
}, [ruleId])
- const handleValidate = () => {
- // 模拟验证逻辑
- if (rule.condition.trim() === "") {
- setValidationResult({
- isValid: false,
- message: "规则条件不能为空",
+ const loadRule = async (id: string) => {
+ setIsLoading(true)
+ try {
+ // 模拟API调用
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ // 这里应该从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() === "") {
- setValidationResult({
- isValid: false,
- message: "SQL 语句不能为空",
- })
- return
+ if (rule.conditions.length === 0) {
+ errors.push("至少需要一个条件")
}
- // 模拟验证成功
- setValidationResult({
- isValid: true,
- message: "规则验证通过,可以保存或执行",
+ rule.conditions.forEach((condition, index) => {
+ if (!condition.field || !condition.operator || !condition.value) {
+ errors.push(`条件 ${index + 1} 不完整`)
+ }
})
+
+ 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 = () => {
- // 这里应该是保存规则的逻辑
- console.log("保存规则:", rule)
- // 模拟保存成功
- alert("规则保存成功")
+ const errors = validateRule()
+ setValidationErrors(errors)
+
+ if (errors.length === 0) {
+ onSave(rule)
+ }
}
- const handleExecute = () => {
- // 这里应该是执行规则的逻辑
- console.log("执行规则:", rule)
- // 模拟执行成功
- alert("规则执行成功,影响用户 1250 人")
+ const handleTest = async () => {
+ setIsLoading(true)
+ try {
+ // 模拟测试规则
+ 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 (
+
+
+
+
+
+ )
}
return (
{ruleId ? "编辑规则" : "创建新规则"}
-
- {ruleId ? "修改现有标签规则" : "创建新的标签生成规则"}
-
+ {ruleId ? "修改现有标签规则的配置" : "创建新的自动化标签规则"}
-
-
-
-
-
-
setRule({ ...rule, name: e.target.value })}
- className="mt-1"
- />
+
+
+
+ 基本信息
+ 条件设置
+ 动作设置
+ 执行计划
+
+
+
+
+
+
+ setRule({ ...rule, name: e.target.value })}
+ placeholder="输入规则名称"
+ />
+
+
+
+
+
-
-
+
+
+
-
-
- setRule({ ...rule, targetTag: e.target.value })}
- className="mt-1"
- />
-
-
-
-
-
-
-
-
-
-
- setRule({ ...rule, status: checked ? "active" : "draft" })}
- />
-
-
-
-
-
-
-
-
-
- 可视化编辑
- SQL 编辑
-
-
-
+
- {validationResult && (
-
- {validationResult.isValid ? (
-
- ) : (
-
- )}
- {validationResult.isValid ? "验证通过" : "验证失败"}
- {validationResult.message}
+
+
+
+
+ {rule.conditions.map((condition, index) => (
+
+
+
条件 {index + 1}
+ {rule.conditions.length > 1 && (
+ removeCondition(index)}>
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateCondition(index, "value", e.target.value)}
+ placeholder="输入比较值"
+ />
+
+
+
+ {index < rule.conditions.length - 1 && (
+
+
+
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ {rule.actions.map((action, index) => (
+
+
+
动作 {index + 1}
+ {rule.actions.length > 1 && (
+ removeAction(index)}>
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ updateAction(index, "target", e.target.value)}
+ placeholder={
+ action.type === "add_tag" || action.type === "remove_tag" ? "标签名称" : "字段名称"
+ }
+ />
+
+
+
+
+ updateAction(index, "value", e.target.value)}
+ placeholder="输入值"
+ />
+
+
+
+ ))}
+
+
+
+
+ 执行计划
+
+
+
+
+
+
+
+ {rule.schedule?.type === "scheduled" && (
+
+
+
+ setRule({
+ ...rule,
+ schedule: { ...rule.schedule, startTime: e.target.value },
+ })
+ }
+ />
+
+ )}
+
+ {rule.schedule?.type === "recurring" && (
+
+
+
+
+ )}
+
+
+
+
+
+
+ {validationErrors.length > 0 && (
+
+
+
+
+ {validationErrors.map((error, index) => (
+ - {error}
+ ))}
+
+
)}
+
+ {testResults && (
+
+
+
+
+
+ 匹配用户: {testResults.matchedUsers}
+
+
+ 预期影响: {testResults.estimatedImpact}
+
+
+ 执行时间: {testResults.executionTime}
+
+
+
+
+ )}
+
+
+
+
+
+ {isLoading ? "测试中..." : "测试规则"}
+
+
+
+
+
+ 取消
+
+
+
+ 保存规则
+
+
+
-
-
-
- 验证规则
-
-
-
- 取消
- 保存规则
-
- 执行规则
-
-
-
)
}
diff --git a/dependencies.ts b/dependencies.ts
new file mode 100644
index 0000000..48cd41e
--- /dev/null
+++ b/dependencies.ts
@@ -0,0 +1,3 @@
+// 强制安装缺失的依赖包
+import "cmdk"
+import "@radix-ui/react-separator"
diff --git a/package.json b/package.json
index 393f5f6..21cde87 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-radio-group": "latest",
"@radix-ui/react-scroll-area": "latest",
"@radix-ui/react-select": "latest",
+ "@radix-ui/react-separator": "latest",
"@radix-ui/react-slider": "latest",
"@radix-ui/react-slot": "latest",
"@radix-ui/react-switch": "latest",
@@ -33,6 +34,7 @@
"@tanstack/react-virtual": "latest",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "latest",
"d3": "latest",
"date-fns": "latest",
"docx": "latest",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e71931a..1d5ddcc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
'@radix-ui/react-select':
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)
+ '@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':
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)
@@ -80,6 +83,9 @@ importers:
clsx:
specifier: ^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:
specifier: latest
version: 7.9.0
@@ -732,6 +738,19 @@ packages:
'@types/react-dom':
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':
resolution: {integrity: sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==}
peerDependencies:
@@ -1352,6 +1371,12 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
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:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -3770,6 +3795,15 @@ snapshots:
'@types/react': 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)':
dependencies:
'@radix-ui/number': 1.1.1
@@ -4373,6 +4407,18 @@ snapshots:
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:
dependencies:
color-name: 1.1.4