feat: 本次提交更新内容如下

梳理了icon ,增加了layout的loading
This commit is contained in:
笔记本里的永平
2025-07-19 16:10:26 +08:00
parent 4af615889b
commit b1938a76c0
31 changed files with 3181 additions and 908 deletions

View File

@@ -0,0 +1,250 @@
.scenario-list-page {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.new-plan-btn {
font-size: 14px;
height: 32px;
padding: 0 12px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.scenario-card {
margin-bottom: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.scenario-info {
padding: 16px;
}
.scenario-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.scenario-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
}
.scenario-stats {
display: flex;
gap: 24px;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #666;
svg {
font-size: 16px;
color: #1890ff;
}
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: center;
.adm-input {
flex: 1;
border-radius: 8px;
}
}
.refresh-btn {
height: 40px;
width: 40px;
padding: 0;
border-radius: 8px;
}
.plan-list {
background: transparent;
border-radius: 12px;
overflow: hidden;
}
.plan-item {
background: white;
margin-bottom: 12px;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:last-child {
margin-bottom: 0;
}
}
.plan-content {
flex: 1;
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.plan-name {
font-size: 16px;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 12px;
}
.plan-meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #999;
}
.plan-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-text {
color: #999;
font-size: 14px;
margin-bottom: 20px;
}
.create-first-btn {
height: 40px;
padding: 0 24px;
border-radius: 20px;
}
.api-dialog {
background: white;
border-radius: 16px 16px 0 0;
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.dialog-content {
flex: 1;
overflow-y: auto;
}
.api-item {
margin-bottom: 20px;
label {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.adm-input {
border-radius: 8px;
}
}
.qr-dialog {
background: white;
border-radius: 16px;
padding: 20px;
width: 300px;
max-width: 90vw;
}
.qr-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
gap: 16px;
color: #666;
font-size: 14px;
}
.qr-image {
width: 100%;
max-width: 200px;
height: auto;
border-radius: 8px;
}
.qr-error {
text-align: center;
color: #ff4d4f;
font-size: 14px;
padding: 40px 20px;
}

View File

@@ -1,8 +1,618 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
const ScenarioList: React.FC = () => {
return <PlaceholderPage title="场景列表" />;
};
export default ScenarioList;
import React, { useEffect, useState, useCallback } from "react";
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import {
NavBar,
List,
Button,
Toast,
SpinLoading,
Dialog,
Input,
Popup,
Card,
Tag,
Space,
Divider,
} from "antd-mobile";
import {
PlusOutlined,
UserOutlined,
CalendarOutlined,
CopyOutlined,
DeleteOutlined,
EditOutlined,
SettingOutlined,
SearchOutlined,
ReloadOutlined,
QrcodeOutlined,
UpOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import {
getPlanList,
getPlanDetail,
copyPlan,
deletePlan,
getWxMinAppCode,
} from "./api";
import style from "./ScenarioList.module.scss";
interface Task {
id: string;
name: string;
status: number;
created_at: string;
updated_at: string;
enabled: boolean;
total_customers?: number;
today_customers?: number;
}
interface ScenarioData {
id: string;
name: string;
image: string;
description: string;
totalPlans: number;
totalCustomers: number;
todayCustomers: number;
growth: string;
}
interface ApiSettings {
apiKey: string;
webhookUrl: string;
taskId: string;
}
const ScenarioList: React.FC = () => {
const { scenarioId, scenarioName } = useParams<{
scenarioId: string;
scenarioName: string;
}>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [scenario, setScenario] = useState<ScenarioData | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [showApiDialog, setShowApiDialog] = useState(false);
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
apiKey: "",
webhookUrl: "",
taskId: "",
});
const [searchTerm, setSearchTerm] = useState("");
const [loadingTasks, setLoadingTasks] = useState(false);
const [showQrDialog, setShowQrDialog] = useState(false);
const [qrLoading, setQrLoading] = useState(false);
const [qrImg, setQrImg] = useState("");
// 获取渠道中文名称
const getChannelName = (channel: string) => {
const channelMap: Record<string, string> = {
douyin: "抖音直播获客",
kuaishou: "快手直播获客",
xiaohongshu: "小红书种草获客",
weibo: "微博话题获客",
haibao: "海报扫码获客",
phone: "电话号码获客",
gongzhonghao: "公众号引流获客",
weixinqun: "微信群裂变获客",
payment: "付款码获客",
api: "API接口获客",
};
return channelMap[channel] || `${channel}获客`;
};
// 获取场景描述
const getScenarioDescription = (channel: string) => {
const descriptions: Record<string, string> = {
douyin: "通过抖音平台进行精准获客,利用短视频内容吸引目标用户",
xiaohongshu: "利用小红书平台进行内容营销获客,通过优质内容建立品牌形象",
gongzhonghao: "通过微信公众号进行获客,建立私域流量池",
haibao: "通过海报分享进行获客,快速传播品牌信息",
phone: "通过电话营销进行获客,直接与客户沟通",
weixinqun: "通过微信群进行获客,利用社交裂变效应",
payment: "通过付款码进行获客,便捷的支付方式",
api: "通过API接口进行获客支持第三方系统集成",
};
return descriptions[channel] || "通过该平台进行获客";
};
useEffect(() => {
const fetchScenarioData = async () => {
if (!scenarioId) return;
setLoading(true);
setError("");
try {
// 获取计划列表
const response = await getPlanList(scenarioId, 1, 20);
// 设置计划列表
if (response && response.data && response.data.list) {
setTasks(response.data.list);
} else {
setTasks([]);
}
// 构建场景数据
const scenarioData: ScenarioData = {
id: scenarioId,
name: scenarioName || "",
image: "",
description: getScenarioDescription(scenarioId),
totalPlans: response?.data?.list?.length || 0,
totalCustomers: 0,
todayCustomers: 0,
growth: "",
};
setScenario(scenarioData);
} catch (error) {
console.error("获取场景数据失败:", error);
// 即使API失败也要创建基本的场景数据
const scenarioData: ScenarioData = {
id: scenarioId,
name: getScenarioName(),
image: "",
description: getScenarioDescription(scenarioId),
totalPlans: 0,
totalCustomers: 0,
todayCustomers: 0,
growth: "",
};
setScenario(scenarioData);
setTasks([]);
} finally {
setLoading(false);
}
};
fetchScenarioData();
}, [scenarioId]);
// 获取场景名称
const getScenarioName = useCallback(() => {
const urlName = searchParams.get("name");
if (urlName) {
return urlName;
}
return getChannelName(scenarioId || "");
}, [searchParams, scenarioId]);
// 更新场景数据中的名称
useEffect(() => {
setScenario((prev) =>
prev
? {
...prev,
name: (() => {
const urlName = searchParams.get("name");
if (urlName) return urlName;
return getChannelName(scenarioId || "");
})(),
}
: null
);
}, [searchParams, scenarioId]);
const handleCopyPlan = async (taskId: string) => {
const taskToCopy = tasks.find((task) => task.id === taskId);
if (!taskToCopy) return;
try {
const response = await copyPlan(taskId);
if (response && response.code === 200) {
Toast.show({
content: `已成功复制"${taskToCopy.name}"`,
position: "top",
});
// 刷新列表
handleRefresh();
} else {
Toast.show({
content: response?.msg || "复制失败",
position: "top",
});
}
} catch (error) {
Toast.show({
content: "复制失败,请重试",
position: "top",
});
}
};
const handleDeletePlan = async (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId);
if (!taskToDelete) return;
const result = await Dialog.confirm({
content: `确定要删除"${taskToDelete.name}"吗?`,
confirmText: "删除",
cancelText: "取消",
});
if (result) {
try {
const response = await deletePlan(taskId);
if (response && response.code === 200) {
Toast.show({
content: "计划已删除",
position: "top",
});
// 刷新列表
handleRefresh();
} else {
Toast.show({
content: response?.msg || "删除失败",
position: "top",
});
}
} catch (error) {
Toast.show({
content: "删除失败,请重试",
position: "top",
});
}
}
};
const handleOpenApiSettings = async (taskId: string) => {
try {
const response = await getPlanDetail(taskId);
if (response && response.code === 200 && response.data) {
const detail = response.data;
setCurrentApiSettings({
apiKey: detail.apiKey || "",
webhookUrl: detail.webhookUrl || "",
taskId: taskId,
});
setShowApiDialog(true);
}
} catch (error) {
Toast.show({
content: "获取API设置失败",
position: "top",
});
}
};
const handleCopyApiUrl = (url: string) => {
navigator.clipboard.writeText(url);
Toast.show({
content: "API地址已复制到剪贴板",
position: "top",
});
};
const handleCreateNewPlan = () => {
navigate(`/scenarios/new/${scenarioId}`);
};
const handleShowQrCode = async (taskId: string) => {
setQrLoading(true);
setShowQrDialog(true);
try {
const response = await getWxMinAppCode(taskId);
if (response && response.code === 200 && response.data) {
setQrImg(response.data.qrCode || "");
} else {
Toast.show({
content: "获取二维码失败",
position: "top",
});
}
} catch (error) {
Toast.show({
content: "获取二维码失败",
position: "top",
});
} finally {
setQrLoading(false);
}
};
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return "success";
case 0:
return "default";
case -1:
return "danger";
default:
return "default";
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "运行中";
case 0:
return "已暂停";
case -1:
return "已停止";
default:
return "未知";
}
};
const handleRefresh = async () => {
setLoadingTasks(true);
try {
const response = await getPlanList(scenarioId!, 1, 20);
if (response && response.data && response.data.list) {
setTasks(response.data.list);
}
} catch (error) {
Toast.show({
content: "刷新失败",
position: "top",
});
} finally {
setLoadingTasks(false);
}
};
const filteredTasks = tasks.filter((task) =>
task.name.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) {
return (
<Layout
header={
<NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}></div>
</NavBar>
}
footer={<MeauMobile />}
>
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
<div className={style["loading-text"]}>...</div>
</div>
</Layout>
);
}
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={<div className={style["nav-title"]}>{scenario?.name}</div>}
right={
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
className={style["new-plan-btn"]}
>
<PlusOutlined />
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div className={style["scenario-list-page"]}>
{/* 场景信息卡片 */}
{scenario && (
<Card className={style["scenario-card"]}>
<div className={style["scenario-info"]}>
<div className={style["scenario-name"]}>{scenario.name}</div>
<div className={style["scenario-desc"]}>
{scenario.description}
</div>
<div className={style["scenario-stats"]}>
<div className={style["stat-item"]}>
<UserOutlined />
<span>: {scenario.totalPlans}</span>
</div>
<div className={style["stat-item"]}>
<CalendarOutlined />
<span>: {scenario.todayCustomers}</span>
</div>
</div>
</div>
</Card>
)}
{/* 搜索栏 */}
<div className={style["search-bar"]}>
<div className={style["search-input-wrapper"]}>
<SearchOutlined className={style["search-icon"]} />
<Input
placeholder="搜索计划名称"
value={searchTerm}
onChange={setSearchTerm}
clearable
/>
</div>
<Button
size="small"
onClick={handleRefresh}
loading={loadingTasks}
className={style["refresh-btn"]}
>
<ReloadOutlined />
</Button>
</div>
{/* 计划列表 */}
<List className={style["plan-list"]}>
{filteredTasks.length === 0 ? (
<div className={style["empty-state"]}>
<div className={style["empty-text"]}>
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
</div>
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
</div>
) : (
filteredTasks.map((task) => (
<List.Item
key={task.id}
className={style["plan-item"]}
onClick={() => navigate(`/scenarios/edit/${task.id}`)}
>
<div className={style["plan-content"]}>
<div className={style["plan-header"]}>
<div className={style["plan-name"]}>{task.name}</div>
<Tag color={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Tag>
</div>
<div className={style["plan-meta"]}>
<span>: {task.created_at}</span>
{task.total_customers !== undefined && (
<span>: {task.total_customers}</span>
)}
{task.today_customers !== undefined && (
<span>: {task.today_customers}</span>
)}
</div>
</div>
<div className={style["plan-actions"]}>
<Space>
<Button
size="mini"
onClick={(e) => {
e.stopPropagation();
handleCopyPlan(task.id);
}}
>
<CopyOutlined />
</Button>
<Button
size="mini"
onClick={(e) => {
e.stopPropagation();
handleShowQrCode(task.id);
}}
>
<QrcodeOutlined />
</Button>
<Button
size="mini"
onClick={(e) => {
e.stopPropagation();
handleOpenApiSettings(task.id);
}}
>
<SettingOutlined />
</Button>
<Button
size="mini"
color="danger"
onClick={(e) => {
e.stopPropagation();
handleDeletePlan(task.id);
}}
>
<DeleteOutlined />
</Button>
</Space>
</div>
</List.Item>
))
)}
</List>
{/* API设置弹窗 */}
<Popup
visible={showApiDialog}
onMaskClick={() => setShowApiDialog(false)}
position="bottom"
bodyStyle={{ height: "60vh" }}
>
<div className={style["api-dialog"]}>
<div className={style["dialog-header"]}>
<h3>API设置</h3>
<Button size="small" onClick={() => setShowApiDialog(false)}>
</Button>
</div>
<div className={style["dialog-content"]}>
<div className={style["api-item"]}>
<label>API Key:</label>
<div className={style["input-with-button"]}>
<Input value={currentApiSettings.apiKey} readOnly />
<Button
size="mini"
onClick={() => handleCopyApiUrl(currentApiSettings.apiKey)}
>
</Button>
</div>
</div>
<div className={style["api-item"]}>
<label>Webhook URL:</label>
<div className={style["input-with-button"]}>
<Input value={currentApiSettings.webhookUrl} readOnly />
<Button
size="mini"
onClick={() =>
handleCopyApiUrl(currentApiSettings.webhookUrl)
}
>
</Button>
</div>
</div>
</div>
</div>
</Popup>
{/* 二维码弹窗 */}
<Popup
visible={showQrDialog}
onMaskClick={() => setShowQrDialog(false)}
position="bottom"
>
<div className={style["qr-dialog"]}>
<div className={style["dialog-header"]}>
<h3></h3>
<Button size="small" onClick={() => setShowQrDialog(false)}>
</Button>
</div>
<div className={style["dialog-content"]}>
{qrLoading ? (
<div className={style["qr-loading"]}>
<SpinLoading color="primary" />
<div>...</div>
</div>
) : qrImg ? (
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
) : (
<div className={style["qr-error"]}></div>
)}
</div>
</div>
</Popup>
</div>
</Layout>
);
};
export default ScenarioList;

