From 9d374d991f17edf7a19af20959f37fed88ac0ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Wed, 9 Jul 2025 15:38:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E9=80=89=E6=8B=A9=E6=A0=B7=E5=BC=8F=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/LayoutWrapper.tsx | 37 +- nkebao/src/components/ui/checkbox.tsx | 10 +- nkebao/src/components/ui/input.tsx | 21 +- nkebao/src/components/ui/radio-group.tsx | 44 + nkebao/src/components/ui/scroll-area.tsx | 48 + nkebao/src/components/ui/switch.tsx | 4 +- nkebao/src/components/ui/toast.tsx | 46 +- .../pages/workspace/auto-like/NewAutoLike.tsx | 1600 ++++++++++++----- 8 files changed, 1331 insertions(+), 479 deletions(-) create mode 100644 nkebao/src/components/ui/radio-group.tsx create mode 100644 nkebao/src/components/ui/scroll-area.tsx diff --git a/nkebao/src/components/LayoutWrapper.tsx b/nkebao/src/components/LayoutWrapper.tsx index 21d43a23d..859eb2e7e 100644 --- a/nkebao/src/components/LayoutWrapper.tsx +++ b/nkebao/src/components/LayoutWrapper.tsx @@ -2,25 +2,12 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import BottomNav from './BottomNav'; -// 不需要底部导航的页面路径 -const NO_BOTTOM_NAV_PATHS = [ - '/login', - '/register', - '/forgot-password', - '/reset-password', - '/devices', - '/devices/', - '/wechat-accounts', - '/wechat-accounts/', - '/scenarios/new', - '/scenarios/', - '/plans/', - '/workspace/auto-group/', - '/workspace/moments-sync/', - '/workspace/traffic-distribution/', - '/workspace/auto-like', - '/404', - '/500' +// 配置需要底部导航的页面路径(白名单) +const BOTTOM_NAV_CONFIG = [ + '/', // 首页 + '/scenarios', // 场景获客 + '/workspace', // 工作台 + '/profile', // 我的 ]; interface LayoutWrapperProps { @@ -31,16 +18,20 @@ export default function LayoutWrapper({ children }: LayoutWrapperProps) { const location = useLocation(); // 检查当前路径是否需要底部导航 - const shouldShowBottomNav = !NO_BOTTOM_NAV_PATHS.some(path => - location.pathname.startsWith(path) - ); + const shouldShowBottomNav = BOTTOM_NAV_CONFIG.some(path => { + // 特殊处理首页路由 '/' + if (path === '/') { + return location.pathname === '/'; + } + return location.pathname === path; + }); // 如果是登录页面,直接渲染内容(不显示底部导航) if (location.pathname === '/login') { return <>{children}; } - // 其他页面显示底部导航 + // 只有在配置列表中的页面才显示底部导航 return (
diff --git a/nkebao/src/components/ui/checkbox.tsx b/nkebao/src/components/ui/checkbox.tsx index 87aa708fd..f5f8daaf9 100644 --- a/nkebao/src/components/ui/checkbox.tsx +++ b/nkebao/src/components/ui/checkbox.tsx @@ -2,6 +2,7 @@ import React from 'react'; interface CheckboxProps { checked?: boolean; + onCheckedChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void; disabled?: boolean; className?: string; @@ -10,17 +11,24 @@ interface CheckboxProps { export function Checkbox({ checked = false, + onCheckedChange, onChange, disabled = false, className = '', id }: CheckboxProps) { + const handleChange = (e: React.ChangeEvent) => { + const newChecked = e.target.checked; + onCheckedChange?.(newChecked); + onChange?.(newChecked); + }; + return ( onChange?.(e.target.checked)} + onChange={handleChange} disabled={disabled} className={`w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 ${className}`} /> diff --git a/nkebao/src/components/ui/input.tsx b/nkebao/src/components/ui/input.tsx index 9bc18656a..f76731134 100644 --- a/nkebao/src/components/ui/input.tsx +++ b/nkebao/src/components/ui/input.tsx @@ -4,30 +4,45 @@ interface InputProps { value?: string; onChange?: (e: React.ChangeEvent) => void; onKeyDown?: (e: React.KeyboardEvent) => void; + onClick?: (e: React.MouseEvent) => void; placeholder?: string; className?: string; readOnly?: boolean; + readonly?: boolean; id?: string; + type?: string; + min?: number; + max?: number; } export function Input({ value, onChange, onKeyDown, + onClick, placeholder, className = '', readOnly = false, - id + readonly = false, + id, + type = 'text', + min, + max }: InputProps) { + const isReadOnly = readOnly || readonly; + return ( ); diff --git a/nkebao/src/components/ui/radio-group.tsx b/nkebao/src/components/ui/radio-group.tsx new file mode 100644 index 000000000..db9147210 --- /dev/null +++ b/nkebao/src/components/ui/radio-group.tsx @@ -0,0 +1,44 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } \ No newline at end of file diff --git a/nkebao/src/components/ui/scroll-area.tsx b/nkebao/src/components/ui/scroll-area.tsx new file mode 100644 index 000000000..e39870840 --- /dev/null +++ b/nkebao/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } \ No newline at end of file diff --git a/nkebao/src/components/ui/switch.tsx b/nkebao/src/components/ui/switch.tsx index 0b2a85f7c..1d363fcc9 100644 --- a/nkebao/src/components/ui/switch.tsx +++ b/nkebao/src/components/ui/switch.tsx @@ -5,15 +5,17 @@ interface SwitchProps { onCheckedChange: (checked: boolean) => void; disabled?: boolean; className?: string; + id?: string; } -export function Switch({ checked, onCheckedChange, disabled = false, className = '' }: SwitchProps) { +export function Switch({ checked, onCheckedChange, disabled = false, className = '', id }: SwitchProps) { return (
); -} \ No newline at end of file +} + +// 直接导出toast函数,用于在组件中直接使用 +export const toast = (toastData: Omit) => { + // 这里需要确保ToastProvider已经包装了应用 + // 在实际使用中,应该通过useToast hook来调用 + console.warn('toast function called without context. Please use useToast hook instead.'); + + // 创建一个简单的DOM toast作为fallback + const toastElement = document.createElement('div'); + toastElement.className = `fixed top-4 right-4 z-50 max-w-sm w-full bg-white rounded-lg shadow-lg border p-4 transform transition-all duration-300 ${ + toastData.variant === 'destructive' + ? 'border-red-200 bg-red-50' + : 'border-gray-200' + }`; + + toastElement.innerHTML = ` +
+
+

+ ${toastData.title} +

+ ${toastData.description ? ` +

+ ${toastData.description} +

+ ` : ''} +
+ +
+ `; + + document.body.appendChild(toastElement); + + // 自动移除 + setTimeout(() => { + if (toastElement.parentElement) { + toastElement.remove(); + } + }, 5000); +}; \ No newline at end of file diff --git a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx b/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx index 8b9f1581b..3ba30dd0a 100644 --- a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx +++ b/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx @@ -1,451 +1,1151 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Card, CardContent, CardHeader, CardTitle } 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 { Badge } from '@/components/ui/badge'; -// import { Progress } from '@/components/ui/progress'; -import { Checkbox } from '@/components/ui/checkbox'; -// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import Layout from '@/components/Layout'; -import PageHeader from '@/components/PageHeader'; -import BottomNav from '@/components/BottomNav'; -import { useToast } from '@/components/ui/toast'; -import '@/components/Layout.css'; -import { createAutoLikeTask, CreateLikeTaskData } from '@/api/autoLike'; -import { ContentType } from '@/types/auto-like'; - -interface Device { - id: string; - name: string; - status: 'online' | 'offline'; - lastActive: string; -} - -interface TargetGroup { - id: string; - name: string; - count: number; - tags: string[]; -} - -export default function NewAutoLike() { - const navigate = useNavigate(); - const { toast } = useToast(); - const [currentStep, setCurrentStep] = useState(1); - const [formData, setFormData] = useState({ - name: '', - interval: 30, - maxLikes: 100, - friendMaxLikes: 10, - startTime: '09:00', - endTime: '18:00', - contentTypes: ['text', 'image'], - devices: [], - friends: [], - targetTags: [], - enableFriendTags: false, - friendTags: '', - }); - - // 模拟设备数据 - const devices: Device[] = [ - { id: '1', name: 'iPhone 14 Pro', status: 'online', lastActive: '2024-03-18 15:30:00' }, - { id: '2', name: 'iPhone 13', status: 'online', lastActive: '2024-03-18 15:25:00' }, - { id: '3', name: 'iPhone 12', status: 'offline', lastActive: '2024-03-18 14:20:00' }, - ]; - - // 模拟目标人群数据 - const targetGroups: TargetGroup[] = [ - { id: '1', name: 'VIP客户', count: 156, tags: ['VIP', '高价值'] }, - { id: '2', name: '活跃用户', count: 234, tags: ['活跃', '互动'] }, - { id: '3', name: '潜在客户', count: 89, tags: ['潜在', '新客户'] }, - ]; - - // 可用标签 - const availableTags = [ - 'VIP', '高价值', '活跃', '互动', '潜在', '新客户', '男性', '女性', '青年', '中年', '高收入', '中收入' - ]; - - const handleInputChange = (field: keyof CreateLikeTaskData, value: any) => { - setFormData((prev: CreateLikeTaskData) => ({ ...prev, [field]: value })); - }; - - const handleDeviceToggle = (deviceId: string) => { - setFormData((prev: CreateLikeTaskData) => ({ - ...prev, - devices: prev.devices.includes(deviceId) - ? prev.devices.filter((id: string) => id !== deviceId) - : [...prev.devices, deviceId] - })); - }; - - const handleGroupToggle = (groupId: string) => { - setFormData((prev: CreateLikeTaskData) => ({ - ...prev, - friends: prev.friends?.includes(groupId) - ? prev.friends.filter((id: string) => id !== groupId) - : [...(prev.friends || []), groupId] - })); - }; - - const handleTagToggle = (tag: string) => { - setFormData((prev: CreateLikeTaskData) => ({ - ...prev, - targetTags: prev.targetTags.includes(tag) - ? prev.targetTags.filter((t: string) => t !== tag) - : [...prev.targetTags, tag] - })); - }; - - const handleContentTypeToggle = (type: ContentType) => { - setFormData((prev: CreateLikeTaskData) => ({ - ...prev, - contentTypes: prev.contentTypes.includes(type) - ? prev.contentTypes.filter((t: ContentType) => t !== type) - : [...prev.contentTypes, type] - })); - }; - - const handleNext = () => { - if (currentStep === 1 && (!formData.name || formData.devices.length === 0)) { - toast({ - title: '请完善信息', - description: '请填写任务名称并选择至少一个设备', - variant: 'destructive', - }); - return; - } - if (currentStep === 2 && (!formData.friends || formData.friends.length === 0)) { - toast({ - title: '请选择目标人群', - description: '请至少选择一个目标人群', - variant: 'destructive', - }); - return; - } - setCurrentStep(prev => Math.min(prev + 1, 3)); - }; - - const handlePrev = () => { - setCurrentStep(prev => Math.max(prev - 1, 1)); - }; - - const handleSubmit = async () => { - try { - const response = await createAutoLikeTask(formData); - - if (response.code === 200) { - toast({ - title: '创建成功', - description: '自动点赞任务已创建', - }); - navigate('/workspace/auto-like'); - } else { - toast({ - title: '创建失败', - description: response.msg || '请稍后重试', - variant: 'destructive', - }); - } - } catch (error) { - console.error('创建失败:', error); - toast({ - title: '创建失败', - description: '请检查网络连接后重试', - variant: 'destructive', - }); - } - }; - - const steps = [ - { title: '基本设置', description: '配置任务基本信息' }, - { title: '目标人群', description: '选择点赞目标' }, - { title: '高级设置', description: '配置高级参数' }, - ]; - - return ( - - } - footer={} - > -
-
- {/* 步骤指示器 */} - - -
- {steps.map((step, index) => ( -
-
- {index + 1} -
-
-
{step.title}
-
{step.description}
-
- {index < steps.length - 1 && ( -
- )} -
- ))} -
- - - - {/* 步骤内容 */} - {currentStep === 1 && ( -
- - - 任务基本信息 - - -
- - handleInputChange('name', e.target.value)} - placeholder="请输入任务名称" - /> -
-
-
- - - - 选择执行设备 - - -
- {devices.map((device) => ( -
handleDeviceToggle(device.id)} - > - handleDeviceToggle(device.id)} - /> -
-
{device.name}
-
- 状态: {device.status === 'online' ? '在线' : '离线'} -
-
-
-
- ))} -
- - -
- )} - - {currentStep === 2 && ( -
- - - 选择目标人群 - - -
- {targetGroups.map((group) => ( -
handleGroupToggle(group.id)} - > - handleGroupToggle(group.id)} - /> -
-
{group.name}
-
- 人数: {group.count} | 标签: {group.tags.join(', ')} -
-
-
- ))} -
-
-
- - - - 选择标签 - - -
- {availableTags.map((tag) => ( - handleTagToggle(tag)} - > - {tag} - - ))} -
-
-
-
- )} - - {currentStep === 3 && ( -
- - - 点赞设置 - - -
-
- - handleInputChange('interval', parseInt(e.target.value))} - /> -
-
- - handleInputChange('maxLikes', parseInt(e.target.value))} - /> -
-
- - handleInputChange('friendMaxLikes', parseInt(e.target.value))} - /> -
-
- -
-
- - handleInputChange('startTime', e.target.value)} - /> -
-
- - handleInputChange('endTime', e.target.value)} - /> -
-
- -
- -
- {(['text', 'image', 'video', 'link'] as ContentType[]).map((type) => ( - handleContentTypeToggle(type)} - > - {type === 'text' ? '文字' : type === 'image' ? '图片' : type === 'video' ? '视频' : '链接'} - - ))} -
-
- -
- handleInputChange('enableFriendTags', checked)} - /> - -
- - {formData.enableFriendTags && ( -
- - handleInputChange('friendTags', e.target.value)} - placeholder="请输入要添加的标签" - /> -
- )} -
-
-
- )} - - {/* 操作按钮 */} -
- - -
- {currentStep < 3 ? ( - - ) : ( - - )} -
-
-
-
- - ); +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ChevronLeft, Search, Plus, Minus, Check, X, Tag as TagIcon } from 'lucide-react'; +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 { 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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { createAutoLikeTask } from '@/api/autoLike'; +import { ContentType } from '@/types/auto-like'; +import { useToast } from '@/components/ui/toast'; +import Layout from '@/components/Layout'; +import { fetchDeviceList } from '@/api/devices'; +import type { Device } from '@/types/device'; +import { useEffect } from 'react'; +import { get } from '@/api/request'; + +interface TagGroup { + id: string; + name: string; + tags: string[]; +} + +// 用于设备选择弹窗的简化设备类型 +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: 'online' | 'offline'; +} + +// 微信好友接口类型 +interface WechatFriend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + customer: string; +} + +// 好友列表API响应类型 +interface FriendsResponse { + code: number; + msg: string; + data: { + list: Array<{ + id: number; + nickname: string; + wechatId: string; + avatar?: string; + customer?: string; + }>; + total: number; + page: number; + limit: number; + }; +} + +// 获取好友列表API函数 +const fetchFriendsList = async (page: number = 1, limit: number = 20, deviceIds: string[]): Promise => { + if (deviceIds.length === 0) { + return { + code: 200, + msg: 'success', + data: { + list: [], + total: 0, + page, + limit + } + }; + } + + const deviceIdsParam = deviceIds.join(','); + return get(`/v1/friend?page=${page}&limit=${limit}&deviceIds=${deviceIdsParam}`); +}; + +// 修改CreateLikeTaskData接口,确保friends字段不是可选的 +interface CreateLikeTaskDataLocal { + name: string; + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; + targetTags: string[]; +} + +export default function NewAutoLike() { + const navigate = useNavigate(); + const { toast } = useToast(); + const [currentStep, setCurrentStep] = useState(1); + const [deviceDialogOpen, setDeviceDialogOpen] = useState(false); + const [friendDialogOpen, setFriendDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + interval: 5, + maxLikes: 200, + startTime: '08:00', + endTime: '22:00', + contentTypes: ['text', 'image', 'video'], + devices: [], + friends: [], // 确保初始化为空数组而不是undefined + targetTags: [], + friendMaxLikes: 10, + enableFriendTags: false, + friendTags: '', + }); + // 新增自动开启的独立状态 + const [autoEnabled, setAutoEnabled] = useState(false); + const [devices, setDevices] = useState([]); + + useEffect(() => { + fetchDeviceList(1, 100).then(res => { + if (res && res.data && Array.isArray(res.data.list)) { + setDevices(res.data.list.map(d => ({ + id: d.id?.toString() || '', + name: d.memo || d.imei || '', + imei: d.imei || '', + wechatId: d.wechatId || '', + status: d.alive === 1 ? 'online' : 'offline', + }))); + } + }); + }, []); + + // 标签组数据 + const [tagGroups] = useState([ + { + 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 handleUpdateFormData = (data: Partial) => { + setFormData((prev) => ({ ...prev, ...data })); + }; + + const handleNext = () => { + setCurrentStep((prev) => Math.min(prev + 1, 3)); + // 滚动到顶部 + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + const handlePrev = () => { + setCurrentStep((prev) => Math.max(prev - 1, 1)); + // 滚动到顶部 + const mainElement = document.querySelector('main'); + if (mainElement) { + mainElement.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + const handleComplete = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + try { + // 转换为API需要的格式 + const apiFormData = { + ...formData, + // 如果API需要其他转换,可以在这里添加 + }; + + const response = await createAutoLikeTask(apiFormData); + if (response.code === 200) { + toast({ + title: '创建成功', + description: '自动点赞任务已创建并开始执行', + }); + navigate('/workspace/auto-like'); + } else { + toast({ + title: '创建失败', + description: response.msg || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + toast({ + title: '创建失败', + description: '请检查网络连接后重试', + variant: 'destructive', + }); + } finally { + setIsSubmitting(false); + } + }; + + const header = ( +
+
+ +

新建自动点赞

+
+ +
+ ); + + return ( + +
+
+ +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( +
+
+ + setDeviceDialogOpen(true)} + value={formData.devices.length > 0 ? `已选择 ${formData.devices.length} 个设备` : ''} + /> +
+ + {formData.devices.length > 0 && ( +
+

已选择的设备

+
+ {formData.devices.map(deviceId => { + const device = devices.find(d => d.id === deviceId); + if (!device) return null; + return ( +
+
+
{device.name}
+
+ {device.status === 'online' ? '在线' : '离线'} +
+
+
+
IMEI: {device.imei}
+
微信号: {device.wechatId}
+
+
+ ); + })} +
+
+ )} + +
+ + +
+ + handleUpdateFormData({ devices })} + devices={devices} + /> +
+ )} + + {currentStep === 3 && ( +
+
+
+ + + + setFriendDialogOpen(true)} + value={formData.friends && formData.friends.length > 0 ? `已选择 ${formData.friends.length} 个好友` : ''} + /> +
+
+
+ + +
+ handleUpdateFormData({ friends })} + deviceIds={formData.devices} + /> +
+ )} +
+
+
+
+ ); +} + +// 步骤指示器组件 +interface StepIndicatorProps { + currentStep: number; +} + +function StepIndicator({ currentStep }: StepIndicatorProps) { + const steps = [ + { title: '基础设置', description: '设置点赞规则' }, + { title: '设备选择', description: '选择执行设备' }, + { title: '人群选择', description: '选择目标人群' }, + ]; + + return ( +
+
+
+ {steps.map((step, index) => ( +
+
+ {index < currentStep ? : index + 1} +
+
+
+ {step.title} +
+
{step.description}
+
+
+ ))} +
+
+
+
+
+
+ ); +} + +// 基础设置组件 +interface BasicSettingsProps { + formData: CreateLikeTaskDataLocal; + onChange: (data: Partial) => void; + onNext: () => void; + autoEnabled: boolean; + setAutoEnabled: (v: boolean) => void; +} + +function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled }: BasicSettingsProps) { + const handleContentTypeChange = (type: ContentType) => { + const currentTypes = [...formData.contentTypes]; + if (currentTypes.includes(type)) { + onChange({ contentTypes: currentTypes.filter((t) => t !== type) }); + } else { + onChange({ contentTypes: [...currentTypes, type] }); + } + }; + + const incrementInterval = () => { + onChange({ interval: Math.min(formData.interval + 5, 60) }); + }; + + const decrementInterval = () => { + onChange({ interval: Math.max(formData.interval - 5, 5) }); + }; + + const incrementMaxLikes = () => { + onChange({ maxLikes: Math.min(formData.maxLikes + 10, 500) }); + }; + + const decrementMaxLikes = () => { + onChange({ maxLikes: Math.max(formData.maxLikes - 10, 10) }); + }; + + return ( +
+
+ + onChange({ name: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+ +
+ +
+ +
+ onChange({ interval: Number.parseInt(e.target.value) || 5 })} + className="h-12 rounded-none border-x-0 border-gray-200 text-center" + /> +
+ 秒 +
+
+ +
+

设置两次点赞之间的最小时间间隔

+
+ +
+ +
+ +
+ onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })} + className="h-12 rounded-none border-x-0 border-gray-200 text-center" + /> +
+ 次/天 +
+
+ +
+

设置每天最多点赞的次数

+
+ +
+ +
+
+ onChange({ startTime: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+
+ onChange({ endTime: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +
+
+

设置每天可以点赞的时间段

+
+ +
+ +
+ {[ + { id: 'text' as ContentType, label: '文字' }, + { id: 'image' as ContentType, label: '图片' }, + { id: 'video' as ContentType, label: '视频' }, + ].map((type) => ( +
handleContentTypeChange(type.id)} + > + {type.label} +
+ ))} +
+

选择要点赞的内容类型

+
+ +
+
+ + onChange({ enableFriendTags: checked })} + /> +
+ {formData.enableFriendTags && ( + <> +
+ + onChange({ friendTags: e.target.value })} + className="h-12 rounded-xl border-gray-200" + /> +

只给有此标签的好友点赞

+
+ + )} +
+ +
+ + +
+ + +
+ ); +} + +// 设备选择对话框组件 +interface DeviceSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + devices: DeviceSelectionItem[]; +} + +function DeviceSelectionDialog({ open, onOpenChange, selectedDevices, onSelect, devices }: DeviceSelectionDialogProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + + const filteredDevices = devices.filter((device) => { + const matchesSearch = + device.name.toLowerCase().includes(searchQuery.toLowerCase()) || + device.imei.toLowerCase().includes(searchQuery.toLowerCase()) || + device.wechatId.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStatus = + statusFilter === 'all' || + (statusFilter === 'online' && device.status === 'online') || + (statusFilter === 'offline' && device.status === 'offline'); + + return matchesSearch && matchesStatus; + }); + + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter(id => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + + + + 选择设备 + + +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+ + +
+ {filteredDevices.map((device) => ( + + ))} +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +} + +// 标签选择器组件 +interface TagSelectorProps { + selectedTags: string[]; + tagOperator: 'and' | 'or'; + onTagsChange: (tags: string[]) => void; + onOperatorChange: (operator: 'and' | 'or') => void; + onBack: () => void; + onComplete: () => void; + tagGroups: TagGroup[]; +} + +function TagSelector({ + selectedTags, + tagOperator, + onTagsChange, + onOperatorChange, + onBack, + onComplete, + tagGroups, +}: TagSelectorProps) { + const [searchQuery, setSearchQuery] = useState(''); + 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 ( + + +
+
+
+

选择目标人群标签

+
+ +
+ + setSearchQuery(e.target.value)} + /> +
+ + + + {tagGroups.slice(0, 4).map((group) => ( + + {group.name} + + ))} + + {tagGroups.map((group) => ( + +
+ {group.tags.map((tag) => ( + toggleTag(tag)} + > + {selectedTags.includes(tag) && } + {tag} + + ))} +
+
+ ))} +
+ + +
+ {filteredTagGroups.length > 0 ? ( + filteredTagGroups.map((group) => ( +
+

{group.name}

+
+ {group.tags.map((tag) => ( +
+ toggleTag(tag)} + /> + +
+ ))} +
+
+ )) + ) : ( +
没有找到匹配的标签
+ )} +
+
+ +
+
+ + setCustomTag(e.target.value)} + className="pl-9" + placeholder="添加自定义标签" + onKeyDown={(e) => e.key === 'Enter' && addCustomTag()} + /> +
+ +
+
+ +
+

标签匹配逻辑

+

选择多个标签之间的匹配关系

+ + onOperatorChange(value as 'and' | 'or')} + className="flex flex-col space-y-2" + > +
+ + +
+
+ + +
+
+
+ +
+

已选择的标签

+
+ {selectedTags.length === 0 ? ( +

未选择任何标签

+ ) : ( +
+ {selectedTags.map((tag) => ( + + {tag} + + + ))} +
+ )} +
+
+ +
+ + +
+
+
+
+ ); +} + +// 微信好友选择弹窗组件 +interface FriendSelectionDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedFriends: string[]; + onSelect: (friends: string[]) => void; + deviceIds: string[]; +} + +function FriendSelectionDialog({ open, onOpenChange, selectedFriends = [], onSelect, deviceIds }: FriendSelectionDialogProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalFriends, setTotalFriends] = useState(0); + const [friends, setFriends] = useState([]); + const [loading, setLoading] = useState(false); + + // 获取好友列表 + useEffect(() => { + if (open && deviceIds.length > 0) { + fetchFriends(currentPage); + } + }, [open, currentPage, deviceIds]); + + // 当设备ID变化时,重置页码 + useEffect(() => { + if (deviceIds.length > 0) { + setCurrentPage(1); + } + }, [deviceIds]); + + // 获取好友列表API + const fetchFriends = async (page: number) => { + if (deviceIds.length === 0) return; + + setLoading(true); + try { + const res = await fetchFriendsList(page, 20, deviceIds); + + if (res && res.code === 200 && res.data) { + setFriends(res.data.list.map((friend) => ({ + id: friend.id?.toString() || '', + nickname: friend.nickname || '', + wechatId: friend.wechatId || '', + avatar: friend.avatar || '', + customer: friend.customer || '', + }))); + + setTotalFriends(res.data.total || 0); + setTotalPages(Math.ceil((res.data.total || 0) / 20)); + } + } catch (error) { + console.error('获取好友列表失败:', error); + } finally { + setLoading(false); + } + }; + + const filteredFriends = friends.filter(friend => + friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || + friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const handleFriendToggle = (friendId: string) => { + if (selectedFriends.includes(friendId)) { + onSelect(selectedFriends.filter(id => id !== friendId)); + } else { + onSelect([...selectedFriends, friendId]); + } + }; + + const handleSelectAll = () => { + // 选择当前页面上所有好友 + const currentPageIds = filteredFriends.map(friend => friend.id); + const newSelectedFriends = [...selectedFriends]; + + // 添加未选中的好友 + currentPageIds.forEach(id => { + if (!selectedFriends.includes(id)) { + newSelectedFriends.push(id); + } + }); + + onSelect(newSelectedFriends); + }; + + const handleUnselectAll = () => { + // 取消当前页面上所有好友的选择 + const currentPageIds = filteredFriends.map(friend => friend.id); + const newSelectedFriends = selectedFriends.filter(id => !currentPageIds.includes(id)); + onSelect(newSelectedFriends); + }; + + const handleConfirm = () => { + onOpenChange(false); + }; + + return ( + + +
+ 选择微信好友 + +
+ setSearchQuery(e.target.value)} + className="pl-10 py-2 rounded-full border-gray-200" + /> + + +
+
+ + + {loading ? ( +
+
加载中...
+
+ ) : filteredFriends.length > 0 ? ( +
+ {filteredFriends.map((friend) => ( + + ))} +
+ ) : ( +
+
+ {deviceIds.length === 0 ? '请先选择设备' : '没有找到好友'} +
+
+ )} +
+ +
+
+ 总计 {totalFriends} 个好友 +
+
+ + {currentPage} / {totalPages} + +
+
+ +
+ + +
+
+
+ ); } \ No newline at end of file