feat: 本次提交更新内容如下

修复
This commit is contained in:
笔记本里的永平
2025-07-07 22:24:14 +08:00
parent bdd4dc3832
commit 70a1016dc0
23 changed files with 3202 additions and 1073 deletions

View File

@@ -1,10 +1,10 @@
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
.container main {
flex: 1;
overflow: auto;
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
.container main {
flex: 1;
overflow: auto;
}

View File

@@ -13,9 +13,14 @@ import DeviceDetail from './pages/devices/DeviceDetail';
import WechatAccounts from './pages/wechat-accounts/WechatAccounts';
import WechatAccountDetail from './pages/wechat-accounts/WechatAccountDetail';
import Workspace from './pages/workspace/Workspace';
import AutoLike from './pages/workspace/auto-like/AutoLike';
import NewAutoLike from './pages/workspace/auto-like/NewAutoLike';
import AutoGroup from './pages/workspace/auto-group/AutoGroup';
import AutoGroupDetail from './pages/workspace/auto-group/Detail';
import GroupPush from './pages/workspace/group-push/GroupPush';
import MomentsSync from './pages/workspace/moments-sync/MomentsSync';
import MomentsSyncDetail from './pages/workspace/moments-sync/Detail';
import AIAssistant from './pages/workspace/ai-assistant/AIAssistant';
import TrafficDistribution from './pages/workspace/traffic-distribution/TrafficDistribution';
import TrafficDistributionDetail from './pages/workspace/traffic-distribution/Detail';
import Scenarios from './pages/scenarios/Scenarios';
@@ -51,9 +56,14 @@ function App() {
<Route path="/wechat-accounts" element={<WechatAccounts />} />
<Route path="/wechat-accounts/:id" element={<WechatAccountDetail />} />
<Route path="/workspace" element={<Workspace />} />
<Route path="/workspace/auto-like" element={<AutoLike />} />
<Route path="/workspace/auto-like/new" element={<NewAutoLike />} />
<Route path="/workspace/auto-group" element={<AutoGroup />} />
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
<Route path="/workspace/group-push" element={<GroupPush />} />
<Route path="/workspace/moments-sync" element={<MomentsSync />} />
<Route path="/workspace/moments-sync/:id" element={<MomentsSyncDetail />} />
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
<Route path="/workspace/traffic-distribution" element={<TrafficDistribution />} />
<Route path="/workspace/traffic-distribution/:id" element={<TrafficDistributionDetail />} />
<Route path="/scenarios" element={<Scenarios />} />

View File

@@ -1,10 +1,10 @@
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
.container main {
flex: 1;
overflow: auto;
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
.container main {
flex: 1;
overflow: auto;
}

View File

@@ -26,6 +26,19 @@ export function CardHeader({ children, className = '' }: CardHeaderProps) {
);
}
interface CardTitleProps {
children: React.ReactNode;
className?: string;
}
export function CardTitle({ children, className = '' }: CardTitleProps) {
return (
<h3 className={`text-lg font-semibold text-gray-900 ${className}`}>
{children}
</h3>
);
}
interface CardContentProps {
children: React.ReactNode;
className?: string;

View File

@@ -0,0 +1,28 @@
import React from 'react';
interface CheckboxProps {
checked?: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
className?: string;
id?: string;
}
export function Checkbox({
checked = false,
onChange,
disabled = false,
className = '',
id
}: CheckboxProps) {
return (
<input
type="checkbox"
id={id}
checked={checked}
onChange={(e) => onChange?.(e.target.checked)}
disabled={disabled}
className={`w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 ${className}`}
/>
);
}

View File

@@ -0,0 +1,184 @@
import React, { useState, useRef, useEffect } from 'react';
interface SelectProps {
value?: string;
onValueChange?: (value: string) => void;
disabled?: boolean;
className?: string;
placeholder?: string;
children: React.ReactNode;
}
interface SelectTriggerProps {
children: React.ReactNode;
className?: string;
}
interface SelectContentProps {
children: React.ReactNode;
className?: string;
}
interface SelectItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
interface SelectValueProps {
placeholder?: string;
className?: string;
}
export function Select({
value,
onValueChange,
disabled = false,
className = '',
placeholder,
children
}: SelectProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(value || '');
const [selectedLabel, setSelectedLabel] = useState('');
const selectRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectedValue(value || '');
}, [value]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSelect = (value: string, label: string) => {
setSelectedValue(value);
setSelectedLabel(label);
onValueChange?.(value);
setIsOpen(false);
};
return (
<div ref={selectRef} className={`relative ${className}`}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
if (child.type === SelectTrigger) {
return React.cloneElement(child as any, {
onClick: () => !disabled && setIsOpen(!isOpen),
disabled,
selectedValue: selectedValue,
selectedLabel: selectedLabel,
placeholder,
isOpen
});
}
if (child.type === SelectContent && isOpen) {
return React.cloneElement(child as any, {
onSelect: handleSelect
});
}
}
return child;
})}
</div>
);
}
export function SelectTrigger({
children,
className = '',
onClick,
disabled,
selectedValue,
selectedLabel,
placeholder,
isOpen
}: SelectTriggerProps & {
onClick?: () => void;
disabled?: boolean;
selectedValue?: string;
selectedLabel?: string;
placeholder?: string;
isOpen?: boolean;
}) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`w-full px-3 py-2 text-left border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${className}`}
>
<span className="flex items-center justify-between">
<span className={selectedValue ? 'text-gray-900' : 'text-gray-500'}>
{selectedLabel || placeholder || '请选择...'}
</span>
<svg
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</span>
</button>
);
}
export function SelectContent({
children,
className = '',
onSelect
}: SelectContentProps & {
onSelect?: (value: string, label: string) => void;
}) {
return (
<div className={`absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto ${className}`}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child) && child.type === SelectItem) {
return React.cloneElement(child as any, {
onSelect
});
}
return child;
})}
</div>
);
}
export function SelectItem({
value,
children,
className = '',
onSelect
}: SelectItemProps & {
onSelect?: (value: string, label: string) => void;
}) {
return (
<button
type="button"
onClick={() => onSelect?.(value, children as string)}
className={`w-full px-3 py-2 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none ${className}`}
>
{children}
</button>
);
}
export function SelectValue({
placeholder,
className = ''
}: SelectValueProps) {
return (
<span className={className}>
{placeholder}
</span>
);
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
interface TextareaProps {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
rows?: number;
}
export function Textarea({
value,
onChange,
onKeyDown,
placeholder,
className = '',
disabled = false,
rows = 3
}: TextareaProps) {
return (
<textarea
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={placeholder}
disabled={disabled}
rows={rows}
className={`w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${className}`}
/>
);
}

View File

@@ -429,12 +429,12 @@ export default function DeviceDetail() {
header={<PageHeader title="设备详情" defaultBackPath="/devices" rightContent={<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"><Settings className="h-5 w-5" /></button>} />}
footer={<BottomNav />}
>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-gray-500">...</p>
</div>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-gray-500">...</p>
</div>
</div>
</Layout>
);
}
@@ -445,22 +445,22 @@ export default function DeviceDetail() {
header={<PageHeader title="设备详情" defaultBackPath="/devices" rightContent={<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"><Settings className="h-5 w-5" /></button>} />}
footer={<BottomNav />}
>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="flex flex-col items-center space-y-4 p-6 bg-white rounded-xl shadow-sm max-w-md">
<div className="w-12 h-12 flex items-center justify-center rounded-full bg-red-100">
<Smartphone className="h-6 w-6 text-red-500" />
</div>
<div className="text-xl font-medium text-center"></div>
<div className="text-sm text-gray-500 text-center">
ID为 "{id}"
</div>
<BackButton
variant="button"
text="返回上一页"
onBack={goBack}
/>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="flex flex-col items-center space-y-4 p-6 bg-white rounded-xl shadow-sm max-w-md">
<div className="w-12 h-12 flex items-center justify-center rounded-full bg-red-100">
<Smartphone className="h-6 w-6 text-red-500" />
</div>
<div className="text-xl font-medium text-center"></div>
<div className="text-sm text-gray-500 text-center">
ID为 "{id}"
</div>
<BackButton
variant="button"
text="返回上一页"
onBack={goBack}
/>
</div>
</div>
</Layout>
);
}

View File

@@ -380,19 +380,19 @@ export default function Devices() {
return (
<Layout
header={
<PageHeader
title="设备管理"
defaultBackPath="/"
rightContent={
<button
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
onClick={handleOpenAddDeviceModal}
>
<Plus className="h-4 w-4" />
</button>
}
/>
<PageHeader
title="设备管理"
defaultBackPath="/"
rightContent={
<button
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
onClick={handleOpenAddDeviceModal}
>
<Plus className="h-4 w-4" />
</button>
}
/>
}
>
<div className="bg-gray-50">

View File

@@ -59,83 +59,83 @@ export default function Profile() {
<Layout
header={
<div className="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 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>
</div>
}
footer={<BottomNav />}
>
<div className="bg-gradient-to-b from-blue-50 to-white">
<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 className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xl font-semibold shadow-lg">
{(userInfo?.username || user?.username || '用户').slice(0, 2).toUpperCase()}
</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 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 className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xl font-semibold shadow-lg">
{(userInfo?.username || user?.username || '用户').slice(0, 2).toUpperCase()}
</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>
</Card>
</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" />
{/* 功能菜单 */}
<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>
))}
</Card>
<ChevronRight className="w-5 h-5 text-gray-400" />
</div>
))}
</Card>
{/* 退出登录按钮 */}
<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>
{/* 退出登录按钮 */}
<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>
</div>
{/* 退出登录确认对话框 */}
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>

