diff --git a/.gitignore b/.gitignore index 8b8fee88..df8964e0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Store_vue/unpackage/ Store_vue/.vscode/ SuperAdmin/.specstory/ Cunkebao/dist +Touchkebao/.specstory/ diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx new file mode 100644 index 00000000..770291a6 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx @@ -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" }, +]; + + + + + + + + diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss new file mode 100644 index 00000000..9ec10d60 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss @@ -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%; + } + } + } + } + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx new file mode 100644 index 00000000..303a8f1a --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx @@ -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: , + bgColor: "#1890ff", + }; + case "reply-rate": + return { + icon: , + bgColor: "#52c41a", + }; + case "avg-reply-time": + return { + icon: , + bgColor: "#722ed1", + }; + case "link-click-rate": + return { + icon: , + 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 ( + + navigate("/pc/powerCenter/message-push-assistant")} + /> + + } + footer={null} + > +
+ {/* 筛选和操作区域 */} +
+
+
+ 时间范围 + +
+
+
+ +
+
+ + {/* KPI统计卡片区域 */} +
+
+ {kpiData.map(kpi => { + const kpiConfig = getKpiConfig(kpi.id); + return ( +
+
+
+
{kpi.value}
+
{kpi.label}
+ {kpi.trend && ( +
+ {kpi.trend.direction === "up" ? ( + + ) : ( + + )} + {kpi.trend.text} +
+ )} + {kpi.subtitle && ( +
{kpi.subtitle}
+ )} +
+
+ {kpiConfig.icon} +
+
+
+ ); + })} +
+
+ + {/* 标签页导航 */} +
+
+
setActiveTab("comparison")} + > + 话术组对比 +
+
setActiveTab("time")} + > + 时段分析 +
+
setActiveTab("depth")} + > + 互动深度 +
+
+
+ + {/* 内容区域 */} +
+ {activeTab === "comparison" && ( +
+
+

话术组效果对比

+

+ 对比不同话术组的推送效果,找出表现最佳的话术 +

+
+
+ {dialogueGroupData.map((group, index) => ( +
+
+
+ 话术组 {index + 1} + + {group.status} + +
+
+ 推送 {group.pushCount}次 +
+
+
+
+ 触达率 +
+ {group.reachRate}% + +
+
+
+ 回复率 +
+ {group.replyRate}% + +
+
+
+ 点击率 +
+ {group.clickRate}% + +
+
+
+ 转化率 +
+ {group.conversionRate}% + +
+
+
+ 平均回复时间 +
+ {group.avgReplyTime} +
+
+
+
+ ))} +
+
+ )} + + {activeTab === "time" && ( +
+
+

时段分析

+

+ 分析不同时段的推送效果,优化推送时间策略 +

+
+
+

时段分析功能开发中...

+
+
+ )} + + {activeTab === "depth" && ( +
+
+

互动深度

+

+ 分析用户互动深度,优化推送内容策略 +

+
+
+

互动深度分析功能开发中...

+
+
+ )} +
+
+
+ ); +}; + +export default DataStatistics; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx index 34c14dd2..1410691b 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.data.tsx @@ -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: , + color: "#ff7a00", + tag: "消息推送", + features: [ + "微信好友消息推送", + "微信群消息推送", + "群公告消息推送", + "AI智能话术改写", + ], + path: "/pc/powerCenter/message-push-assistant", + }, ]; // KPI统计数据 diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.module.scss index 8209d4d6..6e4d6aea 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.module.scss @@ -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; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx index d6d09e5d..51652b1e 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx @@ -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 = () => { {/* 核心功能模块 */}
- - {featureCategories.map(card => ( + {/* 第一行:3个功能卡片 */} + + {featureCategories.slice(0, 3).map(card => (
{ >
-
{card.icon}
+
+ {card.icon} +
{
    {card.features.map((feature, index) => ( -
  • {feature}
  • +
  • + {feature} +
  • ))}
@@ -145,6 +171,61 @@ const PowerCenter: React.FC = () => { ))} + + {/* 第二行:消息推送助手(单独一行,左边对齐) */} + {featureCategories.length > 3 && ( + + +
handleCardClick(featureCategories[3])} + > +
+
+
+ {featureCategories[3].icon} +
+
+ {featureCategories[3].tag} +
+
+ +
+

+ {featureCategories[3].title} +

+

+ {featureCategories[3].description} +

+ +
    + {featureCategories[3].features.map((feature, index) => ( +
  • + {feature} +
  • + ))} +
+
+
+
+ +
+ )}
{/* 页面底部 */}
diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss new file mode 100644 index 00000000..a7d38777 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss @@ -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; + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx new file mode 100644 index 00000000..18ef6ce4 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx @@ -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 = ({ + visible, + pushType, + onCancel, + onConfirm, +}) => { + const [currentStep, setCurrentStep] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(""); + const [selectedAccounts, setSelectedAccounts] = useState([]); + const [selectedContacts, setSelectedContacts] = useState([]); + const [messageContent, setMessageContent] = useState(""); + const [friendInterval, setFriendInterval] = useState(10); + const [messageInterval, setMessageInterval] = useState(1); + const [selectedTag, setSelectedTag] = useState(""); + const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false); + const [aiPrompt, setAiPrompt] = useState(""); + + // 步骤2数据 + const [contactsData, setContactsData] = useState([]); + 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 = () => ( +
+
+

选择微信账号

+

可选择多个微信账号进行推送

+
+
+ } + value={searchKeyword} + onChange={e => setSearchKeyword(e.target.value)} + allowClear + /> +
+
+
+ 0 && + selectedAccounts.length === filteredAccounts.length + } + indeterminate={ + selectedAccounts.length > 0 && + selectedAccounts.length < filteredAccounts.length + } + onChange={handleSelectAll} + disabled={filteredAccounts.length === 0} + > + 全选 + +
+
+ {filteredAccounts.length > 0 ? ( + filteredAccounts.map(account => { + const isSelected = selectedAccounts.some(a => a.id === account.id); + return ( +
handleAccountToggle(account)} + > + + {!account.avatar && (account.nickname || account.name || "").charAt(0)} + +
+ {account.nickname || account.name || "未知"} +
+
+ {account.isOnline ? "在线" : "离线"} +
+ {isSelected && ( + + )} +
+ ); + }) + ) : ( + + )} +
+
+
+ ); + + // 渲染步骤2:选择好友/群 + const renderStep2 = () => ( +
+
+
+ } + value={step2SearchValue} + onChange={e => setStep2SearchValue(e.target.value)} + allowClear + /> + +
+
+ {/* 左侧:好友/群列表 */} +
+
+ + {getStep2Title()}列表(共{filteredContacts.length}个) + +
+
+ {loadingContacts ? ( +
+ + 加载中... +
+ ) : paginatedContacts.length > 0 ? ( + paginatedContacts.map(contact => { + const isSelected = selectedContacts.some( + c => c.id === contact.id, + ); + return ( +
handleContactToggle(contact)} + > + + + ) : ( + + ) + } + /> +
+
+ {contact.nickname} +
+ {contact.conRemark && ( +
+ {contact.conRemark} +
+ )} +
+ {contact.type === "group" && ( + + )} +
+ ); + }) + ) : ( + + )} +
+ {filteredContacts.length > 0 && ( +
+ setStep2Page(p)} + showSizeChanger={false} + /> +
+ )} +
+ + {/* 右侧:已选列表 */} +
+
+ + 已选{getStep2Title()}列表(共{selectedContacts.length}个) + + {selectedContacts.length > 0 && ( + + )} +
+
+ {selectedContacts.length > 0 ? ( + selectedContacts.map(contact => ( +
+
+ + ) : ( + + ) + } + /> +
+
{contact.nickname}
+ {contact.conRemark && ( +
+ {contact.conRemark} +
+ )} +
+ {contact.type === "group" && ( + + )} +
+ handleRemoveContact(contact.id)} + /> +
+ )) + ) : ( + + )} +
+
+
+
+
+ ); + + // 渲染步骤3:一键群发 + const renderStep3 = () => ( +
+
+
+
模拟推送内容
+
+ {messageContent || "开始添加消息内容..."} +
+
+ +
+ setMessageContent(e.target.value)} + rows={4} + onKeyDown={e => { + if (e.ctrlKey && e.key === "Enter") { + e.preventDefault(); + setMessageContent(prev => prev + "\n"); + } + }} + /> +
+
+
+ + AI智能话术改写 + {aiRewriteEnabled && ( + setAiPrompt(e.target.value)} + style={{ marginLeft: 12, width: 200 }} + /> + )} + +
+
+ 按住CTRL+ENTER换行,已配置1个话术组,已选择0个进行推送 +
+
+ +
+
+
好友间间隔
+
+ 间隔时间(秒) + + {friendInterval} - 20 +
+
+
+
消息间间隔
+
+ 间隔时间(秒) + + {messageInterval} - 12 +
+
+
+
完成打标签
+
+ +
+
+
+ +
+
推送预览
+
    +
  • 推送账号: {selectedAccounts.length}个
  • +
  • + 推送{getStep2Title()}: {selectedContacts.length}个 +
  • +
  • 话术组数: 0个
  • +
  • 随机推送: 否
  • +
  • 预计耗时: ~1分钟
  • +
