feat: 功能迁移过来了,接下来优化样式
This commit is contained in:
45
nkebao/src/components/ui/avatar.tsx
Normal file
45
nkebao/src/components/ui/avatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}`;
|
||||
|
||||
109
nkebao/src/components/ui/dropdown-menu.tsx
Normal file
109
nkebao/src/components/ui/dropdown-menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}`}
|
||||
|
||||
17
nkebao/src/components/ui/progress.tsx
Normal file
17
nkebao/src/components/ui/progress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
nkebao/src/components/ui/switch.tsx
Normal file
32
nkebao/src/components/ui/switch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
nkebao/src/components/ui/tabs.tsx
Normal file
52
nkebao/src/components/ui/tabs.tsx
Normal 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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
352
nkebao/src/pages/workspace/auto-group/AutoGroup.tsx
Normal file
352
nkebao/src/pages/workspace/auto-group/AutoGroup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
494
nkebao/src/pages/workspace/auto-like/AutoLike.tsx
Normal file
494
nkebao/src/pages/workspace/auto-like/AutoLike.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user