View File

@@ -0,0 +1,51 @@
import request from "@/api/request";
// 获取计划列表
export const getPlanList = async (scenarioId: string, page: number = 1, limit: number = 20) => {
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, 'GET');
};
// 获取计划详情
export const getPlanDetail = async (planId: string) => {
return request(`/api/scenarios/plans/${planId}`, undefined, 'GET');
};
// 复制计划
export const copyPlan = async (planId: string) => {
return request(`/api/scenarios/plans/${planId}/copy`, undefined, 'POST');
};
// 删除计划
export const deletePlan = async (planId: string) => {
return request(`/api/scenarios/plans/${planId}`, undefined, 'DELETE');
};
// 创建计划
export const createPlan = async (data: any) => {
return request('/api/scenarios/plans', data, 'POST');
};
// 更新计划
export const updatePlan = async (planId: string, data: any) => {
return request(`/api/scenarios/plans/${planId}`, data, 'PUT');
};
// 获取小程序二维码
export const getWxMinAppCode = async (planId: string) => {
return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, 'GET');
};
// 获取场景类型列表
export const getScenarioTypes = async () => {
return request('/api/scenarios/types', undefined, 'GET');
};
// 获取设备列表
export const getDevices = async () => {
return request('/api/devices', undefined, 'GET');
};
// 获取海报列表
export const getPosters = async () => {
return request('/api/posters', undefined, 'GET');
};

