diff --git a/nkebao/src/api/groupPush.ts b/nkebao/src/api/groupPush.ts new file mode 100644 index 00000000..00ab3620 --- /dev/null +++ b/nkebao/src/api/groupPush.ts @@ -0,0 +1,73 @@ +import request 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; +} + +interface ApiResponse { + code: number; + message: string; + data: T; +} + +export async function fetchGroupPushTasks(): Promise { + const response = await request("/v1/workbench/list", { type: 3 }, "GET"); + if (Array.isArray(response)) return response; + if (response && Array.isArray(response.data)) return response.data; + return []; +} + +export async function deleteGroupPushTask(id: string): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`, {}, "DELETE"); +} + +export async function toggleGroupPushTask( + id: string, + status: string +): Promise { + return request( + `/v1/workspace/group-push/tasks/${id}/toggle`, + { status }, + "POST" + ); +} + +export async function copyGroupPushTask(id: string): Promise { + return request(`/v1/workspace/group-push/tasks/${id}/copy`, {}, "POST"); +} + +export async function createGroupPushTask( + taskData: Partial +): Promise { + return request("/v1/workspace/group-push/tasks", taskData, "POST"); +} + +export async function updateGroupPushTask( + id: string, + taskData: Partial +): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT"); +} + +export async function getGroupPushTaskDetail( + id: string +): Promise { + return request(`/v1/workspace/group-push/tasks/${id}`); +} diff --git a/nkebao/src/pages/workspace/group-push/Edit.tsx b/nkebao/src/pages/workspace/group-push/Edit.tsx new file mode 100644 index 00000000..0625950f --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/Edit.tsx @@ -0,0 +1,250 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Button, Spin, message } from "antd"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import StepIndicator from "./form/components/StepIndicator"; +import BasicSettings from "./form/components/BasicSettings"; +import GroupSelector from "./form/components/GroupSelector"; +import ContentSelector from "./form/components/ContentSelector"; +import { + getGroupPushTaskDetail, + updateGroupPushTask, + GroupPushTask, +} from "@/api/groupPush"; + +const steps = [ + { id: 1, title: "步骤 1", subtitle: "基础设置" }, + { id: 2, title: "步骤 2", subtitle: "选择社群" }, + { id: 3, title: "步骤 3", subtitle: "选择内容库" }, + { id: 4, title: "步骤 4", subtitle: "京东联盟" }, +]; + +const EditGroupPush: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [formData, setFormData] = useState(null); + + useEffect(() => { + if (!id) return; + setLoading(true); + getGroupPushTaskDetail(id) + .then((res) => { + const task = res.data || res; + setFormData({ + name: task.name, + pushTimeStart: task.timeRange?.start || "06:00", + pushTimeEnd: task.timeRange?.end || "23:59", + dailyPushCount: task.maxPushPerDay, + pushOrder: task.pushOrder || "latest", + isLoopPush: task.isLoopPush || false, + isImmediatePush: task.pushMode === "immediate", + isEnabled: task.isEnabled || false, + groups: (task.targetGroups || []).map( + (name: string, idx: number) => ({ + id: String(idx + 1), + name, + avatar: "", + serviceAccount: { id: "", name: "", avatar: "" }, + }) + ), + contentLibraries: (task.contentLibraries || []).map( + (name: string, idx: number) => ({ + id: String(idx + 1), + name, + targets: [], + }) + ), + }); + }) + .finally(() => setLoading(false)); + }, [id]); + + const handleBasicSettingsNext = (values: any) => { + setFormData((prev: any) => ({ ...prev, ...values })); + setCurrentStep(2); + }; + const handleGroupsChange = (groups: any[]) => { + setFormData((prev: any) => ({ ...prev, groups })); + }; + const handleLibrariesChange = (contentLibraries: any[]) => { + setFormData((prev: any) => ({ ...prev, contentLibraries })); + }; + const handleSave = async () => { + if (!formData.name.trim()) { + message.error("请输入任务名称"); + return; + } + if (!formData.groups || formData.groups.length === 0) { + message.error("请选择至少一个社群"); + return; + } + if (!formData.contentLibraries || formData.contentLibraries.length === 0) { + message.error("请选择至少一个内容库"); + return; + } + setSaving(true); + try { + 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: any) => g.name), + contentLibraries: formData.contentLibraries.map((c: any) => c.name), + pushMode: formData.isImmediatePush + ? ("immediate" as const) + : ("scheduled" as const), + messageType: "text" as const, + messageContent: "", + targetTags: [], + pushInterval: 60, + }; + const response = await updateGroupPushTask(id!, apiData); + if (response.code === 200) { + message.success("保存成功"); + navigate("/workspace/group-push"); + } else { + message.error("保存失败,请稍后重试"); + } + } catch (error) { + message.error("保存失败,请稍后重试"); + } finally { + setSaving(false); + } + }; + const handleCancel = () => { + navigate("/workspace/group-push"); + }; + + if (loading || !formData) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 编辑群发推送 + + } + footer={} + > +
+ +
+
+ ); + } + + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 编辑群发推送 + + } + footer={} + > +
+ +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={() => setCurrentStep(3)} + onSave={handleSave} + onCancel={handleCancel} + loading={saving} + /> + )} + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={() => setCurrentStep(4)} + onSave={handleSave} + onCancel={handleCancel} + loading={saving} + /> + )} + {currentStep === 4 && ( +
+ 京东联盟设置(此步骤为占位,实际功能待开发) +
+ + + +
+
+ )} +
+
+
+ ); +}; + +export default EditGroupPush; diff --git a/nkebao/src/pages/workspace/group-push/GroupPush.module.scss b/nkebao/src/pages/workspace/group-push/GroupPush.module.scss new file mode 100644 index 00000000..3ea9f972 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/GroupPush.module.scss @@ -0,0 +1,100 @@ + + +.searchBar { + display: flex; + align-items: center; + gap: 8px; + padding: 16px 0 8px 0; +} + +.taskList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.emptyCard { + text-align: center; + padding: 48px 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.taskCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + padding: 20px 16px 12px 16px; +} + +.taskHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.taskTitle { + display: flex; + align-items: center; + font-size: 16px; + font-weight: 600; +} + +.taskActions { + display: flex; + align-items: center; + gap: 8px; +} + +.taskInfoGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px 16px; + font-size: 13px; + color: #666; + margin-bottom: 12px; +} + +.progressBlock { + margin-bottom: 12px; +} + +.progressLabel { + font-size: 13px; + color: #888; + margin-bottom: 4px; +} + +.taskFooter { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: #888; + border-top: 1px dashed #eee; + padding-top: 8px; + margin-top: 8px; +} + +.expandedPanel { + margin-top: 16px; + padding-top: 16px; + border-top: 1px dashed #eee; +} + +.expandedGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; +} + +@media (max-width: 600px) { + .taskCard { + padding: 12px 6px 8px 6px; + } + .expandedGrid { + grid-template-columns: 1fr; + } +} \ 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 382dab52..38e36029 100644 --- a/nkebao/src/pages/workspace/group-push/GroupPush.tsx +++ b/nkebao/src/pages/workspace/group-push/GroupPush.tsx @@ -1,10 +1,393 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const GroupPush: React.FC = () => { - return ( - - ); -}; - -export default GroupPush; +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { + PlusOutlined, + SearchOutlined, + ReloadOutlined, + MoreOutlined, + ClockCircleOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + CopyOutlined, + DownOutlined, + UpOutlined, + SettingOutlined, + CalendarOutlined, + TeamOutlined, + MessageOutlined, + SendOutlined, +} from "@ant-design/icons"; +import { + Card, + Button, + Input, + Badge, + Switch, + Progress, + Dropdown, + Menu, +} from "antd"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchGroupPushTasks, + deleteGroupPushTask, + toggleGroupPushTask, + copyGroupPushTask, + GroupPushTask, +} from "@/api/groupPush"; +import styles from "./GroupPush.module.scss"; + +const { Search } = Input; + +const GroupPush: React.FC = () => { + const navigate = useNavigate(); + 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); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTasks(); + }, []); + + const toggleExpand = (taskId: string) => { + setExpandedTaskId(expandedTaskId === taskId ? null : taskId); + }; + + const handleDelete = async (taskId: string) => { + if (!window.confirm("确定要删除该任务吗?")) return; + await deleteGroupPushTask(taskId); + fetchTasks(); + }; + + const handleEdit = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}/edit`); + }; + + const handleView = (taskId: string) => { + navigate(`/workspace/group-push/${taskId}`); + }; + + const handleCopy = async (taskId: string) => { + await copyGroupPushTask(taskId); + fetchTasks(); + }; + + const toggleTaskStatus = async (taskId: string) => { + const task = tasks.find((t) => t.id === taskId); + if (!task) return; + const newStatus = task.status === 1 ? 2 : 1; + await toggleGroupPushTask(taskId, String(newStatus)); + fetchTasks(); + }; + + 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 "green"; + case 2: + return "gray"; + default: + return "gray"; + } + }; + + 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)} + enterButton={} + style={{ maxWidth: 320 }} + /> +
+
+ {filteredTasks.length === 0 ? ( + + +
+ 暂无推送任务 +
+
+ 创建您的第一个群消息推送任务 +
+ +
+ ) : ( + filteredTasks.map((task) => ( + +
+
+ {task.name} + +
+
+ toggleTaskStatus(task.id)} + /> + + } + onClick={() => handleView(task.id)} + > + 查看 + + } + onClick={() => handleEdit(task.id)} + > + 编辑 + + } + onClick={() => handleCopy(task.id)} + > + 复制 + + } + onClick={() => handleDelete(task.id)} + danger + > + 删除 + + + } + trigger={["click"]} + > +
+
+
+
执行设备:{task.deviceCount} 个
+
目标群组:{task.targetGroups.length} 个
+
+ 推送成功:{task.successCount}/{task.pushCount} +
+
创建人:{task.creator}
+
+
+
推送成功率
+ +
+
+
+ 上次推送:{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) => ( + + ))} +
+
+
+ 消息内容 +
+ 消息类型:{getMessageTypeText(task.messageType)} +
+
+ {task.messageContent} +
+
+
+ 执行进度 +
+ 今日已推送:{task.pushCount} / {task.maxPushPerDay} +
+ + {task.targetTags.length > 0 && ( +
+
目标标签:
+
+ {task.targetTags.map((tag) => ( + + ))} +
+
+ )} +
+
+
+ )} +
+ )) + )} +
+
+
+ ); +}; + +export default GroupPush; diff --git a/nkebao/src/pages/workspace/group-push/detail/index.tsx b/nkebao/src/pages/workspace/group-push/detail/index.tsx new file mode 100644 index 00000000..be665447 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/detail/index.tsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Card, Badge, Button, Progress, Spin } from "antd"; +import { + ArrowLeftOutlined, + SettingOutlined, + TeamOutlined, + MessageOutlined, + CalendarOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { getGroupPushTaskDetail, GroupPushTask } from "@/api/groupPush"; +import styles from "./GroupPush.module.scss"; + +const Detail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [task, setTask] = useState(null); + + useEffect(() => { + if (!id) return; + setLoading(true); + getGroupPushTaskDetail(id) + .then((res) => { + setTask(res.data || res); // 兼容两种返回格式 + }) + .finally(() => setLoading(false)); + }, [id]); + + if (loading) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ +
+
+ ); + } + if (!task) { + return ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ 未找到该任务 +
+
+ ); + } + + const getStatusColor = (status: number) => { + switch (status) { + case 1: + return "green"; + case 2: + return "gray"; + default: + return "gray"; + } + }; + 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 ( + + navigate(-1)} + style={{ marginRight: 12, cursor: "pointer" }} + /> + 群发推送详情 + + } + footer={} + > +
+ +
+
+ {task.name} + +
+
+
+
执行设备:{task.deviceCount} 个
+
目标群组:{task.targetGroups.length} 个
+
+ 推送成功:{task.successCount}/{task.pushCount} +
+
创建人:{task.creator}
+
+
+
推送成功率
+ +
+
+
+ 上次推送:{task.lastPushTime} +
+
创建时间:{task.createTime}
+
+
+
+
+ 基本设置 +
推送间隔:{task.pushInterval} 秒
+
每日最大推送数:{task.maxPushPerDay} 条
+
+ 执行时间段:{task.timeRange.start} - {task.timeRange.end} +
+
+ 推送模式: + {task.pushMode === "immediate" ? "立即推送" : "定时推送"} +
+ {task.scheduledTime && ( +
定时时间:{task.scheduledTime}
+ )} +
+
+ 目标群组 +
+ {task.targetGroups.map((group) => ( + + ))} +
+
+
+ 消息内容 +
消息类型:{getMessageTypeText(task.messageType)}
+
+ {task.messageContent} +
+
+
+ 执行进度 +
+ 今日已推送:{task.pushCount} / {task.maxPushPerDay} +
+ + {task.targetTags.length > 0 && ( +
+
目标标签:
+
+ {task.targetTags.map((tag) => ( + + ))} +
+
+ )} +
+
+
+
+
+
+ ); +}; + +export default Detail; diff --git a/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx new file mode 100644 index 00000000..a7fb8610 --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/BasicSettings.tsx @@ -0,0 +1,237 @@ +import React, { useState } from "react"; +import { Input, Button, Card, Switch } from "antd"; +import { MinusOutlined, PlusOutlined } from "@ant-design/icons"; + +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; +} + +const BasicSettings: React.FC = ({ + defaultValues = { + name: "", + pushTimeStart: "06:00", + pushTimeEnd: "23:59", + dailyPushCount: 20, + pushOrder: "latest", + isLoopPush: false, + isImmediatePush: false, + isEnabled: false, + }, + onNext, + onSave, + onCancel, + loading = false, +}) => { + 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="请输入任务名称" + style={{ marginTop: 4 }} + /> +
+ {/* 允许推送的时间段 */} +
+ 允许推送的时间段: +
+ handleChange("pushTimeStart", e.target.value)} + style={{ width: 120 }} + /> + + handleChange("pushTimeEnd", e.target.value)} + style={{ width: 120 }} + /> +
+
+ {/* 每日推送 */} +
+ 每日推送: +
+
+
+ {/* 推送顺序 */} +
+ 推送顺序: + + + + +
+ {/* 是否循环推送 */} +
+ + * + 是否循环推送: + + handleChange("isLoopPush", checked)} + disabled={loading} + /> +
+ {/* 是否立即推送 */} +
+ + * + 是否立即推送: + + handleChange("isImmediatePush", checked)} + disabled={loading} + /> +
+ {values.isImmediatePush && ( +
+ 如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群 +
+ )} + {/* 是否启用 */} +
+ + *是否启用: + + handleChange("isEnabled", checked)} + disabled={loading} + /> +
+
+
+
+ + + +
+
+ ); +}; + +export default BasicSettings; diff --git a/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx new file mode 100644 index 00000000..863d5b5b --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/ContentSelector.tsx @@ -0,0 +1,247 @@ +import React, { useState } from "react"; +import { Button, Card, Input, Checkbox, Avatar } from "antd"; +import { FileTextOutlined, SearchOutlined } from "@ant-design/icons"; + +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" }, + ], + }, +]; + +const ContentSelector: React.FC = ({ + selectedLibraries, + onLibrariesChange, + onPrevious, + onNext, + onSave, + onCancel, + loading = false, +}) => { + const [searchTerm, setSearchTerm] = useState(""); + const [libraries] = 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 ( +
+ +
+
+ 搜索内容库: + } + placeholder="搜索内容库名称" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + disabled={loading} + style={{ marginTop: 4 }} + /> +
+
+ 0 + } + onChange={handleSelectAll} + disabled={loading} + > + 全选 ({selectedLibraries.length}/{filteredLibraries.length}) + +
+
+ {filteredLibraries.map((library) => ( +
+ + handleLibraryToggle(library, e.target.checked) + } + disabled={loading} + style={{ marginRight: 8 }} + /> + } + size={40} + style={{ + marginRight: 8, + background: "#e6f7ff", + color: "#1890ff", + }} + /> +
+
{library.name}
+
+ 包含 {library.targets.length} 条内容 +
+
+
+ {library.targets.slice(0, 3).map((target) => ( + + ))} + {library.targets.length > 3 && ( +
+ +{library.targets.length - 3} +
+ )} +
+
+ ))} + {filteredLibraries.length === 0 && ( +
+ + 没有找到匹配的内容库 +
+ )} +
+
+
+
+ + + + +
+
+ ); +}; + +export default ContentSelector; diff --git a/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx b/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx new file mode 100644 index 00000000..aed8b2bd --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/GroupSelector.tsx @@ -0,0 +1,245 @@ +import React, { useState } from "react"; +import { Button, Card, Input, Checkbox, Avatar } from "antd"; +import { TeamOutlined, SearchOutlined } from "@ant-design/icons"; + +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", + }, + }, +]; + +const GroupSelector: React.FC = ({ + selectedGroups, + onGroupsChange, + onPrevious, + onNext, + onSave, + onCancel, + loading = false, +}) => { + const [searchTerm, setSearchTerm] = useState(""); + const [groups] = 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 ( +
+ +
+
+ 搜索群组: + } + placeholder="搜索群组名称或客服名称" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + disabled={loading} + style={{ marginTop: 4 }} + /> +
+
+ 0 + } + onChange={handleSelectAll} + disabled={loading} + > + 全选 ({selectedGroups.length}/{filteredGroups.length}) + +
+
+ {filteredGroups.map((group) => ( +
+ handleGroupToggle(group, e.target.checked)} + disabled={loading} + style={{ marginRight: 8 }} + /> + } + style={{ marginRight: 8 }} + /> +
+
{group.name}
+
+ + {group.serviceAccount.name} +
+
+
+ ))} + {filteredGroups.length === 0 && ( +
+ + 没有找到匹配的群组 +
+ )} +
+
+
+
+ + + + +
+
+ ); +}; + +export default GroupSelector; diff --git a/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx new file mode 100644 index 00000000..dc293d2e --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/components/StepIndicator.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Steps } from "antd"; + +interface StepIndicatorProps { + currentStep: number; + steps: { id: number; title: string; subtitle: string }[]; +} + +const StepIndicator: React.FC = ({ + currentStep, + steps, +}) => { + return ( +
+ + {steps.map((step) => ( + + ))} + +
+ ); +}; + +export default StepIndicator; diff --git a/nkebao/src/pages/workspace/group-push/form/index.tsx b/nkebao/src/pages/workspace/group-push/form/index.tsx new file mode 100644 index 00000000..4728be4f --- /dev/null +++ b/nkebao/src/pages/workspace/group-push/form/index.tsx @@ -0,0 +1,224 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "antd"; +import { createGroupPushTask } from "@/api/groupPush"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +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: "京东联盟" }, +]; + +const NewGroupPush: React.FC = () => { + const navigate = useNavigate(); + 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()) { + window.alert("请输入任务名称"); + return; + } + if (formData.groups.length === 0) { + window.alert("请选择至少一个社群"); + return; + } + if (formData.contentLibraries.length === 0) { + window.alert("请选择至少一个内容库"); + return; + } + setLoading(true); + try { + 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) { + window.alert("保存成功"); + navigate("/workspace/group-push"); + } else { + window.alert("保存失败,请稍后重试"); + } + } catch (error) { + window.alert("保存失败,请稍后重试"); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + navigate("/workspace/group-push"); + }; + + return ( + + 新建社群推送任务 + + } + footer={} + > +
+ +
+ {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 && ( +
+ 京东联盟设置(此步骤为占位,实际功能待开发) +
+ + + +
+
+ )} +
+
+
+ ); +}; + +export default NewGroupPush; diff --git a/nkebao/src/pages/workspace/group-push/new.tsx b/nkebao/src/pages/workspace/group-push/new.tsx deleted file mode 100644 index 7147f70c..00000000 --- a/nkebao/src/pages/workspace/group-push/new.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const NewGroupPush: React.FC = () => { - return ; -}; - -export default NewGroupPush; diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index b7ccc7c2..d6917e04 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -5,7 +5,8 @@ import AutoLikeDetail from "@/pages/workspace/auto-like/AutoLikeDetail"; 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 FormGroupPush from "@/pages/workspace/group-push/form"; +import DetailGroupPush from "@/pages/workspace/group-push/detail"; import MomentsSync from "@/pages/workspace/moments-sync/MomentsSync"; import MomentsSyncDetail from "@/pages/workspace/moments-sync/Detail"; import NewMomentsSync from "@/pages/workspace/moments-sync/new"; @@ -60,18 +61,18 @@ const workspaceRoutes = [ auth: true, }, { - path: "/workspace/group-push/new", - element: , + path: "/workspace/group-push/:id", + element: , auth: true, }, { - path: "/workspace/group-push/:id", - element: , + path: "/workspace/group-push/new", + element: , auth: true, }, { path: "/workspace/group-push/:id/edit", - element: , + element: , auth: true, }, // 朋友圈同步