From 3251a1985ea0944ee082c1f2487e45aab2b23796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 20:43:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E5=AD=98=E4=B8=80=E4=B8=8B=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 3 +- .../form/index.module.scss | 207 +++++++++++ .../traffic-distribution/form/index.tsx | 350 +++++++++++++++++- .../traffic-distribution/list/api.ts | 18 + .../list/index.module.scss | 4 + .../traffic-distribution/list/index.tsx | 310 +++++++++++----- 6 files changed, 794 insertions(+), 98 deletions(-) diff --git a/nkebao/.env.development b/nkebao/.env.development index da6a111b..05c62ec4 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,5 @@ # 基础环境变量示例 -VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=http://www.yishi.com +VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/pages/workspace/traffic-distribution/form/index.module.scss b/nkebao/src/pages/workspace/traffic-distribution/form/index.module.scss index e69de29b..f54cd52f 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/form/index.module.scss +++ b/nkebao/src/pages/workspace/traffic-distribution/form/index.module.scss @@ -0,0 +1,207 @@ +.formPage { +} + +.formHeader { + background: #fff; + padding: 0 16px; + height: 56px; + display: flex; + align-items: center; + border-bottom: 1px solid #f0f0f0; + position: relative; +} +.formTitle { + font-size: 18px; + font-weight: 600; + color: #222; + flex: 1; + text-align: center; +} +.backBtn { + position: absolute; + left: 8px; + top: 10px; + font-size: 18px; + color: #222; + border: none; + background: none; +} +.cancelBtn { + position: absolute; + right: 8px; + top: 10px; + color: #888; + border: none; + background: none; +} + +.formStepsWrap { + background: #fff; + padding: 0 0 8px 0; +} +.formSteps { + padding: 0 24px; + margin-top: 8px; +} + +.formBody { + background: #fff; + padding: 12px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} +.sectionTitle { + font-size: 17px; + font-weight: 600; + margin-bottom: 18px; + color: #222; +} + +.accountSelectItem { + margin-bottom: 0 !important; +} +.accountListWrap { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + margin: 10px 0 4px 0; +} +.accountItem { + display: flex; + align-items: center; + font-size: 15px; + background: #f7f8fa; + border-radius: 6px; + padding: 4px 10px; + cursor: pointer; + border: 1px solid #e5e6eb; + transition: border 0.2s; +} +.accountItem input[type="checkbox"] { + margin-right: 6px; +} +.accountSelectedCount { + font-size: 13px; + color: #888; + margin-bottom: 8px; +} + +.radioGroup { + display: flex; + flex-direction: column; + gap: 8px; +} +.radioDesc { + font-size: 13px; + color: #888; + margin-left: 6px; +} + +.sliderLabelWrap { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 2px; +} +.sliderValue { + font-size: 15px; + color: #222; + font-weight: 500; +} +.slider { + margin: 0 0 2px 0; +} +.sliderDesc { + font-size: 13px; + color: #888; + margin-bottom: 8px; +} + +.timeRangeWrap { + display: flex; + gap: 24px; + align-items: flex-end; +} +.timeRangeWrap > div { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: #888; +} + +.formBlock { + margin-bottom: 24px; +} +.formLabel { + font-size: 15px; + font-weight: 500; + margin-bottom: 8px; + color: #222; +} +.checkboxGroup { + display: flex; + flex-wrap: wrap; + gap: 12px 24px; +} +.poolListWrap { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 8px; +} +.poolItem { + display: flex; + align-items: center; + background: #f7f8fa; + border-radius: 6px; + padding: 8px 12px; + border: 1px solid #e5e6eb; + font-size: 15px; + gap: 10px; + cursor: pointer; + transition: border 0.2s; +} +.poolItem input[type="checkbox"] { + margin-right: 6px; +} +.poolName { + font-weight: 500; + color: #222; +} +.poolTags { + font-size: 13px; + color: #888; + margin-left: 8px; +} +.poolCount { + font-size: 13px; + color: #888; + margin-left: auto; +} +.poolSelectedCount { + font-size: 13px; + color: #888; + margin-bottom: 8px; +} +.formStepBtns { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; +} + +// 步骤条美化 +.formSteps :global(.ant-steps-item-title) { + font-size: 15px; +} +.formSteps :global(.ant-steps-item-process .ant-steps-item-title) { + color: #1677ff; + font-weight: 600; +} +.formSteps :global(.ant-steps-item-finish .ant-steps-item-title) { + color: #222; +} +.formSteps :global(.ant-steps-item-wait .ant-steps-item-title) { + color: #888; +} diff --git a/nkebao/src/pages/workspace/traffic-distribution/form/index.tsx b/nkebao/src/pages/workspace/traffic-distribution/form/index.tsx index 52e3530a..617db51b 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/form/index.tsx +++ b/nkebao/src/pages/workspace/traffic-distribution/form/index.tsx @@ -1,3 +1,347 @@ -export default function TrafficDistributionForm() { - return
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"; + +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([]); + const [distributeType, setDistributeType] = useState(1); + const [maxPerDay, setMaxPerDay] = useState(50); + const [timeType, setTimeType] = useState(1); + const [timeRange, setTimeRange] = useState(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([]); + const [targetScenarios, setTargetScenarios] = useState([]); + const [selectedPools, setSelectedPools] = useState([]); + 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 ( + + +
+ +
+ + } + > +
+
+ {current === 0 && ( +
next()} + initialValues={{ distributeType: 1, maxPerDay: 50, timeType: 1 }} + > +
基本信息
+ + + + + setAccountSearch(e.target.value)} + suffix={} + /> +
+ {filteredAccounts.map((acc) => ( + + ))} +
+
+ 已选账号:{selectedAccounts.length} 个 +
+
+ + setDistributeType(e.target.value)} + className={style.radioGroup} + > + + 均分配{" "} + + (流量将均分分配给所有客服) + + + + 优先级分配{" "} + + (按客服优先级顺序分配) + + + + 比例分配{" "} + + (按设置比例分配流量) + + + + + +
+ 每日最大分配量 + {maxPerDay} 人/天 +
+ +
+ 限制每天最多分配的流量数量 +
+
+ + setTimeType(e.target.value)} + className={style.radioGroup} + > + 全天分配 + 自定义时间段 + + + {timeType === 2 && ( + +
+
+ 开始时间 + setTimeRange([v, timeRange?.[1]])} + /> +
+
+ 结束时间 + setTimeRange([timeRange?.[0], v])} + /> +
+
+
+ )} + + + +
+ )} + {current === 1 && ( +
+
目标设置
+
+
目标用户数
+ +
{targetUserCount} 人
+
+
+
目标客户类型
+ +
+
+
获客场景
+ ({ + label: s.label, + value: s.value, + }))} + value={targetScenarios} + onChange={setTargetScenarios} + className={style.checkboxGroup} + /> +
+
+ + +
+
+ )} + {current === 2 && ( +
+
流量池选择
+
+ setPoolSearch(e.target.value)} + style={{ marginBottom: 12 }} + /> +
+ {filteredPools.map((pool) => ( + + ))} +
+
+ 已选流量池:{selectedPools.length} 个 +
+
+
+ + +
+
+ )} +
+
+
+ ); +}; + +export default TrafficDistributionForm; diff --git a/nkebao/src/pages/workspace/traffic-distribution/list/api.ts b/nkebao/src/pages/workspace/traffic-distribution/list/api.ts index a607f207..1b44626e 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/list/api.ts +++ b/nkebao/src/pages/workspace/traffic-distribution/list/api.ts @@ -8,3 +8,21 @@ export function fetchDistributionRuleList(params: { }): Promise { return request("/v1/workbench/list?type=5", params, "GET"); } + +// 编辑计划(更新) +export function updateDistributionRule(data: any): Promise { + return request("/v1/workbench/update", { ...data, type: 5 }, "POST"); +} + +// 暂停/启用计划 +export function toggleDistributionRuleStatus( + id: number, + status: 0 | 1 +): Promise { + return request("/v1/workbench/update-status", { id, status }, "POST"); +} + +// 删除计划 +export function deleteDistributionRule(id: number): Promise { + return request("/v1/workbench/delete", { id }, "POST"); +} diff --git a/nkebao/src/pages/workspace/traffic-distribution/list/index.module.scss b/nkebao/src/pages/workspace/traffic-distribution/list/index.module.scss index df66cbda..2e64f9a9 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/list/index.module.scss +++ b/nkebao/src/pages/workspace/traffic-distribution/list/index.module.scss @@ -99,11 +99,15 @@ font-size: 13px; color: #888; margin-top: 6px; + align-items: center; } .ruleFooterIcon { margin-right: 4px; vertical-align: middle; + font-size: 15px; + position: relative; + top: -2px; } .empty { diff --git a/nkebao/src/pages/workspace/traffic-distribution/list/index.tsx b/nkebao/src/pages/workspace/traffic-distribution/list/index.tsx index 9edbf881..9b4f78dd 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/list/index.tsx +++ b/nkebao/src/pages/workspace/traffic-distribution/list/index.tsx @@ -1,23 +1,37 @@ import React, { useEffect, useState } from "react"; import Layout from "@/components/Layout/Layout"; -import { Input, Button } from "antd"; -import NavCommon from "@/components/NavCommon"; -import { fetchDistributionRuleList } from "./api"; -import type { DistributionRule } from "./data"; -import { Tag, Pagination, Spin, message, Switch } from "antd"; import { - EllipsisOutlined, - UserOutlined, - ClusterOutlined, - AppstoreOutlined, - BarChartOutlined, - TeamOutlined, + Input, + Button, + Tag, + Pagination, + Spin, + message, + Switch, + Dropdown, + Menu, +} from "antd"; +import NavCommon from "@/components/NavCommon"; +import { + fetchDistributionRuleList, + updateDistributionRule, + toggleDistributionRuleStatus, + deleteDistributionRule, +} from "./api"; +import type { DistributionRule } from "./data"; +import { + MoreOutlined, + PlusOutlined, + ReloadOutlined, + EditOutlined, + PauseOutlined, + DeleteOutlined, ClockCircleOutlined, SearchOutlined, - ReloadOutlined, CalendarOutlined, } from "@ant-design/icons"; import style from "./index.module.scss"; +import { useNavigate } from "react-router-dom"; const PAGE_SIZE = 10; @@ -35,6 +49,9 @@ const TrafficDistributionList: React.FC = () => { const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); + // 优化:用menuLoadingId标记当前操作的item + const [menuLoadingId, setMenuLoadingId] = useState(null); + const navigate = useNavigate(); useEffect(() => { fetchList(page, searchQuery); @@ -73,96 +90,204 @@ const TrafficDistributionList: React.FC = () => { fetchList(page, searchQuery); }; - const renderCard = (item: DistributionRule) => ( -
-
- {item.name} -
- - {statusMap[item.status]?.text || "未知"} - - - -
-
-
-
- -
- {item.config?.account?.length || 0} + // 优化:菜单点击事件,menuLoadingId标记当前item + const handleMenuClick = async (key: string, item: DistributionRule) => { + setMenuLoadingId(item.id); + try { + if (key === "edit") { + navigate(`/workspace/traffic-distribution/edit/${item.id}`); + } else if (key === "pause") { + await toggleDistributionRuleStatus(item.id, item.status === 1 ? 0 : 1); + message.success(item.status === 1 ? "已暂停" : "已启用"); + handleRefresh(); + } else if (key === "delete") { + await deleteDistributionRule(item.id); + message.success("删除成功"); + handleRefresh(); + } + } catch (e) { + message.error("操作失败"); + } finally { + setMenuLoadingId(null); + } + }; + + // 新增:Switch点击切换计划状态 + const handleSwitchChange = async ( + checked: boolean, + item: DistributionRule + ) => { + setMenuLoadingId(item.id); + try { + await toggleDistributionRuleStatus(item.id, checked ? 1 : 0); + message.success(checked ? "已启用" : "已暂停"); + // 本地只更新当前item的status,不刷新全列表 + setList((prevList) => + prevList.map((rule) => + rule.id === item.id ? { ...rule, status: checked ? 1 : 0 } : rule + ) + ); + } catch (e) { + message.error("操作失败"); + } finally { + setMenuLoadingId(null); + } + }; + + const renderCard = (item: DistributionRule) => { + const menu = ( + handleMenuClick(key, item)}> + } + disabled={menuLoadingId === item.id} + > + 编辑计划 + + } + disabled={menuLoadingId === item.id} + > + {item.status === 1 ? "暂停计划" : "启用计划"} + + } + disabled={menuLoadingId === item.id} + danger + > + 删除计划 + + + ); + + return ( +
+
+ {item.name} +
+ + {statusMap[item.status]?.text || "未知"} + + handleSwitchChange(checked, item)} + /> + {/* Dropdown 只允许传递单一元素给 menu 属性 */} + , + label: "编辑计划", + disabled: menuLoadingId === item.id, + }, + { + key: "pause", + icon: , + label: item.status === 1 ? "暂停计划" : "启用计划", + disabled: menuLoadingId === item.id, + }, + { + key: "delete", + icon: , + label: "删除计划", + disabled: menuLoadingId === item.id, + danger: true, + }, + ], + onClick: ({ key }) => handleMenuClick(key, item), + }} + trigger={["click"]} + placement="bottomRight" + disabled={menuLoadingId === item.id} + > + +
-
分发账号
-
- -
- {item.config?.devices?.length || 0} +
+
+
+ {item.config?.account?.length || 0} +
+
分发账号
-
分发设备
-
-
- -
- {item.config?.pools?.length || 0} +
+
+ {item.config?.devices?.length || 0} +
+
分发设备
+
+
+
+ {item.config?.pools?.length || 0} +
+
流量池
-
流量池
-
-
-
-
- - - {item.config?.total?.dailyAverage || 0} +
+
+
+ + {item.config?.total?.dailyAverage || 0} + +
+ 日均分发量 +
+
+
+ + {item.config?.total?.totalUsers || 0} + +
+ 总流量池数量 +
+
+
+
+ + + 上次执行:{item.config?.lastUpdated || "-"} -
- 日均分发量 -
-
-
- - - {item.config?.total?.totalUsers || 0} + + + 创建时间:{item.createTime || "-"} -
- 总流量池数量 -
-
- - - 上次执行:{item.config?.lastUpdated || "-"} - - - - 创建时间:{item.createTime || "-"} - -
-
- ); + ); + }; return ( - <> - + { + navigate("/workspace/traffic-distribution/new"); + }} + > + 新建分发 + + } + /> {/* 搜索栏 */}
@@ -176,13 +301,10 @@ const TrafficDistributionList: React.FC = () => { />
+ icon={} + size="large" + >
}