diff --git a/nkebao/package.json b/nkebao/package.json index 9f980db8..5780df46 100644 --- a/nkebao/package.json +++ b/nkebao/package.json @@ -25,7 +25,7 @@ "@radix-ui/react-scroll-area": "latest", "@radix-ui/react-select": "latest", "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slider": "latest", + "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "latest", "@radix-ui/react-tabs": "latest", @@ -65,7 +65,7 @@ "recharts": "latest", "regenerator-runtime": "latest", "sonner": "^1.7.4", - "tailwind-merge": "^2.5.5", + "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "tdesign-mobile-react": "^0.16.0", "vaul": "^0.9.6", diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index 5c88d4c4..8a00028d 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -61,6 +61,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/nkebao/src/api/autoLike.ts b/nkebao/src/api/autoLike.ts index aee536a9..d9b9056c 100644 --- a/nkebao/src/api/autoLike.ts +++ b/nkebao/src/api/autoLike.ts @@ -26,10 +26,28 @@ export async function fetchAutoLikeTasks(): Promise { // 获取单个任务详情 export async function fetchAutoLikeTaskDetail(id: string): Promise { try { - const res = await get>(`/v1/workbench/detail?id=${id}`); - if (res.code === 200 && res.data) { - return res.data; + console.log(`Fetching task detail for id: ${id}`); + // 使用any类型来处理可能的不同响应结构 + const res = await get(`/v1/workbench/detail?id=${id}`); + console.log('Task detail API response:', res); + + if (res.code === 200) { + // 检查响应中的data字段 + if (res.data) { + // 如果data是对象,直接返回 + if (typeof res.data === 'object') { + return res.data; + } else { + console.error('Task detail API response data is not an object:', res.data); + return null; + } + } else { + console.error('Task detail API response missing data field:', res); + return null; + } } + + console.error('Task detail API error:', res.msg || 'Unknown error'); return null; } catch (error) { console.error('获取任务详情失败:', error); diff --git a/nkebao/src/components/LayoutWrapper.tsx b/nkebao/src/components/LayoutWrapper.tsx index 21d43a23..859eb2e7 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 87aa708f..f5f8daaf 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 adfa67d6..f7673113 100644 --- a/nkebao/src/components/ui/input.tsx +++ b/nkebao/src/components/ui/input.tsx @@ -4,47 +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; - name?: string; type?: string; - required?: boolean; min?: number; max?: number; - step?: number; } export function Input({ value, onChange, onKeyDown, + onClick, placeholder, className = '', readOnly = false, + readonly = false, id, - name, type = 'text', - required = false, min, - max, - step, + 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 00000000..db914721 --- /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 00000000..e3987084 --- /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 0b2a85f7..1d363fcc 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 8b9f1581..ac6f0edf 100644 --- a/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx +++ b/nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx @@ -1,451 +1,1236 @@ -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, useEffect } from 'react'; +import { useNavigate, useParams } 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, updateAutoLikeTask, fetchAutoLikeTaskDetail } 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 { 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 { id } = useParams<{ id: string }>(); + const isEditMode = !!id; + const { toast } = useToast(); + const [currentStep, setCurrentStep] = useState(1); + const [deviceDialogOpen, setDeviceDialogOpen] = useState(false); + const [friendDialogOpen, setFriendDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(isEditMode); + 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', + }))); + } + }); + }, []); + + // 如果是编辑模式,获取任务详情 + useEffect(() => { + if (isEditMode && id) { + fetchTaskDetail(); + } + }, [id, isEditMode]); + + // 获取任务详情 + const fetchTaskDetail = async () => { + try { + const taskDetail = await fetchAutoLikeTaskDetail(id!); + console.log('Task detail response:', taskDetail); // 添加日志用于调试 + + if (taskDetail) { + // 使用类型断言处理可能的字段名称差异 + const taskAny = taskDetail as any; + // 处理可能的嵌套结构 + const config = taskAny.config || taskAny; + + setFormData({ + name: taskDetail.name || '', + interval: config.likeInterval || config.interval || 5, + maxLikes: config.maxLikesPerDay || config.maxLikes || 200, + startTime: config.timeRange?.start || config.startTime || '08:00', + endTime: config.timeRange?.end || config.endTime || '22:00', + contentTypes: config.contentTypes || ['text', 'image', 'video'], + devices: config.devices || [], + friends: config.friends || [], + targetTags: config.targetTags || [], + friendMaxLikes: config.friendMaxLikes || 10, + enableFriendTags: config.enableFriendTags || false, + friendTags: config.friendTags || '', + }); + + // 处理状态字段,使用双等号允许类型自动转换 + const status = taskAny.status; + setAutoEnabled(status == 1 || status == 'running'); + } else { + toast({ + title: '获取任务详情失败', + description: '无法找到该任务', + variant: 'destructive', + }); + navigate('/workspace/auto-like'); + } + } catch (error) { + console.error('获取任务详情出错:', error); // 添加错误日志 + toast({ + title: '获取任务详情失败', + description: '请检查网络连接后重试', + variant: 'destructive', + }); + navigate('/workspace/auto-like'); + } finally { + setIsLoading(false); + } + }; + + // 标签组数据 + 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需要其他转换,可以在这里添加 + }; + + let response; + if (isEditMode) { + // 编辑模式,调用更新API + response = await updateAutoLikeTask({ + ...apiFormData, + id: id! + }); + } else { + // 新建模式,调用创建API + response = await createAutoLikeTask(apiFormData); + } + + if (response.code === 200) { + toast({ + title: isEditMode ? '更新成功' : '创建成功', + description: isEditMode ? '自动点赞任务已更新' : '自动点赞任务已创建并开始执行', + }); + navigate('/workspace/auto-like'); + } else { + toast({ + title: isEditMode ? '更新失败' : '创建失败', + description: response.msg || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + toast({ + title: isEditMode ? '更新失败' : '创建失败', + description: '请检查网络连接后重试', + variant: 'destructive', + }); + } finally { + setIsSubmitting(false); + } + }; + + const header = ( +
+
+ +

{isEditMode ? '编辑自动点赞' : '新建自动点赞'}

+
+ +
+ ); + + if (isLoading) { + return ( + +
+
+
+

加载中...

+
+
+
+ ); + } + + 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 diff --git a/nkebao/src/pages/workspace/traffic-distribution/TrafficDistribution.tsx b/nkebao/src/pages/workspace/traffic-distribution/TrafficDistribution.tsx index e35f3ba2..134a2312 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/TrafficDistribution.tsx +++ b/nkebao/src/pages/workspace/traffic-distribution/TrafficDistribution.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, @@ -9,17 +9,9 @@ import { Clock, Edit, Trash2, - Eye, - Copy, - ChevronDown, - ChevronUp, - Settings, - // Calendar, + Pause, Users, Share2, - // CheckCircle, - // XCircle, - TrendingUp, } from 'lucide-react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -27,12 +19,13 @@ import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Progress } from '@/components/ui/progress'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; +// 不再使用 DropdownMenu 组件 +// import { +// DropdownMenu, +// DropdownMenuContent, +// DropdownMenuItem, +// DropdownMenuTrigger, +// } from '@/components/ui/dropdown-menu'; import Layout from '@/components/Layout'; import PageHeader from '@/components/PageHeader'; import BottomNav from '@/components/BottomNav'; @@ -61,18 +54,19 @@ interface DistributionRule { export default function TrafficDistribution() { const navigate = useNavigate(); const { toast } = useToast(); - const [expandedRuleId, setExpandedRuleId] = useState(null); + // 移除expandedRuleId状态 const [searchTerm, setSearchTerm] = useState(''); + const [isLoading, setIsLoading] = useState(false); const [tasks, setTasks] = useState([ { id: '1', - name: 'VIP客户流量分发', - deviceCount: 3, - totalTraffic: 1000, - distributedTraffic: 756, - lastDistributionTime: '2025-02-06 13:12:35', + name: '流量分发', + deviceCount: 2, + totalTraffic: 2, + distributedTraffic: 125, + lastDistributionTime: '2025-07-02 09:00', createTime: '2024-11-20 19:04:14', - creator: 'admin', + creator: '售前', status: 'running', distributionInterval: 300, maxDistributionPerDay: 2000, @@ -86,32 +80,9 @@ export default function TrafficDistribution() { priority: 'high', filterConditions: ['VIP客户', '高价值'], }, - { - id: '2', - name: '新客户流量分发', - deviceCount: 2, - totalTraffic: 500, - distributedTraffic: 234, - lastDistributionTime: '2024-03-04 14:09:35', - createTime: '2024-03-04 14:29:04', - creator: 'manager', - status: 'paused', - distributionInterval: 600, - maxDistributionPerDay: 1000, - timeRange: { start: '09:00', end: '21:00' }, - targetChannels: ['抖音', '快手'], - distributionRatio: { - '抖音': 60, - '快手': 40, - }, - priority: 'medium', - filterConditions: ['新客户', '潜在客户'], - }, ]); - const toggleExpand = (ruleId: string) => { - setExpandedRuleId(expandedRuleId === ruleId ? null : ruleId); - }; + // 移除展开功能 const handleDelete = (ruleId: string) => { const ruleToDelete = tasks.find((rule) => rule.id === ruleId); @@ -169,7 +140,67 @@ export default function TrafficDistribution() { const handleCreateNew = () => { navigate('/workspace/traffic-distribution/new'); + toast({ + title: '创建新分发', + description: '正在前往创建页面', + }); }; + + // 添加卡片菜单组件 + type CardMenuProps = { + onEdit: () => void; + onPause: () => void; + onDelete: () => void; + }; + + function CardMenu({ onEdit, onPause, onDelete }: CardMenuProps) { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + if (open) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
{ onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 编辑计划 +
+
{ onPause(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 暂停计划 +
+
{ onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 删除计划 +
+
+ )} +
+ ); + } const filteredRules = tasks.filter((rule) => rule.name.toLowerCase().includes(searchTerm.toLowerCase()), @@ -201,31 +232,32 @@ export default function TrafficDistribution() { } }; - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': - return 'bg-red-100 text-red-800'; - case 'medium': - return 'bg-yellow-100 text-yellow-800'; - case 'low': - return 'bg-green-100 text-green-800'; - default: - return 'bg-gray-100 text-gray-800'; - } - }; + // 模拟加载数据 + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + try { + // 这里可以添加实际的API调用 + // const response = await fetch('/api/traffic-distribution'); + // const data = await response.json(); + // setTasks(data); + + // 模拟加载延迟 + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (error) { + console.error('获取流量分发数据失败:', error); + toast({ + title: '获取数据失败', + description: '无法获取流量分发数据,请稍后重试', + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }; - const getPriorityText = (priority: string) => { - switch (priority) { - case 'high': - return '高'; - case 'medium': - return '中'; - case 'low': - return '低'; - default: - return '未知'; - } - }; + fetchData(); + }, [toast]); return ( + } /> @@ -251,7 +283,7 @@ export default function TrafficDistribution() {
setSearchTerm(e.target.value)} @@ -260,7 +292,7 @@ export default function TrafficDistribution() { -
@@ -268,189 +300,81 @@ export default function TrafficDistribution() { {/* 规则列表 */}
- {filteredRules.length === 0 ? ( + {isLoading ? ( + // 加载状态 +
+
+
+ ) : filteredRules.length === 0 ? ( -

暂无分发规则

-

创建您的第一个流量分发规则

+

暂无分发计划

+

创建您的第一个流量分发计划

) : ( filteredRules.map((rule) => ( - -
-
+ +
+

{rule.name}

- - {getStatusText(rule.status)} - - - 优先级: {getPriorityText(rule.priority)} - -
-
- toggleRuleStatus(rule.id)} - disabled={rule.status === 'completed'} - /> - - - - - - handleView(rule.id)}> - - 查看 - - handleEdit(rule.id)}> - - 编辑 - - handleCopy(rule.id)}> - - 复制 - - handleDelete(rule.id)}> - - 删除 - - - -
-
- -
-
-
执行设备:{rule.deviceCount} 个
-
目标渠道:{rule.targetChannels.length} 个
-
-
-
已分发:{rule.distributedTraffic}/{rule.totalTraffic}
-
创建人:{rule.creator}
-
-
- - {/* 分发进度 */} -
-
- 分发进度 - - {Math.round((rule.distributedTraffic / rule.totalTraffic) * 100)}% - -
- -
- -
-
- - 上次分发:{rule.lastDistributionTime} -
-
- 创建时间:{rule.createTime} - -
-
- - {expandedRuleId === rule.id && ( -
-
-
-
- -

基本设置

-
-
-
- 分发间隔: - {rule.distributionInterval} 秒 -
-
- 每日最大分发数: - {rule.maxDistributionPerDay} -
-
- 执行时间段: - - {rule.timeRange.start} - {rule.timeRange.end} - -
-
- 优先级: - {getPriorityText(rule.priority)} -
-
-
- -
-
- -

分发渠道

-
-
-
- {rule.targetChannels.map((channel) => ( - - {channel} - - ))} -
-
-
- -
-
- -

分发比例

-
-
- {Object.entries(rule.distributionRatio).map(([channel, ratio]) => ( -
- {channel}: - {ratio}% -
- ))} -
-
- -
-
- -

筛选条件

-
-
-
- {rule.filterConditions.map((condition) => ( - - {condition} - - ))} -
-
-
+
+ + 进行中 + + toggleRuleStatus(rule.id)} + disabled={rule.status === 'completed'} + /> + handleEdit(rule.id)} + onPause={() => toggleRuleStatus(rule.id)} + onDelete={() => handleDelete(rule.id)} + />
- )} +
+ + {/* 统计数据 - 第一行 */} +
+
+
2
+
分发账号
+
+
+
7
+
分发设备
+
+
+
ALL
+
流量池
+
+
+ + {/* 统计数据 - 第二行 */} +
+
+
125
+
日均分发量
+
+
+
2
+
总流量池数量
+
+
+ + {/* 底部信息 */} +
+
+ + 上次执行: {rule.lastDistributionTime} +
+
创建人: {rule.creator}
+
)) )}