View File

@@ -0,0 +1,43 @@
.new-plan-page {
background: #f5f5f5;
min-height: 100vh;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.back-btn {
height: 32px;
width: 32px;
padding: 0;
border-radius: 50%;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.steps-container {
background: white;
padding: 20px 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.step-content {
flex: 1;
padding: 0 16px;
}

View File

@@ -1,8 +1,293 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
const NewPlan: React.FC = () => {
return <PlaceholderPage title="新建场景" />;
};
export default NewPlan;
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile";
import { LeftOutline } from "antd-mobile-icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import BasicSettings from "./steps/BasicSettings";
import FriendRequestSettings from "./steps/FriendRequestSettings";
import MessageSettings from "./steps/MessageSettings";
import {
getScenarioTypes,
createPlan,
updatePlan,
getPlanDetail,
} from "../api";
import style from "./page.module.scss";
// 步骤定义
const steps = [
{ id: 1, title: "步骤一", subtitle: "基础设置" },
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
{ id: 3, title: "步骤三", subtitle: "消息设置" },
];
// 类型定义
interface FormData {
name: string;
scenario: number;
posters: any[];
device: string[];
remarkType: string;
greeting: string;
addInterval: number;
startTime: string;
endTime: string;
enabled: boolean;
sceneId: string | number;
remarkFormat: string;
addFriendInterval: number;
}
const NewPlan: React.FC = () => {
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>({
name: "",
scenario: 1,
posters: [],
device: [],
remarkType: "phone",
greeting: "你好,请通过",
addInterval: 1,
startTime: "09:00",
endTime: "18:00",
enabled: true,
sceneId: "",
remarkFormat: "",
addFriendInterval: 1,
});
const [sceneList, setSceneList] = useState<any[]>([]);
const [sceneLoading, setSceneLoading] = useState(true);
const { scenarioId, planId } = useParams<{
scenarioId: string;
planId: string;
}>();
const [isEdit, setIsEdit] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setSceneLoading(true);
try {
// 获取场景类型
const res = await getScenarioTypes();
if (res?.data) {
setSceneList(res.data);
}
if (planId) {
setIsEdit(true);
// 获取计划详情
const detailRes = await getPlanDetail(planId);
if (detailRes.code === 200 && detailRes.data) {
const detail = detailRes.data;
setFormData((prev) => ({
...prev,
name: detail.name ?? "",
scenario: Number(detail.scenario) || 1,
posters: detail.posters ?? [],
device: detail.device ?? [],
remarkType: detail.remarkType ?? "phone",
greeting: detail.greeting ?? "",
addInterval: detail.addInterval ?? 1,
startTime: detail.startTime ?? "09:00",
endTime: detail.endTime ?? "18:00",
enabled: detail.enabled ?? true,
sceneId: Number(detail.scenario) || 1,
remarkFormat: detail.remarkFormat ?? "",
addFriendInterval: detail.addFriendInterval ?? 1,
}));
}
} else if (scenarioId) {
setFormData((prev) => ({
...prev,
scenario: Number(scenarioId) || 1,
}));
}
} catch (error) {
Toast.show({
content: "加载数据失败",
position: "top",
});
} finally {
setSceneLoading(false);
}
};
// 更新表单数据
const onChange = (data: any) => {
setFormData((prev) => ({ ...prev, ...data }));
};
// 处理保存
const handleSave = async () => {
if (!formData.name.trim()) {
Toast.show({
content: "请输入计划名称",
position: "top",
});
return;
}
setSaving(true);
try {
let result;
if (isEdit && planId) {
// 编辑
const editData = {
...formData,
id: Number(planId),
planId: Number(planId),
};
result = await updatePlan(planId, editData);
} else {
// 新建
result = await createPlan(formData);
}
if (result.code === 200) {
Toast.show({
content: isEdit ? "计划已更新" : "获客计划已创建",
position: "top",
});
const sceneItem = sceneList.find((v) => formData.scenario === v.id);
navigate(
`/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}`
);
} else {
Toast.show({
content: result.msg || "操作失败",
position: "top",
});
}
} catch (error) {
Toast.show({
content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试",
position: "top",
});
} finally {
setSaving(false);
}
};
// 下一步
const handleNext = () => {
if (currentStep === steps.length) {
handleSave();
} else {
setCurrentStep((prev) => prev + 1);
}
};
// 上一步
const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
// 渲染当前步骤内容
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<BasicSettings
formData={formData}
onChange={onChange}
onNext={handleNext}
sceneList={sceneList}
sceneLoading={sceneLoading}
/>
);
case 2:
return (
<FriendRequestSettings
formData={formData}
onChange={onChange}
onNext={handleNext}
onPrev={handlePrev}
/>
);
case 3:
return (
<MessageSettings
formData={formData}
onChange={onChange}
onNext={handleNext}
onPrev={handlePrev}
saving={saving}
/>
);
default:
return null;
}
};
if (sceneLoading) {
return (
<Layout
header={
<NavBar back={null} style={{ background: "#fff" }}>
<div className={style["nav-title"]}>
{isEdit ? "编辑计划" : "新建计划"}
</div>
</NavBar>
}
footer={<MeauMobile />}
>
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
<div className={style["loading-text"]}>...</div>
</div>
</Layout>
);
}
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div className={style["nav-title"]}>
{isEdit ? "编辑计划" : "新建计划"}
</div>
}
right={
<Button
size="small"
onClick={() => navigate(-1)}
className={style["back-btn"]}
>
<LeftOutline />
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div className={style["new-plan-page"]}>
{/* 步骤指示器 */}
<div className={style["steps-container"]}>
<Steps current={currentStep - 1}>
{steps.map((step) => (
<Steps.Step
key={step.id}
title={step.title}
description={step.subtitle}
/>
))}
</Steps>
</div>
{/* 步骤内容 */}
<div className={style["step-content"]}>{renderStepContent()}</div>
</div>
</Layout>
);
};
export default NewPlan;

