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

存一版
This commit is contained in:
笔记本里的永平
2025-07-24 22:07:14 +08:00
parent 3251a1985e
commit 6f280b5816
5 changed files with 916 additions and 347 deletions

View 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");
}

View 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;
}

View 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>
</>
);
}

View File

@@ -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>

View File

@@ -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;