feat: 本次提交更新内容如下
场景获客列表搞定
This commit is contained in:
150
Cunkebao/app/content/new/steps/basic-settings.tsx
Normal file
150
Cunkebao/app/content/new/steps/basic-settings.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { HelpCircle } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import type { ContentLibraryFormData } from "../page"
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: ContentLibraryFormData
|
||||
updateFormData: (field: string, value: any) => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function BasicSettings({ formData, updateFormData, onNext }: BasicSettingsProps) {
|
||||
const isFormValid = formData.name.trim() !== ""
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">基础设置</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">设置内容库的基本信息</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="name" className="text-base">
|
||||
内容库名称
|
||||
</Label>
|
||||
<span className="text-sm text-gray-500">必填</span>
|
||||
</div>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => updateFormData("name", e.target.value)}
|
||||
placeholder="请输入内容库名称"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
{formData.name.trim() === "" && <p className="text-sm text-red-500 mt-1">请输入内容库名称</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description" className="text-base">
|
||||
内容库描述
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => updateFormData("description", e.target.value)}
|
||||
placeholder="请输入内容库描述(选填)"
|
||||
className="mt-1.5 min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base">内容库类型</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-gray-400 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="text-sm">选择内容库的类型,不同类型的内容库适用于不同的场景。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={formData.type}
|
||||
onValueChange={(value) => updateFormData("type", value)}
|
||||
className="mt-2"
|
||||
>
|
||||
<div className="flex items-start space-x-2 p-3 border rounded-md">
|
||||
<RadioGroupItem value="friends" id="friends" className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="friends" className="font-medium">
|
||||
微信好友
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">从微信好友中收集内容,适用于个人互动和一对一营销</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 p-3 border rounded-md mt-2">
|
||||
<RadioGroupItem value="groups" id="groups" className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="groups" className="font-medium">
|
||||
微信群
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">从微信群中收集内容,适用于群组互动和社群营销</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 p-3 border rounded-md mt-2">
|
||||
<RadioGroupItem value="moments" id="moments" className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="moments" className="font-medium">
|
||||
朋友圈
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">从朋友圈中收集内容,适用于社交媒体营销和品牌曝光</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 p-3 border rounded-md mt-2">
|
||||
<RadioGroupItem value="mixed" id="mixed" className="mt-1" />
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="mixed" className="font-medium">
|
||||
混合来源
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">从多种来源收集内容,适用于全方位的内容收集和分析</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-md">
|
||||
<div>
|
||||
<Label htmlFor="enabled" className="font-medium">
|
||||
启用状态
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">设置内容库的初始状态,可以随时更改</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enabled"
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => updateFormData("enabled", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onNext} disabled={!isFormValid}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
150
Cunkebao/app/content/new/steps/content-settings.tsx
Normal file
150
Cunkebao/app/content/new/steps/content-settings.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { HelpCircle, FileText, ImageIcon, Video, LinkIcon, FileAudio } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import type { ContentLibraryFormData } from "../page"
|
||||
|
||||
interface ContentSettingsProps {
|
||||
formData: ContentLibraryFormData
|
||||
updateFormData: (field: string, value: any) => void
|
||||
onNext: () => void
|
||||
onPrevious: () => void
|
||||
}
|
||||
|
||||
export function ContentSettings({ formData, updateFormData, onNext, onPrevious }: ContentSettingsProps) {
|
||||
const contentTypes = [
|
||||
{ id: "text", name: "文本", icon: FileText, description: "收集文本内容,如聊天记录、文章等" },
|
||||
{ id: "image", name: "图片", icon: ImageIcon, description: "收集图片内容,如照片、截图等" },
|
||||
{ id: "video", name: "视频", icon: Video, description: "收集视频内容,如短视频、直播回放等" },
|
||||
{ id: "link", name: "链接", icon: LinkIcon, description: "收集链接内容,如网页链接、小程序链接等" },
|
||||
{ id: "audio", name: "音频", icon: FileAudio, description: "收集音频内容,如语音消息、音乐等" },
|
||||
]
|
||||
|
||||
const handleContentTypeToggle = (type: string) => {
|
||||
const currentTypes = [...formData.contentTypes]
|
||||
if (currentTypes.includes(type)) {
|
||||
updateFormData(
|
||||
"contentTypes",
|
||||
currentTypes.filter((t) => t !== type),
|
||||
)
|
||||
} else {
|
||||
updateFormData("contentTypes", [...currentTypes, type])
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoSyncChange = (checked: boolean) => {
|
||||
updateFormData("autoSync", checked)
|
||||
}
|
||||
|
||||
const handleSyncIntervalChange = (value: string) => {
|
||||
updateFormData("syncInterval", Number.parseInt(value))
|
||||
}
|
||||
|
||||
const isFormValid = formData.contentTypes.length > 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">内容设置</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">设置需要收集的内容类型和同步方式</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base">内容类型</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-gray-400 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="text-sm">选择需要收集的内容类型,可以多选。不同类型的内容将分别存储和管理。</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
{contentTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`flex items-start space-x-2 p-3 border rounded-md ${
|
||||
formData.contentTypes.includes(type.id) ? "bg-blue-50 border-blue-200" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<Checkbox
|
||||
id={`content-type-${type.id}`}
|
||||
checked={formData.contentTypes.includes(type.id)}
|
||||
onCheckedChange={() => handleContentTypeToggle(type.id)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`content-type-${type.id}`} className="font-medium flex items-center">
|
||||
<type.icon className="h-4 w-4 mr-2" />
|
||||
{type.name}
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">{type.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{formData.contentTypes.length === 0 && (
|
||||
<p className="text-sm text-red-500 mt-1">请至少选择一种内容类型</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-md">
|
||||
<div>
|
||||
<Label htmlFor="auto-sync" className="font-medium">
|
||||
自动同步
|
||||
</Label>
|
||||
<p className="text-sm text-gray-500 mt-1">启用后,系统将按照设定的时间间隔自动同步内容</p>
|
||||
</div>
|
||||
<Switch id="auto-sync" checked={formData.autoSync} onCheckedChange={handleAutoSyncChange} />
|
||||
</div>
|
||||
|
||||
{formData.autoSync && (
|
||||
<div>
|
||||
<Label htmlFor="sync-interval" className="text-base">
|
||||
同步间隔
|
||||
</Label>
|
||||
<Select value={formData.syncInterval.toString()} onValueChange={handleSyncIntervalChange}>
|
||||
<SelectTrigger id="sync-interval" className="mt-1.5">
|
||||
<SelectValue placeholder="选择同步间隔" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">每1小时</SelectItem>
|
||||
<SelectItem value="6">每6小时</SelectItem>
|
||||
<SelectItem value="12">每12小时</SelectItem>
|
||||
<SelectItem value="24">每天</SelectItem>
|
||||
<SelectItem value="168">每周</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onPrevious}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={onNext} disabled={!isFormValid}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
244
Cunkebao/app/content/new/steps/preview-confirm.tsx
Normal file
244
Cunkebao/app/content/new/steps/preview-confirm.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { FileText, ImageIcon, Video, LinkIcon, FileAudio, Tag, Clock, Check, X } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import type { ContentLibraryFormData } from "../page"
|
||||
|
||||
interface PreviewAndConfirmProps {
|
||||
formData: ContentLibraryFormData
|
||||
onSubmit: () => void
|
||||
onPrevious: () => void
|
||||
}
|
||||
|
||||
export function PreviewAndConfirm({ formData, onSubmit, onPrevious }: PreviewAndConfirmProps) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const getContentTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <FileText className="h-4 w-4" />
|
||||
case "image":
|
||||
return <ImageIcon className="h-4 w-4" />
|
||||
case "video":
|
||||
return <Video className="h-4 w-4" />
|
||||
case "link":
|
||||
return <LinkIcon className="h-4 w-4" />
|
||||
case "audio":
|
||||
return <FileAudio className="h-4 w-4" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getContentTypeName = (type: string) => {
|
||||
switch (type) {
|
||||
case "text":
|
||||
return "文本"
|
||||
case "image":
|
||||
return "图片"
|
||||
case "video":
|
||||
return "视频"
|
||||
case "link":
|
||||
return "链接"
|
||||
case "audio":
|
||||
return "音频"
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getLibraryTypeName = (type: string) => {
|
||||
switch (type) {
|
||||
case "friends":
|
||||
return "微信好友"
|
||||
case "groups":
|
||||
return "微信群"
|
||||
case "moments":
|
||||
return "朋友圈"
|
||||
case "mixed":
|
||||
return "混合来源"
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getSyncIntervalText = (hours: number) => {
|
||||
if (hours === 1) return "每1小时"
|
||||
if (hours === 6) return "每6小时"
|
||||
if (hours === 12) return "每12小时"
|
||||
if (hours === 24) return "每天"
|
||||
if (hours === 168) return "每周"
|
||||
return `每${hours}小时`
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await onSubmit()
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">预览确认</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">请确认内容库的设置信息,确认无误后点击"创建内容库"</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="basic">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="basic">基础信息</TabsTrigger>
|
||||
<TabsTrigger value="sources">来源信息</TabsTrigger>
|
||||
<TabsTrigger value="content">内容设置</TabsTrigger>
|
||||
<TabsTrigger value="tags">标签信息</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="p-4 border rounded-md mt-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">内容库名称</h3>
|
||||
<p className="mt-1">{formData.name}</p>
|
||||
</div>
|
||||
|
||||
{formData.description && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">内容库描述</h3>
|
||||
<p className="mt-1">{formData.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">内容库类型</h3>
|
||||
<p className="mt-1">{getLibraryTypeName(formData.type)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">启用状态</h3>
|
||||
<div className="flex items-center mt-1">
|
||||
{formData.enabled ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500 mr-1" />
|
||||
<span>已启用</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="h-4 w-4 text-red-500 mr-1" />
|
||||
<span>已禁用</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sources" className="p-4 border rounded-md mt-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">已选择的来源</h3>
|
||||
<div className="mt-2 space-y-2">
|
||||
{formData.sources.length > 0 ? (
|
||||
formData.sources.map((source) => (
|
||||
<div key={source.id} className="flex items-center space-x-2 p-2 border rounded-md">
|
||||
<div className="relative w-8 h-8 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={source.avatar || "/placeholder.svg"}
|
||||
alt={source.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{source.name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{source.type === "friend" ? "微信好友" : source.type === "group" ? "微信群" : "公众号"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">未选择任何来源</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">来源总数</h3>
|
||||
<p className="mt-1">{formData.sources.length} 个来源</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="content" className="p-4 border rounded-md mt-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">内容类型</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.contentTypes.map((type) => (
|
||||
<Badge key={type} variant="outline" className="flex items-center gap-1">
|
||||
{getContentTypeIcon(type)}
|
||||
{getContentTypeName(type)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">同步设置</h3>
|
||||
<div className="flex items-center mt-1">
|
||||
{formData.autoSync ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500 mr-1" />
|
||||
<span>自动同步</span>
|
||||
<Badge variant="outline" className="ml-2 flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{getSyncIntervalText(formData.syncInterval)}
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="h-4 w-4 text-red-500 mr-1" />
|
||||
<span>手动同步</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tags" className="p-4 border rounded-md mt-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">标签</h3>
|
||||
{formData.tags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="flex items-center gap-1">
|
||||
<Tag className="h-3 w-3" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 mt-1">未添加任何标签</p>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onPrevious}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? "创建中..." : "创建内容库"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
259
Cunkebao/app/content/new/steps/source-selection.tsx
Normal file
259
Cunkebao/app/content/new/steps/source-selection.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Search, Users, User, MessageCircle, AlertCircle } from "lucide-react"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import Image from "next/image"
|
||||
import type { ContentLibraryFormData } from "../page"
|
||||
|
||||
interface SourceSelectionProps {
|
||||
formData: ContentLibraryFormData
|
||||
updateFormData: (field: string, value: any) => void
|
||||
onNext: () => void
|
||||
onPrevious: () => void
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockFriends = [
|
||||
{ id: "f1", name: "张三", type: "friend", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "f2", name: "李四", type: "friend", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "f3", name: "王五", type: "friend", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "f4", name: "赵六", type: "friend", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "f5", name: "钱七", type: "friend", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
]
|
||||
|
||||
const mockGroups = [
|
||||
{ id: "g1", name: "产品讨论群", type: "group", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "g2", name: "市场营销群", type: "group", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "g3", name: "技术交流群", type: "group", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "g4", name: "客户服务群", type: "group", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
]
|
||||
|
||||
const mockOfficialAccounts = [
|
||||
{ id: "o1", name: "科技前沿", type: "official", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "o2", name: "营销之道", type: "official", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "o3", name: "数据分析", type: "official", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
]
|
||||
|
||||
export function SourceSelection({ formData, updateFormData, onNext, onPrevious }: SourceSelectionProps) {
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
formData.type === "friends" ? "friends" : formData.type === "groups" ? "groups" : "all",
|
||||
)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedSources, setSelectedSources] = useState<
|
||||
{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
avatar?: string
|
||||
}[]
|
||||
>(formData.sources)
|
||||
|
||||
// 根据内容库类型和当前标签过滤可选来源
|
||||
const getFilteredSources = () => {
|
||||
let sources = []
|
||||
|
||||
if (activeTab === "friends" || activeTab === "all") {
|
||||
sources = [...sources, ...mockFriends]
|
||||
}
|
||||
|
||||
if (activeTab === "groups" || activeTab === "all") {
|
||||
sources = [...sources, ...mockGroups]
|
||||
}
|
||||
|
||||
if (activeTab === "official" || activeTab === "all") {
|
||||
sources = [...sources, ...mockOfficialAccounts]
|
||||
}
|
||||
|
||||
// 应用搜索过滤
|
||||
if (searchQuery) {
|
||||
sources = sources.filter((source) => source.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
const handleSourceToggle = (source: any) => {
|
||||
const isSelected = selectedSources.some((s) => s.id === source.id)
|
||||
|
||||
if (isSelected) {
|
||||
setSelectedSources(selectedSources.filter((s) => s.id !== source.id))
|
||||
} else {
|
||||
setSelectedSources([...selectedSources, source])
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
updateFormData("sources", selectedSources)
|
||||
onNext()
|
||||
}
|
||||
|
||||
const filteredSources = getFilteredSources()
|
||||
const isFormValid = selectedSources.length > 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">来源选择</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">选择内容库的来源,可以是微信好友、微信群或公众号</p>
|
||||
</div>
|
||||
|
||||
{formData.type !== "mixed" && (
|
||||
<Alert variant="info" className="bg-blue-50 text-blue-700 border-blue-200">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{formData.type === "friends"
|
||||
? "您已选择微信好友类型的内容库,请选择需要收集内容的微信好友。"
|
||||
: formData.type === "groups"
|
||||
? "您已选择微信群类型的内容库,请选择需要收集内容的微信群。"
|
||||
: "您已选择朋友圈类型的内容库,请选择需要收集内容的来源。"}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索来源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue={activeTab} value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="friends" disabled={formData.type === "groups"}>
|
||||
<User className="h-4 w-4 mr-1" />
|
||||
好友
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="groups" disabled={formData.type === "friends"}>
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
群组
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="official">
|
||||
<MessageCircle className="h-4 w-4 mr-1" />
|
||||
公众号
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-4 border rounded-md">
|
||||
<div className="p-3 border-b bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">可选来源</span>
|
||||
<span className="text-xs text-gray-500">已选择 {selectedSources.length} 个来源</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="p-3 space-y-2">
|
||||
{filteredSources.length > 0 ? (
|
||||
filteredSources.map((source) => {
|
||||
const isSelected = selectedSources.some((s) => s.id === source.id)
|
||||
return (
|
||||
<div
|
||||
key={source.id}
|
||||
className={`flex items-center justify-between p-2 border rounded-md ${
|
||||
isSelected ? "bg-blue-50 border-blue-200" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`source-${source.id}`}
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => handleSourceToggle(source)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative w-8 h-8 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={source.avatar || "/placeholder.svg"}
|
||||
alt={source.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{source.name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{source.type === "friend"
|
||||
? "微信好友"
|
||||
: source.type === "group"
|
||||
? "微信群"
|
||||
: "公众号"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||
<p>未找到匹配的来源</p>
|
||||
<p className="text-sm mt-1">请尝试其他搜索关键词</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
{selectedSources.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label className="text-base mb-2 block">已选择的来源</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedSources.map((source) => (
|
||||
<div
|
||||
key={source.id}
|
||||
className="flex items-center space-x-2 bg-blue-50 border border-blue-200 rounded-md px-3 py-1"
|
||||
>
|
||||
<div className="relative w-5 h-5 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={source.avatar || "/placeholder.svg"}
|
||||
alt={source.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm">{source.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 rounded-full"
|
||||
onClick={() => handleSourceToggle(source)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onPrevious}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleNext} disabled={!isFormValid}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
151
Cunkebao/app/content/new/steps/tag-settings.tsx
Normal file
151
Cunkebao/app/content/new/steps/tag-settings.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tag, Plus, X } from "lucide-react"
|
||||
import type { ContentLibraryFormData } from "../page"
|
||||
|
||||
interface TagSettingsProps {
|
||||
formData: ContentLibraryFormData
|
||||
updateFormData: (field: string, value: any) => void
|
||||
onNext: () => void
|
||||
onPrevious: () => void
|
||||
}
|
||||
|
||||
// 预设标签
|
||||
const presetTags = ["营销素材", "产品介绍", "客户反馈", "行业资讯", "竞品分析", "培训资料", "案例分享", "活动宣传"]
|
||||
|
||||
export function TagSettings({ formData, updateFormData, onNext, onPrevious }: TagSettingsProps) {
|
||||
const [newTag, setNewTag] = useState("")
|
||||
const [tags, setTags] = useState<string[]>(formData.tags)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (!newTag.trim()) {
|
||||
setError("标签不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if (tags.includes(newTag.trim())) {
|
||||
setError("标签已存在")
|
||||
return
|
||||
}
|
||||
|
||||
setTags([...tags, newTag.trim()])
|
||||
setNewTag("")
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleRemoveTag = (tag: string) => {
|
||||
setTags(tags.filter((t) => t !== tag))
|
||||
}
|
||||
|
||||
const handleAddPresetTag = (tag: string) => {
|
||||
if (tags.includes(tag)) {
|
||||
return
|
||||
}
|
||||
setTags([...tags, tag])
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
updateFormData("tags", tags)
|
||||
onNext()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">标签设置</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">为内容库添加标签,便于后续管理和查找</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-base">添加标签</Label>
|
||||
<div className="flex mt-1.5">
|
||||
<div className="relative flex-1">
|
||||
<Tag className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
value={newTag}
|
||||
onChange={(e) => {
|
||||
setNewTag(e.target.value)
|
||||
setError(null)
|
||||
}}
|
||||
placeholder="输入标签名称"
|
||||
className="pl-9"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
handleAddTag()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" onClick={handleAddTag} className="ml-2" disabled={!newTag.trim()}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-base">已添加的标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="flex items-center gap-1 px-3 py-1.5">
|
||||
<Tag className="h-3 w-3" />
|
||||
{tag}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0 ml-1 rounded-full"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label className="text-base">推荐标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{presetTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="outline"
|
||||
className={`cursor-pointer hover:bg-gray-100 ${
|
||||
tags.includes(tag) ? "bg-blue-50 text-blue-700 border-blue-200" : ""
|
||||
}`}
|
||||
onClick={() => handleAddPresetTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button variant="outline" onClick={onPrevious}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user