View File

@@ -0,0 +1,63 @@
.basic-settings {
padding: 16px 0;
}
.form-card {
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.adm-form-item-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.adm-input {
border-radius: 8px;
}
.adm-selector {
border-radius: 8px;
}
}
.time-input {
width: 120px;
border-radius: 8px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 40vh;
gap: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.actions {
padding: 20px 0;
}
.next-btn {
width: 100%;
height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}

View File

@@ -0,0 +1,210 @@
import React, { useState, useEffect } from "react";
import {
Form,
Input,
Selector,
Button,
SpinLoading,
Toast,
Card,
Space,
} from "antd-mobile";
import { getDevices, getPosters } from "../../api";
import style from "./BasicSettings.module.scss";
interface BasicSettingsProps {
formData: any;
onChange: (data: any) => void;
onNext: () => void;
sceneList: any[];
sceneLoading: boolean;
}
const BasicSettings: React.FC<BasicSettingsProps> = ({
formData,
onChange,
onNext,
sceneList,
sceneLoading,
}) => {
const [devices, setDevices] = useState<any[]>([]);
const [posters, setPosters] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
// 获取设备列表
const devicesRes = await getDevices();
if (devicesRes?.data) {
setDevices(devicesRes.data);
}
// 获取海报列表
const postersRes = await getPosters();
if (postersRes?.data) {
setPosters(postersRes.data);
}
} catch (error) {
Toast.show({
content: "加载数据失败",
position: "top",
});
} finally {
setLoading(false);
}
};
const handleNext = () => {
if (!formData.name.trim()) {
Toast.show({
content: "请输入计划名称",
position: "top",
});
return;
}
if (!formData.scenario) {
Toast.show({
content: "请选择场景类型",
position: "top",
});
return;
}
if (formData.device.length === 0) {
Toast.show({
content: "请选择设备",
position: "top",
});
return;
}
onNext();
};
if (loading || sceneLoading) {
return (
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
<div className={style["loading-text"]}>...</div>
</div>
);
}
return (
<div className={style["basic-settings"]}>
<Card className={style["form-card"]}>
<Form layout="vertical">
{/* 计划名称 */}
<Form.Item label="计划名称" required className={style["form-item"]}>
<Input
placeholder="请输入计划名称"
value={formData.name}
onChange={(value) => onChange({ name: value })}
clearable
/>
</Form.Item>
{/* 场景类型 */}
<Form.Item label="场景类型" required className={style["form-item"]}>
<Selector
options={sceneList.map((scene) => ({
label: scene.name,
value: scene.id,
}))}
value={[formData.scenario]}
onChange={(value) => {
const selectedScene = sceneList.find(
(scene) => scene.id === value[0]
);
onChange({
scenario: value[0],
sceneId: value[0],
name: selectedScene?.name || "",
});
}}
/>
</Form.Item>
{/* 选择设备 */}
<Form.Item label="选择设备" required className={style["form-item"]}>
<Selector
options={devices.map((device) => ({
label: device.name,
value: device.id,
}))}
value={formData.device}
onChange={(value) => onChange({ device: value })}
multiple
/>
</Form.Item>
{/* 选择海报 */}
<Form.Item label="选择海报" className={style["form-item"]}>
<Selector
options={posters.map((poster) => ({
label: poster.name,
value: poster.id,
}))}
value={formData.posters}
onChange={(value) => onChange({ posters: value })}
multiple
/>
</Form.Item>
{/* 工作时间 */}
<Form.Item label="工作时间" className={style["form-item"]}>
<Space>
<Input
type="time"
value={formData.startTime}
onChange={(value) => onChange({ startTime: value })}
className={style["time-input"]}
/>
<span></span>
<Input
type="time"
value={formData.endTime}
onChange={(value) => onChange({ endTime: value })}
className={style["time-input"]}
/>
</Space>
</Form.Item>
{/* 添加间隔 */}
<Form.Item label="添加间隔(分钟)" className={style["form-item"]}>
<Input
type="number"
placeholder="请输入添加间隔"
value={formData.addInterval.toString()}
onChange={(value) =>
onChange({ addInterval: Number(value) || 1 })
}
min={1}
max={60}
/>
</Form.Item>
</Form>
</Card>
{/* 操作按钮 */}
<div className={style["actions"]}>
<Button
color="primary"
size="large"
onClick={handleNext}
className={style["next-btn"]}
>
</Button>
</div>
</div>
);
};
export default BasicSettings;

