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

列表样式存一下
This commit is contained in:
笔记本里的永平
2025-07-24 16:22:39 +08:00
parent 2a40a40697
commit 9a33801f12
16 changed files with 487 additions and 29 deletions

View File

@@ -1,8 +0,0 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
const TrafficDistributionDetail: React.FC = () => {
return <PlaceholderPage title="流量分发详情" />;
};
export default TrafficDistributionDetail;

View File

@@ -1,8 +0,0 @@
import React from "react";
import PlaceholderPage from "@/components/PlaceholderPage";
const NewDistribution: React.FC = () => {
return <PlaceholderPage title="新建流量分发" />;
};
export default NewDistribution;

View File

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

View File

@@ -0,0 +1,3 @@
export default function TrafficDistributionDetail() {
return <div>TrafficDistributionDetail</div>;
}

View File

@@ -0,0 +1,3 @@
export default function TrafficDistributionForm() {
return <div>TrafficDistributionForm</div>;
}

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

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

View File

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

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

View File

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