这块改完了

This commit is contained in:
云电脑的永平
2025-07-16 14:19:47 +08:00
parent 1bb34b4932
commit d9ef245018
8 changed files with 1742 additions and 473 deletions

View File

@@ -19,6 +19,7 @@ import NewDistribution from './pages/workspace/traffic-distribution/NewDistribut
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 NewGroupPush from './pages/workspace/group-push/new';
import MomentsSync from './pages/workspace/moments-sync/MomentsSync';
import MomentsSyncDetail from './pages/workspace/moments-sync/Detail';
import NewMomentsSync from './pages/workspace/moments-sync/new';
@@ -71,6 +72,7 @@ function App() {
<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/group-push/new" element={<NewGroupPush />} />
<Route path="/workspace/moments-sync" element={<MomentsSync />} />
<Route path="/workspace/moments-sync/new" element={<NewMomentsSync />} />
<Route path="/workspace/moments-sync/:id" element={<MomentsSyncDetail />} />

201
nkebao/src/api/groupPush.ts Normal file
View File

@@ -0,0 +1,201 @@
import { get, post, put, del } from './request';
// 群发推送任务类型定义
export interface GroupPushTask {
id: string;
name: string;
status: number; // 1: 运行中, 2: 已暂停
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;
}
// API响应类型
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
/**
* 获取群发推送任务列表
*/
export async function fetchGroupPushTasks(): Promise<GroupPushTask[]> {
try {
const response = await get<ApiResponse<GroupPushTask[]>>('/v1/workspace/group-push/tasks');
if (response.code === 200 && Array.isArray(response.data)) {
return response.data;
}
// 如果API不可用返回模拟数据
return getMockGroupPushTasks();
} catch (error) {
console.error('获取群发推送任务失败:', error);
// 返回模拟数据作为降级方案
return getMockGroupPushTasks();
}
}
/**
* 删除群发推送任务
*/
export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
try {
const response = await del<ApiResponse>(`/v1/workspace/group-push/tasks/${id}`);
return response;
} catch (error) {
console.error('删除群发推送任务失败:', error);
throw error;
}
}
/**
* 切换群发推送任务状态
*/
export async function toggleGroupPushTask(id: string, status: string): Promise<ApiResponse> {
try {
const response = await post<ApiResponse>(`/v1/workspace/group-push/tasks/${id}/toggle`, {
status
});
return response;
} catch (error) {
console.error('切换群发推送任务状态失败:', error);
throw error;
}
}
/**
* 复制群发推送任务
*/
export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
try {
const response = await post<ApiResponse>(`/v1/workspace/group-push/tasks/${id}/copy`);
return response;
} catch (error) {
console.error('复制群发推送任务失败:', error);
throw error;
}
}
/**
* 创建群发推送任务
*/
export async function createGroupPushTask(taskData: Partial<GroupPushTask>): Promise<ApiResponse> {
try {
const response = await post<ApiResponse>('/v1/workspace/group-push/tasks', taskData);
return response;
} catch (error) {
console.error('创建群发推送任务失败:', error);
throw error;
}
}
/**
* 更新群发推送任务
*/
export async function updateGroupPushTask(id: string, taskData: Partial<GroupPushTask>): Promise<ApiResponse> {
try {
const response = await put<ApiResponse>(`/v1/workspace/group-push/tasks/${id}`, taskData);
return response;
} catch (error) {
console.error('更新群发推送任务失败:', error);
throw error;
}
}
/**
* 获取群发推送任务详情
*/
export async function getGroupPushTaskDetail(id: string): Promise<GroupPushTask> {
try {
const response = await get<ApiResponse<GroupPushTask>>(`/v1/workspace/group-push/tasks/${id}`);
if (response.code === 200 && response.data) {
return response.data;
}
throw new Error(response.message || '获取任务详情失败');
} catch (error) {
console.error('获取群发推送任务详情失败:', error);
throw error;
}
}
/**
* 模拟数据 - 当API不可用时使用
*/
function getMockGroupPushTasks(): GroupPushTask[] {
return [
{
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: 1, // 运行中
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: 2, // 已暂停
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',
},
{
id: '3',
name: '新客户欢迎消息',
deviceCount: 3,
targetGroups: ['新客户群', '体验群'],
pushCount: 234,
successCount: 218,
lastPushTime: '2025-02-06 15:30:22',
createTime: '2024-12-01 09:15:30',
creator: 'admin',
status: 1, // 运行中
pushInterval: 30,
maxPushPerDay: 300,
timeRange: { start: '08:00', end: '22:00' },
messageType: 'text',
messageContent: '欢迎加入我们的大家庭!这里有最新的产品信息和优惠活动...',
targetTags: ['新客户', '欢迎'],
pushMode: 'immediate',
},
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,243 @@
import React, { useState } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Minus, Plus } from 'lucide-react';
interface BasicSettingsProps {
defaultValues?: {
name: string;
pushTimeStart: string;
pushTimeEnd: string;
dailyPushCount: number;
pushOrder: 'earliest' | 'latest';
isLoopPush: boolean;
isImmediatePush: boolean;
isEnabled: boolean;
};
onNext: (values: any) => void;
onSave: (values: any) => void;
onCancel: () => void;
loading?: boolean;
}
export default function BasicSettings({
defaultValues = {
name: '',
pushTimeStart: '06:00',
pushTimeEnd: '23:59',
dailyPushCount: 20,
pushOrder: 'latest',
isLoopPush: false,
isImmediatePush: false,
isEnabled: false,
},
onNext,
onSave,
onCancel,
loading = false,
}: BasicSettingsProps) {
const [values, setValues] = useState(defaultValues);
const handleChange = (field: string, value: any) => {
setValues((prev) => ({ ...prev, [field]: value }));
};
const handleCountChange = (increment: boolean) => {
setValues((prev) => ({
...prev,
dailyPushCount: increment ? prev.dailyPushCount + 1 : Math.max(1, prev.dailyPushCount - 1),
}));
};
return (
<div className="space-y-6">
<Card>
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
{/* 任务名称 */}
<div className="space-y-2">
<Label htmlFor="taskName" className="flex items-center text-sm font-medium">
<span className="text-red-500 mr-1">*</span>:
</Label>
<Input
id="taskName"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="请输入任务名称"
/>
</div>
{/* 允许推送的时间段 */}
<div className="space-y-2">
<Label className="text-sm font-medium">:</Label>
<div className="flex items-center space-x-2">
<Input
type="time"
value={values.pushTimeStart}
onChange={(e) => handleChange('pushTimeStart', e.target.value)}
className="w-full"
/>
<span className="text-gray-500"></span>
<Input
type="time"
value={values.pushTimeEnd}
onChange={(e) => handleChange('pushTimeEnd', e.target.value)}
className="w-full"
/>
</div>
</div>
{/* 每日推送 */}
<div className="space-y-2">
<Label className="text-sm font-medium">:</Label>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
type="button"
onClick={() => handleCountChange(false)}
className="h-9 w-9"
disabled={loading}
>
<Minus className="h-4 w-4" />
</Button>
<Input
type="number"
value={values.dailyPushCount.toString()}
onChange={(e) => handleChange('dailyPushCount', Number.parseInt(e.target.value) || 1)}
className="w-20 text-center"
min={1}
disabled={loading}
/>
<Button
variant="outline"
size="icon"
type="button"
onClick={() => handleCountChange(true)}
className="h-9 w-9"
disabled={loading}
>
<Plus className="h-4 w-4" />
</Button>
<span className="text-gray-500"></span>
</div>
</div>
{/* 推送顺序 */}
<div className="space-y-2">
<Label className="text-sm font-medium">:</Label>
<div className="flex">
<Button
type="button"
variant={values.pushOrder === 'earliest' ? 'default' : 'outline'}
className={`rounded-r-none flex-1 ${values.pushOrder === 'earliest' ? '' : 'text-gray-500'}`}
onClick={() => handleChange('pushOrder', 'earliest')}
disabled={loading}
>
</Button>
<Button
type="button"
variant={values.pushOrder === 'latest' ? 'default' : 'outline'}
className={`rounded-l-none flex-1 ${values.pushOrder === 'latest' ? '' : 'text-gray-500'}`}
onClick={() => handleChange('pushOrder', 'latest')}
disabled={loading}
>
</Button>
</div>
</div>
{/* 是否循环推送 */}
<div className="flex items-center justify-between">
<Label htmlFor="isLoopPush" className="flex items-center text-sm font-medium">
<span className="text-red-500 mr-1">*</span>:
</Label>
<div className="flex items-center space-x-2">
<span className={values.isLoopPush ? 'text-gray-400' : 'text-gray-900'}></span>
<Switch
id="isLoopPush"
checked={values.isLoopPush}
onCheckedChange={(checked) => handleChange('isLoopPush', checked)}
disabled={loading}
/>
<span className={values.isLoopPush ? 'text-gray-900' : 'text-gray-400'}></span>
</div>
</div>
{/* 是否立即推送 */}
<div className="flex items-center justify-between">
<Label htmlFor="isImmediatePush" className="flex items-center text-sm font-medium">
<span className="text-red-500 mr-1">*</span>:
</Label>
<div className="flex items-center space-x-2">
<span className={values.isImmediatePush ? 'text-gray-400' : 'text-gray-900'}></span>
<Switch
id="isImmediatePush"
checked={values.isImmediatePush}
onCheckedChange={(checked) => handleChange('isImmediatePush', checked)}
disabled={loading}
/>
<span className={values.isImmediatePush ? 'text-gray-900' : 'text-gray-400'}></span>
</div>
</div>
{values.isImmediatePush && (
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-sm text-yellow-700">
</div>
)}
{/* 是否启用 */}
<div className="flex items-center justify-between">
<Label htmlFor="isEnabled" className="flex items-center text-sm font-medium">
<span className="text-red-500 mr-1">*</span>:
</Label>
<div className="flex items-center space-x-2">
<span className={values.isEnabled ? 'text-gray-400' : 'text-gray-900'}></span>
<Switch
id="isEnabled"
checked={values.isEnabled}
onCheckedChange={(checked) => handleChange('isEnabled', checked)}
disabled={loading}
/>
<span className={values.isEnabled ? 'text-gray-900' : 'text-gray-400'}></span>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="flex space-x-2 justify-center sm:justify-end">
<Button
type="button"
onClick={() => onNext(values)}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
<Button
type="button"
variant="outline"
onClick={() => onSave(values)}
className="flex-1 sm:flex-none"
disabled={loading}
>
{loading ? '保存中...' : '保存'}
</Button>
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,237 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Search, FileText } from 'lucide-react';
interface ContentLibrary {
id: string;
name: string;
targets: Array<{
id: string;
avatar: string;
}>;
}
interface ContentSelectorProps {
selectedLibraries: ContentLibrary[];
onLibrariesChange: (libraries: ContentLibrary[]) => void;
onPrevious: () => void;
onNext: () => void;
onSave: () => void;
onCancel: () => void;
loading?: boolean;
}
// 模拟内容库数据
const mockLibraries: ContentLibrary[] = [
{
id: '1',
name: '产品推广内容库',
targets: [
{ id: '1', avatar: 'https://via.placeholder.com/32' },
{ id: '2', avatar: 'https://via.placeholder.com/32' },
{ id: '3', avatar: 'https://via.placeholder.com/32' },
],
},
{
id: '2',
name: '活动宣传内容库',
targets: [
{ id: '4', avatar: 'https://via.placeholder.com/32' },
{ id: '5', avatar: 'https://via.placeholder.com/32' },
],
},
{
id: '3',
name: '客户服务内容库',
targets: [
{ id: '6', avatar: 'https://via.placeholder.com/32' },
{ id: '7', avatar: 'https://via.placeholder.com/32' },
{ id: '8', avatar: 'https://via.placeholder.com/32' },
{ id: '9', avatar: 'https://via.placeholder.com/32' },
],
},
{
id: '4',
name: '节日问候内容库',
targets: [
{ id: '10', avatar: 'https://via.placeholder.com/32' },
{ id: '11', avatar: 'https://via.placeholder.com/32' },
],
},
{
id: '5',
name: '新品发布内容库',
targets: [
{ id: '12', avatar: 'https://via.placeholder.com/32' },
{ id: '13', avatar: 'https://via.placeholder.com/32' },
{ id: '14', avatar: 'https://via.placeholder.com/32' },
],
},
];
export default function ContentSelector({
selectedLibraries,
onLibrariesChange,
onPrevious,
onNext,
onSave,
onCancel,
loading = false,
}: ContentSelectorProps) {
const [searchTerm, setSearchTerm] = useState('');
const [libraries, setLibraries] = useState<ContentLibrary[]>(mockLibraries);
const filteredLibraries = libraries.filter((library) =>
library.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => {
if (checked) {
onLibrariesChange([...selectedLibraries, library]);
} else {
onLibrariesChange(selectedLibraries.filter((l) => l.id !== library.id));
}
};
const handleSelectAll = () => {
if (selectedLibraries.length === filteredLibraries.length) {
onLibrariesChange([]);
} else {
onLibrariesChange(filteredLibraries);
}
};
const isLibrarySelected = (libraryId: string) => {
return selectedLibraries.some((library) => library.id === libraryId);
};
return (
<div className="space-y-6">
<Card>
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
{/* 搜索框 */}
<div className="space-y-2">
<Label className="text-sm font-medium">:</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索内容库名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9"
disabled={loading}
/>
</div>
</div>
{/* 全选按钮 */}
<div className="flex items-center space-x-2">
<Checkbox
id="selectAll"
checked={selectedLibraries.length === filteredLibraries.length && filteredLibraries.length > 0}
onCheckedChange={handleSelectAll}
disabled={loading}
/>
<Label htmlFor="selectAll" className="text-sm font-medium">
({selectedLibraries.length}/{filteredLibraries.length})
</Label>
</div>
{/* 内容库列表 */}
<div className="space-y-2 max-h-96 overflow-y-auto">
{filteredLibraries.map((library) => (
<div
key={library.id}
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
>
<Checkbox
id={library.id}
checked={isLibrarySelected(library.id)}
onCheckedChange={(checked) => handleLibraryToggle(library, checked as boolean)}
disabled={loading}
/>
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<FileText className="h-5 w-5 text-blue-600" />
</div>
<div className="flex-1">
<div className="font-medium text-sm">{library.name}</div>
<div className="text-xs text-gray-500">
{library.targets.length}
</div>
</div>
<div className="flex -space-x-1">
{library.targets.slice(0, 3).map((target) => (
<img
key={target.id}
src={target.avatar}
alt=""
className="w-6 h-6 rounded-full border border-white"
/>
))}
{library.targets.length > 3 && (
<div className="w-6 h-6 rounded-full bg-gray-200 border border-white flex items-center justify-center">
<span className="text-xs text-gray-600">+{library.targets.length - 3}</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
{filteredLibraries.length === 0 && (
<div className="text-center py-8 text-gray-500">
<FileText className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)}
</div>
</CardContent>
</Card>
<div className="flex space-x-2 justify-center sm:justify-end">
<Button
type="button"
variant="outline"
onClick={onPrevious}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
<Button
type="button"
onClick={onNext}
className="flex-1 sm:flex-none"
disabled={loading || selectedLibraries.length === 0}
>
</Button>
<Button
type="button"
variant="outline"
onClick={onSave}
className="flex-1 sm:flex-none"
disabled={loading}
>
{loading ? '保存中...' : '保存'}
</Button>
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Search, Users } from 'lucide-react';
interface WechatGroup {
id: string;
name: string;
avatar: string;
serviceAccount: {
id: string;
name: string;
avatar: string;
};
}
interface GroupSelectorProps {
selectedGroups: WechatGroup[];
onGroupsChange: (groups: WechatGroup[]) => void;
onPrevious: () => void;
onNext: () => void;
onSave: () => void;
onCancel: () => void;
loading?: boolean;
}
// 模拟群组数据
const mockGroups: WechatGroup[] = [
{
id: '1',
name: 'VIP客户群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '1',
name: '客服小美',
avatar: 'https://via.placeholder.com/32',
},
},
{
id: '2',
name: '潜在客户群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '1',
name: '客服小美',
avatar: 'https://via.placeholder.com/32',
},
},
{
id: '3',
name: '活动群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '2',
name: '推广专员',
avatar: 'https://via.placeholder.com/32',
},
},
{
id: '4',
name: '推广群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '2',
name: '推广专员',
avatar: 'https://via.placeholder.com/32',
},
},
{
id: '5',
name: '新客户群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '3',
name: '销售小王',
avatar: 'https://via.placeholder.com/32',
},
},
{
id: '6',
name: '体验群',
avatar: 'https://via.placeholder.com/40',
serviceAccount: {
id: '3',
name: '销售小王',
avatar: 'https://via.placeholder.com/32',
},
},
];
export default function GroupSelector({
selectedGroups,
onGroupsChange,
onPrevious,
onNext,
onSave,
onCancel,
loading = false,
}: GroupSelectorProps) {
const [searchTerm, setSearchTerm] = useState('');
const [groups, setGroups] = useState<WechatGroup[]>(mockGroups);
const filteredGroups = groups.filter((group) =>
group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleGroupToggle = (group: WechatGroup, checked: boolean) => {
if (checked) {
onGroupsChange([...selectedGroups, group]);
} else {
onGroupsChange(selectedGroups.filter((g) => g.id !== group.id));
}
};
const handleSelectAll = () => {
if (selectedGroups.length === filteredGroups.length) {
onGroupsChange([]);
} else {
onGroupsChange(filteredGroups);
}
};
const isGroupSelected = (groupId: string) => {
return selectedGroups.some((group) => group.id === groupId);
};
return (
<div className="space-y-6">
<Card>
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
{/* 搜索框 */}
<div className="space-y-2">
<Label className="text-sm font-medium">:</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索群组名称或客服名称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9"
disabled={loading}
/>
</div>
</div>
{/* 全选按钮 */}
<div className="flex items-center space-x-2">
<Checkbox
id="selectAll"
checked={selectedGroups.length === filteredGroups.length && filteredGroups.length > 0}
onCheckedChange={handleSelectAll}
disabled={loading}
/>
<Label htmlFor="selectAll" className="text-sm font-medium">
({selectedGroups.length}/{filteredGroups.length})
</Label>
</div>
{/* 群组列表 */}
<div className="space-y-2 max-h-96 overflow-y-auto">
{filteredGroups.map((group) => (
<div
key={group.id}
className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50"
>
<Checkbox
id={group.id}
checked={isGroupSelected(group.id)}
onCheckedChange={(checked) => handleGroupToggle(group, checked as boolean)}
disabled={loading}
/>
<div className="flex items-center space-x-3 flex-1">
<img
src={group.avatar}
alt={group.name}
className="w-10 h-10 rounded-full"
/>
<div className="flex-1">
<div className="font-medium text-sm">{group.name}</div>
<div className="text-xs text-gray-500 flex items-center">
<img
src={group.serviceAccount.avatar}
alt={group.serviceAccount.name}
className="w-4 h-4 rounded-full mr-1"
/>
{group.serviceAccount.name}
</div>
</div>
</div>
</div>
))}
</div>
{filteredGroups.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Users className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p></p>
</div>
)}
</div>
</CardContent>
</Card>
<div className="flex space-x-2 justify-center sm:justify-end">
<Button
type="button"
variant="outline"
onClick={onPrevious}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
<Button
type="button"
onClick={onNext}
className="flex-1 sm:flex-none"
disabled={loading || selectedGroups.length === 0}
>
</Button>
<Button
type="button"
variant="outline"
onClick={onSave}
className="flex-1 sm:flex-none"
disabled={loading}
>
{loading ? '保存中...' : '保存'}
</Button>
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Steps, StepItem } from 'tdesign-mobile-react';
interface StepIndicatorProps {
currentStep: number;
steps: { id: number; title: string; subtitle: string }[];
}
export default function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
return (
<div className="mb-6">
<Steps current={currentStep - 1}>
{steps.map((step) => (
<StepItem key={step.id} title={step.subtitle} />
))}
</Steps>
</div>
);
}

View File

@@ -0,0 +1,260 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { createGroupPushTask } from '@/api/groupPush';
import Layout from '@/components/Layout';
import PageHeader from '@/components/PageHeader';
import StepIndicator from './components/StepIndicator';
import BasicSettings from './components/BasicSettings';
import GroupSelector from './components/GroupSelector';
import ContentSelector from './components/ContentSelector';
// 类型定义
interface WechatGroup {
id: string;
name: string;
avatar: string;
serviceAccount: {
id: string;
name: string;
avatar: string;
};
}
interface ContentLibrary {
id: string;
name: string;
targets: Array<{
id: string;
avatar: string;
}>;
}
interface FormData {
name: string;
pushTimeStart: string;
pushTimeEnd: string;
dailyPushCount: number;
pushOrder: 'earliest' | 'latest';
isLoopPush: boolean;
isImmediatePush: boolean;
isEnabled: boolean;
groups: WechatGroup[];
contentLibraries: ContentLibrary[];
}
const steps = [
{ id: 1, title: '步骤 1', subtitle: '基础设置' },
{ id: 2, title: '步骤 2', subtitle: '选择社群' },
{ id: 3, title: '步骤 3', subtitle: '选择内容库' },
{ id: 4, title: '步骤 4', subtitle: '京东联盟' },
];
export default function NewGroupPush() {
const navigate = useNavigate();
const { toast } = useToast();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<FormData>({
name: '',
pushTimeStart: '06:00',
pushTimeEnd: '23:59',
dailyPushCount: 20,
pushOrder: 'latest',
isLoopPush: false,
isImmediatePush: false,
isEnabled: false,
groups: [],
contentLibraries: [],
});
const handleBasicSettingsNext = (values: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...values }));
setCurrentStep(2);
};
const handleGroupsChange = (groups: WechatGroup[]) => {
setFormData((prev) => ({ ...prev, groups }));
};
const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => {
setFormData((prev) => ({ ...prev, contentLibraries }));
};
const handleSave = async () => {
if (!formData.name.trim()) {
toast({
title: '请输入任务名称',
variant: 'destructive',
});
return;
}
if (formData.groups.length === 0) {
toast({
title: '请选择至少一个社群',
variant: 'destructive',
});
return;
}
if (formData.contentLibraries.length === 0) {
toast({
title: '请选择至少一个内容库',
variant: 'destructive',
});
return;
}
setLoading(true);
try {
// 转换数据格式以匹配API
const apiData = {
name: formData.name,
timeRange: {
start: formData.pushTimeStart,
end: formData.pushTimeEnd,
},
maxPushPerDay: formData.dailyPushCount,
pushOrder: formData.pushOrder,
isLoopPush: formData.isLoopPush,
isImmediatePush: formData.isImmediatePush,
isEnabled: formData.isEnabled,
targetGroups: formData.groups.map(g => g.name),
contentLibraries: formData.contentLibraries.map(c => c.name),
pushMode: formData.isImmediatePush ? 'immediate' as const : 'scheduled' as const,
messageType: 'text' as const,
messageContent: '',
targetTags: [],
pushInterval: 60,
};
const response = await createGroupPushTask(apiData);
if (response.code === 200) {
toast({
title: '保存成功',
description: `社群推送任务"${formData.name}"已保存`,
});
navigate('/workspace/group-push');
} else {
toast({
title: '保存失败',
description: response.message || '请稍后重试',
variant: 'destructive',
});
}
} catch (error) {
console.error('保存任务失败:', error);
toast({
title: '保存失败',
description: '请稍后重试',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleCancel = () => {
navigate('/workspace/group-push');
};
return (
<Layout
header={
<PageHeader
title="新建社群推送任务"
defaultBackPath="/workspace/group-push"
/>
}
>
<div className="container mx-auto py-4 px-4 sm:px-6 md:py-6">
<StepIndicator currentStep={currentStep} steps={steps} />
<div className="mt-8">
{currentStep === 1 && (
<BasicSettings
defaultValues={{
name: formData.name,
pushTimeStart: formData.pushTimeStart,
pushTimeEnd: formData.pushTimeEnd,
dailyPushCount: formData.dailyPushCount,
pushOrder: formData.pushOrder,
isLoopPush: formData.isLoopPush,
isImmediatePush: formData.isImmediatePush,
isEnabled: formData.isEnabled,
}}
onNext={handleBasicSettingsNext}
onSave={handleSave}
onCancel={handleCancel}
loading={loading}
/>
)}
{currentStep === 2 && (
<GroupSelector
selectedGroups={formData.groups}
onGroupsChange={handleGroupsChange}
onPrevious={() => setCurrentStep(1)}
onNext={() => setCurrentStep(3)}
onSave={handleSave}
onCancel={handleCancel}
loading={loading}
/>
)}
{currentStep === 3 && (
<ContentSelector
selectedLibraries={formData.contentLibraries}
onLibrariesChange={handleLibrariesChange}
onPrevious={() => setCurrentStep(2)}
onNext={() => setCurrentStep(4)}
onSave={handleSave}
onCancel={handleCancel}
loading={loading}
/>
)}
{currentStep === 4 && (
<div className="space-y-6">
<div className="border rounded-md p-8 text-center text-gray-500">
</div>
<div className="flex space-x-2 justify-center sm:justify-end">
<Button
type="button"
variant="outline"
onClick={() => setCurrentStep(3)}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
<Button
type="button"
onClick={handleSave}
className="flex-1 sm:flex-none"
disabled={loading}
>
{loading ? '保存中...' : '完成'}
</Button>
<Button
type="button"
variant="outline"
onClick={handleCancel}
className="flex-1 sm:flex-none"
disabled={loading}
>
</Button>
</div>
</div>
)}
</div>
</div>
</Layout>
);
}