- ) : (
- filteredTasks.map(task => (
+ const renderTaskCard = (task: MomentsSyncTask) => (
@@ -268,23 +221,115 @@ const MomentsSync: React.FC = () => {
?.map(c => c.name)
.join(",") || "默认内容库"}
-
- 创建人:{task.creatorName}
-
+
创建人:{task.creatorName}
上次同步:{task.lastSyncTime || "无"}
-
- 创建时间:{task.createTime}
+
创建时间:{task.createTime}
+ );
+
+ let content: React.ReactNode;
+ if (filteredTasks.length === 0) {
+ content = (
+
+
+
+
+
暂无同步任务
+
+
+ );
+ } else {
+ content = (
+ <>
+ {globalTasks.length > 0 && (
+
+ 全局同步计划将应用于所有设备(包括新添加的设备),请合理设置同步频率与数量。
+
+ )}
+
+ {globalTasks.length > 0 && (
+
+
+
+ 全局朋友圈同步计划
+
+
+ {globalTasks.map(renderTaskCard)}
+
+
+ )}
+
+ {independentTasks.length > 0 && (
+
+
+
+ 独立朋友圈同步计划
+
+
+ {independentTasks.map(renderTaskCard)}
- ))
- )}
+
+ )}
+ >
+ );
+ }
+
+ return (
+
+ navigate("/workspace/moments-sync/new")}
+ >
+ 新建任务
+
+ }
+ />
+
+
+
+ setSearchTerm(e.target.value)}
+ prefix={}
+ allowClear
+ size="large"
+ />
+
+
+ >
+ }
+ loading={loading}
+ >
+
);
diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.module.scss b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.module.scss
index dbe8134c..740dc709 100644
--- a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.module.scss
+++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.module.scss
@@ -1,12 +1,50 @@
.formBg {
+ background: #f8fafc;
+ min-height: 100vh;
+ padding-bottom: 100px;
+}
+
+.container {
padding: 16px;
+ background: #f8fafc;
+ min-height: 100vh;
+ padding-bottom: 100px;
+ box-sizing: border-box;
+ width: 100%;
+ overflow-x: hidden;
+
+ @media (max-width: 375px) {
+ padding: 12px;
+ }
+}
+
+.card {
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ padding: 16px;
+ margin-bottom: 16px;
+ box-sizing: border-box;
+ width: 100%;
+ overflow: hidden;
+
+ @media (max-width: 375px) {
+ padding: 12px;
+ }
}
.formStepBtnRow {
display: flex;
- justify-content: flex-end;
- gap: 12px;
- margin-top: 32px;
- padding: 12px;
+ justify-content: center;
+ gap: 16px;
+ padding: 16px;
+ background: #fff;
+ border-top: 1px solid #f0f0f0;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}
.formSteps {
display: flex;
@@ -78,10 +116,31 @@
margin-bottom: 10px;
}
+.radioGroup {
+ display: flex;
+ gap: 24px;
+}
+
.input {
+ width: 100%;
height: 44px;
+ padding: 10px 12px;
border-radius: 8px;
+ border: 1px solid #e2e8f0;
+ background: #f8fafc;
font-size: 15px;
+ outline: none;
+ transition: all 0.2s;
+ box-sizing: border-box;
+
+ &:focus {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
+ }
+
+ &::placeholder {
+ color: #cbd5e1;
+ }
}
.timeRow {
@@ -213,12 +272,6 @@
margin-top: 8px;
}
-.formStepBtnRow {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- margin-top: 32px;
-}
.prevBtn {
height: 44px;
diff --git a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx
index 017216bc..e86ddca8 100644
--- a/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx
+++ b/Cunkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { Button, Input, Switch, message, Spin } from "antd";
+import { Button, Input, Switch, message, Spin, Radio } from "antd";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
@@ -15,6 +15,7 @@ import {
import DeviceSelection from "@/components/DeviceSelection";
import ContentSelection from "@/components/ContentSelection";
import NavCommon from "@/components/NavCommon";
+import { useUserStore } from "@/store/module/user";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { ContentItem } from "@/components/ContentSelection/data";
@@ -38,12 +39,16 @@ const defaultForm = {
contentTypes: ["text", "image", "video"],
targetTags: [] as string[],
filterKeywords: [] as string[],
+ // 计划类型:0-全局计划,1-独立计划
+ planType: 1,
};
const NewMomentsSync: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEditMode = !!id;
+ const { user } = useUserStore();
+ const isAdmin = user?.isAdmin === 1;
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({ ...defaultForm });
@@ -76,6 +81,7 @@ const NewMomentsSync: React.FC = () => {
contentTypes: res.config?.contentTypes || ["text", "image", "video"],
targetTags: res.config?.targetTags || [],
filterKeywords: res.config?.filterKeywords || [],
+ planType: res.config?.planType ?? (res as any).planType ?? 1,
});
setSelectedDevicesOptions(res.config?.deviceGroupsOptions || []);
setContentGroupsOptions(res.config?.contentGroupsOptions || []);
@@ -147,6 +153,7 @@ const NewMomentsSync: React.FC = () => {
filterKeywords: formData.filterKeywords,
type: 2,
status: formData.enabled ? 1 : 0,
+ planType: (formData as any).planType ?? 1,
};
if (isEditMode && id) {
await updateMomentsSync({ id, ...params });
@@ -168,7 +175,22 @@ const NewMomentsSync: React.FC = () => {
const renderStep = () => {
if (currentStep === 0) {
return (
-
+
+ {/* 计划类型和任务名称 */}
+
+ {isAdmin && (
+
+
计划类型
+
updateForm({ planType: e.target.value })}
+ className={style.radioGroup}
+ >
+ 全局计划
+ 独立计划
+
+
+ )}
+ {/* 允许发布时间段 */}
+
允许发布时间段
@@ -196,9 +221,12 @@ const NewMomentsSync: React.FC = () => {
onChange={e => updateForm({ endTime: e.target.value })}
className={style.inputTime}
/>
+
+ {/* 每日同步数量 */}
+
每日同步数量
@@ -220,9 +248,12 @@ const NewMomentsSync: React.FC = () => {
条朋友圈
+
+ {/* 账号类型和是否启用 */}
+
账号类型
@@ -240,7 +271,6 @@ const NewMomentsSync: React.FC = () => {
-
是否启用
@@ -249,6 +279,7 @@ const NewMomentsSync: React.FC = () => {
onChange={checked => updateForm({ enabled: checked })}
className={style.switch}
/>
+
@@ -256,7 +287,8 @@ const NewMomentsSync: React.FC = () => {
}
if (currentStep === 1) {
return (
-
+
+
选择设备
{
showSelectedList={true}
selectedListMaxHeight={200}
/>
+
);
}
if (currentStep === 2) {
return (
-
+
+
选择内容库
{
已选内容库: {formData.contentGroups.length}个
)}
+
);
@@ -315,10 +350,10 @@ const NewMomentsSync: React.FC = () => {
if (currentStep === 1) {
return (
-
@@ -327,7 +362,7 @@ const NewMomentsSync: React.FC = () => {
if (currentStep === 2) {
return (
-
+
上一步
{
onClick={handleSubmit}
loading={loading}
className={style.completeBtn}
- block
+ style={{ flex: 1 }}
>
完成
@@ -353,7 +388,7 @@ const NewMomentsSync: React.FC = () => {
footer={renderFooter()}
>
-
+
diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/data.ts b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/data.ts
index ab82999f..7f449f22 100644
--- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/data.ts
+++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/data.ts
@@ -46,6 +46,8 @@ export interface TrafficDistributionFormData {
id?: string;
type: number;
name: string;
+ // 计划类型:0-全局计划,1-独立计划
+ planType?: number;
source: string;
sourceIcon: string;
description: string;
diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.module.scss b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.module.scss
index f593d3fe..998e9a05 100644
--- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.module.scss
+++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.module.scss
@@ -1,4 +1,7 @@
.formPage {
+ background: #f8fafc;
+ min-height: 100vh;
+ padding-bottom: 100px;
}
.formHeader {
@@ -45,10 +48,54 @@
}
.formBody {
+ padding: 16px;
+ background: #f8fafc;
+ min-height: 100vh;
+ padding-bottom: 100px;
+ box-sizing: border-box;
+ width: 100%;
+ overflow-x: hidden;
+
+ @media (max-width: 375px) {
+ padding: 12px;
+ }
+}
+
+.card {
background: #fff;
- padding: 12px;
- border-radius: 10px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ padding: 16px;
+ margin-bottom: 16px;
+ box-sizing: border-box;
+ width: 100%;
+ overflow: hidden;
+
+ @media (max-width: 375px) {
+ padding: 12px;
+ }
+
+ // 输入框样式
+ :global(.ant-input) {
+ width: 100%;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid #e2e8f0;
+ background: #f8fafc;
+ font-size: 14px;
+ outline: none;
+ transition: all 0.2s;
+ box-sizing: border-box;
+
+ &:focus {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
+ }
+
+ &::placeholder {
+ color: #cbd5e1;
+ }
+ }
}
.sectionTitle {
font-size: 17px;
@@ -136,6 +183,13 @@
flex-direction: column;
gap: 8px;
}
+
+.radioGroupHorizontal {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+}
+
.radioDesc {
font-size: 13px;
color: #888;
diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx
index edd1e1d4..6aca18b5 100644
--- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx
+++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
-import { Form, Input, Button, Radio, Slider, TimePicker, message } from "antd";
+import { Form, Input, Button, Radio, Slider, TimePicker, message, Card } from "antd";
import { useNavigate, useParams } from "react-router-dom";
import style from "./index.module.scss";
import StepIndicator from "@/components/StepIndicator";
@@ -17,6 +17,7 @@ import {
createTrafficDistribution,
} from "./api";
import type { TrafficDistributionFormData } from "./data";
+import { useUserStore } from "@/store/module/user";
import dayjs from "dayjs";
const stepList = [
@@ -30,6 +31,8 @@ const TrafficDistributionForm: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEdit = !!id;
+ const { user } = useUserStore();
+ const isAdmin = user?.isAdmin === 1;
const [current, setCurrent] = useState(0);
const [selectedDevices, setSelectedDevices] = useState
(
@@ -181,6 +184,7 @@ const TrafficDistributionForm: React.FC = () => {
accountGroupsOptions,
poolGroups: poolGroupsOptions.map(v => v.id),
enabled: true,
+ planType: formValues.planType ?? 1,
};
if (isEdit) {
@@ -273,6 +277,7 @@ const TrafficDistributionForm: React.FC = () => {
layout="vertical"
initialValues={{
name: isEdit ? "" : generateDefaultName(),
+ planType: 1,
distributeType: 1,
maxPerDay: 50,
timeType: 1,
@@ -281,99 +286,124 @@ const TrafficDistributionForm: React.FC = () => {
onFinish={handleSubmit}
style={{ display: current === 0 ? "block" : "none" }}
>
- 基本信息
-
-
-
-
-
-
- 均分配
-
- (流量将均分分配给所有客服)
-
-
-
- 优先级分配
-
- (按客服优先级顺序分配)
-
-
-
- 比例分配
- (按设置比例分配流量)
-
-
-
-
-
- 每日最大分配量
-
- {maxPerDay || 0} 人/天
-
-
+ {/* 计划类型和计划名称 */}
+
+ {isAdmin && (
+
+
+ 全局计划
+ 独立计划
+
+
+ )}
-
+
- 限制每天最多分配的流量数量
-
-
-
- 全天分配
- 自定义时间段
-
-
- {timeType === 2 && (
- ({
- validator(_, value) {
- if (getFieldValue("timeType") === 1) {
- return Promise.resolve();
- }
- if (value && value.length === 2) {
- return Promise.resolve();
- }
- return Promise.reject(new Error("请选择开始和结束时间"));
- },
- }),
- ]}
- >
-
-
- )}
+
- 客服选择
-
-
{
- setAccountGroupsOptions(accounts);
- }}
- placeholder="请选择客服"
- showSelectedList={true}
- selectedListMaxHeight={300}
- accountGroups={accountGroups}
- />
-
+ {/* 分配方式 */}
+
+
+
+
+ 均分配
+
+ (流量将均分分配给所有客服)
+
+
+
+ 优先级分配
+
+ (按客服优先级顺序分配)
+
+
+
+ 比例分配
+ (按设置比例分配流量)
+
+
+
+
+
+ {/* 分配限制 */}
+
+
+
+ 每日最大分配量
+
+ {maxPerDay || 0} 人/天
+
+
+
+
+
+ 限制每天最多分配的流量数量
+
+
+
+ {/* 时间限制 */}
+
+
+
+ 全天分配
+ 自定义时间段
+
+
+ {timeType === 2 && (
+ ({
+ validator(_, value) {
+ if (getFieldValue("timeType") === 1) {
+ return Promise.resolve();
+ }
+ if (value && value.length === 2) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error("请选择开始和结束时间"));
+ },
+ }),
+ ]}
+ >
+
+
+ )}
+
+
+ {/* 客服选择 */}
+
+ 客服选择
+
+
{
+ setAccountGroupsOptions(accounts);
+ }}
+ placeholder="请选择客服"
+ showSelectedList={true}
+ selectedListMaxHeight={300}
+ accountGroups={accountGroups}
+ />
+
+
{current === 1 && (
-
+
目标设置
{
deviceGroups={deviceGroups}
/>
-
+
)}
{current === 2 && (
-
+
+
+
)}
diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss
index 03733959..e2e68567 100644
--- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss
+++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss
@@ -12,6 +12,43 @@
padding: 0 16px;
}
+.infoBox {
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ border-radius: 12px;
+ padding: 12px 14px;
+ margin-bottom: 16px;
+ font-size: 13px;
+ color: #1e40af;
+}
+
+.section {
+ margin-bottom: 20px;
+}
+
+.sectionTitle {
+ font-size: 15px;
+ font-weight: 600;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: #1e293b;
+}
+
+.sectionDot {
+ width: 4px;
+ height: 14px;
+ border-radius: 2px;
+ background: #2563eb;
+}
+
+.sectionTitleIndependent {
+ .sectionDot {
+ background: #fb923c;
+ }
+}
+
.ruleCard {
background: #fff;
border-radius: 12px;
diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx
index 1a220b6f..34969bfc 100644
--- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx
+++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.tsx
@@ -75,7 +75,12 @@ const TrafficDistributionList: React.FC = () => {
limit: PAGE_SIZE,
keyword,
});
- setList(res?.list || []);
+ const rawList: DistributionRule[] = res?.list || [];
+ const normalized = rawList.map(item => ({
+ ...item,
+ planType: (item as any).planType ?? item.config?.planType ?? 1,
+ }));
+ setList(normalized);
setTotal(Number(res?.total) || 0);
} catch (e) {
message.error("获取流量分发列表失败");
@@ -93,6 +98,9 @@ const TrafficDistributionList: React.FC = () => {
fetchList(page, searchQuery);
};
+ const globalRules = list.filter(item => item.planType === 0);
+ const independentRules = list.filter(item => item.planType !== 0);
+
// 优化:菜单点击事件,menuLoadingId标记当前item
const handleMenuClick = async (key: string, item: DistributionRule) => {
setMenuLoadingId(item.id);
@@ -322,6 +330,45 @@ const TrafficDistributionList: React.FC = () => {
);
};
+ let content: React.ReactNode;
+ if (loading) {
+ content = ;
+ } else if (list.length === 0) {
+ content = 暂无数据
;
+ } else {
+ content = (
+ <>
+ {globalRules.length > 0 && (
+
+ 全局流量分发计划将作用于所有账号和设备,请谨慎配置每日分配量与时间段。
+
+ )}
+
+ {globalRules.length > 0 && (
+
+
+
+ 全局流量分发计划
+
+ {globalRules.map(renderCard)}
+
+ )}
+
+ {independentRules.length > 0 && (
+
+
+
+ 独立流量分发计划
+
+ {independentRules.map(renderCard)}
+
+ )}
+ >
+ );
+ }
+
return (
{
}
>
-
- {loading ? (
-
- ) : list.length > 0 ? (
- list.map(renderCard)
- ) : (
-
暂无数据
- )}
-
+
{content}
{/* 账号列表弹窗 */}