Refactor CreatePushTask component to streamline account and contact selection process. Update styles for layout consistency, enhance user experience with improved step indicators, and simplify state management for selected accounts and contacts.

This commit is contained in:
超级老白兔
2025-11-10 18:08:53 +08:00
parent d940d36123
commit 5a287a42ac
7 changed files with 752 additions and 704 deletions

View File

@@ -2,7 +2,6 @@
display: flex;
height: 100vh;
flex-direction: column;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
}
.container main {

View File

@@ -0,0 +1,107 @@
"use client";
import React, { useMemo, useState } from "react";
import { Avatar, Empty, Input } from "antd";
import { CheckCircleOutlined, SearchOutlined } from "@ant-design/icons";
import styles from "../../index.module.scss";
interface StepSelectAccountProps {
customerList: any[];
selectedAccounts: any[];
onChange: (accounts: any[]) => void;
}
const StepSelectAccount: React.FC<StepSelectAccountProps> = ({
customerList,
selectedAccounts,
onChange,
}) => {
const [searchKeyword, setSearchKeyword] = useState("");
const filteredAccounts = useMemo(() => {
if (!searchKeyword.trim()) return customerList;
const keyword = searchKeyword.toLowerCase();
return customerList.filter(
account =>
(account.nickname || "").toLowerCase().includes(keyword) ||
(account.wechatId || "").toLowerCase().includes(keyword),
);
}, [customerList, searchKeyword]);
const handleAccountToggle = (account: any) => {
const isSelected = selectedAccounts.some(a => a.id === account.id);
if (isSelected) {
onChange(selectedAccounts.filter(a => a.id !== account.id));
return;
}
onChange([...selectedAccounts, account]);
};
return (
<div className={styles.step1Content}>
<div className={styles.stepHeader}>
<h3></h3>
<p></p>
</div>
<div className={styles.searchBar}>
<Input
placeholder="请输入昵称/微信号进行搜索"
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={e => setSearchKeyword(e.target.value)}
allowClear
/>
</div>
{filteredAccounts.length > 0 ? (
<div className={styles.accountCards}>
{filteredAccounts.map(account => {
const isSelected = selectedAccounts.some(s => s.id === account.id);
return (
<div
key={account.id}
className={`${styles.accountCard} ${isSelected ? styles.selected : ""}`}
onClick={() => handleAccountToggle(account)}
>
<Avatar
src={account.avatar}
size={40}
style={{ backgroundColor: "#1890ff" }}
shape="square"
>
{!account.avatar &&
(account.nickname || account.name || "").charAt(0)}
</Avatar>
<div className={styles.accountInfo}>
<div className={styles.accountName}>
{account.nickname || account.name || "未知"}
</div>
<div className={styles.accountStatus}>
<span
className={`${styles.statusDot} ${account.isOnline ? styles.online : styles.offline}`}
/>
<span className={styles.statusText}>
{account.isOnline ? "在线" : "离线"}
</span>
</div>
</div>
{isSelected && (
<CheckCircleOutlined className={styles.checkmark} />
)}
</div>
);
})}
</div>
) : (
<Empty
description={searchKeyword ? "未找到匹配的账号" : "暂无微信账号"}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
);
};
export default StepSelectAccount;

View File

@@ -0,0 +1,339 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
Avatar,
Button,
Checkbox,
Empty,
Input,
Pagination,
Spin,
message,
} from "antd";
import {
CloseOutlined,
SearchOutlined,
TeamOutlined,
UserOutlined,
} from "@ant-design/icons";
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
import styles from "../../index.module.scss";
import { ContactItem, PushType } from "../../types";
interface StepSelectContactsProps {
pushType: PushType;
selectedAccounts: any[];
selectedContacts: ContactItem[];
onChange: (contacts: ContactItem[]) => void;
}
const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
pushType,
selectedAccounts,
selectedContacts,
onChange,
}) => {
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
const [loadingContacts, setLoadingContacts] = useState(false);
const [page, setPage] = useState(1);
const [searchValue, setSearchValue] = useState("");
const [total, setTotal] = useState(0);
const pageSize = 20;
const stepTitle = useMemo(() => {
switch (pushType) {
case "friend-message":
return "好友";
case "group-message":
case "group-announcement":
return "群";
default:
return "选择";
}
}, [pushType]);
const loadContacts = useCallback(async () => {
if (selectedAccounts.length === 0) {
setContactsData([]);
setTotal(0);
return;
}
setLoadingContacts(true);
try {
const accountIds = selectedAccounts.map(a => a.id);
const allData: ContactItem[] = [];
let totalCount = 0;
for (const accountId of accountIds) {
const params: any = {
page,
limit: pageSize,
wechatAccountId: accountId,
};
if (searchValue.trim()) {
params.keyword = searchValue.trim();
}
const response =
pushType === "friend-message"
? await getContactList(params)
: await getGroupList(params);
const data =
response.data?.list || response.data || response.list || [];
const totalValue = response.data?.total || response.total || 0;
const filteredData = data.filter((item: any) => {
const itemAccountId = item.wechatAccountId || item.accountId;
return itemAccountId === accountId;
});
filteredData.forEach((item: ContactItem) => {
if (!allData.some(d => d.id === item.id)) {
allData.push(item);
}
});
totalCount += totalValue;
}
setContactsData(allData);
setTotal(totalCount > 0 ? totalCount : allData.length);
} catch (error) {
console.error("加载数据失败:", error);
message.error("加载数据失败");
setContactsData([]);
setTotal(0);
} finally {
setLoadingContacts(false);
}
}, [page, pushType, searchValue, selectedAccounts]);
useEffect(() => {
loadContacts();
}, [loadContacts]);
useEffect(() => {
if (!searchValue.trim()) {
return;
}
setPage(1);
}, [searchValue]);
useEffect(() => {
setPage(1);
if (selectedAccounts.length === 0 && selectedContacts.length > 0) {
onChange([]);
}
}, [onChange, selectedAccounts, selectedContacts.length]);
const handleSearchChange = (value: string) => {
setSearchValue(value);
if (!value.trim()) {
setPage(1);
}
};
const filteredContacts = useMemo(() => {
if (searchValue.trim()) {
return contactsData;
}
return contactsData;
}, [contactsData, searchValue]);
const handleContactToggle = (contact: ContactItem) => {
const isSelected = selectedContacts.some(c => c.id === contact.id);
if (isSelected) {
onChange(selectedContacts.filter(c => c.id !== contact.id));
return;
}
onChange([...selectedContacts, contact]);
};
const handleRemoveContact = (contactId: number) => {
onChange(selectedContacts.filter(c => c.id !== contactId));
};
const handleSelectAllContacts = () => {
if (filteredContacts.length === 0) return;
const allSelected = filteredContacts.every(contact =>
selectedContacts.some(c => c.id === contact.id),
);
if (allSelected) {
const currentIds = filteredContacts.map(c => c.id);
onChange(selectedContacts.filter(c => !currentIds.includes(c.id)));
return;
}
const toAdd = filteredContacts.filter(
contact => !selectedContacts.some(c => c.id === contact.id),
);
onChange([...selectedContacts, ...toAdd]);
};
const handlePageChange = (p: number) => {
setPage(p);
};
return (
<div className={styles.stepContent}>
<div className={styles.step2Content}>
<div className={styles.stepHeader}>
<h3>{stepTitle}</h3>
<p>{stepTitle}</p>
</div>
<div className={styles.searchContainer}>
<Input
placeholder={`筛选${stepTitle}`}
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => handleSearchChange(e.target.value)}
allowClear
/>
<Button onClick={handleSelectAllContacts}></Button>
</div>
<div className={styles.contentBody}>
<div className={styles.contactList}>
<div className={styles.listHeader}>
<span>
{stepTitle}({total})
</span>
</div>
<div className={styles.listContent}>
{loadingContacts ? (
<div className={styles.loadingContainer}>
<Spin size="large" />
<span>...</span>
</div>
) : filteredContacts.length > 0 ? (
filteredContacts.map(contact => {
const isSelected = selectedContacts.some(
c => c.id === contact.id,
);
return (
<div
key={contact.id}
className={`${styles.contactItem} ${isSelected ? styles.selected : ""}`}
onClick={() => handleContactToggle(contact)}
>
<Checkbox checked={isSelected} />
<Avatar
src={contact.avatar}
size={40}
icon={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<div className={styles.contactInfo}>
<div className={styles.contactName}>
{contact.nickname}
</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
{contact.type === "group" && (
<TeamOutlined className={styles.groupIcon} />
)}
</div>
);
})
) : (
<Empty
description={
searchValue
? `未找到匹配的${stepTitle}`
: `暂无${stepTitle}`
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
{total > 0 && (
<div className={styles.paginationContainer}>
<Pagination
size="small"
current={page}
pageSize={pageSize}
total={total}
onChange={handlePageChange}
showSizeChanger={false}
/>
</div>
)}
</div>
<div className={styles.selectedList}>
<div className={styles.listHeader}>
<span>
{stepTitle}({selectedContacts.length})
</span>
{selectedContacts.length > 0 && (
<Button type="link" size="small" onClick={() => onChange([])}>
</Button>
)}
</div>
<div className={styles.listContent}>
{selectedContacts.length > 0 ? (
selectedContacts.map(contact => (
<div key={contact.id} className={styles.selectedItem}>
<div className={styles.contactInfo}>
<Avatar
src={contact.avatar}
size={40}
icon={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<div className={styles.contactName}>
<div>{contact.nickname}</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
{contact.type === "group" && (
<TeamOutlined className={styles.groupIcon} />
)}
</div>
<CloseOutlined
className={styles.removeIcon}
onClick={e => {
e.stopPropagation();
handleRemoveContact(contact.id);
}}
/>
</div>
))
) : (
<Empty
description={`请选择${stepTitle}`}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</div>
</div>
</div>
</div>
);
};
export default StepSelectContacts;

View File

@@ -0,0 +1,184 @@
"use client";
import React, { useState } from "react";
import { Button, Input, Select, Slider, Switch } from "antd";
import styles from "../../index.module.scss";
import { ContactItem } from "../../types";
import ContentSelection from "@/components/ContentSelection";
import { ContentItem } from "@/components/ContentSelection/data";
interface StepSendMessageProps {
selectedAccounts: any[];
selectedContacts: ContactItem[];
targetLabel: string;
messageContent: string;
onMessageContentChange: (value: string) => void;
friendInterval: number;
onFriendIntervalChange: (value: number) => void;
messageInterval: number;
onMessageIntervalChange: (value: number) => void;
selectedTag: string;
onSelectedTagChange: (value: string) => void;
aiRewriteEnabled: boolean;
onAiRewriteToggle: (value: boolean) => void;
aiPrompt: string;
onAiPromptChange: (value: string) => void;
}
const StepSendMessage: React.FC<StepSendMessageProps> = ({
selectedAccounts,
selectedContacts,
targetLabel,
messageContent,
onMessageContentChange,
friendInterval,
onFriendIntervalChange,
messageInterval,
onMessageIntervalChange,
selectedTag,
onSelectedTagChange,
aiRewriteEnabled,
onAiRewriteToggle,
aiPrompt,
onAiPromptChange,
}) => {
const [selectedContentLibraries, setSelectedContentLibraries] = useState<
ContentItem[]
>([]);
return (
<div className={styles.stepContent}>
<div className={styles.step3Content}>
<div className={styles.leftColumn}>
<div className={styles.messagePreview}>
<div className={styles.previewTitle}></div>
<div className={styles.messageBubble}>
<div className={styles.currentEditingLabel}></div>
<div className={styles.messageText}>
{messageContent || "开始添加消息内容..."}
</div>
</div>
</div>
<div className={styles.savedScriptGroups}>
<div className={styles.scriptGroupTitle}></div>
<ContentSelection
selectedOptions={selectedContentLibraries}
onSelect={setSelectedContentLibraries}
placeholder="选择话术内容"
showSelectedList
selectedListMaxHeight={220}
/>
</div>
<div className={styles.messageInputArea}>
<Input.TextArea
className={styles.messageInput}
placeholder="请输入内容"
value={messageContent}
onChange={e => onMessageContentChange(e.target.value)}
rows={4}
onKeyDown={e => {
if (e.ctrlKey && e.key === "Enter") {
e.preventDefault();
onMessageContentChange(`${messageContent}\n`);
}
}}
/>
<div className={styles.attachmentButtons}>
<Button type="text" icon="😊" />
<Button type="text" icon="🖼️" />
<Button type="text" icon="📎" />
<Button type="text" icon="🔗" />
<Button type="text" icon="⭐" />
</div>
<div className={styles.aiRewriteSection}>
<Switch checked={aiRewriteEnabled} onChange={onAiRewriteToggle} />
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
{aiRewriteEnabled && (
<Input
placeholder="输入改写提示词"
value={aiPrompt}
onChange={e => onAiPromptChange(e.target.value)}
style={{ marginLeft: 12, width: 200 }}
/>
)}
<Button type="primary" style={{ marginLeft: 12 }}>
+
</Button>
</div>
<div className={styles.messageHint}>
CTRL+ENTER换行,{selectedContentLibraries.length}
,{selectedContacts.length}
</div>
</div>
</div>
<div className={styles.rightColumn}>
<div className={styles.settingsPanel}>
<div className={styles.settingsTitle}></div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={10}
max={20}
value={friendInterval}
onChange={value => onFriendIntervalChange(value as number)}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{friendInterval} - 20</span>
</div>
</div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={1}
max={12}
value={messageInterval}
onChange={value => onMessageIntervalChange(value as number)}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{messageInterval} - 12</span>
</div>
</div>
</div>
<div className={styles.tagSection}>
<div className={styles.settingLabel}></div>
<Select
value={selectedTag}
onChange={onSelectedTagChange}
placeholder="选择标签"
style={{ width: "100%" }}
>
<Select.Option value="potential"></Select.Option>
<Select.Option value="customer"></Select.Option>
<Select.Option value="partner"></Select.Option>
</Select>
</div>
<div className={styles.pushPreview}>
<div className={styles.previewTitle}></div>
<ul>
<li>: {selectedAccounts.length}</li>
<li>
{targetLabel}: {selectedContacts.length}
</li>
<li>: {selectedContentLibraries.length}</li>
<li>随机推送: </li>
<li>: ~1</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default StepSendMessage;

View File

@@ -1,7 +1,5 @@
.container {
padding: 24px;
background: #f5f5f5;
min-height: calc(100vh - 64px);
padding: 15px;
display: flex;
flex-direction: column;
}
@@ -586,6 +584,7 @@
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin: 20px;
.footerLeft {
font-size: 14px;

View File

@@ -1,29 +1,7 @@
import React, { useState, useEffect, useMemo } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Input,
Button,
Avatar,
Checkbox,
Empty,
Spin,
message,
Pagination,
Slider,
Select,
Switch,
Radio,
} from "antd";
import {
SearchOutlined,
CloseOutlined,
UserOutlined,
TeamOutlined,
CheckCircleOutlined,
SendOutlined,
CopyOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { Button, message } from "antd";
import { SendOutlined } from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import Layout from "@/components/Layout/LayoutFiexd";
import styles from "./index.module.scss";
@@ -31,33 +9,18 @@ import {
useCustomerStore,
updateCustomerList,
} from "@/store/module/weChat/customer";
import {
getContactList,
getGroupList,
getCustomerList,
} from "@/pages/pc/ckbox/weChat/api";
import { getCustomerList } from "@/pages/pc/ckbox/weChat/api";
export type PushType =
| "friend-message"
| "group-message"
| "group-announcement";
interface ContactItem {
id: number;
nickname: string;
avatar?: string;
conRemark?: string;
wechatId?: string;
gender?: number;
region?: string;
type?: "friend" | "group";
}
import StepSelectAccount from "./components/StepSelectAccount";
import StepSelectContacts from "./components/StepSelectContacts";
import StepSendMessage from "./components/StepSendMessage";
import { ContactItem, PushType } from "./types";
import StepIndicator from "@/components/StepIndicator";
const CreatePushTask: React.FC = () => {
const navigate = useNavigate();
const { pushType } = useParams<{ pushType: PushType }>();
// 验证推送类型
const validPushType: PushType =
pushType === "friend-message" ||
pushType === "group-message" ||
@@ -66,7 +29,6 @@ const CreatePushTask: React.FC = () => {
: "friend-message";
const [currentStep, setCurrentStep] = useState(1);
const [searchKeyword, setSearchKeyword] = useState("");
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
const [messageContent, setMessageContent] = useState("");
@@ -75,23 +37,9 @@ const CreatePushTask: React.FC = () => {
const [selectedTag, setSelectedTag] = useState<string>("");
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
const [aiPrompt, setAiPrompt] = useState("");
const [selectedScriptGroup, setSelectedScriptGroup] =
useState<string>("group1");
const [scriptGroups] = useState([
{ id: "group1", name: "话术组 1", messageCount: 1, content: "啊实打实" },
]);
// 步骤2数据
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
const [loadingContacts, setLoadingContacts] = useState(false);
const [step2Page, setStep2Page] = useState(1);
const [step2SearchValue, setStep2SearchValue] = useState("");
const [step2Total, setStep2Total] = useState(0);
const step2PageSize = 20;
const customerList = useCustomerStore(state => state.customerList);
// 如果 customerList 为空,重新请求客服账户列表接口
useEffect(() => {
if (customerList.length === 0) {
getCustomerList()
@@ -105,8 +53,7 @@ const CreatePushTask: React.FC = () => {
}
}, [customerList.length]);
// 获取标题和描述
const getTitle = () => {
const title = useMemo(() => {
switch (validPushType) {
case "friend-message":
return "好友消息推送";
@@ -117,14 +64,11 @@ const CreatePushTask: React.FC = () => {
default:
return "消息推送";
}
};
}, [validPushType]);
const getSubtitle = () => {
return "智能批量推送,AI智能话术改写";
};
const subtitle = "智能批量推送,AI智能话术改写";
// 步骤2的标题
const getStep2Title = () => {
const step2Title = useMemo(() => {
switch (validPushType) {
case "friend-message":
return "好友";
@@ -132,188 +76,14 @@ const CreatePushTask: React.FC = () => {
case "group-announcement":
return "群";
default:
return "选择";
return "对象";
}
};
}, [validPushType]);
// 重置状态
const handleClose = () => {
navigate("/pc/powerCenter/message-push-assistant");
};
// 步骤1过滤微信账号
const filteredAccounts = useMemo(() => {
if (!searchKeyword.trim()) return customerList;
const keyword = searchKeyword.toLowerCase();
return customerList.filter(
account =>
(account.nickname || "").toLowerCase().includes(keyword) ||
(account.wechatId || "").toLowerCase().includes(keyword),
);
}, [customerList, searchKeyword]);
// 步骤1切换账号选择
const handleAccountToggle = (account: any) => {
setSelectedAccounts(prev => {
const isSelected = prev.some(a => a.id === account.id);
if (isSelected) {
return prev.filter(a => a.id !== account.id);
}
return [...prev, account];
});
};
// 步骤1清空选择
const handleClearSelection = () => {
setSelectedAccounts([]);
};
// 步骤2加载好友/群数据
const loadStep2Data = async () => {
if (selectedAccounts.length === 0) {
setContactsData([]);
setStep2Total(0);
return;
}
setLoadingContacts(true);
try {
const accountIds = selectedAccounts.map(a => a.id);
// 如果有多个账号,分别请求每个账号的数据并合并
const allData: ContactItem[] = [];
let totalCount = 0;
// 为每个账号请求数据
for (const accountId of accountIds) {
const params: any = {
page: step2Page,
limit: step2PageSize,
wechatAccountId: accountId, // 传递微信账号ID
};
if (step2SearchValue.trim()) {
params.keyword = step2SearchValue.trim();
}
let response;
if (validPushType === "friend-message") {
// 好友消息推送:获取好友列表
response = await getContactList(params);
} else {
// 群消息推送/群公告推送:获取群列表
response = await getGroupList(params);
}
// 处理响应数据
const data =
response.data?.list || response.data || response.list || [];
const total = response.data?.total || response.total || 0;
// 过滤出属于当前账号的数据(双重保险)
const filteredData = data.filter((item: any) => {
const itemAccountId = item.wechatAccountId || item.accountId;
return itemAccountId === accountId;
});
// 合并数据去重根据id
filteredData.forEach((item: ContactItem) => {
if (!allData.some(d => d.id === item.id)) {
allData.push(item);
}
});
totalCount += total;
}
// 如果多个账号,需要重新排序和分页
// 这里简化处理:显示所有合并后的数据,但总数使用第一个账号的总数
// 实际应该根据业务需求调整
setContactsData(allData);
setStep2Total(totalCount > 0 ? totalCount : allData.length);
} catch (error) {
console.error("加载数据失败:", error);
message.error("加载数据失败");
setContactsData([]);
setStep2Total(0);
} finally {
setLoadingContacts(false);
}
};
// 步骤2当进入步骤2或分页变化时加载数据
useEffect(() => {
if (currentStep === 2 && selectedAccounts.length > 0) {
loadStep2Data();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStep, selectedAccounts, step2Page, validPushType]);
// 步骤2搜索时重置分页并重新加载数据
useEffect(() => {
if (currentStep === 2 && selectedAccounts.length > 0) {
// 搜索时重置到第一页
if (step2SearchValue.trim() && step2Page !== 1) {
setStep2Page(1);
} else if (!step2SearchValue.trim() && step2Page === 1) {
// 清空搜索时,如果已经在第一页,直接加载
loadStep2Data();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step2SearchValue]);
// 步骤2过滤联系人前端过滤如果后端已支持搜索则不需要
const filteredContacts = useMemo(() => {
// 如果后端已支持搜索,直接返回数据
if (step2SearchValue.trim()) {
// 后端已搜索,直接返回
return contactsData;
}
return contactsData;
}, [contactsData, step2SearchValue]);
// 步骤2显示的数据后端已分页直接使用
const paginatedContacts = filteredContacts;
// 步骤2切换联系人选择
const handleContactToggle = (contact: ContactItem) => {
setSelectedContacts(prev => {
const isSelected = prev.some(c => c.id === contact.id);
if (isSelected) {
return prev.filter(c => c.id !== contact.id);
}
return [...prev, contact];
});
};
// 步骤2移除已选联系人
const handleRemoveContact = (contactId: number) => {
setSelectedContacts(prev => prev.filter(c => c.id !== contactId));
};
// 步骤2全选当前页
const handleSelectAllContacts = () => {
if (paginatedContacts.length === 0) return;
const allSelected = paginatedContacts.every(contact =>
selectedContacts.some(c => c.id === contact.id),
);
if (allSelected) {
// 取消全选当前页
const currentPageIds = paginatedContacts.map(c => c.id);
setSelectedContacts(prev =>
prev.filter(c => !currentPageIds.includes(c.id)),
);
} else {
// 全选当前页
const toAdd = paginatedContacts.filter(
contact => !selectedContacts.some(c => c.id === contact.id),
);
setSelectedContacts(prev => [...prev, ...toAdd]);
}
};
// 下一步
const handleNext = () => {
if (currentStep === 1) {
if (selectedAccounts.length === 0) {
@@ -321,7 +91,10 @@ const CreatePushTask: React.FC = () => {
return;
}
setCurrentStep(2);
} else if (currentStep === 2) {
return;
}
if (currentStep === 2) {
if (selectedContacts.length === 0) {
message.warning(
`请至少选择一个${validPushType === "friend-message" ? "好友" : "群"}`,
@@ -332,14 +105,20 @@ const CreatePushTask: React.FC = () => {
}
};
// 上一步
const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// 发送
const handleClearAccounts = () => {
if (selectedAccounts.length === 0) {
message.info("暂无已选微信账号");
return;
}
setSelectedAccounts([]);
};
const handleSend = () => {
if (!messageContent.trim()) {
message.warning("请输入消息内容");
@@ -361,460 +140,42 @@ const CreatePushTask: React.FC = () => {
navigate("/pc/powerCenter/message-push-assistant");
};
// 渲染步骤1选择微信账号
const renderStep1 = () => {
return (
<div className={styles.step1Content}>
<div className={styles.stepHeader}>
<h3></h3>
<p></p>
</div>
<div className={styles.searchBar}>
<Input
placeholder="请输入昵称/微信号进行搜索"
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={e => setSearchKeyword(e.target.value)}
allowClear
/>
</div>
{/* 账号列表 */}
{filteredAccounts.length > 0 ? (
<div className={styles.accountCards}>
{filteredAccounts.map(account => {
const isSelected = selectedAccounts.some(
s => s.id === account.id,
);
return (
<div
key={account.id}
className={`${styles.accountCard} ${isSelected ? styles.selected : ""}`}
onClick={() => handleAccountToggle(account)}
>
<Avatar
src={account.avatar}
size={40}
style={{ backgroundColor: "#1890ff" }}
shape="square"
>
{!account.avatar &&
(account.nickname || account.name || "").charAt(0)}
</Avatar>
<div className={styles.accountInfo}>
<div className={styles.accountName}>
{account.nickname || account.name || "未知"}
</div>
<div className={styles.accountStatus}>
<span
className={`${styles.statusDot} ${account.isOnline ? styles.online : styles.offline}`}
/>
<span className={styles.statusText}>
{account.isOnline ? "在线" : "离线"}
</span>
</div>
</div>
{isSelected && (
<CheckCircleOutlined className={styles.checkmark} />
)}
</div>
);
})}
</div>
) : (
<Empty
description={searchKeyword ? "未找到匹配的账号" : "暂无微信账号"}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
);
};
// 渲染步骤2选择好友/群
const renderStep2 = () => (
<div className={styles.stepContent}>
<div className={styles.step2Content}>
<div className={styles.stepHeader}>
<h3>{getStep2Title()}</h3>
<p>{getStep2Title()}</p>
</div>
<div className={styles.searchContainer}>
<Input
placeholder={`筛选${getStep2Title()}`}
prefix={<SearchOutlined />}
value={step2SearchValue}
onChange={e => setStep2SearchValue(e.target.value)}
allowClear
/>
<Button onClick={handleSelectAllContacts}></Button>
</div>
<div className={styles.contentBody}>
{/* 左侧:好友/群列表 */}
<div className={styles.contactList}>
<div className={styles.listHeader}>
<span>
{getStep2Title()}({step2Total})
</span>
</div>
<div className={styles.listContent}>
{loadingContacts ? (
<div className={styles.loadingContainer}>
<Spin size="large" />
<span>...</span>
</div>
) : paginatedContacts.length > 0 ? (
paginatedContacts.map(contact => {
const isSelected = selectedContacts.some(
c => c.id === contact.id,
);
return (
<div
key={contact.id}
className={`${styles.contactItem} ${isSelected ? styles.selected : ""}`}
onClick={() => handleContactToggle(contact)}
>
<Checkbox checked={isSelected} />
<Avatar
src={contact.avatar}
size={40}
icon={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<div className={styles.contactInfo}>
<div className={styles.contactName}>
{contact.nickname}
</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
{contact.type === "group" && (
<TeamOutlined className={styles.groupIcon} />
)}
</div>
);
})
) : (
<Empty
description={
step2SearchValue
? `未找到匹配的${getStep2Title()}`
: `暂无${getStep2Title()}`
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
{step2Total > 0 && (
<div className={styles.paginationContainer}>
<Pagination
size="small"
current={step2Page}
pageSize={step2PageSize}
total={step2Total}
onChange={p => setStep2Page(p)}
showSizeChanger={false}
/>
</div>
)}
</div>
{/* 右侧:已选列表 */}
<div className={styles.selectedList}>
<div className={styles.listHeader}>
<span>
{getStep2Title()}({selectedContacts.length})
</span>
{selectedContacts.length > 0 && (
<Button
type="link"
size="small"
onClick={() => setSelectedContacts([])}
>
</Button>
)}
</div>
<div className={styles.listContent}>
{selectedContacts.length > 0 ? (
selectedContacts.map(contact => (
<div key={contact.id} className={styles.selectedItem}>
<div className={styles.contactInfo}>
<Avatar
src={contact.avatar}
size={40}
icon={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<div className={styles.contactName}>
<div>{contact.nickname}</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
{contact.type === "group" && (
<TeamOutlined className={styles.groupIcon} />
)}
</div>
<CloseOutlined
className={styles.removeIcon}
onClick={() => handleRemoveContact(contact.id)}
/>
</div>
))
) : (
<Empty
description={`请选择${getStep2Title()}`}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</div>
</div>
</div>
</div>
);
// 渲染步骤3一键群发
const renderStep3 = () => (
<div className={styles.stepContent}>
<div className={styles.step3Content}>
{/* 左侧栏:内容编辑 */}
<div className={styles.leftColumn}>
{/* 模拟推送内容 */}
<div className={styles.messagePreview}>
<div className={styles.previewTitle}></div>
<div className={styles.messageBubble}>
<div className={styles.currentEditingLabel}></div>
<div className={styles.messageText}>
{messageContent || "开始添加消息内容..."}
</div>
</div>
</div>
{/* 已保存话术组 */}
<div className={styles.savedScriptGroups}>
<div className={styles.scriptGroupTitle}>
({scriptGroups.length})
</div>
{scriptGroups.map(group => (
<div key={group.id} className={styles.scriptGroupItem}>
<div className={styles.scriptGroupHeader}>
<div className={styles.scriptGroupLeft}>
<Radio
checked={selectedScriptGroup === group.id}
onChange={() => setSelectedScriptGroup(group.id)}
/>
<span className={styles.scriptGroupName}>{group.name}</span>
<span className={styles.messageCount}>
{group.messageCount}
</span>
</div>
<div className={styles.scriptGroupActions}>
<Button
type="text"
icon={<CopyOutlined />}
size="small"
className={styles.actionButton}
/>
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
className={styles.actionButton}
/>
</div>
</div>
{selectedScriptGroup === group.id && (
<div className={styles.scriptGroupContent}>
{group.content}
</div>
)}
</div>
))}
</div>
{/* 消息输入区域 */}
<div className={styles.messageInputArea}>
<Input.TextArea
className={styles.messageInput}
placeholder="请输入内容"
value={messageContent}
onChange={e => setMessageContent(e.target.value)}
rows={4}
onKeyDown={e => {
if (e.ctrlKey && e.key === "Enter") {
e.preventDefault();
setMessageContent(prev => prev + "\n");
}
}}
/>
<div className={styles.attachmentButtons}>
<Button type="text" icon="😊" />
<Button type="text" icon="🖼️" />
<Button type="text" icon="📎" />
<Button type="text" icon="🔗" />
<Button type="text" icon="⭐" />
</div>
<div className={styles.aiRewriteSection}>
<Switch
checked={aiRewriteEnabled}
onChange={setAiRewriteEnabled}
/>
<span style={{ marginLeft: 8 }}>AI智能话术改写</span>
{aiRewriteEnabled && (
<Input
placeholder="输入改写提示词"
value={aiPrompt}
onChange={e => setAiPrompt(e.target.value)}
style={{ marginLeft: 12, width: 200 }}
/>
)}
<Button type="primary" style={{ marginLeft: 12 }}>
+
</Button>
</div>
<div className={styles.messageHint}>
CTRL+ENTER换行,{scriptGroups.length}
,0
</div>
</div>
</div>
{/* 右侧栏:设置和预览 */}
<div className={styles.rightColumn}>
{/* 相关设置 */}
<div className={styles.settingsPanel}>
<div className={styles.settingsTitle}></div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={10}
max={20}
value={friendInterval}
onChange={setFriendInterval}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{friendInterval} - 20</span>
</div>
</div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
min={1}
max={12}
value={messageInterval}
onChange={setMessageInterval}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>{messageInterval} - 12</span>
</div>
</div>
</div>
{/* 完成打标签 */}
<div className={styles.tagSection}>
<div className={styles.settingLabel}></div>
<Select
value={selectedTag}
onChange={setSelectedTag}
placeholder="选择标签"
style={{ width: "100%" }}
>
<Select.Option value="potential"></Select.Option>
<Select.Option value="customer"></Select.Option>
<Select.Option value="partner"></Select.Option>
</Select>
</div>
{/* 推送预览 */}
<div className={styles.pushPreview}>
<div className={styles.previewTitle}></div>
<ul>
<li>: {selectedAccounts.length}</li>
<li>
{getStep2Title()}: {selectedContacts.length}
</li>
<li>话术组数: 0个</li>
<li>随机推送: </li>
<li>: ~1</li>
</ul>
</div>
</div>
</div>
</div>
);
return (
<Layout
header={
<div style={{ padding: "20px" }}>
<PowerNavigation
title={getTitle()}
subtitle={getSubtitle()}
showBackButton={true}
backButtonText="返回"
onBackClick={handleClose}
<>
<div style={{ padding: "20px" }}>
<PowerNavigation
title={title}
subtitle={subtitle}
showBackButton={true}
backButtonText="返回"
onBackClick={handleClose}
/>
</div>
<StepIndicator
currentStep={currentStep}
steps={[
{
id: 1,
title: "选择微信",
subtitle: "选择微信",
},
{
id: 2,
title: `选择${step2Title}`,
subtitle: `选择${step2Title}`,
},
{
id: 3,
title: "一键群发",
subtitle: "一键群发",
},
]}
/>
</div>
</>
}
footer={null}
>
<div className={styles.container}>
{/* 步骤指示器 */}
<div className={styles.steps}>
<div
className={`${styles.step} ${currentStep >= 1 ? styles.active : ""} ${currentStep > 1 ? styles.completed : ""}`}
>
<div className={styles.stepIcon}>
{currentStep > 1 ? <CheckCircleOutlined /> : "1"}
</div>
<span></span>
</div>
<div
className={`${styles.step} ${currentStep >= 2 ? styles.active : ""} ${currentStep > 2 ? styles.completed : ""}`}
>
<div className={styles.stepIcon}>
{currentStep > 2 ? <CheckCircleOutlined /> : "2"}
</div>
<span>{getStep2Title()}</span>
</div>
<div
className={`${styles.step} ${currentStep >= 3 ? styles.active : ""}`}
>
<div className={styles.stepIcon}>3</div>
<span></span>
</div>
</div>
{/* 步骤内容 */}
<div className={styles.stepBody}>
{currentStep === 1 && renderStep1()}
{currentStep === 2 && renderStep2()}
{currentStep === 3 && renderStep3()}
</div>
{/* 底部操作栏 */}
footer={
<div className={styles.footer}>
<div className={styles.footerLeft}>
{currentStep === 1 && (
@@ -822,12 +183,12 @@ const CreatePushTask: React.FC = () => {
)}
{currentStep === 2 && (
<span>
{selectedContacts.length}{getStep2Title()}
{selectedContacts.length}{step2Title}
</span>
)}
{currentStep === 3 && (
<span>
: {selectedAccounts.length}, {getStep2Title()}:{" "}
: {selectedAccounts.length}, {step2Title}:{" "}
{selectedContacts.length}
</span>
)}
@@ -835,7 +196,12 @@ const CreatePushTask: React.FC = () => {
<div className={styles.footerRight}>
{currentStep === 1 && (
<>
<Button onClick={handleClearSelection}></Button>
<Button
onClick={handleClearAccounts}
disabled={selectedAccounts.length === 0}
>
</Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
@@ -863,6 +229,45 @@ const CreatePushTask: React.FC = () => {
)}
</div>
</div>
}
>
<div className={styles.container}>
<div className={styles.stepBody}>
{currentStep === 1 && (
<StepSelectAccount
customerList={customerList}
selectedAccounts={selectedAccounts}
onChange={setSelectedAccounts}
/>
)}
{currentStep === 2 && (
<StepSelectContacts
pushType={validPushType}
selectedAccounts={selectedAccounts}
selectedContacts={selectedContacts}
onChange={setSelectedContacts}
/>
)}
{currentStep === 3 && (
<StepSendMessage
selectedAccounts={selectedAccounts}
selectedContacts={selectedContacts}
targetLabel={step2Title}
messageContent={messageContent}
onMessageContentChange={setMessageContent}
friendInterval={friendInterval}
onFriendIntervalChange={setFriendInterval}
messageInterval={messageInterval}
onMessageIntervalChange={setMessageInterval}
selectedTag={selectedTag}
onSelectedTagChange={setSelectedTag}
aiRewriteEnabled={aiRewriteEnabled}
onAiRewriteToggle={setAiRewriteEnabled}
aiPrompt={aiPrompt}
onAiPromptChange={setAiPrompt}
/>
)}
</div>
</div>
</Layout>
);

View File

@@ -0,0 +1,15 @@
export type PushType =
| "friend-message"
| "group-message"
| "group-announcement";
export interface ContactItem {
id: number;
nickname: string;
avatar?: string;
conRemark?: string;
wechatId?: string;
gender?: number;
region?: string;
type?: "friend" | "group";
}