存客宝 React
This commit is contained in:
344
Cunkebao/app/workspace/ai-strategy/[id]/page.tsx
Normal file
344
Cunkebao/app/workspace/ai-strategy/[id]/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
"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, Tag, Users, Database, 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 StrategyReport {
|
||||
id: string
|
||||
name: string
|
||||
deviceNames: string[]
|
||||
trafficPoolNames: string[]
|
||||
createdAt: string
|
||||
completedAt: string
|
||||
strategyType: "jd" | "yisi" | "database" | "custom"
|
||||
strategyName: string
|
||||
trafficPoolSize: number
|
||||
optimizedUsers: number
|
||||
summary: string
|
||||
userSegments: {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
}[]
|
||||
userAttributes: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
}[]
|
||||
}
|
||||
recommendations: string[]
|
||||
}
|
||||
|
||||
export default function StrategyReportPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter()
|
||||
const [report, setReport] = useState<StrategyReport | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载报告数据
|
||||
const fetchReport = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
const mockReport: StrategyReport = {
|
||||
id: params.id,
|
||||
name: "京东会员优化",
|
||||
deviceNames: ["设备1", "设备2"],
|
||||
trafficPoolNames: ["微信好友池-设备1", "设备流量池-设备1"],
|
||||
createdAt: "2023-12-15T10:30:00Z",
|
||||
completedAt: "2023-12-15T11:45:00Z",
|
||||
strategyType: "jd",
|
||||
strategyName: "京东会员识别",
|
||||
trafficPoolSize: 287,
|
||||
optimizedUsers: 142,
|
||||
summary:
|
||||
"通过京东会员识别策略,在287位用户中成功识别出142位京东会员用户,占比49.5%。其中PLUS会员38位,VIP会员56位,普通会员48位。这些用户主要集中在电子产品和美妆护肤品类,消费能力中上。建议针对PLUS会员和VIP会员进行高端产品推广,对普通会员提供会员升级优惠。",
|
||||
userSegments: [
|
||||
{ name: "PLUS会员", count: 38, percentage: 27 },
|
||||
{ name: "VIP会员", count: 56, percentage: 39 },
|
||||
{ name: "普通会员", count: 48, percentage: 34 },
|
||||
],
|
||||
userAttributes: {
|
||||
购物偏好: [
|
||||
{ name: "电子产品", count: 52, percentage: 37 },
|
||||
{ name: "美妆护肤", count: 43, percentage: 30 },
|
||||
{ name: "家居生活", count: 28, percentage: 20 },
|
||||
{ name: "服装鞋包", count: 19, percentage: 13 },
|
||||
],
|
||||
消费能力: [
|
||||
{ name: "高", count: 35, percentage: 25 },
|
||||
{ name: "中高", count: 48, percentage: 34 },
|
||||
{ name: "中", count: 39, percentage: 27 },
|
||||
{ name: "中低", count: 20, percentage: 14 },
|
||||
],
|
||||
活跃度: [
|
||||
{ name: "高度活跃", count: 42, percentage: 30 },
|
||||
{ name: "中度活跃", count: 63, percentage: 44 },
|
||||
{ name: "低度活跃", count: 37, percentage: 26 },
|
||||
],
|
||||
},
|
||||
recommendations: [
|
||||
"针对PLUS会员推送高端电子产品和限量版美妆产品",
|
||||
"为VIP会员提供专属优惠券和新品预售资格",
|
||||
"向普通会员推广PLUS会员权益,提供升级优惠",
|
||||
"根据用户购物偏好进行精准内容推送",
|
||||
"对高消费能力用户提供专属客服和VIP服务",
|
||||
],
|
||||
}
|
||||
|
||||
setReport(mockReport)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchReport()
|
||||
}, [params.id])
|
||||
|
||||
const getStrategyTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "jd":
|
||||
return "京东会员"
|
||||
case "yisi":
|
||||
return "易思接口"
|
||||
case "database":
|
||||
return "数据库匹配"
|
||||
case "custom":
|
||||
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">
|
||||
<Tag className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">导入标签</span>
|
||||
</Button>
|
||||
<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">
|
||||
{getStrategyTypeLabel(report.strategyType)}
|
||||
</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.trafficPoolSize} 用户</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">
|
||||
<Database className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">优化用户数</p>
|
||||
<p className="font-semibold">{report.optimizedUsers} 用户</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">
|
||||
<BarChart2 className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">优化比例</p>
|
||||
<p className="font-semibold">
|
||||
{Math.round((report.optimizedUsers / report.trafficPoolSize) * 100)}%
|
||||
</p>
|
||||
</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="segments" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="segments">用户分类</TabsTrigger>
|
||||
<TabsTrigger value="attributes">用户属性</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="segments" 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.userSegments.map((segment, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{segment.name}</span>
|
||||
<span className="font-medium">
|
||||
{segment.count} ({segment.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: `${segment.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">
|
||||
{report.userSegments.map((segment, index) => (
|
||||
<div key={index}>
|
||||
<div className="text-sm text-gray-500">{segment.name}</div>
|
||||
<div className="font-medium">
|
||||
{segment.count} ({segment.percentage}%)
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">优化建议</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{report.recommendations.map((recommendation, index) => (
|
||||
<div key={index} className="flex items-start gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-sm font-medium text-blue-600">{index + 1}</span>
|
||||
</div>
|
||||
<p className="text-gray-700">{recommendation}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attributes" className="space-y-4">
|
||||
{Object.entries(report.userAttributes).map(([category, attributes], categoryIndex) => (
|
||||
<Card key={categoryIndex}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{category}分析</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">{category}分布</h3>
|
||||
<div className="space-y-3">
|
||||
{attributes.map((attribute, index) => (
|
||||
<div key={index}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{attribute.name}</span>
|
||||
<span className="font-medium">
|
||||
{attribute.count} ({attribute.percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full"
|
||||
style={{ width: `${attribute.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-2 text-center mt-2 gap-2">
|
||||
{attributes.map((attribute, index) => (
|
||||
<div key={index}>
|
||||
<div className="text-sm text-gray-500">{attribute.name}</div>
|
||||
<div className="font-medium">
|
||||
{attribute.count} ({attribute.percentage}%)
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</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,235 @@
|
||||
"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 { Smartphone, Database, Calendar, Settings, Tag, Mail } from "lucide-react"
|
||||
import { format } from "date-fns"
|
||||
|
||||
interface ConfirmStrategyProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function ConfirmStrategy({ formData, updateFormData, onSubmit, onBack }: ConfirmStrategyProps) {
|
||||
const [planName, setPlanName] = useState(formData.name || "")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const getStrategyTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "jd":
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-orange-100 flex items-center justify-center flex-shrink-0">
|
||||
<Database className="h-4 w-4 text-orange-600" />
|
||||
</div>
|
||||
)
|
||||
case "yisi":
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
)
|
||||
case "database":
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<Database className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
)
|
||||
case "custom":
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
|
||||
<Settings className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center flex-shrink-0">
|
||||
<Settings className="h-4 w-4 text-gray-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getStrategyTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "jd":
|
||||
return "京东会员"
|
||||
case "yisi":
|
||||
return "易思接口"
|
||||
case "database":
|
||||
return "数据库匹配"
|
||||
case "custom":
|
||||
return "自定义策略"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
updateFormData({
|
||||
name: planName,
|
||||
})
|
||||
|
||||
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.deviceNames.join(", ")}</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">
|
||||
<Database className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">流量池</p>
|
||||
<div className="space-y-1">
|
||||
{formData.trafficPoolNames.map((name: string, index: number) => (
|
||||
<p key={index} className="text-sm text-gray-600">
|
||||
{name}
|
||||
</p>
|
||||
))}
|
||||
<p className="text-sm font-medium">共 {formData.trafficPoolSize} 位用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
{getStrategyTypeIcon(formData.strategyType)}
|
||||
<div>
|
||||
<p className="font-medium">优化策略</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{getStrategyTypeLabel(formData.strategyType)} - {formData.strategyName}
|
||||
</p>
|
||||
|
||||
{formData.strategyType === "jd" && (
|
||||
<div className="mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
会员等级:{" "}
|
||||
{formData.strategyConfig.memberLevel === "all"
|
||||
? "所有会员"
|
||||
: formData.strategyConfig.memberLevel === "plus"
|
||||
? "PLUS会员"
|
||||
: formData.strategyConfig.memberLevel === "vip"
|
||||
? "VIP会员"
|
||||
: "普通会员"}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.strategyType === "yisi" && (
|
||||
<div className="mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
分析深度:{" "}
|
||||
{formData.strategyConfig.analysisDepth === "basic"
|
||||
? "基础分析"
|
||||
: formData.strategyConfig.analysisDepth === "standard"
|
||||
? "标准分析"
|
||||
: "深度分析"}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.strategyType === "database" && (
|
||||
<div className="mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
表名: {formData.strategyConfig.tableName}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs ml-1">
|
||||
匹配字段: {formData.strategyConfig.matchFields}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</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">
|
||||
<Calendar className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">执行时间</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{formData.executionConfig.scheduleType === "immediate"
|
||||
? "提交后立即执行"
|
||||
: `定时执行: ${format(new Date(formData.executionConfig.scheduledTime), "yyyy-MM-dd HH:mm")}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0">
|
||||
<Settings className="h-4 w-4 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">结果处理</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{formData.executionConfig.notifyOnComplete && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
完成后通知
|
||||
</Badge>
|
||||
)}
|
||||
{formData.executionConfig.importTags && (
|
||||
<Badge variant="outline" className="text-xs flex items-center gap-1">
|
||||
<Tag className="h-3 w-3" />
|
||||
导入标签
|
||||
</Badge>
|
||||
)}
|
||||
{formData.executionConfig.sendToWechat && (
|
||||
<Badge variant="outline" className="text-xs flex items-center gap-1">
|
||||
<Mail className="h-3 w-3" />
|
||||
发送到微信: {formData.executionConfig.wechatId}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!planName.trim() || isSubmitting}>
|
||||
{isSubmitting ? "提交中..." : "开始优化"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon, Clock } from "lucide-react"
|
||||
|
||||
interface ExecutionSetupProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function ExecutionSetup({ formData, updateFormData, onNext, onBack }: ExecutionSetupProps) {
|
||||
const [scheduleType, setScheduleType] = useState<string>(formData.executionConfig?.scheduleType || "immediate")
|
||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(
|
||||
formData.executionConfig?.scheduledTime ? new Date(formData.executionConfig.scheduledTime) : undefined,
|
||||
)
|
||||
const [scheduledTime, setScheduledTime] = useState<string>(
|
||||
formData.executionConfig?.scheduledTime
|
||||
? format(new Date(formData.executionConfig.scheduledTime), "HH:mm")
|
||||
: "09:00",
|
||||
)
|
||||
const [notifyOnComplete, setNotifyOnComplete] = useState<boolean>(
|
||||
formData.executionConfig?.notifyOnComplete !== undefined ? formData.executionConfig.notifyOnComplete : true,
|
||||
)
|
||||
const [importTags, setImportTags] = useState<boolean>(
|
||||
formData.executionConfig?.importTags !== undefined ? formData.executionConfig.importTags : true,
|
||||
)
|
||||
const [sendToWechat, setSendToWechat] = useState<boolean>(
|
||||
formData.executionConfig?.sendToWechat !== undefined ? formData.executionConfig.sendToWechat : false,
|
||||
)
|
||||
const [wechatId, setWechatId] = useState<string>(formData.executionConfig?.wechatId || "")
|
||||
|
||||
const handleContinue = () => {
|
||||
let scheduledTimeString = ""
|
||||
|
||||
if (scheduleType === "scheduled" && scheduledDate) {
|
||||
const [hours, minutes] = scheduledTime.split(":").map(Number)
|
||||
const dateObj = new Date(scheduledDate)
|
||||
dateObj.setHours(hours, minutes)
|
||||
scheduledTimeString = dateObj.toISOString()
|
||||
}
|
||||
|
||||
updateFormData({
|
||||
executionConfig: {
|
||||
scheduleType,
|
||||
scheduledTime: scheduledTimeString,
|
||||
notifyOnComplete,
|
||||
importTags,
|
||||
sendToWechat,
|
||||
wechatId: sendToWechat ? wechatId : "",
|
||||
},
|
||||
})
|
||||
|
||||
onNext()
|
||||
}
|
||||
|
||||
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 className="space-y-4">
|
||||
<Label className="text-base font-medium">执行时间</Label>
|
||||
<RadioGroup value={scheduleType} onValueChange={setScheduleType} className="space-y-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="immediate" id="immediate" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="immediate" className="font-medium">
|
||||
立即执行
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">提交后立即开始执行策略优化</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<RadioGroupItem value="scheduled" id="scheduled" />
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="scheduled" className="font-medium">
|
||||
定时执行
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500">在指定的时间执行策略优化</p>
|
||||
|
||||
{scheduleType === "scheduled" && (
|
||||
<div className="flex flex-col sm:flex-row gap-3 mt-2">
|
||||
<div>
|
||||
<Label htmlFor="date" className="text-sm">
|
||||
日期
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start text-left font-normal mt-1" id="date">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{scheduledDate ? format(scheduledDate, "yyyy-MM-dd") : "选择日期"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar mode="single" selected={scheduledDate} onSelect={setScheduledDate} initialFocus />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="time" className="text-sm">
|
||||
时间
|
||||
</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Clock className="mr-2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
id="time"
|
||||
type="time"
|
||||
value={scheduledTime}
|
||||
onChange={(e) => setScheduledTime(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">结果处理</Label>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="notify"
|
||||
checked={notifyOnComplete}
|
||||
onCheckedChange={(checked) => setNotifyOnComplete(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="notify" className="cursor-pointer">
|
||||
完成后通知我
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="import"
|
||||
checked={importTags}
|
||||
onCheckedChange={(checked) => setImportTags(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="import" className="cursor-pointer">
|
||||
将结果导入为流量池标签
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="wechat"
|
||||
checked={sendToWechat}
|
||||
onCheckedChange={(checked) => setSendToWechat(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="wechat" className="cursor-pointer">
|
||||
发送报告到微信
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{sendToWechat && (
|
||||
<div className="pl-6 pt-2">
|
||||
<Label htmlFor="wechatId" className="text-sm">
|
||||
微信号
|
||||
</Label>
|
||||
<Input
|
||||
id="wechatId"
|
||||
value={wechatId}
|
||||
onChange={(e) => setWechatId(e.target.value)}
|
||||
placeholder="输入接收报告的微信号"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={(scheduleType === "scheduled" && !scheduledDate) || (sendToWechat && !wechatId)}
|
||||
>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
interface Step {
|
||||
number: number
|
||||
title: string
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
steps: Step[]
|
||||
currentStep: number
|
||||
}
|
||||
|
||||
export function StepIndicator({ steps, currentStep }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = currentStep > step.number
|
||||
const isCurrent = currentStep === step.number
|
||||
|
||||
return (
|
||||
<div key={step.number} className="flex items-center">
|
||||
{/* 步骤连接线 */}
|
||||
{index > 0 && <div className={`h-1 w-full ${isCompleted ? "bg-blue-500" : "bg-gray-200"}`} />}
|
||||
|
||||
{/* 步骤圆点 */}
|
||||
<div className="relative flex items-center justify-center">
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${
|
||||
isCompleted
|
||||
? "border-blue-500 bg-blue-500"
|
||||
: isCurrent
|
||||
? "border-blue-500 bg-white"
|
||||
: "border-gray-300 bg-white"
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckIcon className="h-5 w-5 text-white" />
|
||||
) : (
|
||||
<span className={`text-sm font-medium ${isCurrent ? "text-blue-500" : "text-gray-500"}`}>
|
||||
{step.number}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 步骤标题 */}
|
||||
<div className="absolute top-10 whitespace-nowrap">
|
||||
<p className={`text-sm font-medium ${isCompleted || isCurrent ? "text-blue-500" : "text-gray-500"}`}>
|
||||
{step.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { ShoppingBag, Database, BarChart, Settings } from "lucide-react"
|
||||
|
||||
interface StrategySelectionProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export function StrategySelection({ formData, updateFormData, onNext, onBack }: StrategySelectionProps) {
|
||||
const [strategyType, setStrategyType] = useState<string>(formData.strategyType || "")
|
||||
const [jdConfig, setJdConfig] = useState({
|
||||
username: formData.strategyConfig?.username || "",
|
||||
password: formData.strategyConfig?.password || "",
|
||||
memberLevel: formData.strategyConfig?.memberLevel || "all",
|
||||
})
|
||||
const [yisiConfig, setYisiConfig] = useState({
|
||||
apiKey: formData.strategyConfig?.apiKey || "",
|
||||
apiEndpoint: formData.strategyConfig?.apiEndpoint || "https://api.yisi.com/user-profile",
|
||||
analysisDepth: formData.strategyConfig?.analysisDepth || "basic",
|
||||
})
|
||||
const [dbConfig, setDbConfig] = useState({
|
||||
connectionString: formData.strategyConfig?.connectionString || "",
|
||||
tableName: formData.strategyConfig?.tableName || "",
|
||||
matchFields: formData.strategyConfig?.matchFields || "phone,wechatId",
|
||||
})
|
||||
const [customConfig, setCustomConfig] = useState({
|
||||
name: formData.strategyConfig?.name || "",
|
||||
description: formData.strategyConfig?.description || "",
|
||||
})
|
||||
|
||||
const getStrategyName = () => {
|
||||
switch (strategyType) {
|
||||
case "jd":
|
||||
return "京东会员识别"
|
||||
case "yisi":
|
||||
return "易思用户画像"
|
||||
case "database":
|
||||
return "数据库客户匹配"
|
||||
case "custom":
|
||||
return customConfig.name || "自定义策略"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
const getStrategyConfig = () => {
|
||||
switch (strategyType) {
|
||||
case "jd":
|
||||
return jdConfig
|
||||
case "yisi":
|
||||
return yisiConfig
|
||||
case "database":
|
||||
return dbConfig
|
||||
case "custom":
|
||||
return customConfig
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
updateFormData({
|
||||
strategyType,
|
||||
strategyName: getStrategyName(),
|
||||
strategyConfig: getStrategyConfig(),
|
||||
})
|
||||
onNext()
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<RadioGroup
|
||||
value={strategyType}
|
||||
onValueChange={setStrategyType}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<RadioGroupItem value="jd" id="jd" className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor="jd"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-blue-500 [&:has([data-state=checked])]:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<ShoppingBag className="mb-3 h-6 w-6 text-orange-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium">京东会员识别</p>
|
||||
<p className="text-sm text-gray-500">识别流量池中的京东会员用户</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RadioGroupItem value="yisi" id="yisi" className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor="yisi"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-blue-500 [&:has([data-state=checked])]:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<BarChart className="mb-3 h-6 w-6 text-blue-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium">易思接口</p>
|
||||
<p className="text-sm text-gray-500">通过易思API分析用户画像</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RadioGroupItem value="database" id="database" className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor="database"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-blue-500 [&:has([data-state=checked])]:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<Database className="mb-3 h-6 w-6 text-green-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium">数据库匹配</p>
|
||||
<p className="text-sm text-gray-500">与现有客户数据库进行匹配</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<RadioGroupItem value="custom" id="custom" className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor="custom"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-blue-500 [&:has([data-state=checked])]:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<Settings className="mb-3 h-6 w-6 text-purple-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium">自定义策略</p>
|
||||
<p className="text-sm text-gray-500">创建自定义的优化策略</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{strategyType === "jd" && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<h3 className="font-medium">京东会员识别配置</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="jd-username">京东账号</Label>
|
||||
<Input
|
||||
id="jd-username"
|
||||
value={jdConfig.username}
|
||||
onChange={(e) => setJdConfig({ ...jdConfig, username: e.target.value })}
|
||||
placeholder="输入京东账号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="jd-password">京东密码</Label>
|
||||
<Input
|
||||
id="jd-password"
|
||||
type="password"
|
||||
value={jdConfig.password}
|
||||
onChange={(e) => setJdConfig({ ...jdConfig, password: e.target.value })}
|
||||
placeholder="输入京东密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="jd-level">会员等级筛选</Label>
|
||||
<select
|
||||
id="jd-level"
|
||||
value={jdConfig.memberLevel}
|
||||
onChange={(e) => setJdConfig({ ...jdConfig, memberLevel: e.target.value })}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background"
|
||||
>
|
||||
<option value="all">所有会员</option>
|
||||
<option value="plus">PLUS会员</option>
|
||||
<option value="vip">VIP会员</option>
|
||||
<option value="regular">普通会员</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{strategyType === "yisi" && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<h3 className="font-medium">易思接口配置</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yisi-apikey">API密钥</Label>
|
||||
<Input
|
||||
id="yisi-apikey"
|
||||
value={yisiConfig.apiKey}
|
||||
onChange={(e) => setYisiConfig({ ...yisiConfig, apiKey: e.target.value })}
|
||||
placeholder="输入易思API密钥"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yisi-endpoint">API端点</Label>
|
||||
<Input
|
||||
id="yisi-endpoint"
|
||||
value={yisiConfig.apiEndpoint}
|
||||
onChange={(e) => setYisiConfig({ ...yisiConfig, apiEndpoint: e.target.value })}
|
||||
placeholder="输入API端点URL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yisi-depth">分析深度</Label>
|
||||
<select
|
||||
id="yisi-depth"
|
||||
value={yisiConfig.analysisDepth}
|
||||
onChange={(e) => setYisiConfig({ ...yisiConfig, analysisDepth: e.target.value })}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background"
|
||||
>
|
||||
<option value="basic">基础分析</option>
|
||||
<option value="standard">标准分析</option>
|
||||
<option value="deep">深度分析</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{strategyType === "database" && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<h3 className="font-medium">数据库匹配配置</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-connection">连接字符串</Label>
|
||||
<Input
|
||||
id="db-connection"
|
||||
value={dbConfig.connectionString}
|
||||
onChange={(e) => setDbConfig({ ...dbConfig, connectionString: e.target.value })}
|
||||
placeholder="输入数据库连接字符串"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-table">表名</Label>
|
||||
<Input
|
||||
id="db-table"
|
||||
value={dbConfig.tableName}
|
||||
onChange={(e) => setDbConfig({ ...dbConfig, tableName: e.target.value })}
|
||||
placeholder="输入表名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-fields">匹配字段</Label>
|
||||
<Input
|
||||
id="db-fields"
|
||||
value={dbConfig.matchFields}
|
||||
onChange={(e) => setDbConfig({ ...dbConfig, matchFields: e.target.value })}
|
||||
placeholder="输入匹配字段,用逗号分隔"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">多个字段用逗号分隔,如:phone,wechatId</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{strategyType === "custom" && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<h3 className="font-medium">自定义策略配置</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-name">策略名称</Label>
|
||||
<Input
|
||||
id="custom-name"
|
||||
value={customConfig.name}
|
||||
onChange={(e) => setCustomConfig({ ...customConfig, name: e.target.value })}
|
||||
placeholder="输入策略名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-desc">策略描述</Label>
|
||||
<textarea
|
||||
id="custom-desc"
|
||||
value={customConfig.description}
|
||||
onChange={(e) => setCustomConfig({ ...customConfig, description: e.target.value })}
|
||||
placeholder="输入策略描述"
|
||||
className="w-full min-h-[100px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={
|
||||
!strategyType ||
|
||||
(strategyType === "jd" && (!jdConfig.username || !jdConfig.password)) ||
|
||||
(strategyType === "yisi" && !yisiConfig.apiKey) ||
|
||||
(strategyType === "database" && (!dbConfig.connectionString || !dbConfig.tableName)) ||
|
||||
(strategyType === "custom" && !customConfig.name)
|
||||
}
|
||||
>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Search, Smartphone, Users } from "lucide-react"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline"
|
||||
}
|
||||
|
||||
interface TrafficPool {
|
||||
id: string
|
||||
name: string
|
||||
deviceId: string
|
||||
deviceName: string
|
||||
type: "wechat" | "device"
|
||||
userCount: number
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
interface TrafficPoolSelectionProps {
|
||||
formData: any
|
||||
updateFormData: (data: any) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function TrafficPoolSelection({ formData, updateFormData, onNext }: TrafficPoolSelectionProps) {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [trafficPools, setTrafficPools] = useState<TrafficPool[]>([])
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>(formData.deviceIds || [])
|
||||
const [selectedPoolIds, setSelectedPoolIds] = useState<string[]>(formData.trafficPoolIds || [])
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState("devices")
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载设备和流量池数据
|
||||
const fetchData = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const mockDevices: Device[] = [
|
||||
{ id: "device-1", name: "iPhone 13 Pro", status: "online" },
|
||||
{ id: "device-2", name: "Xiaomi 12", status: "online" },
|
||||
{ id: "device-3", name: "Huawei P40", status: "offline" },
|
||||
{ id: "device-4", name: "OPPO Find X3", status: "online" },
|
||||
{ id: "device-5", name: "Samsung S21", status: "offline" },
|
||||
]
|
||||
|
||||
const mockTrafficPools: TrafficPool[] = [
|
||||
{
|
||||
id: "pool-1",
|
||||
name: "微信好友池-设备1",
|
||||
deviceId: "device-1",
|
||||
deviceName: "iPhone 13 Pro",
|
||||
type: "wechat",
|
||||
userCount: 156,
|
||||
lastUpdated: "2023-12-15T10:30:00Z",
|
||||
},
|
||||
{
|
||||
id: "pool-2",
|
||||
name: "设备流量池-设备1",
|
||||
deviceId: "device-1",
|
||||
deviceName: "iPhone 13 Pro",
|
||||
type: "device",
|
||||
userCount: 287,
|
||||
lastUpdated: "2023-12-14T15:45:00Z",
|
||||
},
|
||||
{
|
||||
id: "pool-3",
|
||||
name: "微信好友池-设备2",
|
||||
deviceId: "device-2",
|
||||
deviceName: "Xiaomi 12",
|
||||
type: "wechat",
|
||||
userCount: 203,
|
||||
lastUpdated: "2023-12-16T09:15:00Z",
|
||||
},
|
||||
{
|
||||
id: "pool-4",
|
||||
name: "设备流量池-设备3",
|
||||
deviceId: "device-3",
|
||||
deviceName: "Huawei P40",
|
||||
type: "device",
|
||||
userCount: 156,
|
||||
lastUpdated: "2023-12-13T14:20:00Z",
|
||||
},
|
||||
{
|
||||
id: "pool-5",
|
||||
name: "微信好友池-设备4",
|
||||
deviceId: "device-4",
|
||||
deviceName: "OPPO Find X3",
|
||||
type: "wechat",
|
||||
userCount: 178,
|
||||
lastUpdated: "2023-12-12T11:30:00Z",
|
||||
},
|
||||
]
|
||||
|
||||
setDevices(mockDevices)
|
||||
setTrafficPools(mockTrafficPools)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handleDeviceToggle = (deviceId: string) => {
|
||||
setSelectedDeviceIds((prev) => {
|
||||
if (prev.includes(deviceId)) {
|
||||
return prev.filter((id) => id !== deviceId)
|
||||
} else {
|
||||
return [...prev, deviceId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePoolToggle = (poolId: string) => {
|
||||
setSelectedPoolIds((prev) => {
|
||||
if (prev.includes(poolId)) {
|
||||
return prev.filter((id) => id !== poolId)
|
||||
} else {
|
||||
return [...prev, poolId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filteredDevices = devices.filter((device) => device.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const filteredPools = trafficPools.filter(
|
||||
(pool) =>
|
||||
(selectedDeviceIds.length === 0 || selectedDeviceIds.includes(pool.deviceId)) &&
|
||||
pool.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const handleContinue = () => {
|
||||
const selectedPools = trafficPools.filter((pool) => selectedPoolIds.includes(pool.id))
|
||||
|
||||
const totalUserCount = selectedPools.reduce((sum, pool) => sum + pool.userCount, 0)
|
||||
|
||||
updateFormData({
|
||||
deviceIds: selectedDeviceIds,
|
||||
deviceNames: devices.filter((device) => selectedDeviceIds.includes(device.id)).map((device) => device.name),
|
||||
trafficPoolIds: selectedPoolIds,
|
||||
trafficPoolNames: selectedPools.map((pool) => pool.name),
|
||||
trafficPoolSize: totalUserCount,
|
||||
})
|
||||
|
||||
onNext()
|
||||
}
|
||||
|
||||
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 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>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="devices">设备选择</TabsTrigger>
|
||||
<TabsTrigger value="pools">流量池选择</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="devices" className="space-y-4 pt-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : filteredDevices.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{filteredDevices.map((device) => (
|
||||
<Card
|
||||
key={device.id}
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedDeviceIds.includes(device.id) ? "border-blue-500 shadow-sm" : "hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleDeviceToggle(device.id)}
|
||||
>
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
device.status === "online" ? "bg-green-100" : "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Smartphone
|
||||
className={`h-5 w-5 ${device.status === "online" ? "text-green-600" : "text-gray-400"}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{device.name}</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
device.status === "online"
|
||||
? "bg-green-50 text-green-700 border-green-200"
|
||||
: "bg-gray-50 text-gray-700 border-gray-200"
|
||||
}
|
||||
>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selectedDeviceIds.includes(device.id)}
|
||||
onCheckedChange={() => handleDeviceToggle(device.id)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">未找到匹配的设备</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pools" className="space-y-4 pt-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-8 h-8 border-4 border-t-blue-500 border-blue-200 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : filteredPools.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredPools.map((pool) => (
|
||||
<Card
|
||||
key={pool.id}
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedPoolIds.includes(pool.id) ? "border-blue-500 shadow-sm" : "hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handlePoolToggle(pool.id)}
|
||||
>
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
pool.type === "wechat" ? "bg-green-100" : "bg-blue-100"
|
||||
}`}
|
||||
>
|
||||
<Users className={`h-5 w-5 ${pool.type === "wechat" ? "text-green-600" : "text-blue-600"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{pool.name}</p>
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||
<span>设备: {pool.deviceName}</span>
|
||||
<span>•</span>
|
||||
<span>用户数: {pool.userCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={selectedPoolIds.includes(pool.id)}
|
||||
onCheckedChange={() => handlePoolToggle(pool.id)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
{selectedDeviceIds.length > 0 ? "所选设备没有可用的流量池" : "请先选择设备或搜索流量池"}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
已选择 {selectedDeviceIds.length} 个设备, {selectedPoolIds.length} 个流量池
|
||||
</div>
|
||||
<Button onClick={handleContinue} disabled={selectedPoolIds.length === 0}>
|
||||
继续
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
129
Cunkebao/app/workspace/ai-strategy/new/page.tsx
Normal file
129
Cunkebao/app/workspace/ai-strategy/new/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"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 { TrafficPoolSelection } from "../components/traffic-pool-selection"
|
||||
import { StrategySelection } from "../components/strategy-selection"
|
||||
import { ExecutionSetup } from "../components/execution-setup"
|
||||
import { ConfirmStrategy } from "../components/confirm-strategy"
|
||||
|
||||
export default function NewStrategyPage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
deviceIds: [] as string[],
|
||||
deviceNames: [] as string[],
|
||||
trafficPoolIds: [] as string[],
|
||||
trafficPoolNames: [] as string[],
|
||||
trafficPoolSize: 0,
|
||||
strategyType: "",
|
||||
strategyName: "",
|
||||
strategyConfig: {} as Record<string, any>,
|
||||
executionConfig: {
|
||||
scheduleType: "immediate", // "immediate", "scheduled"
|
||||
scheduledTime: "",
|
||||
notifyOnComplete: true,
|
||||
importTags: true,
|
||||
sendToWechat: false,
|
||||
wechatId: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep === 1) {
|
||||
router.back()
|
||||
} else {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 模拟提交
|
||||
console.log("提交策略优化:", formData)
|
||||
|
||||
// 模拟加载
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 跳转到列表页
|
||||
router.push("/workspace/ai-strategy")
|
||||
}
|
||||
|
||||
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: "选择优化策略" },
|
||||
{ number: 3, title: "执行设置" },
|
||||
{ number: 4, title: "确认策略" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="p-6">
|
||||
{currentStep === 1 && (
|
||||
<TrafficPoolSelection formData={formData} updateFormData={updateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<StrategySelection
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<ExecutionSetup
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 4 && (
|
||||
<ConfirmStrategy
|
||||
formData={formData}
|
||||
updateFormData={updateFormData}
|
||||
onSubmit={handleSubmit}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
371
Cunkebao/app/workspace/ai-strategy/page.tsx
Normal file
371
Cunkebao/app/workspace/ai-strategy/page.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
"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, Tag } 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 StrategyPlan {
|
||||
id: string
|
||||
name: string
|
||||
deviceIds: string[]
|
||||
deviceNames: string[]
|
||||
status: "running" | "completed" | "failed"
|
||||
createdAt: string
|
||||
completedAt?: string
|
||||
strategyType: "jd" | "yisi" | "database" | "custom"
|
||||
strategyName: string
|
||||
trafficPoolSize: number
|
||||
optimizedUsers: number
|
||||
}
|
||||
|
||||
export default function AIStrategyPage() {
|
||||
const router = useRouter()
|
||||
const [plans, setPlans] = useState<StrategyPlan[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟加载数据
|
||||
const fetchPlans = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const mockPlans: StrategyPlan[] = [
|
||||
{
|
||||
id: "plan-1",
|
||||
name: "京东会员优化",
|
||||
deviceIds: ["device-1", "device-2"],
|
||||
deviceNames: ["设备1", "设备2"],
|
||||
status: "completed",
|
||||
createdAt: "2023-12-15T10:30:00Z",
|
||||
completedAt: "2023-12-15T11:45:00Z",
|
||||
strategyType: "jd",
|
||||
strategyName: "京东会员识别",
|
||||
trafficPoolSize: 287,
|
||||
optimizedUsers: 142,
|
||||
},
|
||||
{
|
||||
id: "plan-2",
|
||||
name: "易思API用户分析",
|
||||
deviceIds: ["device-3"],
|
||||
deviceNames: ["设备3"],
|
||||
status: "running",
|
||||
createdAt: "2023-12-16T09:15:00Z",
|
||||
strategyType: "yisi",
|
||||
strategyName: "易思用户画像",
|
||||
trafficPoolSize: 156,
|
||||
optimizedUsers: 0,
|
||||
},
|
||||
{
|
||||
id: "plan-3",
|
||||
name: "数据库客户匹配",
|
||||
deviceIds: ["device-1", "device-4"],
|
||||
deviceNames: ["设备1", "设备4"],
|
||||
status: "completed",
|
||||
createdAt: "2023-12-14T14:20:00Z",
|
||||
completedAt: "2023-12-14T16:10:00Z",
|
||||
strategyType: "database",
|
||||
strategyName: "CRM客户匹配",
|
||||
trafficPoolSize: 423,
|
||||
optimizedUsers: 215,
|
||||
},
|
||||
]
|
||||
|
||||
setPlans(mockPlans)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
fetchPlans()
|
||||
}, [])
|
||||
|
||||
const getStatusBadge = (status: StrategyPlan["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 getStrategyLabel = (type: StrategyPlan["strategyType"]) => {
|
||||
switch (type) {
|
||||
case "jd":
|
||||
return "京东会员"
|
||||
case "yisi":
|
||||
return "易思接口"
|
||||
case "database":
|
||||
return "数据库匹配"
|
||||
case "custom":
|
||||
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-strategy/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.deviceNames.join(", ")}</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">{getStrategyLabel(plan.strategyType)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">策略名称</span>
|
||||
<span className="font-medium">{plan.strategyName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">流量池大小</span>
|
||||
<span className="font-medium">{plan.trafficPoolSize} 用户</span>
|
||||
</div>
|
||||
{plan.status === "completed" && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">优化用户数</span>
|
||||
<span className="font-medium">{plan.optimizedUsers} 用户</span>
|
||||
</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">
|
||||
<Tag className="h-4 w-4" />
|
||||
<span>导入标签</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>发送报告</span>
|
||||
</Button>
|
||||
<Link href={`/workspace/ai-strategy/${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-strategy/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.deviceNames.join(", ")}</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">{getStrategyLabel(plan.strategyType)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">策略名称</span>
|
||||
<span className="font-medium">{plan.strategyName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">流量池大小</span>
|
||||
<span className="font-medium">{plan.trafficPoolSize} 用户</span>
|
||||
</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.deviceNames.join(", ")}</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">{getStrategyLabel(plan.strategyType)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">策略名称</span>
|
||||
<span className="font-medium">{plan.strategyName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">流量池大小</span>
|
||||
<span className="font-medium">{plan.trafficPoolSize} 用户</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">优化用户数</span>
|
||||
<span className="font-medium">{plan.optimizedUsers} 用户</span>
|
||||
</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-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">
|
||||
<Tag className="h-4 w-4" />
|
||||
<span>导入标签</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex items-center gap-1">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>发送报告</span>
|
||||
</Button>
|
||||
<Link href={`/workspace/ai-strategy/${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