feat: 功能迁移过来了,接下来优化样式

This commit is contained in:
许永平
2025-07-06 13:18:08 +08:00
parent ae6821d917
commit 2cdbd4dd73
14 changed files with 2288 additions and 127 deletions

View File

@@ -0,0 +1,45 @@
import React from 'react';
interface AvatarProps {
children: React.ReactNode;
className?: string;
}
export function Avatar({ children, className = '' }: AvatarProps) {
return (
<div className={`relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full ${className}`}>
{children}
</div>
);
}
interface AvatarImageProps {
src?: string;
alt?: string;
className?: string;
}
export function AvatarImage({ src, alt, className = '' }: AvatarImageProps) {
if (!src) return null;
return (
<img
src={src}
alt={alt || '头像'}
className={`aspect-square h-full w-full object-cover ${className}`}
/>
);
}
interface AvatarFallbackProps {
children: React.ReactNode;
className?: string;
}
export function AvatarFallback({ children, className = '' }: AvatarFallbackProps) {
return (
<div className={`flex h-full w-full items-center justify-center rounded-full bg-gray-100 text-gray-600 ${className}`}>
{children}
</div>
);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'secondary' | 'success' | 'destructive';
variant?: 'default' | 'secondary' | 'success' | 'destructive' | 'outline';
className?: string;
onClick?: (e: React.MouseEvent) => void;
}
@@ -19,7 +19,8 @@ export function Badge({
default: 'bg-blue-100 text-blue-800',
secondary: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
destructive: 'bg-red-100 text-red-800'
destructive: 'bg-red-100 text-red-800',
outline: 'border border-gray-300 bg-white text-gray-700'
};
const classes = `${baseClasses} ${variantClasses[variant]} ${className}`;

View File

@@ -0,0 +1,109 @@
import React, { useState, useRef, useEffect } from 'react';
interface DropdownMenuProps {
children: React.ReactNode;
}
export function DropdownMenu({ children }: DropdownMenuProps) {
return <>{children}</>;
}
interface DropdownMenuTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}
export function DropdownMenuTrigger({ children }: DropdownMenuTriggerProps) {
return <>{children}</>;
}
interface DropdownMenuContentProps {
children: React.ReactNode;
align?: 'start' | 'center' | 'end';
}
export function DropdownMenuContent({ children, align = 'end' }: DropdownMenuContentProps) {
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const trigger = triggerRef.current;
if (!trigger) return;
const handleClick = () => setIsOpen(!isOpen);
trigger.addEventListener('click', handleClick);
return () => {
trigger.removeEventListener('click', handleClick);
};
}, [isOpen]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(event.target as Node) &&
triggerRef.current &&
!triggerRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative">
<div ref={triggerRef}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
...child.props,
children: (
<>
{child.props.children}
{isOpen && (
<div
ref={contentRef}
className={`absolute z-50 mt-2 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-md ${
align === 'start' ? 'left-0' : align === 'center' ? 'left-1/2 transform -translate-x-1/2' : 'right-0'
}`}
>
{children}
</div>
)}
</>
)
});
}
return child;
})}
</div>
</div>
);
}
interface DropdownMenuItemProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function DropdownMenuItem({ children, onClick, disabled = false }: DropdownMenuItemProps) {
return (
<button
className={`relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 disabled:pointer-events-none disabled:opacity-50 ${
disabled ? 'cursor-not-allowed opacity-50' : ''
}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}

View File

@@ -3,6 +3,7 @@ import React from 'react';
interface InputProps {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
placeholder?: string;
className?: string;
readOnly?: boolean;
@@ -12,6 +13,7 @@ interface InputProps {
export function Input({
value,
onChange,
onKeyDown,
placeholder,
className = '',
readOnly = false,
@@ -23,6 +25,7 @@ export function Input({
type="text"
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={placeholder}
readOnly={readOnly}
className={`flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}

View File

@@ -0,0 +1,17 @@
import React from 'react';
interface ProgressProps {
value: number;
className?: string;
}
export function Progress({ value, className = '' }: ProgressProps) {
return (
<div className={`w-full bg-gray-200 rounded-full h-2 ${className}`}>
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
interface SwitchProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}
export function Switch({ checked, onCheckedChange, disabled = false, className = '' }: SwitchProps) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onCheckedChange(!checked)}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50
${checked ? 'bg-blue-600' : 'bg-gray-200'}
${className}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${checked ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
);
}

View File

@@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,5 +1,350 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronLeft, Filter, Search, RefreshCw, Plus, Edit, Trash2, Eye, MoreVertical } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { useToast } from '@/components/ui/toast';
import { get, del } from '@/api/request';
interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
interface LibraryListResponse {
list: ContentLibrary[];
total: number;
}
interface WechatGroupMember {
id: string;
nickname: string;
wechatId: string;
avatar: string;
gender?: 'male' | 'female';
role?: 'owner' | 'admin' | 'member';
joinTime?: string;
}
interface ContentLibrary {
id: string;
name: string;
source: 'friends' | 'groups';
targetAudience: {
id: string;
nickname: string;
avatar: string;
}[];
creator: string;
creatorName?: string;
itemCount: number;
lastUpdated: string;
enabled: boolean;
sourceFriends: string[];
sourceGroups: string[];
friendsData?: any[];
groupsData?: any[];
keywordInclude: string[];
keywordExclude: string[];
isEnabled: number;
aiPrompt: string;
timeEnabled: number;
timeStart: string;
timeEnd: string;
status: number;
createTime: string;
updateTime: string;
sourceType: number;
selectedGroupMembers?: WechatGroupMember[];
}
export default function Content() {
return <div></div>;
const navigate = useNavigate();
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState('all');
const [loading, setLoading] = useState(false);
const { toast } = useToast();
// 获取内容库列表
const fetchLibraries = useCallback(async () => {
setLoading(true);
try {
const queryParams = new URLSearchParams({
page: '1',
limit: '100',
...(searchQuery ? { keyword: searchQuery } : {}),
...(activeTab !== 'all' ? { sourceType: activeTab === 'friends' ? '1' : '2' } : {})
});
const response = await get<ApiResponse<LibraryListResponse>>(`/v1/content/library/list?${queryParams.toString()}`);
if (response.code === 200 && response.data) {
// 转换数据格式以匹配原有UI
const transformedLibraries = response.data.list.map((item: any) => {
const friendsData = Array.isArray(item.selectedFriends) ? item.selectedFriends : [];
const groupsData = Array.isArray(item.selectedGroups) ? item.selectedGroups : [];
const transformedItem: ContentLibrary = {
id: item.id,
name: item.name,
source: item.sourceType === 1 ? 'friends' : 'groups',
targetAudience: [
...friendsData.map((friend: any) => ({
id: friend.id,
nickname: friend.nickname || `好友${friend.id}`,
avatar: friend.avatar || '/placeholder.svg'
})),
...groupsData.map((group: any) => ({
id: group.id,
nickname: group.name || `群组${group.id}`,
avatar: group.avatar || '/placeholder.svg'
}))
],
creator: item.creatorName || '系统',
creatorName: item.creatorName,
itemCount: item.itemCount,
lastUpdated: item.updateTime,
enabled: item.isEnabled === 1,
sourceFriends: item.sourceFriends || [],
sourceGroups: item.sourceGroups || [],
friendsData: friendsData,
groupsData: groupsData,
keywordInclude: item.keywordInclude || [],
keywordExclude: item.keywordExclude || [],
isEnabled: item.isEnabled,
aiPrompt: item.aiPrompt || '',
timeEnabled: item.timeEnabled,
timeStart: item.timeStart || '',
timeEnd: item.timeEnd || '',
status: item.status,
createTime: item.createTime,
updateTime: item.updateTime,
sourceType: item.sourceType,
selectedGroupMembers: item.selectedGroupMembers || []
};
return transformedItem;
});
setLibraries(transformedLibraries);
} else {
toast({ title: '获取失败', description: response.msg || '获取内容库列表失败' });
}
} catch (error: any) {
console.error('获取内容库列表失败:', error);
toast({ title: '网络错误', description: error?.message || '请检查网络连接' });
} finally {
setLoading(false);
}
}, [searchQuery, activeTab, toast]);
useEffect(() => {
fetchLibraries();
}, [fetchLibraries]);
const handleCreateNew = () => {
navigate('/content/new');
};
const handleEdit = (id: string) => {
navigate(`/content/${id}/edit`);
};
const handleDelete = async (id: string) => {
try {
const response = await del<ApiResponse>(`/v1/content/library/delete?id=${id}`);
if (response.code === 200) {
toast({ title: '删除成功', description: '内容库已删除' });
fetchLibraries();
} else {
toast({ title: '删除失败', description: response.msg || '删除失败' });
}
} catch (error: any) {
console.error('删除内容库失败:', error);
toast({ title: '网络错误', description: error?.message || '请检查网络连接' });
}
};
const handleViewMaterials = (id: string) => {
navigate(`/content/${id}/materials`);
};
const handleSearch = () => {
fetchLibraries();
};
const handleRefresh = () => {
fetchLibraries();
};
const filteredLibraries = libraries.filter(
(library) =>
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
library.targetAudience.some((target) => target.nickname.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<div className="flex-1 bg-gray-50 min-h-screen">
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-lg font-medium"></h1>
</div>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</header>
<div className="p-4 space-y-4">
<Card className="p-4">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索内容库..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-9"
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Filter className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleRefresh}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="all"></TabsTrigger>
<TabsTrigger value="friends"></TabsTrigger>
<TabsTrigger value="groups"></TabsTrigger>
</TabsList>
</Tabs>
<div className="space-y-3">
{loading ? (
<div className="flex justify-center items-center py-12">
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin" />
</div>
) : filteredLibraries.length === 0 ? (
<div className="flex justify-center items-center py-12">
<div className="text-center">
<p className="text-gray-500 mb-4"></p>
<Button onClick={handleCreateNew} size="sm">
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
) : (
filteredLibraries.map((library) => (
<Card key={library.id} className="p-4 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<h3 className="font-medium text-base">{library.name}</h3>
<Badge variant={library.isEnabled === 1 ? 'default' : 'secondary'} className="text-xs">
{library.isEnabled === 1 ? '已启用' : '未启用'}
</Badge>
</div>
<div className="text-sm text-gray-500 space-y-1">
<div className="flex items-center space-x-1">
<span></span>
{library.sourceType === 1 && library.sourceFriends?.length > 0 ? (
<div className="flex -space-x-1 overflow-hidden">
{(library.friendsData || []).slice(0, 3).map((friend) => (
<img
key={friend.id}
src={friend.avatar || '/placeholder.svg'}
alt={friend.nickname || `好友${friend.id}`}
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
/>
))}
{library.sourceFriends.length > 3 && (
<span className="flex items-center justify-center h-6 w-6 rounded-full bg-gray-200 text-xs font-medium text-gray-800">
+{library.sourceFriends.length - 3}
</span>
)}
</div>
) : library.sourceType === 2 && library.sourceGroups?.length > 0 ? (
<div className="flex items-center space-x-2">
<div className="flex -space-x-1 overflow-hidden">
{(library.groupsData || []).slice(0, 3).map((group) => (
<img
key={group.id}
src={group.avatar || '/placeholder.svg'}
alt={group.name || `群组${group.id}`}
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
/>
))}
{library.sourceGroups.length > 3 && (
<span className="flex items-center justify-center h-6 w-6 rounded-full bg-gray-200 text-xs font-medium text-gray-800">
+{library.sourceGroups.length - 3}
</span>
)}
</div>
</div>
) : (
<div className="w-6 h-6 bg-gray-200 rounded-full"></div>
)}
</div>
<div>{library.creator}</div>
<div>{library.itemCount}</div>
<div>{new Date(library.updateTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEdit(library.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(library.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewMaterials(library.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Card>
))
)}
</div>
</div>
</Card>
</div>
</div>
);
}

View File

@@ -1,67 +1,50 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronRight, Settings, Bell, LogOut } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useAuth } from '@/contexts/AuthContext';
import { useToast } from '@/components/ui/toast';
import { LogOut, User, Settings, Shield, Bell } from 'lucide-react';
const menuItems = [
{ href: '/devices', label: '设备管理' },
{ href: '/wechat-accounts', label: '微信号管理' },
{ href: '/traffic-pool', label: '流量池' },
{ href: '/content', label: '内容库' },
];
export default function Profile() {
const navigate = useNavigate();
const { user, logout, isAuthenticated } = useAuth();
const { toast } = useToast();
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
const [userInfo, setUserInfo] = useState<any>(null);
// 从localStorage获取用户信息
useEffect(() => {
const userInfoStr = localStorage.getItem('userInfo');
if (userInfoStr) {
setUserInfo(JSON.parse(userInfoStr));
}
}, []);
const handleLogout = () => {
// 清除本地存储的用户信息
localStorage.removeItem('token');
localStorage.removeItem('token_expired');
localStorage.removeItem('s2_accountId');
localStorage.removeItem('userInfo');
setShowLogoutDialog(false);
logout();
navigate('/login');
toast({
title: '已退出登录',
description: '感谢使用存客宝',
});
};
const menuItems = [
{
icon: <User className="h-5 w-5" />,
title: '个人信息',
description: '查看和编辑个人资料',
onClick: () => {
toast({
title: '功能开发中',
description: '个人信息编辑功能正在开发中',
});
}
},
{
icon: <Settings className="h-5 w-5" />,
title: '账户设置',
description: '密码、安全设置等',
onClick: () => {
toast({
title: '功能开发中',
description: '账户设置功能正在开发中',
});
}
},
{
icon: <Shield className="h-5 w-5" />,
title: '隐私安全',
description: '隐私设置和安全选项',
onClick: () => {
toast({
title: '功能开发中',
description: '隐私安全功能正在开发中',
});
}
},
{
icon: <Bell className="h-5 w-5" />,
title: '消息通知',
description: '通知设置和消息管理',
onClick: () => {
toast({
title: '功能开发中',
description: '消息通知功能正在开发中',
});
}
}
];
if (!isAuthenticated) {
return (
<div className="flex h-screen items-center justify-center">
@@ -71,83 +54,102 @@ export default function Profile() {
}
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-md mx-auto space-y-6">
{/* 用户信息卡片 */}
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
{user?.avatar ? (
<img
src={user.avatar}
alt="头像"
className="w-16 h-16 rounded-full object-cover"
/>
) : (
<User className="h-8 w-8 text-blue-600" />
)}
</div>
<div className="flex-1">
<h2 className="text-xl font-semibold text-gray-900">
{user?.username || '用户'}
</h2>
<p className="text-gray-500">
{user?.account || '暂无手机号'}
</p>
{user?.s2_accountId && (
<p className="text-sm text-gray-400">
ID: {user.s2_accountId}
</p>
)}
</div>
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white pb-16">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="flex justify-between items-center p-4">
<h1 className="text-xl font-semibold text-blue-600"></h1>
<div className="flex items-center space-x-2">
<Button variant="ghost" size="icon">
<Settings className="w-5 h-5" />
</Button>
<Button variant="ghost" size="icon">
<Bell className="w-5 h-5" />
</Button>
</div>
</div>
</header>
{/* 菜单列表 */}
<div className="bg-white rounded-lg shadow-sm">
{menuItems.map((item, index) => (
<div key={index}>
<button
onClick={item.onClick}
className="w-full flex items-center space-x-4 p-4 hover:bg-gray-50 transition-colors"
>
<div className="text-gray-400">
{item.icon}
</div>
<div className="flex-1 text-left">
<h3 className="font-medium text-gray-900">{item.title}</h3>
<p className="text-sm text-gray-500">{item.description}</p>
</div>
<div className="text-gray-400">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
{index < menuItems.length - 1 && (
<div className="border-b border-gray-100 mx-4" />
)}
<div className="p-4 space-y-6">
{/* 用户信息卡片 */}
<Card className="p-6">
<div className="flex items-center space-x-4">
<Avatar className="w-20 h-20">
<AvatarImage src={userInfo?.avatar || user?.avatar || ''} />
<AvatarFallback>
{(userInfo?.username || user?.username || '用户').slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h2 className="text-xl font-semibold text-blue-600">
{userInfo?.username || user?.username || '用户'}
</h2>
<p className="text-gray-500">
: {userInfo?.account || user?.account || Math.floor(10000000 + Math.random() * 90000000).toString()}
</p>
<div className="mt-2">
<Button
variant="outline"
size="sm"
onClick={() => {
toast({
title: '功能开发中',
description: '编辑资料功能正在开发中',
});
}}
>
</Button>
</div>
</div>
</div>
</Card>
{/* 功能菜单 */}
<Card className="divide-y">
{menuItems.map((item) => (
<div
key={item.href || item.label}
className="p-4 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => (item.href ? navigate(item.href) : null)}
>
<div className="flex items-center">
<span>{item.label}</span>
</div>
<ChevronRight className="w-5 h-5 text-gray-400" />
</div>
))}
</div>
</Card>
{/* 退出登录按钮 */}
<div className="bg-white rounded-lg shadow-sm">
<button
onClick={handleLogout}
className="w-full flex items-center space-x-4 p-4 text-red-600 hover:bg-red-50 transition-colors"
>
<LogOut className="h-5 w-5" />
<span className="font-medium">退</span>
</button>
</div>
{/* 版本信息 */}
<div className="text-center text-sm text-gray-400">
<p> v1.0.0</p>
<p className="mt-1">© 2024 . All rights reserved.</p>
</div>
<Button
variant="ghost"
className="w-full text-red-500 hover:text-red-600 hover:bg-red-50 mt-6"
onClick={() => setShowLogoutDialog(true)}
>
<LogOut className="w-5 h-5 mr-2" />
退
</Button>
</div>
{/* 退出登录确认对话框 */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>退</DialogTitle>
<DialogDescription>
退退使
</DialogDescription>
</DialogHeader>
<div className="flex justify-end space-x-2 mt-4">
<Button variant="outline" onClick={() => setShowLogoutDialog(false)}>
</Button>
<Button variant="destructive" onClick={handleLogout}>
退
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,5 +1,173 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Search, RefreshCw } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useToast } from '@/components/ui/toast';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { get } from '@/api/request';
interface UserTag {
id: string;
name: string;
color: string;
}
interface TrafficUser {
id: string;
avatar: string;
nickname: string;
wechatId: string;
phone: string;
region: string;
note: string;
status: number;
addTime: string;
source: string;
assignedTo: string;
category: 'potential' | 'customer' | 'lost';
tags: UserTag[];
}
interface ApiResponse<T> {
code: number;
msg: string;
data: T;
}
interface TrafficPoolResponse {
list: TrafficUser[];
pagination: {
total: number;
current: number;
pageSize: number;
totalPages: number;
};
}
export default function TrafficPool() {
return <div></div>;
const [users, setUsers] = useState<TrafficUser[]>([]);
const [activeCategory, setActiveCategory] = useState('potential');
const [searchQuery, setSearchQuery] = useState('');
// 格式化时间
const formatDateTime = (dateString: string) => {
if (!dateString) return '--';
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-');
} catch (error) {
return dateString;
}
};
const { toast } = useToast();
// 获取流量池用户
const fetchUsers = useCallback(async () => {
try {
const params = new URLSearchParams({
page: '1',
limit: '30',
...(searchQuery ? { keyword: searchQuery } : {}),
});
const endpoint = activeCategory === 'customer' ? '/v1/traffic/pool/converted' : '/v1/traffic/pool';
const response = await get<ApiResponse<TrafficPoolResponse>>(`${endpoint}?${params.toString()}`);
if (response.code === 200 && response.data) {
setUsers(response.data.list);
} else {
toast({ title: '获取失败', description: response.msg || '获取流量池失败' });
}
} catch (error: any) {
toast({ title: '网络错误', description: error?.message || '请检查网络连接' });
}
}, [activeCategory, searchQuery, toast]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
return (
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white pb-16">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="flex justify-between items-center p-4">
<h1 className="text-xl font-semibold text-blue-600"></h1>
<Button variant="ghost" size="icon" onClick={fetchUsers}>
<RefreshCw className="w-5 h-5" />
</Button>
</div>
</header>
<div className="p-4 space-y-4">
<div className="flex gap-2 items-center">
<Input
placeholder="搜索昵称/微信号/手机号"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-64"
/>
<Button variant="outline" onClick={fetchUsers}>
<Search className="w-4 h-4 mr-1" />
</Button>
</div>
<Tabs value={activeCategory} onValueChange={setActiveCategory} className="mt-2">
<TabsList>
<TabsTrigger value="potential"></TabsTrigger>
<TabsTrigger value="customer"></TabsTrigger>
<TabsTrigger value="lost"></TabsTrigger>
</TabsList>
</Tabs>
<Card className="mt-4">
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
<th className="px-2 py-2"></th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id} className="border-b hover:bg-gray-50">
<td className="px-2 py-2">
<Avatar className="w-10 h-10">
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.nickname?.slice(0, 2) || '用户'}</AvatarFallback>
</Avatar>
</td>
<td className="px-2 py-2">{user.nickname}</td>
<td className="px-2 py-2">{user.wechatId}</td>
<td className="px-2 py-2">{user.phone}</td>
<td className="px-2 py-2">{user.region}</td>
<td className="px-2 py-2">
{user.tags?.map(tag => (
<Badge key={tag.id} className={`mr-1`}>{tag.name}</Badge>
))}
</td>
<td className="px-2 py-2">{user.note}</td>
<td className="px-2 py-2">{formatDateTime(user.addTime)}</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-400"></td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
</div>
</div>
);
}

View File

@@ -1,5 +1,197 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ThumbsUp, MessageSquare, Send, Users, Share2, Brain, BarChart2, LineChart, Clock } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
export default function Workspace() {
return <div></div>;
// 模拟任务数据
const taskStats = {
total: 42,
inProgress: 12,
completed: 30,
todayTasks: 12,
activityRate: 98,
};
// 常用功能 - 保持原有排列
const commonFeatures = [
{
id: "auto-like",
name: "自动点赞",
description: "智能自动点赞互动",
icon: <ThumbsUp className="h-5 w-5 text-red-500" />,
path: "/workspace/auto-like",
bgColor: "bg-red-100",
isNew: true,
},
{
id: "moments-sync",
name: "朋友圈同步",
description: "自动同步朋友圈内容",
icon: <Clock className="h-5 w-5 text-purple-500" />,
path: "/workspace/moments-sync",
bgColor: "bg-purple-100",
},
{
id: "group-push",
name: "群消息推送",
description: "智能群发助手",
icon: <Send className="h-5 w-5 text-orange-500" />,
path: "/workspace/group-push",
bgColor: "bg-orange-100",
},
{
id: "auto-group",
name: "自动建群",
description: "智能拉好友建群",
icon: <Users className="h-5 w-5 text-green-500" />,
path: "/workspace/auto-group",
bgColor: "bg-green-100",
},
{
id: "traffic-distribution",
name: "流量分发",
description: "管理流量分发和分配",
icon: <Share2 className="h-5 w-5 text-blue-500" />,
path: "/workspace/traffic-distribution",
bgColor: "bg-blue-100",
},
{
id: "ai-assistant",
name: "AI对话助手",
description: "智能回复,提高互动质量",
icon: <MessageSquare className="h-5 w-5 text-blue-500" />,
path: "/workspace/ai-assistant",
bgColor: "bg-blue-100",
isNew: true,
},
];
// AI智能助手
const aiFeatures = [
{
id: "ai-analyzer",
name: "AI数据分析",
description: "智能分析客户行为特征",
icon: <BarChart2 className="h-5 w-5 text-indigo-500" />,
path: "/workspace/ai-analyzer",
bgColor: "bg-indigo-100",
isNew: true,
},
{
id: "ai-strategy",
name: "AI策略优化",
description: "智能优化获客策略",
icon: <Brain className="h-5 w-5 text-cyan-500" />,
path: "/workspace/ai-strategy",
bgColor: "bg-cyan-100",
isNew: true,
},
{
id: "ai-forecast",
name: "AI销售预测",
description: "智能预测销售趋势",
icon: <LineChart className="h-5 w-5 text-amber-500" />,
path: "/workspace/ai-forecast",
bgColor: "bg-amber-100",
},
];
return (
<div className="flex-1 p-4 bg-gray-50 pb-16">
<div className="max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-4"></h1>
{/* 任务统计卡片 */}
<div className="grid grid-cols-2 gap-3 mb-6">
<Card className="overflow-hidden">
<CardContent className="p-4">
<div className="text-sm text-gray-500"></div>
<div className="text-3xl font-bold text-blue-500 mt-1">{taskStats.total}</div>
<Progress value={(taskStats.inProgress / taskStats.total) * 100} className="h-2 mt-2 bg-blue-100" />
<div className="text-xs text-gray-500 mt-1">
: {taskStats.inProgress} / : {taskStats.completed}
</div>
</CardContent>
</Card>
<Card className="overflow-hidden">
<CardContent className="p-4">
<div className="text-sm text-gray-500"></div>
<div className="text-3xl font-bold text-green-500 mt-1">{taskStats.todayTasks}</div>
<div className="flex items-center mt-2">
<svg
className="w-4 h-4 text-green-500 mr-1"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12H7L10 19L14 5L17 12H21"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="text-sm"> {taskStats.activityRate}%</span>
</div>
</CardContent>
</Card>
</div>
{/* 常用功能 */}
<div className="mb-6">
<h2 className="text-lg font-medium mb-3"></h2>
<div className="grid grid-cols-2 gap-3">
{commonFeatures.map((feature) => (
<Link to={feature.path} key={feature.id}>
<Card className="overflow-hidden hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className={`w-10 h-10 rounded-lg ${feature.bgColor} flex items-center justify-center mb-3`}>
{feature.icon}
</div>
<div className="flex items-center">
<div className="font-medium">{feature.name}</div>
{feature.isNew && (
<Badge className="ml-2 bg-blue-100 text-blue-600 hover:bg-blue-100 border-0">New</Badge>
)}
</div>
<div className="text-xs text-gray-500 mt-1">{feature.description}</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
{/* AI智能助手 */}
<div>
<h2 className="text-lg font-medium mb-3">AI </h2>
<div className="grid grid-cols-2 gap-3">
{aiFeatures.map((feature) => (
<Link to={feature.path} key={feature.id}>
<Card className="overflow-hidden hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className={`w-10 h-10 rounded-lg ${feature.bgColor} flex items-center justify-center mb-3`}>
{feature.icon}
</div>
<div className="flex items-center">
<div className="font-medium">{feature.name}</div>
{feature.isNew && (
<Badge className="ml-2 bg-blue-100 text-blue-600 hover:bg-blue-100 border-0">New</Badge>
)}
</div>
<div className="text-xs text-gray-500 mt-1">{feature.description}</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,352 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Plus, Filter, Search, RefreshCw, MoreVertical, Users, Edit, Trash2, Eye, Copy } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Switch } from '@/components/ui/switch';
import PageHeader from '@/components/PageHeader';
import { useToast } from '@/components/ui/toast';
interface GroupTask {
id: string;
name: string;
status: number; // 1-运行中0-暂停
deviceCount: number;
groupCount: number;
memberCount: number;
lastCreateTime: string;
createTime: string;
creator: string;
creatorName: string;
config: {
devices: string[];
targetGroups: string[];
maxMembersPerGroup: number;
};
}
export default function AutoGroup() {
const navigate = useNavigate();
const { toast } = useToast();
const [tasks, setTasks] = useState<GroupTask[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
// 模拟数据
const mockTasks: GroupTask[] = [
{
id: '1',
name: '自动建群任务1',
status: 1,
deviceCount: 3,
groupCount: 15,
memberCount: 450,
lastCreateTime: '2024-03-18 16:30:00',
createTime: '2024-03-15 10:00:00',
creator: 'admin',
creatorName: '管理员',
config: {
devices: ['device1', 'device2', 'device3'],
targetGroups: ['VIP客户', '活跃用户'],
maxMembersPerGroup: 30
}
},
{
id: '2',
name: '自动建群任务2',
status: 0,
deviceCount: 2,
groupCount: 8,
memberCount: 240,
lastCreateTime: '2024-03-17 14:20:00',
createTime: '2024-03-14 15:30:00',
creator: 'user1',
creatorName: '用户1',
config: {
devices: ['device4', 'device5'],
targetGroups: ['新客户'],
maxMembersPerGroup: 25
}
}
];
// 获取任务列表
const fetchTasks = async () => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟搜索过滤
let filteredTasks = mockTasks;
if (searchQuery) {
filteredTasks = mockTasks.filter(task =>
task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
setTasks(filteredTasks);
setTotal(filteredTasks.length);
} catch (error: any) {
console.error('获取自动建群任务列表失败:', error);
toast({
title: '获取失败',
description: error?.message || '请检查网络连接',
});
} finally {
setIsLoading(false);
}
};
// 组件加载时获取任务列表
useEffect(() => {
fetchTasks();
}, [currentPage, pageSize]);
// 处理页码变化
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 搜索任务
const handleSearch = () => {
fetchTasks();
};
// 切换任务状态
const toggleTaskStatus = async (taskId: string, currentStatus: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const newStatus = currentStatus === 1 ? 0 : 1;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: newStatus } : task
)
);
toast({
title: '状态更新成功',
description: `任务已${newStatus === 1 ? '启用' : '暂停'}`,
});
} catch (error: any) {
console.error('更新任务状态失败:', error);
toast({
title: '更新失败',
description: error?.message || '更新任务状态失败',
});
}
};
// 执行删除
const handleDelete = async (taskId: string) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
setTasks(tasks.filter((task) => task.id !== taskId));
toast({
title: '删除成功',
description: '已成功删除建群任务',
});
} catch (error: any) {
console.error('删除任务失败:', error);
toast({
title: '删除失败',
description: error?.message || '删除任务失败',
});
}
};
// 编辑任务
const handleEdit = (taskId: string) => {
navigate(`/workspace/auto-group/${taskId}/edit`);
};
// 查看任务详情
const handleView = (taskId: string) => {
navigate(`/workspace/auto-group/${taskId}`);
};
// 复制任务
const handleCopy = async (taskId: string) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const taskToCopy = tasks.find(task => task.id === taskId);
if (taskToCopy) {
const newTask = {
...taskToCopy,
id: Date.now().toString(),
name: `${taskToCopy.name} (副本)`,
status: 0,
createTime: new Date().toLocaleString(),
groupCount: 0,
memberCount: 0,
lastCreateTime: '-'
};
setTasks([newTask, ...tasks]);
toast({
title: '复制成功',
description: '已成功复制建群任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
toast({
title: '复制失败',
description: error?.message || '复制任务失败',
});
}
};
// 过滤任务
const filteredTasks = tasks.filter(
(task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<PageHeader
title="自动建群"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/auto-group/new">
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
<div className="p-4">
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={fetchTasks} disabled={isLoading}>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</div>
</Card>
<div className="space-y-4">
{isLoading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : filteredTasks.length > 0 ? (
filteredTasks.map((task) => (
<Card key={task.id} className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="font-medium">{task.name}</h3>
<Badge variant={task.status === 1 ? 'default' : 'secondary'}>
{task.status === 1 ? '运行中' : '已暂停'}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(task.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="text-sm text-gray-500">
<div className="mb-1">{task.deviceCount} </div>
<div className="mb-1">{task.groupCount} </div>
<div>{task.creatorName}</div>
</div>
<div className="text-sm text-gray-500">
<div className="mb-1">{task.memberCount} </div>
<div className="mb-1">{task.lastCreateTime}</div>
<div>{task.createTime}</div>
</div>
</div>
<div className="border-t pt-4">
<div className="flex items-center text-sm text-gray-500">
<Users className="h-4 w-4 mr-2" />
<span>{task.config.targetGroups.join(', ')}</span>
</div>
</div>
</Card>
))
) : (
<div className="text-center py-12 text-gray-400">
{searchQuery ? '没有找到匹配的任务' : '暂无建群任务'}
</div>
)}
</div>
{/* 分页 */}
{total > pageSize && (
<div className="flex justify-center mt-6 space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1 || isLoading}
>
</Button>
<div className="flex items-center space-x-1">
<span className="text-sm text-gray-500"> {currentPage} </span>
<span className="text-sm text-gray-500"> {Math.ceil(total / pageSize)} </span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(Math.ceil(total / pageSize), currentPage + 1))}
disabled={currentPage >= Math.ceil(total / pageSize) || isLoading}
>
</Button>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,494 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Edit,
Trash2,
Eye,
Copy,
Settings,
Users,
ThumbsUp,
} from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Switch } from '@/components/ui/switch';
import { Progress } from '@/components/ui/progress';
import PageHeader from '@/components/PageHeader';
import { useToast } from '@/components/ui/toast';
interface TaskConfig {
id: number;
workbenchId: number;
interval: number;
maxLikes: number;
friendMaxLikes?: number;
startTime: string;
endTime: string;
contentTypes: string[];
devices: number[];
targetGroups: string[];
tagOperator: number;
createTime: string;
updateTime: string;
todayLikeCount?: number;
totalLikeCount?: number;
friends?: string[];
enableFriendTags?: boolean;
friendTags?: string;
}
interface Task {
id: number;
name: string;
type: number;
status: number;
autoStart: number;
createTime: string;
updateTime: string;
config: TaskConfig;
}
interface TaskListResponse {
code: number;
msg: string;
data: {
list: Task[];
total: number;
};
}
interface ApiResponse {
code: number;
msg: string;
}
export default function AutoLike() {
const navigate = useNavigate();
const { toast } = useToast();
const [expandedTaskId, setExpandedTaskId] = useState<number | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(false);
const [searchName, setSearchName] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
// 模拟数据
const mockTasks: Task[] = [
{
id: 1,
name: '智能点赞任务1',
type: 1,
status: 1,
autoStart: 1,
createTime: '2024-03-18 10:00:00',
updateTime: '2024-03-18 16:30:00',
config: {
id: 1,
workbenchId: 1,
interval: 30,
maxLikes: 100,
friendMaxLikes: 3,
startTime: '09:00',
endTime: '18:00',
contentTypes: ['text', 'image'],
devices: [1, 2, 3],
targetGroups: ['VIP客户', '活跃用户'],
tagOperator: 1,
createTime: '2024-03-18 10:00:00',
updateTime: '2024-03-18 16:30:00',
todayLikeCount: 45,
totalLikeCount: 1234,
friends: ['friend1', 'friend2', 'friend3'],
enableFriendTags: true,
friendTags: '重要客户'
}
},
{
id: 2,
name: '自动点赞任务2',
type: 1,
status: 2,
autoStart: 0,
createTime: '2024-03-17 14:20:00',
updateTime: '2024-03-18 12:15:00',
config: {
id: 2,
workbenchId: 2,
interval: 60,
maxLikes: 50,
friendMaxLikes: 2,
startTime: '10:00',
endTime: '20:00',
contentTypes: ['video'],
devices: [4, 5],
targetGroups: ['新客户'],
tagOperator: 2,
createTime: '2024-03-17 14:20:00',
updateTime: '2024-03-18 12:15:00',
todayLikeCount: 0,
totalLikeCount: 567,
friends: ['friend4', 'friend5'],
enableFriendTags: false
}
}
];
const fetchTasks = async (page: number, name?: string) => {
setLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟搜索过滤
let filteredTasks = mockTasks;
if (name) {
filteredTasks = mockTasks.filter(task =>
task.name.toLowerCase().includes(name.toLowerCase())
);
}
setTasks(filteredTasks);
setTotal(filteredTasks.length);
} catch (error: any) {
console.error('获取任务列表失败:', error);
toast({
title: '获取失败',
description: error?.message || '请检查网络连接',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks(currentPage, searchName);
}, [currentPage]);
const handleSearch = () => {
setCurrentPage(1);
fetchTasks(1, searchName);
};
const handleRefresh = () => {
fetchTasks(currentPage, searchName);
};
const toggleExpand = (taskId: number) => {
setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
};
const handleDelete = async (taskId: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
setTasks(tasks.filter(task => task.id !== taskId));
toast({
title: '删除成功',
description: '已成功删除点赞任务',
});
} catch (error: any) {
console.error('删除任务失败:', error);
toast({
title: '删除失败',
description: error?.message || '请检查网络连接',
});
}
};
const handleEdit = (taskId: number) => {
navigate(`/workspace/auto-like/${taskId}/edit`);
};
const handleView = (taskId: number) => {
navigate(`/workspace/auto-like/${taskId}`);
};
const handleCopy = async (taskId: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const taskToCopy = tasks.find(task => task.id === taskId);
if (taskToCopy) {
const newTask = {
...taskToCopy,
id: Date.now(),
name: `${taskToCopy.name} (副本)`,
status: 2,
createTime: new Date().toLocaleString(),
updateTime: new Date().toLocaleString(),
config: {
...taskToCopy.config,
id: Date.now(),
workbenchId: Date.now(),
todayLikeCount: 0,
totalLikeCount: 0,
createTime: new Date().toLocaleString(),
updateTime: new Date().toLocaleString(),
}
};
setTasks([newTask, ...tasks]);
toast({
title: '复制成功',
description: '已成功复制点赞任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
toast({
title: '复制失败',
description: error?.message || '请检查网络连接',
});
}
};
const toggleTaskStatus = async (taskId: number, currentStatus: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const newStatus = currentStatus === 1 ? 2 : 1;
setTasks(tasks.map(task =>
task.id === taskId
? { ...task, status: newStatus }
: task
));
toast({
title: '状态更新成功',
description: `任务${newStatus === 1 ? '已启动' : '已暂停'}`,
});
} catch (error: any) {
console.error('更新任务状态失败:', error);
toast({
title: '更新失败',
description: error?.message || '请检查网络连接',
});
}
};
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<PageHeader
title="自动点赞"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/auto-like/new">
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
<div className="p-4">
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={loading}>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</Card>
<div className="space-y-4">
{tasks.map((task) => (
<Card key={task.id} className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="font-medium">{task.name}</h3>
<Badge variant={task.status === 1 ? 'default' : 'secondary'}>
{task.status === 1 ? '进行中' : '已暂停'}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(task.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="text-sm text-gray-500">
<div className="mb-1">{task.config.devices.length} </div>
<div className="mb-1">{task.config.friends?.length || 0} </div>
<div>{task.updateTime}</div>
</div>
<div className="text-sm text-gray-500">
<div className="mb-1">{task.config.interval} </div>
<div className="mb-1">{task.config.maxLikes} </div>
<div>{task.createTime}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 border-t pt-4">
<div className="text-sm">
<div className="flex items-center">
<ThumbsUp className="h-4 w-4 mr-2 text-blue-500" />
<span className="text-gray-500"></span>
<span className="ml-1 font-medium">{task.config.todayLikeCount || 0} </span>
</div>
</div>
<div className="text-sm">
<div className="flex items-center">
<ThumbsUp className="h-4 w-4 mr-2 text-green-500" />
<span className="text-gray-500"></span>
<span className="ml-1 font-medium">{task.config.totalLikeCount || 0} </span>
</div>
</div>
</div>
{expandedTaskId === task.id && (
<div className="mt-4 pt-4 border-t border-dashed">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-center">
<Settings className="h-5 w-5 mr-2 text-gray-500" />
<h4 className="font-medium"></h4>
</div>
<div className="space-y-2 pl-7">
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.config.interval} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.config.maxLikes} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.config.friendMaxLikes || 3} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{task.config.startTime} - {task.config.endTime}
</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Users className="h-5 w-5 mr-2 text-gray-500" />
<h4 className="font-medium"></h4>
</div>
<div className="space-y-2 pl-7">
<div className="flex flex-wrap gap-2">
{task.config.targetGroups.map((tag, index) => (
<Badge key={`${task.id}-tag-${index}-${tag}`} variant="outline" className="bg-gray-50">
{tag}
</Badge>
))}
</div>
<div className="text-sm text-gray-500">
{task.config.tagOperator === 1 ? '满足所有标签' : '满足任一标签'}
</div>
{task.config.enableFriendTags && task.config.friendTags && (
<div className="mt-2">
<div className="text-sm font-medium mb-1"></div>
<Badge variant="outline" className="bg-blue-50 border-blue-200">
{task.config.friendTags}
</Badge>
</div>
)}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<ThumbsUp className="h-5 w-5 mr-2 text-gray-500" />
<h4 className="font-medium"></h4>
</div>
<div className="space-y-2 pl-7">
<div className="flex flex-wrap gap-2">
{task.config.contentTypes.map((type, index) => (
<Badge key={`${task.id}-type-${index}-${type}`} variant="outline" className="bg-gray-50">
{type === 'text' ? '文字' : type === 'image' ? '图片' : '视频'}
</Badge>
))}
</div>
</div>
</div>
</div>
</div>
)}
</Card>
))}
</div>
{/* 分页 */}
{total > pageSize && (
<div className="flex justify-center mt-6 space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1 || loading}
>
</Button>
<div className="flex items-center space-x-1">
<span className="text-sm text-gray-500"> {currentPage} </span>
<span className="text-sm text-gray-500"> {Math.ceil(total / pageSize)} </span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(total / pageSize), prev + 1))}
disabled={currentPage >= Math.ceil(total / pageSize) || loading}
>
</Button>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,5 +1,354 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Plus, Filter, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye, Copy } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Switch } from '@/components/ui/switch';
import PageHeader from '@/components/PageHeader';
import { useToast } from '@/components/ui/toast';
interface SyncTask {
id: string;
name: string;
status: number; // 1-运行中0-暂停
deviceCount: number;
contentLib: string;
syncCount: number;
lastSyncTime: string;
createTime: string;
creator: string;
config: {
devices: string[];
contentLibraryNames: string[];
};
creatorName: string;
}
export default function MomentsSync() {
return <div></div>;
const navigate = useNavigate();
const { toast } = useToast();
const [tasks, setTasks] = useState<SyncTask[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
// 模拟数据
const mockTasks: SyncTask[] = [
{
id: '1',
name: '朋友圈同步任务1',
status: 1,
deviceCount: 3,
contentLib: '营销素材库',
syncCount: 156,
lastSyncTime: '2024-03-18 16:30:00',
createTime: '2024-03-15 10:00:00',
creator: 'admin',
creatorName: '管理员',
config: {
devices: ['device1', 'device2', 'device3'],
contentLibraryNames: ['营销素材库', '产品介绍库']
}
},
{
id: '2',
name: '朋友圈同步任务2',
status: 0,
deviceCount: 2,
contentLib: '产品介绍库',
syncCount: 89,
lastSyncTime: '2024-03-17 14:20:00',
createTime: '2024-03-14 15:30:00',
creator: 'user1',
creatorName: '用户1',
config: {
devices: ['device4', 'device5'],
contentLibraryNames: ['产品介绍库']
}
}
];
// 获取任务列表
const fetchTasks = async () => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟搜索过滤
let filteredTasks = mockTasks;
if (searchQuery) {
filteredTasks = mockTasks.filter(task =>
task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
setTasks(filteredTasks);
setTotal(filteredTasks.length);
} catch (error: any) {
console.error('获取朋友圈同步任务列表失败:', error);
toast({
title: '获取失败',
description: error?.message || '请检查网络连接',
});
} finally {
setIsLoading(false);
}
};
// 组件加载时获取任务列表
useEffect(() => {
fetchTasks();
}, [currentPage, pageSize]);
// 处理页码变化
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 处理每页条数变化
const handlePageSizeChange = (size: number) => {
setPageSize(size);
setCurrentPage(1); // 重置到第一页
};
// 搜索任务
const handleSearch = () => {
fetchTasks();
};
// 切换任务状态
const toggleTaskStatus = async (taskId: string, currentStatus: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const newStatus = currentStatus === 1 ? 0 : 1;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: newStatus } : task
)
);
toast({
title: '状态更新成功',
description: `任务已${newStatus === 1 ? '启用' : '暂停'}`,
});
} catch (error: any) {
console.error('更新任务状态失败:', error);
toast({
title: '更新失败',
description: error?.message || '更新任务状态失败',
});
}
};
// 执行删除
const handleDelete = async (taskId: string) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
setTasks(tasks.filter((task) => task.id !== taskId));
toast({
title: '删除成功',
description: '已成功删除同步任务',
});
} catch (error: any) {
console.error('删除任务失败:', error);
toast({
title: '删除失败',
description: error?.message || '删除任务失败',
});
}
};
// 编辑任务
const handleEdit = (taskId: string) => {
navigate(`/workspace/moments-sync/${taskId}/edit`);
};
// 查看任务详情
const handleView = (taskId: string) => {
navigate(`/workspace/moments-sync/${taskId}`);
};
// 复制任务
const handleCopy = async (taskId: string) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const taskToCopy = tasks.find(task => task.id === taskId);
if (taskToCopy) {
const newTask = {
...taskToCopy,
id: Date.now().toString(),
name: `${taskToCopy.name} (副本)`,
status: 0,
createTime: new Date().toLocaleString(),
syncCount: 0,
lastSyncTime: '-'
};
setTasks([newTask, ...tasks]);
toast({
title: '复制成功',
description: '已成功复制同步任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
toast({
title: '复制失败',
description: error?.message || '复制任务失败',
});
}
};
// 过滤任务
const filteredTasks = tasks.filter(
(task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<PageHeader
title="朋友圈同步"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/moments-sync/new">
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
<div className="p-4">
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={fetchTasks} disabled={isLoading}>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</div>
</Card>
<div className="space-y-4">
{isLoading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : filteredTasks.length > 0 ? (
filteredTasks.map((task) => (
<Card key={task.id} className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="font-medium">{task.name}</h3>
<Badge variant={task.status === 1 ? 'default' : 'secondary'}>
{task.status === 1 ? '运行中' : '已暂停'}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(task.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="text-sm text-gray-500">
<div className="mb-1">{task.deviceCount} </div>
<div className="mb-1">{task.contentLib}</div>
<div>{task.creatorName}</div>
</div>
<div className="text-sm text-gray-500">
<div className="mb-1">{task.syncCount} </div>
<div className="mb-1">{task.lastSyncTime}</div>
<div>{task.createTime}</div>
</div>
</div>
<div className="border-t pt-4">
<div className="flex items-center text-sm text-gray-500">
<Clock className="h-4 w-4 mr-2" />
<span>{task.config.devices.join(', ')}</span>
</div>
</div>
</Card>
))
) : (
<div className="text-center py-12 text-gray-400">
{searchQuery ? '没有找到匹配的任务' : '暂无同步任务'}
</div>
)}
</div>
{/* 分页 */}
{total > pageSize && (
<div className="flex justify-center mt-6 space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1 || isLoading}
>
</Button>
<div className="flex items-center space-x-1">
<span className="text-sm text-gray-500"> {currentPage} </span>
<span className="text-sm text-gray-500"> {Math.ceil(total / pageSize)} </span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(Math.ceil(total / pageSize), currentPage + 1))}
disabled={currentPage >= Math.ceil(total / pageSize) || isLoading}
>
</Button>
</div>
)}
</div>
</div>
);
}