feat: 本次提交更新内容如下
修复
This commit is contained in:
18
Layout.css
18
Layout.css
@@ -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;
|
||||
}
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
28
nkebao/src/components/ui/checkbox.tsx
Normal file
28
nkebao/src/components/ui/checkbox.tsx
Normal 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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
184
nkebao/src/components/ui/select.tsx
Normal file
184
nkebao/src/components/ui/select.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
nkebao/src/components/ui/textarea.tsx
Normal file
33
nkebao/src/components/ui/textarea.tsx
Normal 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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -153,10 +153,10 @@ export default function WechatAccounts() {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title="微信号"
|
||||
defaultBackPath="/"
|
||||
/>
|
||||
<PageHeader
|
||||
title="微信号"
|
||||
defaultBackPath="/"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="bg-gray-50">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
389
nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx
Normal file
389
nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
469
nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx
Normal file
469
nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
474
nkebao/src/pages/workspace/group-push/GroupPush.tsx
Normal file
474
nkebao/src/pages/workspace/group-push/GroupPush.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user