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:
@@ -2,7 +2,6 @@
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.container main {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
下一步 >
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user