+
+
+
+ ); + + return ( + +
+ +
+

{getTitle()}

+

{getSubtitle()}

+
+
+ + {/* 步骤指示器 */} +
+
= 1 ? styles.active : ""} ${currentStep > 1 ? styles.completed : ""}`} + > +
+ {currentStep > 1 ? : "1"} +
+ 选择微信 +
+
= 2 ? styles.active : ""} ${currentStep > 2 ? styles.completed : ""}`} + > +
+ {currentStep > 2 ? : "2"} +
+ 选择{getStep2Title()} +
+
= 3 ? styles.active : ""}`} + > +
3
+ 一键群发 +
+
+ + {/* 步骤内容 */} +
+ {currentStep === 1 && renderStep1()} + {currentStep === 2 && renderStep2()} + {currentStep === 3 && renderStep3()} +
+ + {/* 底部操作栏 */} +
+
+ {currentStep === 1 && ( + 已选择{selectedAccounts.length}个微信账号 + )} + {currentStep === 2 && ( + + 已选择{selectedContacts.length}个{getStep2Title()} + + )} + {currentStep === 3 && ( + + 推送账号: {selectedAccounts.length}个, 推送{getStep2Title()}:{" "} + {selectedContacts.length}个 + + )} +
+
+ {currentStep === 1 && ( + <> + + + + )} + {currentStep === 2 && ( + <> + + + + )} + {currentStep === 3 && ( + <> + + + + )} +
+
+
+ ); +}; + +export default PushTaskModal; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss new file mode 100644 index 00000000..04b7188a --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss @@ -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; + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx new file mode 100644 index 00000000..30bb057b --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx @@ -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([]); + const [selectedContacts, setSelectedContacts] = useState([]); + const [messageContent, setMessageContent] = useState(""); + const [friendInterval, setFriendInterval] = useState(10); + const [messageInterval, setMessageInterval] = useState(1); + const [selectedTag, setSelectedTag] = useState(""); + const [aiRewriteEnabled, setAiRewriteEnabled] = useState(false); + const [aiPrompt, setAiPrompt] = useState(""); + const [selectedScriptGroup, setSelectedScriptGroup] = useState("group1"); + const [scriptGroups] = useState([ + { id: "group1", name: "话术组 1", messageCount: 1, content: "啊实打实" }, + ]); + + // 步骤2数据 + const [contactsData, setContactsData] = useState([]); + 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 ( +
+
+

选择微信账号

+

可选择多个微信账号进行推送

+
+ +
+ } + value={searchKeyword} + onChange={e => setSearchKeyword(e.target.value)} + allowClear + /> +
+ + {/* 未选择的账号列表 */} + {unselectedAccounts.length > 0 || filteredAccounts.length === 0 ? ( +
+ {unselectedAccounts.length > 0 ? ( + unselectedAccounts.map(account => ( +
handleAccountToggle(account)} + > + + {!account.avatar && (account.nickname || account.name || "").charAt(0)} + +
+
+ {account.nickname || account.name || "未知"} +
+
+ {account.isOnline ? "在线" : "离线"} +
+
+
+ )) + ) : ( + + )} +
+ ) : null} + + {/* 已选择的账号 */} + {selectedAccounts.length > 0 && ( +
+
+ 已选择 {selectedAccounts.length} 个微信账号 + +
+
+ {selectedAccounts.map(account => ( +
+ + {!account.avatar && (account.nickname || account.name || "").charAt(0)} + +
+
+ {account.nickname || account.name || "未知"} +
+
+ {account.isOnline ? "在线" : "离线"} +
+
+ +
+ ))} +
+
+ )} +
+ ); + }; + + // 渲染步骤2:选择好友/群 + const renderStep2 = () => ( +
+
+
+

选择{getStep2Title()}

+

从{getStep2Title()}列表中选择推送对象

+
+
+ } + value={step2SearchValue} + onChange={e => setStep2SearchValue(e.target.value)} + allowClear + /> + +
+
+ {/* 左侧:好友/群列表 */} +
+
+ + {getStep2Title()}列表(共{step2Total}个) + +
+
+ {loadingContacts ? ( +
+ + 加载中... +
+ ) : paginatedContacts.length > 0 ? ( + paginatedContacts.map(contact => { + const isSelected = selectedContacts.some( + c => c.id === contact.id, + ); + return ( +
handleContactToggle(contact)} + > + + + ) : ( + + ) + } + /> +
+
+ {contact.nickname} +
+ {contact.conRemark && ( +
+ {contact.conRemark} +
+ )} +
+ {contact.type === "group" && ( + + )} +
+ ); + }) + ) : ( + + )} +
+ {step2Total > 0 && ( +
+ setStep2Page(p)} + showSizeChanger={false} + /> +
+ )} +
+ + {/* 右侧:已选列表 */} +
+
+ + 已选{getStep2Title()}列表(共{selectedContacts.length}个) + + {selectedContacts.length > 0 && ( + + )} +
+
+ {selectedContacts.length > 0 ? ( + selectedContacts.map(contact => ( +
+
+ + ) : ( + + ) + } + /> +
+
{contact.nickname}
+ {contact.conRemark && ( +
+ {contact.conRemark} +
+ )} +
+ {contact.type === "group" && ( + + )} +
+ handleRemoveContact(contact.id)} + /> +
+ )) + ) : ( + + )} +
+
+
+
+
+ ); + + // 渲染步骤3:一键群发 + const renderStep3 = () => ( +
+
+ {/* 左侧栏:内容编辑 */} +
+ {/* 模拟推送内容 */} +
+
模拟推送内容
+
+
当前编辑话术
+
+ {messageContent || "开始添加消息内容..."} +
+
+
+ + {/* 已保存话术组 */} +
+
+ 已保存话术组({scriptGroups.length}) +
+ {scriptGroups.map(group => ( +
+
+
+ setSelectedScriptGroup(group.id)} + /> + {group.name} + + {group.messageCount}条消息 + +
+
+
+
+ {selectedScriptGroup === group.id && ( +
+ {group.content} +
+ )} +
+ ))} +
+ + {/* 消息输入区域 */} +
+ setMessageContent(e.target.value)} + rows={4} + onKeyDown={e => { + if (e.ctrlKey && e.key === "Enter") { + e.preventDefault(); + setMessageContent(prev => prev + "\n"); + } + }} + /> +
+
+
+ + AI智能话术改写 + {aiRewriteEnabled && ( + setAiPrompt(e.target.value)} + style={{ marginLeft: 12, width: 200 }} + /> + )} + +
+
+ 按住CTRL+ENTER换行,已配置{scriptGroups.length}个话术组,已选择0个进行推送 +
+
+
+ + {/* 右侧栏:设置和预览 */} +
+ {/* 相关设置 */} +
+
相关设置
+
+
好友间间隔
+
+ 间隔时间(秒) + + {friendInterval} - 20 +
+
+
+
消息间间隔
+
+ 间隔时间(秒) + + {messageInterval} - 12 +
+
+
+ + {/* 完成打标签 */} +
+
完成打标签
+ +
+ + {/* 推送预览 */} +
+
推送预览
+
    +
  • 推送账号: {selectedAccounts.length}个
  • +
  • + 推送{getStep2Title()}: {selectedContacts.length}个 +
  • +
  • 话术组数: 0个
  • +
  • 随机推送: 否
  • +
  • 预计耗时: ~1分钟
  • +
+
+
+
+
+ ); + + return ( + + +
+ } + footer={null} + > +
+ {/* 步骤指示器 */} +
+
= 1 ? styles.active : ""} ${currentStep > 1 ? styles.completed : ""}`} + > +
+ {currentStep > 1 ? : "1"} +
+ 选择微信 +
+
= 2 ? styles.active : ""} ${currentStep > 2 ? styles.completed : ""}`} + > +
+ {currentStep > 2 ? : "2"} +
+ 选择{getStep2Title()} +
+
= 3 ? styles.active : ""}`} + > +
3
+ 一键群发 +
+
+ + {/* 步骤内容 */} +
+ {currentStep === 1 && renderStep1()} + {currentStep === 2 && renderStep2()} + {currentStep === 3 && renderStep3()} +
+ + {/* 底部操作栏 */} +
+
+ {currentStep === 1 && ( + 已选择{selectedAccounts.length}个微信账号 + )} + {currentStep === 2 && ( + + 已选择{selectedContacts.length}个{getStep2Title()} + + )} + {currentStep === 3 && ( + + 推送账号: {selectedAccounts.length}个, 推送{getStep2Title()}:{" "} + {selectedContacts.length}个 + + )} +
+
+ {currentStep === 1 && ( + <> + + + + )} + {currentStep === 2 && ( + <> + + + + )} + {currentStep === 3 && ( + <> + + + + )} +
+
+
+ + ); +}; + +export default CreatePushTask; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss new file mode 100644 index 00000000..ccef81f1 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss @@ -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; + } + } + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx new file mode 100644 index 00000000..acf205f3 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx @@ -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: , + color: "#1890ff", + onClick: () => { + navigate("/pc/powerCenter/message-push-assistant/create-push-task/friend-message"); + }, + }, + { + id: "group-message", + title: "群消息推送", + description: "向选定的微信群批量发送消息", + icon: , + color: "#52c41a", + onClick: () => { + navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-message"); + }, + }, + { + id: "group-announcement", + title: "群公告推送", + description: "向选定的微信群发布群公告", + icon: , + color: "#722ed1", + onClick: () => { + navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-announcement"); + }, + }, + ]; + + // 数据与记录卡片数据 + const dataRecordCards = [ + { + id: "data-statistics", + title: "数据统计", + description: "查看推送效果统计与话术对比分析", + icon: , + color: "#ff7a00", + onClick: () => { + navigate("/pc/powerCenter/data-statistics"); + }, + }, + { + id: "push-history", + title: "推送历史", + description: "查看所有推送任务的历史记录", + icon: , + color: "#666666", + onClick: () => { + navigate("/pc/powerCenter/push-history"); + }, + }, + ]; + + return ( + + navigate("/pc/powerCenter")} + /> +
+ } + footer={null} + > +
+ {/* 创建推送任务部分 */} +
+

创建推送任务

+
+ {createTaskCards.map(card => ( +
+
+ {card.icon} +
+
+

{card.title}

+

{card.description}

+
+
+ + + 立即创建 + +
+
+ ))} +
+
+ + {/* 数据与记录部分 */} +
+

数据与记录

+
+ {dataRecordCards.map(card => ( +
+
+ {card.icon} +
+
+

{card.title}

+

{card.description}

+
+
+ ))} +
+
+
+ + ); +}; + +export default MessagePushAssistant; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts new file mode 100644 index 00000000..dceeed01 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -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 => { + 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 || "获取推送历史失败", + }; + } +}; + + + + + + + + diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss new file mode 100644 index 00000000..3de5b962 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss @@ -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; + } + } +} + + + + + + + + diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx new file mode 100644 index 00000000..0697b99d --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -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([]); + const [searchValue, setSearchValue] = useState(""); + const [typeFilter, setTypeFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("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 ( + + {config.text} + + ); + }; + + // 获取状态标签 + const getStatusTag = (status: PushStatus) => { + const statusMap = { + [PushStatus.COMPLETED]: { + text: "已完成", + color: "#52c41a", + icon: , + }, + [PushStatus.IN_PROGRESS]: { + text: "进行中", + color: "#1890ff", + icon: , + }, + [PushStatus.FAILED]: { + text: "失败", + color: "#ff4d4f", + icon: , + }, + }; + const config = statusMap[status] || { + text: "未知", + color: "#666", + icon: null, + }; + return ( + + {config.text} + + ); + }; + + // 表格列定义 + const columns = [ + { + title: "推送类型", + dataIndex: "pushType", + key: "pushType", + width: 120, + render: (type: PushType) => getPushTypeTag(type), + }, + { + title: "推送内容", + dataIndex: "pushContent", + key: "pushContent", + ellipsis: true, + render: (text: string) => ( + {text} + ), + }, + { + title: "目标数量", + dataIndex: "targetCount", + key: "targetCount", + width: 100, + align: "center" as const, + render: (count: number) => {count}, + }, + { + title: "成功数", + dataIndex: "successCount", + key: "successCount", + width: 100, + align: "center" as const, + render: (count: number) => ( + {count} + ), + }, + { + title: "失败数", + dataIndex: "failureCount", + key: "failureCount", + width: 100, + align: "center" as const, + render: (count: number) => ( + {count} + ), + }, + { + 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) => ( + {time} + ), + }, + { + title: "操作", + key: "action", + width: 80, + align: "center" as const, + render: (_: any, record: PushHistoryRecord) => ( + + ), + }, + ]; + + return ( + + navigate("/pc/powerCenter/message-push-assistant")} + /> +
+ } + footer={null} + > +
+ {/* 筛选区域 */} +
+
+

推送历史记录

+
+
+ } + value={searchValue} + onChange={e => setSearchValue(e.target.value)} + onPressEnter={e => handleSearch(e.currentTarget.value)} + className={styles.searchInput} + allowClear + /> + + +
+
+ + {/* 数据表格 */} +
+ + + + {/* 分页组件 */} + {pagination.total > 0 && ( +
+
+ 共{pagination.total}条记录 +
+ +
+ )} + + + ); +}; + +export default PushHistory; + + + + + + + + diff --git a/Touchkebao/src/router/module/pc.tsx b/Touchkebao/src/router/module/pc.tsx index 98578346..fa3dcd96 100644 --- a/Touchkebao/src/router/module/pc.tsx +++ b/Touchkebao/src/router/module/pc.tsx @@ -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: , }, + { + path: "powerCenter/message-push-assistant", + element: , + }, + { + path: "powerCenter/message-push-assistant/create-push-task/:pushType", + element: , + }, + { + path: "powerCenter/data-statistics", + element: , + }, + { + path: "powerCenter/push-history", + element: , + }, ], }, ];