存客宝 React
This commit is contained in:
464
Cunkebao/app/workspace/ai-analyzer/[id]/page.tsx
Normal file
464
Cunkebao/app/workspace/ai-analyzer/[id]/page.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Download, Mail, Share2, Users, MessageCircle, BarChart2, PieChart } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
interface AnalysisReport {
|
||||
id: string
|
||||
name: string
|
||||
wechatId: string
|
||||
deviceName: string
|
||||
createdAt: string
|
||||
completedAt: string
|
||||
keywords: string[]
|
||||
promptWords: string[]
|
||||
type: "friends" | "moments" | "both"
|
||||
summary: string
|
||||
friendsAnalysis: {
|
||||
totalFriends: number
|
||||
matchedFriends: number
|
||||
categories: {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
}[]
|
||||
demographics: {
|
||||
gender: { male: number; female: number; unknown: number }
|
||||
ageGroups: { [key: string]: number }
|
||||
regions: { [key: string]: number }
|
||||
}
|
||||
}
|
||||
momentsAnalysis: {
|
||||
totalPosts: number
|
||||
analyzedPosts: number
|
||||
topTopics: { topic: string; count: number }[]
|
||||
sentiment: { positive: number; neutral: number; negative: number }
|
||||
activityTrend: { month: string; count: number }[]
|
||||
}
|
||||
}
|
||||
|
||||
export default function AnalysisReportPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const [report, setReport] = useState<AnalysisReport | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载报告数据
|
||||
const fetchReport = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
const mockReport: AnalysisReport = {
|
||||
id: params.id,
|
||||
name: "美妆用户分析",
|
||||
wechatId: "wxid_abc123",
|
||||
deviceName: "设备1",
|
||||
createdAt: "2023-12-15T10:30:00Z",
|
||||
completedAt: "2023-12-15T11:45:00Z",
|
||||
keywords: ["美妆", "护肤", "彩妆"],
|
||||
promptWords: ["人群属性", "喜好分析", "消费能力"],
|
||||
type: "both",
|
||||
summary:
|
||||
"该微信账号的好友中有约32%与美妆相关,主要集中在25-34岁的女性用户,地域分布以一线城市为主。朋友圈内容分析显示,美妆相关话题互动率较高,用户对高端护肤品牌表现出较强的兴趣,消费能力中上。建议针对这部分用户推送高端护肤品和彩妆新品信息,重点关注节假日促销活动的反馈。",
|
||||
friendsAnalysis: {
|
||||
totalFriends: 287,
|
||||
matchedFriends: 92,
|
||||
categories: [
|
||||
{ name: "美妆爱好者", count: 48, percentage: 52 },
|
||||
{ name: "偶尔关注", count: 31, percentage: 34 },
|
||||
{ name: "专业人士", count: 13, percentage: 14 },
|
||||
],
|
||||
demographics: {
|
||||
gender: { male: 12, female: 76, unknown: 4 },
|
||||
ageGroups: { "18-24": 15, "25-34": 53, "35-44": 19, "45+": 5 },
|
||||
regions: { 北京: 21, 上海: 18, 广州: 14, 深圳: 12, 其他: 27 },
|
||||
},
|
||||
},
|
||||
momentsAnalysis: {
|
||||
totalPosts: 1240,
|
||||
analyzedPosts: 850,
|
||||
topTopics: [
|
||||
{ topic: "护肤品", count: 127 },
|
||||
{ topic: "彩妆", count: 98 },
|
||||
{ topic: "美容仪器", count: 76 },
|
||||
{ topic: "品牌活动", count: 65 },
|
||||
{ topic: "新品上市", count: 52 },
|
||||
],
|
||||
sentiment: { positive: 68, neutral: 27, negative: 5 },
|
||||
activityTrend: [
|
||||
{ month: "7月", count: 78 },
|
||||
{ month: "8月", count: 92 },
|
||||
{ month: "9月", count: 85 },
|
||||
{ month: "10月", count: 110 },
|
||||
{ month: "11月", count: 125 },
|
||||
{ month: "12月", count: 142 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
setReport(mockReport)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchReport()
|
||||
}, [params.id])
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "friends":
|
||||
return "好友信息分析"
|
||||
case "moments":
|
||||
return "朋友圈内容分析"
|
||||
case "both":
|
||||
return "综合分析"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen max-h-screen bg-gray-50">
|
||||
{/* 头部 */}
|
||||
<div className="bg-white p-4 border-b flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">分析报告</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">发送报告</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Download className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">下载报告</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Share2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">分享</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : report ? (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* 报告头部 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2">
|
||||
<div>
|
||||
<CardTitle className="text-xl">{report.name}</CardTitle>
|
||||
<p className="text-sm text-gray-500 mt-1">{new Date(report.completedAt).toLocaleString()} 完成</p>
|
||||
</div>
|
||||
<Badge className="self-start sm:self-auto bg-green-100 text-green-800 border-green-200">
|
||||
{getTypeLabel(report.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<Users className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">分析好友</p>
|
||||
<p className="font-semibold">
|
||||
{report.friendsAnalysis.matchedFriends} / {report.friendsAnalysis.totalFriends}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
|
||||
<MessageCircle className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">分析朋友圈</p>
|
||||
<p className="font-semibold">
|
||||
{report.momentsAnalysis.analyzedPosts} / {report.momentsAnalysis.totalPosts}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<BarChart2 className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">关键词</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{report.keywords.map((keyword, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-medium mb-2">分析摘要</h3>
|
||||
<p className="text-sm text-gray-700">{report.summary}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 详细分析 */}
|
||||
<Tabs defaultValue="friends" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="friends">好友分析</TabsTrigger>
|
||||
<TabsTrigger value="moments">朋友圈分析</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="friends" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">好友分类分析</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">好友分类</h3>
|
||||
<div className="space-y-3">
|
||||
{report.friendsAnalysis.categories.map((category, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{category.name}</span>
|
||||
<span className="font-medium">
|
||||
{category.count} ({category.percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${category.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">性别分布</h3>
|
||||
<div className="flex items-center h-40 justify-center">
|
||||
<PieChart className="h-32 w-32 text-gray-400" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 text-center mt-2">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">女性</div>
|
||||
<div className="font-medium">
|
||||
{report.friendsAnalysis.demographics.gender.female} (
|
||||
{Math.round(
|
||||
(report.friendsAnalysis.demographics.gender.female /
|
||||
report.friendsAnalysis.matchedFriends) *
|
||||
100,
|
||||
)}
|
||||
%)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">男性</div>
|
||||
<div className="font-medium">
|
||||
{report.friendsAnalysis.demographics.gender.male} (
|
||||
{Math.round(
|
||||
(report.friendsAnalysis.demographics.gender.male /
|
||||
report.friendsAnalysis.matchedFriends) *
|
||||
100,
|
||||
)}
|
||||
%)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">未知</div>
|
||||
<div className="font-medium">
|
||||
{report.friendsAnalysis.demographics.gender.unknown} (
|
||||
{Math.round(
|
||||
(report.friendsAnalysis.demographics.gender.unknown /
|
||||
report.friendsAnalysis.matchedFriends) *
|
||||
100,
|
||||
)}
|
||||
%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">年龄分布</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(report.friendsAnalysis.demographics.ageGroups).map(([age, count], index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{age}岁</span>
|
||||
<span className="font-medium">
|
||||
{count} ({Math.round((count / report.friendsAnalysis.matchedFriends) * 100)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full"
|
||||
style={{
|
||||
width: `${Math.round((count / report.friendsAnalysis.matchedFriends) * 100)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">地域分布</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(report.friendsAnalysis.demographics.regions).map(([region, count], index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{region}</span>
|
||||
<span className="font-medium">
|
||||
{count} ({Math.round((count / report.friendsAnalysis.matchedFriends) * 100)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-500 h-2 rounded-full"
|
||||
style={{
|
||||
width: `${Math.round((count / report.friendsAnalysis.matchedFriends) * 100)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="moments" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">朋友圈内容分析</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">热门话题</h3>
|
||||
<div className="space-y-3">
|
||||
{report.momentsAnalysis.topTopics.map((topic, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{topic.topic}</span>
|
||||
<span className="font-medium">{topic.count} 次提及</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{
|
||||
width: `${Math.round((topic.count / report.momentsAnalysis.topTopics[0].count) * 100)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">情感分析</h3>
|
||||
<div className="flex items-center h-40 justify-center">
|
||||
<PieChart className="h-32 w-32 text-gray-400" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 text-center mt-2">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">积极</div>
|
||||
<div className="font-medium">{report.momentsAnalysis.sentiment.positive}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">中性</div>
|
||||
<div className="font-medium">{report.momentsAnalysis.sentiment.neutral}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">消极</div>
|
||||
<div className="font-medium">{report.momentsAnalysis.sentiment.negative}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="font-medium mb-3">活跃度趋势</h3>
|
||||
<div className="h-60 flex items-end justify-between">
|
||||
{report.momentsAnalysis.activityTrend.map((item, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div
|
||||
className="w-12 bg-blue-500 rounded-t-md"
|
||||
style={{
|
||||
height: `${Math.round((item.count / Math.max(...report.momentsAnalysis.activityTrend.map((i) => i.count))) * 180)}px`,
|
||||
}}
|
||||
></div>
|
||||
<div className="text-xs mt-2">{item.month}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">AI 洞察</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<h4 className="font-medium text-blue-800 mb-2">用户兴趣洞察</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
根据朋友圈内容分析,该用户对高端护肤品牌表现出较强的兴趣,尤其关注新品上市和限时促销活动。用户经常分享美容心得和产品使用体验,互动率较高的内容多与护肤品和彩妆相关。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 rounded-lg border border-green-100">
|
||||
<h4 className="font-medium text-green-800 mb-2">消费能力分析</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
用户消费能力属于中上水平,对高端美妆品牌有明显偏好。朋友圈中提及的产品价格区间多在300-1000元,节假日期间有较高的购买意愿和分享欲望。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-purple-50 rounded-lg border border-purple-100">
|
||||
<h4 className="font-medium text-purple-800 mb-2">社交特征分析</h4>
|
||||
<p className="text-sm text-purple-700">
|
||||
用户社交活跃度高,朋友圈更新频率平均为每周3-4次。内容多以分享生活、美妆心得为主,互动性强,对评论和点赞有较高的回应率。用户倾向于在晚间19:00-22:00发布内容,周末活跃度高于工作日。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">未找到报告数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { X, Plus } from "lucide-react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
interface AnalysisSettingsProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function AnalysisSettings({ formData, updateFormData, onNext, onBack }: AnalysisSettingsProps) {
|
||||
const [analysisType, setAnalysisType] = useState<string>(formData.analysisType || "both")
|
||||
const [keyword, setKeyword] = useState("")
|
||||
const [promptWord, setPromptWord] = useState("")
|
||||
const [keywords, setKeywords] = useState<string[]>(formData.keywords || [])
|
||||
const [promptWords, setPromptWords] = useState<string[]>(formData.promptWords || [])
|
||||
const [activeTab, setActiveTab] = useState("friends")
|
||||
|
||||
const handleAddKeyword = () => {
|
||||
if (keyword.trim() && !keywords.includes(keyword.trim())) {
|
||||
const newKeywords = [...keywords, keyword.trim()]
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
setKeyword("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveKeyword = (index: number) => {
|
||||
const newKeywords = keywords.filter((_, i) => i !== index)
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
}
|
||||
|
||||
const handleAddPromptWord = () => {
|
||||
if (promptWord.trim() && !promptWords.includes(promptWord.trim())) {
|
||||
const newPromptWords = [...promptWords, promptWord.trim()]
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
setPromptWord("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemovePromptWord = (index: number) => {
|
||||
const newPromptWords = promptWords.filter((_, i) => i !== index)
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
}
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
setAnalysisType(value)
|
||||
updateFormData({ analysisType: value })
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, type: "keyword" | "prompt") => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
if (type === "keyword") {
|
||||
handleAddKeyword()
|
||||
} else {
|
||||
handleAddPromptWord()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
updateFormData({
|
||||
analysisType,
|
||||
keywords,
|
||||
promptWords,
|
||||
})
|
||||
onNext()
|
||||
}
|
||||
|
||||
const suggestedKeywords = ["美妆", "护肤", "彩妆", "健身", "教育", "培训", "金融", "投资", "旅游", "美食"]
|
||||
const suggestedPrompts = ["人群属性", "喜好分析", "消费能力", "兴趣爱好", "活跃度", "社交特征"]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">设置分析内容</h2>
|
||||
<p className="text-gray-500 text-sm">选择分析类型并设置关键词和提示词</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base font-medium">分析类型</Label>
|
||||
<RadioGroup value={analysisType} onValueChange={handleTypeChange} className="mt-2 space-y-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="friends" id="friends" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="friends" className="font-medium">
|
||||
好友信息分析
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">分析微信号下所有好友的昵称及微信信息,基于指定用户词进行筛选分析</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="moments" id="moments" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="moments" className="font-medium">
|
||||
朋友圈内容分析
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">
|
||||
运用AI技术对所有用户朋友圈内容进行分析,通过提示词辅助深入剖析用户类型
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="both" id="both" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="both" className="font-medium">
|
||||
综合分析(推荐)
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">同时分析好友信息和朋友圈内容,获得更全面的数据洞察</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="friends">好友分析设置</TabsTrigger>
|
||||
<TabsTrigger value="moments">朋友圈分析设置</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="friends" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label htmlFor="keywords" className="text-base font-medium">
|
||||
用户关键词
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-2">
|
||||
设置用于筛选好友的关键词,例如"美妆"将筛选出昵称或微信信息中与美妆相关的好友
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="keywords"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "keyword")}
|
||||
placeholder="输入关键词并回车"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="button" onClick={handleAddKeyword} disabled={!keyword.trim()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{keywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{keywords.map((kw, index) => (
|
||||
<Badge key={index} variant="secondary" className="pl-2 pr-1 py-1 flex items-center gap-1">
|
||||
{kw}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveKeyword(index)}
|
||||
className="ml-1 rounded-full hover:bg-gray-200 p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-sm text-gray-500 mb-2">推荐关键词:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedKeywords.map((kw) => (
|
||||
<Badge
|
||||
key={kw}
|
||||
variant="outline"
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
if (!keywords.includes(kw)) {
|
||||
const newKeywords = [...keywords, kw]
|
||||
setKeywords(newKeywords)
|
||||
updateFormData({ keywords: newKeywords })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{kw}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="moments" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label htmlFor="promptWords" className="text-base font-medium">
|
||||
分析提示词
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1 mb-2">
|
||||
设置辅助AI分析的提示词,如"人群属性"、"喜好分析"等,帮助深入剖析用户类型
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="promptWords"
|
||||
value={promptWord}
|
||||
onChange={(e) => setPromptWord(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "prompt")}
|
||||
placeholder="输入提示词并回车"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="button" onClick={handleAddPromptWord} disabled={!promptWord.trim()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{promptWords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{promptWords.map((pw, index) => (
|
||||
<Badge key={index} variant="secondary" className="pl-2 pr-1 py-1 flex items-center gap-1">
|
||||
{pw}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemovePromptWord(index)}
|
||||
className="ml-1 rounded-full hover:bg-gray-200 p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3">
|
||||
<p className="text-sm text-gray-500 mb-2">推荐提示词:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedPrompts.map((pw) => (
|
||||
<Badge
|
||||
key={pw}
|
||||
variant="outline"
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
if (!promptWords.includes(pw)) {
|
||||
const newPromptWords = [...promptWords, pw]
|
||||
setPromptWords(newPromptWords)
|
||||
updateFormData({ promptWords: newPromptWords })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pw}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={
|
||||
((analysisType === "friends" || analysisType === "both") && keywords.length === 0) ||
|
||||
((analysisType === "moments" || analysisType === "both") && promptWords.length === 0)
|
||||
}
|
||||
>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Smartphone, User, MessageCircle, Mail } from "lucide-react"
|
||||
|
||||
interface ConfirmAnalysisProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function ConfirmAnalysis({ formData, updateFormData, onSubmit, onBack }: ConfirmAnalysisProps) {
|
||||
const [planName, setPlanName] = useState(formData.name || "")
|
||||
const [emailReport, setEmailReport] = useState(formData.emailReport || false)
|
||||
const [email, setEmail] = useState(formData.email || "")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const getAnalysisTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "friends":
|
||||
return "好友信息分析"
|
||||
case "moments":
|
||||
return "朋友圈内容分析"
|
||||
case "both":
|
||||
return "综合分析"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
updateFormData({
|
||||
name: planName,
|
||||
emailReport,
|
||||
email: emailReport ? email : "",
|
||||
})
|
||||
|
||||
setIsSubmitting(true)
|
||||
await onSubmit()
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">确认分析计划</h2>
|
||||
<p className="text-gray-500 text-sm">确认分析计划信息并提交</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="planName" className="text-base font-medium">
|
||||
计划名称
|
||||
</Label>
|
||||
<Input
|
||||
id="planName"
|
||||
value={planName}
|
||||
onChange={(e) => setPlanName(e.target.value)}
|
||||
placeholder="输入分析计划名称"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-4">
|
||||
<h3 className="font-medium">分析计划信息</h3>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<Smartphone className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">设备信息</p>
|
||||
<p className="text-sm text-gray-600">{formData.deviceName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<User className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">微信账号</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{formData.wechatName} ({formData.wechatId})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">分析类型</p>
|
||||
<p className="text-sm text-gray-600">{getAnalysisTypeLabel(formData.analysisType)}</p>
|
||||
|
||||
{(formData.analysisType === "friends" || formData.analysisType === "both") &&
|
||||
formData.keywords.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium">用户关键词:</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{formData.keywords.map((keyword: string, index: number) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(formData.analysisType === "moments" || formData.analysisType === "both") &&
|
||||
formData.promptWords.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium">分析提示词:</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{formData.promptWords.map((promptWord: string, index: number) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{promptWord}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="emailReport"
|
||||
checked={emailReport}
|
||||
onCheckedChange={(checked) => {
|
||||
setEmailReport(checked as boolean)
|
||||
if (!checked) {
|
||||
setEmail("")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="emailReport" className="font-medium cursor-pointer">
|
||||
分析完成后发送报告到邮箱
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{emailReport && (
|
||||
<div className="pl-6">
|
||||
<Label htmlFor="email" className="text-sm">
|
||||
邮箱地址
|
||||
</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Mail className="h-4 w-4 text-gray-400 mr-2" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="输入接收报告的邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!planName.trim() || (emailReport && !email.trim()) || isSubmitting}>
|
||||
{isSubmitting ? "提交中..." : "开始分析"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Search, Smartphone } from "lucide-react"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline"
|
||||
wechatAccounts: {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
}[]
|
||||
}
|
||||
|
||||
interface DeviceSelectionProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function DeviceSelection({ formData, updateFormData, onNext }: DeviceSelectionProps) {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedDevice, setSelectedDevice] = useState<string>(formData.deviceId || "")
|
||||
const [selectedWechatAccount, setSelectedWechatAccount] = useState<string>(formData.wechatId || "")
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载设备数据
|
||||
const fetchDevices = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const mockDevices: Device[] = [
|
||||
{
|
||||
id: "device-1",
|
||||
name: "iPhone 13",
|
||||
status: "online",
|
||||
wechatAccounts: [
|
||||
{ id: "wx-1", nickname: "张小明", wechatId: "wxid_zhang123" },
|
||||
{ id: "wx-2", nickname: "李业务", wechatId: "wxid_libiz456" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "device-2",
|
||||
name: "华为 P40",
|
||||
status: "online",
|
||||
wechatAccounts: [{ id: "wx-3", nickname: "王经理", wechatId: "wxid_wang789" }],
|
||||
},
|
||||
{
|
||||
id: "device-3",
|
||||
name: "小米 12",
|
||||
status: "offline",
|
||||
wechatAccounts: [
|
||||
{ id: "wx-4", nickname: "赵销售", wechatId: "wxid_zhao321" },
|
||||
{ id: "wx-5", nickname: "陈顾问", wechatId: "wxid_chen654" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
setDevices(mockDevices)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchDevices()
|
||||
}, [])
|
||||
|
||||
const filteredDevices = devices.filter(
|
||||
(device) =>
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.wechatAccounts.some(
|
||||
(account) =>
|
||||
account.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
account.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
),
|
||||
)
|
||||
|
||||
const handleDeviceSelect = (deviceId: string) => {
|
||||
setSelectedDevice(deviceId)
|
||||
setSelectedWechatAccount("") // 重置微信账号选择
|
||||
|
||||
const selectedDeviceData = devices.find((d) => d.id === deviceId)
|
||||
if (selectedDeviceData) {
|
||||
updateFormData({
|
||||
deviceId,
|
||||
deviceName: selectedDeviceData.name,
|
||||
wechatId: "",
|
||||
wechatName: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleWechatSelect = (wechatId: string) => {
|
||||
setSelectedWechatAccount(wechatId)
|
||||
|
||||
const selectedDeviceData = devices.find((d) => d.id === selectedDevice)
|
||||
if (selectedDeviceData) {
|
||||
const selectedWechatData = selectedDeviceData.wechatAccounts.find((w) => w.id === wechatId)
|
||||
if (selectedWechatData) {
|
||||
updateFormData({
|
||||
wechatId: wechatId,
|
||||
wechatName: selectedWechatData.nickname,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (selectedDevice && selectedWechatAccount) {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-1">选择设备与微信号</h2>
|
||||
<p className="text-gray-500 text-sm">选择要进行AI数据分析的设备和微信账号</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索设备或微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-base font-medium">1. 选择设备</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2">
|
||||
{filteredDevices.map((device) => (
|
||||
<Card
|
||||
key={device.id}
|
||||
className={`p-3 cursor-pointer hover:shadow-md transition-shadow ${
|
||||
selectedDevice === device.id ? "ring-2 ring-blue-500" : ""
|
||||
} ${device.status === "offline" ? "opacity-60" : ""}`}
|
||||
onClick={() => device.status === "online" && handleDeviceSelect(device.id)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3">
|
||||
<Smartphone className="h-5 w-5 text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{device.name}</div>
|
||||
<div className="text-sm text-gray-500">{device.wechatAccounts.length} 个微信账号</div>
|
||||
</div>
|
||||
<div
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
device.status === "online" ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="col-span-2 text-center py-8 bg-gray-50 rounded-lg border">
|
||||
<p className="text-gray-500">未找到匹配的设备</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedDevice && (
|
||||
<div>
|
||||
<Label className="text-base font-medium">2. 选择微信账号</Label>
|
||||
<RadioGroup value={selectedWechatAccount} onValueChange={handleWechatSelect} className="mt-2 space-y-2">
|
||||
{devices
|
||||
.find((d) => d.id === selectedDevice)
|
||||
?.wechatAccounts.map((account) => (
|
||||
<div key={account.id} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={account.id} id={account.id} />
|
||||
<Label htmlFor={account.id} className="flex items-center cursor-pointer">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center mr-2">
|
||||
<span className="text-sm font-medium">{account.nickname[0]}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{account.nickname}</div>
|
||||
<div className="text-xs text-gray-500">{account.wechatId}</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={handleContinue} disabled={!selectedDevice || !selectedWechatAccount}>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react"
|
||||
|
||||
interface Step {
|
||||
number: number
|
||||
title: string
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
currentStep: number
|
||||
steps: Step[]
|
||||
}
|
||||
|
||||
export function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const isActive = currentStep >= step.number
|
||||
const isLast = index === steps.length - 1
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.number}>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
isActive ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{step.number}
|
||||
</div>
|
||||
<span className={`ml-2 text-sm font-medium ${isActive ? "text-blue-600" : "text-gray-500"}`}>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div className={`flex-1 h-0.5 mx-4 ${currentStep > step.number ? "bg-blue-600" : "bg-gray-200"}`} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { BarChart3, Users, Activity, Brain, Search } from "lucide-react"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
taskName: string
|
||||
analysisTypes: string[]
|
||||
}
|
||||
updateFormData: (
|
||||
data: Partial<{
|
||||
taskName: string
|
||||
analysisTypes: string[]
|
||||
}>,
|
||||
) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
interface AnalysisType {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
export function BasicSettings({ formData, updateFormData, onNext }: BasicSettingsProps) {
|
||||
const [errors, setErrors] = useState<{ taskName?: string; analysisTypes?: string }>({})
|
||||
|
||||
const analysisTypes: AnalysisType[] = [
|
||||
{
|
||||
id: "comprehensive",
|
||||
name: "综合分析",
|
||||
description: "全面分析用户画像、行为习惯和互动模式",
|
||||
icon: <Brain className="h-5 w-5 text-blue-500" />,
|
||||
},
|
||||
{
|
||||
id: "friend-info",
|
||||
name: "好友信息分析",
|
||||
description: "分析好友基本信息、地域分布和标签特征",
|
||||
icon: <Users className="h-5 w-5 text-green-500" />,
|
||||
},
|
||||
{
|
||||
id: "user-behavior",
|
||||
name: "用户行为分析",
|
||||
description: "分析用户互动频率、活跃时间和内容偏好",
|
||||
icon: <Activity className="h-5 w-5 text-purple-500" />,
|
||||
},
|
||||
{
|
||||
id: "content-preference",
|
||||
name: "内容偏好分析",
|
||||
description: "分析用户对不同类型内容的反应和互动情况",
|
||||
icon: <BarChart3 className="h-5 w-5 text-orange-500" />,
|
||||
},
|
||||
{
|
||||
id: "keyword-analysis",
|
||||
name: "关键词分析",
|
||||
description: "分析用户聊天和互动中的高频关键词和话题",
|
||||
icon: <Search className="h-5 w-5 text-red-500" />,
|
||||
},
|
||||
]
|
||||
|
||||
const handleAnalysisTypeChange = (typeId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
updateFormData({
|
||||
analysisTypes: [...formData.analysisTypes, typeId],
|
||||
})
|
||||
} else {
|
||||
updateFormData({
|
||||
analysisTypes: formData.analysisTypes.filter((id) => id !== typeId),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { taskName?: string; analysisTypes?: string } = {}
|
||||
|
||||
if (!formData.taskName.trim()) {
|
||||
newErrors.taskName = "请输入任务名称"
|
||||
}
|
||||
|
||||
if (formData.analysisTypes.length === 0) {
|
||||
newErrors.analysisTypes = "请至少选择一种分析类型"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (validateForm()) {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">创建分析计划</h2>
|
||||
<p className="text-gray-500 mb-6">设置分析任务名称并选择需要的分析类型</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="taskName" className="text-base">
|
||||
任务名称
|
||||
</Label>
|
||||
<Input
|
||||
id="taskName"
|
||||
placeholder="例如:11月用户行为分析"
|
||||
value={formData.taskName}
|
||||
onChange={(e) => updateFormData({ taskName: e.target.value })}
|
||||
className="mt-1"
|
||||
/>
|
||||
{errors.taskName && <p className="text-red-500 text-sm mt-1">{errors.taskName}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base">分析类型(可多选)</Label>
|
||||
|
||||
{errors.analysisTypes && <p className="text-red-500 text-sm">{errors.analysisTypes}</p>}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{analysisTypes.map((type) => (
|
||||
<Card
|
||||
key={type.id}
|
||||
className={`p-4 cursor-pointer hover:shadow-md transition-shadow ${
|
||||
formData.analysisTypes.includes(type.id) ? "border-2 border-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id={`type-${type.id}`}
|
||||
checked={formData.analysisTypes.includes(type.id)}
|
||||
onCheckedChange={(checked) => handleAnalysisTypeChange(type.id, checked as boolean)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`type-${type.id}`} className="flex items-center cursor-pointer">
|
||||
<div className="mr-2">{type.icon}</div>
|
||||
<span className="font-medium">{type.name}</span>
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">{type.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, Tag, MapPin } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { DeviceSelector } from "@/app/components/DeviceSelector"
|
||||
|
||||
interface TargetSelectionProps {
|
||||
formData: {
|
||||
targetType: string
|
||||
selectedDevices: string[]
|
||||
selectedTrafficPool: string
|
||||
tags: string[]
|
||||
regions: string[]
|
||||
keywords: string[]
|
||||
}
|
||||
updateFormData: (
|
||||
data: Partial<{
|
||||
targetType: string
|
||||
selectedDevices: string[]
|
||||
selectedTrafficPool: string
|
||||
tags: string[]
|
||||
regions: string[]
|
||||
keywords: string[]
|
||||
}>,
|
||||
) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
interface TrafficUser {
|
||||
id: string
|
||||
avatar: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
phone: string
|
||||
region: string
|
||||
tags: string[]
|
||||
addTime: string
|
||||
source: string
|
||||
}
|
||||
|
||||
export function TargetSelection({ formData, updateFormData, onSubmit, onBack }: TargetSelectionProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [trafficUsers, setTrafficUsers] = useState<TrafficUser[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||
const [selectedRegions, setSelectedRegions] = useState<string[]>([])
|
||||
const [errors, setErrors] = useState<{ selection?: string }>({})
|
||||
|
||||
// 模拟加载流量池用户数据
|
||||
useEffect(() => {
|
||||
const fetchTrafficUsers = async () => {
|
||||
setIsLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
const mockUsers: TrafficUser[] = [
|
||||
{
|
||||
id: "user-1",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "张小明",
|
||||
wechatId: "zhangxm123",
|
||||
phone: "138****1234",
|
||||
region: "北京",
|
||||
tags: ["新用户", "低活跃度"],
|
||||
addTime: "2023-10-15",
|
||||
source: "朋友圈",
|
||||
},
|
||||
{
|
||||
id: "user-2",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "李华",
|
||||
wechatId: "lihua456",
|
||||
phone: "139****5678",
|
||||
region: "上海",
|
||||
tags: ["高消费", "高活跃度"],
|
||||
addTime: "2023-09-20",
|
||||
source: "群聊",
|
||||
},
|
||||
{
|
||||
id: "user-3",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "王芳",
|
||||
wechatId: "wangfang789",
|
||||
phone: "137****9012",
|
||||
region: "广州",
|
||||
tags: ["潜在客户", "有购买意向"],
|
||||
addTime: "2023-11-05",
|
||||
source: "搜索",
|
||||
},
|
||||
{
|
||||
id: "user-4",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "赵明",
|
||||
wechatId: "zhaoming321",
|
||||
phone: "136****3456",
|
||||
region: "深圳",
|
||||
tags: ["网购达人", "中高消费"],
|
||||
addTime: "2023-10-28",
|
||||
source: "附近的人",
|
||||
},
|
||||
{
|
||||
id: "user-5",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "刘洋",
|
||||
wechatId: "liuyang654",
|
||||
phone: "135****7890",
|
||||
region: "杭州",
|
||||
tags: ["90后", "学生"],
|
||||
addTime: "2023-11-10",
|
||||
source: "名片",
|
||||
},
|
||||
{
|
||||
id: "user-6",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "陈静",
|
||||
wechatId: "chenjing987",
|
||||
phone: "134****1234",
|
||||
region: "成都",
|
||||
tags: ["00后", "学生"],
|
||||
addTime: "2023-11-15",
|
||||
source: "朋友推荐",
|
||||
},
|
||||
{
|
||||
id: "user-7",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "林小红",
|
||||
wechatId: "linxh123",
|
||||
phone: "133****5678",
|
||||
region: "南京",
|
||||
tags: ["潜在客户", "华东地区"],
|
||||
addTime: "2023-10-05",
|
||||
source: "扫码",
|
||||
},
|
||||
{
|
||||
id: "user-8",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "黄强",
|
||||
wechatId: "huangq456",
|
||||
phone: "132****9012",
|
||||
region: "武汉",
|
||||
tags: ["高消费", "有购买意向"],
|
||||
addTime: "2023-09-15",
|
||||
source: "朋友圈",
|
||||
},
|
||||
]
|
||||
|
||||
setTrafficUsers(mockUsers)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchTrafficUsers()
|
||||
}, [])
|
||||
|
||||
const allTags = Array.from(new Set(trafficUsers.flatMap((user) => user.tags)))
|
||||
const allRegions = Array.from(new Set(trafficUsers.map((user) => user.region)))
|
||||
|
||||
const filteredUsers = trafficUsers.filter((user) => {
|
||||
const matchesSearch =
|
||||
user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.region.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const matchesTags = selectedTags.length === 0 || selectedTags.some((tag) => user.tags.includes(tag))
|
||||
|
||||
const matchesRegions = selectedRegions.length === 0 || selectedRegions.includes(user.region)
|
||||
|
||||
return matchesSearch && matchesTags && matchesRegions
|
||||
})
|
||||
|
||||
const handleDeviceSelect = (deviceIds: string[]) => {
|
||||
updateFormData({ selectedDevices: deviceIds })
|
||||
}
|
||||
|
||||
const handleUserSelect = (userId: string) => {
|
||||
updateFormData({
|
||||
selectedTrafficPool: userId === formData.selectedTrafficPool ? "" : userId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleTagSelect = (tag: string) => {
|
||||
setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]))
|
||||
}
|
||||
|
||||
const handleRegionSelect = (region: string) => {
|
||||
setSelectedRegions((prev) => (prev.includes(region) ? prev.filter((r) => r !== region) : [...prev, region]))
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { selection?: string } = {}
|
||||
|
||||
if (formData.targetType === "device" && formData.selectedDevices.length === 0) {
|
||||
newErrors.selection = "请至少选择一个设备"
|
||||
} else if (formData.targetType === "trafficPool" && !formData.selectedTrafficPool) {
|
||||
newErrors.selection = "请选择一个流量池"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validateForm()) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">选择分析对象</h2>
|
||||
<p className="text-gray-500 mb-6">选择要分析的设备或流量池</p>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue="device"
|
||||
onValueChange={(value) => updateFormData({ targetType: value })}
|
||||
value={formData.targetType}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="device">设备列表</TabsTrigger>
|
||||
<TabsTrigger value="trafficPool">流量池</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="device" className="space-y-4">
|
||||
<DeviceSelector
|
||||
onSelect={handleDeviceSelect}
|
||||
initialSelectedDevices={formData.selectedDevices}
|
||||
excludeUsedDevices={false}
|
||||
/>
|
||||
|
||||
{errors.selection && formData.targetType === "device" && (
|
||||
<p className="text-red-500 text-sm">{errors.selection}</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="trafficPool" className="space-y-6">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索标签、地区、昵称信息"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Label className="flex items-center">
|
||||
<Tag className="h-4 w-4 mr-1" />
|
||||
<span>标签筛选:</span>
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{allTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Label className="flex items-center">
|
||||
<MapPin className="h-4 w-4 mr-1" />
|
||||
<span>地区筛选:</span>
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{allRegions.map((region) => (
|
||||
<Badge
|
||||
key={region}
|
||||
variant={selectedRegions.includes(region) ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleRegionSelect(region)}
|
||||
>
|
||||
{region}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">用户</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">微信号</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">地区</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">标签</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">来源</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">添加时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className={`border-t hover:bg-gray-50 cursor-pointer ${
|
||||
formData.selectedTrafficPool === user.id ? "bg-blue-50" : ""
|
||||
}`}
|
||||
onClick={() => handleUserSelect(user.id)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-8 w-8 flex-shrink-0 rounded-full overflow-hidden mr-3">
|
||||
<img
|
||||
src={user.avatar || "/placeholder.svg"}
|
||||
alt={user.nickname}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{user.nickname}</div>
|
||||
<div className="text-gray-500 text-xs">{user.phone}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{user.wechatId}</td>
|
||||
<td className="px-4 py-3 text-sm">{user.region}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{user.source}</td>
|
||||
<td className="px-4 py-3 text-sm">{user.addTime}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredUsers.length === 0 && !isLoading && (
|
||||
<div className="text-center py-8 bg-gray-50 rounded-lg border">
|
||||
<p className="text-gray-500">没有找到符合条件的微信用户</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors.selection && formData.targetType === "trafficPool" && (
|
||||
<p className="text-red-500 text-sm">{errors.selection}</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{formData.targetType === "device" && formData.selectedDevices.length > 0 && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
已选择 {formData.selectedDevices.length} 个设备进行分析
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{formData.targetType === "trafficPool" && formData.selectedTrafficPool && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
已选择用户: {trafficUsers.find((u) => u.id === formData.selectedTrafficPool)?.nickname}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>提交</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
103
Cunkebao/app/workspace/ai-analyzer/create/page.tsx
Normal file
103
Cunkebao/app/workspace/ai-analyzer/create/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { BasicSettings } from "./components/basic-settings"
|
||||
import { TargetSelection } from "./components/target-selection"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Steps, Step } from "@/components/ui/steps"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export default function CreateAnalyzerPage() {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
analysisTypes: [] as string[],
|
||||
targetType: "device",
|
||||
selectedDevices: [] as string[],
|
||||
selectedTrafficPool: "",
|
||||
tags: [] as string[],
|
||||
regions: [] as string[],
|
||||
keywords: [] as string[],
|
||||
})
|
||||
|
||||
const updateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep(1)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setCurrentStep(0)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 模拟提交
|
||||
try {
|
||||
// 显示加载状态
|
||||
toast({
|
||||
title: "正在创建分析计划...",
|
||||
description: "请稍候",
|
||||
})
|
||||
|
||||
// 模拟API请求延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 成功提示
|
||||
toast({
|
||||
title: "分析计划创建成功",
|
||||
description: "您可以在列表中查看分析进度",
|
||||
variant: "success",
|
||||
})
|
||||
|
||||
// 跳转回列表页
|
||||
router.push("/workspace/ai-analyzer")
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{ id: "basic", title: "基础设置" },
|
||||
{ id: "target", title: "选择分析对象" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl py-6">
|
||||
<div className="mb-8">
|
||||
<Steps currentStep={currentStep} className="mb-8">
|
||||
{steps.map((step, index) => (
|
||||
<Step key={step.id} title={step.title} />
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{currentStep === 0 && (
|
||||
<BasicSettings formData={formData} updateFormData={updateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<TargetSelection
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onBack={handleBack}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
taskName: string
|
||||
analysisTypes: string[]
|
||||
}
|
||||
updateFormData: (
|
||||
data: Partial<{
|
||||
taskName: string
|
||||
analysisTypes: string[]
|
||||
}>,
|
||||
) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
const analysisTypeOptions = [
|
||||
{ id: "comprehensive", label: "综合分析" },
|
||||
{ id: "friends", label: "好友信息分析" },
|
||||
{ id: "behavior", label: "用户行为分析" },
|
||||
{ id: "moments", label: "朋友圈内容分析" },
|
||||
{ id: "interaction", label: "互动频率分析" },
|
||||
]
|
||||
|
||||
export function BasicSettings({ formData, updateFormData, onNext }: BasicSettingsProps) {
|
||||
const [errors, setErrors] = useState<{ taskName?: string; analysisTypes?: string }>({})
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { taskName?: string; analysisTypes?: string } = {}
|
||||
|
||||
if (!formData.taskName?.trim()) {
|
||||
newErrors.taskName = "请输入任务名称"
|
||||
}
|
||||
|
||||
if (formData.analysisTypes.length === 0) {
|
||||
newErrors.analysisTypes = "请至少选择一种分析类型"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (validateForm()) {
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeChange = (typeId: string, checked: boolean) => {
|
||||
const newTypes = checked
|
||||
? [...formData.analysisTypes, typeId]
|
||||
: formData.analysisTypes.filter((id) => id !== typeId)
|
||||
|
||||
updateFormData({ analysisTypes: newTypes })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">基础设置</h2>
|
||||
<p className="text-gray-500 mb-6">设置分析计划的基本信息和分析类型</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="task-name" className="text-base">
|
||||
任务名称
|
||||
</Label>
|
||||
<Input
|
||||
id="task-name"
|
||||
placeholder="请输入任务名称"
|
||||
value={formData.taskName || ""}
|
||||
onChange={(e) => updateFormData({ taskName: e.target.value })}
|
||||
className="mt-1"
|
||||
/>
|
||||
{errors.taskName && <p className="text-red-500 text-sm mt-1">{errors.taskName}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base">分析类型</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{analysisTypeOptions.map((option) => (
|
||||
<div key={option.id} className="flex items-center space-x-2 bg-gray-50 p-3 rounded-md">
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
checked={formData.analysisTypes.includes(option.id)}
|
||||
onCheckedChange={(checked) => handleTypeChange(option.id, checked === true)}
|
||||
/>
|
||||
<Label htmlFor={option.id} className="cursor-pointer">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{errors.analysisTypes && <p className="text-red-500 text-sm">{errors.analysisTypes}</p>}
|
||||
</div>
|
||||
|
||||
{formData.analysisTypes.length > 0 && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
已选择 {formData.analysisTypes.length} 种分析类型,分析结果将更加全面
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button onClick={handleNext} className="w-32">
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, Tag, MapPin } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { DeviceSelector } from "@/app/components/DeviceSelector"
|
||||
|
||||
interface TargetSelectionProps {
|
||||
formData: {
|
||||
targetType: string
|
||||
selectedDevices: string[]
|
||||
selectedTrafficPool: string
|
||||
tags: string[]
|
||||
regions: string[]
|
||||
keywords: string[]
|
||||
}
|
||||
updateFormData: (
|
||||
data: Partial<{
|
||||
targetType: string
|
||||
selectedDevices: string[]
|
||||
selectedTrafficPool: string
|
||||
tags: string[]
|
||||
regions: string[]
|
||||
keywords: string[]
|
||||
}>,
|
||||
) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
interface TrafficUser {
|
||||
id: string
|
||||
avatar: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
phone: string
|
||||
region: string
|
||||
tags: string[]
|
||||
addTime: string
|
||||
source: string
|
||||
}
|
||||
|
||||
export function TargetSelection({ formData, updateFormData, onSubmit, onBack }: TargetSelectionProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [trafficUsers, setTrafficUsers] = useState<TrafficUser[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||
const [selectedRegions, setSelectedRegions] = useState<string[]>([])
|
||||
const [errors, setErrors] = useState<{ selection?: string }>({})
|
||||
|
||||
// 模拟加载流量池用户数据
|
||||
useEffect(() => {
|
||||
const fetchTrafficUsers = async () => {
|
||||
setIsLoading(true)
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
|
||||
const mockUsers: TrafficUser[] = [
|
||||
{
|
||||
id: "user-1",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "张小明",
|
||||
wechatId: "zhangxm123",
|
||||
phone: "138****1234",
|
||||
region: "北京",
|
||||
tags: ["新用户", "低活跃度"],
|
||||
addTime: "2023-10-15",
|
||||
source: "朋友圈",
|
||||
},
|
||||
{
|
||||
id: "user-2",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "李华",
|
||||
wechatId: "lihua456",
|
||||
phone: "139****5678",
|
||||
region: "上海",
|
||||
tags: ["高消费", "高活跃度"],
|
||||
addTime: "2023-09-20",
|
||||
source: "群聊",
|
||||
},
|
||||
{
|
||||
id: "user-3",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "王芳",
|
||||
wechatId: "wangfang789",
|
||||
phone: "137****9012",
|
||||
region: "广州",
|
||||
tags: ["潜在客户", "有购买意向"],
|
||||
addTime: "2023-11-05",
|
||||
source: "搜索",
|
||||
},
|
||||
{
|
||||
id: "user-4",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "赵明",
|
||||
wechatId: "zhaoming321",
|
||||
phone: "136****3456",
|
||||
region: "深圳",
|
||||
tags: ["网购达人", "中高消费"],
|
||||
addTime: "2023-10-28",
|
||||
source: "附近的人",
|
||||
},
|
||||
{
|
||||
id: "user-5",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "刘洋",
|
||||
wechatId: "liuyang654",
|
||||
phone: "135****7890",
|
||||
region: "杭州",
|
||||
tags: ["90后", "学生"],
|
||||
addTime: "2023-11-10",
|
||||
source: "名片",
|
||||
},
|
||||
{
|
||||
id: "user-6",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "陈静",
|
||||
wechatId: "chenjing987",
|
||||
phone: "134****1234",
|
||||
region: "成都",
|
||||
tags: ["00后", "学生"],
|
||||
addTime: "2023-11-15",
|
||||
source: "朋友推荐",
|
||||
},
|
||||
{
|
||||
id: "user-7",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "林小红",
|
||||
wechatId: "linxh123",
|
||||
phone: "133****5678",
|
||||
region: "南京",
|
||||
tags: ["潜在客户", "华东地区"],
|
||||
addTime: "2023-10-05",
|
||||
source: "扫码",
|
||||
},
|
||||
{
|
||||
id: "user-8",
|
||||
avatar: "/placeholder.svg?height=40&width=40",
|
||||
nickname: "黄强",
|
||||
wechatId: "huangq456",
|
||||
phone: "132****9012",
|
||||
region: "武汉",
|
||||
tags: ["高消费", "有购买意向"],
|
||||
addTime: "2023-09-15",
|
||||
source: "朋友圈",
|
||||
},
|
||||
]
|
||||
|
||||
setTrafficUsers(mockUsers)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchTrafficUsers()
|
||||
}, [])
|
||||
|
||||
const allTags = Array.from(new Set(trafficUsers.flatMap((user) => user.tags)))
|
||||
const allRegions = Array.from(new Set(trafficUsers.map((user) => user.region)))
|
||||
|
||||
const filteredUsers = trafficUsers.filter((user) => {
|
||||
const matchesSearch =
|
||||
user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.region.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const matchesTags = selectedTags.length === 0 || selectedTags.some((tag) => user.tags.includes(tag))
|
||||
|
||||
const matchesRegions = selectedRegions.length === 0 || selectedRegions.includes(user.region)
|
||||
|
||||
return matchesSearch && matchesTags && matchesRegions
|
||||
})
|
||||
|
||||
const handleDeviceSelect = (deviceIds: string[]) => {
|
||||
updateFormData({ selectedDevices: deviceIds })
|
||||
}
|
||||
|
||||
const handleUserSelect = (userId: string) => {
|
||||
updateFormData({
|
||||
selectedTrafficPool: userId === formData.selectedTrafficPool ? "" : userId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleTagSelect = (tag: string) => {
|
||||
setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]))
|
||||
}
|
||||
|
||||
const handleRegionSelect = (region: string) => {
|
||||
setSelectedRegions((prev) => (prev.includes(region) ? prev.filter((r) => r !== region) : [...prev, region]))
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: { selection?: string } = {}
|
||||
|
||||
if (formData.targetType === "device" && formData.selectedDevices.length === 0) {
|
||||
newErrors.selection = "请至少选择一个设备"
|
||||
} else if (formData.targetType === "trafficPool" && !formData.selectedTrafficPool) {
|
||||
newErrors.selection = "请选择一个流量池"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validateForm()) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">选择分析对象</h2>
|
||||
<p className="text-gray-500 mb-6">选择要分析的设备或流量池</p>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue="device"
|
||||
onValueChange={(value) => updateFormData({ targetType: value })}
|
||||
value={formData.targetType}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="device">设备列表</TabsTrigger>
|
||||
<TabsTrigger value="trafficPool">流量池</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="device" className="space-y-4">
|
||||
<DeviceSelector
|
||||
onSelect={handleDeviceSelect}
|
||||
initialSelectedDevices={formData.selectedDevices}
|
||||
excludeUsedDevices={false}
|
||||
/>
|
||||
|
||||
{errors.selection && formData.targetType === "device" && (
|
||||
<p className="text-red-500 text-sm">{errors.selection}</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="trafficPool" className="space-y-6">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索标签、地区、昵称信息"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Label className="flex items-center">
|
||||
<Tag className="h-4 w-4 mr-1" />
|
||||
<span>标签筛选:</span>
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{allTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Label className="flex items-center">
|
||||
<MapPin className="h-4 w-4 mr-1" />
|
||||
<span>地区筛选:</span>
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{allRegions.map((region) => (
|
||||
<Badge
|
||||
key={region}
|
||||
variant={selectedRegions.includes(region) ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleRegionSelect(region)}
|
||||
>
|
||||
{region}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">用户</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">微信号</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">地区</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">标签</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">来源</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-500">添加时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className={`border-t hover:bg-gray-50 cursor-pointer ${
|
||||
formData.selectedTrafficPool === user.id ? "bg-blue-50" : ""
|
||||
}`}
|
||||
onClick={() => handleUserSelect(user.id)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-8 w-8 flex-shrink-0 rounded-full overflow-hidden mr-3">
|
||||
<img
|
||||
src={user.avatar || "/placeholder.svg"}
|
||||
alt={user.nickname}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{user.nickname}</div>
|
||||
<div className="text-gray-500 text-xs">{user.phone}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{user.wechatId}</td>
|
||||
<td className="px-4 py-3 text-sm">{user.region}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{user.source}</td>
|
||||
<td className="px-4 py-3 text-sm">{user.addTime}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredUsers.length === 0 && !isLoading && (
|
||||
<div className="text-center py-8 bg-gray-50 rounded-lg border">
|
||||
<p className="text-gray-500">没有找到符合条件的微信用户</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors.selection && formData.targetType === "trafficPool" && (
|
||||
<p className="text-red-500 text-sm">{errors.selection}</p>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{formData.targetType === "device" && formData.selectedDevices.length > 0 && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
已选择 {formData.selectedDevices.length} 个设备进行分析
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{formData.targetType === "trafficPool" && formData.selectedTrafficPool && (
|
||||
<Alert className="bg-blue-50 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500" />
|
||||
<AlertDescription className="text-blue-700">
|
||||
已选择用户: {trafficUsers.find((u) => u.id === formData.selectedTrafficPool)?.nickname}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>提交</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
117
Cunkebao/app/workspace/ai-analyzer/new/page.tsx
Normal file
117
Cunkebao/app/workspace/ai-analyzer/new/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { StepIndicator } from "../components/step-indicator"
|
||||
import { BasicSettings } from "./components/basic-settings"
|
||||
import { TargetSelection } from "./components/target-selection"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export default function CreateAnalysisPlanPage() {
|
||||
const router = useRouter()
|
||||
const { toast } = useToast()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
analysisTypes: [] as string[],
|
||||
targetType: "device", // "device" or "trafficPool"
|
||||
selectedDevices: [] as string[],
|
||||
selectedTrafficPool: "",
|
||||
tags: [] as string[],
|
||||
regions: [] as string[],
|
||||
keywords: [] as string[],
|
||||
})
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep === 1) {
|
||||
router.back()
|
||||
} else {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 显示加载状态
|
||||
toast({
|
||||
title: "正在创建分析计划...",
|
||||
description: "请稍候",
|
||||
})
|
||||
|
||||
// 模拟API请求延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 成功提示
|
||||
toast({
|
||||
title: "分析计划创建成功",
|
||||
description: "您可以在列表中查看分析进度",
|
||||
})
|
||||
|
||||
// 跳转回列表页
|
||||
router.push("/workspace/ai-analyzer")
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "创建失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen max-h-screen bg-gray-50">
|
||||
{/* 头部 */}
|
||||
<div className="bg-white p-4 border-b flex items-center">
|
||||
<Button variant="ghost" size="icon" onClick={handleBack} className="mr-2">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">新建分析计划</h1>
|
||||
</div>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<div className="bg-white border-b">
|
||||
<div className="max-w-4xl mx-auto py-4 px-4">
|
||||
<StepIndicator
|
||||
currentStep={currentStep}
|
||||
steps={[
|
||||
{ number: 1, title: "基础设置" },
|
||||
{ number: 2, title: "选择分析对象" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="p-6">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings formData={formData} updateFormData={updateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<TargetSelection
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
368
Cunkebao/app/workspace/ai-analyzer/page.tsx
Normal file
368
Cunkebao/app/workspace/ai-analyzer/page.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ArrowLeft, Plus, FileText, BarChart2, Mail } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import Link from "next/link"
|
||||
|
||||
interface AnalysisPlan {
|
||||
id: string
|
||||
name: string
|
||||
wechatId: string
|
||||
deviceName: string
|
||||
status: "running" | "completed" | "failed"
|
||||
createdAt: string
|
||||
completedAt?: string
|
||||
keywords: string[]
|
||||
type: "friends" | "moments" | "both" | "behavior"
|
||||
}
|
||||
|
||||
export default function AIAnalyzerPage() {
|
||||
const router = useRouter()
|
||||
const [plans, setPlans] = useState<AnalysisPlan[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载数据
|
||||
const fetchPlans = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const mockPlans: AnalysisPlan[] = [
|
||||
{
|
||||
id: "plan-1",
|
||||
name: "美妆用户分析",
|
||||
wechatId: "wxid_abc123",
|
||||
deviceName: "设备1",
|
||||
status: "completed",
|
||||
createdAt: "2023-12-15T10:30:00Z",
|
||||
completedAt: "2023-12-15T11:45:00Z",
|
||||
keywords: ["美妆", "护肤", "彩妆"],
|
||||
type: "both",
|
||||
},
|
||||
{
|
||||
id: "plan-2",
|
||||
name: "健身爱好者分析",
|
||||
wechatId: "wxid_fit456",
|
||||
deviceName: "设备2",
|
||||
status: "running",
|
||||
createdAt: "2023-12-16T09:15:00Z",
|
||||
keywords: ["健身", "运动", "健康"],
|
||||
type: "friends",
|
||||
},
|
||||
{
|
||||
id: "plan-3",
|
||||
name: "教育行业分析",
|
||||
wechatId: "wxid_edu789",
|
||||
deviceName: "设备3",
|
||||
status: "completed",
|
||||
createdAt: "2023-12-14T14:20:00Z",
|
||||
completedAt: "2023-12-14T16:10:00Z",
|
||||
keywords: ["教育", "培训", "学习"],
|
||||
type: "moments",
|
||||
},
|
||||
{
|
||||
id: "plan-4",
|
||||
name: "用户行为分析",
|
||||
wechatId: "wxid_beh123",
|
||||
deviceName: "设备4",
|
||||
status: "completed",
|
||||
createdAt: "2023-12-18T08:20:00Z",
|
||||
completedAt: "2023-12-18T10:15:00Z",
|
||||
keywords: ["活跃度", "互动", "转化"],
|
||||
type: "behavior",
|
||||
},
|
||||
]
|
||||
|
||||
setPlans(mockPlans)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchPlans()
|
||||
}, [])
|
||||
|
||||
const getStatusBadge = (status: AnalysisPlan["status"]) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return (
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-600 border-blue-200">
|
||||
分析中
|
||||
</Badge>
|
||||
)
|
||||
case "completed":
|
||||
return (
|
||||
<Badge variant="outline" className="bg-green-50 text-green-600 border-green-200">
|
||||
已完成
|
||||
</Badge>
|
||||
)
|
||||
case "failed":
|
||||
return (
|
||||
<Badge variant="outline" className="bg-red-50 text-red-600 border-red-200">
|
||||
失败
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: AnalysisPlan["type"]) => {
|
||||
switch (type) {
|
||||
case "friends":
|
||||
return "好友信息分析"
|
||||
case "moments":
|
||||
return "朋友圈内容分析"
|
||||
case "both":
|
||||
return "综合分析"
|
||||
case "behavior":
|
||||
return "用户行为分析"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen max-h-screen bg-gray-50">
|
||||
{/* 头部 */}
|
||||
<div className="bg-white p-4 border-b flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">AI数据分析</h1>
|
||||
</div>
|
||||
<Button onClick={() => router.push("/workspace/ai-analyzer/new")} className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
新建分析计划
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<Tabs defaultValue="all" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 mb-6">
|
||||
<TabsTrigger value="all">全部计划</TabsTrigger>
|
||||
<TabsTrigger value="running">进行中</TabsTrigger>
|
||||
<TabsTrigger value="completed">已完成</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="all" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : plans.length > 0 ? (
|
||||
plans.map((plan) => (
|
||||
<Card key={plan.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
设备: {plan.deviceName} | 微信号: {plan.wechatId}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{getStatusBadge(plan.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">分析类型</span>
|
||||
<span className="font-medium">{getTypeLabel(plan.type)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">关键词</span>
|
||||
<div className="flex flex-wrap gap-1 justify-end">
|
||||
{plan.keywords.map((keyword, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">创建时间</span>
|
||||
<span>{new Date(plan.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
{plan.completedAt && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">完成时间</span>
|
||||
<span>{new Date(plan.completedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
{plan.status === "completed" && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>发送报告</span>
|
||||
</Button>
|
||||
<Link href={`/workspace/ai-analyzer/${plan.id}`}>
|
||||
<Button size="sm" className="flex items-center gap-1">
|
||||
<BarChart2 className="h-4 w-4" />
|
||||
<span>查看报告</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{plan.status === "running" && (
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>查看进度</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 bg-white rounded-lg border">
|
||||
<div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
<BarChart2 className="h-8 w-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-1">暂无分析计划</h3>
|
||||
<p className="text-gray-500 mb-4">创建一个新的AI数据分析计划来开始</p>
|
||||
<Button onClick={() => router.push("/workspace/ai-analyzer/new")}>新建分析计划</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="running" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : plans.filter((p) => p.status === "running").length > 0 ? (
|
||||
plans
|
||||
.filter((p) => p.status === "running")
|
||||
.map((plan) => (
|
||||
<Card key={plan.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
设备: {plan.deviceName} | 微信号: {plan.wechatId}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{getStatusBadge(plan.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">分析类型</span>
|
||||
<span className="font-medium">{getTypeLabel(plan.type)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">关键词</span>
|
||||
<div className="flex flex-wrap gap-1 justify-end">
|
||||
{plan.keywords.map((keyword, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">创建时间</span>
|
||||
<span>{new Date(plan.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>查看进度</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 bg-white rounded-lg border">
|
||||
<p className="text-gray-500">暂无进行中的分析计划</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="completed" className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : plans.filter((p) => p.status === "completed").length > 0 ? (
|
||||
plans
|
||||
.filter((p) => p.status === "completed")
|
||||
.map((plan) => (
|
||||
<Card key={plan.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
设备: {plan.deviceName} | 微信号: {plan.wechatId}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{getStatusBadge(plan.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">分析类型</span>
|
||||
<span className="font-medium">{getTypeLabel(plan.type)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">关键词</span>
|
||||
<div className="flex flex-wrap gap-1 justify-end">
|
||||
{plan.keywords.map((keyword, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">创建时间</span>
|
||||
<span>{new Date(plan.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
{plan.completedAt && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">完成时间</span>
|
||||
<span>{new Date(plan.completedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>发送报告</span>
|
||||
</Button>
|
||||
<Link href={`/workspace/ai-analyzer/${plan.id}`}>
|
||||
<Button size="sm" className="flex items-center gap-1">
|
||||
<BarChart2 className="h-4 w-4" />
|
||||
<span>查看报告</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 bg-white rounded-lg border">
|
||||
<p className="text-gray-500">暂无已完成的分析计划</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user