feat: 本次提交更新内容如下
开始迁移工作台了,有好多问题
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Plus,
|
||||
@@ -35,79 +35,52 @@ import PageHeader from '@/components/PageHeader';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
interface LikeTask {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'running' | 'paused';
|
||||
deviceCount: number;
|
||||
targetGroup: string;
|
||||
likeCount: number;
|
||||
lastLikeTime: string;
|
||||
createTime: string;
|
||||
creator: string;
|
||||
likeInterval: number;
|
||||
maxLikesPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
contentTypes: string[];
|
||||
targetTags: string[];
|
||||
}
|
||||
import {
|
||||
fetchAutoLikeTasks,
|
||||
deleteAutoLikeTask,
|
||||
toggleAutoLikeTask,
|
||||
copyAutoLikeTask,
|
||||
LikeTask,
|
||||
} from '@/api/autoLike';
|
||||
|
||||
export default function AutoLike() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [tasks, setTasks] = useState<LikeTask[]>([
|
||||
{
|
||||
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: '潜在客户点赞',
|
||||
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 [tasks, setTasks] = useState<LikeTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取任务列表
|
||||
const fetchTasks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await fetchAutoLikeTasks();
|
||||
setTasks(list);
|
||||
} catch {
|
||||
toast({ title: '获取任务失败', variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, []);
|
||||
|
||||
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 handleDelete = async (id: string) => {
|
||||
if (!window.confirm('确定要删除该任务吗?')) return;
|
||||
try {
|
||||
await deleteAutoLikeTask(id);
|
||||
toast({ title: '删除成功' });
|
||||
fetchTasks();
|
||||
} catch {
|
||||
toast({ title: '删除失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (taskId: string) => {
|
||||
@@ -118,37 +91,24 @@ export default function AutoLike() {
|
||||
navigate(`/workspace/auto-like/${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 handleCopy = async (id: string) => {
|
||||
try {
|
||||
await copyAutoLikeTask(id);
|
||||
toast({ title: '复制成功' });
|
||||
fetchTasks();
|
||||
} catch {
|
||||
toast({ title: '复制失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
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 toggleTaskStatus = async (id: string, status: string) => {
|
||||
try {
|
||||
await toggleAutoLikeTask(id, status);
|
||||
toast({ title: '操作成功' });
|
||||
fetchTasks();
|
||||
} catch {
|
||||
toast({ title: '操作失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
@@ -207,22 +167,29 @@ export default function AutoLike() {
|
||||
<Input
|
||||
placeholder="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon">
|
||||
{/* 移除筛选按钮 */}
|
||||
{/* <Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button> */}
|
||||
<Button variant="outline" size="icon" onClick={fetchTasks}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.length === 0 ? (
|
||||
{loading ? (
|
||||
<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>
|
||||
</Card>
|
||||
) : 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>
|
||||
@@ -245,7 +212,7 @@ export default function AutoLike() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={task.status === 'running'}
|
||||
onCheckedChange={() => toggleTaskStatus(task.id)}
|
||||
onCheckedChange={() => toggleTaskStatus(task.id, task.status === 'running' ? 'paused' : 'running')}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -38,9 +38,12 @@ export default function NewAutoLike() {
|
||||
description: '',
|
||||
likeInterval: 30,
|
||||
maxLikesPerDay: 100,
|
||||
friendMaxLikes: 10,
|
||||
startTime: '09:00',
|
||||
endTime: '18:00',
|
||||
contentTypes: ['text', 'image'],
|
||||
includeKeywords: '',
|
||||
excludeKeywords: '',
|
||||
selectedDevices: [] as string[],
|
||||
selectedGroups: [] as string[],
|
||||
targetTags: [] as string[],
|
||||
@@ -355,6 +358,14 @@ export default function NewAutoLike() {
|
||||
onChange={(e) => handleInputChange('maxLikesPerDay', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="friendMaxLikes">单个好友每日最大点赞数</Label>
|
||||
<Input
|
||||
id="friendMaxLikes"
|
||||
value={formData.friendMaxLikes.toString()}
|
||||
onChange={(e) => handleInputChange('friendMaxLikes', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -383,14 +394,15 @@ export default function NewAutoLike() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>内容类型</CardTitle>
|
||||
<CardTitle>内容筛选</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{[
|
||||
{ value: 'text', label: '文字' },
|
||||
{ value: 'image', label: '图片' },
|
||||
{ value: 'video', label: '视频' },
|
||||
{ value: 'link', label: '链接' },
|
||||
].map((type) => (
|
||||
<Badge
|
||||
key={type.value}
|
||||
@@ -402,6 +414,28 @@ export default function NewAutoLike() {
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="includeKeywords">包含关键词</Label>
|
||||
<textarea
|
||||
id="includeKeywords"
|
||||
className="w-full border rounded p-2 text-sm"
|
||||
placeholder="多个关键词用逗号或换行分隔"
|
||||
value={formData.includeKeywords}
|
||||
onChange={e => handleInputChange('includeKeywords', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="excludeKeywords">排除关键词</Label>
|
||||
<textarea
|
||||
id="excludeKeywords"
|
||||
className="w-full border rounded p-2 text-sm"
|
||||
placeholder="多个关键词用逗号或换行分隔"
|
||||
value={formData.excludeKeywords}
|
||||
onChange={e => handleInputChange('excludeKeywords', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -431,13 +465,52 @@ export default function NewAutoLike() {
|
||||
<span className="text-gray-500">每日上限:</span>
|
||||
<span>{formData.maxLikesPerDay} 次</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">单好友上限:</span>
|
||||
<span>{formData.friendMaxLikes} 次</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">执行时间:</span>
|
||||
<span>{formData.startTime} - {formData.endTime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">内容类型:</span>
|
||||
<span>{formData.contentTypes.map(t => ({text:'文字',image:'图片',video:'视频',link:'链接'}[t])).join('、')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">包含关键词:</span>
|
||||
<span>{formData.includeKeywords}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">排除关键词:</span>
|
||||
<span>{formData.excludeKeywords}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>点赞后自动打标签</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center mb-2">
|
||||
<Switch
|
||||
checked={formData.enableFriendTags}
|
||||
onCheckedChange={v => handleInputChange('enableFriendTags', v)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">启用后,点赞后会自动为好友打上指定标签</span>
|
||||
</div>
|
||||
{formData.enableFriendTags && (
|
||||
<Input
|
||||
placeholder="请输入标签,多个标签用逗号分隔"
|
||||
value={formData.friendTags}
|
||||
onChange={e => handleInputChange('friendTags', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
144
nkebao/src/pages/workspace/moments-sync/index.tsx
Normal file
144
nkebao/src/pages/workspace/moments-sync/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RefreshCw, Search, RefreshCw as SyncIcon, Eye } from 'lucide-react';
|
||||
import { fetchMomentsSyncDevices, syncMoments, syncAllMoments, fetchMomentsLog, MomentsSyncDevice } from '@/api/momentsSync';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
|
||||
export default function MomentsSyncPage() {
|
||||
const { toast } = useToast();
|
||||
const [devices, setDevices] = useState<MomentsSyncDevice[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [logModal, setLogModal] = useState<{ open: boolean; log: string; name: string }>({ open: false, log: '', name: '' });
|
||||
|
||||
const fetchDevices = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchMomentsSyncDevices(search);
|
||||
setDevices(res.data?.list || []);
|
||||
} catch {
|
||||
toast({ title: '获取设备失败', variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchDevices(); }, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchDevices();
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setSearch('');
|
||||
fetchDevices();
|
||||
};
|
||||
|
||||
const handleSync = async (id: string) => {
|
||||
try {
|
||||
await syncMoments(id);
|
||||
toast({ title: '同步已发起' });
|
||||
fetchDevices();
|
||||
} catch {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncAll = async () => {
|
||||
try {
|
||||
await syncAllMoments();
|
||||
toast({ title: '全部同步已发起' });
|
||||
fetchDevices();
|
||||
} catch {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewLog = async (device: MomentsSyncDevice) => {
|
||||
try {
|
||||
const res = await fetchMomentsLog(device.id);
|
||||
setLogModal({ open: true, log: res.data?.log || device.log || '暂无日志', name: device.name });
|
||||
} catch {
|
||||
setLogModal({ open: true, log: device.log || '暂无日志', name: device.name });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<PageHeader title="朋友圈同步" defaultBackPath="/workspace" />}
|
||||
>
|
||||
<div className="bg-gray-50 min-h-screen pb-20">
|
||||
<div className="p-4 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={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={handleSyncAll} variant="default" size="sm">
|
||||
<SyncIcon className="h-4 w-4 mr-1" />全部同步
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
{loading ? (
|
||||
<Card className="p-8 text-center">加载中...</Card>
|
||||
) : devices.length === 0 ? (
|
||||
<Card className="p-8 text-center">暂无设备</Card>
|
||||
) : (
|
||||
devices.map(device => (
|
||||
<Card key={device.id} className="p-4 flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<span className="font-medium text-base">{device.name}</span>
|
||||
<Badge variant={
|
||||
device.status === 'success' ? 'success' :
|
||||
device.status === 'syncing' ? 'default' :
|
||||
device.status === 'error' ? 'destructive' : 'outline'
|
||||
}>
|
||||
{device.status === 'success' ? '已完成' :
|
||||
device.status === 'syncing' ? '同步中' :
|
||||
device.status === 'error' ? '失败' : '待同步'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-2">上次同步:{device.lastSyncTime || '无'}</div>
|
||||
<Progress value={device.progress} className="h-2 mb-2" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2 md:mt-0">
|
||||
<Button size="sm" variant="outline" onClick={() => handleSync(device.id)} disabled={device.status === 'syncing'}>
|
||||
<SyncIcon className="h-4 w-4 mr-1" />同步
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleViewLog(device)}>
|
||||
<Eye className="h-4 w-4 mr-1" />日志
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{/* 日志弹窗 */}
|
||||
{logModal.open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
||||
<div className="bg-white rounded-lg shadow-lg max-w-lg w-full p-6 relative">
|
||||
<div className="font-bold mb-2">{logModal.name} - 同步日志</div>
|
||||
<pre className="bg-gray-100 rounded p-2 text-xs max-h-60 overflow-auto whitespace-pre-wrap">{logModal.log}</pre>
|
||||
<Button className="absolute top-2 right-2" size="icon" variant="ghost" onClick={() => setLogModal({ open: false, log: '', name: '' })}>关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user