FEAT => 本次更新项目为:

This commit is contained in:
超级老白兔
2025-09-25 11:28:17 +08:00
parent e00bbf576a
commit 0159a246ac
10 changed files with 4 additions and 1163 deletions

View File

@@ -27,7 +27,10 @@ const PowerCenter: React.FC = () => {
</div>
<div className={styles.categoryInfo}>
<h2 className={styles.categoryTitle}>{category.title}</h2>
<span className={styles.categoryCount}>
<span
className={styles.categoryCount}
style={{ backgroundColor: category.color, color: "#ffffff" }}
>
{category.count}
</span>
</div>

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const MomentsMarketing: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="朋友圈营销"
subtitle="AI智能生成朋友圈内容提升品牌曝光度"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
</div>
);
};
export default MomentsMarketing;

View File

@@ -1,215 +0,0 @@
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.stepsContainer {
padding: 24px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.steps {
max-width: 600px;
margin: 0 auto;
}
.stepContent {
margin: 15px;
}
.stepCard {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 8px;
:global(.ant-card-head) {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
.ant-card-head-title {
font-weight: 600;
color: #495057;
.anticon {
margin-right: 8px;
color: #1890ff;
}
}
}
}
.ageRange {
display: flex;
align-items: center;
.ant-input-number {
width: 80px;
}
}
.estimatedCount {
margin-top: 24px;
padding: 16px;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
.anticon {
color: #1890ff;
font-size: 16px;
}
span {
font-size: 14px;
color: #262626;
strong {
color: #1890ff;
font-size: 16px;
}
}
}
.variableList {
display: flex;
flex-wrap: wrap;
gap: 8px;
.ant-tag {
margin: 0;
transition: all 0.2s;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
.messagePreview {
margin-top: 24px;
h4 {
margin-bottom: 12px;
color: #262626;
font-weight: 600;
}
}
.previewContent {
padding: 16px;
background: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 6px;
min-height: 80px;
font-size: 14px;
line-height: 1.6;
color: #262626;
white-space: pre-wrap;
}
.summary {
h4 {
margin-bottom: 16px;
color: #262626;
font-weight: 600;
}
}
.summaryGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.summaryItem {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
.anticon {
color: #1890ff;
font-size: 16px;
}
span {
font-size: 14px;
color: #262626;
}
}
.stepActions {
display: flex;
justify-content: center;
padding: 24px 0;
background: #fff;
.ant-btn {
min-width: 100px;
}
}
// 响应式设计
@media (max-width: 768px) {
.container {
padding: 16px;
}
.stepsContainer {
padding: 16px;
}
.summaryGrid {
grid-template-columns: 1fr;
}
.ageRange {
flex-direction: column;
align-items: flex-start;
gap: 8px;
span {
display: none;
}
}
}
// 表单样式优化
:global {
.ant-form-item-label > label {
font-weight: 500;
color: #262626;
}
.ant-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
}
.ant-upload-list-picture-card .ant-upload-list-item {
border-radius: 6px;
}
.ant-steps-item-title {
font-weight: 500 !important;
}
}

View File

@@ -1,726 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import Layout from "@/components/Layout/LayoutFiexd";
import {
Card,
Button,
Steps,
Form,
Input,
Select,
Checkbox,
Radio,
DatePicker,
TimePicker,
InputNumber,
Upload,
Tag,
Divider,
Space,
message,
} from "antd";
import {
UserOutlined,
MessageOutlined,
SendOutlined,
PlusOutlined,
TagsOutlined,
ClockCircleOutlined,
TeamOutlined,
AimOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
import type { UploadFile } from "antd";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const { Step } = Steps;
const { TextArea } = Input;
const { Option } = Select;
interface CustomerFilter {
tags: string[];
regions: string[];
ageRange: [number, number];
gender: "all" | "male" | "female";
lastContactTime: "all" | "7days" | "30days" | "90days" | "180days";
purchaseHistory: "all" | "purchased" | "no-purchase" | "high-value";
}
interface MessageContent {
type: "text" | "image" | "mixed";
text: string;
images: UploadFile[];
variables: string[];
}
interface SendSettings {
sendMode: "immediate" | "scheduled";
scheduledTime?: string;
sendInterval: number;
maxPerDay: number;
timeRange: [string, string];
}
const PrecisionSend: React.FC = () => {
const [currentStep, setCurrentStep] = useState(0);
const [form] = Form.useForm();
const [customerFilter, setCustomerFilter] = useState<CustomerFilter>({
tags: [],
regions: [],
ageRange: [18, 65],
gender: "all",
lastContactTime: "all",
purchaseHistory: "all",
});
const [messageContent, setMessageContent] = useState<MessageContent>({
type: "text",
text: "",
images: [],
variables: [],
});
const [sendSettings, setSendSettings] = useState<SendSettings>({
sendMode: "immediate",
sendInterval: 5,
maxPerDay: 100,
timeRange: ["09:00", "18:00"],
});
const [estimatedCount, setEstimatedCount] = useState(0);
const [loading, setLoading] = useState(false);
const customerTags = [
"高价值客户",
"潜在客户",
"活跃用户",
"沉默用户",
"VIP客户",
"新客户",
"老客户",
"流失客户",
];
const regions = [
"北京",
"上海",
"广州",
"深圳",
"杭州",
"南京",
"成都",
"武汉",
"西安",
"重庆",
"天津",
"苏州",
];
const messageVariables = [
"{客户姓名}",
"{公司名称}",
"{联系人}",
"{产品名称}",
"{优惠金额}",
"{到期时间}",
"{客服电话}",
"{官网地址}",
];
const handleNext = () => {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
}
};
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = async () => {
try {
// 验证必填项
if (!messageContent.text && messageContent.images.length === 0) {
message.error("请输入消息内容或上传图片");
return;
}
if (
sendSettings.sendMode === "scheduled" &&
!sendSettings.scheduledTime
) {
message.error("请选择发送时间");
return;
}
if (estimatedCount === 0) {
message.error("没有符合条件的客户,请调整筛选条件");
return;
}
setLoading(true);
const hide = message.loading("正在创建发送任务...", 0);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 2000));
hide();
message.success(
`发送任务已创建成功!预计发送给 ${estimatedCount} 位客户`,
);
// 重置表单
setCurrentStep(0);
form.resetFields();
setCustomerFilter({
tags: [],
regions: [],
ageRange: [18, 65],
gender: "all",
lastContactTime: "all",
purchaseHistory: "all",
});
setMessageContent({
type: "text",
text: "",
images: [],
variables: [],
});
setSendSettings({
sendMode: "immediate",
sendInterval: 5,
maxPerDay: 100,
timeRange: ["09:00", "18:00"],
});
} catch (error) {
message.error("提交失败,请检查网络连接后重试");
console.error("Submit error:", error);
} finally {
setLoading(false);
}
};
// 计算预估客户数量
const calculateEstimatedCount = useCallback(() => {
let baseCount = 1000; // 假设基础客户数
// 根据筛选条件调整数量
if (customerFilter.tags.length > 0) {
baseCount = Math.floor(
baseCount * (0.9 - customerFilter.tags.length * 0.1),
);
}
if (customerFilter.regions.length > 0) {
baseCount = Math.floor(
baseCount * (0.95 - customerFilter.regions.length * 0.05),
);
}
if (customerFilter.gender !== "all") {
baseCount = Math.floor(baseCount * 0.5);
}
if (customerFilter.purchaseHistory !== "all") {
baseCount = Math.floor(baseCount * 0.6);
}
setEstimatedCount(Math.max(baseCount, 0));
}, [customerFilter]);
useEffect(() => {
calculateEstimatedCount();
}, [calculateEstimatedCount]);
const renderStepContent = () => {
switch (currentStep) {
case 0:
return (
<Card
title={
<>
<UserOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="客户标签" name="tags">
<Checkbox.Group
options={customerTags.map(tag => ({
label: tag,
value: tag,
}))}
value={customerFilter.tags}
onChange={values =>
setCustomerFilter({
...customerFilter,
tags: values as string[],
})
}
/>
</Form.Item>
<Form.Item label="地区筛选" name="regions">
<Select
mode="multiple"
placeholder="选择目标地区"
value={customerFilter.regions}
onChange={values =>
setCustomerFilter({ ...customerFilter, regions: values })
}
>
{regions.map(region => (
<Option key={region} value={region}>
{region}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="年龄范围" name="ageRange">
<div className={styles.ageRange}>
<InputNumber
min={18}
max={100}
value={customerFilter.ageRange[0]}
onChange={value =>
setCustomerFilter({
...customerFilter,
ageRange: [value || 18, customerFilter.ageRange[1]],
})
}
/>
<span style={{ margin: "0 8px" }}>-</span>
<InputNumber
min={18}
max={100}
value={customerFilter.ageRange[1]}
onChange={value =>
setCustomerFilter({
...customerFilter,
ageRange: [customerFilter.ageRange[0], value || 65],
})
}
/>
</div>
</Form.Item>
<Form.Item label="性别" name="gender">
<Radio.Group
value={customerFilter.gender}
onChange={e =>
setCustomerFilter({
...customerFilter,
gender: e.target.value,
})
}
>
<Radio value="all"></Radio>
<Radio value="male"></Radio>
<Radio value="female"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="最后联系时间" name="lastContactTime">
<Select
value={customerFilter.lastContactTime}
onChange={value =>
setCustomerFilter({
...customerFilter,
lastContactTime: value,
})
}
>
<Option value="all"></Option>
<Option value="7days">7</Option>
<Option value="30days">30</Option>
<Option value="90days">90</Option>
<Option value="180days">180</Option>
</Select>
</Form.Item>
<Form.Item label="购买历史" name="purchaseHistory">
<Select
value={customerFilter.purchaseHistory}
onChange={value =>
setCustomerFilter({
...customerFilter,
purchaseHistory: value,
})
}
>
<Option value="all"></Option>
<Option value="purchased"></Option>
<Option value="no-purchase"></Option>
<Option value="high-value"></Option>
</Select>
</Form.Item>
</Form>
<div className={styles.estimatedCount}>
<TeamOutlined />
<span>
<strong>{estimatedCount}</strong>
</span>
</div>
</Card>
);
case 1:
return (
<Card
title={
<>
<MessageOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="消息类型" name="messageType">
<Radio.Group
value={messageContent.type}
onChange={e =>
setMessageContent({
...messageContent,
type: e.target.value,
})
}
>
<Radio value="text"></Radio>
<Radio value="image"></Radio>
<Radio value="mixed"></Radio>
</Radio.Group>
</Form.Item>
{(messageContent.type === "text" ||
messageContent.type === "mixed") && (
<Form.Item label="消息文本" name="messageContent">
<TextArea
rows={6}
placeholder="请输入消息内容,支持变量如 {客户姓名}、{产品名称} 等"
value={messageContent.text}
onChange={e =>
setMessageContent({
...messageContent,
text: e.target.value,
})
}
showCount
maxLength={500}
/>
</Form.Item>
)}
{(messageContent.type === "image" ||
messageContent.type === "mixed") && (
<Form.Item label="上传图片" name="images">
<Upload
listType="picture-card"
fileList={messageContent.images}
onChange={({ fileList }) =>
setMessageContent({ ...messageContent, images: fileList })
}
beforeUpload={file => {
const isImage = file.type?.startsWith("image/");
if (!isImage) {
message.error("只能上传图片文件!");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error("图片大小不能超过 2MB!");
}
return false; // 阻止自动上传
}}
accept="image/*"
>
{messageContent.images.length < 9 && (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}></div>
</div>
)}
</Upload>
</Form.Item>
)}
<Form.Item label="可用变量">
<div className={styles.variableList}>
{messageVariables.map(variable => (
<Tag
key={variable}
icon={<TagsOutlined />}
color="blue"
style={{ cursor: "pointer", margin: "4px" }}
onClick={() => {
const newText = messageContent.text + variable;
setMessageContent({ ...messageContent, text: newText });
}}
>
{variable}
</Tag>
))}
</div>
</Form.Item>
<Divider />
<div className={styles.previewArea}>
<h4></h4>
<div className={styles.messagePreview}>
{messageContent.text && (
<div className={styles.textPreview}>
{messageContent.text}
</div>
)}
{messageContent.images.length > 0 && (
<div className={styles.imagePreview}>
{messageContent.images.map((image, index) => (
<img
key={index}
src={image.url || image.thumbUrl}
alt={`preview-${index}`}
style={{
width: 60,
height: 60,
objectFit: "cover",
margin: 4,
borderRadius: 4,
}}
/>
))}
</div>
)}
</div>
</div>
</Form>
</Card>
);
case 2:
return (
<Card
title={
<>
<SendOutlined />
</>
}
className={styles.stepCard}
>
<Form form={form} layout="vertical">
<Form.Item label="发送模式" name="sendMode">
<Radio.Group
value={sendSettings.sendMode}
onChange={e =>
setSendSettings({
...sendSettings,
sendMode: e.target.value,
})
}
>
<Radio value="immediate"></Radio>
<Radio value="scheduled"></Radio>
</Radio.Group>
</Form.Item>
{sendSettings.sendMode === "scheduled" && (
<Form.Item label="定时时间" name="scheduledTime">
<DatePicker
showTime
placeholder="选择发送时间"
onChange={(date, dateString) =>
setSendSettings({
...sendSettings,
scheduledTime: dateString as string,
})
}
disabledDate={current =>
current && current < dayjs().endOf("day")
}
/>
</Form.Item>
)}
<Form.Item label="发送间隔(秒)" name="sendInterval">
<InputNumber
min={1}
max={60}
value={sendSettings.sendInterval}
onChange={value =>
setSendSettings({
...sendSettings,
sendInterval: value || 5,
})
}
addonAfter="秒"
/>
</Form.Item>
<Form.Item label="每日最大发送数" name="maxPerDay">
<InputNumber
min={1}
max={1000}
value={sendSettings.maxPerDay}
onChange={value =>
setSendSettings({
...sendSettings,
maxPerDay: value || 100,
})
}
addonAfter="条"
/>
</Form.Item>
<Form.Item label="发送时间段" name="timeRange">
<TimePicker.RangePicker
format="HH:mm"
value={
sendSettings.timeRange.map(time =>
time ? dayjs(time, "HH:mm") : null,
) as any
}
onChange={(times, timeStrings) =>
setSendSettings({
...sendSettings,
timeRange: timeStrings as [string, string],
})
}
placeholder={["开始时间", "结束时间"]}
/>
</Form.Item>
<Form.Item label="高级设置">
<Space direction="vertical">
<Checkbox> ()</Checkbox>
<Checkbox> ()</Checkbox>
<Checkbox> ()</Checkbox>
</Space>
</Form.Item>
</Form>
<Divider />
<div className={styles.summary}>
<h4>
<AimOutlined style={{ marginRight: 8 }} />
</h4>
<div className={styles.summaryGrid}>
<div className={styles.summaryItem}>
<TeamOutlined />
<span>
<strong>{estimatedCount}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<ClockCircleOutlined />
<span>
<strong>
{sendSettings.sendMode === "immediate"
? "立即发送"
: `定时发送 (${sendSettings.scheduledTime || "未设置"})`}
</strong>
</span>
</div>
<div className={styles.summaryItem}>
<AimOutlined />
<span>
<strong>{sendSettings.sendInterval}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<TagsOutlined />
<span>
<strong>{sendSettings.maxPerDay}</strong>
</span>
</div>
<div className={styles.summaryItem}>
<ClockCircleOutlined />
<span>
<strong>
{sendSettings.timeRange[0]} - {sendSettings.timeRange[1]}
</strong>
</span>
</div>
</div>
</div>
</Card>
);
default:
return null;
}
};
return (
<Layout
header={
<>
<PowerNavigation
title="精准群发1"
subtitle="基于客户标签和行为数据进行精准群发"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.stepsContainer}>
<Steps current={currentStep} className={styles.steps}>
<Step title="客户筛选" icon={<UserOutlined />} />
<Step title="消息内容" icon={<MessageOutlined />} />
<Step title="发送设置" icon={<SendOutlined />} />
</Steps>
</div>
</>
}
footer={
<div className={styles.stepActions}>
<Space>
{currentStep > 0 && (
<Button size="large" onClick={handlePrev} disabled={loading}>
</Button>
)}
{currentStep < 2 ? (
<Button
type="primary"
size="large"
onClick={() => {
// 简单验证当前步骤
if (currentStep === 0 && estimatedCount === 0) {
message.warning("请设置筛选条件以选择目标客户");
return;
}
if (
currentStep === 1 &&
!messageContent.text &&
messageContent.images.length === 0
) {
message.warning("请输入消息内容或上传图片");
return;
}
handleNext();
}}
disabled={loading}
>
</Button>
) : (
<Button
type="primary"
size="large"
onClick={handleSubmit}
loading={loading}
icon={<SendOutlined />}
>
</Button>
)}
</Space>
</div>
}
>
<div className={styles.container}>
<div className={styles.stepContent}>{renderStepContent()}</div>
</div>
</Layout>
);
};
export default PrecisionSend;

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const SopSend: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="SOP群发"
subtitle="使用触客宝SOP标准化流程进行批量消息发送"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>SOP群发功能正在开发中...</p>
</div>
</div>
</div>
);
};
export default SopSend;

View File

@@ -1,43 +0,0 @@
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
h1 {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
.content {
min-height: 400px;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
background: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 6px;
p {
font-size: 16px;
color: #8c8c8c;
margin: 0;
}
}

View File

@@ -1,24 +0,0 @@
import React from "react";
import PowerNavigation from "@/components/PowerNavtion";
import styles from "./index.module.scss";
const TagManagement: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="标签管理"
subtitle="智能客户标签分类,精准用户画像分析"
showBackButton={true}
backButtonText="返回功能中心"
/>
<div className={styles.content}>
{/* 功能内容待开发 */}
<div className={styles.placeholder}>
<p>...</p>
</div>
</div>
</div>
);
};
export default TagManagement;

