feat: 改完了

This commit is contained in:
许永平
2025-07-09 20:47:05 +08:00
parent 0abfd5fc83
commit d04a593724
3 changed files with 161 additions and 336 deletions

View File

@@ -22,7 +22,6 @@ 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 NewMomentsSync from './pages/workspace/moments-sync/new';
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';
@@ -69,7 +68,7 @@ function App() {
<Route path="/workspace/moments-sync" element={<MomentsSync />} />
<Route path="/workspace/moments-sync/new" element={<NewMomentsSync />} />
<Route path="/workspace/moments-sync/:id" element={<MomentsSyncDetail />} />
<Route path="/workspace/moments-sync/edit/:id" element={<EditMomentsSync />} />
<Route path="/workspace/moments-sync/edit/:id" element={<NewMomentsSync />} />
<Route path="/workspace/ai-assistant" element={<AIAssistant />} />
<Route path="/workspace/traffic-distribution" element={<TrafficDistribution />} />
<Route path="/workspace/traffic-distribution/:id" element={<TrafficDistributionDetail />} />

View File

@@ -1,214 +0,0 @@
import React, { useState, useEffect, useCallback } 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: '',
});
const fetchTaskDetail = useCallback(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);
}
}, [id, toast]);
useEffect(() => {
if (id) {
fetchTaskDetail();
}
}, [id, fetchTaskDetail]);
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),
contentLibraries: form.contentLib.split(',').map(s => s.trim()).filter(Boolean),
syncInterval: Number(form.syncInterval),
syncCount: Number(form.maxSyncPerDay),
syncType: form.syncMode === 'auto' ? 1 : 2,
startTime: form.timeStart,
endTime: form.timeEnd,
accountType: 1, // 默认为业务号
contentTypes: form.contentTypes.split(',').map(s => s.trim()).filter(Boolean) as ('text' | 'image' | 'video' | 'link')[],
targetTags: form.targetTags.split(',').map(s => s.trim()).filter(Boolean),
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,15 +1,15 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/toast';
import { createMomentsSyncTask } from '@/api/momentsSync';
import { ChevronLeft, Clock, Plus, Minus, Search } from 'lucide-react';
import { createMomentsSyncTask, updateMomentsSyncTask, fetchMomentsSyncTaskDetail } from '@/api/momentsSync';
import { ChevronLeft, Clock, Search } from 'lucide-react';
import Layout from '@/components/Layout';
import BottomNav from '@/components/BottomNav';
import { DeviceSelectionDialog } from '@/components/DeviceSelectionDialog';
import { ContentLibrarySelectionDialog } from '@/components/ContentLibrarySelectionDialog';
import { ContentType } from '@/types/moments-sync';
// 步骤指示器组件
interface StepIndicatorProps {
@@ -69,7 +69,7 @@ interface BasicSettingsProps {
interval: number;
accountType: "business" | "personal";
enabled: boolean;
contentTypes: ('text' | 'image' | 'video')[];
contentTypes: ContentType[];
targetTags: string[];
filterKeywords: string[];
};
@@ -204,13 +204,13 @@ function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
<Button
variant="ghost"
onClick={() => {
const newTypes = formData.contentTypes.includes(type)
? formData.contentTypes.filter(t => t !== type)
: [...formData.contentTypes, type];
const newTypes = formData.contentTypes.includes(type as ContentType)
? formData.contentTypes.filter(t => t !== type as ContentType)
: [...formData.contentTypes, type as ContentType];
onChange({ contentTypes: newTypes });
}}
className={`w-full h-12 justify-between rounded-lg ${
formData.contentTypes.includes(type)
formData.contentTypes.includes(type as ContentType)
? "bg-blue-600 hover:bg-blue-600 text-white"
: "bg-white hover:bg-gray-50"
}`}
@@ -244,6 +244,7 @@ function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
export default function NewMomentsSyncTask() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
@@ -259,11 +260,13 @@ export default function NewMomentsSyncTask() {
enabled: true,
selectedDevices: [] as string[],
selectedLibraries: [] as string[],
contentTypes: ['text', 'image', 'video'] as ('text' | 'image' | 'video')[],
contentTypes: ['text', 'image', 'video'] as ContentType[],
targetTags: [] as string[],
filterKeywords: [] as string[],
});
const isEditMode = !!id;
const handleUpdateFormData = (data: Partial<typeof formData>) => {
setFormData((prev) => ({ ...prev, ...data }));
};
@@ -276,6 +279,41 @@ export default function NewMomentsSyncTask() {
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
// 获取任务详情
const fetchTaskDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const taskData = await fetchMomentsSyncTaskDetail(id);
if (taskData) {
setFormData({
taskName: taskData.name,
startTime: taskData.timeRange?.start || '06:00',
endTime: taskData.timeRange?.end || '23:59',
syncCount: taskData.maxSyncPerDay || 5,
interval: taskData.syncInterval || 30,
accountType: taskData.syncMode === 'auto' ? 'business' : 'personal',
enabled: taskData.status === 1,
selectedDevices: taskData.devices || [],
selectedLibraries: taskData.contentLib ? [taskData.contentLib] : [],
contentTypes: taskData.contentTypes || ['text', 'image', 'video'],
targetTags: taskData.targetTags || [],
filterKeywords: taskData.filterKeywords || [],
});
}
} catch (error) {
toast({ title: '获取任务详情失败', variant: 'destructive' });
} finally {
setLoading(false);
}
}, [id, toast]);
useEffect(() => {
if (isEditMode) {
fetchTaskDetail();
}
}, [isEditMode, fetchTaskDetail]);
const handleComplete = async () => {
if (!formData.taskName.trim()) {
toast({ title: '请输入任务名称', variant: 'destructive' });
@@ -292,24 +330,35 @@ export default function NewMomentsSyncTask() {
setLoading(true);
try {
await createMomentsSyncTask({
name: formData.taskName,
devices: formData.selectedDevices,
contentLibraries: formData.selectedLibraries,
syncInterval: formData.interval,
syncCount: formData.syncCount,
syncType: formData.accountType === 'business' ? 1 : 2,
startTime: formData.startTime,
endTime: formData.endTime,
accountType: formData.accountType === 'business' ? 1 : 2,
contentTypes: formData.contentTypes,
targetTags: formData.targetTags,
filterKeywords: formData.filterKeywords,
const taskData = {
name: formData.taskName,
devices: formData.selectedDevices,
contentLibraries: formData.selectedLibraries,
syncInterval: formData.interval,
syncCount: formData.syncCount,
syncType: formData.accountType === 'business' ? 1 : 2,
startTime: formData.startTime,
endTime: formData.endTime,
accountType: formData.accountType === 'business' ? 1 : 2,
contentTypes: formData.contentTypes,
targetTags: formData.targetTags,
filterKeywords: formData.filterKeywords,
};
if (isEditMode && id) {
await updateMomentsSyncTask({
id,
...taskData,
});
toast({ title: '创建成功' });
navigate('/workspace/moments-sync');
toast({ title: '更新成功' });
navigate(`/workspace/moments-sync/${id}`);
} else {
await createMomentsSyncTask(taskData);
toast({ title: '创建成功' });
navigate('/workspace/moments-sync');
}
} catch (error) {
toast({ title: '创建失败', variant: 'destructive' });
toast({ title: isEditMode ? '更新失败' : '创建失败', variant: 'destructive' });
} finally {
setLoading(false);
}
@@ -323,7 +372,7 @@ export default function NewMomentsSyncTask() {
<Button variant="ghost" size="icon" onClick={() => navigate(-1)} className="hover:bg-gray-50 mr-2">
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-lg font-medium"></h1>
<h1 className="text-lg font-medium">{isEditMode ? '编辑朋友圈同步' : '新建朋友圈同步'}</h1>
</div>
</div>
}
@@ -332,110 +381,101 @@ export default function NewMomentsSyncTask() {
<div className="p-4 space-y-6">
<div className="mt-4">
<StepIndicator currentStep={currentStep} />
<div className="mt-8">
{currentStep === 1 && (
<BasicSettings formData={formData} onChange={handleUpdateFormData} onNext={handleNext} />
)}
{currentStep === 2 && (
<div className="space-y-6">
<div className="relative">
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
<Input
placeholder="选择设备"
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
onClick={() => setDeviceDialogOpen(true)}
readOnly
{currentStep === 2 && (
<div className="space-y-6">
<div className="relative">
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
<Input
placeholder="选择设备"
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
onClick={() => setDeviceDialogOpen(true)}
readOnly
/>
</div>
{formData.selectedDevices.length > 0 && (
<div className="text-base text-gray-500">
{formData.selectedDevices.length}
<div className="text-sm text-gray-400 mt-1">
{formData.selectedDevices.map(id => {
// 这里可以根据实际API获取设备名称
return `设备 ${id}`;
}).join(', ')}
</div>
</div>
)}
<div className="flex space-x-4 pt-4">
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
</Button>
<Button
onClick={handleNext}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
</Button>
</div>
<DeviceSelectionDialog
open={deviceDialogOpen}
onOpenChange={setDeviceDialogOpen}
selectedDevices={formData.selectedDevices}
onSelect={(devices) => {
handleUpdateFormData({ selectedDevices: devices });
}}
/>
</div>
{formData.selectedDevices.length > 0 && (
<div className="text-base text-gray-500">
{formData.selectedDevices.length}
<div className="text-sm text-gray-400 mt-1">
{formData.selectedDevices.map(id => {
// 这里可以根据实际API获取设备名称
return `设备 ${id}`;
}).join(', ')}
</div>
)}
{currentStep === 3 && (
<div className="space-y-6">
<div className="relative">
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
<Input
placeholder="选择内容库"
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
onClick={() => setContentLibraryDialogOpen(true)}
readOnly
/>
</div>
)}
<div className="flex space-x-4 pt-4">
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
</Button>
<Button
onClick={handleNext}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
</Button>
</div>
<DeviceSelectionDialog
open={deviceDialogOpen}
onOpenChange={setDeviceDialogOpen}
selectedDevices={formData.selectedDevices}
onSelect={(devices) => {
handleUpdateFormData({ selectedDevices: devices });
}}
/>
</div>
)}
{currentStep === 3 && (
<div className="space-y-6">
<div className="relative">
<Search className="absolute left-3 top-4 h-5 w-5 text-gray-400" />
<Input
placeholder="选择内容库"
className="h-12 pl-11 rounded-xl border-gray-200 text-base"
onClick={() => setContentLibraryDialogOpen(true)}
readOnly
{formData.selectedLibraries.length > 0 && (
<div className="text-base text-gray-500">
{formData.selectedLibraries.length}
<div className="text-sm text-gray-400 mt-1">
{formData.selectedLibraries.map(id => {
// 这里可以根据实际API获取内容库名称
return `朋友圈内容库${id}`;
}).join(', ')}
</div>
</div>
)}
<div className="flex space-x-4 pt-4">
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
</Button>
<Button
onClick={handleComplete}
loading={loading}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
{loading ? (isEditMode ? '更新中...' : '创建中...') : (isEditMode ? '更新' : '完成')}
</Button>
</div>
<ContentLibrarySelectionDialog
open={contentLibraryDialogOpen}
onOpenChange={setContentLibraryDialogOpen}
selectedLibraries={formData.selectedLibraries}
onSelect={(libraries) => {
handleUpdateFormData({ selectedLibraries: libraries });
}}
/>
</div>
{formData.selectedLibraries.length > 0 && (
<div className="text-base text-gray-500">
{formData.selectedLibraries.length}
<div className="text-sm text-gray-400 mt-1">
{formData.selectedLibraries.map(id => {
// 这里可以根据实际API获取内容库名称
return `朋友圈内容库${id}`;
}).join(', ')}
</div>
</div>
)}
<div className="flex space-x-4 pt-4">
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-xl text-base">
</Button>
<Button
onClick={handleComplete}
loading={loading}
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
{loading ? '创建中...' : '完成'}
</Button>
</div>
<ContentLibrarySelectionDialog
open={contentLibraryDialogOpen}
onOpenChange={setContentLibraryDialogOpen}
selectedLibraries={formData.selectedLibraries}
onSelect={(libraries) => {
handleUpdateFormData({ selectedLibraries: libraries });
}}
/>
</div>
)}
)}
</div>
</div>
</div>
</div>
</Layout>
);
}
}