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 React, { useState, useEffect } from "react";
|
||||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
const GroupPush: React.FC = () => {
|
PlusOutlined,
|
||||||
return (
|
SearchOutlined,
|
||||||
<PlaceholderPage title="群发推送" showAddButton addButtonText="新建推送" />
|
ReloadOutlined,
|
||||||
);
|
MoreOutlined,
|
||||||
};
|
ClockCircleOutlined,
|
||||||
|
EditOutlined,
|
||||||
export default GroupPush;
|
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 AutoGroup from "@/pages/workspace/auto-group/AutoGroup";
|
||||||
import AutoGroupDetail from "@/pages/workspace/auto-group/Detail";
|
import AutoGroupDetail from "@/pages/workspace/auto-group/Detail";
|
||||||
import GroupPush from "@/pages/workspace/group-push/GroupPush";
|
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 MomentsSync from "@/pages/workspace/moments-sync/MomentsSync";
|
||||||
import MomentsSyncDetail from "@/pages/workspace/moments-sync/Detail";
|
import MomentsSyncDetail from "@/pages/workspace/moments-sync/Detail";
|
||||||
import NewMomentsSync from "@/pages/workspace/moments-sync/new";
|
import NewMomentsSync from "@/pages/workspace/moments-sync/new";
|
||||||
@@ -60,18 +61,18 @@ const workspaceRoutes = [
|
|||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/workspace/group-push/new",
|
path: "/workspace/group-push/:id",
|
||||||
element: <NewGroupPush />,
|
element: <DetailGroupPush />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/workspace/group-push/:id",
|
path: "/workspace/group-push/new",
|
||||||
element: <PlaceholderPage title="群发推送详情" />,
|
element: <FormGroupPush />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/workspace/group-push/:id/edit",
|
path: "/workspace/group-push/:id/edit",
|
||||||
element: <PlaceholderPage title="编辑群发推送" />,
|
element: <FormGroupPush />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
// 朋友圈同步
|
// 朋友圈同步
|
||||||
|
|||||||
Reference in New Issue
Block a user