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 && (
-
-
-
- 点赞设置
-
-
-
-
-
-
-
-
-
- {(['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"
+ />
+
+ 次/天
+
+
+
+
+
设置每天最多点赞的次数
+
+
+
+
+
+
设置每天可以点赞的时间段
+
+
+
+
+
+ {[
+ { 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 (
+
+ );
+}
+
+// 标签选择器组件
+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 (
+
+ );
}
\ 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}
- toggleExpand(rule.id)}
- >
- {expandedRuleId === rule.id ? (
-
- ) : (
-
- )}
-
-
-
-
- {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)}
+ />
- )}
+
+
+ {/* 统计数据 - 第一行 */}
+
+
+ {/* 统计数据 - 第二行 */}
+
+
+ {/* 底部信息 */}
+
+
+
+ 上次执行: {rule.lastDistributionTime}
+
+
创建人: {rule.creator}
+
))
)}