View File

@@ -0,0 +1,56 @@
.friend-request-settings {
padding: 16px 0;
}
.form-card {
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.adm-form-item-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.adm-input {
border-radius: 8px;
}
.adm-selector {
border-radius: 8px;
}
.adm-text-area {
border-radius: 8px;
}
}
.actions {
padding: 20px 0;
}
.prev-btn {
flex: 1;
height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}
.next-btn {
flex: 1;
height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}

View File

@@ -0,0 +1,113 @@
import React from "react";
import {
Form,
Input,
Selector,
Button,
Card,
Space,
TextArea,
} from "antd-mobile";
import style from "./FriendRequestSettings.module.scss";
interface FriendRequestSettingsProps {
formData: any;
onChange: (data: any) => void;
onNext: () => void;
onPrev: () => void;
}
const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
formData,
onChange,
onNext,
onPrev,
}) => {
const remarkTypeOptions = [
{ label: "手机号", value: "phone" },
{ label: "微信号", value: "wechat" },
{ label: "QQ号", value: "qq" },
{ label: "自定义", value: "custom" },
];
const handleNext = () => {
if (!formData.greeting.trim()) {
// 可以添加验证逻辑
}
onNext();
};
return (
<div className={style["friend-request-settings"]}>
<Card className={style["form-card"]}>
<Form layout="vertical">
{/* 备注类型 */}
<Form.Item label="备注类型" required className={style["form-item"]}>
<Selector
options={remarkTypeOptions}
value={[formData.remarkType]}
onChange={(value) => onChange({ remarkType: value[0] })}
/>
</Form.Item>
{/* 备注格式 */}
{formData.remarkType === "custom" && (
<Form.Item label="备注格式" required className={style["form-item"]}>
<Input
placeholder="请输入备注格式,如:{name}-{phone}"
value={formData.remarkFormat}
onChange={(value) => onChange({ remarkFormat: value })}
clearable
/>
</Form.Item>
)}
{/* 打招呼消息 */}
<Form.Item label="打招呼消息" required className={style["form-item"]}>
<TextArea
placeholder="请输入打招呼消息"
value={formData.greeting}
onChange={(value) => onChange({ greeting: value })}
rows={4}
maxLength={200}
showCount
/>
</Form.Item>
{/* 好友申请间隔 */}
<Form.Item label="好友申请间隔(分钟)" className={style["form-item"]}>
<Input
type="number"
placeholder="请输入好友申请间隔"
value={formData.addFriendInterval.toString()}
onChange={(value) =>
onChange({ addFriendInterval: Number(value) || 1 })
}
min={1}
max={60}
/>
</Form.Item>
</Form>
</Card>
{/* 操作按钮 */}
<div className={style["actions"]}>
<Space style={{ width: "100%" }}>
<Button size="large" onClick={onPrev} className={style["prev-btn"]}>
</Button>
<Button
color="primary"
size="large"
onClick={handleNext}
className={style["next-btn"]}
>
</Button>
</Space>
</div>
</div>
);
};
export default FriendRequestSettings;

