代码提交
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ Store_vue/unpackage/
|
||||
Store_vue/.vscode/
|
||||
SuperAdmin/.specstory/
|
||||
Cunkebao/dist
|
||||
Touchkebao/.specstory/
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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统计数据
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
下一步 >
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步 >
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSend}
|
||||
>
|
||||
一键发送
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PushTaskModal;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
下一步 >
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步 >
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
<Button onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSend}
|
||||
>
|
||||
一键发送
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePushTask;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 || "获取推送历史失败",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
379
Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx
Normal file
379
Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx
Normal 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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user