feat: 本次提交更新内容如下
迁移社群推送
This commit is contained in:
73
nkebao/src/api/groupPush.ts
Normal file
73
nkebao/src/api/groupPush.ts
Normal file
@@ -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<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export async function fetchGroupPushTasks(): Promise<GroupPushTask[]> {
|
||||
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<ApiResponse> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`, {}, "DELETE");
|
||||
}
|
||||
|
||||
export async function toggleGroupPushTask(
|
||||
id: string,
|
||||
status: string
|
||||
): Promise<ApiResponse> {
|
||||
return request(
|
||||
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
||||
{ status },
|
||||
"POST"
|
||||
);
|
||||
}
|
||||
|
||||
export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}/copy`, {}, "POST");
|
||||
}
|
||||
|
||||
export async function createGroupPushTask(
|
||||
taskData: Partial<GroupPushTask>
|
||||
): Promise<ApiResponse> {
|
||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||
}
|
||||
|
||||
export async function updateGroupPushTask(
|
||||
id: string,
|
||||
taskData: Partial<GroupPushTask>
|
||||
): Promise<ApiResponse> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
||||
}
|
||||
|
||||
export async function getGroupPushTaskDetail(
|
||||
id: string
|
||||
): Promise<GroupPushTask> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`);
|
||||
}
|
||||
250
nkebao/src/pages/workspace/group-push/Edit.tsx
Normal file
250
nkebao/src/pages/workspace/group-push/Edit.tsx
Normal file
@@ -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<any>(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 (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
<ArrowLeftOutlined
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 12, cursor: "pointer" }}
|
||||
/>
|
||||
编辑群发推送
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ padding: 48, textAlign: "center" }}>
|
||||
<Spin />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
<ArrowLeftOutlined
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 12, cursor: "pointer" }}
|
||||
/>
|
||||
编辑群发推送
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ maxWidth: 600, margin: "0 auto", padding: 16 }}>
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
<div style={{ marginTop: 32 }}>
|
||||
{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={saving}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<GroupSelector
|
||||
selectedGroups={formData.groups}
|
||||
onGroupsChange={handleGroupsChange}
|
||||
onPrevious={() => setCurrentStep(1)}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
loading={saving}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<ContentSelector
|
||||
selectedLibraries={formData.contentLibraries}
|
||||
onLibrariesChange={handleLibrariesChange}
|
||||
onPrevious={() => setCurrentStep(2)}
|
||||
onNext={() => setCurrentStep(4)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
loading={saving}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<div style={{ padding: 32, textAlign: "center", color: "#888" }}>
|
||||
京东联盟设置(此步骤为占位,实际功能待开发)
|
||||
<div
|
||||
style={{
|
||||
marginTop: 24,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => setCurrentStep(3)} disabled={saving}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSave} loading={saving}>
|
||||
完成
|
||||
</Button>
|
||||
<Button onClick={handleCancel} disabled={saving}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditGroupPush;
|
||||
100
nkebao/src/pages/workspace/group-push/GroupPush.module.scss
Normal file
100
nkebao/src/pages/workspace/group-push/GroupPush.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,393 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const GroupPush: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="群发推送" showAddButton addButtonText="新建推送" />
|
||||
);
|
||||
};
|
||||
|
||||
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<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [tasks, setTasks] = useState<GroupPushTask[]>([]);
|
||||
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 (
|
||||
<Layout
|
||||
header={
|
||||
<div style={{ background: "#fff", padding: "0 16px" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", height: 48 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 18 }}>群消息推送</span>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
style={{ marginLeft: "auto" }}
|
||||
onClick={handleCreateNew}
|
||||
>
|
||||
新建任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={styles.bg}>
|
||||
<div className={styles.searchBar}>
|
||||
<Search
|
||||
placeholder="搜索任务名称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
enterButton={<SearchOutlined />}
|
||||
style={{ maxWidth: 320 }}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchTasks}
|
||||
loading={loading}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.taskList}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Card className={styles.emptyCard}>
|
||||
<SendOutlined
|
||||
style={{ fontSize: 48, color: "#ccc", marginBottom: 12 }}
|
||||
/>
|
||||
<div style={{ color: "#888", fontSize: 16, marginBottom: 8 }}>
|
||||
暂无推送任务
|
||||
</div>
|
||||
<div style={{ color: "#bbb", fontSize: 13, marginBottom: 16 }}>
|
||||
创建您的第一个群消息推送任务
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreateNew}
|
||||
>
|
||||
创建第一个任务
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
<Card key={task.id} className={styles.taskCard}>
|
||||
<div className={styles.taskHeader}>
|
||||
<div className={styles.taskTitle}>
|
||||
<span>{task.name}</span>
|
||||
<Badge
|
||||
color={getStatusColor(task.status)}
|
||||
text={getStatusText(task.status)}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.taskActions}>
|
||||
<Switch
|
||||
checked={task.status === 1}
|
||||
onChange={() => toggleTaskStatus(task.id)}
|
||||
/>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key="view"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(task.id)}
|
||||
>
|
||||
查看
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="edit"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(task.id)}
|
||||
>
|
||||
编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="copy"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(task.id)}
|
||||
>
|
||||
复制
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="delete"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(task.id)}
|
||||
danger
|
||||
>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<MoreOutlined />} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.taskInfoGrid}>
|
||||
<div>执行设备:{task.deviceCount} 个</div>
|
||||
<div>目标群组:{task.targetGroups.length} 个</div>
|
||||
<div>
|
||||
推送成功:{task.successCount}/{task.pushCount}
|
||||
</div>
|
||||
<div>创建人:{task.creator}</div>
|
||||
</div>
|
||||
<div className={styles.progressBlock}>
|
||||
<div className={styles.progressLabel}>推送成功率</div>
|
||||
<Progress
|
||||
percent={getSuccessRate(task.pushCount, task.successCount)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.taskFooter}>
|
||||
<div>
|
||||
<ClockCircleOutlined /> 上次推送:{task.lastPushTime}
|
||||
</div>
|
||||
<div>
|
||||
创建时间:{task.createTime}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={
|
||||
expandedTaskId === task.id ? (
|
||||
<UpOutlined />
|
||||
) : (
|
||||
<DownOutlined />
|
||||
)
|
||||
}
|
||||
onClick={() => toggleExpand(task.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{expandedTaskId === task.id && (
|
||||
<div className={styles.expandedPanel}>
|
||||
<div className={styles.expandedGrid}>
|
||||
<div>
|
||||
<SettingOutlined /> <b>基本设置</b>
|
||||
<div>推送间隔:{task.pushInterval} 秒</div>
|
||||
<div>每日最大推送数:{task.maxPushPerDay} 条</div>
|
||||
<div>
|
||||
执行时间段:{task.timeRange.start} -{" "}
|
||||
{task.timeRange.end}
|
||||
</div>
|
||||
<div>
|
||||
推送模式:
|
||||
{task.pushMode === "immediate"
|
||||
? "立即推送"
|
||||
: "定时推送"}
|
||||
</div>
|
||||
{task.scheduledTime && (
|
||||
<div>定时时间:{task.scheduledTime}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<TeamOutlined /> <b>目标群组</b>
|
||||
<div
|
||||
style={{ display: "flex", flexWrap: "wrap", gap: 4 }}
|
||||
>
|
||||
{task.targetGroups.map((group) => (
|
||||
<Badge
|
||||
key={group}
|
||||
color="blue"
|
||||
text={group}
|
||||
style={{ background: "#f0f5ff", marginRight: 4 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MessageOutlined /> <b>消息内容</b>
|
||||
<div>
|
||||
消息类型:{getMessageTypeText(task.messageType)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{task.messageContent}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CalendarOutlined /> <b>执行进度</b>
|
||||
<div>
|
||||
今日已推送:{task.pushCount} / {task.maxPushPerDay}
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(task.pushCount / task.maxPushPerDay) * 100
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
{task.targetTags.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div>目标标签:</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{task.targetTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
color="purple"
|
||||
text={tag}
|
||||
style={{
|
||||
background: "#f9f0ff",
|
||||
marginRight: 4,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupPush;
|
||||
|
||||
258
nkebao/src/pages/workspace/group-push/detail/index.tsx
Normal file
258
nkebao/src/pages/workspace/group-push/detail/index.tsx
Normal file
@@ -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<GroupPushTask | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
getGroupPushTaskDetail(id)
|
||||
.then((res) => {
|
||||
setTask(res.data || res); // 兼容两种返回格式
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
<ArrowLeftOutlined
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 12, cursor: "pointer" }}
|
||||
/>
|
||||
群发推送详情
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ padding: 48, textAlign: "center" }}>
|
||||
<Spin />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
if (!task) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
<ArrowLeftOutlined
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 12, cursor: "pointer" }}
|
||||
/>
|
||||
群发推送详情
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ padding: 48, textAlign: "center", color: "#888" }}>
|
||||
未找到该任务
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
<ArrowLeftOutlined
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 12, cursor: "pointer" }}
|
||||
/>
|
||||
群发推送详情
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={styles.bg}>
|
||||
<Card className={styles.taskCard}>
|
||||
<div className={styles.taskHeader}>
|
||||
<div className={styles.taskTitle}>
|
||||
<span>{task.name}</span>
|
||||
<Badge
|
||||
color={getStatusColor(task.status)}
|
||||
text={getStatusText(task.status)}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.taskInfoGrid}>
|
||||
<div>执行设备:{task.deviceCount} 个</div>
|
||||
<div>目标群组:{task.targetGroups.length} 个</div>
|
||||
<div>
|
||||
推送成功:{task.successCount}/{task.pushCount}
|
||||
</div>
|
||||
<div>创建人:{task.creator}</div>
|
||||
</div>
|
||||
<div className={styles.progressBlock}>
|
||||
<div className={styles.progressLabel}>推送成功率</div>
|
||||
<Progress
|
||||
percent={getSuccessRate(task.pushCount, task.successCount)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.taskFooter}>
|
||||
<div>
|
||||
<CalendarOutlined /> 上次推送:{task.lastPushTime}
|
||||
</div>
|
||||
<div>创建时间:{task.createTime}</div>
|
||||
</div>
|
||||
<div className={styles.expandedPanel}>
|
||||
<div className={styles.expandedGrid}>
|
||||
<div>
|
||||
<SettingOutlined /> <b>基本设置</b>
|
||||
<div>推送间隔:{task.pushInterval} 秒</div>
|
||||
<div>每日最大推送数:{task.maxPushPerDay} 条</div>
|
||||
<div>
|
||||
执行时间段:{task.timeRange.start} - {task.timeRange.end}
|
||||
</div>
|
||||
<div>
|
||||
推送模式:
|
||||
{task.pushMode === "immediate" ? "立即推送" : "定时推送"}
|
||||
</div>
|
||||
{task.scheduledTime && (
|
||||
<div>定时时间:{task.scheduledTime}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<TeamOutlined /> <b>目标群组</b>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{task.targetGroups.map((group) => (
|
||||
<Badge
|
||||
key={group}
|
||||
color="blue"
|
||||
text={group}
|
||||
style={{ background: "#f0f5ff", marginRight: 4 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MessageOutlined /> <b>消息内容</b>
|
||||
<div>消息类型:{getMessageTypeText(task.messageType)}</div>
|
||||
<div
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{task.messageContent}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CalendarOutlined /> <b>执行进度</b>
|
||||
<div>
|
||||
今日已推送:{task.pushCount} / {task.maxPushPerDay}
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(task.pushCount / task.maxPushPerDay) * 100
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
{task.targetTags.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div>目标标签:</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{task.targetTags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
color="purple"
|
||||
text={tag}
|
||||
style={{ background: "#f9f0ff", marginRight: 4 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detail;
|
||||
@@ -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<BasicSettingsProps> = ({
|
||||
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 (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Card>
|
||||
<div style={{ padding: 16 }}>
|
||||
{/* 任务名称 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ color: "red", marginRight: 4 }}>*</span>任务名称:
|
||||
<Input
|
||||
value={values.name}
|
||||
onChange={(e) => handleChange("name", e.target.value)}
|
||||
placeholder="请输入任务名称"
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
{/* 允许推送的时间段 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>允许推送的时间段:</span>
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 4 }}>
|
||||
<Input
|
||||
type="time"
|
||||
value={values.pushTimeStart}
|
||||
onChange={(e) => handleChange("pushTimeStart", e.target.value)}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<span style={{ color: "#888" }}>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={values.pushTimeEnd}
|
||||
onChange={(e) => handleChange("pushTimeEnd", e.target.value)}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 每日推送 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>每日推送:</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() => handleCountChange(false)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
value={values.dailyPushCount}
|
||||
onChange={(e) =>
|
||||
handleChange(
|
||||
"dailyPushCount",
|
||||
Number.parseInt(e.target.value) || 1
|
||||
)
|
||||
}
|
||||
style={{ width: 80, textAlign: "center" }}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleCountChange(true)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<span style={{ color: "#888" }}>条内容</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 推送顺序 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>推送顺序:</span>
|
||||
<Button.Group style={{ marginLeft: 8 }}>
|
||||
<Button
|
||||
type={values.pushOrder === "earliest" ? "primary" : "default"}
|
||||
onClick={() => handleChange("pushOrder", "earliest")}
|
||||
disabled={loading}
|
||||
>
|
||||
按最早
|
||||
</Button>
|
||||
<Button
|
||||
type={values.pushOrder === "latest" ? "primary" : "default"}
|
||||
onClick={() => handleChange("pushOrder", "latest")}
|
||||
disabled={loading}
|
||||
>
|
||||
按最新
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
{/* 是否循环推送 */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span style={{ color: "red", marginRight: 4 }}>*</span>
|
||||
是否循环推送:
|
||||
</span>
|
||||
<Switch
|
||||
checked={values.isLoopPush}
|
||||
onChange={(checked) => handleChange("isLoopPush", checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
{/* 是否立即推送 */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span style={{ color: "red", marginRight: 4 }}>*</span>
|
||||
是否立即推送:
|
||||
</span>
|
||||
<Switch
|
||||
checked={values.isImmediatePush}
|
||||
onChange={(checked) => handleChange("isImmediatePush", checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
{values.isImmediatePush && (
|
||||
<div
|
||||
style={{
|
||||
background: "#fffbe6",
|
||||
border: "1px solid #ffe58f",
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
color: "#ad8b00",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群
|
||||
</div>
|
||||
)}
|
||||
{/* 是否启用 */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span style={{ color: "red", marginRight: 4 }}>*</span>是否启用:
|
||||
</span>
|
||||
<Switch
|
||||
checked={values.isEnabled}
|
||||
onChange={(checked) => handleChange("isEnabled", checked)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
justifyContent: "flex-end",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => onNext(values)} disabled={loading}>
|
||||
下一步
|
||||
</Button>
|
||||
<Button onClick={() => onSave(values)} disabled={loading}>
|
||||
{loading ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
<Button onClick={onCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSettings;
|
||||
@@ -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<ContentSelectorProps> = ({
|
||||
selectedLibraries,
|
||||
onLibrariesChange,
|
||||
onPrevious,
|
||||
onNext,
|
||||
onSave,
|
||||
onCancel,
|
||||
loading = false,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [libraries] = 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 style={{ marginBottom: 24 }}>
|
||||
<Card>
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>搜索内容库:</span>
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索内容库名称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
disabled={loading}
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedLibraries.length === filteredLibraries.length &&
|
||||
filteredLibraries.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
disabled={loading}
|
||||
>
|
||||
全选 ({selectedLibraries.length}/{filteredLibraries.length})
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ maxHeight: 320, overflowY: "auto" }}>
|
||||
{filteredLibraries.map((library) => (
|
||||
<div
|
||||
key={library.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: 8,
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
background: isLibrarySelected(library.id)
|
||||
? "#e6f7ff"
|
||||
: "#fff",
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isLibrarySelected(library.id)}
|
||||
onChange={(e) =>
|
||||
handleLibraryToggle(library, e.target.checked)
|
||||
}
|
||||
disabled={loading}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Avatar
|
||||
icon={<FileTextOutlined />}
|
||||
size={40}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
background: "#e6f7ff",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>{library.name}</div>
|
||||
<div style={{ fontSize: 12, color: "#888", marginTop: 2 }}>
|
||||
包含 {library.targets.length} 条内容
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 2 }}>
|
||||
{library.targets.slice(0, 3).map((target) => (
|
||||
<Avatar
|
||||
key={target.id}
|
||||
src={target.avatar}
|
||||
size={24}
|
||||
style={{ border: "1px solid #fff" }}
|
||||
/>
|
||||
))}
|
||||
{library.targets.length > 3 && (
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
background: "#eee",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 12,
|
||||
color: "#888",
|
||||
border: "1px solid #fff",
|
||||
}}
|
||||
>
|
||||
+{library.targets.length - 3}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredLibraries.length === 0 && (
|
||||
<div style={{ textAlign: "center", color: "#bbb", padding: 32 }}>
|
||||
<FileTextOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
没有找到匹配的内容库
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
justifyContent: "flex-end",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<Button onClick={onPrevious} disabled={loading}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
disabled={loading || selectedLibraries.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
<Button onClick={onSave} disabled={loading}>
|
||||
{loading ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
<Button onClick={onCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentSelector;
|
||||
@@ -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<GroupSelectorProps> = ({
|
||||
selectedGroups,
|
||||
onGroupsChange,
|
||||
onPrevious,
|
||||
onNext,
|
||||
onSave,
|
||||
onCancel,
|
||||
loading = false,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [groups] = 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 style={{ marginBottom: 24 }}>
|
||||
<Card>
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span>搜索群组:</span>
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索群组名称或客服名称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
disabled={loading}
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedGroups.length === filteredGroups.length &&
|
||||
filteredGroups.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
disabled={loading}
|
||||
>
|
||||
全选 ({selectedGroups.length}/{filteredGroups.length})
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{ maxHeight: 320, overflowY: "auto" }}>
|
||||
{filteredGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: 8,
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
background: isGroupSelected(group.id) ? "#e6f7ff" : "#fff",
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isGroupSelected(group.id)}
|
||||
onChange={(e) => handleGroupToggle(group, e.target.checked)}
|
||||
disabled={loading}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Avatar
|
||||
src={group.avatar}
|
||||
size={40}
|
||||
icon={<TeamOutlined />}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>{group.name}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#888",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={group.serviceAccount.avatar}
|
||||
size={16}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
{group.serviceAccount.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredGroups.length === 0 && (
|
||||
<div style={{ textAlign: "center", color: "#bbb", padding: 32 }}>
|
||||
<TeamOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
没有找到匹配的群组
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
justifyContent: "flex-end",
|
||||
marginTop: 16,
|
||||
}}
|
||||
>
|
||||
<Button onClick={onPrevious} disabled={loading}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
disabled={loading || selectedGroups.length === 0}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
<Button onClick={onSave} disabled={loading}>
|
||||
{loading ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
<Button onClick={onCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupSelector;
|
||||
@@ -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<StepIndicatorProps> = ({
|
||||
currentStep,
|
||||
steps,
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Steps current={currentStep - 1}>
|
||||
{steps.map((step) => (
|
||||
<Steps.Step key={step.id} title={step.subtitle} />
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepIndicator;
|
||||
224
nkebao/src/pages/workspace/group-push/form/index.tsx
Normal file
224
nkebao/src/pages/workspace/group-push/form/index.tsx
Normal file
@@ -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<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()) {
|
||||
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 (
|
||||
<Layout
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "0 16px",
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
}}
|
||||
>
|
||||
新建社群推送任务
|
||||
</div>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ maxWidth: 600, margin: "0 auto", padding: 16 }}>
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
<div style={{ marginTop: 32 }}>
|
||||
{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 style={{ padding: 32, textAlign: "center", color: "#888" }}>
|
||||
京东联盟设置(此步骤为占位,实际功能待开发)
|
||||
<div
|
||||
style={{
|
||||
marginTop: 24,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => setCurrentStep(3)} disabled={loading}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSave} loading={loading}>
|
||||
完成
|
||||
</Button>
|
||||
<Button onClick={handleCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewGroupPush;
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewGroupPush: React.FC = () => {
|
||||
return <PlaceholderPage title="新建群发推送" />;
|
||||
};
|
||||
|
||||
export default NewGroupPush;
|
||||
@@ -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: <NewGroupPush />,
|
||||
path: "/workspace/group-push/:id",
|
||||
element: <DetailGroupPush />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-push/:id",
|
||||
element: <PlaceholderPage title="群发推送详情" />,
|
||||
path: "/workspace/group-push/new",
|
||||
element: <FormGroupPush />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-push/:id/edit",
|
||||
element: <PlaceholderPage title="编辑群发推送" />,
|
||||
element: <FormGroupPush />,
|
||||
auth: true,
|
||||
},
|
||||
// 朋友圈同步
|
||||
|
||||
Reference in New Issue
Block a user