【操盘手端】自动点赞提交

This commit is contained in:
Ghost
2025-04-10 16:40:30 +08:00
parent c7062445ab
commit e3d29f0935
29 changed files with 2863 additions and 1684 deletions

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View 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>
)
}