View File

@@ -109,7 +109,7 @@ export default function ScenarioDetail() {
todayCustomers,
growth: `+${Math.floor(Math.random() * 20) + 5}%`,
};
setScenario(scenarioData);
} catch (error) {
console.error('获取场景数据失败:', error);
@@ -345,8 +345,8 @@ export default function ScenarioDetail() {
>
</button>
</div>
</div>
</div>
</Layout>
);
}
@@ -367,7 +367,7 @@ export default function ScenarioDetail() {
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-gray-500">...</p>
</div>
</div>
</div>
</Layout>
);
}
@@ -375,65 +375,65 @@ export default function ScenarioDetail() {
return (
<Layout
header={
<PageHeader
title={scenario.name}
defaultBackPath="/scenarios"
rightContent={
<button
<PageHeader
title={scenario.name}
defaultBackPath="/scenarios"
rightContent={
<button
onClick={handleCreateNewPlan}
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
>
<Plus className="h-4 w-4 mr-1" />
</button>
}
/>
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
>
<Plus className="h-4 w-4 mr-1" />
</button>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{/* 场景描述 */}
<div className="bg-white rounded-lg p-4 mb-6">
<p className="text-gray-600 text-sm">{scenario.description}</p>
</div>
<div className="p-4">
{/* 场景描述 */}
<div className="bg-white rounded-lg p-4 mb-6">
<p className="text-gray-600 text-sm">{scenario.description}</p>
</div>
{/* 数据统计 */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-white rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm"></p>
<p className="text-2xl font-bold text-gray-900">{scenario.totalCustomers}</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
<div className="flex items-center mt-2 text-green-500 text-sm">
<TrendingUp className="h-4 w-4 mr-1" />
<span>{scenario.growth}</span>
{/* 数据统计 */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-white rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm"></p>
<p className="text-2xl font-bold text-gray-900">{scenario.totalCustomers}</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
<div className="bg-white rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm"></p>
<p className="text-2xl font-bold text-gray-900">{scenario.todayCustomers}</p>
</div>
<Calendar className="h-8 w-8 text-green-500" />
</div>
<div className="flex items-center mt-2 text-gray-500 text-sm">
<span>: {scenario.totalPlans}</span>
</div>
<div className="flex items-center mt-2 text-green-500 text-sm">
<TrendingUp className="h-4 w-4 mr-1" />
<span>{scenario.growth}</span>
</div>
</div>
{/* 计划列表 */}
<div className="bg-white rounded-lg">
<div className="p-4 border-b">
<h2 className="text-lg font-medium"></h2>
<div className="bg-white rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-500 text-sm"></p>
<p className="text-2xl font-bold text-gray-900">{scenario.todayCustomers}</p>
</div>
<Calendar className="h-8 w-8 text-green-500" />
</div>
<div className="flex items-center mt-2 text-gray-500 text-sm">
<span>: {scenario.totalPlans}</span>
</div>
</div>
</div>
{/* 计划列表 */}
<div className="bg-white rounded-lg">
<div className="p-4 border-b">
<h2 className="text-lg font-medium"></h2>
</div>
{tasks.length === 0 ? (
<div className="p-8 text-center">
<div className="mb-4">
@@ -441,33 +441,33 @@ export default function ScenarioDetail() {
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm"></p>
</div>
<button
<button
onClick={handleCreateNewPlan}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
>
<Plus className="h-4 w-4 mr-2" />
</button>
</div>
) : (
<div className="divide-y">
</button>
</div>
) : (
<div className="divide-y">
{tasks.map((task) => (
<div key={task.id} className="p-4 hover:bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center mb-2">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center mb-2">
<h3 className="font-medium text-gray-900">{task.name}</h3>
<span className={`ml-2 px-2 py-1 text-xs rounded-full ${getStatusColor(task.status)}`}>
{getStatusText(task.status)}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" />
<span>: {task.lastUpdated}</span>
</div>
</div>
<div className="flex items-center mt-2 text-sm text-gray-500">
<span>: {task.stats?.devices || 0} | : {task.stats?.acquired || 0} | : {task.stats?.added || 0}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
@@ -502,15 +502,15 @@ export default function ScenarioDetail() {
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* API接口设置对话框 */}
{showApiDialog && (

View File

@@ -187,13 +187,13 @@ export default function GongzhonghaoScenario() {
const task = tasks.find((t) => t.id === taskId);
if (task) {
// 直接使用列表数据不调用详情API
setCurrentApiSettings({
setCurrentApiSettings({
apiKey: `api_key_${taskId}`, // 使用任务ID生成API密钥
webhookUrl: `${API_BASE_URL}/v1/api/scenarios/${taskId}`,
fullUrl: `${API_BASE_URL}/v1/api/scenarios/${taskId}/text`,
taskId,
});
setShowApiDialog(true);
taskId,
});
setShowApiDialog(true);
}
};
@@ -217,58 +217,58 @@ export default function GongzhonghaoScenario() {
return (
<Layout
header={
<PageHeader
title={channelName}
defaultBackPath="/scenarios"
rightContent={
<Button onClick={handleCreateNewPlan} size="sm" className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
</Button>
}
/>
<PageHeader
title={channelName}
defaultBackPath="/scenarios"
rightContent={
<Button onClick={handleCreateNewPlan} size="sm" className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
</Button>
}
/>
}
>
<div className="bg-gradient-to-b from-blue-50 to-white">
<div className="p-4 md:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="space-y-4">
{loading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : error ? (
<div className="text-center py-12 text-red-500">{error}</div>
) : tasks.length > 0 ? (
<>
{tasks.map((task) => (
<div key={task.id}>
<ScenarioAcquisitionCard
task={task}
channel={channel}
onEdit={() => handleEditPlan(task.id)}
onCopy={handleCopyPlan}
onDelete={handleDeletePlan}
onStatusChange={handleStatusChange}
onOpenSettings={handleOpenApiSettings}
/>
</div>
))}
<div className="flex justify-center mt-6 gap-2">
<Button disabled={page === 1} onClick={() => setPage(page - 1)}></Button>
<span className="px-2"> {page} / {Math.max(1, Math.ceil(total / pageSize))} </span>
<Button disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}></Button>
<div className="p-4 md:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="space-y-4">
{loading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : error ? (
<div className="text-center py-12 text-red-500">{error}</div>
) : tasks.length > 0 ? (
<>
{tasks.map((task) => (
<div key={task.id}>
<ScenarioAcquisitionCard
task={task}
channel={channel}
onEdit={() => handleEditPlan(task.id)}
onCopy={handleCopyPlan}
onDelete={handleDeletePlan}
onStatusChange={handleStatusChange}
onOpenSettings={handleOpenApiSettings}
/>
</div>
</>
) : (
<div className="text-center py-12 bg-white rounded-lg shadow-sm md:col-span-2 lg:col-span-3">
<div className="text-gray-400 mb-4"></div>
<Button onClick={handleCreateNewPlan} className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
</Button>
))}
<div className="flex justify-center mt-6 gap-2">
<Button disabled={page === 1} onClick={() => setPage(page - 1)}></Button>
<span className="px-2"> {page} / {Math.max(1, Math.ceil(total / pageSize))} </span>
<Button disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}></Button>
</div>
)}
</div>
</>
) : (
<div className="text-center py-12 bg-white rounded-lg shadow-sm md:col-span-2 lg:col-span-3">
<div className="text-gray-400 mb-4"></div>
<Button onClick={handleCreateNewPlan} className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
)}
</div>
</div>
</div>
{/* API接口设置对话框 */}
<Dialog open={showApiDialog} onOpenChange={setShowApiDialog}>
<DialogContent className="sm:max-w-md">

View File

@@ -126,124 +126,124 @@ export default function HaibaoScenario() {
return (
<Layout
header={
<PageHeader
title="海报获客"
defaultBackPath="/scenarios"
rightContent={
<button
onClick={() => navigate('/scenarios/new?scenario=haibao')}
<PageHeader
title="海报获客"
defaultBackPath="/scenarios"
rightContent={
<button
onClick={() => navigate('/scenarios/new?scenario=haibao')}
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
>
<Plus className="h-4 w-4 mr-1" />
</button>
}
/>
>
<Plus className="h-4 w-4 mr-1" />
</button>
}
/>
}
>
<div className="bg-gradient-to-b from-blue-50 to-white">
<div className="p-4 max-w-7xl mx-auto">
{tasks.map((task) => (
<div key={task.id} className="bg-white rounded-lg shadow-sm border border-gray-100 mb-4 overflow-hidden">
{/* 任务头部 */}
<div className="p-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}>
{getStatusIcon(task.status)}
</div>
<div>
<h3 className="font-medium text-gray-900">{task.name}</h3>
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(task.status)}`}>
{getStatusText(task.status)}
</span>
<span>: {task.lastUpdated}</span>
</div>
</div>
<div className="p-4 max-w-7xl mx-auto">
{tasks.map((task) => (
<div key={task.id} className="bg-white rounded-lg shadow-sm border border-gray-100 mb-4 overflow-hidden">
{/* 任务头部 */}
<div className="p-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}>
{getStatusIcon(task.status)}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleCopyPlan(task.id)}
className="p-2 text-gray-400 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
title="复制计划"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={() => handleDeletePlan(task.id)}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="删除计划"
>
<Trash2 className="h-4 w-4" />
</button>
<div>
<h3 className="font-medium text-gray-900">{task.name}</h3>
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(task.status)}`}>
{getStatusText(task.status)}
</span>
<span>: {task.lastUpdated}</span>
</div>
</div>
</div>
</div>
{/* 统计信息 */}
<div className="p-4">
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<Users className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-orange-600">{task.stats.devices}</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<TrendingUp className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-green-600">{task.stats.acquired}</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<Users className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-purple-600">{task.stats.added}</div>
</div>
</div>
{/* 趋势图表 */}
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">7</h4>
<div className="flex items-end space-x-1 h-20">
{task.trend.map((item, index) => (
<div key={index} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-orange-200 rounded-t"
style={{ height: `${(item.customers / 30) * 100}%` }}
></div>
<span className="text-xs text-gray-500 mt-1">{item.date}</span>
</div>
))}
</div>
</div>
{/* 执行信息 */}
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">: {task.executionTime}</span>
<span className="text-orange-600">{task.nextExecutionTime}</span>
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handleCopyPlan(task.id)}
className="p-2 text-gray-400 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
title="复制计划"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={() => handleDeletePlan(task.id)}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="删除计划"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
{/* 海报管理 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="text-center py-8 text-gray-500">
<Image className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p>...</p>
{/* 统计信息 */}
<div className="p-4">
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<Users className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-orange-600">{task.stats.devices}</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<TrendingUp className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-green-600">{task.stats.acquired}</div>
</div>
<div className="text-center">
<div className="flex items-center justify-center text-gray-500 mb-1">
<Users className="h-4 w-4 mr-1" />
<span className="text-sm"></span>
</div>
<div className="text-xl font-bold text-purple-600">{task.stats.added}</div>
</div>
</div>
{/* 趋势图表 */}
<div className="mb-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">7</h4>
<div className="flex items-end space-x-1 h-20">
{task.trend.map((item, index) => (
<div key={index} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-orange-200 rounded-t"
style={{ height: `${(item.customers / 30) * 100}%` }}
></div>
<span className="text-xs text-gray-500 mt-1">{item.date}</span>
</div>
))}
</div>
</div>
{/* 执行信息 */}
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">: {task.executionTime}</span>
<span className="text-orange-600">{task.nextExecutionTime}</span>
</div>
</div>
</div>
</div>
))}
{/* 海报管理 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="text-center py-8 text-gray-500">
<Image className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p>...</p>
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -405,10 +405,10 @@ export default function WechatAccountDetail() {
return (
<Layout
header={
<PageHeader
title="账号详情"
defaultBackPath="/wechat-accounts"
/>
<PageHeader
title="账号详情"
defaultBackPath="/wechat-accounts"
/>
}
>
<div className="bg-gradient-to-b from-blue-50 to-white">

View File

@@ -153,10 +153,10 @@ export default function WechatAccounts() {
return (
<Layout
header={
<PageHeader
title="微信号"
defaultBackPath="/"
/>
<PageHeader
title="微信号"
defaultBackPath="/"
/>
}
>
<div className="bg-gray-50">

View File

@@ -4,6 +4,10 @@ import { ThumbsUp, MessageSquare, Send, Users, Share2, Brain, BarChart2, LineCha
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import '@/components/Layout.css';
export default function Workspace() {
// 模拟任务数据
@@ -100,10 +104,18 @@ export default function Workspace() {
];
return (
<div className="flex-1 p-4 bg-gray-50 pb-16">
<Layout
header={
<PageHeader
title="工作台"
defaultBackPath="/"
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
<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">
@@ -193,5 +205,7 @@ export default function Workspace() {
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,389 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Send,
Settings,
RefreshCw,
Trash2,
Download,
Copy,
MoreVertical,
Bot,
User,
Sparkles,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface Message {
id: string;
type: 'user' | 'assistant';
content: string;
timestamp: Date;
isTyping?: boolean;
}
interface Conversation {
id: string;
title: string;
messages: Message[];
createdAt: Date;
updatedAt: Date;
}
export default function AIAssistant() {
const navigate = useNavigate();
const { toast } = useToast();
const [conversations, setConversations] = useState<Conversation[]>([]);
const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
const [inputMessage, setInputMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 模拟初始对话
useEffect(() => {
const initialConversation: Conversation = {
id: '1',
title: '新对话',
messages: [
{
id: '1',
type: 'assistant',
content: '您好我是AI助手可以帮助您处理各种问题。请问有什么可以帮您的吗',
timestamp: new Date(),
},
],
createdAt: new Date(),
updatedAt: new Date(),
};
setConversations([initialConversation]);
setCurrentConversation(initialConversation);
}, []);
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [currentConversation?.messages]);
const handleSendMessage = async () => {
if (!inputMessage.trim() || !currentConversation) return;
const userMessage: Message = {
id: Date.now().toString(),
type: 'user',
content: inputMessage,
timestamp: new Date(),
};
// 添加用户消息
const updatedConversation = {
...currentConversation,
messages: [...currentConversation.messages, userMessage],
updatedAt: new Date(),
};
setCurrentConversation(updatedConversation);
setConversations(prev =>
prev.map(conv =>
conv.id === currentConversation.id ? updatedConversation : conv
)
);
setInputMessage('');
setIsLoading(true);
// 模拟AI回复
setTimeout(() => {
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
type: 'assistant',
content: generateAIResponse(inputMessage),
timestamp: new Date(),
};
const finalConversation = {
...updatedConversation,
messages: [...updatedConversation.messages, assistantMessage],
updatedAt: new Date(),
};
setCurrentConversation(finalConversation);
setConversations(prev =>
prev.map(conv =>
conv.id === currentConversation.id ? finalConversation : conv
)
);
setIsLoading(false);
}, 1000 + Math.random() * 2000);
};
const generateAIResponse = (userMessage: string): string => {
const responses = [
'我理解您的问题,让我为您详细解答...',
'这是一个很好的问题!根据我的分析...',
'我可以帮您处理这个问题,建议您...',
'基于您提供的信息,我认为...',
'让我为您提供一些建议和解决方案...',
];
return responses[Math.floor(Math.random() * responses.length)];
};
const handleNewConversation = () => {
const newConversation: Conversation = {
id: Date.now().toString(),
title: '新对话',
messages: [
{
id: '1',
type: 'assistant',
content: '您好我是AI助手可以帮助您处理各种问题。请问有什么可以帮您的吗',
timestamp: new Date(),
},
],
createdAt: new Date(),
updatedAt: new Date(),
};
setConversations(prev => [newConversation, ...prev]);
setCurrentConversation(newConversation);
};
const handleDeleteConversation = (conversationId: string) => {
if (!window.confirm('确定要删除这个对话吗?')) return;
setConversations(prev => prev.filter(conv => conv.id !== conversationId));
if (currentConversation?.id === conversationId) {
const remainingConversations = conversations.filter(conv => conv.id !== conversationId);
setCurrentConversation(remainingConversations[0] || null);
}
};
const handleCopyMessage = (content: string) => {
navigator.clipboard.writeText(content);
toast({
title: '已复制',
description: '消息内容已复制到剪贴板',
});
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<Layout
header={
<PageHeader
title="AI对话助手"
defaultBackPath="/workspace"
rightContent={
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => setShowSettings(!showSettings)}
>
<Settings className="h-4 w-4" />
</Button>
<Button onClick={handleNewConversation}>
<Sparkles className="h-4 w-4 mr-2" />
</Button>
</div>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="flex h-full">
{/* 侧边栏 - 对话列表 */}
<div className="w-80 bg-white border-r border-gray-200 flex flex-col">
<div className="p-4 border-b">
<h2 className="text-lg font-medium"></h2>
</div>
<div className="flex-1 overflow-y-auto">
{conversations.map((conversation) => (
<div
key={conversation.id}
className={`p-4 border-b cursor-pointer hover:bg-gray-50 ${
currentConversation?.id === conversation.id ? 'bg-blue-50 border-blue-200' : ''
}`}
onClick={() => setCurrentConversation(conversation)}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-sm truncate">{conversation.title}</h3>
<p className="text-xs text-gray-500 mt-1">
{conversation.messages.length}
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleDeleteConversation(conversation.id)}>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</div>
{/* 主聊天区域 */}
<div className="flex-1 flex flex-col">
{currentConversation ? (
<>
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{currentConversation.messages.map((message) => (
<div
key={message.id}
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-xs lg:max-w-md ${message.type === 'user' ? 'order-2' : 'order-1'}`}>
<div className={`flex items-start space-x-2 ${message.type === 'user' ? 'flex-row-reverse space-x-reverse' : ''}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
message.type === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{message.type === 'user' ? (
<User className="h-4 w-4" />
) : (
<Bot className="h-4 w-4" />
)}
</div>
<div className={`flex-1 ${message.type === 'user' ? 'text-right' : ''}`}>
<Card className={`inline-block ${message.type === 'user' ? 'bg-blue-500 text-white' : 'bg-white'}`}>
<CardContent className="p-3">
<div className="flex items-start justify-between">
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 ml-2 opacity-0 group-hover:opacity-100"
>
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleCopyMessage(message.content)}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className={`text-xs mt-2 ${message.type === 'user' ? 'text-blue-100' : 'text-gray-500'}`}>
{formatTime(message.timestamp)}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="max-w-xs lg:max-w-md">
<div className="flex items-start space-x-2">
<div className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center">
<Bot className="h-4 w-4" />
</div>
<Card className="bg-white">
<CardContent className="p-3">
<div className="flex items-center space-x-2">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
<span className="text-sm text-gray-500">AI正在思考...</span>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 输入区域 */}
<div className="border-t bg-white p-4">
<div className="flex items-end space-x-2">
<div className="flex-1">
<Textarea
placeholder="输入您的问题..."
value={inputMessage}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setInputMessage(e.target.value)}
onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
className="min-h-[60px] max-h-[120px] resize-none"
disabled={isLoading}
/>
</div>
<Button
onClick={handleSendMessage}
disabled={!inputMessage.trim() || isLoading}
className="h-[60px] px-4"
>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<Bot className="h-16 w-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500 mb-4">AI助手将帮助您解决各种问题</p>
<Button onClick={handleNewConversation}>
<Sparkles className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
)}
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -1,234 +1,217 @@
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 React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Clock,
Edit,
Trash2,
Eye,
Copy,
ChevronDown,
ChevronUp,
Settings,
Calendar,
Users,
UserPlus,
CheckCircle,
XCircle,
} 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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface GroupTask {
id: string;
name: string;
status: number; // 1-运行中0-暂停
status: 'running' | 'paused' | 'completed';
deviceCount: number;
groupCount: number;
memberCount: number;
targetFriends: number;
createdGroups: number;
lastCreateTime: string;
createTime: string;
creator: string;
creatorName: string;
config: {
devices: string[];
targetGroups: string[];
maxMembersPerGroup: number;
};
createInterval: number;
maxGroupsPerDay: number;
timeRange: { start: string; end: string };
groupSize: { min: number; max: number };
targetTags: string[];
groupNameTemplate: string;
groupDescription: string;
}
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[] = [
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [tasks, setTasks] = useState<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',
name: 'VIP客户建群',
deviceCount: 2,
targetFriends: 156,
createdGroups: 12,
lastCreateTime: '2025-02-06 13:12:35',
createTime: '2024-11-20 19:04:14',
creator: 'admin',
creatorName: '管理员',
config: {
devices: ['device1', 'device2', 'device3'],
targetGroups: ['VIP客户', '活跃用户'],
maxMembersPerGroup: 30
}
status: 'running',
createInterval: 300,
maxGroupsPerDay: 20,
timeRange: { start: '09:00', end: '21:00' },
groupSize: { min: 20, max: 50 },
targetTags: ['VIP客户', '高价值'],
groupNameTemplate: 'VIP客户交流群{序号}',
groupDescription: 'VIP客户专属交流群提供优质服务',
},
{
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
}
}
];
name: '产品推广建群',
deviceCount: 1,
targetFriends: 89,
createdGroups: 8,
lastCreateTime: '2024-03-04 14:09:35',
createTime: '2024-03-04 14:29:04',
creator: 'manager',
status: 'paused',
createInterval: 600,
maxGroupsPerDay: 10,
timeRange: { start: '10:00', end: '20:00' },
groupSize: { min: 15, max: 30 },
targetTags: ['潜在客户', '中意向'],
groupNameTemplate: '产品推广群{序号}',
groupDescription: '产品推广交流群,了解最新产品信息',
},
]);
// 获取任务列表
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);
}
const toggleExpand = (taskId: string) => {
setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
};
// 组件加载时获取任务列表
useEffect(() => {
fetchTasks();
}, [currentPage, pageSize]);
const handleDelete = (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId);
if (!taskToDelete) return;
// 处理页码变化
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));
if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
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);
const handleCopy = (taskId: string) => {
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: '-'
id: `${Date.now()}`,
name: `${taskToCopy.name} (复制)`,
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
};
setTasks([newTask, ...tasks]);
setTasks([...tasks, newTask]);
toast({
title: '复制成功',
description: '已成功复制建群任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
};
const toggleTaskStatus = (taskId: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: task.status === 'running' ? 'paused' : 'running' } : task,
),
);
toast({
title: '复制失败',
description: error?.message || '复制任务失败',
title: task.status === 'running' ? '已暂停' : '已启动',
description: `${task.name}任务${task.status === 'running' ? '已暂停' : '已启动'}`,
});
};
const handleCreateNew = () => {
navigate('/workspace/auto-group/new');
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800';
case 'paused':
return 'bg-gray-100 text-gray-800';
case 'completed':
return 'bg-blue-100 text-blue-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
// 过滤任务
const filteredTasks = tasks.filter(
(task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const getStatusText = (status: string) => {
switch (status) {
case 'running':
return '进行中';
case 'paused':
return '已暂停';
case 'completed':
return '已完成';
default:
return '未知';
}
};
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<Layout
header={
<PageHeader
title="自动建群"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/auto-group/new">
<Button>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{/* 搜索和筛选 */}
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
@@ -236,35 +219,47 @@ export default function AutoGroup() {
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Button variant="outline" size="icon">
<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 variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</Card>
{/* 任务列表 */}
<div className="space-y-4">
{isLoading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : filteredTasks.length > 0 ? (
{filteredTasks.length === 0 ? (
<Card className="p-8 text-center">
<UserPlus className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Card>
) : (
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 className={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<Switch
checked={task.status === 'running'}
onCheckedChange={() => toggleTaskStatus(task.id)}
disabled={task.status === 'completed'}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
@@ -295,58 +290,131 @@ export default function AutoGroup() {
<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>{task.deviceCount} </div>
<div>{task.targetFriends} </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>{task.createdGroups} </div>
<div>{task.creator}</div>
</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 className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{task.lastCreateTime}
</div>
<div className="flex items-center">
<span>{task.createTime}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0 h-6 w-6"
onClick={() => toggleExpand(task.id)}
>
{expandedTaskId === task.id ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</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.createInterval} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.maxGroupsPerDay} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{task.timeRange.start} - {task.timeRange.end}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.groupSize.min}-{task.groupSize.max} </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.targetTags.map((tag) => (
<Badge key={tag} variant="outline" className="bg-gray-50">
{tag}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<UserPlus 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="text-sm">
<div className="text-gray-500 mb-1"></div>
<div className="bg-gray-50 p-2 rounded text-xs">
{task.groupNameTemplate}
</div>
</div>
<div className="text-sm">
<div className="text-gray-500 mb-1"></div>
<div className="bg-gray-50 p-2 rounded text-xs">
{task.groupDescription}
</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 className="space-y-4">
<div className="flex items-center">
<Calendar 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 mb-1">
<span className="text-gray-500"></span>
<span>
{task.createdGroups} / {task.maxGroupsPerDay}
</span>
</div>
<Progress
value={(task.createdGroups / task.maxGroupsPerDay) * 100}
className="h-2"
/>
</div>
</div>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(Math.ceil(total / pageSize), currentPage + 1))}
disabled={currentPage >= Math.ceil(total / pageSize) || isLoading}
>
</Button>
)}
</Card>
))
)}
</div>
)}
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -1,16 +1,20 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Clock,
Edit,
Trash2,
Eye,
Copy,
ChevronDown,
ChevronUp,
Settings,
Calendar,
Users,
ThumbsUp,
} from 'lucide-react';
@@ -18,281 +22,184 @@ 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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
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;
interface LikeTask {
id: string;
name: string;
type: number;
status: number;
autoStart: number;
status: 'running' | 'paused';
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
updateTime: string;
config: TaskConfig;
}
interface TaskListResponse {
code: number;
msg: string;
data: {
list: Task[];
total: number;
};
}
interface ApiResponse {
code: number;
msg: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: string[];
targetTags: 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[] = [
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [tasks, setTasks] = useState<LikeTask[]>([
{
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: '1',
name: '高频互动点赞',
deviceCount: 2,
targetGroup: '高频互动好友',
likeCount: 156,
lastLikeTime: '2025-02-06 13:12:35',
createTime: '2024-11-20 19:04:14',
creator: 'admin',
status: 'running',
likeInterval: 5,
maxLikesPerDay: 200,
timeRange: { start: '08:00', end: '22:00' },
contentTypes: ['text', 'image', 'video'],
targetTags: ['高频互动', '高意向', '男性'],
},
{
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
}
}
];
id: '2',
name: '潜在客户点赞',
deviceCount: 1,
targetGroup: '潜在客户',
likeCount: 89,
lastLikeTime: '2024-03-04 14:09:35',
createTime: '2024-03-04 14:29:04',
creator: 'manager',
status: 'paused',
likeInterval: 10,
maxLikesPerDay: 150,
timeRange: { start: '09:00', end: '21:00' },
contentTypes: ['image', 'video'],
targetTags: ['潜在客户', '中意向', '女性'],
},
]);
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) => {
const toggleExpand = (taskId: string) => {
setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
};
const handleDelete = async (taskId: number) => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const handleDelete = (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId);
if (!taskToDelete) return;
if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
setTasks(tasks.filter(task => task.id !== taskId));
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) => {
const handleEdit = (taskId: string) => {
navigate(`/workspace/auto-like/${taskId}/edit`);
};
const handleView = (taskId: number) => {
const handleView = (taskId: string) => {
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);
const handleCopy = (taskId: string) => {
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(),
}
id: `${Date.now()}`,
name: `${taskToCopy.name} (复制)`,
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
};
setTasks([newTask, ...tasks]);
setTasks([...tasks, newTask]);
toast({
title: '复制成功',
description: '已成功复制点赞任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
};
const toggleTaskStatus = (taskId: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: task.status === 'running' ? 'paused' : 'running' } : task,
),
);
toast({
title: '复制失败',
description: error?.message || '请检查网络连接',
title: task.status === 'running' ? '已暂停' : '已启动',
description: `${task.name}任务${task.status === 'running' ? '已暂停' : '已启动'}`,
});
};
const handleCreateNew = () => {
navigate('/workspace/auto-like/new');
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800';
case 'paused':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
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 || '请检查网络连接',
});
const getStatusText = (status: string) => {
switch (status) {
case 'running':
return '进行中';
case 'paused':
return '已暂停';
default:
return '未知';
}
};
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<Layout
header={
<PageHeader
title="自动点赞"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/auto-like/new">
<Button>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{/* 搜索和筛选 */}
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
@@ -300,32 +207,46 @@ export default function AutoLike() {
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
<Button variant="outline" size="icon">
<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 variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</Card>
{/* 任务列表 */}
<div className="space-y-4">
{tasks.map((task) => (
{filteredTasks.length === 0 ? (
<Card className="p-8 text-center">
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Card>
) : (
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 className={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<Switch
checked={task.status === 'running'}
onCheckedChange={() => toggleTaskStatus(task.id)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
@@ -354,33 +275,36 @@ export default function AutoLike() {
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="grid grid-cols-2 gap-4 mb-4">
<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>{task.deviceCount} </div>
<div>{task.targetGroup}</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>{task.likeCount} </div>
<div>{task.creator}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 border-t pt-4">
<div className="text-sm">
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<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>
<Clock className="w-4 h-4 mr-1" />
{task.lastLikeTime}
</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>
<span>{task.createTime}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0 h-6 w-6"
onClick={() => toggleExpand(task.id)}
>
{expandedTaskId === task.id ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
</div>
@@ -395,20 +319,16 @@ export default function AutoLike() {
<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>
<span>{task.likeInterval} </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>
<span>{task.maxLikesPerDay} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{task.config.startTime} - {task.config.endTime}
{task.timeRange.start} - {task.timeRange.end}
</span>
</div>
</div>
@@ -421,23 +341,12 @@ export default function AutoLike() {
</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">
{task.targetTags.map((tag) => (
<Badge key={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>
@@ -448,47 +357,42 @@ export default function AutoLike() {
</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">
{task.contentTypes.map((type) => (
<Badge key={type} variant="outline" className="bg-gray-50">
{type === 'text' ? '文字' : type === 'image' ? '图片' : '视频'}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Calendar 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 mb-1">
<span className="text-gray-500"></span>
<span>
{task.likeCount} / {task.maxLikesPerDay}
</span>
</div>
<Progress
value={(task.likeCount / task.maxLikesPerDay) * 100}
className="h-2"
/>
</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>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,469 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Checkbox } from '@/components/ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface Device {
id: string;
name: string;
status: 'online' | 'offline';
lastActive: string;
}
interface TargetGroup {
id: string;
name: string;
count: number;
tags: string[];
}
export default function NewAutoLike() {
const navigate = useNavigate();
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
name: '',
description: '',
likeInterval: 30,
maxLikesPerDay: 100,
startTime: '09:00',
endTime: '18:00',
contentTypes: ['text', 'image'],
selectedDevices: [] as string[],
selectedGroups: [] as string[],
targetTags: [] as string[],
enableFriendTags: false,
friendTags: '',
});
// 模拟设备数据
const devices: Device[] = [
{ id: '1', name: 'iPhone 14 Pro', status: 'online', lastActive: '2024-03-18 15:30:00' },
{ id: '2', name: 'iPhone 13', status: 'online', lastActive: '2024-03-18 15:25:00' },
{ id: '3', name: 'iPhone 12', status: 'offline', lastActive: '2024-03-18 14:20:00' },
];
// 模拟目标人群数据
const targetGroups: TargetGroup[] = [
{ id: '1', name: 'VIP客户', count: 156, tags: ['VIP', '高价值'] },
{ id: '2', name: '活跃用户', count: 234, tags: ['活跃', '互动'] },
{ id: '3', name: '潜在客户', count: 89, tags: ['潜在', '新客户'] },
];
// 可用标签
const availableTags = [
'VIP', '高价值', '活跃', '互动', '潜在', '新客户', '男性', '女性', '青年', '中年', '高收入', '中收入'
];
const handleInputChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleDeviceToggle = (deviceId: string) => {
setFormData(prev => ({
...prev,
selectedDevices: prev.selectedDevices.includes(deviceId)
? prev.selectedDevices.filter(id => id !== deviceId)
: [...prev.selectedDevices, deviceId]
}));
};
const handleGroupToggle = (groupId: string) => {
setFormData(prev => ({
...prev,
selectedGroups: prev.selectedGroups.includes(groupId)
? prev.selectedGroups.filter(id => id !== groupId)
: [...prev.selectedGroups, groupId]
}));
};
const handleTagToggle = (tag: string) => {
setFormData(prev => ({
...prev,
targetTags: prev.targetTags.includes(tag)
? prev.targetTags.filter(t => t !== tag)
: [...prev.targetTags, tag]
}));
};
const handleContentTypeToggle = (type: string) => {
setFormData(prev => ({
...prev,
contentTypes: prev.contentTypes.includes(type)
? prev.contentTypes.filter(t => t !== type)
: [...prev.contentTypes, type]
}));
};
const handleNext = () => {
if (currentStep === 1 && (!formData.name || formData.selectedDevices.length === 0)) {
toast({
title: '请完善信息',
description: '请填写任务名称并选择至少一个设备',
variant: 'destructive',
});
return;
}
if (currentStep === 2 && formData.selectedGroups.length === 0) {
toast({
title: '请选择目标人群',
description: '请至少选择一个目标人群',
variant: 'destructive',
});
return;
}
setCurrentStep(prev => Math.min(prev + 1, 3));
};
const handlePrev = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const handleSubmit = async () => {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
toast({
title: '创建成功',
description: '自动点赞任务已创建',
});
navigate('/workspace/auto-like');
} catch (error) {
toast({
title: '创建失败',
description: '请检查网络连接后重试',
variant: 'destructive',
});
}
};
const steps = [
{ title: '基本设置', description: '配置任务基本信息' },
{ title: '目标人群', description: '选择点赞目标' },
{ title: '高级设置', description: '配置高级参数' },
];
return (
<Layout
header={
<PageHeader
title="新建自动点赞任务"
defaultBackPath="/workspace/auto-like"
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{/* 步骤指示器 */}
<Card className="mb-4">
<CardContent className="p-4">
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<div key={index} className="flex items-center">
<div className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
index + 1 <= currentStep
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{index + 1}
</div>
<div className="ml-2">
<div className="text-sm font-medium">{step.title}</div>
<div className="text-xs text-gray-500">{step.description}</div>
</div>
{index < steps.length - 1 && (
<div className={`w-8 h-0.5 mx-2 ${
index + 1 < currentStep ? 'bg-blue-600' : 'bg-gray-200'
}`} />
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* 步骤1: 基本设置 */}
{currentStep === 1 && (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="name"></Label>
<Input
id="name"
placeholder="请输入任务名称"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<Label htmlFor="description"></Label>
<Input
id="description"
placeholder="请输入任务描述(可选)"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{devices.map((device) => (
<div
key={device.id}
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer ${
formData.selectedDevices.includes(device.id)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => handleDeviceToggle(device.id)}
>
<div className="flex items-center space-x-3">
<div className={`w-2 h-2 rounded-full ${
device.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
}`} />
<div>
<div className="font-medium">{device.name}</div>
<div className="text-sm text-gray-500">
{device.status === 'online' ? '在线' : '离线'} ·
: {device.lastActive}
</div>
</div>
</div>
<Checkbox
checked={formData.selectedDevices.includes(device.id)}
onChange={() => handleDeviceToggle(device.id)}
/>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* 步骤2: 目标人群 */}
{currentStep === 2 && (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{targetGroups.map((group) => (
<div
key={group.id}
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer ${
formData.selectedGroups.includes(group.id)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => handleGroupToggle(group.id)}
>
<div>
<div className="font-medium">{group.name}</div>
<div className="text-sm text-gray-500">
{group.count}
</div>
<div className="flex flex-wrap gap-1 mt-1">
{group.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
<Checkbox
checked={formData.selectedGroups.includes(group.id)}
onChange={() => handleGroupToggle(group.id)}
/>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{availableTags.map((tag) => (
<Badge
key={tag}
variant={formData.targetTags.includes(tag) ? 'default' : 'outline'}
className="cursor-pointer"
onClick={() => handleTagToggle(tag)}
>
{tag}
</Badge>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* 步骤3: 高级设置 */}
{currentStep === 3 && (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="interval"></Label>
<Input
id="interval"
value={formData.likeInterval.toString()}
onChange={(e) => handleInputChange('likeInterval', parseInt(e.target.value))}
/>
</div>
<div>
<Label htmlFor="maxLikes"></Label>
<Input
id="maxLikes"
value={formData.maxLikesPerDay.toString()}
onChange={(e) => handleInputChange('maxLikesPerDay', parseInt(e.target.value))}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="startTime"></Label>
<input
id="startTime"
type="time"
value={formData.startTime}
onChange={(e) => handleInputChange('startTime', e.target.value)}
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"
/>
</div>
<div>
<Label htmlFor="endTime"></Label>
<input
id="endTime"
type="time"
value={formData.endTime}
onChange={(e) => handleInputChange('endTime', e.target.value)}
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"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{[
{ value: 'text', label: '文字' },
{ value: 'image', label: '图片' },
{ value: 'video', label: '视频' },
].map((type) => (
<Badge
key={type.value}
variant={formData.contentTypes.includes(type.value) ? 'default' : 'outline'}
className="cursor-pointer"
onClick={() => handleContentTypeToggle(type.value)}
>
{type.label}
</Badge>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.selectedDevices.length} </span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.selectedGroups.length} </span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.likeInterval} </span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.maxLikesPerDay} </span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">:</span>
<span>{formData.startTime} - {formData.endTime}</span>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* 底部按钮 */}
<div className="flex justify-between mt-6">
<Button
variant="outline"
onClick={handlePrev}
disabled={currentStep === 1}
>
</Button>
<div className="flex space-x-2">
{currentStep < 3 ? (
<Button onClick={handleNext}>
</Button>
) : (
<Button onClick={handleSubmit}>
</Button>
)}
</div>
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,474 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Clock,
Edit,
Trash2,
Eye,
Copy,
ChevronDown,
ChevronUp,
Settings,
Calendar,
Users,
Send,
CheckCircle,
XCircle,
MessageSquare,
} 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 { Switch } from '@/components/ui/switch';
import { Progress } from '@/components/ui/progress';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface PushTask {
id: string;
name: string;
status: 'running' | 'paused' | 'completed';
deviceCount: number;
targetGroups: string[];
pushCount: number;
successCount: number;
lastPushTime: string;
createTime: string;
creator: string;
pushInterval: number;
maxPushPerDay: number;
timeRange: { start: string; end: string };
messageType: 'text' | 'image' | 'video' | 'link';
messageContent: string;
targetTags: string[];
pushMode: 'immediate' | 'scheduled';
scheduledTime?: string;
}
export default function GroupPush() {
const navigate = useNavigate();
const { toast } = useToast();
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [tasks, setTasks] = useState<PushTask[]>([
{
id: '1',
name: '产品推广群发',
deviceCount: 2,
targetGroups: ['VIP客户群', '潜在客户群'],
pushCount: 156,
successCount: 142,
lastPushTime: '2025-02-06 13:12:35',
createTime: '2024-11-20 19:04:14',
creator: 'admin',
status: 'running',
pushInterval: 60,
maxPushPerDay: 200,
timeRange: { start: '09:00', end: '21:00' },
messageType: 'text',
messageContent: '新品上市,限时优惠!点击查看详情...',
targetTags: ['VIP客户', '高意向'],
pushMode: 'immediate',
},
{
id: '2',
name: '活动通知推送',
deviceCount: 1,
targetGroups: ['活动群', '推广群'],
pushCount: 89,
successCount: 78,
lastPushTime: '2024-03-04 14:09:35',
createTime: '2024-03-04 14:29:04',
creator: 'manager',
status: 'paused',
pushInterval: 120,
maxPushPerDay: 100,
timeRange: { start: '10:00', end: '20:00' },
messageType: 'image',
messageContent: '活动海报.jpg',
targetTags: ['活跃用户', '中意向'],
pushMode: 'scheduled',
scheduledTime: '2024-03-05 10:00:00',
},
]);
const toggleExpand = (taskId: string) => {
setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
};
const handleDelete = (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId);
if (!taskToDelete) return;
if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
setTasks(tasks.filter((task) => task.id !== taskId));
toast({
title: '删除成功',
description: '已成功删除推送任务',
});
};
const handleEdit = (taskId: string) => {
navigate(`/workspace/group-push/${taskId}/edit`);
};
const handleView = (taskId: string) => {
navigate(`/workspace/group-push/${taskId}`);
};
const handleCopy = (taskId: string) => {
const taskToCopy = tasks.find((task) => task.id === taskId);
if (taskToCopy) {
const newTask = {
...taskToCopy,
id: `${Date.now()}`,
name: `${taskToCopy.name} (复制)`,
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
};
setTasks([...tasks, newTask]);
toast({
title: '复制成功',
description: '已成功复制推送任务',
});
}
};
const toggleTaskStatus = (taskId: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: task.status === 'running' ? 'paused' : 'running' } : task,
),
);
toast({
title: task.status === 'running' ? '已暂停' : '已启动',
description: `${task.name}任务${task.status === 'running' ? '已暂停' : '已启动'}`,
});
};
const handleCreateNew = () => {
navigate('/workspace/group-push/new');
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800';
case 'paused':
return 'bg-gray-100 text-gray-800';
case 'completed':
return 'bg-blue-100 text-blue-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'running':
return '进行中';
case 'paused':
return '已暂停';
case 'completed':
return '已完成';
default:
return '未知';
}
};
const getMessageTypeText = (type: string) => {
switch (type) {
case 'text':
return '文字';
case 'image':
return '图片';
case 'video':
return '视频';
case 'link':
return '链接';
default:
return '未知';
}
};
const getSuccessRate = (pushCount: number, successCount: number) => {
if (pushCount === 0) return 0;
return Math.round((successCount / pushCount) * 100);
};
return (
<Layout
header={
<PageHeader
title="群消息推送"
defaultBackPath="/workspace"
rightContent={
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<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={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</Card>
{/* 任务列表 */}
<div className="space-y-4">
{filteredTasks.length === 0 ? (
<Card className="p-8 text-center">
<Send className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Card>
) : (
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 className={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={task.status === 'running'}
onCheckedChange={() => toggleTaskStatus(task.id)}
disabled={task.status === 'completed'}
/>
<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>{task.deviceCount} </div>
<div>{task.targetGroups.length} </div>
</div>
<div className="text-sm text-gray-500">
<div>{task.successCount}/{task.pushCount}</div>
<div>{task.creator}</div>
</div>
</div>
{/* 成功率进度条 */}
<div className="mb-4">
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-500"></span>
<span className="font-medium">{getSuccessRate(task.pushCount, task.successCount)}%</span>
</div>
<Progress
value={getSuccessRate(task.pushCount, task.successCount)}
className="h-2"
/>
</div>
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{task.lastPushTime}
</div>
<div className="flex items-center">
<span>{task.createTime}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0 h-6 w-6"
onClick={() => toggleExpand(task.id)}
>
{expandedTaskId === task.id ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</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.pushInterval} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.maxPushPerDay} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{task.timeRange.start} - {task.timeRange.end}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.pushMode === 'immediate' ? '立即推送' : '定时推送'}</span>
</div>
{task.scheduledTime && (
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.scheduledTime}</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.targetGroups.map((group) => (
<Badge key={group} variant="outline" className="bg-gray-50">
{group}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<MessageSquare 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>{getMessageTypeText(task.messageType)}</span>
</div>
<div className="text-sm">
<div className="text-gray-500 mb-1"></div>
<div className="bg-gray-50 p-2 rounded text-xs">
{task.messageContent}
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Calendar 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 mb-1">
<span className="text-gray-500"></span>
<span>
{task.pushCount} / {task.maxPushPerDay}
</span>
</div>
<Progress
value={(task.pushCount / task.maxPushPerDay) * 100}
className="h-2"
/>
{task.targetTags.length > 0 && (
<div className="mt-2">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="flex flex-wrap gap-1">
{task.targetTags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)}
</Card>
))
)}
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -1,236 +1,230 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Plus, Search, RefreshCw, MoreVertical, Clock, Edit, Trash2, Eye, Copy } from 'lucide-react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Clock,
Edit,
Trash2,
Eye,
Copy,
ChevronDown,
ChevronUp,
Settings,
Calendar,
Users,
Share2,
CheckCircle,
XCircle,
} 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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface SyncTask {
id: string;
name: string;
status: number; // 1-运行中0-暂停
status: 'running' | 'paused' | 'completed';
deviceCount: number;
contentLib: string;
targetGroup: string;
syncCount: number;
lastSyncTime: string;
createTime: string;
creator: string;
config: {
devices: string[];
contentLibraryNames: string[];
};
creatorName: string;
syncInterval: number;
maxSyncPerDay: number;
timeRange: { start: string; end: string };
contentTypes: string[];
targetTags: string[];
syncMode: 'auto' | 'manual';
filterKeywords: string[];
}
export default function MomentsSync() {
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[] = [
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [tasks, setTasks] = useState<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',
name: '朋友圈自动同步',
deviceCount: 2,
targetGroup: '所有好友',
syncCount: 45,
lastSyncTime: '2025-02-06 13:12:35',
createTime: '2024-11-20 19:04:14',
creator: 'admin',
creatorName: '管理员',
config: {
devices: ['device1', 'device2', 'device3'],
contentLibraryNames: ['营销素材库', '产品介绍库']
}
status: 'running',
syncInterval: 30,
maxSyncPerDay: 100,
timeRange: { start: '08:00', end: '22:00' },
contentTypes: ['text', 'image', 'video'],
targetTags: ['重要客户', '活跃用户'],
syncMode: 'auto',
filterKeywords: ['产品', '服务', '优惠'],
},
{
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: ['产品介绍库']
}
}
];
name: '营销内容同步',
deviceCount: 1,
targetGroup: '目标客户',
syncCount: 23,
lastSyncTime: '2024-03-04 14:09:35',
createTime: '2024-03-04 14:29:04',
creator: 'manager',
status: 'paused',
syncInterval: 60,
maxSyncPerDay: 50,
timeRange: { start: '09:00', end: '21:00' },
contentTypes: ['image', 'video'],
targetTags: ['潜在客户', '中意向'],
syncMode: 'manual',
filterKeywords: ['营销', '推广'],
},
]);
// 获取任务列表
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);
}
const toggleExpand = (taskId: string) => {
setExpandedTaskId(expandedTaskId === taskId ? null : taskId);
};
// 组件加载时获取任务列表
useEffect(() => {
fetchTasks();
}, [currentPage, pageSize]);
const handleDelete = (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId);
if (!taskToDelete) return;
// 处理页码变化
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));
if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
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);
const handleCopy = (taskId: string) => {
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: '-'
id: `${Date.now()}`,
name: `${taskToCopy.name} (复制)`,
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
};
setTasks([newTask, ...tasks]);
setTasks([...tasks, newTask]);
toast({
title: '复制成功',
description: '已成功复制同步任务',
});
}
} catch (error: any) {
console.error('复制任务失败:', error);
};
const toggleTaskStatus = (taskId: string) => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return;
setTasks(
tasks.map((task) =>
task.id === taskId ? { ...task, status: task.status === 'running' ? 'paused' : 'running' } : task,
),
);
toast({
title: '复制失败',
description: error?.message || '复制任务失败',
title: task.status === 'running' ? '已暂停' : '已启动',
description: `${task.name}任务${task.status === 'running' ? '已暂停' : '已启动'}`,
});
};
const handleCreateNew = () => {
navigate('/workspace/moments-sync/new');
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800';
case 'paused':
return 'bg-gray-100 text-gray-800';
case 'completed':
return 'bg-blue-100 text-blue-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
// 过滤任务
const filteredTasks = tasks.filter(
(task) => task.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const getStatusText = (status: string) => {
switch (status) {
case 'running':
return '进行中';
case 'paused':
return '已暂停';
case 'completed':
return '已完成';
default:
return '未知';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'paused':
return <XCircle className="h-4 w-4 text-gray-500" />;
case 'completed':
return <CheckCircle className="h-4 w-4 text-blue-500" />;
default:
return <XCircle className="h-4 w-4 text-gray-500" />;
}
};
return (
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
<Layout
header={
<PageHeader
title="朋友圈同步"
defaultBackPath="/workspace"
rightContent={
<Link to="/workspace/moments-sync/new">
<Button>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{/* 搜索和筛选 */}
<Card className="p-4 mb-4">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
@@ -238,35 +232,47 @@ export default function MomentsSync() {
<Input
placeholder="搜索任务名称"
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" size="icon" onClick={handleSearch}>
{/* <Filter className="h-4 w-4" /> */}
<Button variant="outline" size="icon">
<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 variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</Card>
{/* 任务列表 */}
<div className="space-y-4">
{isLoading ? (
<div className="text-center py-12 text-gray-400">...</div>
) : filteredTasks.length > 0 ? (
{filteredTasks.length === 0 ? (
<Card className="p-8 text-center">
<Share2 className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Card>
) : (
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 className={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch checked={task.status === 1} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
<Switch
checked={task.status === 'running'}
onCheckedChange={() => toggleTaskStatus(task.id)}
disabled={task.status === 'completed'}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
@@ -297,58 +303,138 @@ export default function MomentsSync() {
<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>{task.deviceCount} </div>
<div>{task.targetGroup}</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>{task.syncCount} </div>
<div>{task.creator}</div>
</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 className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{task.lastSyncTime}
</div>
<div className="flex items-center">
<span>{task.createTime}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0 h-6 w-6"
onClick={() => toggleExpand(task.id)}
>
{expandedTaskId === task.id ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</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.syncInterval} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.maxSyncPerDay} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{task.timeRange.start} - {task.timeRange.end}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{task.syncMode === 'auto' ? '自动' : '手动'}</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.targetTags.map((tag) => (
<Badge key={tag} variant="outline" className="bg-gray-50">
{tag}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Share2 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.contentTypes.map((type) => (
<Badge key={type} variant="outline" className="bg-gray-50">
{type === 'text' ? '文字' : type === 'image' ? '图片' : '视频'}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Calendar 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 mb-1">
<span className="text-gray-500"></span>
<span>
{task.syncCount} / {task.maxSyncPerDay}
</span>
</div>
<Progress
value={(task.syncCount / task.maxSyncPerDay) * 100}
className="h-2"
/>
{task.filterKeywords.length > 0 && (
<div className="mt-2">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="flex flex-wrap gap-1">
{task.filterKeywords.map((keyword) => (
<Badge key={keyword} variant="outline" className="text-xs">
{keyword}
</Badge>
))}
</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>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(Math.ceil(total / pageSize), currentPage + 1))}
disabled={currentPage >= Math.ceil(total / pageSize) || isLoading}
>
</Button>
)}
</Card>
))
)}
</div>
)}
</div>
</div>
</div>
</Layout>
);
}

View File

@@ -1,5 +1,462 @@
import React from 'react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Plus,
Filter,
Search,
RefreshCw,
MoreVertical,
Clock,
Edit,
Trash2,
Eye,
Copy,
ChevronDown,
ChevronUp,
Settings,
Calendar,
Users,
Share2,
CheckCircle,
XCircle,
TrendingUp,
} 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 { Switch } from '@/components/ui/switch';
import { Progress } from '@/components/ui/progress';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import BottomNav from '@/components/BottomNav';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface DistributionRule {
id: string;
name: string;
status: 'running' | 'paused' | 'completed';
deviceCount: number;
totalTraffic: number;
distributedTraffic: number;
lastDistributionTime: string;
createTime: string;
creator: string;
distributionInterval: number;
maxDistributionPerDay: number;
timeRange: { start: string; end: string };
targetChannels: string[];
distributionRatio: Record<string, number>;
priority: 'high' | 'medium' | 'low';
filterConditions: string[];
}
export default function TrafficDistribution() {
return <div></div>;
const navigate = useNavigate();
const { toast } = useToast();
const [expandedRuleId, setExpandedRuleId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [tasks, setTasks] = useState<DistributionRule[]>([
{
id: '1',
name: 'VIP客户流量分发',
deviceCount: 3,
totalTraffic: 1000,
distributedTraffic: 756,
lastDistributionTime: '2025-02-06 13:12:35',
createTime: '2024-11-20 19:04:14',
creator: 'admin',
status: 'running',
distributionInterval: 300,
maxDistributionPerDay: 2000,
timeRange: { start: '08:00', end: '22:00' },
targetChannels: ['抖音', '小红书', '公众号'],
distributionRatio: {
'抖音': 40,
'小红书': 35,
'公众号': 25,
},
priority: 'high',
filterConditions: ['VIP客户', '高价值'],
},
{
id: '2',
name: '新客户流量分发',
deviceCount: 2,
totalTraffic: 500,
distributedTraffic: 234,
lastDistributionTime: '2024-03-04 14:09:35',
createTime: '2024-03-04 14:29:04',
creator: 'manager',
status: 'paused',
distributionInterval: 600,
maxDistributionPerDay: 1000,
timeRange: { start: '09:00', end: '21:00' },
targetChannels: ['抖音', '快手'],
distributionRatio: {
'抖音': 60,
'快手': 40,
},
priority: 'medium',
filterConditions: ['新客户', '潜在客户'],
},
]);
const toggleExpand = (ruleId: string) => {
setExpandedRuleId(expandedRuleId === ruleId ? null : ruleId);
};
const handleDelete = (ruleId: string) => {
const ruleToDelete = tasks.find((rule) => rule.id === ruleId);
if (!ruleToDelete) return;
if (!window.confirm(`确定要删除"${ruleToDelete.name}"吗?`)) return;
setTasks(tasks.filter((rule) => rule.id !== ruleId));
toast({
title: '删除成功',
description: '已成功删除分发规则',
});
};
const handleEdit = (ruleId: string) => {
navigate(`/workspace/traffic-distribution/${ruleId}/edit`);
};
const handleView = (ruleId: string) => {
navigate(`/workspace/traffic-distribution/${ruleId}`);
};
const handleCopy = (ruleId: string) => {
const ruleToCopy = tasks.find((rule) => rule.id === ruleId);
if (ruleToCopy) {
const newRule = {
...ruleToCopy,
id: `${Date.now()}`,
name: `${ruleToCopy.name} (复制)`,
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
};
setTasks([...tasks, newRule]);
toast({
title: '复制成功',
description: '已成功复制分发规则',
});
}
};
const toggleRuleStatus = (ruleId: string) => {
const rule = tasks.find((r) => r.id === ruleId);
if (!rule) return;
setTasks(
tasks.map((rule) =>
rule.id === ruleId ? { ...rule, status: rule.status === 'running' ? 'paused' : 'running' } : rule,
),
);
toast({
title: rule.status === 'running' ? '已暂停' : '已启动',
description: `${rule.name}规则${rule.status === 'running' ? '已暂停' : '已启动'}`,
});
};
const handleCreateNew = () => {
navigate('/workspace/traffic-distribution/new');
};
const filteredRules = tasks.filter((rule) =>
rule.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'bg-green-100 text-green-800';
case 'paused':
return 'bg-gray-100 text-gray-800';
case 'completed':
return 'bg-blue-100 text-blue-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'running':
return '进行中';
case 'paused':
return '已暂停';
case 'completed':
return '已完成';
default:
return '未知';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return 'bg-red-100 text-red-800';
case 'medium':
return 'bg-yellow-100 text-yellow-800';
case 'low':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getPriorityText = (priority: string) => {
switch (priority) {
case 'high':
return '高';
case 'medium':
return '中';
case 'low':
return '低';
default:
return '未知';
}
};
return (
<Layout
header={
<PageHeader
title="流量分发"
defaultBackPath="/workspace"
rightContent={
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
}
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<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={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</Card>
{/* 规则列表 */}
<div className="space-y-4">
{filteredRules.length === 0 ? (
<Card className="p-8 text-center">
<Share2 className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500 text-lg font-medium mb-2"></p>
<p className="text-gray-400 text-sm mb-4"></p>
<Button onClick={handleCreateNew}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Card>
) : (
filteredRules.map((rule) => (
<Card key={rule.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">{rule.name}</h3>
<Badge className={getStatusColor(rule.status)}>
{getStatusText(rule.status)}
</Badge>
<Badge className={getPriorityColor(rule.priority)}>
: {getPriorityText(rule.priority)}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={rule.status === 'running'}
onCheckedChange={() => toggleRuleStatus(rule.id)}
disabled={rule.status === 'completed'}
/>
<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(rule.id)}>
<Eye className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(rule.id)}>
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCopy(rule.id)}>
<Copy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(rule.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>{rule.deviceCount} </div>
<div>{rule.targetChannels.length} </div>
</div>
<div className="text-sm text-gray-500">
<div>{rule.distributedTraffic}/{rule.totalTraffic}</div>
<div>{rule.creator}</div>
</div>
</div>
{/* 分发进度 */}
<div className="mb-4">
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-500"></span>
<span className="font-medium">
{Math.round((rule.distributedTraffic / rule.totalTraffic) * 100)}%
</span>
</div>
<Progress
value={(rule.distributedTraffic / rule.totalTraffic) * 100}
className="h-2"
/>
</div>
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
<div className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
{rule.lastDistributionTime}
</div>
<div className="flex items-center">
<span>{rule.createTime}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0 h-6 w-6"
onClick={() => toggleExpand(rule.id)}
>
{expandedRuleId === rule.id ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
</div>
{expandedRuleId === rule.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>{rule.distributionInterval} </span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{rule.maxDistributionPerDay}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>
{rule.timeRange.start} - {rule.timeRange.end}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span>{getPriorityText(rule.priority)}</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<Share2 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">
{rule.targetChannels.map((channel) => (
<Badge key={channel} variant="outline" className="bg-gray-50">
{channel}
</Badge>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<TrendingUp className="h-5 w-5 mr-2 text-gray-500" />
<h4 className="font-medium"></h4>
</div>
<div className="space-y-2 pl-7">
{Object.entries(rule.distributionRatio).map(([channel, ratio]) => (
<div key={channel} className="flex justify-between text-sm">
<span className="text-gray-500">{channel}</span>
<span>{ratio}%</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">
{rule.filterConditions.map((condition) => (
<Badge key={condition} variant="outline" className="bg-gray-50">
{condition}
</Badge>
))}
</div>
</div>
</div>
</div>
</div>
)}
</Card>
))
)}
</div>
</div>
</div>
</Layout>
);
}