feat: 本次提交更新内容如下
存一版
This commit is contained in:
10
nkebao/src/components/AccountSelection/api.ts
Normal file
10
nkebao/src/components/AccountSelection/api.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取好友列表
|
||||
export function getAccountList(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return request("/v1/workbench/account-list", params, "GET");
|
||||
}
|
||||
231
nkebao/src/components/AccountSelection/index.module.scss
Normal file
231
nkebao/src/components/AccountSelection/index.module.scss
Normal file
@@ -0,0 +1,231 @@
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
}
|
||||
.inputIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #bdbdbd;
|
||||
font-size: 20px;
|
||||
}
|
||||
.input {
|
||||
padding-left: 38px !important;
|
||||
height: 48px;
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid #e5e6eb !important;
|
||||
font-size: 16px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.popupContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
.popupHeader {
|
||||
padding: 24px;
|
||||
}
|
||||
.popupTitle {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.searchWrapper {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.searchInput {
|
||||
padding-left: 40px !important;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
border-radius: 24px !important;
|
||||
border: 1px solid #e5e6eb !important;
|
||||
font-size: 15px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #bdbdbd;
|
||||
font-size: 16px;
|
||||
}
|
||||
.clearBtn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 50%;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.friendList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.friendListInner {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.friendItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: #f5f6fa;
|
||||
}
|
||||
}
|
||||
.radioWrapper {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioSelected {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioUnselected {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e5e6eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.radioDot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
}
|
||||
.friendInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.friendAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatarImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.friendDetail {
|
||||
flex: 1;
|
||||
}
|
||||
.friendName {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.friendId {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.friendCustomer {
|
||||
font-size: 13px;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
.loadingBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.loadingText {
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
}
|
||||
.emptyBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.emptyText {
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.paginationRow {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
}
|
||||
.totalCount {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
.paginationControls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.pageBtn {
|
||||
padding: 0 8px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
}
|
||||
.pageInfo {
|
||||
font-size: 14px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.popupFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
}
|
||||
.selectedCount {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
.footerBtnGroup {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.cancelBtn {
|
||||
padding: 0 24px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid #e5e6eb;
|
||||
}
|
||||
.confirmBtn {
|
||||
padding: 0 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
340
nkebao/src/components/AccountSelection/index.tsx
Normal file
340
nkebao/src/components/AccountSelection/index.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Popup } from "antd-mobile";
|
||||
import { Button, Input } from "antd";
|
||||
import { getAccountList } from "./api";
|
||||
import style from "./index.module.scss";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import PopupHeader from "@/components/PopuLayout/header";
|
||||
import PopupFooter from "@/components/PopuLayout/footer";
|
||||
|
||||
// 账号对象类型
|
||||
export interface AccountItem {
|
||||
id: number;
|
||||
userName: string;
|
||||
realName: string;
|
||||
departmentName: string;
|
||||
avatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 组件属性接口
|
||||
interface AccountSelectionProps {
|
||||
value: number[];
|
||||
onChange: (ids: number[]) => void;
|
||||
accounts?: AccountItem[];
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
selectedListMaxHeight?: number;
|
||||
showInput?: boolean;
|
||||
showSelectedList?: boolean;
|
||||
readonly?: boolean;
|
||||
onConfirm?: (selectedIds: number[], selectedItems: AccountItem[]) => void;
|
||||
}
|
||||
|
||||
export default function AccountSelection({
|
||||
value,
|
||||
onChange,
|
||||
accounts: propAccounts = [],
|
||||
placeholder = "选择账号",
|
||||
className = "",
|
||||
visible,
|
||||
onVisibleChange,
|
||||
selectedListMaxHeight = 300,
|
||||
showInput = true,
|
||||
showSelectedList = true,
|
||||
readonly = false,
|
||||
onConfirm,
|
||||
}: AccountSelectionProps) {
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [accountsList, setAccountsList] = useState<AccountItem[]>(propAccounts);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 受控弹窗逻辑
|
||||
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||
const setRealVisible = (v: boolean) => {
|
||||
if (onVisibleChange) onVisibleChange(v);
|
||||
if (visible === undefined) setPopupVisible(v);
|
||||
};
|
||||
|
||||
// 打开弹窗时先显示弹窗再请求账号数据
|
||||
const openPopup = () => {
|
||||
if (readonly) return;
|
||||
setCurrentPage(1);
|
||||
setSearchQuery("");
|
||||
setRealVisible(true);
|
||||
setTimeout(async () => {
|
||||
if (typeof getAccountList === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getAccountList({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
keyword: "",
|
||||
});
|
||||
if (response && response.list) {
|
||||
setAccountsList(response.list);
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 搜索时请求账号数据
|
||||
useEffect(() => {
|
||||
if (!realVisible) return;
|
||||
const timer = setTimeout(async () => {
|
||||
setCurrentPage(1);
|
||||
if (typeof getAccountList === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getAccountList({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
keyword: searchQuery,
|
||||
});
|
||||
if (response && response.list) {
|
||||
setAccountsList(response.list);
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchQuery, realVisible]);
|
||||
|
||||
// 渲染和过滤都依赖内部accountsList
|
||||
const filteredAccounts = accountsList.filter(
|
||||
(acc) =>
|
||||
acc.userName.includes(searchQuery) ||
|
||||
acc.realName.includes(searchQuery) ||
|
||||
acc.departmentName.includes(searchQuery)
|
||||
);
|
||||
|
||||
// 处理账号选择
|
||||
const handleAccountToggle = (accountId: number) => {
|
||||
if (readonly) return;
|
||||
const newSelected = value.includes(accountId)
|
||||
? value.filter((id) => id !== accountId)
|
||||
: [...value, accountId];
|
||||
onChange(newSelected);
|
||||
};
|
||||
|
||||
// 获取显示文本
|
||||
const getDisplayText = () => {
|
||||
if (value.length === 0) return "";
|
||||
return `已选择 ${value.length} 个账号`;
|
||||
};
|
||||
|
||||
// 获取已选账号详细信息
|
||||
const selectedAccountObjs = [
|
||||
...accountsList.filter((acc) => value.includes(acc.id)),
|
||||
...value
|
||||
.filter((id) => !accountsList.some((acc) => acc.id === id))
|
||||
.map((id) => ({
|
||||
id,
|
||||
userName: String(id),
|
||||
realName: "",
|
||||
departmentName: "",
|
||||
})),
|
||||
];
|
||||
|
||||
// 删除已选账号
|
||||
const handleRemoveAccount = (id: number) => {
|
||||
if (readonly) return;
|
||||
onChange(value.filter((d) => d !== id));
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm(value, selectedAccountObjs);
|
||||
}
|
||||
setRealVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 输入框 */}
|
||||
{showInput && (
|
||||
<div className={`${style.inputWrapper} ${className}`}>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={getDisplayText()}
|
||||
onClick={openPopup}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear={!readonly}
|
||||
size="large"
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
style={
|
||||
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 已选账号列表窗口 */}
|
||||
{showSelectedList && selectedAccountObjs.length > 0 && (
|
||||
<div
|
||||
className={style.selectedListWindow}
|
||||
style={{
|
||||
maxHeight: selectedListMaxHeight,
|
||||
overflowY: "auto",
|
||||
marginTop: 8,
|
||||
border: "1px solid #e5e6eb",
|
||||
borderRadius: 8,
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
{selectedAccountObjs.map((acc) => (
|
||||
<div
|
||||
key={acc.id}
|
||||
className={style.selectedListRow}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "4px 8px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{acc.userName}({acc.realName})- {acc.departmentName}
|
||||
</div>
|
||||
{!readonly && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
color: "#ff4d4f",
|
||||
border: "none",
|
||||
background: "none",
|
||||
minWidth: 24,
|
||||
height: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={() => handleRemoveAccount(acc.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 弹窗 */}
|
||||
<Popup
|
||||
visible={realVisible && !readonly}
|
||||
onMaskClick={() => setRealVisible(false)}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "100vh" }}
|
||||
>
|
||||
<Layout
|
||||
header={
|
||||
<PopupHeader
|
||||
title="选择账号"
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
searchPlaceholder="搜索账号"
|
||||
loading={loading}
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<PopupFooter
|
||||
total={filteredAccounts.length}
|
||||
currentPage={currentPage}
|
||||
totalPages={1}
|
||||
loading={loading}
|
||||
selectedCount={value.length}
|
||||
onPageChange={setCurrentPage}
|
||||
onCancel={() => setRealVisible(false)}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.friendList}>
|
||||
{loading ? (
|
||||
<div className={style.loadingBox}>
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : filteredAccounts.length > 0 ? (
|
||||
<div className={style.friendListInner}>
|
||||
{filteredAccounts.map((acc) => (
|
||||
<label
|
||||
key={acc.id}
|
||||
className={style.friendItem}
|
||||
onClick={() => !readonly && handleAccountToggle(acc.id)}
|
||||
>
|
||||
<div className={style.radioWrapper}>
|
||||
<div
|
||||
className={
|
||||
value.includes(acc.id)
|
||||
? style.radioSelected
|
||||
: style.radioUnselected
|
||||
}
|
||||
>
|
||||
{value.includes(acc.id) && (
|
||||
<div className={style.radioDot}></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.friendInfo}>
|
||||
<div className={style.friendAvatar}>
|
||||
{acc.avatar ? (
|
||||
<img
|
||||
src={acc.avatar}
|
||||
alt={acc.userName}
|
||||
className={style.avatarImg}
|
||||
/>
|
||||
) : (
|
||||
acc.userName.charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<div className={style.friendDetail}>
|
||||
<div className={style.friendName}>{acc.userName}</div>
|
||||
<div className={style.friendId}>
|
||||
真实姓名: {acc.realName}
|
||||
</div>
|
||||
<div className={style.friendId}>
|
||||
部门: {acc.departmentName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.emptyBox}>
|
||||
<div className={style.emptyText}>
|
||||
{searchQuery
|
||||
? `没有找到包含"${searchQuery}"的账号`
|
||||
: "没有找到账号"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import DeviceSelection from "@/components/DeviceSelection";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import GroupSelection from "@/components/GroupSelection";
|
||||
import ContentLibrarySelection from "@/components/ContentLibrarySelection";
|
||||
import AccountSelection from "@/components/AccountSelection";
|
||||
|
||||
const ComponentTest: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -25,6 +26,8 @@ const ComponentTest: React.FC = () => {
|
||||
// 内容库选择状态
|
||||
const [selectedLibraries, setSelectedLibraries] = useState<string[]>([]);
|
||||
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="组件调试" />}>
|
||||
<div style={{ padding: 16 }}>
|
||||
@@ -131,6 +134,23 @@ const ComponentTest: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab title="账号选择" key="accounts">
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<h3 style={{ marginBottom: 16 }}>AccountSelection 组件测试</h3>
|
||||
<AccountSelection
|
||||
value={selectedAccounts}
|
||||
onChange={setSelectedAccounts}
|
||||
// 可根据实际API和props补充其它参数
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<strong>已选账号:</strong>
|
||||
{selectedAccounts.length > 0
|
||||
? selectedAccounts.join(", ")
|
||||
: "无"}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -1,347 +1,315 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Radio,
|
||||
Slider,
|
||||
TimePicker,
|
||||
message,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { LeftOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import style from "./index.module.scss";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
|
||||
const accountList = [
|
||||
{ label: "客服A", value: "a" },
|
||||
{ label: "客服B", value: "b" },
|
||||
{ label: "客服C", value: "c" },
|
||||
];
|
||||
const scenarioList = [
|
||||
{ label: "海报获客", value: "poster" },
|
||||
{ label: "电话获客", value: "phone" },
|
||||
{ label: "抖音获客", value: "douyin" },
|
||||
{ label: "小红书获客", value: "xiaohongshu" },
|
||||
{ label: "微信群获客", value: "weixinqun" },
|
||||
{ label: "API获客", value: "api" },
|
||||
{ label: "订单获客", value: "order" },
|
||||
{ label: "付款码获客", value: "payment" },
|
||||
];
|
||||
const poolList = [
|
||||
{
|
||||
id: "pool-1",
|
||||
name: "高价值客户池",
|
||||
userCount: 156,
|
||||
tags: ["高价值", "优先添加"],
|
||||
},
|
||||
{ id: "pool-2", name: "潜在客户池", userCount: 289, tags: ["潜在客户"] },
|
||||
{ id: "pool-3", name: "新用户池", userCount: 432, tags: ["新用户"] },
|
||||
];
|
||||
|
||||
const stepList = [
|
||||
{ id: 1, title: "基本信息", subtitle: "基本信息" },
|
||||
{ id: 2, title: "目标设置", subtitle: "目标设置" },
|
||||
{ id: 3, title: "流量池选择", subtitle: "流量池选择" },
|
||||
];
|
||||
|
||||
const TrafficDistributionForm: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const navigate = useNavigate();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||
const [distributeType, setDistributeType] = useState(1);
|
||||
const [maxPerDay, setMaxPerDay] = useState(50);
|
||||
const [timeType, setTimeType] = useState(1);
|
||||
const [timeRange, setTimeRange] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 账号搜索(模拟)
|
||||
const [accountSearch, setAccountSearch] = useState("");
|
||||
const filteredAccounts = accountList.filter((acc) =>
|
||||
acc.label.includes(accountSearch)
|
||||
);
|
||||
|
||||
const [targetUserCount, setTargetUserCount] = useState(100);
|
||||
const [targetTypes, setTargetTypes] = useState<string[]>([]);
|
||||
const [targetScenarios, setTargetScenarios] = useState<string[]>([]);
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>([]);
|
||||
const [poolSearch, setPoolSearch] = useState("");
|
||||
|
||||
const handleFinish = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// TODO: 提交接口
|
||||
message.success("新建流量分发成功");
|
||||
navigate(-1);
|
||||
} catch (e) {
|
||||
message.error("新建失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 步骤切换
|
||||
const next = () => setCurrent((cur) => cur + 1);
|
||||
const prev = () => setCurrent((cur) => cur - 1);
|
||||
|
||||
// 过滤流量池
|
||||
const filteredPools = poolList.filter((pool) =>
|
||||
pool.name.includes(poolSearch)
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon title="新建流量分发" />
|
||||
<div className={style.formStepsWrap}>
|
||||
<StepIndicator currentStep={current + 1} steps={stepList} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={style.formPage}>
|
||||
<div className={style.formBody}>
|
||||
{current === 0 && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={() => next()}
|
||||
initialValues={{ distributeType: 1, maxPerDay: 50, timeType: 1 }}
|
||||
>
|
||||
<div className={style.sectionTitle}>基本信息</div>
|
||||
<Form.Item
|
||||
label="计划名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入计划名称" }]}
|
||||
>
|
||||
<Input placeholder="流量分发 20250724 1700" maxLength={30} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="选择账号"
|
||||
required
|
||||
className={style.accountSelectItem}
|
||||
>
|
||||
<Input
|
||||
placeholder="请选择账号"
|
||||
value={accountSearch}
|
||||
onChange={(e) => setAccountSearch(e.target.value)}
|
||||
suffix={<span className={style.accountSearchIcon} />}
|
||||
/>
|
||||
<div className={style.accountListWrap}>
|
||||
{filteredAccounts.map((acc) => (
|
||||
<label key={acc.value} className={style.accountItem}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAccounts.includes(acc.value)}
|
||||
onChange={(e) => {
|
||||
setSelectedAccounts((val) =>
|
||||
e.target.checked
|
||||
? [...val, acc.value]
|
||||
: val.filter((v) => v !== acc.value)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>{acc.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.accountSelectedCount}>
|
||||
已选账号:<span>{selectedAccounts.length}</span> 个
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="分配方式" name="distributeType" required>
|
||||
<Radio.Group
|
||||
value={distributeType}
|
||||
onChange={(e) => setDistributeType(e.target.value)}
|
||||
className={style.radioGroup}
|
||||
>
|
||||
<Radio value={1}>
|
||||
均分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(流量将均分分配给所有客服)
|
||||
</span>
|
||||
</Radio>
|
||||
<Radio value={2}>
|
||||
优先级分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(按客服优先级顺序分配)
|
||||
</span>
|
||||
</Radio>
|
||||
<Radio value={3}>
|
||||
比例分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(按设置比例分配流量)
|
||||
</span>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="分配限制" required>
|
||||
<div className={style.sliderLabelWrap}>
|
||||
<span>每日最大分配量</span>
|
||||
<span className={style.sliderValue}>{maxPerDay} 人/天</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={maxPerDay}
|
||||
onChange={setMaxPerDay}
|
||||
className={style.slider}
|
||||
/>
|
||||
<div className={style.sliderDesc}>
|
||||
限制每天最多分配的流量数量
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间限制" name="timeType" required>
|
||||
<Radio.Group
|
||||
value={timeType}
|
||||
onChange={(e) => setTimeType(e.target.value)}
|
||||
className={style.radioGroup}
|
||||
>
|
||||
<Radio value={1}>全天分配</Radio>
|
||||
<Radio value={2}>自定义时间段</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{timeType === 2 && (
|
||||
<Form.Item label="" required>
|
||||
<div className={style.timeRangeWrap}>
|
||||
<div>
|
||||
<span>开始时间</span>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
style={{ width: 120 }}
|
||||
value={timeRange?.[0]}
|
||||
onChange={(v) => setTimeRange([v, timeRange?.[1]])}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>结束时间</span>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
style={{ width: 120 }}
|
||||
value={timeRange?.[1]}
|
||||
onChange={(v) => setTimeRange([timeRange?.[0], v])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
{current === 1 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>目标设置</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>目标用户数</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={1000}
|
||||
value={targetUserCount}
|
||||
onChange={setTargetUserCount}
|
||||
className={style.slider}
|
||||
/>
|
||||
<div className={style.sliderValue}>{targetUserCount} 人</div>
|
||||
</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>目标客户类型</div>
|
||||
<Checkbox.Group
|
||||
options={["高价值客户", "新用户", "潜在客户", "流失预警"]}
|
||||
value={targetTypes}
|
||||
onChange={setTargetTypes}
|
||||
className={style.checkboxGroup}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>获客场景</div>
|
||||
<Checkbox.Group
|
||||
options={scenarioList.map((s) => ({
|
||||
label: s.label,
|
||||
value: s.value,
|
||||
}))}
|
||||
value={targetScenarios}
|
||||
onChange={setTargetScenarios}
|
||||
className={style.checkboxGroup}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.formStepBtns}>
|
||||
<Button onClick={prev} style={{ marginRight: 12 }}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{current === 2 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>流量池选择</div>
|
||||
<div className={style.formBlock}>
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={poolSearch}
|
||||
onChange={(e) => setPoolSearch(e.target.value)}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className={style.poolListWrap}>
|
||||
{filteredPools.map((pool) => (
|
||||
<label key={pool.id} className={style.poolItem}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPools.includes(pool.id)}
|
||||
onChange={(e) => {
|
||||
setSelectedPools((val) =>
|
||||
e.target.checked
|
||||
? [...val, pool.id]
|
||||
: val.filter((v) => v !== pool.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className={style.poolName}>{pool.name}</span>
|
||||
<span className={style.poolTags}>
|
||||
{pool.tags.join("/")}
|
||||
</span>
|
||||
<span className={style.poolCount}>
|
||||
{pool.userCount}人
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.poolSelectedCount}>
|
||||
已选流量池:<span>{selectedPools.length}</span> 个
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.formStepBtns}>
|
||||
<Button onClick={prev} style={{ marginRight: 12 }}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => message.success("提交成功")}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficDistributionForm;
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Radio,
|
||||
Slider,
|
||||
TimePicker,
|
||||
message,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import { LeftOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import style from "./index.module.scss";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import AccountSelection from "@/components/AccountSelection";
|
||||
import { getAccountList } from "@/components/AccountSelection/api";
|
||||
|
||||
const scenarioList = [
|
||||
{ label: "海报获客", value: "poster" },
|
||||
{ label: "电话获客", value: "phone" },
|
||||
{ label: "抖音获客", value: "douyin" },
|
||||
{ label: "小红书获客", value: "xiaohongshu" },
|
||||
{ label: "微信群获客", value: "weixinqun" },
|
||||
{ label: "API获客", value: "api" },
|
||||
{ label: "订单获客", value: "order" },
|
||||
{ label: "付款码获客", value: "payment" },
|
||||
];
|
||||
const poolList = [
|
||||
{
|
||||
id: "pool-1",
|
||||
name: "高价值客户池",
|
||||
userCount: 156,
|
||||
tags: ["高价值", "优先添加"],
|
||||
},
|
||||
{ id: "pool-2", name: "潜在客户池", userCount: 289, tags: ["潜在客户"] },
|
||||
{ id: "pool-3", name: "新用户池", userCount: 432, tags: ["新用户"] },
|
||||
];
|
||||
|
||||
const stepList = [
|
||||
{ id: 1, title: "基本信息", subtitle: "基本信息" },
|
||||
{ id: 2, title: "目标设置", subtitle: "目标设置" },
|
||||
{ id: 3, title: "流量池选择", subtitle: "流量池选择" },
|
||||
];
|
||||
|
||||
const TrafficDistributionForm: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const navigate = useNavigate();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [selectedAccountIds, setSelectedAccountIds] = useState<number[]>([]);
|
||||
const [distributeType, setDistributeType] = useState(1);
|
||||
const [maxPerDay, setMaxPerDay] = useState(50);
|
||||
const [timeType, setTimeType] = useState(1);
|
||||
const [timeRange, setTimeRange] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [targetUserCount, setTargetUserCount] = useState(100);
|
||||
const [targetTypes, setTargetTypes] = useState<string[]>([]);
|
||||
const [targetScenarios, setTargetScenarios] = useState<string[]>([]);
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>([]);
|
||||
const [poolSearch, setPoolSearch] = useState("");
|
||||
|
||||
const handleFinish = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// TODO: 提交接口
|
||||
message.success("新建流量分发成功");
|
||||
navigate(-1);
|
||||
} catch (e) {
|
||||
message.error("新建失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 步骤切换
|
||||
const next = () => setCurrent((cur) => cur + 1);
|
||||
const prev = () => setCurrent((cur) => cur - 1);
|
||||
|
||||
// 过滤流量池
|
||||
const filteredPools = poolList.filter((pool) =>
|
||||
pool.name.includes(poolSearch)
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon title="新建流量分发" />
|
||||
<div className={style.formStepsWrap}>
|
||||
<StepIndicator currentStep={current + 1} steps={stepList} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={style.formPage}>
|
||||
<div className={style.formBody}>
|
||||
{current === 0 && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={() => next()}
|
||||
initialValues={{ distributeType: 1, maxPerDay: 50, timeType: 1 }}
|
||||
>
|
||||
<div className={style.sectionTitle}>基本信息</div>
|
||||
<Form.Item
|
||||
label="计划名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: "请输入计划名称" }]}
|
||||
>
|
||||
<Input placeholder="流量分发 20250724 1700" maxLength={30} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="选择账号"
|
||||
required
|
||||
className={style.accountSelectItem}
|
||||
>
|
||||
<AccountSelection
|
||||
value={selectedAccountIds}
|
||||
onChange={setSelectedAccountIds}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="分配方式" name="distributeType" required>
|
||||
<Radio.Group
|
||||
value={distributeType}
|
||||
onChange={(e) => setDistributeType(e.target.value)}
|
||||
className={style.radioGroup}
|
||||
>
|
||||
<Radio value={1}>
|
||||
均分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(流量将均分分配给所有客服)
|
||||
</span>
|
||||
</Radio>
|
||||
<Radio value={2}>
|
||||
优先级分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(按客服优先级顺序分配)
|
||||
</span>
|
||||
</Radio>
|
||||
<Radio value={3}>
|
||||
比例分配{" "}
|
||||
<span className={style.radioDesc}>
|
||||
(按设置比例分配流量)
|
||||
</span>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="分配限制" required>
|
||||
<div className={style.sliderLabelWrap}>
|
||||
<span>每日最大分配量</span>
|
||||
<span className={style.sliderValue}>{maxPerDay} 人/天</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
value={maxPerDay}
|
||||
onChange={setMaxPerDay}
|
||||
className={style.slider}
|
||||
/>
|
||||
<div className={style.sliderDesc}>
|
||||
限制每天最多分配的流量数量
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间限制" name="timeType" required>
|
||||
<Radio.Group
|
||||
value={timeType}
|
||||
onChange={(e) => setTimeType(e.target.value)}
|
||||
className={style.radioGroup}
|
||||
>
|
||||
<Radio value={1}>全天分配</Radio>
|
||||
<Radio value={2}>自定义时间段</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{timeType === 2 && (
|
||||
<Form.Item label="" required>
|
||||
<div className={style.timeRangeWrap}>
|
||||
<div>
|
||||
<span>开始时间</span>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
style={{ width: 120 }}
|
||||
value={timeRange?.[0]}
|
||||
onChange={(v) => setTimeRange([v, timeRange?.[1]])}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>结束时间</span>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
style={{ width: 120 }}
|
||||
value={timeRange?.[1]}
|
||||
onChange={(v) => setTimeRange([timeRange?.[0], v])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
{current === 1 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>目标设置</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>目标用户数</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={1000}
|
||||
value={targetUserCount}
|
||||
onChange={setTargetUserCount}
|
||||
className={style.slider}
|
||||
/>
|
||||
<div className={style.sliderValue}>{targetUserCount} 人</div>
|
||||
</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>目标客户类型</div>
|
||||
<Checkbox.Group
|
||||
options={["高价值客户", "新用户", "潜在客户", "流失预警"]}
|
||||
value={targetTypes}
|
||||
onChange={setTargetTypes}
|
||||
className={style.checkboxGroup}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.formBlock}>
|
||||
<div className={style.formLabel}>获客场景</div>
|
||||
<Checkbox.Group
|
||||
options={scenarioList.map((s) => ({
|
||||
label: s.label,
|
||||
value: s.value,
|
||||
}))}
|
||||
value={targetScenarios}
|
||||
onChange={setTargetScenarios}
|
||||
className={style.checkboxGroup}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.formStepBtns}>
|
||||
<Button onClick={prev} style={{ marginRight: 12 }}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{current === 2 && (
|
||||
<div>
|
||||
<div className={style.sectionTitle}>流量池选择</div>
|
||||
<div className={style.formBlock}>
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={poolSearch}
|
||||
onChange={(e) => setPoolSearch(e.target.value)}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<div className={style.poolListWrap}>
|
||||
{filteredPools.map((pool) => (
|
||||
<label key={pool.id} className={style.poolItem}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPools.includes(pool.id)}
|
||||
onChange={(e) => {
|
||||
setSelectedPools((val) =>
|
||||
e.target.checked
|
||||
? [...val, pool.id]
|
||||
: val.filter((v) => v !== pool.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className={style.poolName}>{pool.name}</span>
|
||||
<span className={style.poolTags}>
|
||||
{pool.tags.join("/")}
|
||||
</span>
|
||||
<span className={style.poolCount}>
|
||||
{pool.userCount}人
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.poolSelectedCount}>
|
||||
已选流量池:<span>{selectedPools.length}</span> 个
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.formStepBtns}>
|
||||
<Button onClick={prev} style={{ marginRight: 12 }}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => message.success("提交成功")}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficDistributionForm;
|
||||
|
||||
Reference in New Issue
Block a user