feat:缓存下

This commit is contained in:
许永平
2025-07-09 18:08:17 +08:00
parent 37dabcfdf6
commit c6107f56af
6 changed files with 397 additions and 5 deletions

View File

@@ -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 />} />

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>