View File

@@ -0,0 +1,64 @@
.message-settings {
padding: 16px 0;
}
.form-card {
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.adm-form-item-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.adm-input {
border-radius: 8px;
}
.adm-text-area {
border-radius: 8px;
}
}
.switch-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
span {
font-size: 14px;
color: #333;
}
}
.actions {
padding: 20px 0;
}
.prev-btn {
flex: 1;
height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}
.save-btn {
flex: 1;
height: 48px;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}

View File

@@ -0,0 +1,140 @@
import React from "react";
import {
Form,
Input,
Selector,
Button,
Card,
Space,
TextArea,
Switch,
} from "antd-mobile";
import style from "./MessageSettings.module.scss";
interface MessageSettingsProps {
formData: any;
onChange: (data: any) => void;
onNext: () => void;
onPrev: () => void;
saving: boolean;
}
const MessageSettings: React.FC<MessageSettingsProps> = ({
formData,
onChange,
onNext,
onPrev,
saving,
}) => {
const handleSave = () => {
onNext();
};
return (
<div className={style["message-settings"]}>
<Card className={style["form-card"]}>
<Form layout="vertical">
{/* 启用状态 */}
<Form.Item label="启用状态" className={style["form-item"]}>
<div className={style["switch-item"]}>
<span></span>
<Switch
checked={formData.enabled}
onChange={(checked) => onChange({ enabled: checked })}
/>
</div>
</Form.Item>
{/* 自动回复消息 */}
<Form.Item label="自动回复消息" className={style["form-item"]}>
<TextArea
placeholder="请输入自动回复消息"
value={formData.autoReply || ""}
onChange={(value) => onChange({ autoReply: value })}
rows={4}
maxLength={500}
showCount
/>
</Form.Item>
{/* 关键词回复 */}
<Form.Item label="关键词回复" className={style["form-item"]}>
<TextArea
placeholder="请输入关键词回复规则,格式:关键词=回复内容"
value={formData.keywordReply || ""}
onChange={(value) => onChange({ keywordReply: value })}
rows={4}
maxLength={1000}
showCount
/>
</Form.Item>
{/* 群发消息 */}
<Form.Item label="群发消息" className={style["form-item"]}>
<TextArea
placeholder="请输入群发消息内容"
value={formData.groupMessage || ""}
onChange={(value) => onChange({ groupMessage: value })}
rows={4}
maxLength={500}
showCount
/>
</Form.Item>
{/* 消息发送间隔 */}
<Form.Item label="消息发送间隔(秒)" className={style["form-item"]}>
<Input
type="number"
placeholder="请输入消息发送间隔"
value={formData.messageInterval?.toString() || "5"}
onChange={(value) =>
onChange({ messageInterval: Number(value) || 5 })
}
min={1}
max={300}
/>
</Form.Item>
{/* 每日发送限制 */}
<Form.Item label="每日发送限制" className={style["form-item"]}>
<Input
type="number"
placeholder="请输入每日发送限制数量"
value={formData.dailyLimit?.toString() || "100"}
onChange={(value) =>
onChange({ dailyLimit: Number(value) || 100 })
}
min={1}
max={1000}
/>
</Form.Item>
</Form>
</Card>
{/* 操作按钮 */}
<div className={style["actions"]}>
<Space style={{ width: "100%" }}>
<Button
size="large"
onClick={onPrev}
className={style["prev-btn"]}
disabled={saving}
>
</Button>
<Button
color="primary"
size="large"
onClick={handleSave}
className={style["save-btn"]}
loading={saving}
>
{saving ? "保存中..." : "保存计划"}
</Button>
</Space>
</div>
</div>
);
};
export default MessageSettings;

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { NavBar, Button, Toast, SpinLoading } from "antd-mobile";
import { AddOutline } from "antd-mobile-icons";
import { RiseOutlined } from "@ant-design/icons";
import { PlusOutlined, UpOutlined } from "@ant-design/icons";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import Layout from "@/components/Layout/Layout";
import { getScenarios } from "./api";
@@ -91,7 +90,7 @@ const Scene: React.FC = () => {
className={style["new-plan-btn"]}
style={{ marginLeft: "auto" }}
>
<AddOutline />
<PlusOutlined />
</Button>
</NavBar>
}
@@ -118,7 +117,7 @@ const Scene: React.FC = () => {
className={style["new-plan-btn"]}
style={{ marginLeft: "auto" }}
>
<AddOutline />
<PlusOutlined />
</Button>
</NavBar>
}
@@ -149,7 +148,7 @@ const Scene: React.FC = () => {
className={style["new-plan-btn"]}
style={{ marginLeft: "auto" }}
>
<AddOutline />
<PlusOutlined />
</Button>
}
></NavBar>
@@ -189,7 +188,7 @@ const Scene: React.FC = () => {
: {scenario.count}
</span>
<span className={style["card-growth"]}>
<RiseOutlined
<UpOutlined
style={{ fontSize: 14, color: "#52c41a", marginRight: 2 }}
/>
{scenario.growth}

View File

@@ -1,37 +1,37 @@
import React from "react";
import { NavBar, Button } from "antd-mobile";
import { AddOutline } from "antd-mobile-icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const WechatAccounts: React.FC = () => {
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
}
right={
<Button size="small" color="primary">
<AddOutline />
<span style={{ marginLeft: 4, fontSize: 12 }}></span>
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default WechatAccounts;
import React from "react";
import { NavBar, Button } from "antd-mobile";
import { PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const WechatAccounts: React.FC = () => {
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
}
right={
<Button size="small" color="primary">
<PlusOutlined />
<span style={{ marginLeft: 4, fontSize: 12 }}></span>
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default WechatAccounts;

View File

@@ -2,15 +2,15 @@ import React from "react";
import { Link } from "react-router-dom";
import { Card, NavBar, Badge } from "antd-mobile";
import {
LikeOutline,
MessageOutline,
SendOutline,
TeamOutline,
LinkOutline,
AppOutline,
PieOutline,
ClockCircleOutline,
} from "antd-mobile-icons";
LikeOutlined,
MessageOutlined,
SendOutlined,
TeamOutlined,
LinkOutlined,
AppstoreOutlined,
PieChartOutlined,
ClockCircleOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import styles from "./Workspace.module.scss";
@@ -32,7 +32,7 @@ const Workspace: React.FC = () => {
name: "自动点赞",
description: "智能自动点赞互动",
icon: (
<LikeOutline className={styles.icon} style={{ color: "#ff4d4f" }} />
<LikeOutlined className={styles.icon} style={{ color: "#ff4d4f" }} />
),
path: "/workspace/auto-like",
bgColor: "#fff2f0",
@@ -43,7 +43,7 @@ const Workspace: React.FC = () => {
name: "朋友圈同步",
description: "自动同步朋友圈内容",
icon: (
<ClockCircleOutline
<ClockCircleOutlined
className={styles.icon}
style={{ color: "#722ed1" }}
/>
@@ -56,7 +56,7 @@ const Workspace: React.FC = () => {
name: "群消息推送",
description: "智能群发助手",
icon: (
<SendOutline className={styles.icon} style={{ color: "#fa8c16" }} />
<SendOutlined className={styles.icon} style={{ color: "#fa8c16" }} />
),
path: "/workspace/group-push",
bgColor: "#fff7e6",
@@ -66,7 +66,7 @@ const Workspace: React.FC = () => {
name: "自动建群",
description: "智能拉好友建群",
icon: (
<TeamOutline className={styles.icon} style={{ color: "#52c41a" }} />
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
),
path: "/workspace/auto-group",
bgColor: "#f6ffed",
@@ -76,7 +76,7 @@ const Workspace: React.FC = () => {
name: "流量分发",
description: "管理流量分发和分配",
icon: (
<LinkOutline className={styles.icon} style={{ color: "#1890ff" }} />
<LinkOutlined className={styles.icon} style={{ color: "#1890ff" }} />
),
path: "/workspace/traffic-distribution",
bgColor: "#e6f7ff",
@@ -86,7 +86,7 @@ const Workspace: React.FC = () => {
name: "AI对话助手",
description: "智能回复,提高互动质量",
icon: (
<MessageOutline className={styles.icon} style={{ color: "#1890ff" }} />
<MessageOutlined className={styles.icon} style={{ color: "#1890ff" }} />
),
path: "/workspace/ai-assistant",
bgColor: "#e6f7ff",
@@ -100,7 +100,12 @@ const Workspace: React.FC = () => {
id: "ai-analyzer",
name: "AI数据分析",
description: "智能分析客户行为特征",
icon: <PieOutline className={styles.icon} style={{ color: "#531dab" }} />,
icon: (
<PieChartOutlined
className={styles.icon}
style={{ color: "#531dab" }}
/>
),
path: "/workspace/ai-analyzer",
bgColor: "#f0f0ff",
isNew: true,
@@ -109,7 +114,12 @@ const Workspace: React.FC = () => {
id: "ai-strategy",
name: "AI策略优化",
description: "智能优化获客策略",
icon: <AppOutline className={styles.icon} style={{ color: "#13c2c2" }} />,
icon: (
<AppstoreOutlined
className={styles.icon}
style={{ color: "#13c2c2" }}
/>
),
path: "/workspace/ai-strategy",
bgColor: "#e6fffb",
isNew: true,
@@ -118,7 +128,12 @@ const Workspace: React.FC = () => {
id: "ai-forecast",
name: "AI销售预测",
description: "智能预测销售趋势",
icon: <PieOutline className={styles.icon} style={{ color: "#d48806" }} />,
icon: (
<PieChartOutlined
className={styles.icon}
style={{ color: "#d48806" }}
/>
),
path: "/workspace/ai-forecast",
bgColor: "#fffbe6",
},

View File

@@ -1,38 +1,38 @@
import React from "react";
import { NavBar, Button } from "antd-mobile";
import { AddOutline } from "antd-mobile-icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const AutoLike: React.FC = () => {
return (
<Layout
header={
<NavBar
backArrow
style={{ background: "#fff" }}
onBack={() => window.history.back()}
left={
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
}
right={
<Button size="small" color="primary">
<AddOutline />
<span style={{ marginLeft: 4, fontSize: 12 }}></span>
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default AutoLike;
import React from "react";
import { NavBar, Button } from "antd-mobile";
import { PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const AutoLike: React.FC = () => {
return (
<Layout
header={
<NavBar
backArrow
style={{ background: "#fff" }}
onBack={() => window.history.back()}
left={
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
}
right={
<Button size="small" color="primary">
<PlusOutlined />
<span style={{ marginLeft: 4, fontSize: 12 }}></span>
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default AutoLike;

View File

@@ -1,12 +1,12 @@
import React, { useState } from "react";
import { Card, NavBar, TabBar, Grid } from "antd-mobile";
import {
AppOutline,
UserOutline,
PieOutline, // 替换 BarChartOutline
ShopbagOutline,
BellOutline,
} from "antd-mobile-icons";
AppstoreOutlined,
UserOutlined,
PieChartOutlined,
ShoppingOutlined,
BellOutlined,
} from "@ant-design/icons";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
@@ -14,22 +14,22 @@ const sceneStats = [
{
label: "公众号获客",
value: 234,
icon: <ShopbagOutline style={{ fontSize: 28, color: "#4caf50" }} />,
icon: <ShoppingOutlined style={{ fontSize: 28, color: "#4caf50" }} />,
},
{
label: "海报获客",
value: 167,
icon: <AppOutline style={{ fontSize: 28, color: "#ff9800" }} />,
icon: <AppstoreOutlined style={{ fontSize: 28, color: "#ff9800" }} />,
},
{
label: "抖音获客",
value: 156,
icon: <PieOutline style={{ fontSize: 28, color: "#2196f3" }} />,
icon: <PieChartOutlined style={{ fontSize: 28, color: "#2196f3" }} />,
},
{
label: "小红书获客",
value: 89,
icon: <UserOutline style={{ fontSize: 28, color: "#e91e63" }} />,
icon: <UserOutlined style={{ fontSize: 28, color: "#e91e63" }} />,
},
];