【操盘手端】自动点赞提交
This commit is contained in:
3
Cunkebao/app/workspace/auto-group/loading.tsx
Normal file
3
Cunkebao/app/workspace/auto-group/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PlusCircle, ArrowLeft } from "lucide-react"
|
||||
import { StepIndicator } from "./components/step-indicator"
|
||||
import { GroupSettings } from "./components/group-settings"
|
||||
import { DeviceSelection } from "./components/device-selection"
|
||||
import { TagSelection } from "./components/tag-selection"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
// 保留原有的卡片列表视图
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { ChevronLeft, Plus, Filter, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Users, Settings, RefreshCcw } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Users, Settings } from "lucide-react"
|
||||
|
||||
interface Plan {
|
||||
id: string
|
||||
@@ -49,97 +46,30 @@ const mockPlans: Plan[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const steps = [
|
||||
{ title: "群配置", description: "设置群人数与组数" },
|
||||
{ title: "设备选择", description: "选择执行设备" },
|
||||
{ title: "人群标签", description: "选择目标人群" },
|
||||
]
|
||||
|
||||
export default function AutoGroupPage() {
|
||||
const router = useRouter()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const { toast } = useToast()
|
||||
const [plans, setPlans] = useState(mockPlans)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "新建群计划",
|
||||
fixedWechatIds: [] as string[],
|
||||
groupingOption: "all" as "all" | "fixed",
|
||||
fixedGroupCount: 5,
|
||||
selectedDevices: [] as string[],
|
||||
audienceTags: [] as string[],
|
||||
trafficTags: [] as string[],
|
||||
matchLogic: "or" as "and" | "or",
|
||||
excludeTags: ["已拉群"] as string[],
|
||||
})
|
||||
const handleDelete = (planId: string) => {
|
||||
setPlans(plans.filter((plan) => plan.id !== planId))
|
||||
}
|
||||
|
||||
const handleStepClick = useCallback((step: number) => {
|
||||
setCurrentStep(step)
|
||||
}, [])
|
||||
const handleEdit = (planId: string) => {
|
||||
router.push(`/workspace/auto-group/${planId}/edit`)
|
||||
}
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}, [])
|
||||
const handleView = (planId: string) => {
|
||||
router.push(`/workspace/auto-group/${planId}`)
|
||||
}
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}, [])
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
// 这里可以添加表单提交逻辑
|
||||
console.log("Form submitted:", formData)
|
||||
toast({
|
||||
title: "计划创建成功",
|
||||
description: `已成功创建"${formData.name}"计划`,
|
||||
})
|
||||
setIsCreating(false)
|
||||
setCurrentStep(0)
|
||||
// 重置表单数据
|
||||
setFormData({
|
||||
name: "新建群计划",
|
||||
fixedWechatIds: [],
|
||||
groupingOption: "all",
|
||||
fixedGroupCount: 5,
|
||||
selectedDevices: [],
|
||||
audienceTags: [],
|
||||
trafficTags: [],
|
||||
matchLogic: "or",
|
||||
excludeTags: ["已拉群"],
|
||||
})
|
||||
}, [formData, toast])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setIsCreating(false)
|
||||
setCurrentStep(0)
|
||||
}, [])
|
||||
|
||||
// 使用useCallback包装回调函数,避免不必要的重新创建
|
||||
const handleGroupSettingsChange = useCallback(
|
||||
(values: {
|
||||
name: string
|
||||
fixedWechatIds: string[]
|
||||
groupingOption: "all" | "fixed"
|
||||
fixedGroupCount: number
|
||||
}) => {
|
||||
setFormData((prev) => ({ ...prev, ...values }))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const handleDevicesChange = useCallback((devices: string[]) => {
|
||||
setFormData((prev) => ({ ...prev, selectedDevices: devices }))
|
||||
}, [])
|
||||
|
||||
const handleTagsChange = useCallback(
|
||||
(values: {
|
||||
audienceTags: string[]
|
||||
trafficTags: string[]
|
||||
matchLogic: "and" | "or"
|
||||
excludeTags: string[]
|
||||
}) => {
|
||||
setFormData((prev) => ({ ...prev, ...values }))
|
||||
},
|
||||
[],
|
||||
)
|
||||
const togglePlanStatus = (planId: string) => {
|
||||
setPlans(
|
||||
plans.map((plan) =>
|
||||
plan.id === planId ? { ...plan, status: plan.status === "running" ? "stopped" : "running" } : plan,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const getStatusColor = (status: Plan["status"]) => {
|
||||
switch (status) {
|
||||
@@ -167,129 +97,114 @@ export default function AutoGroupPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (isCreating) {
|
||||
return (
|
||||
<div className="container p-4 mx-auto max-w-7xl">
|
||||
<div className="flex items-center mb-6">
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel} className="mr-2">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
返回
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">新建自动拉群计划</h1>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<StepIndicator currentStep={currentStep} steps={steps} onStepClick={handleStepClick} />
|
||||
</div>
|
||||
|
||||
{currentStep === 0 && (
|
||||
<GroupSettings
|
||||
onNext={handleNext}
|
||||
initialValues={{
|
||||
name: formData.name,
|
||||
fixedWechatIds: formData.fixedWechatIds,
|
||||
groupingOption: formData.groupingOption,
|
||||
fixedGroupCount: formData.fixedGroupCount,
|
||||
}}
|
||||
onValuesChange={handleGroupSettingsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<DeviceSelection
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
initialSelectedDevices={formData.selectedDevices}
|
||||
onDevicesChange={handleDevicesChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<TagSelection
|
||||
onPrevious={handlePrevious}
|
||||
onComplete={handleComplete}
|
||||
initialValues={{
|
||||
audienceTags: formData.audienceTags,
|
||||
trafficTags: formData.trafficTags,
|
||||
matchLogic: formData.matchLogic,
|
||||
excludeTags: formData.excludeTags,
|
||||
}}
|
||||
onValuesChange={handleTagsChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container p-4 mx-auto max-w-7xl">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">微信自动拉群</h1>
|
||||
<Button onClick={() => setIsCreating(true)} className="bg-blue-500 hover:bg-blue-600">
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
新建计划
|
||||
</Button>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">自动建群、自动进群</h1>
|
||||
</div>
|
||||
<Link href="/workspace/auto-group/new">
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Tabs defaultValue="active" className="w-full">
|
||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||
<TabsTrigger value="active">进行中</TabsTrigger>
|
||||
<TabsTrigger value="completed">已完成</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="p-4">
|
||||
<Card className="p-4 mb-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="搜索任务名称" className="pl-9" />
|
||||
</div>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<TabsContent value="active" className="mt-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{mockPlans.map((plan) => (
|
||||
<Card key={plan.id} className="border border-gray-100">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-medium">{plan.name}</CardTitle>
|
||||
<Badge className={getStatusColor(plan.status)}>{getStatusText(plan.status)}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
已建群数:{plan.groupCount}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
群规模:{plan.groupSize}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="w-4 h-4 mr-2" />
|
||||
更新时间:{plan.lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<div className="space-y-4">
|
||||
{plans.map((plan) => (
|
||||
<Card key={plan.id} className="p-4 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-blue-100 to-blue-50 rounded-bl-full -z-10"></div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{plan.name}</h3>
|
||||
<Badge variant="outline" className={getStatusColor(plan.status)}>
|
||||
{getStatusText(plan.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={plan.status === "running"} onCheckedChange={() => togglePlanStatus(plan.id)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => handleView(plan.id)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(plan.id)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(plan.id)}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
已建群数:{plan.groupCount}
|
||||
</div>
|
||||
<div className="flex items-center mt-1">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
群规模:{plan.groupSize}人/群
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>总人数:{plan.totalFriends}人</div>
|
||||
<div className="mt-1">
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{plan.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" className="bg-red-500 hover:bg-red-600">
|
||||
停止
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="completed">
|
||||
<div className="h-[calc(100vh-200px)] flex items-center justify-center text-gray-500">暂无已完成的计划</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
更新时间:{plan.lastUpdated}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Search, Plus, Trash2, LucideTag, Users } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
interface TagGroup {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: "profession" | "interest" | "age" | "consumption" | "interaction" | "custom"
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
interface UserProfile {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
tags: string[]
|
||||
profession?: string
|
||||
interest?: string
|
||||
region?: string
|
||||
lastActive?: string
|
||||
}
|
||||
import { Check, Plus, Tag, X } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export interface AudienceTagsData {
|
||||
selectedTags: string[]
|
||||
@@ -53,457 +15,147 @@ export interface AudienceTagsData {
|
||||
}
|
||||
|
||||
interface AudienceTagsProps {
|
||||
initialData?: Partial<AudienceTagsData>
|
||||
initialData: AudienceTagsData
|
||||
onSave: (data: AudienceTagsData) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
// 模拟标签数据
|
||||
const predefinedTags = [
|
||||
"高意向",
|
||||
"中意向",
|
||||
"低意向",
|
||||
"新客户",
|
||||
"老客户",
|
||||
"VIP客户",
|
||||
"男性",
|
||||
"女性",
|
||||
"年轻人",
|
||||
"中年人",
|
||||
"老年人",
|
||||
"城市",
|
||||
"农村",
|
||||
"高收入",
|
||||
"中等收入",
|
||||
"低收入",
|
||||
]
|
||||
|
||||
export function AudienceTags({ initialData, onSave, onBack }: AudienceTagsProps) {
|
||||
const [tagGroups, setTagGroups] = useState<TagGroup[]>([])
|
||||
const [users, setUsers] = useState<UserProfile[]>([])
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>(initialData?.selectedTags || [])
|
||||
const [tagOperator, setTagOperator] = useState<"and" | "or">(initialData?.tagOperator || "or")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
const [newTagName, setNewTagName] = useState("")
|
||||
const [newTagDescription, setNewTagDescription] = useState("")
|
||||
const [newTagType, setNewTagType] = useState<TagGroup["type"]>("custom")
|
||||
const [isCreateTagDialogOpen, setIsCreateTagDialogOpen] = useState(false)
|
||||
const [formData, setFormData] = useState<AudienceTagsData>(initialData)
|
||||
const [newTag, setNewTag] = useState("")
|
||||
|
||||
// 模拟获取标签组和用户数据
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
const toggleTag = (tag: string) => {
|
||||
const newSelectedTags = formData.selectedTags.includes(tag)
|
||||
? formData.selectedTags.filter((t) => t !== tag)
|
||||
: [...formData.selectedTags, tag]
|
||||
|
||||
// 模拟标签组数据
|
||||
const mockTagGroups: TagGroup[] = [
|
||||
{
|
||||
id: "profession",
|
||||
name: "职业",
|
||||
description: "按照好友的职业分类",
|
||||
type: "profession",
|
||||
tags: [
|
||||
{ id: "teacher", name: "教师", count: 15 },
|
||||
{ id: "doctor", name: "医生", count: 8 },
|
||||
{ id: "engineer", name: "工程师", count: 22 },
|
||||
{ id: "business", name: "企业白领", count: 30 },
|
||||
{ id: "freelancer", name: "自由职业", count: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "interest",
|
||||
name: "兴趣爱好",
|
||||
description: "按照好友的兴趣爱好分类",
|
||||
type: "interest",
|
||||
tags: [
|
||||
{ id: "photography", name: "摄影爱好者", count: 18 },
|
||||
{ id: "sports", name: "运动达人", count: 25 },
|
||||
{ id: "food", name: "美食爱好者", count: 32 },
|
||||
{ id: "travel", name: "旅行达人", count: 20 },
|
||||
{ id: "tech", name: "科技发烧友", count: 15 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "age",
|
||||
name: "年龄范围",
|
||||
description: "按照好友的年龄范围分类",
|
||||
type: "age",
|
||||
tags: [
|
||||
{ id: "18-25", name: "18-25岁", count: 22 },
|
||||
{ id: "26-35", name: "26-35岁", count: 45 },
|
||||
{ id: "36-45", name: "36-45岁", count: 30 },
|
||||
{ id: "46-55", name: "46-55岁", count: 15 },
|
||||
{ id: "56+", name: "56岁以上", count: 8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "consumption",
|
||||
name: "消费能力",
|
||||
description: "按照好友的消费能力分类",
|
||||
type: "consumption",
|
||||
tags: [
|
||||
{ id: "high", name: "高消费", count: 12 },
|
||||
{ id: "medium", name: "中等消费", count: 48 },
|
||||
{ id: "low", name: "低消费", count: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "interaction",
|
||||
name: "互动频率",
|
||||
description: "按照与好友的互动频率分类",
|
||||
type: "interaction",
|
||||
tags: [
|
||||
{ id: "high-interaction", name: "高频互动", count: 15 },
|
||||
{ id: "medium-interaction", name: "中频互动", count: 35 },
|
||||
{ id: "low-interaction", name: "低频互动", count: 40 },
|
||||
{ id: "new-friend", name: "近期新添加", count: 10 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "custom",
|
||||
name: "自定义标签",
|
||||
description: "自定义创建的标签",
|
||||
type: "custom",
|
||||
tags: [
|
||||
{ id: "potential-customer", name: "潜在客户", count: 28 },
|
||||
{ id: "vip", name: "VIP客户", count: 10 },
|
||||
{ id: "partner", name: "合作伙伴", count: 5 },
|
||||
],
|
||||
},
|
||||
]
|
||||
setFormData({ ...formData, selectedTags: newSelectedTags })
|
||||
}
|
||||
|
||||
setTagGroups(mockTagGroups)
|
||||
|
||||
// 模拟用户数据
|
||||
const mockUsers: UserProfile[] = Array.from({ length: 50 }, (_, i) => {
|
||||
const professionTag = mockTagGroups[0].tags[Math.floor(Math.random() * mockTagGroups[0].tags.length)]
|
||||
const interestTag = mockTagGroups[1].tags[Math.floor(Math.random() * mockTagGroups[1].tags.length)]
|
||||
const ageTag = mockTagGroups[2].tags[Math.floor(Math.random() * mockTagGroups[2].tags.length)]
|
||||
const consumptionTag = mockTagGroups[3].tags[Math.floor(Math.random() * mockTagGroups[3].tags.length)]
|
||||
const interactionTag = mockTagGroups[4].tags[Math.floor(Math.random() * mockTagGroups[4].tags.length)]
|
||||
|
||||
// 随机选择一些标签
|
||||
const userTags = [
|
||||
professionTag.id,
|
||||
interestTag.id,
|
||||
Math.random() > 0.5 ? ageTag.id : null,
|
||||
Math.random() > 0.5 ? consumptionTag.id : null,
|
||||
Math.random() > 0.5 ? interactionTag.id : null,
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
// 随机添加一些自定义标签
|
||||
if (Math.random() > 0.7) {
|
||||
const customTag = mockTagGroups[5].tags[Math.floor(Math.random() * mockTagGroups[5].tags.length)]
|
||||
userTags.push(customTag.id)
|
||||
}
|
||||
|
||||
return {
|
||||
id: `user-${i + 1}`,
|
||||
name: `用户${i + 1}`,
|
||||
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`,
|
||||
tags: userTags,
|
||||
profession: professionTag.name,
|
||||
interest: interestTag.name,
|
||||
region: ["北京", "上海", "广州", "深圳", "杭州"][Math.floor(Math.random() * 5)],
|
||||
lastActive: `${Math.floor(Math.random() * 24)}小时前`,
|
||||
}
|
||||
const addCustomTag = () => {
|
||||
if (newTag.trim() && !predefinedTags.includes(newTag) && !formData.selectedTags.includes(newTag)) {
|
||||
setFormData({
|
||||
...formData,
|
||||
selectedTags: [...formData.selectedTags, newTag.trim()],
|
||||
})
|
||||
|
||||
setUsers(mockUsers)
|
||||
setNewTag("")
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
// 获取所有标签
|
||||
const allTags = tagGroups.flatMap((group) => group.tags)
|
||||
|
||||
// 根据选中的标签过滤用户
|
||||
const filteredUsers = users.filter((user) => {
|
||||
if (selectedTags.length === 0) return true
|
||||
|
||||
if (tagOperator === "and") {
|
||||
return selectedTags.every((tagId) => user.tags.includes(tagId))
|
||||
} else {
|
||||
return selectedTags.some((tagId) => user.tags.includes(tagId))
|
||||
}
|
||||
})
|
||||
|
||||
// 根据搜索查询过滤标签
|
||||
const filteredTagGroups = tagGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
tags: group.tags.filter((tag) => tag.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
}))
|
||||
.filter((group) => group.tags.length > 0)
|
||||
|
||||
// 根据标签类型过滤标签组
|
||||
const tabFilteredTagGroups =
|
||||
activeTab === "all" ? filteredTagGroups : filteredTagGroups.filter((group) => group.id === activeTab)
|
||||
|
||||
// 切换标签选择
|
||||
const toggleTag = (tagId: string) => {
|
||||
setSelectedTags(selectedTags.includes(tagId) ? selectedTags.filter((id) => id !== tagId) : [...selectedTags, tagId])
|
||||
}
|
||||
|
||||
// 创建新标签
|
||||
const handleCreateTag = () => {
|
||||
if (newTagName.trim()) {
|
||||
const newTag: Tag = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: newTagName.trim(),
|
||||
count: 0,
|
||||
}
|
||||
|
||||
setTagGroups(
|
||||
tagGroups.map((group) => (group.id === "custom" ? { ...group, tags: [...group.tags, newTag] } : group)),
|
||||
)
|
||||
|
||||
setNewTagName("")
|
||||
setNewTagDescription("")
|
||||
setIsCreateTagDialogOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存选择的标签
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
selectedTags,
|
||||
tagOperator,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>指定点赞的人群标签</CardTitle>
|
||||
<CardDescription>选择特定标签,只对带有这些标签的好友朋友圈进行点赞</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* 标签选择逻辑 */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<Label className="font-medium">标签匹配逻辑:</Label>
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="text-base font-medium">筛选标签</Label>
|
||||
<p className="text-sm text-muted-foreground mb-4">选择需要点赞的人群标签</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{predefinedTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={formData.selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer py-1 px-3"
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
{formData.selectedTags.includes(tag) && <Check className="h-3 w-3 mr-1" />}
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<div className="relative flex-1">
|
||||
<Tag className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
className="pl-9"
|
||||
placeholder="添加自定义标签"
|
||||
onKeyDown={(e) => e.key === "Enter" && addCustomTag()}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={addCustomTag} disabled={!newTag.trim()}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">标签匹配逻辑</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择多个标签之间的匹配关系</p>
|
||||
|
||||
<RadioGroup
|
||||
value={tagOperator}
|
||||
onValueChange={(value) => setTagOperator(value as "and" | "or")}
|
||||
className="flex space-x-4"
|
||||
value={formData.tagOperator}
|
||||
onValueChange={(value) => setFormData({ ...formData, tagOperator: value as "and" | "or" })}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="and" id="and" />
|
||||
<Label htmlFor="and">同时满足所有标签</Label>
|
||||
<RadioGroupItem value="and" id="and-operator" />
|
||||
<Label htmlFor="and-operator" className="font-normal">
|
||||
所有标签都必须匹配(AND)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="or" id="or" />
|
||||
<Label htmlFor="or">满足任一标签</Label>
|
||||
<RadioGroupItem value="or" id="or-operator" />
|
||||
<Label htmlFor="or-operator" className="font-normal">
|
||||
匹配任意一个标签即可(OR)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 已选标签展示 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">已选标签:</Label>
|
||||
<div className="flex flex-wrap gap-2 min-h-10 p-2 border rounded-md">
|
||||
{selectedTags.length === 0 ? (
|
||||
<span className="text-sm text-muted-foreground">未选择任何标签,将对所有好友点赞</span>
|
||||
<div>
|
||||
<Label className="text-base font-medium">已选择的标签</Label>
|
||||
<div className="mt-2 min-h-[60px] border rounded-md p-3">
|
||||
{formData.selectedTags.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">未选择任何标签</p>
|
||||
) : (
|
||||
selectedTags.map((tagId) => {
|
||||
const tag = allTags.find((t) => t.id === tagId)
|
||||
return tag ? (
|
||||
<Badge key={tagId} className="flex items-center gap-1">
|
||||
{tag.name}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
||||
onClick={() => toggleTag(tagId)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span className="sr-only">Remove</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.selectedTags.map((tag) => (
|
||||
<Badge key={tag} className="flex items-center gap-1 py-1 px-2">
|
||||
{tag}
|
||||
<Button variant="ghost" size="icon" className="h-4 w-4 p-0 ml-1" onClick={() => toggleTag(tag)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
) : null
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标签搜索和分类 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索标签"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Dialog open={isCreateTagDialogOpen} onOpenChange={setIsCreateTagDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="ml-2">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
创建标签
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建新标签</DialogTitle>
|
||||
<DialogDescription>创建一个新的标签来分类您的好友</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tagName">标签名称</Label>
|
||||
<Input
|
||||
id="tagName"
|
||||
placeholder="输入标签名称"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tagDescription">标签描述(可选)</Label>
|
||||
<Textarea
|
||||
id="tagDescription"
|
||||
placeholder="输入标签描述"
|
||||
value={newTagDescription}
|
||||
onChange={(e) => setNewTagDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tagType">标签类型</Label>
|
||||
<RadioGroup
|
||||
value={newTagType}
|
||||
onValueChange={(value) => setNewTagType(value as TagGroup["type"])}
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="profession" id="profession" />
|
||||
<Label htmlFor="profession">职业</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="interest" id="interest" />
|
||||
<Label htmlFor="interest">兴趣爱好</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="age" id="age" />
|
||||
<Label htmlFor="age">年龄范围</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="consumption" id="consumption" />
|
||||
<Label htmlFor="consumption">消费能力</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="interaction" id="interaction" />
|
||||
<Label htmlFor="interaction">互动频率</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="custom" id="custom" />
|
||||
<Label htmlFor="custom">自定义</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsCreateTagDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleCreateTag} disabled={!newTagName.trim()}>
|
||||
创建
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid grid-cols-7">
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="profession">职业</TabsTrigger>
|
||||
<TabsTrigger value="interest">兴趣</TabsTrigger>
|
||||
<TabsTrigger value="age">年龄</TabsTrigger>
|
||||
<TabsTrigger value="consumption">消费</TabsTrigger>
|
||||
<TabsTrigger value="interaction">互动</TabsTrigger>
|
||||
<TabsTrigger value="custom">自定义</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-6">
|
||||
{tabFilteredTagGroups.map((group) => (
|
||||
<div key={group.id} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center">
|
||||
<LucideTag className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
{group.name}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">{group.description}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag.id}
|
||||
variant={selectedTags.includes(tag.id) ? "default" : "outline"}
|
||||
className="cursor-pointer flex items-center gap-1"
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
>
|
||||
{tag.name}
|
||||
<span className="text-xs opacity-70">({tag.count})</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{tabFilteredTagGroups.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">未找到符合条件的标签</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预览匹配的用户 */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center">
|
||||
<Users className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
匹配的好友预览
|
||||
</h3>
|
||||
<Badge variant="outline">共 {filteredUsers.length} 人</Badge>
|
||||
</div>
|
||||
<ScrollArea className="h-64 border rounded-md">
|
||||
<div className="p-2 space-y-2">
|
||||
{filteredUsers.slice(0, 20).map((user) => (
|
||||
<div key={user.id} className="flex items-center justify-between p-2 rounded-md hover:bg-muted/50">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback>{user.name.substring(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{user.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{user.profession} · {user.region} · 最近活跃: {user.lastActive}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px] justify-end">
|
||||
{user.tags.slice(0, 2).map((tagId) => {
|
||||
const tag = allTags.find((t) => t.id === tagId)
|
||||
return tag ? (
|
||||
<Badge key={tagId} variant="outline" className="text-xs">
|
||||
{tag.name}
|
||||
</Badge>
|
||||
) : null
|
||||
})}
|
||||
{user.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{user.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="text-center py-4 text-muted-foreground">未找到匹配的好友</div>
|
||||
)}
|
||||
{filteredUsers.length > 20 && (
|
||||
<div className="text-center py-2 text-muted-foreground text-sm">
|
||||
显示前 20 位好友,共 {filteredUsers.length} 位匹配
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Button variant="outline" className="flex-1" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button className="flex-1" onClick={() => onSave(formData)} disabled={formData.selectedTags.length === 0}>
|
||||
完成设置
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回上一步
|
||||
</Button>
|
||||
<Button onClick={handleSave}>完成设置</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
201
Cunkebao/app/workspace/auto-like/components/basic-settings.tsx
Normal file
201
Cunkebao/app/workspace/auto-like/components/basic-settings.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
"use client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Minus, Plus } from "lucide-react"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
taskName: string
|
||||
likeInterval: number
|
||||
maxLikesPerDay: number
|
||||
timeRange: { start: string; end: string }
|
||||
contentTypes: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
onChange: (data: Partial<BasicSettingsProps["formData"]>) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||
const handleContentTypeChange = (type: string) => {
|
||||
const currentTypes = [...formData.contentTypes]
|
||||
if (currentTypes.includes(type)) {
|
||||
onChange({ contentTypes: currentTypes.filter((t) => t !== type) })
|
||||
} else {
|
||||
onChange({ contentTypes: [...currentTypes, type] })
|
||||
}
|
||||
}
|
||||
|
||||
const incrementInterval = () => {
|
||||
onChange({ likeInterval: Math.min(formData.likeInterval + 5, 60) })
|
||||
}
|
||||
|
||||
const decrementInterval = () => {
|
||||
onChange({ likeInterval: Math.max(formData.likeInterval - 5, 5) })
|
||||
}
|
||||
|
||||
const incrementMaxLikes = () => {
|
||||
onChange({ maxLikesPerDay: Math.min(formData.maxLikesPerDay + 10, 200) })
|
||||
}
|
||||
|
||||
const decrementMaxLikes = () => {
|
||||
onChange({ maxLikesPerDay: Math.max(formData.maxLikesPerDay - 10, 10) })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="task-name">任务名称</Label>
|
||||
<Input
|
||||
id="task-name"
|
||||
placeholder="请输入任务名称"
|
||||
value={formData.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="like-interval">点赞间隔</Label>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={decrementInterval}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="like-interval"
|
||||
type="number"
|
||||
min={5}
|
||||
max={60}
|
||||
value={formData.likeInterval}
|
||||
onChange={(e) => onChange({ likeInterval: Number.parseInt(e.target.value) || 5 })}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
|
||||
分钟
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={incrementInterval}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置两次点赞之间的最小时间间隔</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max-likes">每日最大点赞数</Label>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-l-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={decrementMaxLikes}
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="max-likes"
|
||||
type="number"
|
||||
min={10}
|
||||
max={200}
|
||||
value={formData.maxLikesPerDay}
|
||||
onChange={(e) => onChange({ maxLikesPerDay: Number.parseInt(e.target.value) || 10 })}
|
||||
className="h-12 rounded-none border-x-0 border-gray-200 text-center"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
|
||||
次/天
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-12 w-12 rounded-r-xl border-gray-200 bg-white hover:bg-gray-50"
|
||||
onClick={incrementMaxLikes}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每天最多点赞的次数</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>点赞时间范围</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.timeRange.start}
|
||||
onChange={(e) => onChange({ timeRange: { ...formData.timeRange, start: e.target.value } })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.timeRange.end}
|
||||
onChange={(e) => onChange({ timeRange: { ...formData.timeRange, end: e.target.value } })}
|
||||
className="h-12 rounded-xl border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">设置每天可以点赞的时间段</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>点赞内容类型</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ id: "text", label: "文字" },
|
||||
{ id: "image", label: "图片" },
|
||||
{ id: "video", label: "视频" },
|
||||
].map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`flex items-center justify-center h-12 rounded-xl border cursor-pointer ${
|
||||
formData.contentTypes.includes(type.id)
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 text-gray-600"
|
||||
}`}
|
||||
onClick={() => handleContentTypeChange(type.id)}
|
||||
>
|
||||
{type.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">选择要点赞的内容类型</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<Label htmlFor="auto-enabled" className="cursor-pointer">
|
||||
自动开启
|
||||
</Label>
|
||||
<Switch
|
||||
id="auto-enabled"
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={onNext} className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm">
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, RefreshCw, Loader2 } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { api } from "@/lib/api"
|
||||
|
||||
interface ServerDevice {
|
||||
id: number
|
||||
imei: string
|
||||
memo: string
|
||||
wechatId: string
|
||||
alive: number
|
||||
totalFriend: number
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: number
|
||||
name: string
|
||||
imei: string
|
||||
wxid: string
|
||||
status: "online" | "offline"
|
||||
totalFriend: number
|
||||
}
|
||||
|
||||
interface DeviceSelectionDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
selectedDevices: number[]
|
||||
onSelect: (devices: number[]) => void
|
||||
}
|
||||
|
||||
export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect }: DeviceSelectionDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [tempSelectedDevices, setTempSelectedDevices] = useState<number[]>(selectedDevices)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
fetchDevices()
|
||||
}
|
||||
}, [open, selectedDevices])
|
||||
|
||||
const fetchDevices = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await api.get<{code: number, msg: string, data: {list: ServerDevice[], total: number}}>('/v1/devices?page=1&limit=100')
|
||||
|
||||
if (response.code === 200 && response.data.list) {
|
||||
const transformedDevices: Device[] = response.data.list.map(device => ({
|
||||
id: device.id,
|
||||
name: device.memo || device.imei || '',
|
||||
imei: device.imei || '',
|
||||
wxid: device.wechatId || '',
|
||||
status: device.alive === 1 ? "online" : "offline",
|
||||
totalFriend: device.totalFriend || 0
|
||||
}))
|
||||
setDevices(transformedDevices)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
const handleDeviceToggle = (deviceId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setTempSelectedDevices(prev => [...prev, deviceId])
|
||||
} else {
|
||||
setTempSelectedDevices(prev => prev.filter(id => id !== deviceId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
onSelect(tempSelectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setTempSelectedDevices(selectedDevices)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const filteredDevices = devices.filter((device) => {
|
||||
const searchLower = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
(device.name || '').toLowerCase().includes(searchLower) ||
|
||||
(device.imei || '').toLowerCase().includes(searchLower) ||
|
||||
(device.wxid || '').toLowerCase().includes(searchLower)
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" ||
|
||||
(statusFilter === "online" && device.status === "online") ||
|
||||
(statusFilter === "offline" && device.status === "offline")
|
||||
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择设备</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center space-x-4 my-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索设备IMEI/备注/微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="online">在线</SelectItem>
|
||||
<SelectItem value="offline">离线</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 -mx-6 px-6" style={{overflowY: 'auto'}}>
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filteredDevices.map((device) => (
|
||||
<label
|
||||
key={device.id}
|
||||
className="flex items-center space-x-3 p-4 rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
style={{paddingLeft: '0px',paddingRight: '0px'}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={tempSelectedDevices.includes(device.id)}
|
||||
onCheckedChange={(checked) => handleDeviceToggle(device.id, checked as boolean)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div>IMEI: {device.imei || '--'}</div>
|
||||
<div>微信号: {device.wxid || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="mt-4 flex gap-4 -mx-6 px-6">
|
||||
<Button className="flex-1" onClick={handleConfirm}>确认</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
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 { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||
import { CheckCircle2, ChevronDown, ChevronUp, Smartphone } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Search, RefreshCw, Smartphone, Database, Users } from "lucide-react"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
name: string
|
||||
status: "online" | "offline"
|
||||
wechatId: string
|
||||
}
|
||||
|
||||
interface DatabaseItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
count: number
|
||||
}
|
||||
|
||||
interface AudienceGroup {
|
||||
id: string
|
||||
name: string
|
||||
count: number
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface DeviceSelectionData {
|
||||
selectedDevices: string[]
|
||||
@@ -39,256 +16,213 @@ export interface DeviceSelectionData {
|
||||
}
|
||||
|
||||
interface DeviceSelectionProps {
|
||||
initialData?: Partial<DeviceSelectionData>
|
||||
initialData: DeviceSelectionData
|
||||
onSave: (data: DeviceSelectionData) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
// 模拟设备数据
|
||||
const mockDevices = [
|
||||
{ id: "1", name: "iPhone 13", status: "online", lastActive: "刚刚" },
|
||||
{ id: "2", name: "华为 P40", status: "offline", lastActive: "3小时前" },
|
||||
{ id: "3", name: "小米 11", status: "online", lastActive: "1小时前" },
|
||||
{ id: "4", name: "OPPO Find X3", status: "offline", lastActive: "昨天" },
|
||||
{ id: "5", name: "vivo X60", status: "online", lastActive: "刚刚" },
|
||||
]
|
||||
|
||||
// 模拟数据库选项
|
||||
const databaseOptions = [
|
||||
{ id: "all", name: "全部客户" },
|
||||
{ id: "new", name: "新客户" },
|
||||
{ id: "vip", name: "VIP客户" },
|
||||
]
|
||||
|
||||
// 模拟用户群体选项
|
||||
const audienceOptions = [
|
||||
{ id: "all", name: "全部好友" },
|
||||
{ id: "active", name: "活跃好友" },
|
||||
{ id: "inactive", name: "不活跃好友" },
|
||||
{ id: "recent", name: "最近添加" },
|
||||
]
|
||||
|
||||
export function DeviceSelection({ initialData, onSave, onBack }: DeviceSelectionProps) {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [databases, setDatabases] = useState<DatabaseItem[]>([])
|
||||
const [audienceGroups, setAudienceGroups] = useState<AudienceGroup[]>([])
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialData?.selectedDevices || [])
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<string>(initialData?.selectedDatabase || "")
|
||||
const [selectedAudience, setSelectedAudience] = useState<string>(initialData?.selectedAudience || "")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
const [formData, setFormData] = useState<DeviceSelectionData>(initialData)
|
||||
const [devices, setDevices] = useState(mockDevices)
|
||||
const [showAllDevices, setShowAllDevices] = useState(false)
|
||||
|
||||
// 模拟获取设备数据
|
||||
useEffect(() => {
|
||||
// 模拟设备数据
|
||||
const mockDevices: Device[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `device-${i + 1}`,
|
||||
name: `设备 ${i + 1}`,
|
||||
status: Math.random() > 0.3 ? "online" : "offline",
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
}))
|
||||
setDevices(mockDevices)
|
||||
const toggleDevice = (deviceId: string) => {
|
||||
const newSelectedDevices = formData.selectedDevices.includes(deviceId)
|
||||
? formData.selectedDevices.filter((id) => id !== deviceId)
|
||||
: [...formData.selectedDevices, deviceId]
|
||||
|
||||
// 模拟数据库数据
|
||||
const mockDatabases: DatabaseItem[] = [
|
||||
{
|
||||
id: "db-1",
|
||||
name: "默认数据库",
|
||||
description: "系统默认的数据库",
|
||||
count: 1250,
|
||||
},
|
||||
{
|
||||
id: "db-2",
|
||||
name: "高净值客户",
|
||||
description: "高消费能力的客户群体",
|
||||
count: 450,
|
||||
},
|
||||
{
|
||||
id: "db-3",
|
||||
name: "潜在客户",
|
||||
description: "有购买意向的潜在客户",
|
||||
count: 780,
|
||||
},
|
||||
]
|
||||
setDatabases(mockDatabases)
|
||||
|
||||
// 模拟目标人群数据
|
||||
const mockAudienceGroups: AudienceGroup[] = [
|
||||
{
|
||||
id: "audience-1",
|
||||
name: "全部好友",
|
||||
count: 1250,
|
||||
description: "所有微信好友",
|
||||
},
|
||||
{
|
||||
id: "audience-2",
|
||||
name: "高频互动好友",
|
||||
count: 320,
|
||||
description: "经常互动的好友",
|
||||
},
|
||||
{
|
||||
id: "audience-3",
|
||||
name: "潜在客户",
|
||||
count: 450,
|
||||
description: "有购买意向的好友",
|
||||
},
|
||||
{
|
||||
id: "audience-4",
|
||||
name: "VIP客户",
|
||||
description: "已成交的VIP客户",
|
||||
},
|
||||
]
|
||||
setAudienceGroups(mockAudienceGroups)
|
||||
|
||||
// 设置默认选中的数据库和目标人群
|
||||
if (!initialData?.selectedDatabase) {
|
||||
setSelectedDatabase("db-1")
|
||||
}
|
||||
if (!initialData?.selectedAudience) {
|
||||
setSelectedAudience("audience-1")
|
||||
}
|
||||
}, [initialData?.selectedDatabase, initialData?.selectedAudience])
|
||||
|
||||
// 过滤设备
|
||||
const filteredDevices = devices.filter((device) => {
|
||||
const matchesSearch =
|
||||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
device.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesTab =
|
||||
activeTab === "all" ||
|
||||
(activeTab === "selected" && selectedDevices.includes(device.id)) ||
|
||||
(activeTab === "online" && device.status === "online") ||
|
||||
(activeTab === "offline" && device.status === "offline")
|
||||
|
||||
return matchesSearch && matchesTab
|
||||
})
|
||||
|
||||
// 选择/取消选择单个设备
|
||||
const handleDeviceSelect = (deviceId: string) => {
|
||||
setSelectedDevices(
|
||||
selectedDevices.includes(deviceId)
|
||||
? selectedDevices.filter((id) => id !== deviceId)
|
||||
: [...selectedDevices, deviceId],
|
||||
)
|
||||
setFormData({ ...formData, selectedDevices: newSelectedDevices })
|
||||
}
|
||||
|
||||
// 保存选择的设备
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
selectedDevices,
|
||||
selectedDatabase,
|
||||
selectedAudience,
|
||||
})
|
||||
const selectAllDevices = () => {
|
||||
const allDeviceIds = devices.map((device) => device.id)
|
||||
setFormData({ ...formData, selectedDevices: allDeviceIds })
|
||||
}
|
||||
|
||||
const clearDeviceSelection = () => {
|
||||
setFormData({ ...formData, selectedDevices: [] })
|
||||
}
|
||||
|
||||
// 用于显示的设备
|
||||
const displayedDevices = showAllDevices ? devices : devices.slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>选择执行点赞任务的设备</CardTitle>
|
||||
<CardDescription>选择要执行自动点赞任务的设备、数据库和目标人群</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* 设备筛选和搜索 */}
|
||||
<div className="space-y-4">
|
||||
<Label className="font-medium">选择设备</Label>
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索设备名称/微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Label className="text-base font-medium">选择点赞设备</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={selectAllDevices}>
|
||||
全选
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={clearDeviceSelection}>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
|
||||
<div className="space-y-2">
|
||||
{displayedDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className={`flex items-center p-3 rounded-md border cursor-pointer transition-colors ${
|
||||
formData.selectedDevices.includes(device.id)
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => toggleDevice(device.id)}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
device.status === "online" ? "bg-green-100" : "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Smartphone
|
||||
className={`h-4 w-4 ${device.status === "online" ? "text-green-600" : "text-gray-400"}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{device.name}</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant={device.status === "online" ? "success" : "secondary"} className="text-xs">
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">{device.lastActive}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.selectedDevices.includes(device.id) && <CheckCircle2 className="h-5 w-5 text-primary" />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{devices.length > 3 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full text-muted-foreground"
|
||||
onClick={() => setShowAllDevices(!showAllDevices)}
|
||||
>
|
||||
{showAllDevices ? (
|
||||
<>
|
||||
<ChevronUp className="h-4 w-4 mr-2" />
|
||||
收起
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-4 w-4 mr-2" />
|
||||
展开更多({devices.length - 3}台)
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-base font-medium">选择目标客户</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择需要点赞的目标客户群体</p>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-between">
|
||||
{databaseOptions.find((option) => option.id === formData.selectedDatabase)?.name ||
|
||||
"选择客户数据库"}
|
||||
<ChevronDown className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
<ScrollArea className="h-[200px]">
|
||||
{databaseOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onClick={() => setFormData({ ...formData, selectedDatabase: option.id })}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
>
|
||||
{option.name}
|
||||
{formData.selectedDatabase === option.id && <CheckCircle2 className="h-4 w-4 text-primary" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base font-medium">选择好友范围</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择需要点赞的好友范围</p>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-between">
|
||||
{audienceOptions.find((option) => option.id === formData.selectedAudience)?.name ||
|
||||
"选择好友范围"}
|
||||
<ChevronDown className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
<ScrollArea className="h-[200px]">
|
||||
{audienceOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onClick={() => setFormData({ ...formData, selectedAudience: option.id })}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
>
|
||||
{option.name}
|
||||
{formData.selectedAudience === option.id && <CheckCircle2 className="h-4 w-4 text-primary" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between space-x-4">
|
||||
<Button variant="outline" className="flex-1" onClick={onBack}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => onSave(formData)}
|
||||
disabled={
|
||||
formData.selectedDevices.length === 0 || !formData.selectedDatabase || !formData.selectedAudience
|
||||
}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 设备分类标签页 */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid grid-cols-4">
|
||||
<TabsTrigger value="all">全部设备</TabsTrigger>
|
||||
<TabsTrigger value="selected">已选择 ({selectedDevices.length})</TabsTrigger>
|
||||
<TabsTrigger value="online">在线设备</TabsTrigger>
|
||||
<TabsTrigger value="offline">离线设备</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* 设备列表 */}
|
||||
<div className="space-y-3">
|
||||
{filteredDevices.map((device) => (
|
||||
<Card
|
||||
key={device.id}
|
||||
className={`p-4 hover:shadow-md transition-shadow ${
|
||||
selectedDevices.includes(device.id) ? "ring-2 ring-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
checked={selectedDevices.includes(device.id)}
|
||||
onCheckedChange={() => handleDeviceSelect(device.id)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-medium truncate flex items-center">
|
||||
<Smartphone className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
{device.name}
|
||||
</div>
|
||||
<Badge variant={device.status === "online" ? "success" : "secondary"} className="text-xs">
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">微信号: {device.wechatId}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{filteredDevices.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">未找到符合条件的设备</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据库选择 */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Database className="h-5 w-5 text-muted-foreground" />
|
||||
<Label className="font-medium">选择数据库</Label>
|
||||
</div>
|
||||
<RadioGroup value={selectedDatabase} onValueChange={setSelectedDatabase} className="space-y-3">
|
||||
{databases.map((db) => (
|
||||
<div key={db.id} className="flex items-start space-x-3">
|
||||
<RadioGroupItem value={db.id} id={db.id} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={db.id} className="font-medium">
|
||||
{db.name}
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{db.count}条数据
|
||||
</Badge>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">{db.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 目标人群选择 */}
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="h-5 w-5 text-muted-foreground" />
|
||||
<Label className="font-medium">选择目标人群</Label>
|
||||
</div>
|
||||
<RadioGroup value={selectedAudience} onValueChange={setSelectedAudience} className="space-y-3">
|
||||
{audienceGroups.map((group) => (
|
||||
<div key={group.id} className="flex items-start space-x-3">
|
||||
<RadioGroupItem value={group.id} id={group.id} />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={group.id} className="font-medium">
|
||||
{group.name}
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{group.count}人
|
||||
</Badge>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">{group.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
返回上一步
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={selectedDevices.length === 0}>
|
||||
完成设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Info, Plus, Trash2 } from "lucide-react"
|
||||
import { Plus, Trash2, Clock } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export interface TimeRange {
|
||||
id: string
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
import { useViewMode } from "@/app/components/LayoutWrapper"
|
||||
|
||||
export interface LikeRulesData {
|
||||
enableAutoLike: boolean
|
||||
@@ -28,80 +21,30 @@ export interface LikeRulesData {
|
||||
keywordFilters: string[]
|
||||
friendGroups: string[]
|
||||
excludedGroups: string[]
|
||||
timeRanges: TimeRange[]
|
||||
timeRanges: { id: string; start: string; end: string }[]
|
||||
randomizeInterval: boolean
|
||||
minInterval?: number
|
||||
maxInterval?: number
|
||||
minInterval: number
|
||||
maxInterval: number
|
||||
}
|
||||
|
||||
interface LikeRulesProps {
|
||||
initialData?: Partial<LikeRulesData>
|
||||
initialData: LikeRulesData
|
||||
onSave: (data: LikeRulesData) => void
|
||||
}
|
||||
|
||||
export function LikeRules({ initialData, onSave }: LikeRulesProps) {
|
||||
const [formData, setFormData] = useState<LikeRulesData>({
|
||||
enableAutoLike: initialData?.enableAutoLike ?? true,
|
||||
likeInterval: initialData?.likeInterval ?? 15,
|
||||
maxLikesPerDay: initialData?.maxLikesPerDay ?? 50,
|
||||
likeOldContent: initialData?.likeOldContent ?? false,
|
||||
contentTypes: initialData?.contentTypes ?? ["text", "image", "video"],
|
||||
keywordFilters: initialData?.keywordFilters ?? [],
|
||||
friendGroups: initialData?.friendGroups ?? ["all"],
|
||||
excludedGroups: initialData?.excludedGroups ?? [],
|
||||
timeRanges: initialData?.timeRanges ?? [{ id: "1", start: "09:00", end: "11:00" }],
|
||||
randomizeInterval: initialData?.randomizeInterval ?? false,
|
||||
minInterval: initialData?.minInterval ?? 5,
|
||||
maxInterval: initialData?.maxInterval ?? 30,
|
||||
})
|
||||
|
||||
const [formData, setFormData] = useState<LikeRulesData>(initialData)
|
||||
const [newKeyword, setNewKeyword] = useState("")
|
||||
const { viewMode } = useViewMode()
|
||||
|
||||
// 内容类型选项
|
||||
const contentTypeOptions = [
|
||||
{ id: "text", label: "纯文字动态" },
|
||||
{ id: "image", label: "图片动态" },
|
||||
{ id: "video", label: "视频动态" },
|
||||
{ id: "link", label: "链接分享" },
|
||||
{ id: "original", label: "仅原创内容" },
|
||||
]
|
||||
|
||||
// 好友分组选项(模拟数据)
|
||||
const friendGroupOptions = [
|
||||
{ id: "all", label: "所有好友" },
|
||||
{ id: "work", label: "工作相关" },
|
||||
{ id: "family", label: "亲友" },
|
||||
{ id: "clients", label: "客户" },
|
||||
{ id: "potential", label: "潜在客户" },
|
||||
]
|
||||
|
||||
// 添加时间范围
|
||||
const addTimeRange = () => {
|
||||
const newId = String(formData.timeRanges.length + 1)
|
||||
setFormData({
|
||||
...formData,
|
||||
timeRanges: [...formData.timeRanges, { id: newId, start: "12:00", end: "14:00" }],
|
||||
})
|
||||
const handleContentTypeToggle = (type: string) => {
|
||||
const updatedTypes = formData.contentTypes.includes(type)
|
||||
? formData.contentTypes.filter((t) => t !== type)
|
||||
: [...formData.contentTypes, type]
|
||||
setFormData({ ...formData, contentTypes: updatedTypes })
|
||||
}
|
||||
|
||||
// 删除时间范围
|
||||
const removeTimeRange = (id: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
timeRanges: formData.timeRanges.filter((range) => range.id !== id),
|
||||
})
|
||||
}
|
||||
|
||||
// 更新时间范围
|
||||
const updateTimeRange = (id: string, field: "start" | "end", value: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
timeRanges: formData.timeRanges.map((range) => (range.id === id ? { ...range, [field]: value } : range)),
|
||||
})
|
||||
}
|
||||
|
||||
// 添加关键词
|
||||
const addKeyword = () => {
|
||||
const addKeywordFilter = () => {
|
||||
if (newKeyword.trim() && !formData.keywordFilters.includes(newKeyword.trim())) {
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -111,378 +54,276 @@ export function LikeRules({ initialData, onSave }: LikeRulesProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除关键词
|
||||
const removeKeyword = (keyword: string) => {
|
||||
const removeKeywordFilter = (keyword: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
keywordFilters: formData.keywordFilters.filter((k) => k !== keyword),
|
||||
})
|
||||
}
|
||||
|
||||
// 切换内容类型
|
||||
const toggleContentType = (typeId: string) => {
|
||||
const addTimeRange = () => {
|
||||
const newId = String(formData.timeRanges.length + 1)
|
||||
setFormData({
|
||||
...formData,
|
||||
contentTypes: formData.contentTypes.includes(typeId)
|
||||
? formData.contentTypes.filter((id) => id !== typeId)
|
||||
: [...formData.contentTypes, typeId],
|
||||
timeRanges: [...formData.timeRanges, { id: newId, start: "09:00", end: "18:00" }],
|
||||
})
|
||||
}
|
||||
|
||||
// 切换好友分组
|
||||
const toggleFriendGroup = (groupId: string) => {
|
||||
if (groupId === "all") {
|
||||
const updateTimeRange = (id: string, field: "start" | "end", value: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
timeRanges: formData.timeRanges.map((range) => (range.id === id ? { ...range, [field]: value } : range)),
|
||||
})
|
||||
}
|
||||
|
||||
const removeTimeRange = (id: string) => {
|
||||
if (formData.timeRanges.length > 1) {
|
||||
setFormData({
|
||||
...formData,
|
||||
friendGroups: ["all"],
|
||||
excludedGroups: [],
|
||||
timeRanges: formData.timeRanges.filter((range) => range.id !== id),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果当前包含"all",则移除它
|
||||
let newGroups = formData.friendGroups.filter((id) => id !== "all")
|
||||
|
||||
if (formData.friendGroups.includes(groupId)) {
|
||||
newGroups = newGroups.filter((id) => id !== groupId)
|
||||
// 如果没有选择任何组,默认回到"all"
|
||||
if (newGroups.length === 0) {
|
||||
newGroups = ["all"]
|
||||
}
|
||||
} else {
|
||||
newGroups.push(groupId)
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
friendGroups: newGroups,
|
||||
})
|
||||
}
|
||||
|
||||
// 切换排除分组
|
||||
const toggleExcludedGroup = (groupId: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
excludedGroups: formData.excludedGroups.includes(groupId)
|
||||
? formData.excludedGroups.filter((id) => id !== groupId)
|
||||
: [...formData.excludedGroups, groupId],
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = () => {
|
||||
onSave(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>点赞规则设置</CardTitle>
|
||||
<CardDescription>设定自动点赞的规则和时间间隔</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* 基本设置 */}
|
||||
<div className="space-y-4">
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className={`space-y-6 ${viewMode === "desktop" ? "p-6" : "p-4"}`}>
|
||||
<div className={`grid ${viewMode === "desktop" ? "grid-cols-2 gap-8" : "grid-cols-1 gap-4"}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="enableAutoLike" className="font-medium">
|
||||
<div>
|
||||
<Label htmlFor="enable-auto-like" className="text-base font-medium">
|
||||
启用自动点赞
|
||||
</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>开启后系统将根据设置自动为朋友圈点赞</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className="text-sm text-muted-foreground">开启后,系统将按照设定的规则自动点赞</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enableAutoLike"
|
||||
id="enable-auto-like"
|
||||
checked={formData.enableAutoLike}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, enableAutoLike: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">内容类型</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择需要自动点赞的内容类型</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="text-content"
|
||||
checked={formData.contentTypes.includes("text")}
|
||||
onCheckedChange={() => handleContentTypeToggle("text")}
|
||||
/>
|
||||
<label htmlFor="text-content" className="text-sm">
|
||||
文字
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="image-content"
|
||||
checked={formData.contentTypes.includes("image")}
|
||||
onCheckedChange={() => handleContentTypeToggle("image")}
|
||||
/>
|
||||
<label htmlFor="image-content" className="text-sm">
|
||||
图片
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="video-content"
|
||||
checked={formData.contentTypes.includes("video")}
|
||||
onCheckedChange={() => handleContentTypeToggle("video")}
|
||||
/>
|
||||
<label htmlFor="video-content" className="text-sm">
|
||||
视频
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max-likes" className="text-base font-medium">
|
||||
每日最大点赞数
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">设置每日最多点赞次数,建议不超过100次</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
id="max-likes"
|
||||
value={[formData.maxLikesPerDay]}
|
||||
min={10}
|
||||
max={150}
|
||||
step={5}
|
||||
onValueChange={(value) => setFormData({ ...formData, maxLikesPerDay: value[0] })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="bg-primary text-primary-foreground rounded-md px-3 py-1 font-medium min-w-[60px] text-center">
|
||||
{formData.maxLikesPerDay}次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="like-interval" className="text-base font-medium">
|
||||
点赞间隔
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">设置点赞之间的时间间隔(分钟)</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
id="like-interval"
|
||||
value={[formData.likeInterval]}
|
||||
min={1}
|
||||
max={60}
|
||||
step={1}
|
||||
onValueChange={(value) => setFormData({ ...formData, likeInterval: value[0] })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="bg-primary text-primary-foreground rounded-md px-3 py-1 font-medium min-w-[60px] text-center">
|
||||
{formData.likeInterval}分钟
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="intervalType" className="font-medium">
|
||||
点赞间隔设置
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.randomizeInterval ? "random" : "fixed"}
|
||||
onValueChange={(value) => setFormData({ ...formData, randomizeInterval: value === "random" })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="选择间隔类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fixed">固定间隔</SelectItem>
|
||||
<SelectItem value="random">随机间隔</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div>
|
||||
<Label htmlFor="randomize-interval" className="text-base font-medium">
|
||||
随机化间隔
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">开启后,系统将在设定范围内随机选择点赞间隔</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="randomize-interval"
|
||||
checked={formData.randomizeInterval}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, randomizeInterval: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.randomizeInterval ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="minInterval" className="text-sm">
|
||||
最小间隔(分钟)
|
||||
</Label>
|
||||
<span className="text-sm text-muted-foreground">{formData.minInterval}分钟</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="minInterval"
|
||||
{formData.randomizeInterval && (
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<Label htmlFor="min-interval">最小间隔(分钟)</Label>
|
||||
<Input
|
||||
id="min-interval"
|
||||
type="number"
|
||||
value={formData.minInterval}
|
||||
onChange={(e) => setFormData({ ...formData, minInterval: Number.parseInt(e.target.value) || 1 })}
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
value={[formData.minInterval || 5]}
|
||||
onValueChange={(value) => setFormData({ ...formData, minInterval: value[0] })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maxInterval" className="text-sm">
|
||||
最大间隔(分钟)
|
||||
</Label>
|
||||
<span className="text-sm text-muted-foreground">{formData.maxInterval}分钟</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="maxInterval"
|
||||
min={formData.minInterval || 5}
|
||||
max={120}
|
||||
step={1}
|
||||
value={[formData.maxInterval || 30]}
|
||||
onValueChange={(value) => setFormData({ ...formData, maxInterval: value[0] })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
<div>
|
||||
<Label htmlFor="max-interval">最大间隔(分钟)</Label>
|
||||
<Input
|
||||
id="max-interval"
|
||||
type="number"
|
||||
value={formData.maxInterval}
|
||||
onChange={(e) => setFormData({ ...formData, maxInterval: Number.parseInt(e.target.value) || 1 })}
|
||||
min={formData.minInterval + 1}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="likeInterval" className="text-sm">
|
||||
点赞间隔(分钟)
|
||||
</Label>
|
||||
<span className="text-sm text-muted-foreground">{formData.likeInterval}分钟</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="likeInterval"
|
||||
min={1}
|
||||
max={60}
|
||||
step={1}
|
||||
value={[formData.likeInterval]}
|
||||
onValueChange={(value) => setFormData({ ...formData, likeInterval: value[0] })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maxLikesPerDay" className="font-medium">
|
||||
每日最大点赞数
|
||||
</Label>
|
||||
<span className="text-sm text-muted-foreground">{formData.maxLikesPerDay}个</span>
|
||||
<Label className="text-base font-medium">时间范围</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">设置自动点赞的时间段</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{formData.timeRanges.map((range) => (
|
||||
<div key={range.id} className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="time"
|
||||
value={range.start}
|
||||
onChange={(e) => updateTimeRange(range.id, "start", e.target.value)}
|
||||
className="w-32"
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={range.end}
|
||||
onChange={(e) => updateTimeRange(range.id, "end", e.target.value)}
|
||||
className="w-32"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeTimeRange(range.id)}
|
||||
disabled={formData.timeRanges.length <= 1}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button variant="outline" size="sm" onClick={addTimeRange} className="mt-2">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
添加时间段
|
||||
</Button>
|
||||
</div>
|
||||
<Slider
|
||||
id="maxLikesPerDay"
|
||||
min={10}
|
||||
max={200}
|
||||
step={10}
|
||||
value={[formData.maxLikesPerDay]}
|
||||
onValueChange={(value) => setFormData({ ...formData, maxLikesPerDay: value[0] })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="likeOldContent" className="font-medium">
|
||||
点赞历史内容
|
||||
</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>开启后系统将点赞好友的历史朋友圈内容</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">关键词过滤</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">添加包含特定关键词的内容才会被点赞</p>
|
||||
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<Input
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder="输入关键词"
|
||||
className="flex-1"
|
||||
onKeyDown={(e) => e.key === "Enter" && addKeywordFilter()}
|
||||
/>
|
||||
<Button onClick={addKeywordFilter} variant="secondary">
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<Switch
|
||||
id="likeOldContent"
|
||||
checked={formData.likeOldContent}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, likeOldContent: checked })}
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容类型设置 */}
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<Label className="font-medium">点赞内容类型</Label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{contentTypeOptions.map((type) => (
|
||||
<div key={type.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`content-${type.id}`}
|
||||
checked={formData.contentTypes.includes(type.id)}
|
||||
onCheckedChange={() => toggleContentType(type.id)}
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
<Label htmlFor={`content-${type.id}`} className="text-sm">
|
||||
{type.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键词过滤 */}
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<Label className="font-medium">关键词过滤</Label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
placeholder="输入关键词"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
className="flex-1"
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
<Button type="button" onClick={addKeyword} disabled={!formData.enableAutoLike || !newKeyword.trim()}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
{formData.keywordFilters.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.keywordFilters.length === 0 && (
|
||||
<span className="text-sm text-muted-foreground">未设置关键词过滤</span>
|
||||
)}
|
||||
|
||||
{formData.keywordFilters.map((keyword) => (
|
||||
<Badge key={keyword} variant="secondary" className="flex items-center gap-1">
|
||||
{keyword}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
||||
onClick={() => removeKeyword(keyword)}
|
||||
disabled={!formData.enableAutoLike}
|
||||
className="h-4 w-4 p-0 ml-1"
|
||||
onClick={() => removeKeywordFilter(keyword)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span className="sr-only">Remove</span>
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">添加关键词后,系统将只对包含这些关键词的内容进行点赞</p>
|
||||
</div>
|
||||
|
||||
{/* 好友分组设置 */}
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<Label className="font-medium">好友分组筛选</Label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{friendGroupOptions.map((group) => (
|
||||
<div key={group.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`group-${group.id}`}
|
||||
checked={formData.friendGroups.includes(group.id)}
|
||||
onCheckedChange={() => toggleFriendGroup(group.id)}
|
||||
disabled={!formData.enableAutoLike || (group.id !== "all" && formData.friendGroups.includes("all"))}
|
||||
/>
|
||||
<Label htmlFor={`group-${group.id}`} className="text-sm">
|
||||
{group.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!formData.friendGroups.includes("all") && (
|
||||
<div className="mt-4">
|
||||
<Label className="font-medium text-sm">排除分组</Label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2">
|
||||
{friendGroupOptions
|
||||
.filter((group) => group.id !== "all" && !formData.friendGroups.includes(group.id))
|
||||
.map((group) => (
|
||||
<div key={`exclude-${group.id}`} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`exclude-${group.id}`}
|
||||
checked={formData.excludedGroups.includes(group.id)}
|
||||
onCheckedChange={() => toggleExcludedGroup(group.id)}
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
<Label htmlFor={`exclude-${group.id}`} className="text-sm">
|
||||
{group.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 时间范围设置 */}
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium">点赞时间段</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addTimeRange}
|
||||
disabled={!formData.enableAutoLike || formData.timeRanges.length >= 5}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加时间段
|
||||
</Button>
|
||||
<div>
|
||||
<Label htmlFor="like-old-content" className="text-base font-medium">
|
||||
点赞历史内容
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">开启后,系统会点赞朋友圈中较旧的内容</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="like-old-content"
|
||||
checked={formData.likeOldContent}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, likeOldContent: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{formData.timeRanges.map((range) => (
|
||||
<div key={range.id} className="flex items-center space-x-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={range.start}
|
||||
onChange={(e) => updateTimeRange(range.id, "start", e.target.value)}
|
||||
className="w-32"
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={range.end}
|
||||
onChange={(e) => updateTimeRange(range.id, "end", e.target.value)}
|
||||
className="w-32"
|
||||
disabled={!formData.enableAutoLike}
|
||||
/>
|
||||
{formData.timeRanges.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeTimeRange(range.id)}
|
||||
disabled={!formData.enableAutoLike}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">设置点赞的时间段,系统将在这些时间段内执行点赞任务</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit}>保存并继续</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full" onClick={() => onSave(formData)}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,51 @@
|
||||
"use client"
|
||||
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { cn } from "@/app/lib/utils"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
interface Step {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
steps: Step[]
|
||||
interface StepProps {
|
||||
currentStep: number
|
||||
onStepClick?: (index: number) => void
|
||||
}
|
||||
|
||||
export function StepIndicator({ steps, currentStep, onStepClick }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ol className="flex items-center w-full">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep
|
||||
const isCurrent = index === currentStep
|
||||
const isClickable = onStepClick && index <= currentStep
|
||||
export function StepIndicator({ currentStep }: StepProps) {
|
||||
const steps = [
|
||||
{ title: "基础设置", description: "设置点赞规则" },
|
||||
{ title: "设备选择", description: "选择执行设备" },
|
||||
{ title: "人群选择", description: "选择目标人群" },
|
||||
]
|
||||
|
||||
return (
|
||||
<li
|
||||
key={step.id}
|
||||
className={cn("flex items-center space-x-2.5 flex-1", index !== steps.length - 1 ? "relative" : "")}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center justify-center w-8 h-8 rounded-full shrink-0 text-sm font-medium",
|
||||
isCompleted
|
||||
? "bg-primary text-primary-foreground"
|
||||
: isCurrent
|
||||
? "bg-primary/20 text-primary border border-primary"
|
||||
: "bg-muted text-muted-foreground",
|
||||
isClickable ? "cursor-pointer" : "",
|
||||
)}
|
||||
onClick={() => isClickable && onStepClick(index)}
|
||||
return (
|
||||
<div className="px-6">
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex flex-col items-center relative z-10">
|
||||
<div
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
index < currentStep
|
||||
? "bg-blue-600 text-white"
|
||||
: index === currentStep
|
||||
? "border-2 border-blue-600 text-blue-600"
|
||||
: "border-2 border-gray-300 text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? <CheckIcon className="w-5 h-5" /> : index + 1}
|
||||
</span>
|
||||
<span>
|
||||
<h3
|
||||
className={cn(
|
||||
"font-medium leading-tight",
|
||||
isCompleted || isCurrent ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{step.name}
|
||||
</h3>
|
||||
{step.description && (
|
||||
<p className="text-sm text-muted-foreground hidden md:block">{step.description}</p>
|
||||
)}
|
||||
</span>
|
||||
{index !== steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-4 left-8 -translate-y-1/2 w-full h-0.5",
|
||||
isCompleted ? "bg-primary" : "bg-muted",
|
||||
)}
|
||||
style={{ width: "calc(100% - 2rem)" }}
|
||||
></div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
{index < currentStep ? <Check className="w-5 h-5" /> : index + 1}
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<div className={`text-sm font-medium ${index <= currentStep ? "text-gray-900" : "text-gray-400"}`}>
|
||||
{step.title}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{step.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute top-4 left-0 w-full h-0.5 bg-gray-200 -translate-y-1/2 z-0">
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-blue-600 transition-all duration-300"
|
||||
style={{ width: `${((currentStep - 1) / (steps.length - 1)) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
243
Cunkebao/app/workspace/auto-like/components/tag-selector.tsx
Normal file
243
Cunkebao/app/workspace/auto-like/components/tag-selector.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Search, Tag, Check, X } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
|
||||
interface TagGroup {
|
||||
id: string
|
||||
name: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
interface TagSelectorProps {
|
||||
selectedTags: string[]
|
||||
tagOperator: "and" | "or"
|
||||
onTagsChange: (tags: string[]) => void
|
||||
onOperatorChange: (operator: "and" | "or") => void
|
||||
onBack: () => void
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
export function TagSelector({
|
||||
selectedTags,
|
||||
tagOperator,
|
||||
onTagsChange,
|
||||
onOperatorChange,
|
||||
onBack,
|
||||
onComplete,
|
||||
}: TagSelectorProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [tagGroups, setTagGroups] = useState<TagGroup[]>([
|
||||
{
|
||||
id: "intention",
|
||||
name: "意向度",
|
||||
tags: ["高意向", "中意向", "低意向"],
|
||||
},
|
||||
{
|
||||
id: "customer",
|
||||
name: "客户类型",
|
||||
tags: ["新客户", "老客户", "VIP客户"],
|
||||
},
|
||||
{
|
||||
id: "gender",
|
||||
name: "性别",
|
||||
tags: ["男性", "女性"],
|
||||
},
|
||||
{
|
||||
id: "age",
|
||||
name: "年龄段",
|
||||
tags: ["年轻人", "中年人", "老年人"],
|
||||
},
|
||||
{
|
||||
id: "location",
|
||||
name: "地区",
|
||||
tags: ["城市", "农村"],
|
||||
},
|
||||
{
|
||||
id: "income",
|
||||
name: "收入",
|
||||
tags: ["高收入", "中等收入", "低收入"],
|
||||
},
|
||||
{
|
||||
id: "interaction",
|
||||
name: "互动频率",
|
||||
tags: ["高频互动", "中频互动", "低频互动"],
|
||||
},
|
||||
])
|
||||
const [customTag, setCustomTag] = useState("")
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
if (selectedTags.includes(tag)) {
|
||||
onTagsChange(selectedTags.filter((t) => t !== tag))
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tag])
|
||||
}
|
||||
}
|
||||
|
||||
const addCustomTag = () => {
|
||||
if (customTag.trim() && !selectedTags.includes(customTag.trim())) {
|
||||
onTagsChange([...selectedTags, customTag.trim()])
|
||||
setCustomTag("")
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
onTagsChange(selectedTags.filter((t) => t !== tag))
|
||||
}
|
||||
|
||||
const filteredTagGroups = tagGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
tags: group.tags.filter((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
}))
|
||||
.filter((group) => group.tags.length > 0)
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium">选择目标人群标签</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索标签"
|
||||
className="pl-9"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="intention" className="mb-6">
|
||||
<TabsList className="grid grid-cols-4 mb-4">
|
||||
{tagGroups.slice(0, 4).map((group) => (
|
||||
<TabsTrigger key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tagGroups.map((group) => (
|
||||
<TabsContent key={group.id} value={group.id} className="mt-0">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer py-1 px-3"
|
||||
onClick={() => toggleTag(tag)}
|
||||
>
|
||||
{selectedTags.includes(tag) && <Check className="h-3 w-3 mr-1" />}
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<ScrollArea className="h-48 border rounded-md p-4 mb-4">
|
||||
<div className="space-y-4">
|
||||
{filteredTagGroups.length > 0 ? (
|
||||
filteredTagGroups.map((group) => (
|
||||
<div key={group.id} className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-500">{group.name}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.tags.map((tag) => (
|
||||
<div key={tag} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`tag-${tag}`}
|
||||
checked={selectedTags.includes(tag)}
|
||||
onCheckedChange={() => toggleTag(tag)}
|
||||
/>
|
||||
<Label htmlFor={`tag-${tag}`} className="text-sm font-normal">
|
||||
{tag}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-500">没有找到匹配的标签</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<div className="relative flex-1">
|
||||
<Tag className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={customTag}
|
||||
onChange={(e) => setCustomTag(e.target.value)}
|
||||
className="pl-9"
|
||||
placeholder="添加自定义标签"
|
||||
onKeyDown={(e) => e.key === "Enter" && addCustomTag()}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={addCustomTag} disabled={!customTag.trim()}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-medium">标签匹配逻辑</h3>
|
||||
<p className="text-sm text-muted-foreground mb-2">选择多个标签之间的匹配关系</p>
|
||||
|
||||
<RadioGroup
|
||||
value={tagOperator}
|
||||
onValueChange={(value) => onOperatorChange(value as "and" | "or")}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="and" id="and-operator" />
|
||||
<Label htmlFor="and-operator" className="font-normal">
|
||||
所有标签都必须匹配(AND)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="or" id="or-operator" />
|
||||
<Label htmlFor="or-operator" className="font-normal">
|
||||
匹配任意一个标签即可(OR)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-medium mb-2">已选择的标签</h3>
|
||||
<div className="min-h-[60px] border rounded-md p-3">
|
||||
{selectedTags.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">未选择任何标签</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTags.map((tag) => (
|
||||
<Badge key={tag} className="flex items-center gap-1 py-1 px-2">
|
||||
{tag}
|
||||
<Button variant="ghost" size="icon" className="h-4 w-4 p-0 ml-1" onClick={() => removeTag(tag)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
3
Cunkebao/app/workspace/auto-like/loading.tsx
Normal file
3
Cunkebao/app/workspace/auto-like/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
3
Cunkebao/app/workspace/auto-like/new/loading.tsx
Normal file
3
Cunkebao/app/workspace/auto-like/new/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
160
Cunkebao/app/workspace/auto-like/new/page.tsx
Normal file
160
Cunkebao/app/workspace/auto-like/new/page.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Search } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { StepIndicator } from "../components/step-indicator"
|
||||
import { BasicSettings } from "../components/basic-settings"
|
||||
import { DeviceSelectionDialog } from "../components/device-selection-dialog"
|
||||
import { TagSelector } from "../components/tag-selector"
|
||||
import { api, ApiResponse } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
export default function NewAutoLikePage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: "",
|
||||
likeInterval: 5, // 默认5秒
|
||||
maxLikesPerDay: 200, // 默认200次
|
||||
timeRange: { start: "08:00", end: "22:00" },
|
||||
contentTypes: ["text", "image", "video"],
|
||||
enabled: true,
|
||||
selectedDevices: [] as number[],
|
||||
selectedTags: [] as string[],
|
||||
tagOperator: "and" as "and" | "or",
|
||||
})
|
||||
|
||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }))
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 3))
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1))
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/create', {
|
||||
type: 1,
|
||||
name: formData.taskName,
|
||||
interval: formData.likeInterval,
|
||||
maxLikes: formData.maxLikesPerDay,
|
||||
startTime: formData.timeRange.start,
|
||||
endTime: formData.timeRange.end,
|
||||
contentTypes: formData.contentTypes,
|
||||
enabled: formData.enabled,
|
||||
devices: formData.selectedDevices,
|
||||
targetGroups: formData.selectedTags,
|
||||
tagOperator: formData.tagOperator === 'and' ? 1 : 2
|
||||
});
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast(response.msg, "success");
|
||||
router.push("/workspace/auto-like");
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("创建自动点赞任务失败:", error);
|
||||
showToast(error?.message || "请检查网络连接或稍后再试", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F9FA] pb-20">
|
||||
<header className="sticky top-0 z-10 bg-white">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()} className="hover:bg-gray-50">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h1 className="ml-2 text-lg font-medium">新建自动点赞</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mt-8">
|
||||
<StepIndicator currentStep={currentStep} />
|
||||
|
||||
<div className="mt-8">
|
||||
{currentStep === 1 && (
|
||||
<BasicSettings formData={formData} onChange={handleUpdateFormData} onNext={handleNext} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-6 px-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
placeholder="选择设备"
|
||||
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
|
||||
onClick={() => setDeviceDialogOpen(true)}
|
||||
readOnly
|
||||
value={formData.selectedDevices.length > 0 ? `已选择 ${formData.selectedDevices.length} 个设备` : ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.selectedDevices.length > 0 && (
|
||||
<div className="text-base text-gray-500">已选设备:{formData.selectedDevices.length} 个</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
disabled={formData.selectedDevices.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DeviceSelectionDialog
|
||||
open={deviceDialogOpen}
|
||||
onOpenChange={setDeviceDialogOpen}
|
||||
selectedDevices={formData.selectedDevices}
|
||||
onSelect={(devices) => {
|
||||
handleUpdateFormData({ selectedDevices: devices })
|
||||
setDeviceDialogOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className="px-6">
|
||||
<TagSelector
|
||||
selectedTags={formData.selectedTags}
|
||||
tagOperator={formData.tagOperator}
|
||||
onTagsChange={(tags) => handleUpdateFormData({ selectedTags: tags })}
|
||||
onOperatorChange={(operator) => handleUpdateFormData({ tagOperator: operator })}
|
||||
onBack={handlePrev}
|
||||
onComplete={handleComplete}
|
||||
/>
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,129 +1,403 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
ChevronLeft,
|
||||
Plus,
|
||||
Filter,
|
||||
Search,
|
||||
RefreshCw,
|
||||
MoreVertical,
|
||||
Clock,
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
Copy,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Settings,
|
||||
Calendar,
|
||||
Users,
|
||||
ThumbsUp,
|
||||
} from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { StepIndicator } from "./components/step-indicator"
|
||||
import { TimeSettings, type TimeSettingsData } from "./components/time-settings"
|
||||
import { DeviceSelection, type DeviceSelectionData } from "./components/device-selection"
|
||||
import type { LikeConfigData } from "./components/like-config"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Link from "next/link"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "device-selection",
|
||||
name: "设备选择",
|
||||
description: "选择执行自动点赞的设备",
|
||||
},
|
||||
{
|
||||
id: "audience-selection",
|
||||
name: "人群选择",
|
||||
description: "选择点赞目标人群",
|
||||
},
|
||||
{
|
||||
id: "time-settings",
|
||||
name: "时间设置",
|
||||
description: "设置点赞时间和频率",
|
||||
},
|
||||
]
|
||||
interface TaskConfig {
|
||||
id: number
|
||||
workbenchId: number
|
||||
interval: number
|
||||
maxLikes: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
contentTypes: string[]
|
||||
devices: number[]
|
||||
targetGroups: string[]
|
||||
tagOperator: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
status: number
|
||||
autoStart: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
config: TaskConfig
|
||||
}
|
||||
|
||||
interface TaskListResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
list: Task[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string
|
||||
}
|
||||
|
||||
export default function AutoLikePage() {
|
||||
const router = useRouter()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [formData, setFormData] = useState({
|
||||
deviceSelection: {
|
||||
selectedDevices: [],
|
||||
selectedDatabase: "",
|
||||
selectedAudience: "",
|
||||
} as DeviceSelectionData,
|
||||
timeSettings: {
|
||||
enableAutoLike: true,
|
||||
timeRanges: [{ id: "1", start: "06:00", end: "08:00" }],
|
||||
likeInterval: 15,
|
||||
randomizeInterval: false,
|
||||
} as TimeSettingsData,
|
||||
likeConfig: {
|
||||
likeAll: false,
|
||||
likeFirstPage: true,
|
||||
maxLikesPerDay: 50,
|
||||
likeImmediately: true,
|
||||
excludeTags: [],
|
||||
includeTags: [],
|
||||
} as LikeConfigData,
|
||||
})
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<number | null>(null)
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchName, setSearchName] = useState("")
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const pageSize = 10
|
||||
|
||||
const handleDeviceSelectionSave = (data: DeviceSelectionData) => {
|
||||
setFormData({ ...formData, deviceSelection: data })
|
||||
setCurrentStep(1)
|
||||
}
|
||||
const fetchTasks = async (page: number, name?: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const queryParams = new URLSearchParams({
|
||||
type: '1',
|
||||
page: page.toString(),
|
||||
limit: pageSize.toString(),
|
||||
})
|
||||
if (name) {
|
||||
queryParams.append('name', name)
|
||||
}
|
||||
const response = await api.get<TaskListResponse>(`/v1/workbench/list?${queryParams.toString()}`)
|
||||
|
||||
const handleTimeSettingsSave = (data: TimeSettingsData) => {
|
||||
setFormData({ ...formData, timeSettings: data })
|
||||
setCurrentStep(2)
|
||||
}
|
||||
|
||||
const handleLikeConfigSave = (data: LikeConfigData) => {
|
||||
setFormData({ ...formData, likeConfig: data })
|
||||
// 提交表单或导航到确认页面
|
||||
router.push("/workspace")
|
||||
}
|
||||
|
||||
const handleStepClick = (index: number) => {
|
||||
if (index <= currentStep) {
|
||||
setCurrentStep(index)
|
||||
if (response.code === 200) {
|
||||
setTasks(response.data.list)
|
||||
setTotal(response.data.total)
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取任务列表失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
} else {
|
||||
router.push("/workspace")
|
||||
useEffect(() => {
|
||||
fetchTasks(currentPage, searchName)
|
||||
}, [currentPage])
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1)
|
||||
fetchTasks(1, searchName)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchTasks(currentPage, searchName)
|
||||
}
|
||||
|
||||
const toggleExpand = (taskId: number) => {
|
||||
setExpandedTaskId(expandedTaskId === taskId ? null : taskId)
|
||||
}
|
||||
|
||||
const handleDelete = async (taskId: number) => {
|
||||
try {
|
||||
const response = await api.delete<ApiResponse>(`/v1/workbench/delete?id=${taskId}`)
|
||||
|
||||
if (response.code === 200) {
|
||||
// 删除成功后刷新列表
|
||||
fetchTasks(currentPage, searchName)
|
||||
showToast(response.msg || "已成功删除点赞任务", "success")
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除任务失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (taskId: number) => {
|
||||
router.push(`/workspace/auto-like/${taskId}/edit`)
|
||||
}
|
||||
|
||||
const handleView = (taskId: number) => {
|
||||
router.push(`/workspace/auto-like/${taskId}`)
|
||||
}
|
||||
|
||||
const handleCopy = async (taskId: number) => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/copy', {
|
||||
id: taskId
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 复制成功后刷新列表
|
||||
fetchTasks(currentPage, searchName)
|
||||
showToast(response.msg || "已成功复制点赞任务", "success")
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("复制任务失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTaskStatus = async (taskId: number, currentStatus: number) => {
|
||||
try {
|
||||
const response = await api.post<ApiResponse>('/v1/workbench/update-status', {
|
||||
id: taskId,
|
||||
status: currentStatus === 1 ? 2 : 1
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新本地状态
|
||||
setTasks(tasks.map(task =>
|
||||
task.id === taskId
|
||||
? { ...task, status: currentStatus === 1 ? 2 : 1 }
|
||||
: task
|
||||
))
|
||||
|
||||
const newStatus = currentStatus === 1 ? 2 : 1
|
||||
showToast(response.msg || `任务${newStatus === 1 ? "已启动" : "已暂停"}`, "success")
|
||||
} else {
|
||||
showToast(response.msg || "请稍后再试", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("更新任务状态失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" onClick={handleBack} className="mb-4">
|
||||
← 返回
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">自动点赞</h1>
|
||||
<p className="text-muted-foreground">设置自动点赞功能,提高互动率和活跃度</p>
|
||||
</div>
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">自动点赞</h1>
|
||||
</div>
|
||||
<Link href="/workspace/auto-like/new">
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mb-8">
|
||||
<StepIndicator steps={steps} currentStep={currentStep} onStepClick={handleStepClick} />
|
||||
</div>
|
||||
|
||||
{currentStep === 0 && (
|
||||
<DeviceSelection
|
||||
initialData={formData.deviceSelection}
|
||||
onSave={handleDeviceSelectionSave}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>人群选择</CardTitle>
|
||||
<CardDescription>选择需要自动点赞的目标人群</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* 这里可以添加人群选择的具体内容 */}
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(0)}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={() => setCurrentStep(2)}>下一步</Button>
|
||||
<div className="p-4">
|
||||
<Card className="p-4 mb-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="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && <TimeSettings initialData={formData.timeSettings} onSave={handleTimeSettingsSave} />}
|
||||
<div className="space-y-4">
|
||||
{tasks.map((task) => (
|
||||
<Card key={task.id} className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={task.status === 1 ? "default" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleView(task.id)}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>执行设备:{task.config.devices.length} 个</div>
|
||||
<div>目标人群:{task.config.targetGroups.join(', ')}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>点赞间隔:{task.config.interval} 秒</div>
|
||||
<div>每日上限:{task.config.maxLikes} 次</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
更新时间:{task.updateTime}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span>创建时间:{task.createTime}</span>
|
||||
<Button variant="ghost" size="sm" className="ml-2 p-0 h-6 w-6" onClick={() => toggleExpand(task.id)}>
|
||||
{expandedTaskId === task.id ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expandedTaskId === task.id && (
|
||||
<div className="mt-4 pt-4 border-t border-dashed">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Settings className="h-5 w-5 mr-2 text-gray-500" />
|
||||
<h4 className="font-medium">基本设置</h4>
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">点赞间隔:</span>
|
||||
<span>{task.config.interval} 秒</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">每日最大点赞数:</span>
|
||||
<span>{task.config.maxLikes} 次</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">执行时间段:</span>
|
||||
<span>
|
||||
{task.config.startTime} - {task.config.endTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-5 w-5 mr-2 text-gray-500" />
|
||||
<h4 className="font-medium">目标人群</h4>
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.config.targetGroups.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="bg-gray-50">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
匹配方式:{task.config.tagOperator === 1 ? "满足所有标签" : "满足任一标签"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<ThumbsUp className="h-5 w-5 mr-2 text-gray-500" />
|
||||
<h4 className="font-medium">点赞内容类型</h4>
|
||||
</div>
|
||||
<div className="space-y-2 pl-7">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.config.contentTypes.map((type) => (
|
||||
<Badge key={type} variant="outline" className="bg-gray-50">
|
||||
{type === "text" ? "文字" : type === "image" ? "图片" : "视频"}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{total > pageSize && (
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm text-gray-500">第 {currentPage} 页</span>
|
||||
<span className="text-sm text-gray-500">共 {Math.ceil(total / pageSize)} 页</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(total / pageSize), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,13 @@ export function BasicSettings({
|
||||
<div className="grid gap-2">
|
||||
<Label className="flex items-center">每日推送:</Label>
|
||||
<div className="flex items-center space-x-2 max-w-md">
|
||||
<Button variant="outline" size="icon" type="button" onClick={() => handleCountChange(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={() => handleCountChange(false)}
|
||||
className="bg-white border-gray-200"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
@@ -99,7 +105,13 @@ export function BasicSettings({
|
||||
className="w-20 text-center"
|
||||
min="1"
|
||||
/>
|
||||
<Button variant="outline" size="icon" type="button" onClick={() => handleCountChange(true)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={() => handleCountChange(true)}
|
||||
className="bg-white border-gray-200"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<span>条内容</span>
|
||||
@@ -193,4 +205,3 @@ export function BasicSettings({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Plus, Minus, Clock, HelpCircle } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { useViewMode } from "@/app/components/LayoutWrapper"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: {
|
||||
@@ -20,132 +21,136 @@ interface BasicSettingsProps {
|
||||
}
|
||||
|
||||
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
|
||||
return (
|
||||
<div className="space-y-8 px-6">
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">任务名称</div>
|
||||
<Input
|
||||
value={formData.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
placeholder="请输入任务名称"
|
||||
className="h-12 border-0 border-b border-gray-200 rounded-none focus-visible:ring-0 focus-visible:border-blue-600 px-0 text-base"
|
||||
/>
|
||||
</div>
|
||||
<div className={`space-y-6 ${viewMode === "desktop" ? "p-6" : "p-4"}`}>
|
||||
<div className={`grid ${viewMode === "desktop" ? "grid-cols-2 gap-8" : "grid-cols-1 gap-4"}`}>
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">任务名称</div>
|
||||
<Input
|
||||
value={formData.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
placeholder="请输入任务名称"
|
||||
className="h-12 border-0 border-b border-gray-200 rounded-none focus-visible:ring-0 focus-visible:border-blue-600 px-0 text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">允许发布时间段</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-gray-500">至</span>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">允许发布时间段</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-gray-500">至</span>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||||
className="h-12 pl-10 rounded-xl border-gray-200 text-base"
|
||||
/>
|
||||
<Clock className="absolute left-3 top-4 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
||||
className="h-12 w-12 rounded-xl"
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="w-8 text-center text-lg font-medium">{formData.syncCount}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
|
||||
className="h-12 w-12 rounded-xl"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-gray-500">条朋友圈</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">账号类型</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "business" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "business"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
业务号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "business" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
业务号能够循环推送内容库中的内容。当内容库所有内容循环推送完毕后,若有新内容则优先推送新内容,若无新内容则继续循环推送。
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "personal" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "personal"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
人设号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "personal" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>用于实时更新同步,有新动态时进行同步,无动态则不同步。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Minus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="w-8 text-center text-lg font-medium">{formData.syncCount}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => onChange({ syncCount: formData.syncCount + 1 })}
|
||||
className="h-12 w-12 rounded-xl bg-white border-gray-200"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-gray-500">条朋友圈</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-base font-medium">是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-base font-medium mb-2">账号类型</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "business" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "business"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
业务号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "business" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]">
|
||||
<p>
|
||||
业务号能够循环推送内容库中的内容。当内容库所有内容循环推送完毕后,若有新内容则优先推送新内容,若无新内容则继续循环推送。
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onChange({ accountType: "personal" })}
|
||||
className={`w-full h-12 justify-between rounded-lg ${
|
||||
formData.accountType === "personal"
|
||||
? "bg-blue-600 hover:bg-blue-600 text-white"
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
人设号
|
||||
<HelpCircle
|
||||
className={`h-4 w-4 ${formData.accountType === "personal" ? "text-white/70" : "text-gray-400"}`}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>用于实时更新同步,有新动态时进行同步,无动态则不同步。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-base font-medium">是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ enabled: checked })}
|
||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -157,4 +162,3 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Search, RefreshCw } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { ImeiDisplay } from "@/components/ImeiDisplay"
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
@@ -124,15 +123,12 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{device.name}</span>
|
||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
||||
<Badge variant={device.status === "online" ? "success" : "secondary"}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">IMEI:</span>
|
||||
<ImeiDisplay imei={device.imei} containerWidth={160} />
|
||||
</div>
|
||||
<div>IMEI: {device.imei}</div>
|
||||
<div>微信号: {device.wxid}</div>
|
||||
</div>
|
||||
{device.usedInPlans > 0 && (
|
||||
@@ -147,4 +143,3 @@ export function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onS
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="w-12 text-center">{formData.syncCount}</span>
|
||||
<span className="w-12 text-center bg-primary text-primary-foreground rounded-md px-3 py-1 font-medium">
|
||||
{formData.syncCount}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@@ -135,4 +137,3 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Search, RefreshCw, X, ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { Search, RefreshCw, X } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import { ImeiDisplay } from "@/components/ImeiDisplay"
|
||||
|
||||
// 定义类型,避免导入错误
|
||||
interface Device {
|
||||
@@ -149,16 +148,6 @@ export function DeviceSelector({ formData, onChange, onNext, onPrev }: DeviceSel
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevPage = () => {
|
||||
setCurrentPage((prev) => Math.max(1, prev - 1))
|
||||
}
|
||||
|
||||
const handleNextPage = () => {
|
||||
setCurrentPage((prev) => Math.min(Math.ceil(filteredDevices.length / itemsPerPage), prev + 1))
|
||||
}
|
||||
|
||||
const isLastPage = currentPage === Math.ceil(filteredDevices.length / itemsPerPage)
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
@@ -204,14 +193,15 @@ export function DeviceSelector({ formData, onChange, onNext, onPrev }: DeviceSel
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-medium truncate">{device.name}</div>
|
||||
<Badge variant={device.status === "online" ? "default" : "secondary"}>
|
||||
<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" ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 flex items-center">
|
||||
<span className="mr-1">IMEI:</span>
|
||||
<ImeiDisplay imei={device.imei} containerWidth={160} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">IMEI: {device.imei}</div>
|
||||
<div className="text-sm text-gray-500">微信号: {device.wechatId}</div>
|
||||
{device.usedInPlans > 0 && (
|
||||
<div className="text-sm text-orange-500">已用于 {device.usedInPlans} 个计划</div>
|
||||
@@ -221,25 +211,24 @@ export function DeviceSelector({ formData, onChange, onNext, onPrev }: DeviceSel
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationPrevious
|
||||
onClick={handlePrevPage}
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
/>
|
||||
{Array.from({ length: Math.ceil(filteredDevices.length / itemsPerPage) }).map((_, index) => (
|
||||
<PaginationItem key={index}>
|
||||
<PaginationLink
|
||||
onClick={() => setCurrentPage(index + 1)}
|
||||
isActive={currentPage === index + 1}
|
||||
>
|
||||
{index + 1}
|
||||
{Array.from({ length: Math.ceil(filteredDevices.length / itemsPerPage) }, (_, i) => i + 1).map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink onClick={() => setCurrentPage(page)} isActive={currentPage === page}>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationNext
|
||||
onClick={handleNextPage}
|
||||
disabled={isLastPage}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(Math.ceil(filteredDevices.length / itemsPerPage), prev + 1))
|
||||
}
|
||||
disabled={currentPage === Math.ceil(filteredDevices.length / itemsPerPage)}
|
||||
/>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
@@ -279,32 +268,7 @@ export function DeviceSelector({ formData, onChange, onNext, onPrev }: DeviceSel
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center space-x-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handlePrevPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-gray-500">
|
||||
{currentPage} / {Math.ceil(filteredDevices.length / itemsPerPage)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleNextPage}
|
||||
disabled={isLastPage}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function NewTrafficDistributionLoading() {
|
||||
<div className="flex items-center justify-between">
|
||||
{[1, 2, 3].map((step) => (
|
||||
<div key={step} className="flex flex-col items-center">
|
||||
<Skeleton className="w-8 h-8 rounded-full" />
|
||||
<Skeleton className="w-10 h-10 rounded-full" />
|
||||
<Skeleton className="h-4 w-16 mt-1" />
|
||||
</div>
|
||||
))}
|
||||
@@ -35,6 +35,7 @@ export default function NewTrafficDistributionLoading() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-4 w-48 mt-1" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
@@ -58,4 +59,3 @@ export default function NewTrafficDistributionLoading() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Switch } from "@/components/ui/switch"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { TrafficPoolSelector } from "@/app/components/traffic-pool-selector"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
|
||||
export default function NewTrafficDistributionPage() {
|
||||
const router = useRouter()
|
||||
@@ -50,7 +51,38 @@ export default function NewTrafficDistributionPage() {
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
if (currentStep === 1 && !formData.name) {
|
||||
toast({
|
||||
title: "请填写规则名称",
|
||||
description: "规则名称为必填项",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (currentStep === 2 && !formData.allDevices && (!formData.targetDevices || formData.targetDevices.length === 0)) {
|
||||
toast({
|
||||
title: "请选择设备",
|
||||
description: "请选择至少一台设备或选择所有设备",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (currentStep === 3 && !formData.selectedPool) {
|
||||
toast({
|
||||
title: "请选择流量池",
|
||||
description: "请选择一个流量池进行分发",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (currentStep < 3) {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
} else {
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -62,15 +94,13 @@ export default function NewTrafficDistributionPage() {
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// 这里处理表单提交逻辑
|
||||
console.log("提交表单数据:", formData)
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "流量分发规则已创建",
|
||||
})
|
||||
router.push("/workspace/traffic-distribution")
|
||||
}
|
||||
|
||||
const isStep1Valid = formData.name && formData.source
|
||||
const isStep2Valid = formData.targetGroups.length > 0 || formData.targetDevices.length > 0
|
||||
const isStep3Valid = true // 规则设置可以有默认值
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-white min-h-screen">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
@@ -118,7 +148,7 @@ export default function NewTrafficDistributionPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 步骤1:基本信息 */}
|
||||
{/* 步骤1:规则设定 */}
|
||||
{currentStep === 1 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -210,7 +240,7 @@ export default function NewTrafficDistributionPage() {
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex justify-end">
|
||||
<Button onClick={handleNext} disabled={!formData.name}>
|
||||
<Button onClick={handleNext}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -219,7 +249,7 @@ export default function NewTrafficDistributionPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 步骤2:目标设置 */}
|
||||
{/* 步骤2:选择设备 */}
|
||||
{currentStep === 2 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -282,10 +312,7 @@ export default function NewTrafficDistributionPage() {
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!formData.allDevices && (!formData.targetDevices || formData.targetDevices.length === 0)}
|
||||
>
|
||||
<Button onClick={handleNext}>
|
||||
下一步
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -294,7 +321,7 @@ export default function NewTrafficDistributionPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 步骤3:规则配置 */}
|
||||
{/* 步骤3:选择流量池 */}
|
||||
{currentStep === 3 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -422,4 +449,3 @@ export default function NewTrafficDistributionPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
31
Cunkebao/lib/toast.ts
Normal file
31
Cunkebao/lib/toast.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
// 创建提示元素
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed inset-0 flex items-center justify-center z-50`;
|
||||
|
||||
// 创建遮罩层
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'fixed inset-0';
|
||||
|
||||
// 创建内容框
|
||||
const content = document.createElement('div');
|
||||
content.className = `relative bg-black/50 p-4 rounded-lg shadow-lg text-white z-50 min-w-[300px] max-w-[80%]`;
|
||||
|
||||
// 创建消息
|
||||
const messageElement = document.createElement('p');
|
||||
messageElement.className = 'text-base text-center';
|
||||
messageElement.textContent = message;
|
||||
|
||||
// 组装元素
|
||||
content.appendChild(messageElement);
|
||||
toast.appendChild(overlay);
|
||||
toast.appendChild(content);
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 1500);
|
||||
};
|
||||
@@ -46,4 +46,13 @@ Route::group('v1/', function () {
|
||||
Route::group('traffic/pool', function () {
|
||||
Route::post('import', 'app\\cunkebao\\controller\\TrafficPool@importOrders'); // 导入订单标签
|
||||
});
|
||||
|
||||
// 工作台相关
|
||||
Route::group('workbench', function () {
|
||||
Route::post('create', 'app\\cunkebao\\controller\\WorkbenchController@create'); // 创建工作台
|
||||
Route::get('list', 'app\\cunkebao\\controller\\WorkbenchController@getList'); // 获取工作台列表
|
||||
Route::post('update-status', 'app\\cunkebao\\controller\\WorkbenchController@updateStatus'); // 更新工作台状态
|
||||
Route::delete('delete', 'app\\cunkebao\\controller\\WorkbenchController@delete'); // 删除工作台
|
||||
Route::post('copy', 'app\\cunkebao\\controller\\WorkbenchController@copy'); // 拷贝工作台
|
||||
});
|
||||
})->middleware(['jwt']);
|
||||
469
Server/application/cunkebao/controller/WorkbenchController.php
Normal file
469
Server/application/cunkebao/controller/WorkbenchController.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller;
|
||||
|
||||
use app\cunkebao\model\Workbench;
|
||||
use app\cunkebao\model\WorkbenchAutoLike;
|
||||
use app\cunkebao\model\WorkbenchMomentsSync;
|
||||
use app\cunkebao\model\WorkbenchGroupPush;
|
||||
use app\cunkebao\model\WorkbenchGroupCreate;
|
||||
use app\cunkebao\validate\Workbench as WorkbenchValidate;
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 工作台控制器
|
||||
*/
|
||||
class WorkbenchController extends Controller
|
||||
{
|
||||
/**
|
||||
* 工作台类型定义
|
||||
*/
|
||||
const TYPE_AUTO_LIKE = 1; // 自动点赞
|
||||
const TYPE_MOMENTS_SYNC = 2; // 朋友圈同步
|
||||
const TYPE_GROUP_PUSH = 3; // 群消息推送
|
||||
const TYPE_GROUP_CREATE = 4; // 自动建群
|
||||
|
||||
/**
|
||||
* 创建工作台
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return json(['code' => 400, 'msg' => '请求方式错误']);
|
||||
}
|
||||
|
||||
// 获取登录用户信息
|
||||
$userInfo = request()->userInfo;
|
||||
|
||||
// 获取请求参数
|
||||
$param = $this->request->post();
|
||||
|
||||
// 验证数据
|
||||
$validate = new WorkbenchValidate;
|
||||
if (!$validate->scene('create')->check($param)) {
|
||||
return json(['code' => 400, 'msg' => $validate->getError()]);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建工作台基本信息
|
||||
$workbench = new Workbench;
|
||||
$workbench->name = $param['name'];
|
||||
$workbench->type = $param['type'];
|
||||
$workbench->status = 1;
|
||||
$workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
|
||||
$workbench->userId = $userInfo['id'];
|
||||
$workbench->companyId = $userInfo['companyId'];
|
||||
$workbench->createTime = time();
|
||||
$workbench->updateTime = time();
|
||||
$workbench->save();
|
||||
|
||||
// 根据类型创建对应的配置
|
||||
switch ($param['type']) {
|
||||
case self::TYPE_AUTO_LIKE: // 自动点赞
|
||||
$config = new WorkbenchAutoLike;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$config->interval = $param['interval'];
|
||||
$config->maxLikes = $param['maxLikes'];
|
||||
$config->startTime = $param['startTime'];
|
||||
$config->endTime = $param['endTime'];
|
||||
$config->contentTypes = json_encode($param['contentTypes']);
|
||||
$config->devices = json_encode($param['devices']);
|
||||
$config->targetGroups = json_encode($param['targetGroups']);
|
||||
$config->tagOperator = $param['tagOperator'];
|
||||
$config->createTime = time();
|
||||
$config->updateTime = time();
|
||||
$config->save();
|
||||
break;
|
||||
|
||||
case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
|
||||
$config = new WorkbenchMomentsSync;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$config->syncInterval = $param['syncInterval'];
|
||||
$config->syncCount = $param['syncCount'];
|
||||
$config->syncType = $param['syncType'];
|
||||
$config->devices = json_encode($param['devices']);
|
||||
$config->targetGroups = json_encode($param['targetGroups']);
|
||||
$config->createTime = time();
|
||||
$config->updateTime = time();
|
||||
$config->save();
|
||||
break;
|
||||
|
||||
case self::TYPE_GROUP_PUSH: // 群消息推送
|
||||
$config = new WorkbenchGroupPush;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$config->pushInterval = $param['pushInterval'];
|
||||
$config->pushContent = json_encode($param['pushContent']);
|
||||
$config->pushTime = json_encode($param['pushTime']);
|
||||
$config->devices = json_encode($param['devices']);
|
||||
$config->targetGroups = json_encode($param['targetGroups']);
|
||||
$config->save();
|
||||
break;
|
||||
|
||||
case self::TYPE_GROUP_CREATE: // 自动建群
|
||||
$config = new WorkbenchGroupCreate;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$config->groupNamePrefix = $param['groupNamePrefix'];
|
||||
$config->maxGroups = $param['maxGroups'];
|
||||
$config->membersPerGroup = $param['membersPerGroup'];
|
||||
$config->devices = json_encode($param['devices']);
|
||||
$config->targetGroups = json_encode($param['targetGroups']);
|
||||
$config->createTime = time();
|
||||
$config->updateTime = time();
|
||||
$config->save();
|
||||
break;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作台列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$type = $this->request->param('type', '');
|
||||
$keyword = $this->request->param('name', '');
|
||||
|
||||
$where = [
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0]
|
||||
];
|
||||
|
||||
// 添加类型筛选
|
||||
if ($type !== '') {
|
||||
$where[] = ['type', '=', $type];
|
||||
}
|
||||
|
||||
// 添加名称模糊搜索
|
||||
if ($keyword !== '') {
|
||||
$where[] = ['name', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 定义关联关系
|
||||
$with = [
|
||||
'autoLike' => function($query) {
|
||||
$query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,targetGroups');
|
||||
},
|
||||
// 'momentsSync' => function($query) {
|
||||
// $query->field('workbenchId,syncInterval,syncCount,syncType,devices,targetGroups');
|
||||
// },
|
||||
// 'groupPush' => function($query) {
|
||||
// $query->field('workbenchId,pushInterval,pushContent,pushTime,devices,targetGroups');
|
||||
// },
|
||||
// 'groupCreate' => function($query) {
|
||||
// $query->field('workbenchId,groupNamePrefix,maxGroups,membersPerGroup,devices,targetGroups');
|
||||
// }
|
||||
];
|
||||
|
||||
$list = Workbench::where($where)
|
||||
->with($with)
|
||||
->field('id,name,type,status,autoStart,createTime,updateTime')
|
||||
->order('id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
->each(function ($item) {
|
||||
// 处理配置信息
|
||||
switch ($item->type) {
|
||||
case self::TYPE_AUTO_LIKE:
|
||||
if (!empty($item->autoLike)) {
|
||||
$item->config = $item->autoLike;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->targetGroups = json_decode($item->config->targetGroups, true);
|
||||
$item->config->contentTypes = json_decode($item->config->contentTypes, true);
|
||||
}
|
||||
unset($item->autoLike,$item->auto_like);
|
||||
break;
|
||||
case self::TYPE_MOMENTS_SYNC:
|
||||
if (!empty($item->momentsSync)) {
|
||||
$item->config = $item->momentsSync;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->targetGroups = json_decode($item->config->targetGroups, true);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_PUSH:
|
||||
if (!empty($item->groupPush)) {
|
||||
$item->config = $item->groupPush;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->targetGroups = json_decode($item->config->targetGroups, true);
|
||||
$item->config->pushContent = json_decode($item->config->pushContent, true);
|
||||
$item->config->pushTime = json_decode($item->config->pushTime, true);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_CREATE:
|
||||
if (!empty($item->groupCreate)) {
|
||||
$item->config = $item->groupCreate;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->targetGroups = json_decode($item->config->targetGroups, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
unset( $item->momentsSync, $item->groupPush, $item->groupCreate);
|
||||
return $item;
|
||||
});
|
||||
|
||||
$total = Workbench::where($where)->count();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作台详情
|
||||
* @param int $id 工作台ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
// 定义关联关系
|
||||
$with = [
|
||||
'autoLike' => function($query) {
|
||||
$query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,targetGroups');
|
||||
},
|
||||
'momentsSync' => function($query) {
|
||||
$query->field('workbenchId,syncInterval,syncCount,syncType,devices,targetGroups');
|
||||
},
|
||||
'groupPush' => function($query) {
|
||||
$query->field('workbenchId,pushInterval,pushContent,pushTime,devices,targetGroups');
|
||||
},
|
||||
'groupCreate' => function($query) {
|
||||
$query->field('workbenchId,groupNamePrefix,maxGroups,membersPerGroup,devices,targetGroups');
|
||||
}
|
||||
];
|
||||
|
||||
$workbench = Workbench::where([
|
||||
['id', '=', $id],
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0]
|
||||
])
|
||||
->field('id,name,type,status,autoStart,createTime,updateTime')
|
||||
->with($with)
|
||||
->find();
|
||||
|
||||
if (empty($workbench)) {
|
||||
return json(['code' => 404, 'msg' => '工作台不存在']);
|
||||
}
|
||||
|
||||
// 处理配置信息
|
||||
switch ($workbench->type) {
|
||||
case self::TYPE_AUTO_LIKE:
|
||||
if (!empty($workbench->autoLike)) {
|
||||
$workbench->config = $workbench->autoLike;
|
||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
|
||||
$workbench->config->contentTypes = explode(',', $workbench->config->contentTypes);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_MOMENTS_SYNC:
|
||||
if (!empty($workbench->momentsSync)) {
|
||||
$workbench->config = $workbench->momentsSync;
|
||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_PUSH:
|
||||
if (!empty($workbench->groupPush)) {
|
||||
$workbench->config = $workbench->groupPush;
|
||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
|
||||
$workbench->config->pushContent = json_decode($workbench->config->pushContent, true);
|
||||
$workbench->config->pushTime = json_decode($workbench->config->pushTime, true);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_CREATE:
|
||||
if (!empty($workbench->groupCreate)) {
|
||||
$workbench->config = $workbench->groupCreate;
|
||||
$workbench->config->devices = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
unset($workbench->autoLike, $workbench->momentsSync, $workbench->groupPush, $workbench->groupCreate);
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作台状态
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updateStatus()
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return json(['code' => 400, 'msg' => '请求方式错误']);
|
||||
}
|
||||
|
||||
$param = $this->request->post();
|
||||
|
||||
// 验证数据
|
||||
$validate = new WorkbenchValidate;
|
||||
if (!$validate->scene('update_status')->check($param)) {
|
||||
return json(['code' => 400, 'msg' => $validate->getError()]);
|
||||
}
|
||||
|
||||
$workbench = Workbench::where([
|
||||
['id', '=', $param['id']],
|
||||
['userId', '=', $this->request->userInfo['id']]
|
||||
])->find();
|
||||
|
||||
if (empty($workbench)) {
|
||||
return json(['code' => 404, 'msg' => '工作台不存在']);
|
||||
}
|
||||
|
||||
$workbench->status = !$workbench['status'];
|
||||
$workbench->save();
|
||||
|
||||
return json(['code' => 200, 'msg' => '更新成功']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除工作台(软删除)
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$workbench = Workbench::where([
|
||||
['id', '=', $id],
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0]
|
||||
])->find();
|
||||
|
||||
if (!$workbench) {
|
||||
return json(['code' => 404, 'msg' => '工作台不存在']);
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$workbench->isDel = 1;
|
||||
$workbench->deleteTime = date('Y-m-d H:i:s');
|
||||
$workbench->save();
|
||||
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝工作台
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function copy()
|
||||
{
|
||||
if (!$this->request->isPost()) {
|
||||
return json(['code' => 400, 'msg' => '请求方式错误']);
|
||||
}
|
||||
|
||||
$id = $this->request->post('id');
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
// 验证权限并获取原数据
|
||||
$workbench = Workbench::where([
|
||||
['id', '=', $id],
|
||||
['userId', '=', $this->request->userInfo['id']]
|
||||
])->find();
|
||||
|
||||
if (empty($workbench)) {
|
||||
return json(['code' => 404, 'msg' => '工作台不存在']);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建新的工作台基本信息
|
||||
$newWorkbench = new Workbench;
|
||||
$newWorkbench->name = $workbench->name . ' copy';
|
||||
$newWorkbench->type = $workbench->type;
|
||||
$newWorkbench->status = 1; // 新拷贝的默认启用
|
||||
$newWorkbench->autoStart = $workbench->autoStart;
|
||||
$newWorkbench->userId = $this->request->userInfo['id'];
|
||||
$newWorkbench->save();
|
||||
|
||||
// 根据类型拷贝对应的配置
|
||||
switch ($workbench->type) {
|
||||
case self::TYPE_AUTO_LIKE:
|
||||
$config = WorkbenchAutoLike::where('workbenchId', $id)->find();
|
||||
if ($config) {
|
||||
$newConfig = new WorkbenchAutoLike;
|
||||
$newConfig->workbenchId = $newWorkbench->id;
|
||||
$newConfig->interval = $config->interval;
|
||||
$newConfig->maxLikes = $config->maxLikes;
|
||||
$newConfig->startTime = $config->startTime;
|
||||
$newConfig->endTime = $config->endTime;
|
||||
$newConfig->contentTypes = $config->contentTypes;
|
||||
$newConfig->devices = $config->devices;
|
||||
$newConfig->targetGroups = $config->targetGroups;
|
||||
$newConfig->save();
|
||||
}
|
||||
break;
|
||||
case self::TYPE_MOMENTS_SYNC:
|
||||
$config = WorkbenchMomentsSync::where('workbenchId', $id)->find();
|
||||
if ($config) {
|
||||
$newConfig = new WorkbenchMomentsSync;
|
||||
$newConfig->workbenchId = $newWorkbench->id;
|
||||
$newConfig->syncInterval = $config->syncInterval;
|
||||
$newConfig->syncCount = $config->syncCount;
|
||||
$newConfig->syncType = $config->syncType;
|
||||
$newConfig->devices = $config->devices;
|
||||
$newConfig->targetGroups = $config->targetGroups;
|
||||
$newConfig->save();
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_PUSH:
|
||||
$config = WorkbenchGroupPush::where('workbenchId', $id)->find();
|
||||
if ($config) {
|
||||
$newConfig = new WorkbenchGroupPush;
|
||||
$newConfig->workbenchId = $newWorkbench->id;
|
||||
$newConfig->pushInterval = $config->pushInterval;
|
||||
$newConfig->pushContent = $config->pushContent;
|
||||
$newConfig->pushTime = $config->pushTime;
|
||||
$newConfig->devices = $config->devices;
|
||||
$newConfig->targetGroups = $config->targetGroups;
|
||||
$newConfig->save();
|
||||
}
|
||||
break;
|
||||
case self::TYPE_GROUP_CREATE:
|
||||
$config = WorkbenchGroupCreate::where('workbenchId', $id)->find();
|
||||
if ($config) {
|
||||
$newConfig = new WorkbenchGroupCreate;
|
||||
$newConfig->workbenchId = $newWorkbench->id;
|
||||
$newConfig->groupNamePrefix = $config->groupNamePrefix;
|
||||
$newConfig->maxGroups = $config->maxGroups;
|
||||
$newConfig->membersPerGroup = $config->membersPerGroup;
|
||||
$newConfig->devices = $config->devices;
|
||||
$newConfig->targetGroups = $config->targetGroups;
|
||||
$newConfig->save();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Server/application/cunkebao/model/Workbench.php
Normal file
58
Server/application/cunkebao/model/Workbench.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 工作台模型
|
||||
*/
|
||||
class Workbench extends Model
|
||||
{
|
||||
protected $pk = 'id';
|
||||
protected $name = 'workbenches';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
protected $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
// 创建时间获取器
|
||||
public function getCreateTimeAttr($value)
|
||||
{
|
||||
return $value ? date('Y-m-d', is_numeric($value) ? $value : strtotime($value)) : '';
|
||||
}
|
||||
|
||||
// 更新时间获取器
|
||||
public function getUpdateTimeAttr($value)
|
||||
{
|
||||
return $value ? date('Y-m-d', is_numeric($value) ? $value : strtotime($value)) : '';
|
||||
}
|
||||
|
||||
// 自动点赞配置关联
|
||||
public function autoLike()
|
||||
{
|
||||
return $this->hasOne('WorkbenchAutoLike', 'workbenchId', 'id');
|
||||
}
|
||||
|
||||
// 朋友圈同步配置关联
|
||||
public function momentsSync()
|
||||
{
|
||||
return $this->hasOne('WorkbenchMomentsSync', 'workbenchId', 'id');
|
||||
}
|
||||
|
||||
// 群消息推送配置关联
|
||||
public function groupPush()
|
||||
{
|
||||
return $this->hasOne('WorkbenchGroupPush', 'workbenchId', 'id');
|
||||
}
|
||||
|
||||
// 自动建群配置关联
|
||||
public function groupCreate()
|
||||
{
|
||||
return $this->hasOne('WorkbenchGroupCreate', 'workbenchId', 'id');
|
||||
}
|
||||
|
||||
}
|
||||
25
Server/application/cunkebao/model/WorkbenchAutoLike.php
Normal file
25
Server/application/cunkebao/model/WorkbenchAutoLike.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 自动点赞工作台模型
|
||||
*/
|
||||
class WorkbenchAutoLike extends Model
|
||||
{
|
||||
protected $pk = 'id';
|
||||
protected $name = 'workbench_auto_like';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 定义关联的工作台
|
||||
public function workbench()
|
||||
{
|
||||
return $this->belongsTo('Workbench', 'workbenchId', 'id');
|
||||
}
|
||||
}
|
||||
22
Server/application/cunkebao/model/WorkbenchGroupCreate.php
Normal file
22
Server/application/cunkebao/model/WorkbenchGroupCreate.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class WorkbenchGroupCreate extends Model
|
||||
{
|
||||
protected $pk = 'id';
|
||||
protected $name = 'workbench_group_create';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 定义关联的工作台
|
||||
public function workbench()
|
||||
{
|
||||
return $this->belongsTo('Workbench', 'workbenchId', 'id');
|
||||
}
|
||||
}
|
||||
22
Server/application/cunkebao/model/WorkbenchGroupPush.php
Normal file
22
Server/application/cunkebao/model/WorkbenchGroupPush.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class WorkbenchGroupPush extends Model
|
||||
{
|
||||
protected $pk = 'id';
|
||||
protected $name = 'workbench_group_push';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 定义关联的工作台
|
||||
public function workbench()
|
||||
{
|
||||
return $this->belongsTo('Workbench', 'workbenchId', 'id');
|
||||
}
|
||||
}
|
||||
22
Server/application/cunkebao/model/WorkbenchMomentsSync.php
Normal file
22
Server/application/cunkebao/model/WorkbenchMomentsSync.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class WorkbenchMomentsSync extends Model
|
||||
{
|
||||
protected $pk = 'id';
|
||||
protected $name = 'workbench_moments_sync';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 定义关联的工作台
|
||||
public function workbench()
|
||||
{
|
||||
return $this->belongsTo('Workbench', 'workbenchId', 'id');
|
||||
}
|
||||
}
|
||||
128
Server/application/cunkebao/validate/Workbench.php
Normal file
128
Server/application/cunkebao/validate/Workbench.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class Workbench extends Validate
|
||||
{
|
||||
// 工作台类型定义
|
||||
const TYPE_AUTO_LIKE = 1; // 自动点赞
|
||||
const TYPE_MOMENTS_SYNC = 2; // 朋友圈同步
|
||||
const TYPE_GROUP_PUSH = 3; // 群消息推送
|
||||
const TYPE_GROUP_CREATE = 4; // 自动建群
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'name' => 'require|max:100',
|
||||
'type' => 'require|in:1,2,3,4',
|
||||
//'autoStart' => 'require|boolean',
|
||||
// 自动点赞特有参数
|
||||
'interval' => 'requireIf:type,1|number|min:1',
|
||||
'maxLikes' => 'requireIf:type,1|number|min:1',
|
||||
'startTime' => 'requireIf:type,1|dateFormat:H:i',
|
||||
'endTime' => 'requireIf:type,1|dateFormat:H:i',
|
||||
'contentTypes' => 'requireIf:type,1|array|contentTypeEnum:text,image,video',
|
||||
// 朋友圈同步特有参数
|
||||
'syncInterval' => 'requireIf:type,2|number|min:1',
|
||||
'syncCount' => 'requireIf:type,2|number|min:1',
|
||||
'syncType' => 'requireIf:type,2|in:1,2,3,4',
|
||||
// 群消息推送特有参数
|
||||
'pushInterval' => 'requireIf:type,3|number|min:1',
|
||||
'pushContent' => 'requireIf:type,3|array',
|
||||
'pushTime' => 'requireIf:type,3|array',
|
||||
// 自动建群特有参数
|
||||
'groupNamePrefix' => 'requireIf:type,4|max:50',
|
||||
'maxGroups' => 'requireIf:type,4|number|min:1',
|
||||
'membersPerGroup' => 'requireIf:type,4|number|min:1',
|
||||
// 通用参数
|
||||
'devices' => 'require|array',
|
||||
'targetGroups' => 'require|array'
|
||||
];
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
protected $message = [
|
||||
'name.require' => '请输入任务名称',
|
||||
'name.max' => '任务名称最多100个字符',
|
||||
'type.require' => '请选择工作台类型',
|
||||
'type.in' => '工作台类型错误',
|
||||
'autoStart.require' => '请选择是否自动启动',
|
||||
'autoStart.boolean' => '自动启动参数必须为布尔值',
|
||||
// 自动点赞相关提示
|
||||
'interval.requireIf' => '请设置点赞间隔',
|
||||
'interval.number' => '点赞间隔必须为数字',
|
||||
'interval.min' => '点赞间隔必须大于0',
|
||||
'maxLikes.requireIf' => '请设置每日最大点赞数',
|
||||
'maxLikes.number' => '每日最大点赞数必须为数字',
|
||||
'maxLikes.min' => '每日最大点赞数必须大于0',
|
||||
'startTime.requireIf' => '请设置开始时间',
|
||||
'startTime.dateFormat' => '开始时间格式错误',
|
||||
'endTime.requireIf' => '请设置结束时间',
|
||||
'endTime.dateFormat' => '结束时间格式错误',
|
||||
'contentTypes.requireIf' => '请选择点赞内容类型',
|
||||
'contentTypes.array' => '点赞内容类型必须是数组',
|
||||
'contentTypes.contentTypeEnum' => '点赞内容类型只能是text、image、video',
|
||||
// 朋友圈同步相关提示
|
||||
'syncInterval.requireIf' => '请设置同步间隔',
|
||||
'syncInterval.number' => '同步间隔必须为数字',
|
||||
'syncInterval.min' => '同步间隔必须大于0',
|
||||
'syncCount.requireIf' => '请设置同步数量',
|
||||
'syncCount.number' => '同步数量必须为数字',
|
||||
'syncCount.min' => '同步数量必须大于0',
|
||||
'syncType.requireIf' => '请选择同步类型',
|
||||
'syncType.in' => '同步类型错误',
|
||||
// 群消息推送相关提示
|
||||
'pushInterval.requireIf' => '请设置推送间隔',
|
||||
'pushInterval.number' => '推送间隔必须为数字',
|
||||
'pushInterval.min' => '推送间隔必须大于0',
|
||||
'pushContent.requireIf' => '请设置推送内容',
|
||||
'pushContent.array' => '推送内容格式错误',
|
||||
'pushTime.requireIf' => '请设置推送时间',
|
||||
'pushTime.array' => '推送时间格式错误',
|
||||
// 自动建群相关提示
|
||||
'groupNamePrefix.requireIf' => '请设置群名称前缀',
|
||||
'groupNamePrefix.max' => '群名称前缀最多50个字符',
|
||||
'maxGroups.requireIf' => '请设置最大建群数量',
|
||||
'maxGroups.number' => '最大建群数量必须为数字',
|
||||
'maxGroups.min' => '最大建群数量必须大于0',
|
||||
'membersPerGroup.requireIf' => '请设置每个群的人数',
|
||||
'membersPerGroup.number' => '每个群的人数必须为数字',
|
||||
'membersPerGroup.min' => '每个群的人数必须大于0',
|
||||
// 通用提示
|
||||
'devices.require' => '请选择设备',
|
||||
'devices.array' => '设备格式错误',
|
||||
'targetGroups.require' => '请选择目标用户组',
|
||||
'targetGroups.array' => '目标用户组格式错误'
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'create' => ['name', 'type', 'autoStart', 'devices', 'targetGroups',
|
||||
'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
|
||||
'syncInterval', 'syncCount', 'syncType',
|
||||
'pushInterval', 'pushContent', 'pushTime',
|
||||
'groupNamePrefix', 'maxGroups', 'membersPerGroup'
|
||||
],
|
||||
'update_status' => ['id', 'status']
|
||||
];
|
||||
|
||||
/**
|
||||
* 自定义验证规则
|
||||
*/
|
||||
protected function contentTypeEnum($value, $rule, $data)
|
||||
{
|
||||
$allowTypes = explode(',', $rule);
|
||||
foreach ($value as $type) {
|
||||
if (!in_array($type, $allowTypes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user