diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index c3fd82fd..cc32caf5 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/nkebao/src/api/groupPush.ts b/nkebao/src/api/groupPush.ts new file mode 100644 index 00000000..8ad67712 --- /dev/null +++ b/nkebao/src/api/groupPush.ts @@ -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 { + code: number; + message: string; + data: T; +} + +/** + * 获取群发推送任务列表 + */ +export async function fetchGroupPushTasks(): Promise { + try { + const response = await get>('/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 { + try { + const response = await del(`/v1/workspace/group-push/tasks/${id}`); + return response; + } catch (error) { + console.error('删除群发推送任务失败:', error); + throw error; + } +} + +/** + * 切换群发推送任务状态 + */ +export async function toggleGroupPushTask(id: string, status: string): Promise { + try { + const response = await post(`/v1/workspace/group-push/tasks/${id}/toggle`, { + status + }); + return response; + } catch (error) { + console.error('切换群发推送任务状态失败:', error); + throw error; + } +} + +/** + * 复制群发推送任务 + */ +export async function copyGroupPushTask(id: string): Promise { + try { + const response = await post(`/v1/workspace/group-push/tasks/${id}/copy`); + return response; + } catch (error) { + console.error('复制群发推送任务失败:', error); + throw error; + } +} + +/** + * 创建群发推送任务 + */ +export async function createGroupPushTask(taskData: Partial): Promise { + try { + const response = await post('/v1/workspace/group-push/tasks', taskData); + return response; + } catch (error) { + console.error('创建群发推送任务失败:', error); + throw error; + } +} + +/** + * 更新群发推送任务 + */ +export async function updateGroupPushTask(id: string, taskData: Partial): Promise { + try { + const response = await put(`/v1/workspace/group-push/tasks/${id}`, taskData); + return response; + } catch (error) { + console.error('更新群发推送任务失败:', error); + throw error; + } +} + +/** + * 获取群发推送任务详情 + */ +export async function getGroupPushTaskDetail(id: string): Promise { + try { + const response = await get>(`/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', + }, + ]; +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.tsx b/nkebao/src/pages/workspace/group-push/GroupPush.tsx index 3de8edd9..8a32dc66 100644 --- a/nkebao/src/pages/workspace/group-push/GroupPush.tsx +++ b/nkebao/src/pages/workspace/group-push/GroupPush.tsx @@ -1,474 +1,533 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Plus, - Filter, - Search, - RefreshCw, - MoreVertical, - Clock, - Edit, - Trash2, - Eye, - Copy, - ChevronDown, - ChevronUp, - Settings, - Calendar, - Users, - Send, - // CheckCircle, - // XCircle, - MessageSquare, -} from 'lucide-react'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { Switch } from '@/components/ui/switch'; -import { Progress } from '@/components/ui/progress'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import Layout from '@/components/Layout'; -import PageHeader from '@/components/PageHeader'; -import BottomNav from '@/components/BottomNav'; -import { useToast } from '@/components/ui/toast'; -import '@/components/Layout.css'; - -interface PushTask { - id: string; - name: string; - status: 'running' | 'paused' | 'completed'; - 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; -} - -export default function GroupPush() { - const navigate = useNavigate(); - const { toast } = useToast(); - const [expandedTaskId, setExpandedTaskId] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); - const [tasks, setTasks] = useState([ - { - 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: 'running', - 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: 'paused', - 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', - }, - ]); - - const toggleExpand = (taskId: string) => { - setExpandedTaskId(expandedTaskId === taskId ? null : taskId); - }; - - const handleDelete = (taskId: string) => { - const taskToDelete = tasks.find((task) => task.id === taskId); - if (!taskToDelete) return; - - if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return; - - setTasks(tasks.filter((task) => task.id !== taskId)); - toast({ - title: '删除成功', - description: '已成功删除推送任务', - }); - }; - - const handleEdit = (taskId: string) => { - navigate(`/workspace/group-push/${taskId}/edit`); - }; - - const handleView = (taskId: string) => { - navigate(`/workspace/group-push/${taskId}`); - }; - - const handleCopy = (taskId: string) => { - const taskToCopy = tasks.find((task) => task.id === taskId); - if (taskToCopy) { - const newTask = { - ...taskToCopy, - id: `${Date.now()}`, - name: `${taskToCopy.name} (复制)`, - createTime: new Date().toISOString().replace('T', ' ').substring(0, 19), - }; - setTasks([...tasks, newTask]); - toast({ - title: '复制成功', - description: '已成功复制推送任务', - }); - } - }; - - const toggleTaskStatus = (taskId: string) => { - const task = tasks.find((t) => t.id === taskId); - if (!task) return; - - setTasks( - tasks.map((task) => - task.id === taskId ? { ...task, status: task.status === 'running' ? 'paused' : 'running' } : task, - ), - ); - - toast({ - title: task.status === 'running' ? '已暂停' : '已启动', - description: `${task.name}任务${task.status === 'running' ? '已暂停' : '已启动'}`, - }); - }; - - const handleCreateNew = () => { - navigate('/workspace/group-push/new'); - }; - - const filteredTasks = tasks.filter((task) => - task.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - const getStatusColor = (status: string) => { - switch (status) { - case 'running': - return 'bg-green-100 text-green-800'; - case 'paused': - return 'bg-gray-100 text-gray-800'; - case 'completed': - return 'bg-blue-100 text-blue-800'; - default: - return 'bg-gray-100 text-gray-800'; - } - }; - - const getStatusText = (status: string) => { - switch (status) { - case 'running': - return '进行中'; - case 'paused': - return '已暂停'; - case 'completed': - return '已完成'; - default: - return '未知'; - } - }; - - const getMessageTypeText = (type: string) => { - switch (type) { - case 'text': - return '文字'; - case 'image': - return '图片'; - case 'video': - return '视频'; - case 'link': - return '链接'; - default: - return '未知'; - } - }; - - const getSuccessRate = (pushCount: number, successCount: number) => { - if (pushCount === 0) return 0; - return Math.round((successCount / pushCount) * 100); - }; - - return ( - - - 新建任务 - - } - /> - } - footer={} - > -
-
- {/* 搜索和筛选 */} - -
-
- - setSearchTerm(e.target.value)} - /> -
- - -
-
- - {/* 任务列表 */} -
- {filteredTasks.length === 0 ? ( - - -

暂无推送任务

-

创建您的第一个群消息推送任务

- -
- ) : ( - filteredTasks.map((task) => ( - -
-
-

{task.name}

- - {getStatusText(task.status)} - -
-
- toggleTaskStatus(task.id)} - disabled={task.status === 'completed'} - /> - - - - - - handleView(task.id)}> - - 查看 - - handleEdit(task.id)}> - - 编辑 - - handleCopy(task.id)}> - - 复制 - - handleDelete(task.id)}> - - 删除 - - - -
-
- -
-
-
执行设备:{task.deviceCount} 个
-
目标群组:{task.targetGroups.length} 个
-
-
-
推送成功:{task.successCount}/{task.pushCount}
-
创建人:{task.creator}
-
-
- - {/* 成功率进度条 */} -
-
- 推送成功率 - {getSuccessRate(task.pushCount, task.successCount)}% -
- -
- -
-
- - 上次推送:{task.lastPushTime} -
-
- 创建时间:{task.createTime} - -
-
- - {expandedTaskId === task.id && ( -
-
-
-
- -

基本设置

-
-
-
- 推送间隔: - {task.pushInterval} 秒 -
-
- 每日最大推送数: - {task.maxPushPerDay} 条 -
-
- 执行时间段: - - {task.timeRange.start} - {task.timeRange.end} - -
-
- 推送模式: - {task.pushMode === 'immediate' ? '立即推送' : '定时推送'} -
- {task.scheduledTime && ( -
- 定时时间: - {task.scheduledTime} -
- )} -
-
- -
-
- -

目标群组

-
-
-
- {task.targetGroups.map((group) => ( - - {group} - - ))} -
-
-
- -
-
- -

消息内容

-
-
-
- 消息类型: - {getMessageTypeText(task.messageType)} -
-
-
消息内容:
-
- {task.messageContent} -
-
-
-
- -
-
- -

执行进度

-
-
-
- 今日已推送: - - {task.pushCount} / {task.maxPushPerDay} - -
- - {task.targetTags.length > 0 && ( -
-
目标标签:
-
- {task.targetTags.map((tag) => ( - - {tag} - - ))} -
-
- )} -
-
-
-
- )} -
- )) - )} -
-
-
-
- ); +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Plus, + Filter, + Search, + RefreshCw, + MoreVertical, + Clock, + Edit, + Trash2, + Eye, + Copy, + ChevronDown, + ChevronUp, + Settings, + Calendar, + Users, + Send, + // CheckCircle, + // XCircle, + MessageSquare, +} from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Progress } from '@/components/ui/progress'; +import Layout from '@/components/Layout'; +import PageHeader from '@/components/PageHeader'; +import BottomNav from '@/components/BottomNav'; +import { useToast } from '@/components/ui/toast'; +import { fetchGroupPushTasks, deleteGroupPushTask, toggleGroupPushTask, copyGroupPushTask, GroupPushTask } from '@/api/groupPush'; +import '@/components/Layout.css'; + +type CardMenuProps = { + onView: () => void; + onEdit: () => void; + onCopy: () => void; + onDelete: () => void; +}; + +function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) { + const [open, setOpen] = React.useState(false); + const menuRef = React.useRef(null); + + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + if (open) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
{ onView(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 查看 +
+
{ onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 编辑 +
+
{ onCopy(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 复制 +
+
{ onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}> + 删除 +
+
+ )} +
+ ); +} + +// 移除旧的PushTask接口,使用从API导入的GroupPushTask + +export default function GroupPush() { + const navigate = useNavigate(); + const { toast } = useToast(); + const [expandedTaskId, setExpandedTaskId] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(false); + + // 获取任务列表 + const fetchTasks = async () => { + setLoading(true); + try { + const list = await fetchGroupPushTasks(); + setTasks(list); + } catch (error) { + console.error('获取群发推送任务失败:', error); + toast({ + title: '获取任务失败', + description: '请稍后重试', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + // 组件加载时获取数据 + useEffect(() => { + fetchTasks(); + }, []); + + const toggleExpand = (taskId: string) => { + setExpandedTaskId(expandedTaskId === taskId ? null : taskId); + }; + + const handleDelete = async (taskId: string) => { + const taskToDelete = tasks.find((task) => task.id === taskId); + if (!taskToDelete) return; + + if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return; + + try { + const response = await deleteGroupPushTask(taskId); + if (response.code === 200) { + setTasks(tasks.filter((task) => task.id !== taskId)); + toast({ + title: '删除成功', + description: '已成功删除推送任务', + }); + } else { + toast({ + title: '删除失败', + description: response.message || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + console.error('删除任务失败:', error); + toast({ + title: '删除失败', + description: '请稍后重试', + variant: 'destructive', + }); + } + }; + + const handleEdit = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}/edit`); + }; + + const handleView = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}`); + }; + + const handleCopy = async (taskId: string) => { + try { + const response = await copyGroupPushTask(taskId); + if (response.code === 200) { + toast({ + title: '复制成功', + description: '已成功复制推送任务', + }); + // 重新获取任务列表 + fetchTasks(); + } else { + toast({ + title: '复制失败', + description: response.message || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + console.error('复制任务失败:', error); + toast({ + title: '复制失败', + description: '请稍后重试', + variant: 'destructive', + }); + } + }; + + const toggleTaskStatus = async (taskId: string) => { + const task = tasks.find((t) => t.id === taskId); + if (!task) return; + + // 先更新本地状态 + const newStatus = task.status === 1 ? 2 : 1; + setTasks( + tasks.map((task) => + task.id === taskId ? { ...task, status: newStatus } : task, + ), + ); + + try { + const response = await toggleGroupPushTask(taskId, String(newStatus)); + if (response.code === 200) { + toast({ + title: task.status === 1 ? '已暂停' : '已启动', + description: `${task.name}任务${task.status === 1 ? '已暂停' : '已启动'}`, + }); + } else { + // 请求失败,回退本地状态 + setTasks( + tasks.map((task) => + task.id === taskId ? { ...task, status: task.status } : task, + ), + ); + toast({ + title: '操作失败', + description: response.message || '请稍后重试', + variant: 'destructive', + }); + } + } catch (error) { + // 请求异常,回退本地状态 + setTasks( + tasks.map((task) => + task.id === taskId ? { ...task, status: task.status } : task, + ), + ); + toast({ + title: '操作失败', + description: '请稍后重试', + variant: 'destructive', + }); + } + }; + + const handleCreateNew = () => { + navigate('/workspace/group-push/new'); + }; + + const filteredTasks = tasks.filter((task) => + task.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const getStatusColor = (status: number) => { + switch (status) { + case 1: + return 'bg-green-100 text-green-800'; + case 2: + return 'bg-gray-100 text-gray-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const getStatusText = (status: number) => { + switch (status) { + case 1: + return '进行中'; + case 2: + return '已暂停'; + default: + return '未知'; + } + }; + + const getMessageTypeText = (type: string) => { + switch (type) { + case 'text': + return '文字'; + case 'image': + return '图片'; + case 'video': + return '视频'; + case 'link': + return '链接'; + default: + return '未知'; + } + }; + + const getSuccessRate = (pushCount: number, successCount: number) => { + if (pushCount === 0) return 0; + return Math.round((successCount / pushCount) * 100); + }; + + return ( + + + 新建任务 + + } + /> + } + footer={} + > +
+
+ {/* 搜索和筛选 */} + +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + +
+
+ + {/* 任务列表 */} +
+ {filteredTasks.length === 0 ? ( + + +

暂无推送任务

+

创建您的第一个群消息推送任务

+ +
+ ) : ( + filteredTasks.map((task) => ( + +
+
+

{task.name}

+ + {getStatusText(task.status)} + +
+
+ toggleTaskStatus(task.id)} + disabled={false} + /> + handleView(task.id)} + onEdit={() => handleEdit(task.id)} + onCopy={() => handleCopy(task.id)} + onDelete={() => handleDelete(task.id)} + /> +
+
+ +
+
+
执行设备:{task.deviceCount} 个
+
目标群组:{task.targetGroups.length} 个
+
+
+
推送成功:{task.successCount}/{task.pushCount}
+
创建人:{task.creator}
+
+
+ + {/* 成功率进度条 */} +
+
+ 推送成功率 + {getSuccessRate(task.pushCount, task.successCount)}% +
+ +
+ +
+
+ + 上次推送:{task.lastPushTime} +
+
+ 创建时间:{task.createTime} + +
+
+ + {expandedTaskId === task.id && ( +
+
+
+
+ +

基本设置

+
+
+
+ 推送间隔: + {task.pushInterval} 秒 +
+
+ 每日最大推送数: + {task.maxPushPerDay} 条 +
+
+ 执行时间段: + + {task.timeRange.start} - {task.timeRange.end} + +
+
+ 推送模式: + {task.pushMode === 'immediate' ? '立即推送' : '定时推送'} +
+ {task.scheduledTime && ( +
+ 定时时间: + {task.scheduledTime} +
+ )} +
+
+ +
+
+ +

目标群组

+
+
+
+ {task.targetGroups.map((group) => ( + + {group} + + ))} +
+
+
+ +
+
+ +

消息内容

+
+
+
+ 消息类型: + {getMessageTypeText(task.messageType)} +
+
+
消息内容:
+
+ {task.messageContent} +
+
+
+
+ +
+
+ +

执行进度

+
+
+
+ 今日已推送: + + {task.pushCount} / {task.maxPushPerDay} + +
+ + {task.targetTags.length > 0 && ( +
+
目标标签:
+
+ {task.targetTags.map((tag) => ( + + {tag} + + ))} +
+
+ )} +
+
+
+
+ )} +
+ )) + )} +
+
+
+
+ ); } \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/components/BasicSettings.tsx b/nkebao/src/pages/workspace/group-push/components/BasicSettings.tsx new file mode 100644 index 00000000..ccf9b6c1 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/components/BasicSettings.tsx @@ -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 ( +
+ + +
+ {/* 任务名称 */} +
+ + handleChange('name', e.target.value)} + placeholder="请输入任务名称" + /> +
+ + {/* 允许推送的时间段 */} +
+ +
+ handleChange('pushTimeStart', e.target.value)} + className="w-full" + /> + + handleChange('pushTimeEnd', e.target.value)} + className="w-full" + /> +
+
+ + {/* 每日推送 */} +
+ +
+ + handleChange('dailyPushCount', Number.parseInt(e.target.value) || 1)} + className="w-20 text-center" + min={1} + disabled={loading} + /> + + 条内容 +
+
+ + {/* 推送顺序 */} +
+ +
+ + +
+
+ + {/* 是否循环推送 */} +
+ +
+ + handleChange('isLoopPush', checked)} + disabled={loading} + /> + +
+
+ + {/* 是否立即推送 */} +
+ +
+ + handleChange('isImmediatePush', checked)} + disabled={loading} + /> + +
+
+ {values.isImmediatePush && ( +
+ 如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群 +
+ )} + + {/* 是否启用 */} +
+ +
+ + handleChange('isEnabled', checked)} + disabled={loading} + /> + +
+
+
+
+
+ +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/components/ContentSelector.tsx b/nkebao/src/pages/workspace/group-push/components/ContentSelector.tsx new file mode 100644 index 00000000..d05a4467 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/components/ContentSelector.tsx @@ -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(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 ( +
+ + +
+ {/* 搜索框 */} +
+ +
+ + setSearchTerm(e.target.value)} + className="pl-9" + disabled={loading} + /> +
+
+ + {/* 全选按钮 */} +
+ 0} + onCheckedChange={handleSelectAll} + disabled={loading} + /> + +
+ + {/* 内容库列表 */} +
+ {filteredLibraries.map((library) => ( +
+ handleLibraryToggle(library, checked as boolean)} + disabled={loading} + /> +
+
+ +
+
+
{library.name}
+
+ 包含 {library.targets.length} 条内容 +
+
+
+ {library.targets.slice(0, 3).map((target) => ( + + ))} + {library.targets.length > 3 && ( +
+ +{library.targets.length - 3} +
+ )} +
+
+
+ ))} +
+ + {filteredLibraries.length === 0 && ( +
+ +

没有找到匹配的内容库

+
+ )} +
+
+
+ +
+ + + + +
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/components/GroupSelector.tsx b/nkebao/src/pages/workspace/group-push/components/GroupSelector.tsx new file mode 100644 index 00000000..dfec70e1 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/components/GroupSelector.tsx @@ -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(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 ( +
+ + +
+ {/* 搜索框 */} +
+ +
+ + setSearchTerm(e.target.value)} + className="pl-9" + disabled={loading} + /> +
+
+ + {/* 全选按钮 */} +
+ 0} + onCheckedChange={handleSelectAll} + disabled={loading} + /> + +
+ + {/* 群组列表 */} +
+ {filteredGroups.map((group) => ( +
+ handleGroupToggle(group, checked as boolean)} + disabled={loading} + /> +
+ {group.name} +
+
{group.name}
+
+ {group.serviceAccount.name} + {group.serviceAccount.name} +
+
+
+
+ ))} +
+ + {filteredGroups.length === 0 && ( +
+ +

没有找到匹配的群组

+
+ )} +
+
+
+ +
+ + + + +
+
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/components/StepIndicator.tsx b/nkebao/src/pages/workspace/group-push/components/StepIndicator.tsx new file mode 100644 index 00000000..767a6b49 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/components/StepIndicator.tsx @@ -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 ( +
+ + {steps.map((step) => ( + + ))} + +
+ ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/group-push/new.tsx b/nkebao/src/pages/workspace/group-push/new.tsx new file mode 100644 index 00000000..c20b465d --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/new.tsx @@ -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({ + name: '', + pushTimeStart: '06:00', + pushTimeEnd: '23:59', + dailyPushCount: 20, + pushOrder: 'latest', + isLoopPush: false, + isImmediatePush: false, + isEnabled: false, + groups: [], + contentLibraries: [], + }); + + const handleBasicSettingsNext = (values: Partial) => { + 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 ( + + } + > +
+ + +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={() => setCurrentStep(3)} + onSave={handleSave} + onCancel={handleCancel} + loading={loading} + /> + )} + + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={() => setCurrentStep(4)} + onSave={handleSave} + onCancel={handleCancel} + loading={loading} + /> + )} + + {currentStep === 4 && ( +
+
+ 京东联盟设置(此步骤为占位,实际功能待开发) +
+ +
+ + + +
+
+ )} +
+
+
+ ); +}