feat: 本次提交更新内容如下
列表样式存一下
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficDistributionDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="流量分发详情" />;
|
||||
};
|
||||
|
||||
export default TrafficDistributionDetail;
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewDistribution: React.FC = () => {
|
||||
return <PlaceholderPage title="新建流量分发" />;
|
||||
};
|
||||
|
||||
export default NewDistribution;
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficDistribution: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="流量分发" showAddButton addButtonText="新建分发" />
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficDistribution;
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function TrafficDistributionDetail() {
|
||||
return <div>TrafficDistributionDetail</div>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function TrafficDistributionForm() {
|
||||
return <div>TrafficDistributionForm</div>;
|
||||
}
|
||||
10
nkebao/src/pages/workspace/traffic-distribution/list/api.ts
Normal file
10
nkebao/src/pages/workspace/traffic-distribution/list/api.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取流量分发规则列表
|
||||
export function fetchDistributionRuleList(params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
}): Promise<any> {
|
||||
return request("/v1/workbench/list?type=5", params, "GET");
|
||||
}
|
||||
133
nkebao/src/pages/workspace/traffic-distribution/list/data.ts
Normal file
133
nkebao/src/pages/workspace/traffic-distribution/list/data.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// 流量分发相关类型定义
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "online" | "offline" | "busy";
|
||||
battery: number;
|
||||
location: string;
|
||||
wechatAccounts: number;
|
||||
dailyAddLimit: number;
|
||||
todayAdded: number;
|
||||
}
|
||||
|
||||
export interface WechatAccount {
|
||||
id: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
avatar: string;
|
||||
deviceId: string;
|
||||
status: "normal" | "limited" | "blocked";
|
||||
friendCount: number;
|
||||
dailyAddLimit: number;
|
||||
}
|
||||
|
||||
export interface CustomerService {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
status: "online" | "offline" | "busy";
|
||||
assignedUsers: number;
|
||||
}
|
||||
|
||||
export interface TrafficPool {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
userCount?: number;
|
||||
tags?: string[];
|
||||
createdAt?: string;
|
||||
deviceIds?: string[];
|
||||
}
|
||||
|
||||
export interface RFMScore {
|
||||
recency: number;
|
||||
frequency: number;
|
||||
monetary: number;
|
||||
total: number;
|
||||
segment: string;
|
||||
priority: "high" | "medium" | "low";
|
||||
}
|
||||
|
||||
export interface UserTag {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface UserInteraction {
|
||||
id: string;
|
||||
type: "message" | "purchase" | "view" | "click";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface TrafficUser {
|
||||
id: string;
|
||||
avatar: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
phone: string;
|
||||
region: string;
|
||||
note: string;
|
||||
status: "pending" | "added" | "failed" | "duplicate";
|
||||
addTime: string;
|
||||
source: string;
|
||||
scenario: string;
|
||||
deviceId: string;
|
||||
wechatAccountId: string;
|
||||
customerServiceId: string;
|
||||
poolIds: string[];
|
||||
tags: UserTag[];
|
||||
rfmScore: RFMScore;
|
||||
lastInteraction: string;
|
||||
totalSpent: number;
|
||||
interactionCount: number;
|
||||
conversionRate: number;
|
||||
isDuplicate: boolean;
|
||||
mergedAccounts: string[];
|
||||
addStatus: "not_added" | "adding" | "added" | "failed";
|
||||
interactions: UserInteraction[];
|
||||
}
|
||||
|
||||
// 流量分发规则类型
|
||||
export interface DistributionRule {
|
||||
id: number;
|
||||
companyId: number;
|
||||
name: string;
|
||||
type: number;
|
||||
status: number;
|
||||
autoStart: number;
|
||||
userId: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
config: {
|
||||
id: number;
|
||||
workbenchId: number;
|
||||
distributeType: number;
|
||||
maxPerDay: number;
|
||||
timeType: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
account: (string | number)[];
|
||||
devices: string[];
|
||||
pools: string[];
|
||||
exp: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
lastUpdated: string;
|
||||
total: {
|
||||
dailyAverage: number;
|
||||
totalAccounts: number;
|
||||
deviceCount: number;
|
||||
poolCount: string | number;
|
||||
totalUsers: number;
|
||||
};
|
||||
};
|
||||
creatorName: string;
|
||||
auto_like: any;
|
||||
moments_sync: any;
|
||||
group_push: any;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
.listToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.ruleList {
|
||||
padding:0 16px;
|
||||
}
|
||||
|
||||
.ruleCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ececec;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
.ruleCard:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
border-color: #b3e5fc;
|
||||
}
|
||||
|
||||
.ruleHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ruleName {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.ruleStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ruleSwitch {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.ruleMenu {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ruleMeta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
color: #444;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ruleMetaItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.ruleMetaItem:not(:last-child) {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ruleDivider {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin: 12px 0 10px 0;
|
||||
}
|
||||
|
||||
.ruleStats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ruleStatsItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.ruleStatsItem:not(:last-child) {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ruleFooter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.ruleFooterIcon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
background: #fff;
|
||||
}
|
||||
215
nkebao/src/pages/workspace/traffic-distribution/list/index.tsx
Normal file
215
nkebao/src/pages/workspace/traffic-distribution/list/index.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
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,
|
||||
ClockCircleOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
CalendarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const statusMap = {
|
||||
0: { text: "待处理", color: "default" },
|
||||
1: { text: "进行中", color: "processing" },
|
||||
2: { text: "已暂停", color: "warning" },
|
||||
3: { text: "已完成", color: "success" },
|
||||
4: { text: "失败", color: "error" },
|
||||
};
|
||||
|
||||
const TrafficDistributionList: React.FC = () => {
|
||||
const [list, setList] = useState<DistributionRule[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchList(page, searchQuery);
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const fetchList = async (pageNum = 1, keyword = "") => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchDistributionRuleList({
|
||||
page: pageNum,
|
||||
limit: PAGE_SIZE,
|
||||
keyword,
|
||||
});
|
||||
setList(res?.list || []);
|
||||
setTotal(Number(res?.total) || 0);
|
||||
} catch (e) {
|
||||
message.error("获取流量分发列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (val: string) => {
|
||||
setSearchQuery(val);
|
||||
setPage(1);
|
||||
fetchList(1, val);
|
||||
};
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
setPage(p);
|
||||
fetchList(p, searchQuery);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchList(page, searchQuery);
|
||||
};
|
||||
|
||||
const renderCard = (item: DistributionRule) => (
|
||||
<div key={item.id} className={style.ruleCard}>
|
||||
<div className={style.ruleHeader}>
|
||||
<span className={style.ruleName}>{item.name}</span>
|
||||
<div className={style.ruleStatus}>
|
||||
<Tag
|
||||
color={statusMap[item.status]?.color || "default"}
|
||||
style={{ fontWeight: 500, fontSize: 13 }}
|
||||
>
|
||||
{statusMap[item.status]?.text || "未知"}
|
||||
</Tag>
|
||||
<Switch
|
||||
className={style.ruleSwitch}
|
||||
checked={item.status === 1}
|
||||
size="small"
|
||||
disabled
|
||||
/>
|
||||
<EllipsisOutlined className={style.ruleMenu} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.ruleMeta}>
|
||||
<div className={style.ruleMetaItem}>
|
||||
<UserOutlined style={{ color: "#3b82f6", fontSize: 18 }} />
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
{item.config?.account?.length || 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#888" }}>分发账号</div>
|
||||
</div>
|
||||
<div className={style.ruleMetaItem}>
|
||||
<AppstoreOutlined style={{ color: "#22c55e", fontSize: 18 }} />
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
{item.config?.devices?.length || 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#888" }}>分发设备</div>
|
||||
</div>
|
||||
<div className={style.ruleMetaItem}>
|
||||
<ClusterOutlined style={{ color: "#f59e42", fontSize: 18 }} />
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
{item.config?.pools?.length || 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.ruleDivider} />
|
||||
<div className={style.ruleStats}>
|
||||
<div className={style.ruleStatsItem}>
|
||||
<BarChartOutlined style={{ color: "#3b82f6", fontSize: 16 }} />
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
{item.config?.total?.dailyAverage || 0}
|
||||
</span>
|
||||
<div style={{ fontSize: 13, color: "#888", marginTop: 2 }}>
|
||||
日均分发量
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.ruleStatsItem}>
|
||||
<TeamOutlined style={{ color: "#f97316", fontSize: 16 }} />
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
{item.config?.total?.totalUsers || 0}
|
||||
</span>
|
||||
<div style={{ fontSize: 13, color: "#888", marginTop: 2 }}>
|
||||
总流量池数量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.ruleFooter}>
|
||||
<span>
|
||||
<ClockCircleOutlined className={style.ruleFooterIcon} />
|
||||
上次执行:{item.config?.lastUpdated || "-"}
|
||||
</span>
|
||||
<span>
|
||||
<CalendarOutlined className={style.ruleFooterIcon} />
|
||||
创建时间:{item.createTime || "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
// <PopupHeader
|
||||
// title="流量分发"
|
||||
// searchQuery={searchQuery}
|
||||
// setSearchQuery={handleSearch}
|
||||
// loading={loading}
|
||||
// onRefresh={handleRefresh}
|
||||
// />
|
||||
|
||||
<>
|
||||
<NavCommon title="流量分发" />
|
||||
{/* 搜索栏 */}
|
||||
<div className="search-bar">
|
||||
<div className="search-input-wrapper">
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
className="refresh-btn"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
loading={loading}
|
||||
footer={
|
||||
<div className={style.pagination}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style.ruleList}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : list.length > 0 ? (
|
||||
list.map(renderCard)
|
||||
) : (
|
||||
<div className={style.empty}>暂无数据</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficDistributionList;
|
||||
@@ -12,9 +12,9 @@ import MomentsSync from "@/pages/workspace/moments-sync/MomentsSync";
|
||||
import MomentsSyncDetail from "@/pages/workspace/moments-sync/Detail";
|
||||
import NewMomentsSync from "@/pages/workspace/moments-sync/new/index";
|
||||
import AIAssistant from "@/pages/workspace/ai-assistant/AIAssistant";
|
||||
import TrafficDistribution from "@/pages/workspace/traffic-distribution/TrafficDistribution";
|
||||
import TrafficDistributionDetail from "@/pages/workspace/traffic-distribution/Detail";
|
||||
import NewDistribution from "@/pages/workspace/traffic-distribution/NewDistribution";
|
||||
import TrafficDistribution from "@/pages/workspace/traffic-distribution/list/index";
|
||||
import TrafficDistributionDetail from "@/pages/workspace/traffic-distribution/detail/index";
|
||||
import NewDistribution from "@/pages/workspace/traffic-distribution/form/index";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const workspaceRoutes = [
|
||||
|
||||
Reference in New Issue
Block a user