feat:缓存下
This commit is contained in:
@@ -21,6 +21,7 @@ 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 EditMomentsSync from './pages/workspace/moments-sync/edit';
|
||||
import AIAssistant from './pages/workspace/ai-assistant/AIAssistant';
|
||||
import TrafficDistribution from './pages/workspace/traffic-distribution/TrafficDistribution';
|
||||
import TrafficDistributionDetail from './pages/workspace/traffic-distribution/Detail';
|
||||
@@ -65,6 +66,7 @@ function App() {
|
||||
<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/moments-sync/edit/:id" element={<EditMomentsSync />} />
|
||||
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
|
||||
<Route path="/workspace/traffic-distribution" element={<TrafficDistribution />} />
|
||||
<Route path="/workspace/traffic-distribution/:id" element={<TrafficDistributionDetail />} />
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
||||
// 获取单个任务详情
|
||||
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
|
||||
try {
|
||||
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
|
||||
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail?id=${id}`);
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function fetchMomentsSyncTasks(): Promise<MomentsSyncTask[]> {
|
||||
// 获取单个任务详情
|
||||
export async function fetchMomentsSyncTaskDetail(id: string): Promise<MomentsSyncTask | null> {
|
||||
try {
|
||||
const res = await get<ApiResponse<MomentsSyncTask>>(`/v1/workbench/detail/${id}`);
|
||||
const res = await get<ApiResponse<MomentsSyncTask>>(`/v1/workbench/detail?id=${id}`);
|
||||
if (res.code === 200 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,179 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import {
|
||||
fetchMomentsSyncTaskDetail,
|
||||
toggleMomentsSyncTask,
|
||||
syncMoments
|
||||
} from '@/api/momentsSync';
|
||||
import { MomentsSyncTask } from '@/types/moments-sync';
|
||||
import { ChevronLeft, Edit2, RefreshCw } from 'lucide-react';
|
||||
|
||||
export default function MomentsSyncDetail() {
|
||||
const { id } = useParams();
|
||||
return <div>朋友圈同步详情页,当前ID: {id}</div>;
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [task, setTask] = useState<MomentsSyncTask | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const taskData = await fetchMomentsSyncTaskDetail(id);
|
||||
if (taskData) {
|
||||
setTask(taskData);
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: '获取任务详情失败', variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleToggleStatus = async () => {
|
||||
if (!task || !id) return;
|
||||
try {
|
||||
const newStatus = task.status === 1 ? 2 : 1;
|
||||
await toggleMomentsSyncTask(id, newStatus.toString());
|
||||
setTask({ ...task, status: newStatus });
|
||||
toast({ title: newStatus === 1 ? '任务已开启' : '任务已暂停' });
|
||||
} catch (error) {
|
||||
toast({ title: '操作失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
await syncMoments(id);
|
||||
toast({ title: '同步任务已启动' });
|
||||
fetchTaskDetail(); // 刷新任务详情
|
||||
} catch (error) {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
if (id) {
|
||||
navigate(`/workspace/moments-sync/edit/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">任务不存在</p>
|
||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleSync}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 max-w-7xl mx-auto space-y-6">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h2 className="text-2xl font-bold">{task.name}</h2>
|
||||
<Badge variant={task.status === 1 ? "default" : "secondary"}>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch checked={task.status === 1} onCheckedChange={handleToggleStatus} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">任务详情</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>推送设备:{task.deviceCount} 个</p>
|
||||
<p>内容库:{task.contentLib || '未设置'}</p>
|
||||
<p>已同步:{task.syncCount} 条</p>
|
||||
<p>创建人:{task.creator}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">时间信息</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>创建时间:{task.createTime}</p>
|
||||
<p>上次同步:{task.lastSyncTime || '暂无'}</p>
|
||||
<p>更新时间:{task.updateTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">同步设置</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>同步间隔:{task.syncInterval} 秒</p>
|
||||
<p>每日最大:{task.maxSyncPerDay} 条</p>
|
||||
<p>时间范围:{task.timeRange.start} - {task.timeRange.end}</p>
|
||||
<p>同步模式:{task.syncMode === 'auto' ? '自动' : '手动'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">今日统计</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>今日同步:{task.todaySyncCount} 条</p>
|
||||
<p>总同步数:{task.totalSyncCount} 条</p>
|
||||
<p>目标标签:{task.targetTags.length} 个</p>
|
||||
<p>内容类型:{task.contentTypes.join(', ')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
nkebao/src/pages/workspace/moments-sync/edit.tsx
Normal file
213
nkebao/src/pages/workspace/moments-sync/edit.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { fetchMomentsSyncTaskDetail, updateMomentsSyncTask } from '@/api/momentsSync';
|
||||
import { MomentsSyncTask } from '@/types/moments-sync';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
export default function EditMomentsSyncTask() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [task, setTask] = useState<MomentsSyncTask | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
deviceIds: '',
|
||||
contentLib: '',
|
||||
syncMode: 'auto',
|
||||
syncInterval: '30',
|
||||
maxSyncPerDay: '100',
|
||||
timeStart: '08:00',
|
||||
timeEnd: '22:00',
|
||||
targetTags: '',
|
||||
contentTypes: '',
|
||||
filterKeywords: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchTaskDetail();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchTaskDetail = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const taskData = await fetchMomentsSyncTaskDetail(id);
|
||||
if (taskData) {
|
||||
setTask(taskData);
|
||||
setForm({
|
||||
name: taskData.name,
|
||||
deviceIds: taskData.devices.join(','),
|
||||
contentLib: taskData.contentLib || '',
|
||||
syncMode: taskData.syncMode,
|
||||
syncInterval: taskData.syncInterval.toString(),
|
||||
maxSyncPerDay: taskData.maxSyncPerDay.toString(),
|
||||
timeStart: taskData.timeRange.start,
|
||||
timeEnd: taskData.timeRange.end,
|
||||
targetTags: taskData.targetTags.join(','),
|
||||
contentTypes: taskData.contentTypes.join(','),
|
||||
filterKeywords: taskData.filterKeywords.join(','),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: '获取任务详情失败', variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!id) return;
|
||||
|
||||
if (!form.name.trim()) {
|
||||
toast({ title: '请输入任务名称', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!form.deviceIds.trim()) {
|
||||
toast({ title: '请输入推送设备', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!form.contentLib.trim()) {
|
||||
toast({ title: '请输入内容库', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateMomentsSyncTask({
|
||||
id,
|
||||
name: form.name,
|
||||
devices: form.deviceIds.split(',').map(s => s.trim()).filter(Boolean),
|
||||
contentLib: form.contentLib,
|
||||
syncMode: form.syncMode as 'auto' | 'manual',
|
||||
interval: Number(form.syncInterval),
|
||||
maxSync: Number(form.maxSyncPerDay),
|
||||
startTime: form.timeStart,
|
||||
endTime: form.timeEnd,
|
||||
targetTags: form.targetTags.split(',').map(s => s.trim()).filter(Boolean),
|
||||
contentTypes: form.contentTypes.split(',').map(s => s.trim()).filter(Boolean) as ('text' | 'image' | 'video' | 'link')[],
|
||||
filterKeywords: form.filterKeywords.split(',').map(s => s.trim()).filter(Boolean),
|
||||
});
|
||||
toast({ title: '更新成功' });
|
||||
navigate(`/workspace/moments-sync/${id}`);
|
||||
} catch (error) {
|
||||
toast({ title: '更新失败', variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !task) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">任务不存在</p>
|
||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(`/workspace/moments-sync/${id}`)}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">编辑朋友圈同步任务</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-xl mx-auto bg-white rounded shadow p-6 mt-8">
|
||||
<h2 className="text-lg font-bold mb-6">编辑任务配置</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">任务名称</label>
|
||||
<Input value={form.name} onChange={handleChange} placeholder="请输入任务名称" name="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">推送设备ID(逗号分隔)</label>
|
||||
<Input value={form.deviceIds} onChange={handleChange} placeholder="如:dev1,dev2" name="deviceIds" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">内容库</label>
|
||||
<Input value={form.contentLib} onChange={handleChange} placeholder="请输入内容库名称" name="contentLib" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">同步模式</label>
|
||||
<select name="syncMode" value={form.syncMode} onChange={handleChange} className="w-full border rounded px-3 py-2">
|
||||
<option value="auto">自动</option>
|
||||
<option value="manual">手动</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">同步间隔(秒)</label>
|
||||
<Input value={form.syncInterval} onChange={handleChange} name="syncInterval" type="number" min={1} required />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">每日最大同步数</label>
|
||||
<Input value={form.maxSyncPerDay} onChange={handleChange} name="maxSyncPerDay" type="number" min={1} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">开始时间</label>
|
||||
<Input value={form.timeStart} onChange={handleChange} name="timeStart" type="time" required />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block mb-1 font-medium">结束时间</label>
|
||||
<Input value={form.timeEnd} onChange={handleChange} name="timeEnd" type="time" required />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">目标人群标签(逗号分隔)</label>
|
||||
<Input value={form.targetTags} onChange={handleChange} placeholder="如:重要客户,活跃用户" name="targetTags" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">内容类型(逗号分隔,支持text,image,video)</label>
|
||||
<Input value={form.contentTypes} onChange={handleChange} placeholder="如:text,image,video" name="contentTypes" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1 font-medium">关键词过滤(逗号分隔,可选)</label>
|
||||
<Input value={form.filterKeywords} onChange={handleChange} placeholder="如:产品,服务,优惠" name="filterKeywords" />
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button type="submit" loading={loading} className="w-full">{loading ? '更新中...' : '更新任务'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { Card } from '@/components/ui/card';
|
||||
@@ -11,6 +12,7 @@ import { fetchMomentsSyncTasks, syncMoments, syncAllMoments, MomentsSyncTask } f
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
|
||||
export default function MomentsSyncPage() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [tasks, setTasks] = useState<MomentsSyncTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -107,6 +109,9 @@ export default function MomentsSyncPage() {
|
||||
<div className="text-xs text-gray-500 mb-2">已同步:{task.syncCount || 0} 条</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2 md:mt-0">
|
||||
<Button size="sm" variant="outline" onClick={() => navigate(`/workspace/moments-sync/${task.id}`)}>
|
||||
<Eye className="h-4 w-4 mr-1" />查看
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSync(task.id)}>
|
||||
<SyncIcon className="h-4 w-4 mr-1" />同步
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user