这块改完了
This commit is contained in:
@@ -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
201
nkebao/src/api/groupPush.ts
Normal 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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
260
nkebao/src/pages/workspace/group-push/new.tsx
Normal file
260
nkebao/src/pages/workspace/group-push/new.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user