代码提交

This commit is contained in:
wong
2025-11-08 17:29:33 +08:00
parent 018a6162da
commit 674bbe1c3e
17 changed files with 5070 additions and 8 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ Store_vue/unpackage/
Store_vue/.vscode/
SuperAdmin/.specstory/
Cunkebao/dist
Touchkebao/.specstory/

View File

@@ -0,0 +1,113 @@
// KPI数据类型定义
export interface KPIData {
id: string;
value: string;
label: string;
subtitle?: string;
trend?: {
direction: "up" | "down";
text: string;
};
}
// 话术组数据类型定义
export interface DialogueGroupData {
status: string;
reachRate: number;
replyRate: number;
clickRate: number;
conversionRate: number;
avgReplyTime: string;
pushCount: number;
}
// KPI统计数据
export const kpiData: KPIData[] = [
{
id: "reach-rate",
value: "96.5%",
label: "触达率",
subtitle: "成功发送/计划发送",
trend: {
direction: "up",
text: "+2.3% 本月",
},
},
{
id: "reply-rate",
value: "42.8%",
label: "回复率",
subtitle: "收到回复/成功发送",
trend: {
direction: "up",
text: "+5.1% 本月",
},
},
{
id: "avg-reply-time",
value: "18分钟",
label: "平均回复时间",
subtitle: "从发送到回复的平均时长",
trend: {
direction: "down",
text: "-3分钟",
},
},
{
id: "link-click-rate",
value: "28.3%",
label: "链接点击率",
subtitle: "点击链接/成功发送",
trend: {
direction: "up",
text: "+1.8% 本月",
},
},
];
// 话术组对比数据
export const dialogueGroupData: DialogueGroupData[] = [
{
status: "优秀",
reachRate: 98.1,
replyRate: 48.7,
clickRate: 32.5,
conversionRate: 12.8,
avgReplyTime: "15分钟",
pushCount: 156,
},
{
status: "良好",
reachRate: 95.8,
replyRate: 38.2,
clickRate: 25.4,
conversionRate: 9.2,
avgReplyTime: "22分钟",
pushCount: 142,
},
{
status: "一般",
reachRate: 92.3,
replyRate: 28.5,
clickRate: 18.7,
conversionRate: 6.5,
avgReplyTime: "28分钟",
pushCount: 98,
},
];
// 时间范围选项
export const timeRangeOptions = [
{ label: "最近7天", value: "7days" },
{ label: "最近30天", value: "30days" },
{ label: "最近90天", value: "90days" },
{ label: "自定义", value: "custom" },
];

View File