View File

@@ -2,10 +2,6 @@ import CkboxPage from "@/pages/pc/ckbox";
import WeChatPage from "@/pages/pc/ckbox/weChat";
import Dashboard from "@/pages/pc/ckbox/dashboard";
import PowerCenter from "@/pages/pc/ckbox/powerCenter";
import PrecisionSend from "@/pages/pc/ckbox/powerCenter/precision-send";
import SopSend from "@/pages/pc/ckbox/powerCenter/sop-send";
import MomentsMarketing from "@/pages/pc/ckbox/powerCenter/moments-marketing";
import TagManagement from "@/pages/pc/ckbox/powerCenter/tag-management";
import CustomerManagement from "@/pages/pc/ckbox/powerCenter/customer-management";
import CommunicationRecord from "@/pages/pc/ckbox/powerCenter/communication-record";
import ContentManagement from "@/pages/pc/ckbox/powerCenter/content-management";
@@ -34,22 +30,6 @@ const ckboxRoutes = [
path: "powerCenter",
element: <PowerCenter />,
},
{
path: "powerCenter/precision-send",
element: <PrecisionSend />,
},
{
path: "powerCenter/sop-send",
element: <SopSend />,
},
{
path: "powerCenter/moments-marketing",
element: <MomentsMarketing />,
},
{
path: "powerCenter/tag-management",
element: <TagManagement />,
},
{
path: "powerCenter/customer-management",
element: <CustomerManagement />,