@@ -0,0 +1,433 @@
.container {
padding: 24px;
background: #f5f5f5;
min-height: 100vh;
}
// 筛选和操作区域
.filterSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.filterLeft {
display: flex;
gap: 24px;
align-items: center;
.filterItem {
display: flex;
align-items: center;
gap: 12px;
.filterLabel {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.filterSelect {
width: 160px;
border-radius: 6px;
:global(.ant-select-selector) {
border-radius: 6px;
}
}
}
}
.filterRight {
.generateReportBtn {
height: 40px;
padding: 0 24px;
border-radius: 6px;
font-weight: 500;
background: #ff7a00;
border-color: #ff7a00;
&:hover {
background: #ff8c1a;
border-color: #ff8c1a;
}
}
}
}
// KPI统计区域
.kpiSection {
margin-bottom: 24px;
.kpiGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.kpiCard {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
.kpiContentWrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.kpiContent {
flex: 1;
.kpiValue {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 8px;
line-height: 1.2;
}
.kpiLabel {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.kpiTrend {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
.trendIconUp {
color: #52c41a;
font-size: 14px;
}
.trendIconDown {
color: #52c41a;
font-size: 14px;
}
.trendText {
font-size: 12px;
color: #52c41a;
font-weight: 500;
}
}
.kpiSubtitle {
font-size: 12px;
color: #999;
line-height: 1.4;
}
}
.kpiIcon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
flex-shrink: 0;
}
}
}
}
// 标签页导航
.tabsSection {
margin-bottom: 24px;
.tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #e8e8e8;
background: #fff;
border-radius: 8px 8px 0 0;
padding: 16px 16px 0 16px;
.tab {
padding: 12px 24px;
cursor: pointer;
border-bottom: 2px solid transparent;
color: #666;
font-size: 14px;
transition: all 0.3s;
user-select: none;
&:hover {
color: #1890ff;
background-color: #f5f5f5;
}
}
.tabActive {
color: #1890ff;
border-bottom-color: #1890ff;
background-color: #f0f8ff;
font-weight: 500;
}
}
}
// 内容区域
.contentSection {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.sectionHeader {
margin-bottom: 24px;
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.sectionSubtitle {
font-size: 14px;
color: #666;
margin: 0;
}
}
// 话术组对比内容
.comparisonContent {
.dialogueGroupList {
display: flex;
flex-direction: column;
gap: 16px;
.dialogueGroupCard {
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
&:hover {
border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.groupHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.groupTitle {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
.statusTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
}
.pushCount {
font-size: 14px;
color: #666;
}
}
.metricsList {
display: flex;
flex-direction: column;
gap: 16px;
.metricItem {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.metricLabel {
font-size: 14px;
color: #666;
min-width: 100px;
}
.metricValue {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
.metricPercent {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
min-width: 50px;
text-align: right;
}
.metricTime {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.metricProgress {
flex: 1;
max-width: 300px;
:global(.ant-progress-bg) {
background: #1890ff;
}
}
}
}
}
}
}
}
// 时段分析内容
.timeAnalysisContent {
min-height: 400px;
}
// 互动深度内容
.depthAnalysisContent {
min-height: 400px;
}
// 占位符
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
font-size: 16px;
color: #8c8c8c;
}
}
// 响应式设计
@media (max-width: 1200px) {
.kpiSection {
.kpiGrid {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 768px) {
.container {
padding: 16px;
}
.filterSection {
flex-direction: column;
gap: 16px;
align-items: stretch;
.filterLeft {
flex-direction: column;
gap: 12px;
align-items: stretch;
.filterItem {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.filterSelect {
width: 100%;
}
}
}
.filterRight {
.generateReportBtn {
width: 100%;
}
}
}
.kpiSection {
.kpiGrid {
grid-template-columns: 1fr;
}
}
.tabsSection {
.tabs {
flex-direction: column;
padding: 0;
.tab {
border-bottom: 1px solid #e8e8e8;
border-radius: 0;
&:last-child {
border-bottom: none;
}
}
}
}
.contentSection {
padding: 16px;
.comparisonContent {
.dialogueGroupList {
.dialogueGroupCard {
padding: 16px;
.groupHeader {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.metricsList {
.metricItem {
flex-direction: column;
align-items: flex-start;
gap: 8px;
.metricValue {
width: 100%;
.metricProgress {
max-width: 100%;
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,320 @@
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Select, Button, Progress, Tag } from "antd";
import {
BarChartOutlined,
EyeOutlined,
MessageOutlined,
ClockCircleOutlined,
ThunderboltOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import Layout from "@/components/Layout/LayoutFiexd";
import styles from "./index.module.scss";
import { kpiData, dialogueGroupData, timeRangeOptions } from "./index.data";
const DataStatistics: React.FC = () => {
const navigate = useNavigate();
const [timeRange, setTimeRange] = useState("7days");
const [dialogueGroup, setDialogueGroup] = useState("all");
const [activeTab, setActiveTab] = useState("comparison");
// 获取KPI图标和背景色
const getKpiConfig = (id: string) => {
switch (id) {
case "reach-rate":
return {
icon: <EyeOutlined style={{ fontSize: 20 }} />,
bgColor: "#1890ff",
};
case "reply-rate":
return {
icon: <MessageOutlined style={{ fontSize: 20 }} />,
bgColor: "#52c41a",
};
case "avg-reply-time":
return {
icon: <ClockCircleOutlined style={{ fontSize: 20 }} />,
bgColor: "#722ed1",
};
case "link-click-rate":
return {
icon: <ThunderboltOutlined style={{ fontSize: 20 }} />,
bgColor: "#ff7a00",
};
default:
return {
icon: null,
bgColor: "#1890ff",
};
}
};
// 获取状态标签颜色
const getStatusColor = (status: string) => {
switch (status) {
case "优秀":
return "green";
case "良好":
return "blue";
case "一般":
return "orange";
default:
return "default";
}
};
// 处理生成报告
const handleGenerateReport = () => {
console.log("生成报告", { timeRange, dialogueGroup });
// TODO: 实现生成报告功能
};
return (
<Layout
header={
<div style={{ padding: "20px" }}>
<PowerNavigation
title="数据统计"
subtitle="推送效果分析与话术优化建议"
showBackButton={true}
backButtonText="返回"
onBackClick={() => navigate("/pc/powerCenter/message-push-assistant")}
/>
</div>
}
footer={null}
>
<div className={styles.container}>
{/* 筛选和操作区域 */}
<div className={styles.filterSection}>
<div className={styles.filterLeft}>
<div className={styles.filterItem}>
<span className={styles.filterLabel}></span>
<Select
value={timeRange}
onChange={setTimeRange}
className={styles.filterSelect}
options={timeRangeOptions}
/>
</div>
<div className={styles.filterItem}>
<span className={styles.filterLabel}></span>
<Select
value={dialogueGroup}
onChange={setDialogueGroup}
className={styles.filterSelect}
options={[
{ label: "全部话术组", value: "all" },
{ label: "话术组 1", value: "group1" },
{ label: "话术组 2", value: "group2" },
{ label: "话术组 3", value: "group3" },
]}
/>
</div>
</div>
<div className={styles.filterRight}>
<Button
type="primary"
icon={<BarChartOutlined />}
onClick={handleGenerateReport}
className={styles.generateReportBtn}
>
</Button>
</div>
</div>
{/* KPI统计卡片区域 */}
<div className={styles.kpiSection}>
<div className={styles.kpiGrid}>
{kpiData.map(kpi => {
const kpiConfig = getKpiConfig(kpi.id);
return (
<div key={kpi.id} className={styles.kpiCard}>
<div className={styles.kpiContentWrapper}>
<div className={styles.kpiContent}>
<div className={styles.kpiValue}>{kpi.value}</div>
<div className={styles.kpiLabel}>{kpi.label}</div>
{kpi.trend && (
<div className={styles.kpiTrend}>
{kpi.trend.direction === "up" ? (
<ArrowUpOutlined className={styles.trendIconUp} />
) : (
<ArrowDownOutlined className={styles.trendIconDown} />
)}
<span className={styles.trendText}>{kpi.trend.text}</span>
</div>
)}
{kpi.subtitle && (
<div className={styles.kpiSubtitle}>{kpi.subtitle}</div>
)}
</div>
<div
className={styles.kpiIcon}
style={{
backgroundColor: kpiConfig.bgColor,
color: "#fff",
}}
>
{kpiConfig.icon}
</div>
</div>
</div>
);
})}
</div>
</div>
{/* 标签页导航 */}
<div className={styles.tabsSection}>
<div className={styles.tabs}>
<div
className={`${styles.tab} ${
activeTab === "comparison" ? styles.tabActive : ""
}`}
onClick={() => setActiveTab("comparison")}
>
</div>
<div
className={`${styles.tab} ${
activeTab === "time" ? styles.tabActive : ""
}`}
onClick={() => setActiveTab("time")}
>
</div>
<div
className={`${styles.tab} ${
activeTab === "depth" ? styles.tabActive : ""
}`}
onClick={() => setActiveTab("depth")}
>
</div>
</div>
</div>
{/* 内容区域 */}
<div className={styles.contentSection}>
{activeTab === "comparison" && (
<div className={styles.comparisonContent}>
<div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}></h3>
<p className={styles.sectionSubtitle}>
,
</p>
</div>
<div className={styles.dialogueGroupList}>
{dialogueGroupData.map((group, index) => (
<div key={index} className={styles.dialogueGroupCard}>
<div className={styles.groupHeader}>
<div className={styles.groupTitle}>
<span> {index + 1}</span>
<Tag color={getStatusColor(group.status)} className={styles.statusTag}>
{group.status}
</Tag>
</div>
<div className={styles.pushCount}>
{group.pushCount}
</div>
</div>
<div className={styles.metricsList}>
<div className={styles.metricItem}>
<span className={styles.metricLabel}></span>
<div className={styles.metricValue}>
<span className={styles.metricPercent}>{group.reachRate}%</span>
<Progress
percent={group.reachRate}
showInfo={false}
strokeColor="#1890ff"
className={styles.metricProgress}
/>
</div>
</div>
<div className={styles.metricItem}>
<span className={styles.metricLabel}></span>
<div className={styles.metricValue}>
<span className={styles.metricPercent}>{group.replyRate}%</span>
<Progress
percent={group.replyRate}
showInfo={false}
strokeColor="#1890ff"
className={styles.metricProgress}
/>
</div>
</div>
<div className={styles.metricItem}>
<span className={styles.metricLabel}></span>
<div className={styles.metricValue}>
<span className={styles.metricPercent}>{group.clickRate}%</span>
<Progress
percent={group.clickRate}
showInfo={false}
strokeColor="#1890ff"
className={styles.metricProgress}
/>
</div>
</div>
<div className={styles.metricItem}>
<span className={styles.metricLabel}></span>
<div className={styles.metricValue}>
<span className={styles.metricPercent}>{group.conversionRate}%</span>
<Progress
percent={group.conversionRate}
showInfo={false}
strokeColor="#1890ff"
className={styles.metricProgress}
/>
</div>
</div>
<div className={styles.metricItem}>
<span className={styles.metricLabel}></span>
<div className={styles.metricValue}>
<span className={styles.metricTime}>{group.avgReplyTime}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === "time" && (
<div className={styles.timeAnalysisContent}>
<div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}></h3>
<p className={styles.sectionSubtitle}>
</p>
</div>
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
)}
{activeTab === "depth" && (
<div className={styles.depthAnalysisContent}>
<div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}></h3>
<p className={styles.sectionSubtitle}>
</p>
</div>
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
)}
</div>
</div>
</Layout>
);
};
export default DataStatistics;

View File

@@ -1,4 +1,4 @@
import { TeamOutlined, CommentOutlined, BookOutlined } from "@ant-design/icons";
import { TeamOutlined, CommentOutlined, BookOutlined, SendOutlined } from "@ant-design/icons";
// 数据类型定义
export interface FeatureCard {
@@ -22,7 +22,9 @@ export interface KPIData {
};
}
// 功能数据 - 匹配图片中的三个核心模块
// 功能数据 - 匹配图片中的布局
// 第一行客户好友管理、AI接待设置、AI内容库配置
// 第二行:消息推送助手(单独一行,左边对齐)
export const featureCategories: FeatureCard[] = [
{
id: "customer-management",
@@ -69,6 +71,21 @@ export const featureCategories: FeatureCard[] = [
],
path: "/pc/powerCenter/content-library",
},
{
id: "message-push-assistant",
title: "消息推送助手",
description: "批量推送消息,AI智能话术改写,支持好友、群聊、公告推送",
icon: <SendOutlined style={{ fontSize: "32px", color: "#ff7a00" }} />,
color: "#ff7a00",
tag: "消息推送",
features: [
"微信好友消息推送",
"微信群消息推送",
"群公告消息推送",
"AI智能话术改写",
],
path: "/pc/powerCenter/message-push-assistant",
},
];
// KPI统计数据

View File

@@ -75,7 +75,6 @@
display: flex;
align-items: center;
justify-content: center;
background: rgba(24, 144, 255, 0.1);
}
.cardTag {
@@ -127,7 +126,7 @@
&::before {
content: "";
color: #1890ff;
color: var(--dot-color, #1890ff);
font-weight: bold;
position: absolute;
left: 0;

View File

@@ -17,6 +17,19 @@ const PowerCenter: React.FC = () => {
return "#722ed1";
};
// 将十六进制颜色转换为带透明度的rgba
const getIconBgColor = (color: string) => {
// 如果是十六进制颜色转换为rgba
if (color.startsWith("#")) {
const hex = color.slice(1);
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.1)`;
}
return color;
};
const handleCardClick = (card: FeatureCard) => {
if (card.path) {
navigate(card.path);
@@ -112,8 +125,9 @@ const PowerCenter: React.FC = () => {
{/* 核心功能模块 */}
<div className={styles.coreFeatures}>
<Row gutter={24}>
{featureCategories.map(card => (
{/* 第一行3个功能卡片 */}
<Row gutter={24} style={{ marginBottom: 24 }}>
{featureCategories.slice(0, 3).map(card => (
<Col span={8} key={card.id}>
<div
className={styles.featureCard}
@@ -121,7 +135,12 @@ const PowerCenter: React.FC = () => {
>
<div className={styles.cardContent}>
<div className={styles.cardHeader}>
<div className={styles.cardIcon}>{card.icon}</div>
<div
className={styles.cardIcon}
style={{ backgroundColor: getIconBgColor(card.color) }}
>
{card.icon}
</div>
<div
className={styles.cardTag}
style={{ backgroundColor: card.color }}
@@ -136,7 +155,14 @@ const PowerCenter: React.FC = () => {
<ul className={styles.featureList}>
{card.features.map((feature, index) => (
<li key={index}>{feature}</li>
<li
key={index}
style={{
"--dot-color": card.color,
} as React.CSSProperties}
>
{feature}
</li>
))}
</ul>
</div>
@@ -145,6 +171,61 @@ const PowerCenter: React.FC = () => {
</Col>
))}
</Row>
{/* 第二行:消息推送助手(单独一行,左边对齐) */}
{featureCategories.length > 3 && (
<Row gutter={24}>
<Col span={8}>
<div
className={styles.featureCard}
onClick={() => handleCardClick(featureCategories[3])}
>
<div className={styles.cardContent}>
<div className={styles.cardHeader}>
<div
className={styles.cardIcon}
style={{
backgroundColor: getIconBgColor(
featureCategories[3].color
),
}}
>
{featureCategories[3].icon}
</div>
<div
className={styles.cardTag}
style={{ backgroundColor: featureCategories[3].color }}
>
{featureCategories[3].tag}
</div>
</div>
<div className={styles.cardInfo}>
<h3 className={styles.cardTitle}>
{featureCategories[3].title}
</h3>
<p className={styles.cardDescription}>
{featureCategories[3].description}
</p>
<ul className={styles.featureList}>
{featureCategories[3].features.map((feature, index) => (
<li
key={index}
style={{
"--dot-color": featureCategories[3].color,
} as React.CSSProperties}
>
{feature}
</li>
))}
</ul>
</div>
</div>
</div>
</Col>
</Row>
)}
</div>
{/* 页面底部 */}
<div className={styles.footer}>

View File

@@ -0,0 +1,521 @@
.pushTaskModal {
.ant-modal-content {
padding: 0;
border-radius: 12px;
overflow: hidden;
}
.ant-modal-body {
padding: 0;
}
}
.modalHeader {
display: flex;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e8e8e8;
background: #fff;
.backButton {
margin-right: 16px;
color: #666;
padding: 0;
height: auto;
&:hover {
color: #1890ff;
}
}
.headerTitle {
flex: 1;
h2 {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 4px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
}
.steps {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
&:not(:last-child)::after {
content: "";
position: absolute;
top: 20px;
left: 60%;
right: -40%;
height: 2px;
background: #d9d9d9;
z-index: 0;
}
&.active:not(:last-child)::after,
&.completed:not(:last-child)::after {
background: #52c41a;
}
.stepIcon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #d9d9d9;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
position: relative;
z-index: 1;
margin-bottom: 8px;
}
&.active .stepIcon {
background: #52c41a;
}
&.completed .stepIcon {
background: #52c41a;
color: #fff;
}
span {
font-size: 14px;
color: #666;
}
&.active span {
color: #52c41a;
font-weight: 500;
}
}
}
.stepBody {
min-height: 500px;
max-height: 600px;
overflow-y: auto;
padding: 24px;
}
.stepContent {
.stepHeader {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
}
.searchBar {
margin-bottom: 20px;
.ant-input {
height: 40px;
}
}
.accountSelection {
.selectionControls {
margin-bottom: 16px;
}
.accountCards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
max-height: 400px;
overflow-y: auto;
padding: 8px;
.accountCard {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 2px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
background: #fff;
&:hover {
border-color: #1890ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.selected {
border-color: #52c41a;
background: #f6ffed;
}
.cardName {
margin-top: 8px;
font-size: 14px;
color: #1a1a1a;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.onlineStatus {
margin-top: 4px;
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
&.online {
background: #f6ffed;
color: #52c41a;
}
&.offline {
background: #f5f5f5;
color: #999;
}
}
.checkmark {
position: absolute;
top: 8px;
right: 8px;
color: #52c41a;
font-size: 20px;
}
}
}
}
.step2Content {
.searchContainer {
display: flex;
gap: 12px;
margin-bottom: 16px;
.ant-input {
flex: 1;
}
}
.contentBody {
display: flex;
gap: 16px;
min-height: 400px;
}
.contactList,
.selectedList {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.listHeader {
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #d9d9d9;
font-weight: 500;
font-size: 14px;
color: #262626;
display: flex;
justify-content: space-between;
align-items: center;
}
.listContent {
flex: 1;
overflow-y: auto;
padding: 8px;
min-height: 300px;
}
.contactItem {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
cursor: pointer;
gap: 12px;
&:hover {
background-color: #f5f5f5;
}
&.selected {
background-color: #e6f7ff;
}
.contactInfo {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.contactName {
font-size: 14px;
color: #262626;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conRemark {
font-size: 12px;
color: #8c8c8c;
}
.groupIcon {
color: #1890ff;
font-size: 12px;
margin-left: 4px;
}
}
.selectedItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
.contactInfo {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.removeIcon {
color: #8c8c8c;
cursor: pointer;
padding: 4px;
border-radius: 2px;
transition: all 0.2s;
&:hover {
color: #ff4d4f;
background-color: #fff2f0;
}
}
}
.paginationContainer {
padding: 12px;
border-top: 1px solid #d9d9d9;
display: flex;
justify-content: center;
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
flex-direction: column;
gap: 12px;
color: #8c8c8c;
}
}
.step3Content {
display: flex;
flex-direction: column;
gap: 24px;
.messagePreview {
border: 2px dashed #52c41a;
border-radius: 8px;
padding: 20px;
background: #f6ffed;
.previewTitle {
font-size: 14px;
color: #52c41a;
font-weight: 500;
margin-bottom: 12px;
}
.messageBubble {
min-height: 60px;
padding: 12px;
background: #fff;
border-radius: 6px;
color: #666;
font-size: 14px;
line-height: 1.6;
}
}
.messageInputArea {
.messageInput {
margin-bottom: 12px;
}
.attachmentButtons {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.aiRewriteSection {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.messageHint {
font-size: 12px;
color: #999;
}
}
.settingsPanel {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.settingItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
.settingControl {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 14px;
color: #666;
min-width: 80px;
}
}
}
}
.pushPreview {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.previewTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: 14px;
color: #666;
line-height: 1.8;
}
}
}
}
.modalFooter {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-top: 1px solid #e8e8e8;
background: #fff;
.footerLeft {
font-size: 14px;
color: #666;
}
.footerRight {
display: flex;
gap: 12px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.pushTaskModal {
.ant-modal-content {
width: 90vw !important;
}
}
.step2Content {
.contentBody {
flex-direction: column;
}
.contactList,
.selectedList {
min-height: 200px;
}
}
}

View File

@@ -0,0 +1,767 @@
import React, { useState, useEffect, useMemo } from "react";
import {
Modal,
Input,
Button,
Avatar,
Checkbox,
Empty,
Spin,
message,
Pagination,
Slider,
Select,
Switch,
} from "antd";
import {
SearchOutlined,
CloseOutlined,
UserOutlined,
TeamOutlined,
ArrowLeftOutlined,
CheckCircleOutlined,
SendOutlined,
} from "@ant-design/icons";
import styles from "./PushTaskModal.module.scss";
import {
useCustomerStore,
} from "@/store/module/weChat/customer";
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
export type PushType = "friend-message" | "group-message" | "group-announcement";
interface PushTaskModalProps {
visible: boolean;
pushType: PushType;
onCancel: () => void;
onConfirm?: () => void;
}
interface WeChatAccount {
id: number;
name: string;
avatar?: string;
isOnline?: boolean;
wechatId?: string;
}
interface ContactItem {
id: number;
nickname: string;
avatar?: string;
conRemark?: string;
wechatId?: string;
gender?: number;
region?: string;
type?: "friend" | "group";
}
const PushTaskModal: React.FC<PushTaskModalProps> = ({
visible,
pushType,
onCancel,
onConfirm,
}) => {
const [currentStep, setCurrentStep] = useState(1);
const [searchKeyword, setSearchKeyword] = useState("");
const [selectedAccounts, setSelectedAccounts] = useState<any[]>([]);
const [selectedContacts, setSelectedContacts] = useState<ContactItem[]>([]);
const [messageContent, setMessageContent] = useState("");
const [friendInterval, setFriendInterval] = useState(10);
const [messageInterval, setMessageInterval] = useState(1);
const [selectedTag, setSelectedTag] = useState<string>("");
const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false);
const [aiPrompt, setAiPrompt] = useState("");
// 步骤2数据
const [contactsData, setContactsData] = useState<ContactItem[]>([]);
const [loadingContacts, setLoadingContacts] = useState(false);
const [step2Page, setStep2Page] = useState(1);
const [step2SearchValue, setStep2SearchValue] = useState("");
const step2PageSize = 20;
const customerList = useCustomerStore(state => state.customerList);
// 获取标题和描述
const getTitle = () => {
switch (pushType) {
case "friend-message":
return "好友消息推送";
case "group-message":
return "群消息推送";
case "group-announcement":
return "群公告推送";
default:
return "消息推送";
}
};
const getSubtitle = () => {
return "智能批量推送,AI智能话术改写";
};
// 步骤2的标题
const getStep2Title = () => {
switch (pushType) {
case "friend-message":
return "选择好友";
case "group-message":
case "group-announcement":
return "选择群";
default:
return "选择";
}
};
// 重置状态
const handleClose = () => {
setCurrentStep(1);
setSearchKeyword("");
setSelectedAccounts([]);
setSelectedContacts([]);
setMessageContent("");
setFriendInterval(10);
setMessageInterval(1);
setSelectedTag("");
setAiRewriteEnabled(false);
setAiPrompt("");
setStep2Page(1);
setStep2SearchValue("");
setContactsData([]);
onCancel();
};
// 步骤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 handleSelectAll = () => {
if (selectedAccounts.length === filteredAccounts.length) {
setSelectedAccounts([]);
} else {
setSelectedAccounts([...filteredAccounts]);
}
};
// 步骤2加载好友/群数据
const loadStep2Data = async () => {
if (selectedAccounts.length === 0) return;
setLoadingContacts(true);
try {
const accountIds = selectedAccounts.map(a => a.id);
const params: any = {
page: step2Page,
limit: step2PageSize,
};
if (step2SearchValue.trim()) {
params.keyword = step2SearchValue.trim();
}
let response;
if (pushType === "friend-message") {
// 好友消息推送:获取好友列表
response = await getContactList(params);
} else {
// 群消息推送/群公告推送:获取群列表
response = await getGroupList(params);
}
const data = response.data || response.list || [];
setContactsData(data);
} catch (error) {
console.error("加载数据失败:", error);
message.error("加载数据失败");
} finally {
setLoadingContacts(false);
}
};
// 步骤2当进入步骤2时加载数据
useEffect(() => {
if (currentStep === 2 && selectedAccounts.length > 0) {
loadStep2Data();
}
}, [currentStep, selectedAccounts, step2Page, step2SearchValue, pushType]);
// 步骤2过滤联系人
const filteredContacts = useMemo(() => {
if (!step2SearchValue.trim()) return contactsData;
const keyword = step2SearchValue.toLowerCase();
return contactsData.filter(
contact =>
contact.nickname?.toLowerCase().includes(keyword) ||
contact.conRemark?.toLowerCase().includes(keyword) ||
contact.wechatId?.toLowerCase().includes(keyword),
);
}, [contactsData, step2SearchValue]);
// 步骤2分页显示
const paginatedContacts = useMemo(() => {
const start = (step2Page - 1) * step2PageSize;
const end = start + step2PageSize;
return filteredContacts.slice(start, end);
}, [filteredContacts, step2Page]);
// 步骤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) {
message.warning("请至少选择一个微信账号");
return;
}
setCurrentStep(2);
} else if (currentStep === 2) {
if (selectedContacts.length === 0) {
message.warning(`请至少选择一个${pushType === "friend-message" ? "好友" : "群"}`);
return;
}
setCurrentStep(3);
}
};
// 上一步
const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// 发送
const handleSend = () => {
if (!messageContent.trim()) {
message.warning("请输入消息内容");
return;
}
// TODO: 实现发送逻辑
console.log("发送推送", {
pushType,
accounts: selectedAccounts,
contacts: selectedContacts,
messageContent,
friendInterval,
messageInterval,
selectedTag,
aiRewriteEnabled,
aiPrompt,
});
message.success("推送任务已创建");
handleClose();
if (onConfirm) onConfirm();
};
// 渲染步骤1选择微信账号
const renderStep1 = () => (
<div className={styles.stepContent}>
<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>
<div className={styles.accountSelection}>
<div className={styles.selectionControls}>
<Checkbox
checked={
filteredAccounts.length > 0 &&
selectedAccounts.length === filteredAccounts.length
}
indeterminate={
selectedAccounts.length > 0 &&
selectedAccounts.length < filteredAccounts.length
}
onChange={handleSelectAll}
disabled={filteredAccounts.length === 0}
>
</Checkbox>
</div>
<div className={styles.accountCards}>
{filteredAccounts.length > 0 ? (
filteredAccounts.map(account => {
const isSelected = selectedAccounts.some(a => a.id === account.id);
return (
<div
key={account.id}
className={`${styles.accountCard} ${isSelected ? styles.selected : ""}`}
onClick={() => handleAccountToggle(account)}
>
<Avatar
src={account.avatar}
size={48}
style={{ backgroundColor: "#1890ff" }}
>
{!account.avatar && (account.nickname || account.name || "").charAt(0)}
</Avatar>
<div className={styles.cardName}>
{account.nickname || account.name || "未知"}
</div>
<div
className={`${styles.onlineStatus} ${account.isOnline ? styles.online : styles.offline}`}
>
{account.isOnline ? "在线" : "离线"}
</div>
{isSelected && (
<CheckCircleOutlined className={styles.checkmark} />
)}
</div>
);
})
) : (
<Empty
description={searchKeyword ? "未找到匹配的账号" : "暂无微信账号"}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</div>
</div>
);
// 渲染步骤2选择好友/群
const renderStep2 = () => (
<div className={styles.stepContent}>
<div className={styles.step2Content}>
<div className={styles.searchContainer}>
<Input
placeholder="筛选好友"
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()}({filteredContacts.length})
</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>
{filteredContacts.length > 0 && (
<div className={styles.paginationContainer}>
<Pagination
size="small"
current={step2Page}
pageSize={step2PageSize}
total={filteredContacts.length}
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.messagePreview}>
<div className={styles.previewTitle}></div>
<div className={styles.messageBubble}>
{messageContent || "开始添加消息内容..."}
</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换行,1,0
</div>
</div>
<div className={styles.settingsPanel}>
<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 className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<Select
value={selectedTag}
onChange={setSelectedTag}
placeholder="选择标签"
style={{ width: 200 }}
>
<Select.Option value="potential"></Select.Option>
<Select.Option value="customer"></Select.Option>
<Select.Option value="partner"></Select.Option>
</Select>
</div>
</div>
</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>
);
return (
<Modal
open={visible}
onCancel={handleClose}
width={1200}
className={styles.pushTaskModal}
footer={null}
closable={false}
>
<div className={styles.modalHeader}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={handleClose}
className={styles.backButton}
>
</Button>
<div className={styles.headerTitle}>
<h2>{getTitle()}</h2>
<p>{getSubtitle()}</p>
</div>
</div>
{/* 步骤指示器 */}
<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>
{/* 底部操作栏 */}
<div className={styles.modalFooter}>
<div className={styles.footerLeft}>
{currentStep === 1 && (
<span>{selectedAccounts.length}</span>
)}
{currentStep === 2 && (
<span>
{selectedContacts.length}{getStep2Title()}
</span>
)}
{currentStep === 3 && (
<span>
: {selectedAccounts.length}, {getStep2Title()}:{" "}
{selectedContacts.length}
</span>
)}
</div>
<div className={styles.footerRight}>
{currentStep === 1 && (
<>
<Button onClick={handleSelectAll}></Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
</>
)}
{currentStep === 2 && (
<>
<Button onClick={handlePrev}></Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
</>
)}
{currentStep === 3 && (
<>
<Button onClick={handlePrev}></Button>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
>
</Button>
</>
)}
</div>
</div>
</Modal>
);
};
export default PushTaskModal;

View File

@@ -0,0 +1,819 @@
.container {
padding: 24px;
background: #f5f5f5;
min-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
}
.steps {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #fff;
border-radius: 12px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
&:not(:last-child)::after {
content: "";
position: absolute;
top: 20px;
left: 60%;
right: -40%;
height: 2px;
background: #d9d9d9;
z-index: 0;
}
&.active:not(:last-child)::after,
&.completed:not(:last-child)::after {
background: #52c41a;
}
.stepIcon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #d9d9d9;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
position: relative;
z-index: 1;
margin-bottom: 8px;
}
&.active .stepIcon {
background: #52c41a;
}
&.completed .stepIcon {
background: #52c41a;
color: #fff;
}
span {
font-size: 14px;
color: #666;
}
&.active span {
color: #52c41a;
font-weight: 500;
}
}
}
.stepBody {
flex: 1;
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
overflow-y: auto;
min-height: 500px;
}
.stepContent {
.stepHeader {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
}
.searchBar {
margin-bottom: 20px;
:global(.ant-input) {
height: 40px;
}
}
.accountSelection {
.selectionControls {
margin-bottom: 16px;
}
.accountCards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
max-height: 500px;
overflow-y: auto;
padding: 8px;
.accountCard {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 2px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
background: #fff;
&:hover {
border-color: #1890ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.selected {
border-color: #52c41a;
background: #f6ffed;
}
.cardName {
margin-top: 8px;
font-size: 14px;
color: #1a1a1a;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.onlineStatus {
margin-top: 4px;
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
&.online {
background: #f6ffed;
color: #52c41a;
}
&.offline {
background: #f5f5f5;
color: #999;
}
}
.checkmark {
position: absolute;
top: 8px;
right: 8px;
color: #52c41a;
font-size: 20px;
}
}
}
}
.step2Content {
.stepHeader {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
.searchContainer {
display: flex;
gap: 12px;
margin-bottom: 16px;
:global(.ant-input) {
flex: 1;
}
}
.contentBody {
display: flex;
gap: 16px;
min-height: 500px;
}
.contactList,
.selectedList {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.listHeader {
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #d9d9d9;
font-weight: 500;
font-size: 14px;
color: #262626;
display: flex;
justify-content: space-between;
align-items: center;
}
.listContent {
flex: 1;
overflow-y: auto;
padding: 8px;
min-height: 300px;
}
.contactItem {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
cursor: pointer;
gap: 12px;
&:hover {
background-color: #f5f5f5;
}
&.selected {
background-color: #e6f7ff;
}
.contactInfo {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.contactName {
font-size: 14px;
color: #262626;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conRemark {
font-size: 12px;
color: #8c8c8c;
}
.groupIcon {
color: #1890ff;
font-size: 12px;
margin-left: 4px;
}
}
.selectedItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
.contactInfo {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.removeIcon {
color: #8c8c8c;
cursor: pointer;
padding: 4px;
border-radius: 2px;
transition: all 0.2s;
&:hover {
color: #ff4d4f;
background-color: #fff2f0;
}
}
}
.paginationContainer {
padding: 12px;
border-top: 1px solid #d9d9d9;
display: flex;
justify-content: center;
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
flex-direction: column;
gap: 12px;
color: #8c8c8c;
}
}
.step3Content {
display: flex;
gap: 24px;
align-items: flex-start;
// 左侧栏
.leftColumn {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
// 右侧栏
.rightColumn {
width: 400px;
display: flex;
flex-direction: column;
gap: 20px;
}
.messagePreview {
border: 2px dashed #52c41a;
border-radius: 8px;
padding: 20px;
background: #f6ffed;
.previewTitle {
font-size: 14px;
color: #52c41a;
font-weight: 500;
margin-bottom: 12px;
}
.messageBubble {
min-height: 60px;
padding: 12px;
background: #fff;
border-radius: 6px;
color: #666;
font-size: 14px;
line-height: 1.6;
.currentEditingLabel {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.messageText {
color: #333;
white-space: pre-wrap;
word-break: break-word;
}
}
}
// 已保存话术组
.savedScriptGroups {
.scriptGroupTitle {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.scriptGroupItem {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
background: #fff;
.scriptGroupHeader {
display: flex;
justify-content: space-between;
align-items: center;
.scriptGroupLeft {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
:global(.ant-radio) {
margin-right: 4px;
}
.scriptGroupName {
font-size: 14px;
font-weight: 500;
color: #333;
}
.messageCount {
font-size: 12px;
color: #999;
margin-left: 8px;
}
}
.scriptGroupActions {
display: flex;
gap: 4px;
.actionButton {
padding: 4px;
color: #666;
&:hover {
color: #1890ff;
}
}
}
}
.scriptGroupContent {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
font-size: 13px;
color: #666;
}
}
}
.messageInputArea {
.messageInput {
margin-bottom: 12px;
}
.attachmentButtons {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.aiRewriteSection {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.messageHint {
font-size: 12px;
color: #999;
}
}
.settingsPanel {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.settingsTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 16px;
}
.settingItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
.settingControl {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 14px;
color: #666;
min-width: 80px;
}
}
}
}
.tagSection {
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
}
.pushPreview {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #f0f7ff;
.previewTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: 14px;
color: #666;
line-height: 1.8;
}
}
}
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
.footerLeft {
font-size: 14px;
color: #666;
}
.footerRight {
display: flex;
gap: 12px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.container {
padding: 20px;
.step2Content {
.contentBody {
flex-direction: column;
}
.contactList,
.selectedList {
min-height: 200px;
}
}
.step3Content {
.rightColumn {
width: 350px;
}
}
}
}
@media (max-width: 768px) {
.container {
padding: 16px;
.steps {
padding: 16px;
.step {
span {
font-size: 12px;
}
.stepIcon {
width: 32px;
height: 32px;
font-size: 14px;
}
}
}
.stepBody {
padding: 16px;
min-height: 400px;
}
.step2Content {
.contentBody {
min-height: 400px;
}
}
.step3Content {
flex-direction: column;
.leftColumn {
width: 100%;
}
.rightColumn {
width: 100%;
}
}
.footer {
padding: 12px 16px;
flex-direction: column;
gap: 12px;
align-items: stretch;
.footerRight {
width: 100%;
justify-content: flex-end;
}
}
}
}
// 步骤1样式新版按照设计稿
.step1Content {
.stepHeader {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
.searchBar {
margin-bottom: 24px;
:global(.ant-input-affix-wrapper) {
height: 40px;
border-radius: 8px;
}
}
// 未选择的账号列表
.accountList {
margin-bottom: 30px;
max-height: 400px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
.accountItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: #fff;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #52c41a;
background: #fafafa;
}
.accountInfo {
flex: 1;
.accountName {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 4px;
}
.accountId {
font-size: 12px;
color: #999;
}
}
}
}
// 已选择区域
.selectedSection {
.selectedHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
margin-bottom: 12px;
span {
font-size: 14px;
color: #666;
}
.clearButton {
padding: 0;
font-size: 14px;
}
}
.selectedCards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
.selectedCard {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: 2px solid #52c41a;
border-radius: 8px;
background: #f6ffed;
position: relative;
.accountInfo {
flex: 1;
.accountName {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 4px;
}
.accountId {
font-size: 12px;
color: #999;
}
}
.checkIcon {
font-size: 20px;
color: #52c41a;
}
}
}
}
}

View File

@@ -0,0 +1,891 @@
import React, { useState, useEffect, useMemo } 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,
CheckOutlined,
SendOutlined,
CopyOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import Layout from "@/components/Layout/LayoutFiexd";
import styles from "./index.module.scss";
import {
useCustomerStore,
} from "@/store/module/weChat/customer";
import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
export type PushType = "friend-message" | "group-message" | "group-announcement";
interface WeChatAccount {
id: number;
name: string;
avatar?: string;
isOnline?: boolean;
wechatId?: string;
}
interface ContactItem {
id: number;
nickname: string;
avatar?: string;
conRemark?: string;
wechatId?: string;
gender?: number;
region?: string;
type?: "friend" | "group";
}
const CreatePushTask: React.FC = () => {
const navigate = useNavigate();
const { pushType } = useParams<{ pushType: PushType }>();
// 验证推送类型
const validPushType: PushType =
pushType === "friend-message" ||
pushType === "group-message" ||
pushType === "group-announcement"
? pushType
: "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("");
const [friendInterval, setFriendInterval] = useState(10);
const [messageInterval, setMessageInterval] = useState(1);
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);
// 获取标题和描述
const getTitle = () => {
switch (validPushType) {
case "friend-message":
return "好友消息推送";
case "group-message":
return "群消息推送";
case "group-announcement":
return "群公告推送";
default:
return "消息推送";
}
};
const getSubtitle = () => {
return "智能批量推送,AI智能话术改写";
};
// 步骤2的标题
const getStep2Title = () => {
switch (validPushType) {
case "friend-message":
return "好友";
case "group-message":
case "group-announcement":
return "群";
default:
return "选择";
}
};
// 重置状态
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 handleSelectAll = () => {
if (selectedAccounts.length === filteredAccounts.length) {
setSelectedAccounts([]);
} else {
setSelectedAccounts([...filteredAccounts]);
}
};
// 清空所有选择
const handleClearAll = () => {
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) {
message.warning("请至少选择一个微信账号");
return;
}
setCurrentStep(2);
} else if (currentStep === 2) {
if (selectedContacts.length === 0) {
message.warning(`请至少选择一个${validPushType === "friend-message" ? "好友" : "群"}`);
return;
}
setCurrentStep(3);
}
};
// 上一步
const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// 发送
const handleSend = () => {
if (!messageContent.trim()) {
message.warning("请输入消息内容");
return;
}
// TODO: 实现发送逻辑
console.log("发送推送", {
pushType: validPushType,
accounts: selectedAccounts,
contacts: selectedContacts,
messageContent,
friendInterval,
messageInterval,
selectedTag,
aiRewriteEnabled,
aiPrompt,
});
message.success("推送任务已创建");
navigate("/pc/powerCenter/message-push-assistant");
};
// 渲染步骤1选择微信账号
const renderStep1 = () => {
const unselectedAccounts = filteredAccounts.filter(
a => !selectedAccounts.some(s => s.id === a.id)
);
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>
{/* 未选择的账号列表 */}
{unselectedAccounts.length > 0 || filteredAccounts.length === 0 ? (
<div className={styles.accountList}>
{unselectedAccounts.length > 0 ? (
unselectedAccounts.map(account => (
<div
key={account.id}
className={styles.accountItem}
onClick={() => handleAccountToggle(account)}
>
<Avatar
src={account.avatar}
size={48}
style={{ backgroundColor: "#1890ff" }}
>
{!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.accountId}>
{account.isOnline ? "在线" : "离线"}
</div>
</div>
</div>
))
) : (
<Empty
description={searchKeyword ? "未找到匹配的账号" : "暂无微信账号"}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
) : null}
{/* 已选择的账号 */}
{selectedAccounts.length > 0 && (
<div className={styles.selectedSection}>
<div className={styles.selectedHeader}>
<span> {selectedAccounts.length} </span>
<Button
type="link"
onClick={handleClearAll}
className={styles.clearButton}
>
</Button>
</div>
<div className={styles.selectedCards}>
{selectedAccounts.map(account => (
<div key={account.id} className={styles.selectedCard}>
<Avatar
src={account.avatar}
size={48}
style={{ backgroundColor: "#1890ff" }}
>
{!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.accountId}>
{account.isOnline ? "在线" : "离线"}
</div>
</div>
<CheckOutlined className={styles.checkIcon} />
</div>
))}
</div>
</div>
)}
</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>
}
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>
{/* 底部操作栏 */}
<div className={styles.footer}>
<div className={styles.footerLeft}>
{currentStep === 1 && (
<span>{selectedAccounts.length}</span>
)}
{currentStep === 2 && (
<span>
{selectedContacts.length}{getStep2Title()}
</span>
)}
{currentStep === 3 && (
<span>
: {selectedAccounts.length}, {getStep2Title()}:{" "}
{selectedContacts.length}
</span>
)}
</div>
<div className={styles.footerRight}>
{currentStep === 1 && (
<>
<Button onClick={handleSelectAll}></Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
</>
)}
{currentStep === 2 && (
<>
<Button onClick={handlePrev}></Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
</>
)}
{currentStep === 3 && (
<>
<Button onClick={handlePrev}></Button>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
>
</Button>
</>
)}
</div>
</div>
</div>
</Layout>
);
};
export default CreatePushTask;

View File

@@ -0,0 +1,194 @@
.container {
padding: 40px;
background: #ffffff;
min-height: calc(100vh - 64px);
.section {
margin-bottom: 48px;
&:last-child {
margin-bottom: 0;
}
.sectionTitle {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 24px 0;
}
.cardGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
@media (max-width: 1200px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.dataRecordGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.taskCard {
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
.cardIcon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
margin-bottom: 20px;
flex-shrink: 0;
}
.cardContent {
flex: 1;
width: 100%;
.cardTitle {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 12px 0;
line-height: 1.3;
}
.cardDescription {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0;
}
}
.cardAction {
display: flex;
align-items: center;
margin-top: 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
opacity: 0.8;
}
}
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.container {
padding: 32px 24px;
.section {
margin-bottom: 40px;
.sectionTitle {
font-size: 18px;
margin-bottom: 20px;
}
.taskCard {
padding: 24px;
.cardIcon {
width: 48px;
height: 48px;
font-size: 20px;
margin-bottom: 16px;
}
.cardContent {
.cardTitle {
font-size: 16px;
margin-bottom: 10px;
}
.cardDescription {
font-size: 13px;
}
}
}
}
}
}
@media (max-width: 768px) {
.container {
padding: 24px 16px;
.section {
margin-bottom: 32px;
.sectionTitle {
font-size: 16px;
margin-bottom: 16px;
}
.cardGrid {
gap: 16px;
}
.taskCard {
padding: 20px;
.cardIcon {
width: 44px;
height: 44px;
font-size: 18px;
margin-bottom: 12px;
}
.cardContent {
.cardTitle {
font-size: 15px;
margin-bottom: 8px;
}
.cardDescription {
font-size: 12px;
}
}
.cardAction {
margin-top: 16px;
font-size: 13px;
}
}
}
}
}

View File

@@ -0,0 +1,156 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import PowerNavigation from "@/components/PowerNavtion";
import Layout from "@/components/Layout/LayoutFiexd";
import {
UserOutlined,
MessageOutlined,
SoundOutlined,
BarChartOutlined,
HistoryOutlined,
SendOutlined,
} from "@ant-design/icons";
import styles from "./index.module.scss";
export type PushType = "friend-message" | "group-message" | "group-announcement";
const MessagePushAssistant: React.FC = () => {
const navigate = useNavigate();
// 创建推送任务卡片数据
const createTaskCards = [
{
id: "friend-message",
title: "好友消息推送",
description: "向选定的微信好友批量发送消息",
icon: <UserOutlined />,
color: "#1890ff",
onClick: () => {
navigate("/pc/powerCenter/message-push-assistant/create-push-task/friend-message");
},
},
{
id: "group-message",
title: "群消息推送",
description: "向选定的微信群批量发送消息",
icon: <MessageOutlined />,
color: "#52c41a",
onClick: () => {
navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-message");
},
},
{
id: "group-announcement",
title: "群公告推送",
description: "向选定的微信群发布群公告",
icon: <SoundOutlined />,
color: "#722ed1",
onClick: () => {
navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-announcement");
},
},
];
// 数据与记录卡片数据
const dataRecordCards = [
{
id: "data-statistics",
title: "数据统计",
description: "查看推送效果统计与话术对比分析",
icon: <BarChartOutlined />,
color: "#ff7a00",
onClick: () => {
navigate("/pc/powerCenter/data-statistics");
},
},
{
id: "push-history",
title: "推送历史",
description: "查看所有推送任务的历史记录",
icon: <HistoryOutlined />,
color: "#666666",
onClick: () => {
navigate("/pc/powerCenter/push-history");
},
},
];
return (
<Layout
header={
<div style={{ padding: "20px" }}>
<PowerNavigation
title="消息推送助手"
subtitle="智能批量推送,AI智能话术改写"
showBackButton={true}
backButtonText="返回"
onBackClick={() => navigate("/pc/powerCenter")}
/>
</div>
}
footer={null}
>
<div className={styles.container}>
{/* 创建推送任务部分 */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}></h2>
<div className={styles.cardGrid}>
{createTaskCards.map(card => (
<div
key={card.id}
className={styles.taskCard}
onClick={card.onClick}
>
<div
className={styles.cardIcon}
style={{ backgroundColor: card.color }}
>
{card.icon}
</div>
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{card.title}</h3>
<p className={styles.cardDescription}>{card.description}</p>
</div>
<div className={styles.cardAction}>
<SendOutlined
style={{ fontSize: "14px", color: card.color }}
/>
<span style={{ color: card.color, marginLeft: "4px" }}>
</span>
</div>
</div>
))}
</div>
</div>
{/* 数据与记录部分 */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}></h2>
<div className={styles.dataRecordGrid}>
{dataRecordCards.map(card => (
<div
key={card.id}
className={styles.taskCard}
onClick={card.onClick}
>
<div
className={styles.cardIcon}
style={{ backgroundColor: card.color }}
>
{card.icon}
</div>
<div className={styles.cardContent}>
<h3 className={styles.cardTitle}>{card.title}</h3>
<p className={styles.cardDescription}>{card.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</Layout>
);
};
export default MessagePushAssistant;

View File

@@ -0,0 +1,65 @@
import { request } from "@/api/request";
import { PushHistoryRecord } from "./index";
// 获取推送历史接口参数
export interface GetPushHistoryParams {
page?: number;
pageSize?: number;
keyword?: string;
pushType?: string;
status?: string;
}
// 获取推送历史接口响应
export interface GetPushHistoryResponse {
success: boolean;
message?: string;
data?: {
list: PushHistoryRecord[];
total: number;
page: number;
pageSize: number;
};
}
/**
* 获取推送历史列表
*/
export const getPushHistory = async (
params: GetPushHistoryParams
): Promise<GetPushHistoryResponse> => {
try {
// TODO: 替换为实际的API接口地址
const response = await request.get("/api/push-history", { params });
// 如果接口返回的数据格式不同,需要在这里进行转换
if (response.data && response.data.success !== undefined) {
return response.data;
}
// 兼容不同的响应格式
return {
success: true,
data: {
list: response.data?.list || response.data?.data || [],
total: response.data?.total || 0,
page: response.data?.page || params.page || 1,
pageSize: response.data?.pageSize || params.pageSize || 10,
},
};
} catch (error: any) {
console.error("获取推送历史失败:", error);
return {
success: false,
message: error?.message || "获取推送历史失败",
};
}
};

View File

@@ -0,0 +1,286 @@
.pushHistory {
padding: 24px;
background: #f5f5f5;
min-height: calc(100vh - 64px);
// 筛选区域
.filterSection {
background: #fff;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
.filterLeft {
.tableTitle {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
}
.filterRight {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.searchInput {
width: 240px;
border-radius: 8px;
border: 1px solid #e9ecef;
&:focus,
&:hover {
border-color: #1890ff;
}
:global(.ant-input) {
border: none;
box-shadow: none;
&:focus {
box-shadow: none;
}
}
}
.filterSelect {
min-width: 120px;
border-radius: 8px;
border: 1px solid #e9ecef;
&:hover {
border-color: #1890ff;
}
:global(.ant-select-selector) {
border: none !important;
box-shadow: none !important;
border-radius: 8px !important;
}
:global(.ant-select-selection-item) {
color: #333;
}
}
}
}
// 表格区域
.tableSection {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
.dataTable {
:global(.ant-table) {
.ant-table-thead > tr > th {
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
color: #333;
padding: 16px;
}
.ant-table-tbody > tr {
&:hover {
background-color: #f5f5f5;
}
> td {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
}
.ant-table-tbody > tr:last-child > td {
border-bottom: none;
}
}
:global(.ant-table-empty) {
.ant-table-tbody > tr > td {
border-bottom: none;
}
}
}
}
// 分页区域
.paginationSection {
background: #fff;
border-radius: 12px;
padding: 16px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
.paginationInfo {
color: #666;
font-size: 14px;
}
.pagination {
:global(.ant-pagination-item) {
border-radius: 8px;
border: 1px solid #e9ecef;
margin: 0 4px;
&:hover {
border-color: #1890ff;
a {
color: #1890ff;
}
}
&.ant-pagination-item-active {
background: #1890ff;
border-color: #1890ff;
a {
color: #fff;
}
}
}
:global(.ant-pagination-prev),
:global(.ant-pagination-next) {
border-radius: 8px;
border: 1px solid #e9ecef;
margin: 0 4px;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
}
:global(.ant-pagination-disabled) {
&:hover {
border-color: #e9ecef;
color: rgba(0, 0, 0, 0.25);
}
}
}
}
}
// 响应式设计
@media (max-width: 1200px) {
.pushHistory {
padding: 20px;
.filterSection {
padding: 16px 20px;
.filterRight {
.searchInput {
width: 200px;
}
.filterSelect {
min-width: 100px;
}
}
}
}
}
@media (max-width: 768px) {
.pushHistory {
padding: 16px;
.filterSection {
padding: 16px;
flex-direction: column;
align-items: flex-start;
.filterLeft {
width: 100%;
}
.filterRight {
width: 100%;
.searchInput {
width: 100%;
}
.filterSelect {
flex: 1;
min-width: 0;
}
}
}
.tableSection {
padding: 12px;
overflow-x: auto;
.dataTable {
:global(.ant-table) {
min-width: 800px;
}
}
}
.paginationSection {
padding: 12px 16px;
flex-direction: column;
gap: 12px;
align-items: flex-start;
.pagination {
width: 100%;
display: flex;
justify-content: center;
}
}
}
}
@media (max-width: 480px) {
.pushHistory {
padding: 12px;
.filterSection {
padding: 12px;
.filterRight {
flex-direction: column;
.searchInput,
.filterSelect {
width: 100%;
}
}
}
.tableSection {
padding: 8px;
}
.paginationSection {
padding: 8px 12px;
}
}
}

View File

@@ -0,0 +1,379 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Table, Input, Select, Tag, Button, Pagination, message } from "antd";
import {
SearchOutlined,
EyeOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
} from "@ant-design/icons";
import PowerNavigation from "@/components/PowerNavtion";
import Layout from "@/components/Layout/LayoutFiexd";
import { getPushHistory } from "./api";
import styles from "./index.module.scss";
const { Option } = Select;
// 推送类型枚举
export enum PushType {
FRIEND_MESSAGE = "friend-message", // 好友消息
GROUP_MESSAGE = "group-message", // 群消息
GROUP_ANNOUNCEMENT = "group-announcement", // 群公告
}
// 推送状态枚举
export enum PushStatus {
COMPLETED = "completed", // 已完成
IN_PROGRESS = "in-progress", // 进行中
FAILED = "failed", // 失败
}
// 推送历史记录接口
export interface PushHistoryRecord {
id: string;
pushType: PushType;
pushContent: string;
targetCount: number;
successCount: number;
failureCount: number;
status: PushStatus;
createTime: string;
}
const PushHistory: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<PushHistoryRecord[]>([]);
const [searchValue, setSearchValue] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
// 获取推送历史数据
const fetchPushHistory = async (page: number = 1) => {
try {
setLoading(true);
const params: any = {
page,
pageSize: pagination.pageSize,
};
if (searchValue.trim()) {
params.keyword = searchValue.trim();
}
if (typeFilter !== "all") {
params.pushType = typeFilter;
}
if (statusFilter !== "all") {
params.status = statusFilter;
}
const response = await getPushHistory(params);
if (response.success) {
setDataSource(response.data?.list || []);
setPagination(prev => ({
...prev,
current: response.data?.page || page,
total: response.data?.total || 0,
}));
} else {
message.error(response.message || "获取推送历史失败");
setDataSource([]);
}
} catch (error) {
console.error("获取推送历史失败:", error);
message.error("获取推送历史失败,请稍后重试");
setDataSource([]);
} finally {
setLoading(false);
}
};
// 初始化加载数据
useEffect(() => {
fetchPushHistory(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 搜索处理
const handleSearch = (value: string) => {
setSearchValue(value);
setPagination(prev => ({ ...prev, current: 1 }));
fetchPushHistory(1);
};
// 类型筛选处理
const handleTypeFilterChange = (value: string) => {
setTypeFilter(value);
setPagination(prev => ({ ...prev, current: 1 }));
// 延迟执行,等待状态更新
setTimeout(() => {
fetchPushHistory(1);
}, 0);
};
// 状态筛选处理
const handleStatusFilterChange = (value: string) => {
setStatusFilter(value);
setPagination(prev => ({ ...prev, current: 1 }));
// 延迟执行,等待状态更新
setTimeout(() => {
fetchPushHistory(1);
}, 0);
};
// 分页处理
const handlePageChange = (page: number) => {
setPagination(prev => ({ ...prev, current: page }));
fetchPushHistory(page);
};
// 查看详情
const handleViewDetail = (record: PushHistoryRecord) => {
// TODO: 打开详情弹窗或跳转到详情页
console.log("查看详情:", record);
message.info("查看详情功能开发中");
};
// 获取推送类型标签
const getPushTypeTag = (type: PushType) => {
const typeMap = {
[PushType.FRIEND_MESSAGE]: { text: "好友消息", color: "#666" },
[PushType.GROUP_MESSAGE]: { text: "群消息", color: "#666" },
[PushType.GROUP_ANNOUNCEMENT]: { text: "群公告", color: "#666" },
};
const config = typeMap[type] || { text: "未知", color: "#666" };
return (
<Tag color={config.color} style={{ borderRadius: "12px" }}>
{config.text}
</Tag>
);
};
// 获取状态标签
const getStatusTag = (status: PushStatus) => {
const statusMap = {
[PushStatus.COMPLETED]: {
text: "已完成",
color: "#52c41a",
icon: <CheckCircleOutlined />,
},
[PushStatus.IN_PROGRESS]: {
text: "进行中",
color: "#1890ff",
icon: <ClockCircleOutlined />,
},
[PushStatus.FAILED]: {
text: "失败",
color: "#ff4d4f",
icon: <CloseCircleOutlined />,
},
};
const config = statusMap[status] || {
text: "未知",
color: "#666",
icon: null,
};
return (
<Tag
color={config.color}
icon={config.icon}
style={{
borderRadius: "12px",
color: "#fff",
border: "none",
}}
>
{config.text}
</Tag>
);
};
// 表格列定义
const columns = [
{
title: "推送类型",
dataIndex: "pushType",
key: "pushType",
width: 120,
render: (type: PushType) => getPushTypeTag(type),
},
{
title: "推送内容",
dataIndex: "pushContent",
key: "pushContent",
ellipsis: true,
render: (text: string) => (
<span style={{ color: "#333" }}>{text}</span>
),
},
{
title: "目标数量",
dataIndex: "targetCount",
key: "targetCount",
width: 100,
align: "center" as const,
render: (count: number) => <span>{count}</span>,
},
{
title: "成功数",
dataIndex: "successCount",
key: "successCount",
width: 100,
align: "center" as const,
render: (count: number) => (
<span style={{ color: "#52c41a", fontWeight: 500 }}>{count}</span>
),
},
{
title: "失败数",
dataIndex: "failureCount",
key: "failureCount",
width: 100,
align: "center" as const,
render: (count: number) => (
<span style={{ color: "#ff4d4f", fontWeight: 500 }}>{count}</span>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
align: "center" as const,
render: (status: PushStatus) => getStatusTag(status),
},
{
title: "创建时间",
dataIndex: "createTime",
key: "createTime",
width: 180,
render: (time: string) => (
<span style={{ color: "#666", fontSize: "13px" }}>{time}</span>
),
},
{
title: "操作",
key: "action",
width: 80,
align: "center" as const,
render: (_: any, record: PushHistoryRecord) => (
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
style={{ color: "#1890ff" }}
>
</Button>
),
},
];
return (
<Layout
header={
<div style={{ padding: "20px" }}>
<PowerNavigation
title="推送历史"
subtitle="查看所有推送任务的历史记录"
showBackButton={true}
backButtonText="返回"
onBackClick={() => navigate("/pc/powerCenter/message-push-assistant")}
/>
</div>
}
footer={null}
>
<div className={styles.pushHistory}>
{/* 筛选区域 */}
<div className={styles.filterSection}>
<div className={styles.filterLeft}>
<h3 className={styles.tableTitle}></h3>
</div>
<div className={styles.filterRight}>
<Input
placeholder="搜索内容"
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
onPressEnter={e => handleSearch(e.currentTarget.value)}
className={styles.searchInput}
allowClear
/>
<Select
value={typeFilter}
onChange={handleTypeFilterChange}
className={styles.filterSelect}
suffixIcon={<span></span>}
>
<Option value="all"></Option>
<Option value={PushType.FRIEND_MESSAGE}></Option>
<Option value={PushType.GROUP_MESSAGE}></Option>
<Option value={PushType.GROUP_ANNOUNCEMENT}></Option>
</Select>
<Select
value={statusFilter}
onChange={handleStatusFilterChange}
className={styles.filterSelect}
suffixIcon={<span></span>}
>
<Option value="all"></Option>
<Option value={PushStatus.COMPLETED}></Option>
<Option value={PushStatus.IN_PROGRESS}></Option>
<Option value={PushStatus.FAILED}></Option>
</Select>
</div>
</div>
{/* 数据表格 */}
<div className={styles.tableSection}>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
pagination={false}
className={styles.dataTable}
/>
</div>
{/* 分页组件 */}
{pagination.total > 0 && (
<div className={styles.paginationSection}>
<div className={styles.paginationInfo}>
{pagination.total}
</div>
<Pagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper={false}
className={styles.pagination}
/>
</div>
)}
</div>
</Layout>
);
};
export default PushHistory;

View File

@@ -7,6 +7,10 @@ import CommunicationRecord from "@/pages/pc/ckbox/powerCenter/communication-reco
import ContentManagement from "@/pages/pc/ckbox/powerCenter/content-management/index";
import AiTraining from "@/pages/pc/ckbox/powerCenter/ai-training";
import AutoGreeting from "@/pages/pc/ckbox/powerCenter/auto-greeting";
import MessagePushAssistant from "@/pages/pc/ckbox/powerCenter/message-push-assistant";
import CreatePushTask from "@/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task";
import DataStatistics from "@/pages/pc/ckbox/powerCenter/data-statistics";
import PushHistory from "@/pages/pc/ckbox/powerCenter/push-history";
import CommonConfig from "@/pages/pc/ckbox/commonConfig";
const ckboxRoutes = [
{
@@ -50,6 +54,22 @@ const ckboxRoutes = [
path: "powerCenter/auto-greeting",
element: <AutoGreeting />,
},
{
path: "powerCenter/message-push-assistant",
element: <MessagePushAssistant />,
},
{
path: "powerCenter/message-push-assistant/create-push-task/:pushType",
element: <CreatePushTask />,
},
{
path: "powerCenter/data-statistics",
element: <DataStatistics />,
},
{
path: "powerCenter/push-history",
element: <PushHistory />,
},
],
},
];