refactor(router): 移除未使用的页面文件并添加用户设置路由

移除多个未使用的页面组件、样式和API文件,包括流量分发、自动建群、朋友圈同步等功能模块。同时添加新的用户设置路由配置到移动端路由模块中。这些变更旨在清理代码库并支持新的用户设置功能。
This commit is contained in:
超级老白兔
2025-09-11 17:11:30 +08:00
parent fbfce10afb
commit 60173a0efb
107 changed files with 10 additions and 21213 deletions

View File

@@ -1,26 +0,0 @@
import request from "@/api/request";
// 获取场景列表
export function getScenarios(params: any) {
return request("/v1/plan/scenes", params, "GET");
}
// 获取场景详情
export function getScenarioDetail(id: string) {
return request(`/v1/scenarios/${id}`, {}, "GET");
}
// 创建场景
export function createScenario(data: any) {
return request("/v1/scenarios", data, "POST");
}
// 更新场景
export function updateScenario(id: string, data: any) {
return request(`/v1/scenarios/${id}`, data, "PUT");
}
// 删除场景
export function deleteScenario(id: string) {
return request(`/v1/scenarios/${id}`, {}, "DELETE");
}

View File

@@ -1,329 +0,0 @@
// 导航栏样式
.nav-title {
font-size: 18px;
font-weight: 600;
color: var(--primary-color);
}
.nav-text {
color: var(--primary-color);
}
.nav-right {
margin-left: 4px;
font-size: 12px;
}
.new-plan-btn {
border-radius: 20px;
padding: 4px 12px;
height: 32px;
font-size: 12px;
background: var(--primary-gradient);
border: none;
box-shadow: 0 2px 8px var(--primary-shadow);
&:active {
transform: translateY(1px);
box-shadow: 0 1px 4px var(--primary-shadow);
}
}
// 页面容器
.scene-page {
background: #f5f6fa;
min-height: 100vh;
padding: 0 0 60px 0;
}
.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 30px;
}
// 错误提示
.error-notice {
margin-bottom: 12px;
padding: 8px 12px;
background: #fff2e8;
border: 1px solid #ffd591;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(255, 213, 145, 0.2);
}
.error-notice-text {
font-size: 12px;
color: #d46b08;
font-weight: 500;
}
// 加载状态
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 12px;
}
.loading-text {
font-size: 14px;
color: #666;
font-weight: 500;
}
// 错误状态
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
}
.error-text {
font-size: 14px;
color: #ff4d4f;
text-align: center;
font-weight: 500;
}
.retry-button {
min-width: 100px;
border-radius: 20px;
background: var(--primary-gradient);
border: none;
box-shadow: 0 2px 8px var(--primary-shadow);
}
// 页面头部
.scene-header {
margin-bottom: 16px;
text-align: center;
}
.header-title {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 6px;
}
.header-icon {
font-size: 20px;
color: var(--primary-color);
}
.header-subtitle {
font-size: 12px;
color: #666;
line-height: 1.4;
}
// 场景列表
.scenarios-list {
display: flex;
flex-direction: column;
gap: 10px;
}
// 场景卡片
.scenario-item {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
.scenarios-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
}
.scenario-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition:
box-shadow 0.2s,
transform 0.2s;
cursor: pointer;
overflow: hidden;
&:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px) scale(1.02);
}
}
.card-inner {
display: flex;
flex-direction: column;
align-items: center;
padding: 18px 10px 14px 10px;
}
.card-img-wrap {
margin-bottom: 8px;
}
.card-img-bg {
width: 48px;
height: 48px;
background: #f0f2f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.card-img {
width: 32px;
height: 32px;
object-fit: contain;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1677ff;
text-align: center;
margin-bottom: 2px;
}
.card-desc {
font-size: 12px;
color: #888;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-stats {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 4px;
}
.card-count {
font-size: 13px;
color: #666;
}
.card-growth {
font-size: 12px;
color: #52c41a;
display: flex;
align-items: center;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
.empty-icon {
font-size: 36px;
margin-bottom: 12px;
opacity: 0.6;
}
.empty-text {
font-size: 14px;
color: #666;
margin-bottom: 16px;
font-weight: 500;
}
.empty-action {
border-radius: 20px;
background: var(--primary-gradient);
border: none;
box-shadow: 0 2px 8px var(--primary-shadow);
padding: 6px 16px;
font-weight: 500;
font-size: 12px;
}
// 响应式设计
@media (max-width: 480px) {
.scenario-card {
padding: 14px 16px;
min-height: 70px;
}
.scenario-icon {
width: 46px;
height: 46px;
}
.scenario-image {
width: 28px;
height: 28px;
}
.scenario-name {
font-size: 15px;
}
.stat-text {
font-size: 12px;
}
.scenario-growth {
font-size: 15px;
}
.growth-icon {
font-size: 13px;
}
}
@media (max-width: 500px) {
.scenarios-grid {
gap: 10px;
padding: 10px;
}
.card-inner {
padding: 12px 4px 10px 4px;
}
.card-img-bg {
width: 60px;
height: 60px;
}
.card-img {
width: 40px;
height: 40px;
}
.card-title {
font-size: 15px;
}
.card-desc {
font-size: 11px;
}
.card-count {
font-size: 12px;
}
.card-growth {
font-size: 11px;
}
}

View File

@@ -1,159 +0,0 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Toast } from "antd-mobile";
import { PlusOutlined, RiseOutlined } from "@ant-design/icons";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import { getScenarios } from "./api";
import style from "./index.module.scss";
interface Scenario {
id: string;
name: string;
image: string;
description?: string;
count: number;
growth: string;
status: number;
}
const Scene: React.FC = () => {
const navigate = useNavigate();
const [scenarios, setScenarios] = useState<Scenario[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
const fetchScenarios = async () => {
setLoading(true);
setError("");
try {
const response = await getScenarios({ page: 1, limit: 20 });
const transformedScenarios: Scenario[] = response.map((item: any) => ({
id: item.id.toString(),
name: item.name,
image:
item.image ||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png",
description: "",
count: item.count,
growth: item.growth,
status: item.status,
}));
setScenarios(transformedScenarios);
} catch (error) {
setError("获取场景数据失败,请稍后重试");
Toast.show({
content: "获取场景数据失败,请稍后重试",
position: "top",
});
} finally {
setLoading(false);
}
};
fetchScenarios();
}, []);
const handleScenarioClick = (scenarioId: string, scenarioName: string) => {
navigate(
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`,
);
};
const handleNewPlan = () => {
navigate("/scenarios/new");
};
if (error && scenarios.length === 0) {
return (
<Layout
header={
<NavCommon
left={<></>}
title="场景获客"
right={
<Button size="small" color="primary" onClick={handleNewPlan}>
<PlusOutlined />
</Button>
}
/>
}
footer={<MeauMobile activeKey="scenarios" />}
>
<div className={style["error"]}>
<div className={style["error-text"]}>{error}</div>
<Button color="primary" onClick={() => window.location.reload()}>
</Button>
</div>
</Layout>
);
}
return (
<Layout
loading={loading}
header={
<NavCommon
left={<div className="nav-title"></div>}
title={""}
right={
<Button
size="small"
color="primary"
onClick={handleNewPlan}
className="new-plan-btn"
>
<PlusOutlined />
</Button>
}
/>
}
footer={<MeauMobile activeKey="scenarios" />}
>
<div className={style["scene-page"]}>
<div className={style["scenarios-grid"]}>
{scenarios.map(scenario => (
<div
key={scenario.id}
className={style["scenario-card"]}
onClick={() => handleScenarioClick(scenario.id, scenario.name)}
>
<div className={style["card-inner"]}>
<div className={style["card-img-wrap"]}>
<div className={style["card-img-bg"]}>
<img
src={scenario.image}
alt={scenario.name}
className={style["card-img"]}
onError={e => {
e.currentTarget.src =
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png";
}}
/>
</div>
</div>
<div className={style["card-title"]}>{scenario.name}</div>
<div className={style["card-stats"]}>
<span className={style["card-count"]}>
: {scenario.count}
</span>
<span className={style["card-growth"]}>
<RiseOutlined
style={{ fontSize: 14, color: "#52c41a", marginRight: 2 }}
/>
{scenario.growth}
</span>
</div>
</div>
</div>
))}
</div>
</div>
</Layout>
);
};
export default Scene;

View File

@@ -1,41 +0,0 @@
import request from "@/api/request";
import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
// ==================== 计划相关接口 ====================
// 获取计划列表
export function getPlanList(params: {
sceneId: string;
page: number;
pageSize: number;
}): Promise<PlanListResponse> {
return request(`/v1/plan/list`, params, "GET");
}
// 获取计划详情
export function getPlanDetail(planId: string): Promise<PlanDetail> {
return request(`/v1/plan/detail`, { planId }, "GET");
}
// 复制计划
export function copyPlan(planId: string): Promise<ApiResponse<any>> {
return request(`/v1/plan/copy`, { planId }, "GET");
}
// 删除计划
export function deletePlan(planId: string): Promise<ApiResponse<any>> {
return request(`/v1/plan/delete`, { planId }, "DELETE");
}
// 获取小程序二维码
export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
}
//获客列表
export function getUserList(planId: string, type: number) {
return request(`/v1/plan/getUserList`, { planId, type }, "GET");
}
//获客列表
export function getFriendRequestTaskStats(taskId: string) {
return request(`/v1/dashboard/friendRequestTaskStats`, { taskId }, "GET");
}

View File

@@ -1,186 +0,0 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./Popups.module.scss";
import { getUserList } from "../api";
interface AccountItem {
id: string | number;
nickname?: string;
wechatId?: string;
avatar?: string;
status?: string;
userinfo: {
alias: string;
nickname: string;
avatar: string;
wechatId: string;
};
phone?: string;
}
interface AccountListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const AccountListModal: React.FC<AccountListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取账号数据
const fetchAccounts = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await getUserList(ruleId.toString(), 1);
const accountData = detailRes?.list || [];
setAccounts(accountData);
} catch (error) {
console.error("获取账号详情失败:", error);
message.error("获取账号详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchAccounts();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 已添加账号列表` : "已添加账号列表";
const getStatusColor = (status?: string | number) => {
// 确保status是数字类型
const statusNum = Number(status);
switch (statusNum) {
case 0:
return "#faad14"; // 待添加 - 黄色警告色
case 1:
return "#1890ff"; // 添加中 - 蓝色进行中
case 2:
return "#ff4d4f"; // 添加失败 - 红色错误色
case 3:
return "#ff4d4f"; // 添加失败 - 红色错误色
case 4:
return "#52c41a"; // 已添加 - 绿色成功色
default:
return "#d9d9d9"; // 未知状态 - 灰色
}
};
const getStatusText = (status?: number) => {
switch (status) {
case 0:
return "待添加";
case 1:
return "添加中";
case 2:
return "请求已发送待通过";
case 3:
return "添加失败";
case 4:
return "已添加";
default:
return "未知状态";
}
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.accountModal}>
{/* 头部 */}
<div className={style.accountModalHeader}>
<h3 className={style.accountModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.accountModalClose}
/>
</div>
{/* 账号列表 */}
<div className={style.accountList}>
{loading ? (
<div className={style.accountLoading}>
<SpinLoading color="primary" />
<div className={style.accountLoadingText}>
...
</div>
</div>
) : accounts.length > 0 ? (
accounts.map((account, index) => (
<div key={account.id || index} className={style.accountItem}>
<div className={style.accountAvatar}>
<Avatar
src={account.userinfo.avatar}
style={{ "--size": "48px" }}
fallback={
(account.userinfo.nickname ||
account.userinfo.alias ||
"账号")[0]
}
/>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>
{account.userinfo.nickname ||
account.userinfo.alias ||
`账号${account.id}`}
</div>
<div className={style.accountWechatId}>
{account.userinfo.wechatId || "未绑定微信号"}
</div>
</div>
<div className={style.accountStatus}>
<span
className={style.statusDot}
style={{ backgroundColor: getStatusColor(account.status) }}
/>
<span className={style.statusText}>
{getStatusText(Number(account.status))}
</span>
</div>
</div>
))
) : (
<div className={style.accountEmpty}>
<div className={style.accountEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.accountModalFooter}>
<div className={style.accountStats}>
<span> {accounts.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default AccountListModal;

View File

@@ -1,175 +0,0 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./Popups.module.scss";
import { getPlanDetail } from "../api";
interface DeviceItem {
id: string | number;
memo?: string;
imei?: string;
wechatId?: string;
status?: "online" | "offline";
avatar?: string;
totalFriend?: number;
}
interface DeviceListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const DeviceListModal: React.FC<DeviceListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [devices, setDevices] = useState<DeviceItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取设备数据
const fetchDevices = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await getPlanDetail(ruleId.toString());
const deviceData = detailRes?.deviceGroupsOptions || [];
setDevices(deviceData);
} catch (error) {
console.error("获取设备详情失败:", error);
message.error("获取设备详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchDevices();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 分发设备列表` : "分发设备列表";
const getStatusColor = (status?: string) => {
return status === "online" ? "#52c41a" : "#ff4d4f";
};
const getStatusText = (status?: string) => {
return status === "online" ? "在线" : "离线";
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.deviceModal}>
{/* 头部 */}
<div className={style.deviceModalHeader}>
<h3 className={style.deviceModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.deviceModalClose}
/>
</div>
{/* 设备列表 */}
<div className={style.deviceList}>
{loading ? (
<div className={style.deviceLoading}>
<SpinLoading color="primary" />
<div className={style.deviceLoadingText}>...</div>
</div>
) : devices.length > 0 ? (
devices.map((device, index) => (
<div key={device.id || index} className={style.deviceItem}>
{/* 顶部行IMEI */}
<div className={style.deviceHeaderRow}>
<span className={style.deviceImeiText}>
IMEI: {device.imei?.toUpperCase() || "-"}
</span>
</div>
{/* 主要内容区域:头像和详细信息 */}
<div className={style.deviceMainContent}>
{/* 头像 */}
<div className={style.deviceAvatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={style.deviceAvatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={style.deviceInfo}>
<div className={style.deviceInfoHeader}>
<h3 className={style.deviceName}>
{device.memo || "未命名设备"}
</h3>
<span
className={`${style.deviceStatusBadge} ${
device.status === "online"
? style.deviceStatusOnline
: style.deviceStatusOffline
}`}
>
{getStatusText(device.status)}
</span>
</div>
<div className={style.deviceInfoList}>
<div className={style.deviceInfoItem}>
<span className={style.deviceInfoLabel}>:</span>
<span className={style.deviceInfoValue}>
{device.wechatId || "未绑定"}
</span>
</div>
<div className={style.deviceInfoItem}>
<span className={style.deviceInfoLabel}>:</span>
<span
className={`${style.deviceInfoValue} ${style.deviceFriendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div>
</div>
</div>
</div>
))
) : (
<div className={style.deviceEmpty}>
<div className={style.deviceEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.deviceModalFooter}>
<div className={style.deviceStats}>
<span> {devices.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default DeviceListModal;

View File

@@ -1,175 +0,0 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./Popups.module.scss";
import { getUserList } from "../api";
interface AccountItem {
id: string | number;
nickname?: string;
wechatId?: string;
avatar?: string;
status?: string;
userinfo: {
alias: string;
nickname: string;
avatar: string;
wechatId: string;
};
phone?: string;
}
interface AccountListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const AccountListModal: React.FC<AccountListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取账号数据
const fetchAccounts = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await getUserList(ruleId.toString(), 2);
const accountData = detailRes?.list || [];
setAccounts(accountData);
} catch (error) {
console.error("获取账号详情失败:", error);
message.error("获取账号详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchAccounts();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 已添加账号列表` : "已添加账号列表";
const getStatusColor = (status?: string) => {
switch (status) {
case "normal":
return "#52c41a";
case "limited":
return "#faad14";
case "blocked":
return "#ff4d4f";
default:
return "#d9d9d9";
}
};
const getStatusText = (status?: string) => {
switch (status) {
case "normal":
return "正常";
case "limited":
return "受限";
case "blocked":
return "封禁";
default:
return "未知";
}
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.accountModal}>
{/* 头部 */}
<div className={style.accountModalHeader}>
<h3 className={style.accountModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.accountModalClose}
/>
</div>
{/* 账号列表 */}
<div className={style.accountList}>
{loading ? (
<div className={style.accountLoading}>
<SpinLoading color="primary" />
<div className={style.accountLoadingText}>
...
</div>
</div>
) : accounts.length > 0 ? (
accounts.map((account, index) => (
<div key={account.id || index} className={style.accountItem}>
<div className={style.accountAvatar}>
<Avatar
src={account.userinfo.avatar}
style={{ "--size": "48px" }}
fallback={
(account.userinfo.nickname ||
account.userinfo.alias ||
"账号")[0]
}
/>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>
{account.userinfo.nickname ||
account.userinfo.alias ||
`账号${account.id}`}
</div>
<div className={style.accountWechatId}>
{account.userinfo.wechatId || "未绑定微信号"}
</div>
</div>
<div className={style.accountStatus}>
<span
className={style.statusDot}
style={{ backgroundColor: getStatusColor(account.status) }}
/>
<span className={style.statusText}>
{getStatusText(account.status)}
</span>
</div>
</div>
))
) : (
<div className={style.accountEmpty}>
<div className={style.accountEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.accountModalFooter}>
<div className={style.accountStats}>
<span> {accounts.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default AccountListModal;

View File

@@ -1,161 +0,0 @@
import React, { useEffect, useState } from "react";
import { Popup, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./Popups.module.scss";
import { getFriendRequestTaskStats } from "../api";
import LineChart2 from "@/components/LineChart2";
interface StatisticsData {
totalAll: number;
totalError: number;
totalPass: number;
totalPassRate: number;
totalSuccess: number;
totalSuccessRate: number;
}
interface PoolListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const PoolListModal: React.FC<PoolListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [statistics, setStatistics] = useState<StatisticsData>({
totalAll: 0,
totalError: 0,
totalPass: 0,
totalPassRate: 0,
totalSuccess: 0,
totalSuccessRate: 0,
});
const [xData, setXData] = useState<any[]>([]);
const [yData, setYData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
setLoading(true);
getFriendRequestTaskStats(ruleId.toString())
.then(res => {
console.log(res);
setXData(res.dateArray);
setYData([
res.allNumArray,
res.errorNumArray,
res.passNumArray,
res.passRateArray,
res.successNumArray,
res.successRateArray,
]);
setStatistics(res.totalStats);
setLoading(false);
})
.finally(() => {
setLoading(false);
});
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据";
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.poolModal}>
{/* 头部 */}
<div className={style.poolModalHeader}>
<h3 className={style.poolModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.poolModalClose}
/>
</div>
{/* 统计数据表格 */}
<div className={style.statisticsContent}>
{loading ? (
<div className={style.statisticsLoading}>
<SpinLoading color="primary" />
<div className={style.statisticsLoadingText}>
...
</div>
</div>
) : (
<div className={style.statisticsTable}>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalAll}
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalError}
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalSuccess}
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalError}
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalPass}
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalSuccessRate}%
</div>
</div>
<div className={style.statisticsRow}>
<div className={style.statisticsLabel}></div>
<div className={style.statisticsValue}>
{statistics.totalPassRate}%
</div>
</div>
</div>
)}
</div>
{/* 趋势图占位 */}
<div className={style.trendChart}>
<div className={style.chartTitle}></div>
<div className={style.chartPlaceholder}>
<LineChart2 title="趋势图" xData={xData} yData={yData} />
</div>
</div>
</div>
</Popup>
);
};
export default PoolListModal;

View File

@@ -1,744 +0,0 @@
.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;
transition: background-color 0.2s ease;
}
.ruleMetaItem:not(:last-child) {
border-right: 1px solid #f0f0f0;
}
.ruleMetaItem:hover {
background-color: #f8f9fa;
border-radius: 6px;
}
.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: 12px;
color: #888;
margin-top: 6px;
align-items: center;
}
.ruleFooterIcon {
margin-right: 4px;
vertical-align: middle;
font-size: 15px;
position: relative;
top: -2px;
}
.empty {
text-align: center;
color: #bbb;
padding: 40px 0;
}
.pagination {
display: flex;
justify-content: center;
padding: 16px 0;
background: #fff;
}
// 账号列表弹窗样式
.accountModal {
height: 100%;
display: flex;
flex-direction: column;
}
.accountModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.accountModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.accountModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.accountList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.accountItem {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.accountItem:last-child {
border-bottom: none;
}
.accountAvatar {
margin-right: 12px;
flex-shrink: 0;
}
.accountInfo {
flex: 1;
min-width: 0;
}
.accountName {
font-size: 16px;
font-weight: 500;
color: #222;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.accountWechatId {
font-size: 14px;
color: #888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.accountStatus {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.statusDot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.statusText {
font-size: 13px;
color: #666;
}
.accountEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.accountEmptyText {
font-size: 16px;
}
.accountLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.accountLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.accountModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.accountStats {
text-align: center;
font-size: 14px;
color: #666;
}
// 设备列表弹窗样式
.deviceModal {
height: 100%;
display: flex;
flex-direction: column;
}
.deviceModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.deviceModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.deviceModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.deviceList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.deviceItem {
background: #fff;
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #ececec;
transition: all 0.2s ease;
}
.deviceItem:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.deviceHeaderRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.deviceImeiText {
font-size: 13px;
color: #888;
font-weight: 500;
}
.deviceMainContent {
display: flex;
align-items: center;
}
.deviceAvatar {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.deviceAvatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.deviceAvatarText {
color: #fff;
font-size: 18px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.deviceInfo {
flex: 1;
min-width: 0;
}
.deviceInfoHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.deviceName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
}
.deviceStatusBadge {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
}
.deviceStatusOnline {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
.deviceStatusOffline {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
.deviceInfoList {
display: flex;
flex-direction: column;
gap: 4px;
}
.deviceInfoItem {
display: flex;
align-items: center;
font-size: 13px;
}
.deviceInfoLabel {
color: #888;
margin-right: 6px;
min-width: 50px;
}
.deviceInfoValue {
color: #444;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deviceFriendCount {
color: #1890ff;
font-weight: 500;
}
.deviceEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.deviceEmptyText {
font-size: 16px;
}
.deviceLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.deviceLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.deviceModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.deviceStats {
text-align: center;
font-size: 14px;
color: #666;
}
// 流量池列表弹窗样式
.poolModal {
height: 100%;
display: flex;
flex-direction: column;
}
.poolModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.poolModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.poolModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.poolList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.poolItem {
background: #fff;
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #ececec;
transition: all 0.2s ease;
}
.poolItem:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.poolMainContent {
display: flex;
align-items: flex-start;
}
.poolIcon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.poolIconText {
color: #fff;
font-size: 18px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.poolInfo {
flex: 1;
min-width: 0;
}
.poolInfoHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.poolName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
}
.poolUserCount {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
flex-shrink: 0;
}
.poolInfoList {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.poolInfoItem {
display: flex;
align-items: center;
font-size: 13px;
}
.poolInfoLabel {
color: #888;
margin-right: 6px;
min-width: 60px;
}
.poolInfoValue {
color: #444;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.poolTags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.poolTag {
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
background: rgba(0, 0, 0, 0.05);
color: #666;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.poolLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.poolLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.poolEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.poolEmptyText {
font-size: 16px;
}
.poolModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.poolStats {
text-align: center;
font-size: 14px;
color: #666;
}
// 统计数据弹窗样式
.statisticsContent {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.statisticsLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.statisticsLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.statisticsTable {
padding: 16px 0;
}
.statisticsRow {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.statisticsRow:last-child {
border-bottom: none;
}
.statisticsLabel {
font-size: 15px;
color: #666;
font-weight: 500;
}
.statisticsValue {
font-size: 16px;
color: #222;
font-weight: 600;
}
.trendChart {
padding: 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.chartTitle {
font-size: 16px;
font-weight: 600;
color: #222;
margin-bottom: 16px;
}
.chartPlaceholder {
height: 200px;
background: #f8f9fa;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #d9d9d9;
}
.chartNote {
font-size: 14px;
color: #888;
text-align: center;
}

View File

@@ -1,60 +0,0 @@
export interface Task {
id: string;
name: string;
status: number;
created_at: string;
updated_at: string;
enabled: boolean;
total_customers?: number;
today_customers?: number;
lastUpdated?: string;
stats?: {
devices?: number;
acquired?: number;
added?: number;
};
reqConf?: {
device?: string[];
selectedDevices?: string[];
};
acquiredCount?: number;
addedCount?: number;
passRate?: number;
passCount?: number;
}
export interface ApiSettings {
apiKey: string;
webhookUrl: string;
taskId: string;
}
// API响应相关类型
export interface TextUrl {
apiKey: string;
originalString?: string;
sign?: string;
fullUrl: string;
}
export interface PlanDetail {
id: number;
name: string;
scenario: number;
enabled: boolean;
status: number;
apiKey: string;
textUrl: TextUrl;
[key: string]: any;
}
export interface ApiResponse<T> {
code: number;
msg?: string;
data: T;
}
export interface PlanListResponse {
list: Task[];
total: number;
}

View File

@@ -1,444 +0,0 @@
.scenario-list-page {
padding: 0 16px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.search-bar {
display: flex;
gap: 12px;
align-items: center;
padding: 16px;
}
.search-input-wrapper {
position: relative;
flex: 1;
.ant-input {
border-radius: 8px;
height: 40px;
}
}
.plan-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.pagination-container {
display: flex;
justify-content: center;
padding: 14px 0;
background: white;
border-radius: 12px;
margin-top: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
:global(.ant-pagination) {
.ant-pagination-item {
border-radius: 6px;
border: 1px solid #d9d9d9;
&:hover {
border-color: var(--primary-color);
}
&.ant-pagination-item-active {
background: var(--primary-color);
border-color: var(--primary-color);
a {
color: white;
}
}
}
.ant-pagination-prev,
.ant-pagination-next {
border-radius: 6px;
border: 1px solid #d9d9d9;
&:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
}
}
}
.plan-item {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.plan-name {
font-size: 16px;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 12px;
}
.plan-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.more-btn {
padding: 4px;
min-width: auto;
height: 28px;
width: 28px;
border-radius: 4px;
&:hover {
background-color: #f5f5f5;
}
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.stat-item {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
text-align: center;
border: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
&:hover {
background: #e6f7ff;
border-color: #91d5ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(24, 144, 255, 0.1);
}
&:hover::after {
opacity: 1;
}
}
.stat-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
font-weight: 500;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #333;
line-height: 1.2;
}
.plan-footer {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
display: flex;
justify-content: space-between;
}
.last-execution {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #999;
svg {
font-size: 14px;
color: #999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-text {
color: #999;
font-size: 14px;
margin-bottom: 20px;
}
.create-first-btn {
height: 40px;
padding: 0 24px;
border-radius: 20px;
}
// 加载更多按钮样式
.load-more-container {
display: flex;
justify-content: center;
padding: 20px 0;
}
.load-more-btn {
height: 44px;
padding: 0 32px;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
}
// 没有更多数据提示样式
.no-more-data {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
color: #999;
font-size: 14px;
span {
position: relative;
padding: 0 20px;
&::before,
&::after {
content: "";
position: absolute;
top: 50%;
width: 40px;
height: 1px;
background-color: #e0e0e0;
}
&::before {
left: -50px;
}
&::after {
right: -50px;
}
}
}
.action-menu-dialog {
background: white;
border-radius: 16px 16px 0 0;
padding: 20px;
max-height: 60vh;
display: flex;
flex-direction: column;
}
.action-menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f5f5;
}
&.danger {
color: #ff4d4f;
&:hover {
background-color: #fff2f0;
}
}
}
.action-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.action-text {
font-size: 16px;
font-weight: 500;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.dialog-content {
flex: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.qr-dialog {
background: white;
border-radius: 16px;
padding: 20px;
width: 100%;
}
.qr-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
gap: 16px;
color: #666;
font-size: 14px;
}
.qr-image {
width: 100%;
max-width: 200px;
height: auto;
border-radius: 8px;
}
.qr-error {
text-align: center;
color: #ff4d4f;
font-size: 14px;
padding: 40px 20px;
}
.qr-link-section {
margin-top: 20px;
width: 100%;
padding: 0 10px;
}
.link-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
text-align: left;
}
.link-input-wrapper {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
@media (max-width: 480px) {
flex-direction: column;
gap: 12px;
}
}
.link-input {
flex: 1;
.ant-input {
border-radius: 8px;
font-size: 12px;
color: #666;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
@media (max-width: 480px) {
width: 100%;
}
}
.copy-button {
height: 32px;
padding: 0 12px;
border-radius: 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
flex-shrink: 0;
.anticon {
font-size: 12px;
}
@media (max-width: 480px) {
width: 100%;
justify-content: center;
}
}

View File

@@ -1,671 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Button,
Toast,
SpinLoading,
Dialog,
Popup,
Card,
Tag,
} from "antd-mobile";
import { Input, Pagination } from "antd";
import {
PlusOutlined,
CopyOutlined,
DeleteOutlined,
SettingOutlined,
SearchOutlined,
ReloadOutlined,
QrcodeOutlined,
EditOutlined,
MoreOutlined,
ClockCircleOutlined,
} from "@ant-design/icons";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import {
getPlanList,
getPlanDetail,
copyPlan,
deletePlan,
getWxMinAppCode,
} from "./api";
import style from "./index.module.scss";
import { Task, ApiSettings, PlanDetail } from "./data";
import PlanApi from "./planApi";
import { buildApiUrl } from "@/utils/apiUrl";
import DeviceListModal from "./components/DeviceListModal";
import AccountListModal from "./components/AccountListModal";
import OreadyAdd from "./components/OreadyAdd";
import PoolListModal from "./components/PoolListModal";
const ScenarioList: React.FC = () => {
const { scenarioId, scenarioName } = useParams<{
scenarioId: string;
scenarioName: string;
}>();
const navigate = useNavigate();
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [showApiDialog, setShowApiDialog] = useState(false);
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
apiKey: "",
webhookUrl: "",
taskId: "",
});
const [searchTerm, setSearchTerm] = useState("");
const [loadingTasks, setLoadingTasks] = useState(false);
const [showQrDialog, setShowQrDialog] = useState(false);
const [qrLoading, setQrLoading] = useState(false);
const [qrImg, setQrImg] = useState<any>("");
const [currentTaskId, setCurrentTaskId] = useState<string>("");
const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
// 设备列表弹窗状态
const [showDeviceList, setShowDeviceList] = useState(false);
const [currentTask, setCurrentTask] = useState<Task | null>(null);
// 账号列表弹窗状态
const [showAccountList, setShowAccountList] = useState(false);
// 已添加弹窗状态
const [showOreadyAdd, setShowOreadyAdd] = useState(false);
// 通过率弹窗状态
const [showPoolList, setShowPoolList] = useState(false);
// 分页相关状态
const [currentPage, setCurrentPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [total, setTotal] = useState(0);
const pageSize = 20;
// 获取计划列表数据
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
if (!scenarioId) return;
if (isLoadMore) {
setLoadingMore(true);
} else {
setLoadingTasks(true);
}
try {
const response = await getPlanList({
sceneId: scenarioId,
page: page,
pageSize: pageSize,
});
if (response && response.list) {
if (isLoadMore) {
// 加载更多时,追加数据
setTasks(prev => [...prev, ...response.list]);
} else {
// 首次加载或刷新时,替换数据
setTasks(response.list);
}
// 更新分页信息
setTotal(response.total || 0);
setHasMore(response.list.length === pageSize);
setCurrentPage(page);
}
} catch (error) {
console.error("获取计划列表失败:", error);
if (!isLoadMore) {
setTasks([]);
}
Toast.show({
content: "获取数据失败",
position: "top",
});
} finally {
if (isLoadMore) {
setLoadingMore(false);
} else {
setLoadingTasks(false);
}
}
};
useEffect(() => {
const fetchScenarioData = async () => {
if (!scenarioId) return;
setLoading(true);
try {
await fetchPlanList(1, false);
} catch (error) {
console.error("获取场景数据失败:", error);
setTasks([]);
} finally {
setLoading(false);
}
};
fetchScenarioData();
}, [scenarioId]);
// 分页改变处理
const handlePageChange = async (page: number) => {
setCurrentPage(page);
await fetchPlanList(page, false);
};
const handleCopyPlan = async (taskId: string) => {
const taskToCopy = tasks.find(task => task.id === taskId);
if (!taskToCopy) return;
try {
await copyPlan(taskId);
Toast.show({
content: `已成功复制"${taskToCopy.name}"`,
position: "top",
});
// 刷新列表
handleRefresh();
} catch (error) {
Toast.show({
content: "复制失败,请重试",
position: "top",
});
}
};
const handleDeletePlan = async (taskId: string) => {
const taskToDelete = tasks.find(task => task.id === taskId);
if (!taskToDelete) return;
const result = await Dialog.confirm({
content: `确定要删除"${taskToDelete.name}"吗?`,
confirmText: "删除",
cancelText: "取消",
});
if (result) {
try {
await deletePlan(taskId);
Toast.show({
content: "计划已删除",
position: "top",
});
// 刷新列表
handleRefresh();
} catch (error) {
Toast.show({
content: "删除失败,请重试",
position: "top",
});
}
}
};
const handleOpenApiSettings = async (taskId: string) => {
try {
const response: PlanDetail = await getPlanDetail(taskId);
if (response) {
// 处理webhook URL使用工具函数构建完整地址
const webhookUrl = buildApiUrl(
response.textUrl?.fullUrl || `webhook/${taskId}`,
);
setCurrentApiSettings({
apiKey: response.apiKey || "demo-api-key-123456",
webhookUrl: webhookUrl,
taskId: taskId,
});
setShowApiDialog(true);
}
} catch (error) {
Toast.show({
content: "获取计划接口失败",
position: "top",
});
}
};
const handleCreateNewPlan = () => {
navigate(`/scenarios/new/${scenarioId}`);
};
const handleShowQrCode = async (taskId: string) => {
setQrLoading(true);
setShowQrDialog(true);
setQrImg("");
setCurrentTaskId(taskId); // 设置当前任务ID
try {
const response = await getWxMinAppCode(taskId);
setQrImg(response);
} catch (error) {
Toast.show({
content: "获取二维码失败",
position: "top",
});
} finally {
setQrLoading(false);
}
};
// 处理设备列表弹窗
const handleShowDeviceList = (task: Task) => {
setCurrentTask(task);
setShowDeviceList(true);
};
// 处理账号列表弹窗
const handleShowAccountList = (task: Task) => {
setCurrentTask(task);
setShowAccountList(true);
};
// 处理已添加弹窗
const handleShowOreadyAdd = (task: Task) => {
setCurrentTask(task);
setShowOreadyAdd(true);
};
// 处理通过率弹窗
const handleShowPoolList = (task: Task) => {
setCurrentTask(task);
setShowPoolList(true);
};
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return "success";
case 0:
return "default";
case -1:
return "danger";
default:
return "default";
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 0:
return "已暂停";
case -1:
return "已停止";
default:
return "未知";
}
};
const handleRefresh = async () => {
// 重置分页状态
setCurrentPage(1);
setHasMore(true);
await fetchPlanList(1, false);
};
const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
// 生成操作菜单
const getActionMenu = (task: Task) => [
{
key: "edit",
text: "编辑计划",
icon: <EditOutlined />,
onClick: () => {
setShowActionMenu(null);
navigate(`/scenarios/edit/${task.id}`);
},
},
{
key: "copy",
text: "复制计划",
icon: <CopyOutlined />,
onClick: () => {
setShowActionMenu(null);
handleCopyPlan(task.id);
},
},
{
key: "settings",
text: "计划接口",
icon: <SettingOutlined />,
onClick: () => {
setShowActionMenu(null);
handleOpenApiSettings(task.id);
},
},
{
key: "delete",
text: "删除计划",
icon: <DeleteOutlined />,
onClick: () => {
setShowActionMenu(null);
handleDeletePlan(task.id);
},
danger: true,
},
];
const deviceCount = (task: Task) => {
return Array.isArray(task.reqConf?.device)
? task.reqConf!.device.length
: Array.isArray(task.reqConf?.selectedDevices)
? task.reqConf!.selectedDevices.length
: 0;
};
return (
<Layout
header={
<>
<NavCommon
backFn={() => navigate("/scenarios")}
title={scenarioName || ""}
right={
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<div className={style["search-bar"]}>
<div className={style["search-input-wrapper"]}>
<Input
placeholder="搜索计划名称"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={handleRefresh}
loading={loadingTasks}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
loading={loading}
footer={
<div className="pagination-container">
<Pagination
total={total}
pageSize={pageSize}
current={currentPage}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper={false}
/>
</div>
}
>
<div className={style["scenario-list-page"]}>
{/* 计划列表 */}
<div className={style["plan-list"]}>
{filteredTasks.length === 0 ? (
<div className={style["empty-state"]}>
<div className={style["empty-text"]}>
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
</div>
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
</div>
) : (
<>
{filteredTasks.map(task => (
<Card key={task.id} className={style["plan-item"]}>
{/* 头部:标题、状态和操作菜单 */}
<div className={style["plan-header"]}>
<div className={style["plan-name"]}>{task.name}</div>
<div className={style["plan-header-right"]}>
<Tag color={getStatusColor(task.status)}>
{getStatusText(task.status)}
</Tag>
<Button
size="mini"
fill="none"
className={style["more-btn"]}
onClick={e => {
e.stopPropagation(); // 阻止事件冒泡
setShowActionMenu(task.id);
}}
>
<MoreOutlined />
</Button>
</div>
</div>
{/* 统计数据网格 */}
<div className={style["stats-grid"]}>
<div
className={style["stat-item"]}
onClick={e => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleShowDeviceList(task);
}}
>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{deviceCount(task)}
</div>
</div>
<div
className={style["stat-item"]}
onClick={e => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleShowAccountList(task);
}}
>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task?.acquiredCount || 0}
</div>
</div>
<div
className={style["stat-item"]}
onClick={e => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleShowOreadyAdd(task);
}}
>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task.passCount || 0}
</div>
</div>
<div
className={style["stat-item"]}
onClick={e => {
e.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
handleShowPoolList(task);
}}
>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
{task.passRate}%
</div>
</div>
</div>
{/* 底部:上次执行时间 */}
<div className={style["plan-footer"]}>
<div className={style["last-execution"]}>
<ClockCircleOutlined />
<span>: {task.lastUpdated || "--"}</span>
</div>
<div>
<QrcodeOutlined
onClick={() => {
setShowActionMenu(null);
handleShowQrCode(task.id);
}}
/>
</div>
</div>
</Card>
))}
</>
)}
</div>
{/* 计划接口弹窗 */}
<PlanApi
visible={showApiDialog}
onClose={() => setShowApiDialog(false)}
apiKey={currentApiSettings.apiKey}
webhookUrl={currentApiSettings.webhookUrl}
taskId={currentApiSettings.taskId}
/>
{/* 操作菜单弹窗 */}
<Popup
visible={!!showActionMenu}
onMaskClick={() => setShowActionMenu(null)}
position="bottom"
bodyStyle={{ height: "auto", maxHeight: "60vh" }}
>
<div className={style["action-menu-dialog"]}>
<div className={style["dialog-header"]}>
<h3></h3>
<Button size="small" onClick={() => setShowActionMenu(null)}>
</Button>
</div>
<div className={style["dialog-content"]}>
{showActionMenu &&
getActionMenu(tasks.find(t => t.id === showActionMenu)!).map(
item => (
<div
key={item.key}
className={`${style["action-menu-item"]} ${item.danger ? style["danger"] : ""}`}
onClick={item.onClick}
>
<span className={style["action-icon"]}>{item.icon}</span>
<span className={style["action-text"]}>{item.text}</span>
</div>
),
)}
</div>
</div>
</Popup>
{/* 二维码弹窗 */}
<Popup
visible={showQrDialog}
onMaskClick={() => setShowQrDialog(false)}
position="bottom"
>
<div className={style["qr-dialog"]}>
<div className={style["dialog-header"]}>
<h3></h3>
<Button size="small" onClick={() => setShowQrDialog(false)}>
</Button>
</div>
<div className={style["dialog-content"]}>
{qrLoading ? (
<div className={style["qr-loading"]}>
<SpinLoading color="primary" />
<div>...</div>
</div>
) : qrImg ? (
<>
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
{/* 链接复制区域 */}
<div className={style["qr-link-section"]}>
<div className={style["link-label"]}></div>
<div className={style["link-input-wrapper"]}>
<Input
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`}
readOnly
className={style["link-input"]}
placeholder="小程序链接"
/>
<Button
size="small"
onClick={() => {
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",
position: "top",
});
}}
className={style["copy-button"]}
>
<CopyOutlined />
</Button>
</div>
</div>
</>
) : (
<div className={style["qr-error"]}></div>
)}
</div>
</div>
</Popup>
{/* 设备列表弹窗 */}
<DeviceListModal
visible={showDeviceList}
onClose={() => setShowDeviceList(false)}
ruleId={currentTask?.id ? parseInt(currentTask.id) : undefined}
ruleName={currentTask?.name}
/>
{/* 账号列表弹窗 */}
<AccountListModal
visible={showAccountList}
onClose={() => setShowAccountList(false)}
ruleId={currentTask?.id ? parseInt(currentTask.id) : undefined}
ruleName={currentTask?.name}
/>
{/* 已添加弹窗 */}
<OreadyAdd
visible={showOreadyAdd}
onClose={() => setShowOreadyAdd(false)}
ruleId={currentTask?.id ? parseInt(currentTask.id) : undefined}
ruleName={currentTask?.name}
/>
{/* 通过率弹窗 */}
<PoolListModal
visible={showPoolList}
onClose={() => setShowPoolList(false)}
ruleId={currentTask?.id ? parseInt(currentTask.id) : undefined}
ruleName={currentTask?.name}
/>
</div>
</Layout>
);
};
export default ScenarioList;

View File

@@ -1,601 +0,0 @@
// 移动端样式
.plan-api-dialog {
background: white;
border-radius: 16px 16px 0 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
.header-left {
display: flex;
align-items: flex-start;
gap: 12px;
flex: 1;
}
.header-icon {
font-size: 24px;
color: #1890ff;
margin-top: 4px;
}
.header-content {
flex: 1;
h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
}
.close-btn {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #999;
background: transparent;
border: none;
cursor: pointer;
&:hover {
background: #f5f5f5;
}
}
}
.nav-tabs {
display: flex;
background: white;
border-bottom: 1px solid #f0f0f0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.nav-tab {
flex: 1;
min-width: 80px;
padding: 12px 8px;
border: none;
background: transparent;
color: #666;
font-size: 14px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
transition: all 0.2s ease;
white-space: nowrap;
svg {
font-size: 16px;
}
&:hover {
color: #1890ff;
}
&.active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
}
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
.security-note {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
svg {
color: #52c41a;
}
}
.complete-btn {
height: 36px;
padding: 0 24px;
border-radius: 18px;
}
}
// 配置内容样式
.config-content {
.config-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: #333;
.section-icon {
color: #1890ff;
}
}
}
.input-group {
display: flex;
gap: 8px;
margin-bottom: 12px;
.api-input {
flex: 1;
border-radius: 8px;
}
.copy-btn {
height: 40px;
padding: 0 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 4px;
}
}
.security-tip {
padding: 12px;
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 8px;
font-size: 12px;
color: #d46b08;
line-height: 1.5;
}
.params-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 16px;
}
.param-section {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
border: 1px solid #e9ecef;
h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.param-list {
font-size: 12px;
color: #666;
line-height: 1.6;
div {
margin-bottom: 4px;
}
code {
background: #e9ecef;
padding: 2px 4px;
border-radius: 4px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 11px;
}
}
}
}
// 测试内容样式
.test-content {
.test-section {
h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.test-input {
margin-bottom: 16px;
border-radius: 8px;
}
.test-buttons {
display: flex;
gap: 12px;
.test-btn {
flex: 1;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
}
}
}
// 文档内容样式
.docs-content {
.docs-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.doc-card {
text-align: center;
padding: 24px 16px;
border-radius: 12px;
border: 1px solid #f0f0f0;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.doc-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: #f0f8ff;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
font-size: 24px;
color: #1890ff;
}
h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.5;
}
}
}
// 代码内容样式
.code-content {
.language-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
.lang-tab {
padding: 8px 16px;
border: 1px solid #d9d9d9;
background: white;
border-radius: 6px;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
&.active {
background: #1890ff;
border-color: #1890ff;
color: white;
}
}
}
.code-block {
position: relative;
background: #f6f8fa;
border-radius: 8px;
border: 1px solid #e1e4e8;
overflow: hidden;
.code {
margin: 0;
padding: 16px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 13px;
line-height: 1.5;
color: #24292e;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.copy-code-btn {
position: absolute;
top: 8px;
right: 8px;
height: 32px;
padding: 0 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
}
}
// PC端样式覆盖
.plan-api-modal {
.plan-api-dialog {
border-radius: 12px;
height: auto;
max-height: 80vh;
}
.nav-tabs {
.nav-tab {
min-width: 100px;
padding: 16px 12px;
font-size: 15px;
svg {
font-size: 18px;
}
}
}
.dialog-content {
padding: 24px;
}
.params-grid {
grid-template-columns: 1fr 1fr;
}
.docs-grid {
grid-template-columns: 1fr 1fr;
}
.test-buttons {
flex-direction: row;
}
}
// 响应式设计
@media (max-width: 768px) {
.plan-api-dialog {
.header-content {
h3 {
font-size: 16px;
}
p {
font-size: 13px;
}
}
}
.nav-tabs {
.nav-tab {
font-size: 13px;
padding: 10px 6px;
svg {
font-size: 14px;
}
}
}
.dialog-content {
padding: 16px;
}
.config-content {
.params-grid {
grid-template-columns: 1fr;
gap: 8px;
}
}
.docs-content {
.docs-grid {
grid-template-columns: 1fr;
gap: 12px;
}
}
.test-content {
.test-buttons {
flex-direction: column;
gap: 8px;
}
}
.code-content {
.language-tabs {
.lang-tab {
padding: 6px 12px;
font-size: 13px;
}
}
.code-block {
.code {
font-size: 12px;
padding: 12px;
}
}
}
}
// 暗色主题支持
@media (prefers-color-scheme: dark) {
.plan-api-dialog {
background: #1f1f1f;
color: #fff;
.dialog-header {
background: #262626;
border-bottom-color: #434343;
.header-content {
h3 {
color: #fff;
}
p {
color: #a6a6a6;
}
}
}
.nav-tabs {
background: #262626;
border-bottom-color: #434343;
.nav-tab {
color: #a6a6a6;
&:hover {
color: #1890ff;
}
&.active {
color: #1890ff;
border-bottom-color: #1890ff;
}
}
}
.dialog-footer {
background: #262626;
border-top-color: #434343;
.security-note {
color: #a6a6a6;
}
}
}
.config-content {
.section-title {
color: #fff;
}
.security-tip {
background: #2a1f00;
border-color: #d48806;
color: #ffc53d;
}
.param-section {
background: #262626;
border-color: #434343;
h4 {
color: #fff;
}
.param-list {
color: #a6a6a6;
code {
background: #434343;
}
}
}
}
.test-content {
h3 {
color: #fff;
}
}
.docs-content {
.doc-card {
background: #262626;
border-color: #434343;
h4 {
color: #fff;
}
p {
color: #a6a6a6;
}
}
}
.code-content {
.code-block {
background: #0d1117;
border-color: #30363d;
.code {
color: #c9d1d9;
}
}
}
}

View File

@@ -1,437 +0,0 @@
import React, { useState, useMemo } from "react";
import { Popup, Button, Toast, SpinLoading } from "antd-mobile";
import { Modal, Input, Tabs, Card, Tag, Space } from "antd";
import {
CopyOutlined,
CodeOutlined,
BookOutlined,
ThunderboltOutlined,
SettingOutlined,
LinkOutlined,
SafetyOutlined,
CheckCircleOutlined,
} from "@ant-design/icons";
import style from "./planApi.module.scss";
import { buildApiUrl } from "@/utils/apiUrl";
/**
* 计划接口配置弹窗组件
*
* 使用示例:
* ```tsx
* const [showApiDialog, setShowApiDialog] = useState(false);
* const [apiSettings, setApiSettings] = useState({
* apiKey: "your-api-key",
* webhookUrl: "https://api.example.com/webhook",
* taskId: "task-123"
* });
*
* <PlanApi
* visible={showApiDialog}
* onClose={() => setShowApiDialog(false)}
* apiKey={apiSettings.apiKey}
* webhookUrl={apiSettings.webhookUrl}
* taskId={apiSettings.taskId}
* />
* ```
*
* 特性:
* - 移动端使用 PopupPC端使用 Modal
* - 支持四个标签页:接口配置、快速测试、开发文档、代码示例
* - 支持多种编程语言的代码示例
* - 响应式设计,自适应不同屏幕尺寸
* - 支持暗色主题
* - 自动拼接API地址前缀
*/
interface PlanApiProps {
visible: boolean;
onClose: () => void;
apiKey: string;
webhookUrl: string;
taskId: string;
}
interface ApiSettings {
apiKey: string;
webhookUrl: string;
taskId: string;
}
const PlanApi: React.FC<PlanApiProps> = ({
visible,
onClose,
apiKey,
webhookUrl,
taskId,
}) => {
const [activeTab, setActiveTab] = useState("config");
const [activeLanguage, setActiveLanguage] = useState("javascript");
// 处理webhook URL确保包含完整的API地址
const fullWebhookUrl = useMemo(() => {
return buildApiUrl(webhookUrl);
}, [webhookUrl]);
// 生成测试URL
const testUrl = useMemo(() => {
if (!fullWebhookUrl) return "";
return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`;
}, [fullWebhookUrl]);
// 检测是否为移动端
const isMobile = window.innerWidth <= 768;
const handleCopy = (text: string, type: string) => {
navigator.clipboard.writeText(text);
Toast.show({
content: `${type}已复制到剪贴板`,
position: "top",
});
};
const handleTestInBrowser = () => {
window.open(testUrl, "_blank");
};
const renderConfigTab = () => (
<div className={style["config-content"]}>
{/* API密钥配置 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
<CheckCircleOutlined className={style["section-icon"]} />
API密钥
</div>
<Tag color="green"></Tag>
</div>
<div className={style["input-group"]}>
<Input value={apiKey} disabled className={style["api-input"]} />
<Button
size="small"
onClick={() => handleCopy(apiKey, "API密钥")}
className={style["copy-btn"]}
>
<CopyOutlined />
</Button>
</div>
<div className={style["security-tip"]}>
<strong></strong>
API密钥使
</div>
</div>
{/* 接口地址配置 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
<LinkOutlined className={style["section-icon"]} />
</div>
<Tag color="blue">POST请求</Tag>
</div>
<div className={style["input-group"]}>
<Input
value={fullWebhookUrl}
disabled
className={style["api-input"]}
/>
<Button
size="small"
onClick={() => handleCopy(fullWebhookUrl, "接口地址")}
className={style["copy-btn"]}
>
<CopyOutlined />
</Button>
</div>
{/* 参数说明 */}
<div className={style["params-grid"]}>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>name</code> -
</div>
<div>
<code>phone</code> -
</div>
</div>
</div>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>source</code> -
</div>
<div>
<code>remark</code> -
</div>
<div>
<code>tags</code> -
</div>
</div>
</div>
</div>
</div>
</div>
);
const renderQuickTestTab = () => (
<div className={style["test-content"]}>
<div className={style["test-section"]}>
<h3>URL</h3>
<div className={style["input-group"]}>
<Input value={testUrl} disabled className={style["test-input"]} />
</div>
<div className={style["test-buttons"]}>
<Button
onClick={() => handleCopy(testUrl, "测试URL")}
className={style["test-btn"]}
>
<CopyOutlined />
URL
</Button>
<Button
type="primary"
onClick={handleTestInBrowser}
className={style["test-btn"]}
>
</Button>
</div>
</div>
</div>
);
const renderDocsTab = () => (
<div className={style["docs-content"]}>
<div className={style["docs-grid"]}>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<BookOutlined />
</div>
<h4>API文档</h4>
<p></p>
</Card>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<LinkOutlined />
</div>
<h4></h4>
<p></p>
</Card>
</div>
</div>
);
const renderCodeTab = () => {
const codeExamples = {
javascript: `fetch('${fullWebhookUrl}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${apiKey}'
},
body: JSON.stringify({
name: '张三',
phone: '13800138000',
source: '官网表单',
})
})`,
python: `import requests
url = '${fullWebhookUrl}'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${apiKey}'
}
data = {
'name': '张三',
'phone': '13800138000',
'source': '官网表单'
}
response = requests.post(url, json=data, headers=headers)`,
php: `<?php
$url = '${fullWebhookUrl}';
$data = array(
'name' => '张三',
'phone' => '13800138000',
'source' => '官网表单'
);
$options = array(
'http' => array(
'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n",
'method' => 'POST',
'content' => json_encode($data)
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);`,
java: `import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${fullWebhookUrl}"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer ${apiKey}")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,
};
return (
<div className={style["code-content"]}>
<div className={style["language-tabs"]}>
{Object.keys(codeExamples).map(lang => (
<button
key={lang}
className={`${style["lang-tab"]} ${
activeLanguage === lang ? style["active"] : ""
}`}
onClick={() => setActiveLanguage(lang)}
>
{lang.charAt(0).toUpperCase() + lang.slice(1)}
</button>
))}
</div>
<div className={style["code-block"]}>
<pre className={style["code"]}>
<code>
{codeExamples[activeLanguage as keyof typeof codeExamples]}
</code>
</pre>
<Button
size="small"
onClick={() =>
handleCopy(
codeExamples[activeLanguage as keyof typeof codeExamples],
"代码",
)
}
className={style["copy-code-btn"]}
>
<CopyOutlined />
</Button>
</div>
</div>
);
};
const renderContent = () => (
<div className={style["plan-api-dialog"]}>
{/* 头部 */}
<div className={style["dialog-header"]}>
<div className={style["header-left"]}>
<CodeOutlined className={style["header-icon"]} />
<div className={style["header-content"]}>
<h3></h3>
<p>
API接口直接导入客资到该获客计划
</p>
</div>
</div>
<Button size="small" onClick={onClose} className={style["close-btn"]}>
×
</Button>
</div>
{/* 导航标签 */}
<div className={style["nav-tabs"]}>
<button
className={`${style["nav-tab"]} ${activeTab === "config" ? style["active"] : ""}`}
onClick={() => setActiveTab("config")}
>
<SettingOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "test" ? style["active"] : ""}`}
onClick={() => setActiveTab("test")}
>
<ThunderboltOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "docs" ? style["active"] : ""}`}
onClick={() => setActiveTab("docs")}
>
<BookOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "code" ? style["active"] : ""}`}
onClick={() => setActiveTab("code")}
>
<CodeOutlined />
</button>
</div>
{/* 内容区域 */}
<div className={style["dialog-content"]}>
{activeTab === "config" && renderConfigTab()}
{activeTab === "test" && renderQuickTestTab()}
{activeTab === "docs" && renderDocsTab()}
{activeTab === "code" && renderCodeTab()}
</div>
{/* 底部 */}
<div className={style["dialog-footer"]}>
<div className={style["security-note"]}>
<SafetyOutlined />
HTTPS加密
</div>
<Button
type="primary"
onClick={onClose}
className={style["complete-btn"]}
>
</Button>
</div>
</div>
);
// 移动端使用Popup
if (isMobile) {
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{ height: "90vh" }}
>
{renderContent()}
</Popup>
);
}
// PC端使用Modal
return (
<Modal
open={visible}
onCancel={onClose}
footer={null}
width={800}
centered
className={style["plan-api-modal"]}
>
{renderContent()}
</Modal>
);
};
export default PlanApi;

View File

@@ -1,20 +0,0 @@
import request from "@/api/request";
// 获取场景类型列表
export function getScenarioTypes() {
return request("/v1/plan/scenes", undefined, "GET");
}
// 创建计划
export function createPlan(data: any) {
return request("/v1/plan/create", data, "POST");
}
// 更新计划
export function updatePlan(data: any) {
return request("/v1/plan/update", data, "PUT");
}
// 获取计划详情
export function getPlanDetail(planId: string) {
return request(`/v1/plan/detail?planId=${planId}`, undefined, "GET");
}

View File

@@ -1,59 +0,0 @@
// 步骤定义 - 只保留三个步骤
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
export const steps = [
{ id: 1, title: "步骤一", subtitle: "基础设置" },
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
{ id: 3, title: "步骤三", subtitle: "消息设置" },
];
// 类型定义
export interface FormData {
name: string;
scenario: number;
status: number;
sceneId: string | number;
remarkType: string;
greeting: string;
addInterval: number;
startTime: string;
endTime: string;
enabled: boolean;
remarkFormat: string;
addFriendInterval: number;
posters: any[]; // 后续可替换为具体Poster类型
device: string[];
customTags: string[];
customTagsOptions: string[];
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
messagePlans: any[];
[key: string]: any;
}
export const defFormData: FormData = {
name: "",
scenario: 1,
status: 0,
sceneId: "",
remarkType: "phone",
greeting: "你好,请通过",
addInterval: 1,
startTime: "09:00",
endTime: "18:00",
enabled: true,
remarkFormat: "",
addFriendInterval: 1,
posters: [],
device: [],
customTags: [],
customTagsOptions: [],
messagePlans: [],
deviceGroups: [],
deviceGroupsOptions: [],
wechatGroups: [],
wechatGroupsOptions: [],
contentGroups: [],
contentGroupsOptions: [],
};

View File

@@ -1,218 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { message, Button, Space } from "antd";
import NavCommon from "@/components/NavCommon";
import BasicSettings from "./steps/BasicSettings";
import FriendRequestSettings from "./steps/FriendRequestSettings";
import MessageSettings from "./steps/MessageSettings";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
import {
getScenarioTypes,
createPlan,
getPlanDetail,
updatePlan,
} from "./index.api";
import { FormData, defFormData, steps } from "./index.data";
export default function NewPlan() {
const router = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>(defFormData);
const [sceneList, setSceneList] = useState<any[]>([]);
const [sceneLoading, setSceneLoading] = useState(true);
const { scenarioId, planId } = useParams<{
scenarioId: string;
planId: string;
}>();
const [isEdit, setIsEdit] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setSceneLoading(true);
//获取场景类型
getScenarioTypes()
.then(data => {
setSceneList(data || []);
})
.catch(err => {
message.error(err.message || "获取场景类型失败");
})
.finally(() => setSceneLoading(false));
if (planId) {
setIsEdit(true);
//获取计划详情
const detail = await getPlanDetail(planId);
setFormData(prev => ({
...prev,
name: detail.name ?? "",
scenario: Number(detail.scenario) || 1,
scenarioTags: detail.scenarioTags ?? [],
customTags: detail.customTags ?? [],
customTagsOptions: detail.customTags ?? [],
posters: detail.posters ?? [],
device: detail.device ?? [],
remarkType: detail.remarkType ?? "phone",
greeting: detail.greeting ?? "",
addInterval: detail.addInterval ?? 1,
startTime: detail.startTime ?? "09:00",
endTime: detail.endTime ?? "18:00",
enabled: detail.enabled ?? true,
sceneId: Number(detail.scenario) || 1,
remarkFormat: detail.remarkFormat ?? "",
addFriendInterval: detail.addFriendInterval ?? 1,
tips: detail.tips ?? "",
deviceGroups: detail.deviceGroups ?? [],
deviceGroupsOptions: detail.deviceGroupsOptions ?? [],
wechatGroups: detail.wechatGroups ?? [],
wechatGroupsOptions: detail.wechatGroupsOptions ?? [],
contentGroups: detail.contentGroups ?? [],
contentGroupsOptions: detail.contentGroupsOptions ?? [],
status: detail.status ?? 0,
messagePlans: detail.messagePlans ?? [],
}));
} else {
if (scenarioId) {
setFormData(prev => ({
...prev,
...{ scenario: Number(scenarioId) || 1 },
}));
}
}
};
// 更新表单数据
const onChange = (data: any) => {
setFormData(prev => ({ ...prev, ...data }));
};
// 处理保存
const handleSave2 = async () => {
if (isEdit && planId) {
// 编辑:拼接后端需要的完整参数
const editData = {
...formData,
...{ sceneId: Number(formData.scenario) },
id: Number(planId),
planId: Number(planId),
};
console.log("editData", editData);
} else {
// 新建
formData.sceneId = Number(formData.scenario);
console.log("formData", formData);
}
};
// 处理保存
const handleSave = async () => {
try {
if (isEdit && planId) {
// 编辑:拼接后端需要的完整参数
const editData = {
...formData,
...{ sceneId: Number(formData.scenario) },
id: Number(planId),
planId: Number(planId),
// 兼容后端需要的字段
// 你可以根据实际需要补充其它字段
};
await updatePlan(editData);
} else {
// 新建
formData.sceneId = Number(formData.scenario);
await createPlan(formData);
}
message.success(isEdit ? "计划已更新" : "获客计划已创建");
const sceneItem = sceneList.find(v => formData.scenario === v.id);
router(`/scenarios/list/${formData.scenario}/${sceneItem.name}`);
} catch (error) {
message.error(
error instanceof Error
? error.message
: typeof error === "string"
? error
: isEdit
? "更新计划失败,请重试"
: "创建计划失败,请重试",
);
}
};
// 下一步
const handleNext = () => {
if (currentStep === steps.length) {
handleSave();
} else {
setCurrentStep(prev => prev + 1);
}
};
// 上一步
const handlePrev = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
// 渲染当前步骤内容
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<BasicSettings
isEdit={isEdit}
formData={formData}
onChange={onChange}
sceneList={sceneList}
sceneLoading={sceneLoading}
/>
);
case 2:
return (
<FriendRequestSettings formData={formData} onChange={onChange} />
);
case 3:
return <MessageSettings formData={formData} onChange={onChange} />;
default:
return null;
}
};
// 渲染底部按钮
const renderFooterButtons = () => {
return (
<div style={{ padding: "16px", display: "flex", gap: "12px" }}>
{currentStep > 1 && (
<Button onClick={handlePrev} size="large" style={{ flex: 1 }}>
</Button>
)}
<Button
type="primary"
size="large"
onClick={handleNext}
style={{ flex: 1 }}
>
{currentStep === steps.length ? "完成" : "下一步"}
</Button>
</div>
);
};
return (
<Layout
header={
<>
<NavCommon title={isEdit ? "编辑场景计划" : "新建场景计划"} />
<StepIndicator currentStep={currentStep} steps={steps} />
</>
}
footer={renderFooterButtons()}
>
{renderStepContent()}
</Layout>
);
}

View File

@@ -1,564 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { Input, Button, Tag, Switch, Modal, Spin } from "antd";
import {
PlusOutlined,
EyeOutlined,
CloseOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import { uploadFile } from "@/api/common";
import styles from "./base.module.scss";
import { posterTemplates } from "./base.data";
import GroupSelection from "@/components/GroupSelection";
import FileUpload from "@/components/Upload/FileUpload";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
interface BasicSettingsProps {
isEdit: boolean;
formData: any;
onChange: (data: any) => void;
sceneList: any[];
sceneLoading: boolean;
}
interface Material {
id: string;
name: string;
type: string;
url: string;
}
const generatePosterMaterials = (): Material[] => {
return posterTemplates.map(template => ({
id: template.id,
name: template.name,
type: "poster",
url: template.url,
}));
};
const BasicSettings: React.FC<BasicSettingsProps> = ({
formData,
onChange,
sceneList,
sceneLoading,
}) => {
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [materials] = useState<Material[]>(generatePosterMaterials());
const [selectedMaterials, setSelectedMaterials] = useState<Material[]>(
formData.posters?.length > 0 ? formData.posters : [],
);
// 自定义标签相关状态
const [customTagInput, setCustomTagInput] = useState("");
const [customTagsOptions, setCustomTagsOptions] = useState<string[]>(
formData.customTagsOptions || [],
);
const [tips, setTips] = useState(formData.tips || "");
const [selectedScenarioTags, setSelectedScenarioTags] = useState(
formData.scenarioTags || [],
);
const [selectedCustomTags, setSelectedCustomTags] = useState(
formData.customTags || [],
);
// 电话获客相关状态
const [phoneSettings, setPhoneSettings] = useState({
autoAdd: formData.phoneSettings?.autoAdd ?? true,
speechToText: formData.phoneSettings?.speechToText ?? true,
questionExtraction: formData.phoneSettings?.questionExtraction ?? true,
});
// 新增:自定义海报相关状态
const [customPosters, setCustomPosters] = useState<Material[]>([]);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
// 新增用于文件选择的ref
const uploadInputRef = useRef<HTMLInputElement>(null);
// 初始化时,如果没有选择场景,默认选择海报获客
useEffect(() => {
if (!formData.scenario) {
onChange({ ...formData, scenario: "haibao" });
}
}, [formData, onChange]);
// 监听 formData 变化,同步自定义标签和获客标签状态
useEffect(() => {
setCustomTagsOptions(formData.customTagsOptions || []);
setSelectedCustomTags(formData.customTags || []);
}, [formData.customTagsOptions, formData.customTags]);
// 监听获客标签变化
useEffect(() => {
setSelectedScenarioTags(formData.scenarioTags || []);
}, [formData.scenarioTags]);
useEffect(() => {
setTips(formData.tips || "");
}, [formData.tips]);
// 选中场景
const handleScenarioSelect = (sceneId: number) => {
onChange({ ...formData, scenario: sceneId });
};
// 选中/取消标签
const handleScenarioTagToggle = (tag: string) => {
const newTags = selectedScenarioTags.includes(tag)
? selectedScenarioTags.filter((t: string) => t !== tag)
: [...selectedScenarioTags, tag];
setSelectedScenarioTags(newTags);
onChange({ ...formData, scenarioTags: newTags });
};
const handleCustomTagToggle = (tag: string) => {
const newTags = selectedCustomTags.includes(tag)
? selectedCustomTags.filter((t: string) => t !== tag)
: [...selectedCustomTags, tag];
setSelectedCustomTags(newTags);
onChange({ ...formData, customTags: newTags });
};
// 添加自定义标签
const handleAddCustomTag = () => {
if (!customTagInput.trim()) return;
const newTag = customTagInput.trim();
// 已存在则忽略
if (customTagsOptions.includes(newTag)) {
// 若未选中则顺便选中
const maybeSelected = selectedCustomTags.includes(newTag)
? selectedCustomTags
: [...selectedCustomTags, newTag];
setSelectedCustomTags(maybeSelected);
onChange({ ...formData, customTags: maybeSelected });
setCustomTagInput("");
return;
}
const updatedOptions = [...customTagsOptions, newTag];
const updatedSelected = [...selectedCustomTags, newTag];
setCustomTagsOptions(updatedOptions);
setSelectedCustomTags(updatedSelected);
setCustomTagInput("");
onChange({
...formData,
customTagsOptions: updatedOptions,
customTags: updatedSelected,
});
};
// 删除自定义标签
const handleRemoveCustomTag = (tagName: string) => {
const updatedOptions = customTagsOptions.filter(
(tag: string) => tag !== tagName,
);
setCustomTagsOptions(updatedOptions);
// 同时从选中的自定义标签中移除
const updatedSelectedCustom = selectedCustomTags.filter(
(t: string) => t !== tagName,
);
setSelectedCustomTags(updatedSelectedCustom);
onChange({
...formData,
customTagsOptions: updatedOptions,
customTags: updatedSelectedCustom,
});
};
// 新增:删除自定义海报
const handleRemoveCustomPoster = (id: string) => {
setCustomPosters(prev => prev.filter(p => p.id !== id));
// 如果选中则取消选中
if (selectedMaterials.some(m => m.id === id)) {
setSelectedMaterials([]);
onChange({ ...formData, posters: [] });
}
};
// 修改:选中/取消选中海报
const handleMaterialSelect = (material: Material) => {
const isSelected = selectedMaterials.some(m => m.id === material.id);
if (isSelected) {
setSelectedMaterials([]);
onChange({ ...formData, posters: [] });
} else {
setSelectedMaterials([material]);
onChange({ ...formData, posters: [material] });
}
};
// 新增:全屏预览
const handlePreviewImage = (url: string) => {
setPreviewUrl(url);
setIsPreviewOpen(true);
};
// 下载模板
const handleDownloadTemplate = () => {
const template =
"电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03";
const blob = new Blob([template], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "订单导入模板.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
// 当前选中的场景对象
const currentScene = sceneList.find(s => s.id === formData.scenario);
//打开订单
const openOrder =
formData.scenario !== 2 ? { display: "none" } : { display: "block" };
const openPoster =
formData.scenario !== 1 ? { display: "none" } : { display: "block" };
const handleWechatGroupSelect = (groups: GroupSelectionItem[]) => {
onChange({
...formData,
wechatGroups: groups.map(v => v.id),
wechatGroupsOptions: groups,
});
};
return (
<div className={styles["basic-container"]}>
{/* 场景选择区块 */}
<div className={styles["basic-scene-select"]}>
{sceneLoading ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: 80,
}}
>
<Spin size="large"></Spin>
</div>
) : (
<div className={styles["basic-scene-grid"]}>
{sceneList.map(scene => {
const selected = formData.scenario === scene.id;
return (
<button
key={scene.id}
onClick={() => handleScenarioSelect(scene.id)}
className={
styles["basic-scene-btn"] +
(selected ? " " + styles.selected : "")
}
>
{scene.name.replace("获客", "")}
</button>
);
})}
</div>
)}
</div>
{/* 计划名称输入区 */}
<div className={styles["basic-label"]}></div>
<div className={styles["basic-input-block"]}>
<Input
value={formData.name}
onChange={e =>
onChange({ ...formData, name: String(e.target.value) })
}
placeholder="请输入计划名称"
/>
</div>
<div className={styles["basic-label"]}></div>
{/* 标签选择区块 */}
{formData.scenario && (
<div className={styles["basic-tag-list"]}>
{(currentScene?.scenarioTags || []).map((tag: string) => (
<Tag
key={tag}
color={selectedScenarioTags.includes(tag) ? "blue" : "default"}
onClick={() => handleScenarioTagToggle(tag)}
className={styles["basic-tag-item"]}
>
{tag}
</Tag>
))}
{/* 自定义标签 */}
{customTagsOptions.map((tag: string) => (
<Tag
key={tag}
color={selectedCustomTags.includes(tag) ? "blue" : "default"}
onClick={() => handleCustomTagToggle(tag)}
closable
onClose={() => handleRemoveCustomTag(tag)}
className={styles["basic-tag-item"]}
>
{tag}
</Tag>
))}
</div>
)}
{/* 自定义标签输入区 */}
<div className={styles["basic-custom-tag-input"]}>
<Input
type="text"
value={customTagInput}
onChange={e => setCustomTagInput(e.target.value)}
onPressEnter={handleAddCustomTag}
placeholder="添加自定义标签"
/>
<Button
type="primary"
onClick={handleAddCustomTag}
disabled={!customTagInput.trim()}
>
</Button>
</div>
{/* 输入获客成功提示 - 只有海报场景才显示 */}
{formData.scenario === 1 && (
<>
<div className={styles["basic-label"]}></div>
<div className={styles["basic-success-tip"]}>
<Input
type="text"
value={tips}
onChange={e => {
setTips(e.target.value);
onChange({ ...formData, tips: e.target.value });
}}
placeholder="请输入获客成功提示"
/>
</div>
</>
)}
{/* 选素材 */}
<div className={styles["basic-materials"]} style={openPoster}>
<div className={styles["basic-label"]}></div>
<div className={styles["basic-materials-grid"]}>
{[...materials, ...customPosters].map(material => {
const isSelected = selectedMaterials.some(
m => m.id === material.id,
);
const isCustom = material.id.startsWith("custom-");
return (
<div
key={material.id}
className={
styles["basic-material-card"] +
(isSelected ? " " + styles.selected : "")
}
onClick={() => handleMaterialSelect(material)}
>
{/* 预览按钮:自定义海报在左上,内置海报在右上 */}
<span
className={styles["basic-material-preview"]}
onClick={e => {
e.stopPropagation();
handlePreviewImage(material.url);
}}
>
<EyeOutlined
style={{ color: "#fff", width: 18, height: 18 }}
/>
</span>
{/* 删除自定义海报按钮 */}
{isCustom && (
<Button
style={{
position: "absolute",
top: 8,
right: 8,
width: 28,
height: 28,
background: "rgba(0,0,0,0.5)",
border: "none",
borderRadius: "50%",
zIndex: 2,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
lineHeight: 20,
color: "#ffffff",
}}
onClick={e => {
e.stopPropagation();
handleRemoveCustomPoster(material.id);
}}
>
<CloseOutlined />
</Button>
)}
<img
src={material.url}
alt={material.name}
className={styles["basic-material-img"]}
/>
<div className={styles["basic-material-name"]}>
{material.name}
</div>
</div>
);
})}
{/* 添加海报卡片 */}
<div
className={styles["basic-add-material"]}
onClick={() => uploadInputRef.current?.click()}
>
<span style={{ fontSize: 36, color: "#bbb", marginBottom: 8 }}>
<PlusOutlined />
</span>
<span style={{ color: "#888" }}></span>
<input
ref={uploadInputRef}
type="file"
accept="image/*"
style={{ display: "none" }}
onChange={async e => {
const file = e.target.files?.[0];
if (file) {
// 直接上传
try {
const url = await uploadFile(file);
const newPoster = {
id: `custom-${Date.now()}`,
name: "自定义海报",
type: "poster",
url: url,
};
setCustomPosters(prev => [...prev, newPoster]);
} catch (err) {
// 可加toast提示
}
e.target.value = "";
}
}}
/>
</div>
</div>
{/* 全屏图片预览 */}
<Modal
open={isPreviewOpen}
onCancel={() => {
setIsPreviewOpen(false);
setPreviewUrl(null);
}}
footer={null}
width={800}
>
{previewUrl && (
<img
src={previewUrl}
alt="Preview"
style={{ width: "100%", height: "100%" }}
/>
)}
</Modal>
</div>
{/* 群选择 - 只有微信群场景才显示 */}
{formData.scenario === 7 && (
<div className={styles["basic-group-selection"]}>
<div className={styles["basic-label"]}></div>
<GroupSelection
selectedOptions={formData.wechatGroupsOptions || []}
onSelect={handleWechatGroupSelect}
placeholder="请选择微信群"
className={styles["basic-group-selector"]}
/>
</div>
)}
{/* 订单导入区块 - 使用FileUpload组件 */}
<div className={styles["basic-order-upload"]} style={openOrder}>
<div className={styles["basic-order-upload-label"]}></div>
<div className={styles["basic-order-upload-actions"]}>
<Button
style={{ display: "flex", alignItems: "center", gap: 4 }}
onClick={handleDownloadTemplate}
>
<DownloadOutlined style={{ fontSize: 18 }} />
</Button>
</div>
<div className={styles["basic-order-upload-file"]}>
<FileUpload
value={formData.orderFileUrl || ""}
onChange={url => onChange({ ...formData, orderFileUrl: url })}
acceptTypes={["excel"]}
maxCount={1}
maxSize={10}
showPreview={false}
/>
</div>
<div className={styles["basic-order-upload-tip"]}>
Excel
</div>
</div>
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */}
{formData.scenario === 5 && (
<div className={styles["basic-phone-settings"]}>
<div style={{ fontWeight: 600, fontSize: 16, marginBottom: 16 }}>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<Switch
checked={phoneSettings.autoAdd}
onChange={v => setPhoneSettings(s => ({ ...s, autoAdd: v }))}
/>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<Switch
checked={phoneSettings.speechToText}
onChange={v =>
setPhoneSettings(s => ({ ...s, speechToText: v }))
}
/>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<Switch
checked={phoneSettings.questionExtraction}
onChange={v =>
setPhoneSettings(s => ({ ...s, questionExtraction: v }))
}
/>
</div>
</div>
</div>
)}
<div className={styles["basic-footer-switch"]}>
<span></span>
<Switch
checked={formData.status === 1}
onChange={value => onChange({ ...formData, status: value ? 1 : 0 })}
/>
</div>
</div>
);
};
export default BasicSettings;

View File

@@ -1,232 +0,0 @@
"use client";
import React, { useState, useEffect } from "react";
import { Input, Button, Modal, Alert, Select } from "antd";
import { MessageOutlined } from "@ant-design/icons";
import DeviceSelection from "@/components/DeviceSelection";
import styles from "./friend.module.scss";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
interface FriendRequestSettingsProps {
formData: any;
onChange: (data: any) => void;
}
// 招呼语模板
const greetingTemplates = [
"你好,请通过",
"你好,了解XX,请通过",
"你好我是XX产品的客服请通过",
"你好,感谢关注我们的产品",
"你好,很高兴为您服务",
];
// 备注类型选项
const remarkTypes = [
{ value: "phone", label: "手机号" },
{ value: "nickname", label: "昵称" },
{ value: "source", label: "来源" },
];
const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
formData,
onChange,
}) => {
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
const [hasWarnings, setHasWarnings] = useState(false);
const [showRemarkTip, setShowRemarkTip] = useState(false);
// 获取场景标题
const getScenarioTitle = () => {
switch (formData.scenario) {
case "douyin":
return "抖音直播";
case "xiaohongshu":
return "小红书";
case "weixinqun":
return "微信群";
case "gongzhonghao":
return "公众号";
default:
return formData.name || "获客计划";
}
};
// 使用useEffect设置默认值
useEffect(() => {
if (!formData.greeting) {
onChange({
...formData,
greeting: "你好,请通过",
remarkType: "phone", // 默认选择手机号
remarkFormat: `手机号+${getScenarioTitle()}`, // 默认备注格式
addFriendInterval: 1,
});
}
}, [formData, formData.greeting, onChange]);
// 检查是否有未完成的必填项
useEffect(() => {
const hasIncompleteFields = !formData.greeting?.trim();
setHasWarnings(hasIncompleteFields);
}, [formData]);
const handleTemplateSelect = (template: string) => {
onChange({ ...formData, greeting: template });
setIsTemplateDialogOpen(false);
};
const handleDevicesChange = (deviceGroupsOptions: DeviceSelectionItem[]) => {
onChange({
...formData,
deviceGroups: deviceGroupsOptions.map(d => d.id),
deviceGroupsOptions: deviceGroupsOptions,
});
};
return (
<div className={styles["friend-container"]}>
{/* 选择设备区块 */}
<div className={styles["friend-label"]}></div>
<div className={styles["friend-block"]}>
<DeviceSelection
selectedOptions={formData.deviceGroupsOptions}
onSelect={handleDevicesChange}
placeholder="选择设备"
/>
</div>
{/* 好友备注区块 */}
<div className={styles["friend-label"]}></div>
<div className={styles["friend-block"]}>
<div className={styles["friend-remark-container"]}>
<Select
value={formData.remarkType || "phone"}
onChange={value => onChange({ ...formData, remarkType: value })}
style={{ width: "100%" }}
>
{remarkTypes.map(type => (
<Select.Option key={type.value} value={type.value}>
{type.label}
</Select.Option>
))}
</Select>
<span
className={styles["friend-remark-q"]}
onMouseEnter={() => setShowRemarkTip(true)}
onMouseLeave={() => setShowRemarkTip(false)}
>
?
</span>
{showRemarkTip && (
<div className={styles["friend-remark-tip"]}>
<div></div>
<div style={{ marginTop: 8, color: "#888", fontSize: 12 }}>
</div>
<div style={{ marginTop: 4, color: "#1677ff" }}>
{formData.remarkType === "phone" &&
`138****1234+${getScenarioTitle()}`}
{formData.remarkType === "nickname" &&
`小红书用户2851+${getScenarioTitle()}`}
{formData.remarkType === "source" &&
`抖音直播+${getScenarioTitle()}`}
</div>
</div>
)}
</div>
</div>
{/* 招呼语区块 */}
<div className={styles["friend-label"]}></div>
<div className={styles["friend-block"]}>
<Input
value={formData.greeting}
onChange={e => onChange({ ...formData, greeting: e.target.value })}
placeholder="请输入招呼语"
suffix={
<Button
type="link"
onClick={() => setIsTemplateDialogOpen(true)}
style={{ padding: 0 }}
>
<MessageOutlined />
</Button>
}
/>
</div>
{/* 添加间隔区块 */}
<div className={styles["friend-label"]}></div>
<div
className={styles["friend-interval-row"] + " " + styles["friend-block"]}
>
<Input
type="number"
value={formData.addFriendInterval || 1}
onChange={e =>
onChange({
...formData,
addFriendInterval: Number(e.target.value),
})
}
style={{ width: 100 }}
/>
<span></span>
</div>
{/* 允许加人时间段区块 */}
<div className={styles["friend-label"]}></div>
<div className={styles["friend-time-row"] + " " + styles["friend-block"]}>
<Input
type="time"
value={formData.addFriendTimeStart || "09:00"}
onChange={e =>
onChange({ ...formData, addFriendTimeStart: e.target.value })
}
style={{ width: 120 }}
/>
<span></span>
<Input
type="time"
value={formData.addFriendTimeEnd || "18:00"}
onChange={e =>
onChange({ ...formData, addFriendTimeEnd: e.target.value })
}
style={{ width: 120 }}
/>
</div>
{hasWarnings && (
<Alert
message="警告"
description="您有未完成的设置项,建议完善后再进入下一步。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{/* 招呼语模板弹窗 */}
<Modal
open={isTemplateDialogOpen}
onCancel={() => setIsTemplateDialogOpen(false)}
footer={null}
>
<div>
{greetingTemplates.map((template, index) => (
<Button
key={index}
onClick={() => handleTemplateSelect(template)}
className={styles["friend-modal-btn"]}
>
{template}
</Button>
))}
</div>
</Modal>
</div>
);
};
export default FriendRequestSettings;

View File

@@ -1,336 +0,0 @@
import React from "react";
import { Input, Button } from "antd";
import { CloseOutlined, ClockCircleOutlined } from "@ant-design/icons";
import styles from "./messages.module.scss";
// 导入Upload组件
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
import VideoUpload from "@/components/Upload/VideoUpload";
import FileUpload from "@/components/Upload/FileUpload";
import MainImgUpload from "@/components/Upload/MainImgUpload";
// 导入GroupSelection组件
import GroupSelection from "@/components/GroupSelection";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { MessageContentItem, messageTypes } from "./base.data";
interface MessageCardProps {
message: MessageContentItem;
dayIndex: number;
messageIndex: number;
planDay: number;
onUpdateMessage: (
dayIndex: number,
messageIndex: number,
updates: Partial<MessageContentItem>,
) => void;
onRemoveMessage: (dayIndex: number, messageIndex: number) => void;
onToggleIntervalUnit: (dayIndex: number, messageIndex: number) => void;
}
const MessageCard: React.FC<MessageCardProps> = ({
message,
dayIndex,
messageIndex,
planDay,
onUpdateMessage,
onRemoveMessage,
onToggleIntervalUnit,
}) => {
return (
<div className={styles["messages-message-card"]}>
<div className={styles["messages-message-header"]}>
{/* 时间/间隔设置 */}
<div className={styles["messages-message-header-content"]}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{planDay === 0 ? (
<>
<span style={{ minWidth: 36 }}></span>
<Input
type="number"
value={String(message.sendInterval || 5)}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
sendInterval: Number(e.target.value),
})
}
style={{ width: 60 }}
/>
<Button
size="small"
onClick={() => onToggleIntervalUnit(dayIndex, messageIndex)}
>
<ClockCircleOutlined />
{message.intervalUnit === "minutes" ? "分钟" : "秒"}
</Button>
</>
) : (
<>
<span style={{ minWidth: 60 }}></span>
<Input
type="number"
min={0}
max={23}
value={String(message.scheduledTime?.hour || 9)}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 9,
minute: 0,
second: 0,
}),
hour: Number(e.target.value),
},
})
}
style={{ width: 40 }}
/>
<span>:</span>
<Input
type="number"
min={0}
max={59}
value={String(message.scheduledTime?.minute || 0)}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 9,
minute: 0,
second: 0,
}),
minute: Number(e.target.value),
},
})
}
style={{ width: 40 }}
/>
<span>:</span>
<Input
type="number"
min={0}
max={59}
value={String(message.scheduledTime?.second || 0)}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
scheduledTime: {
...(message.scheduledTime || {
hour: 9,
minute: 0,
second: 0,
}),
second: Number(e.target.value),
},
})
}
style={{ width: 40 }}
/>
</>
)}
</div>
<button
className={styles["messages-message-remove-btn"]}
onClick={() => onRemoveMessage(dayIndex, messageIndex)}
title="删除"
>
<CloseOutlined />
</button>
</div>
{/* 类型切换按钮 */}
<div className={styles["messages-message-type-btns"]}>
{messageTypes.map(type => (
<Button
key={type.id}
type={message.type === type.id ? "primary" : "default"}
onClick={() =>
onUpdateMessage(dayIndex, messageIndex, {
type: type.id as any,
})
}
className={styles["messages-message-type-btn"]}
>
<type.icon />
</Button>
))}
</div>
</div>
<div className={styles["messages-message-content"]}>
{/* 文本消息 */}
{message.type === "text" && (
<Input.TextArea
value={message.content}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
content: e.target.value,
})
}
placeholder="请输入消息内容"
autoSize={{ minRows: 3, maxRows: 6 }}
/>
)}
{/* 小程序消息 */}
{message.type === "miniprogram" && (
<>
<Input
value={message.title}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
title: e.target.value,
})
}
placeholder="请输入小程序标题"
style={{ marginBottom: 8 }}
/>
<Input
value={message.description}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
description: e.target.value,
})
}
placeholder="请输入小程序描述"
style={{ marginBottom: 8 }}
/>
<Input
value={message.address}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
address: e.target.value,
})
}
placeholder="请输入小程序路径"
style={{ marginBottom: 8 }}
/>
<div style={{ marginBottom: 8 }}>
<MainImgUpload
value={message.content || ""}
onChange={url =>
onUpdateMessage(dayIndex, messageIndex, {
content: url,
})
}
maxSize={5}
showPreview={true}
/>
</div>
</>
)}
{/* 链接消息 */}
{message.type === "link" && (
<>
<Input
value={message.title}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
title: e.target.value,
})
}
placeholder="请输入链接标题"
style={{ marginBottom: 8 }}
/>
<Input
value={message.description}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
description: e.target.value,
})
}
placeholder="请输入链接描述"
style={{ marginBottom: 8 }}
/>
<Input
value={message.linkUrl}
onChange={e =>
onUpdateMessage(dayIndex, messageIndex, {
linkUrl: e.target.value,
})
}
placeholder="请输入链接地址"
style={{ marginBottom: 8 }}
/>
<div style={{ marginBottom: 8 }}>
<MainImgUpload
value={message.coverImage || ""}
onChange={url =>
onUpdateMessage(dayIndex, messageIndex, {
coverImage: url,
})
}
maxSize={1}
showPreview={true}
/>
</div>
</>
)}
{/* 群邀请消息 */}
{message.type === "group" && (
<div style={{ marginBottom: 8 }}>
<GroupSelection
selectedOptions={message.groupOptions || []}
onSelect={(groups: GroupSelectionItem[]) => {
onUpdateMessage(dayIndex, messageIndex, {
groupIds: groups.map(v => v.id),
groupOptions: groups,
});
}}
placeholder="选择邀请入的群"
showSelectedList={true}
selectedListMaxHeight={200}
/>
</div>
)}
{/* 图片消息 */}
{message.type === "image" && (
<div style={{ marginBottom: 8 }}>
<ImageUpload
value={message.content ? [message.content] : []}
onChange={urls =>
onUpdateMessage(dayIndex, messageIndex, {
content: urls[0] || "",
})
}
count={1}
accept="image/*"
/>
</div>
)}
{/* 视频消息 */}
{message.type === "video" && (
<div style={{ marginBottom: 8 }}>
<VideoUpload
value={message.content || ""}
onChange={url => {
const videoUrl = Array.isArray(url) ? url[0] || "" : url;
onUpdateMessage(dayIndex, messageIndex, {
content: videoUrl,
});
}}
maxSize={50}
maxCount={1}
showPreview={true}
/>
</div>
)}
{/* 文件消息 */}
{message.type === "file" && (
<div style={{ marginBottom: 8 }}>
<FileUpload
value={message.content || ""}
onChange={url => {
const fileUrl = Array.isArray(url) ? url[0] || "" : url;
onUpdateMessage(dayIndex, messageIndex, {
content: fileUrl,
});
}}
maxSize={10}
maxCount={1}
showPreview={true}
acceptTypes={["excel", "word", "ppt"]}
/>
</div>
)}
</div>
</div>
);
};
export default MessageCard;

View File

@@ -1,242 +0,0 @@
import React, { useState } from "react";
import { Button, Tabs, Modal, message } from "antd";
import { PlusOutlined, CloseOutlined } from "@ant-design/icons";
import styles from "./messages.module.scss";
import {
MessageContentItem,
MessageContentGroup,
MessageSettingsProps,
} from "./base.data";
import MessageCard from "./MessageCard";
const MessageSettings: React.FC<MessageSettingsProps> = ({
formData,
onChange,
}) => {
const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false);
// 获取当前的消息计划,如果没有则使用默认值
const getCurrentMessagePlans = (): MessageContentGroup[] => {
if (formData.messagePlans && formData.messagePlans.length > 0) {
return formData.messagePlans;
}
return [
{
day: 0,
messages: [
{
id: "1",
type: "text",
content: "",
sendInterval: 5,
intervalUnit: "seconds",
},
],
},
];
};
// 添加新消息
const handleAddMessage = (dayIndex: number, type = "text") => {
const currentPlans = getCurrentMessagePlans();
const updatedPlans = [...currentPlans];
const newMessage: MessageContentItem = {
id: Date.now().toString(),
type: type as MessageContentItem["type"],
content: "",
};
if (currentPlans[dayIndex].day === 0) {
newMessage.sendInterval = 5;
newMessage.intervalUnit = "seconds";
} else {
newMessage.scheduledTime = {
hour: 9,
minute: 0,
second: 0,
};
}
updatedPlans[dayIndex].messages.push(newMessage);
onChange({ ...formData, messagePlans: updatedPlans });
};
// 更新消息内容
const handleUpdateMessage = (
dayIndex: number,
messageIndex: number,
updates: Partial<MessageContentItem>,
) => {
const currentPlans = getCurrentMessagePlans();
const updatedPlans = [...currentPlans];
updatedPlans[dayIndex].messages[messageIndex] = {
...updatedPlans[dayIndex].messages[messageIndex],
...updates,
};
onChange({ ...formData, messagePlans: updatedPlans });
};
// 删除消息
const handleRemoveMessage = (dayIndex: number, messageIndex: number) => {
const currentPlans = getCurrentMessagePlans();
const updatedPlans = [...currentPlans];
updatedPlans[dayIndex].messages.splice(messageIndex, 1);
onChange({ ...formData, messagePlans: updatedPlans });
};
// 切换时间单位
const toggleIntervalUnit = (dayIndex: number, messageIndex: number) => {
const currentPlans = getCurrentMessagePlans();
const message = currentPlans[dayIndex].messages[messageIndex];
const newUnit = message.intervalUnit === "minutes" ? "seconds" : "minutes";
handleUpdateMessage(dayIndex, messageIndex, { intervalUnit: newUnit });
};
// 添加新的天数计划
const handleAddDayPlan = () => {
const currentPlans = getCurrentMessagePlans();
const newDay = currentPlans.length;
const updatedPlans = [
...currentPlans,
{
day: newDay,
messages: [
{
id: Date.now().toString(),
type: "text",
content: "",
scheduledTime: {
hour: 9,
minute: 0,
second: 0,
},
},
],
},
];
onChange({ ...formData, messagePlans: updatedPlans });
setIsAddDayPlanOpen(false);
message.success(`已添加第${newDay}天的消息计划`);
};
// 删除天数计划
const handleRemoveDayPlan = (dayIndex: number) => {
if (dayIndex === 0) {
message.warning("不能删除即时消息");
return;
}
const currentPlans = getCurrentMessagePlans();
Modal.confirm({
title: "确认删除",
content: `确定要删除第${currentPlans[dayIndex].day}天的消息计划吗?`,
onOk: () => {
const updatedPlans = currentPlans.filter(
(_, index) => index !== dayIndex,
);
// 重新计算天数
const recalculatedPlans = updatedPlans.map((plan, index) => ({
...plan,
day: index,
}));
onChange({ ...formData, messagePlans: recalculatedPlans });
message.success(`已删除第${currentPlans[dayIndex].day}天的消息计划`);
},
});
};
const items = getCurrentMessagePlans().map(
(plan: MessageContentGroup, dayIndex: number) => ({
key: plan.day.toString(),
label: (
<div
style={{
display: "flex",
alignItems: "center",
gap: "2px",
}}
>
<span>{plan.day === 0 ? "即时消息" : `${plan.day}`}</span>
{dayIndex > 0 && (
<Button
type="text"
size="small"
icon={<CloseOutlined />}
onClick={e => {
e.stopPropagation();
handleRemoveDayPlan(dayIndex);
}}
style={{
padding: "0 4px",
minWidth: "auto",
color: "#ff4d4f",
fontSize: "12px",
}}
title="删除此天计划"
/>
)}
</div>
),
children: (
<div className={styles["messages-day-panel"]}>
{plan.messages.map((message, messageIndex) => (
<MessageCard
key={message.id}
message={message}
dayIndex={dayIndex}
messageIndex={messageIndex}
planDay={plan.day}
onUpdateMessage={handleUpdateMessage}
onRemoveMessage={handleRemoveMessage}
onToggleIntervalUnit={toggleIntervalUnit}
/>
))}
<Button
onClick={() => handleAddMessage(dayIndex)}
className={styles["messages-add-message-btn"]}
>
<PlusOutlined className="w-4 h-4 mr-2" />
</Button>
</div>
),
}),
);
return (
<div className={styles["messages-container"]}>
<div className={styles["messages-header"]}>
<h2 className={styles["messages-title"]}></h2>
<Button onClick={() => setIsAddDayPlanOpen(true)}>
<PlusOutlined />
</Button>
</div>
<Tabs
defaultActiveKey="0"
items={items}
className={styles["messages-tab"]}
/>
{/* 添加天数计划弹窗 */}
<Modal
title="添加消息计划"
open={isAddDayPlanOpen}
onCancel={() => setIsAddDayPlanOpen(false)}
onOk={() => {
handleAddDayPlan();
setIsAddDayPlanOpen(false);
}}
>
<p className="text-sm text-gray-500 mb-4"></p>
<Button
onClick={handleAddDayPlan}
className={styles["messages-modal-btn"]}
>
{getCurrentMessagePlans().length}
</Button>
</Modal>
</div>
);
};
export default MessageSettings;

View File

@@ -1,84 +0,0 @@
export const posterTemplates = [
{
id: "poster-1",
name: "点击领取",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif",
},
{
id: "poster-2",
name: "点击合作",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif",
},
{
id: "poster-3",
name: "点击咨询",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif",
},
{
id: "poster-4",
name: "点击签到",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif",
},
{
id: "poster-5",
name: "点击了解",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif",
},
{
id: "poster-6",
name: "点击报名",
url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif",
},
];
// ========================================
import {
MessageOutlined,
PictureOutlined,
VideoCameraOutlined,
FileOutlined,
AppstoreOutlined,
LinkOutlined,
TeamOutlined,
} from "@ant-design/icons";
export interface MessageContentItem {
id: string;
type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group";
content: string;
sendInterval?: number;
intervalUnit?: "seconds" | "minutes";
scheduledTime?: {
hour: number;
minute: number;
second: number;
};
title?: string;
description?: string;
address?: string;
groupIds?: string[]; // 改为数组以支持GroupSelection组件
groupOptions?: any[]; // 添加群选项数组
linkUrl?: string;
coverImage?: string;
[key: string]: any;
}
export interface MessageContentGroup {
day: number;
messages: MessageContentItem[];
}
export interface MessageSettingsProps {
formData: any;
onChange: (data: any) => void;
}
// 消息类型配置
export const messageTypes = [
{ id: "text", icon: MessageOutlined, label: "文本" },
{ id: "image", icon: PictureOutlined, label: "图片" },
{ id: "video", icon: VideoCameraOutlined, label: "视频" },
{ id: "file", icon: FileOutlined, label: "文件" },
{ id: "miniprogram", icon: AppstoreOutlined, label: "小程序" },
{ id: "link", icon: LinkOutlined, label: "链接" },
{ id: "group", icon: TeamOutlined, label: "邀请入群" },
];

View File

@@ -1,163 +0,0 @@
.basic-container {
padding: 12px;
}
.basic-scene-select {
background: #fff;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.basic-scene-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.basic-scene-btn {
height: 40px;
border: none;
border-radius: 10px;
font-weight: 500;
font-size: 16px;
outline: none;
cursor: pointer;
background: rgba(#1677ff, 0.1);
color: #1677ff;
transition: all 0.2s;
}
.basic-scene-btn.selected {
background: #1677ff;
color: #fff;
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.08);
}
.basic-label {
margin-bottom: 12px;
font-weight: 500;
}
.basic-input-block {
border: 1px solid #eee;
margin-bottom: 16px;
}
.basic-tag-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
padding-bottom: 16px;
}
.basic-tag-item {
margin-bottom: 6px;
}
.basic-custom-tag-input {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.basic-success-tip {
display: flex;
}
.basic-materials {
margin: 16px 0;
}
.basic-materials-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.basic-material-preview {
position: absolute;
top: 8px;
padding-left: 2px;
right: 8px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
cursor: pointer;
}
.basic-material-card {
border: 2px solid #eee;
border-radius: 8px;
padding: 6px;
cursor: pointer;
background: #fff;
text-align: center;
position: relative;
min-height: 192px;
transition: border 0.2s;
}
.basic-material-card.selected {
border: 2px solid #1890ff;
background: #e6f7ff;
}
.basic-material-img {
width: 100px;
height: 180px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 0;
display: block;
}
.basic-material-name {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 14px;
padding: 4px 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
z-index: 3;
}
.basic-add-material {
border: 2px dashed #bbb;
border-radius: 8px;
padding: 6px;
cursor: pointer;
background: #fafbfc;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 190px;
}
.basic-order-upload {
margin: 16px 0;
}
.basic-order-upload-label {
font-weight: 500;
margin-bottom: 8px;
}
.basic-order-upload-actions {
display: flex;
gap: 12px;
margin-bottom: 4px;
}
.basic-order-upload-tip {
color: #888;
font-size: 13px;
margin-bottom: 8px;
}
.basic-phone-settings {
margin: 16px 0;
background: #f7f8fa;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-bottom: 12px;
}
.basic-wechat-group {
margin: 16px 0;
}
.basic-footer-switch {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0;
}

View File

@@ -1,97 +0,0 @@
.friend-container {
padding: 12px;
}
.friend-label {
margin-bottom: 12px;
font-weight: 500;
}
.friend-block {
margin-bottom: 16px;
}
.friend-remark-tip {
position: absolute;
right: 0;
top: 100%;
z-index: 10;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 12px;
width: 240px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-top: 8px;
font-size: 13px;
line-height: 1.5;
&::before {
content: "";
position: absolute;
top: -6px;
right: 20px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
}
&::after {
content: "";
position: absolute;
top: -7px;
right: 20px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #e8e8e8;
}
}
.friend-remark-q {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #888;
background: #fff;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
z-index: 2;
border: 1px solid #d9d9d9;
transition: all 0.2s ease;
&:hover {
color: #1677ff;
border-color: #1677ff;
background: #f0f8ff;
}
}
.friend-remark-container {
position: relative;
}
.friend-interval-row {
display: flex;
align-items: center;
gap: 8px;
}
.friend-time-row {
display: flex;
align-items: center;
gap: 8px;
}
.friend-footer {
display: flex;
justify-content: space-between;
margin-top: 32px;
}
.friend-modal-btn {
width: 100%;
margin-bottom: 8px;
}

View File

@@ -1,110 +0,0 @@
.messages-container {
padding: 16px;
}
.messages-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.messages-title {
font-size: 18px;
font-weight: 600;
}
.messages-tab {
margin-bottom: 16px;
}
.messages-day-panel {
border-radius: 10px;
margin-bottom: 16px;
}
.messages-message-card {
background: #fff;
border-radius: 12px;
box-shadow:
0 4px 16px rgba(22, 119, 255, 0.06),
0 1.5px 4px rgba(0, 0, 0, 0.04);
padding: 20px 12px 16px 12px;
margin-bottom: 20px;
border: 1.5px solid #f0f3fa;
transition:
box-shadow 0.2s,
border 0.2s,
transform 0.2s;
position: relative;
}
.messages-message-card:hover {
box-shadow:
0 8px 24px rgba(22, 119, 255, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08);
border: 1.5px solid #1677ff;
transform: translateY(-2px) scale(1.01);
}
.messages-message-header {
}
.messages-message-header-content {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.messages-message-type-btns {
display: flex;
gap: 5px;
margin-bottom: 8px;
}
.messages-message-type-btn {
width: 20px;
}
.messages-message-content {
margin-bottom: 10px;
font-size: 15px;
color: #222;
line-height: 1.7;
}
.messages-message-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.messages-message-remove-btn {
color: #ff4d4f;
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 0 8px;
transition: color 0.2s;
}
.messages-message-remove-btn:hover {
color: #d9363e;
}
.messages-add-message-btn {
width: 100%;
margin-top: 8px;
}
.messages-footer {
display: flex;
justify-content: space-between;
margin-top: 32px;
}
.messages-modal-btn {
width: 100%;
margin-bottom: 8px;
}
.messages-group-select-item {
padding: 16px;
border-radius: 8px;
cursor: pointer;
background: #fff;
margin-bottom: 8px;
border: 1px solid #eee;
transition:
border 0.2s,
background 0.2s;
}
.messages-group-select-item.selected {
background: #e6f7ff;
border: 1.5px solid #1677ff;
}

View File

@@ -1,384 +0,0 @@
import request from "@/api/request";
// ==================== 场景相关接口 ====================
// 获取场景列表
export function getScenarios(params: any) {
return request("/v1/plan/scenes", params, "GET");
}
// 获取场景详情
export function getScenarioDetail(id: string) {
return request(`/v1/scenarios/${id}`, {}, "GET");
}
// 创建场景
export function createScenario(data: any) {
return request("/v1/scenarios", data, "POST");
}
// 更新场景
export function updateScenario(id: string, data: any) {
return request(`/v1/scenarios/${id}`, data, "PUT");
}
// 删除场景
export function deleteScenario(id: string) {
return request(`/v1/scenarios/${id}`, {}, "DELETE");
}
// ==================== 计划相关接口 ====================
// 获取计划列表
export function getPlanList(
scenarioId: string,
page: number = 1,
limit: number = 20,
) {
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET");
}
// 复制计划
export function copyPlan(planId: string) {
return request(`/api/scenarios/plans/${planId}/copy`, undefined, "POST");
}
// 删除计划
export function deletePlan(planId: string) {
return request(`/api/scenarios/plans/${planId}`, undefined, "DELETE");
}
// 获取小程序二维码
export function getWxMinAppCode(planId: string) {
return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, "GET");
}
// ==================== 设备相关接口 ====================
// 获取设备列表
export function getDevices() {
return request("/api/devices", undefined, "GET");
}
// 获取设备详情
export function getDeviceDetail(deviceId: string) {
return request(`/api/devices/${deviceId}`, undefined, "GET");
}
// 创建设备
export function createDevice(data: any) {
return request("/api/devices", data, "POST");
}
// 更新设备
export function updateDevice(deviceId: string, data: any) {
return request(`/api/devices/${deviceId}`, data, "PUT");
}
// 删除设备
export function deleteDevice(deviceId: string) {
return request(`/api/devices/${deviceId}`, undefined, "DELETE");
}
// ==================== 微信号相关接口 ====================
// 获取微信号列表
export function getWechatAccounts() {
return request("/api/wechat-accounts", undefined, "GET");
}
// 获取微信号详情
export function getWechatAccountDetail(accountId: string) {
return request(`/api/wechat-accounts/${accountId}`, undefined, "GET");
}
// 创建微信号
export function createWechatAccount(data: any) {
return request("/api/wechat-accounts", data, "POST");
}
// 更新微信号
export function updateWechatAccount(accountId: string, data: any) {
return request(`/api/wechat-accounts/${accountId}`, data, "PUT");
}
// 删除微信号
export function deleteWechatAccount(accountId: string) {
return request(`/api/wechat-accounts/${accountId}`, undefined, "DELETE");
}
// ==================== 海报相关接口 ====================
// 获取海报列表
export function getPosters() {
return request("/api/posters", undefined, "GET");
}
// 获取海报详情
export function getPosterDetail(posterId: string) {
return request(`/api/posters/${posterId}`, undefined, "GET");
}
// 创建海报
export function createPoster(data: any) {
return request("/api/posters", data, "POST");
}
// 更新海报
export function updatePoster(posterId: string, data: any) {
return request(`/api/posters/${posterId}`, data, "PUT");
}
// 删除海报
export function deletePoster(posterId: string) {
return request(`/api/posters/${posterId}`, undefined, "DELETE");
}
// ==================== 内容相关接口 ====================
// 获取内容列表
export function getContents(params: any) {
return request("/api/contents", params, "GET");
}
// 获取内容详情
export function getContentDetail(contentId: string) {
return request(`/api/contents/${contentId}`, undefined, "GET");
}
// 创建内容
export function createContent(data: any) {
return request("/api/contents", data, "POST");
}
// 更新内容
export function updateContent(contentId: string, data: any) {
return request(`/api/contents/${contentId}`, data, "PUT");
}
// 删除内容
export function deleteContent(contentId: string) {
return request(`/api/contents/${contentId}`, undefined, "DELETE");
}
// ==================== 流量池相关接口 ====================
// 获取流量池列表
export function getTrafficPools() {
return request("/api/traffic-pools", undefined, "GET");
}
// 获取流量池详情
export function getTrafficPoolDetail(poolId: string) {
return request(`/api/traffic-pools/${poolId}`, undefined, "GET");
}
// 创建流量池
export function createTrafficPool(data: any) {
return request("/api/traffic-pools", data, "POST");
}
// 更新流量池
export function updateTrafficPool(poolId: string, data: any) {
return request(`/api/traffic-pools/${poolId}`, data, "PUT");
}
// 删除流量池
export function deleteTrafficPool(poolId: string) {
return request(`/api/traffic-pools/${poolId}`, undefined, "DELETE");
}
// ==================== 工作台相关接口 ====================
// 获取工作台统计数据
export function getWorkspaceStats() {
return request("/api/workspace/stats", undefined, "GET");
}
// 获取自动点赞任务列表
export function getAutoLikeTasks() {
return request("/api/workspace/auto-like/tasks", undefined, "GET");
}
// 创建自动点赞任务
export function createAutoLikeTask(data: any) {
return request("/api/workspace/auto-like/tasks", data, "POST");
}
// 更新自动点赞任务
export function updateAutoLikeTask(taskId: string, data: any) {
return request(`/api/workspace/auto-like/tasks/${taskId}`, data, "PUT");
}
// 删除自动点赞任务
export function deleteAutoLikeTask(taskId: string) {
return request(
`/api/workspace/auto-like/tasks/${taskId}`,
undefined,
"DELETE",
);
}
// ==================== 群发相关接口 ====================
// 获取群发任务列表
export function getGroupPushTasks() {
return request("/api/workspace/group-push/tasks", undefined, "GET");
}
// 创建群发任务
export function createGroupPushTask(data: any) {
return request("/api/workspace/group-push/tasks", data, "POST");
}
// 更新群发任务
export function updateGroupPushTask(taskId: string, data: any) {
return request(`/api/workspace/group-push/tasks/${taskId}`, data, "PUT");
}
// 删除群发任务
export function deleteGroupPushTask(taskId: string) {
return request(
`/api/workspace/group-push/tasks/${taskId}`,
undefined,
"DELETE",
);
}
// ==================== 自动建群相关接口 ====================
// 获取自动建群任务列表
export function getAutoGroupTasks() {
return request("/api/workspace/auto-group/tasks", undefined, "GET");
}
// 创建自动建群任务
export function createAutoGroupTask(data: any) {
return request("/api/workspace/auto-group/tasks", data, "POST");
}
// 更新自动建群任务
export function updateAutoGroupTask(taskId: string, data: any) {
return request(`/api/workspace/auto-group/tasks/${taskId}`, data, "PUT");
}
// 删除自动建群任务
export function deleteAutoGroupTask(taskId: string) {
return request(
`/api/workspace/auto-group/tasks/${taskId}`,
undefined,
"DELETE",
);
}
// ==================== AI助手相关接口 ====================
// 获取AI对话历史
export function getAIChatHistory() {
return request("/api/workspace/ai-assistant/chat-history", undefined, "GET");
}
// 发送AI消息
export function sendAIMessage(data: any) {
return request("/api/workspace/ai-assistant/send-message", data, "POST");
}
// 获取AI分析报告
export function getAIAnalysisReport() {
return request(
"/api/workspace/ai-assistant/analysis-report",
undefined,
"GET",
);
}
// ==================== 订单相关接口 ====================
// 获取订单列表
export function getOrders(params: any) {
return request("/api/orders", params, "GET");
}
// 获取订单详情
export function getOrderDetail(orderId: string) {
return request(`/api/orders/${orderId}`, undefined, "GET");
}
// 创建订单
export function createOrder(data: any) {
return request("/api/orders", data, "POST");
}
// 更新订单
export function updateOrder(orderId: string, data: any) {
return request(`/api/orders/${orderId}`, data, "PUT");
}
// 删除订单
export function deleteOrder(orderId: string) {
return request(`/api/orders/${orderId}`, undefined, "DELETE");
}
// ==================== 用户相关接口 ====================
// 获取用户信息
export function getUserInfo() {
return request("/api/user/info", undefined, "GET");
}
// 更新用户信息
export function updateUserInfo(data: any) {
return request("/api/user/info", data, "PUT");
}
// 修改密码
export function changePassword(data: any) {
return request("/api/user/change-password", data, "POST");
}
// 上传头像
export function uploadAvatar(data: any) {
return request("/api/user/upload-avatar", data, "POST");
}
// ==================== 文件上传相关接口 ====================
// 上传文件
export function uploadFile(data: any) {
return request("/api/upload/file", data, "POST");
}
// 上传图片
export function uploadImage(data: any) {
return request("/api/upload/image", data, "POST");
}
// 删除文件
export function deleteFile(fileId: string) {
return request(`/api/upload/files/${fileId}`, undefined, "DELETE");
}
// ==================== 系统配置相关接口 ====================
// 获取系统配置
export function getSystemConfig() {
return request("/api/system/config", undefined, "GET");
}
// 更新系统配置
export function updateSystemConfig(data: any) {
return request("/api/system/config", data, "PUT");
}
// 获取系统通知
export function getSystemNotifications() {
return request("/api/system/notifications", undefined, "GET");
}
// 标记通知为已读
export function markNotificationAsRead(notificationId: string) {
return request(
`/api/system/notifications/${notificationId}/read`,
undefined,
"PUT",
);
}

View File

@@ -1,94 +0,0 @@
.analyzerPage {
}
.tabs {
background: #fff;
padding: 0 12px;
border-radius: 0 0 12px 12px;
margin-bottom: 8px;
}
.planList {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 12px 16px 12px;
}
.planCard {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 16px 14px 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
}
.cardHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.cardTitle {
font-size: 16px;
font-weight: 700;
color: #222;
}
.statusDone {
background: #e6f9e6;
color: #22c55e;
font-size: 12px;
border-radius: 8px;
padding: 2px 10px;
font-weight: 600;
}
.statusDoing {
background: #e0f2fe;
color: #1677ff;
font-size: 12px;
border-radius: 8px;
padding: 2px 10px;
font-weight: 600;
}
.cardInfo {
font-size: 13px;
color: #444;
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
color: #888;
font-size: 12px;
margin-right: 2px;
}
.keyword {
display: inline-block;
background: #f3f4f6;
color: #1677ff;
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
margin-right: 6px;
margin-bottom: 2px;
}
.cardActions {
display: flex;
gap: 10px;
margin-top: 8px;
}
.actionBtn {
border-radius: 6px !important;
font-size: 13px !important;
padding: 0 12px !important;
}

View File

@@ -1,141 +0,0 @@
import React, { useState } from "react";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import { Tabs } from "antd-mobile";
import { Button } from "antd";
import styles from "./index.module.scss";
import { PlusOutlined } from "@ant-design/icons";
const mockPlans = [
{
id: "1",
title: "美妆用户分析",
status: "done",
device: "设备1",
wechat: "wxid_abc123",
type: "综合分析",
keywords: ["美妆", "护肤", "彩妆"],
createTime: "2023/12/15 18:30:00",
finishTime: "2023/12/15 19:45:00",
},
{
id: "2",
title: "健身爱好者分析",
status: "doing",
device: "设备2",
wechat: "wxid_fit456",
type: "好友信息分析",
keywords: ["健身", "运动", "健康"],
createTime: "2023/12/16 17:15:00",
finishTime: "",
},
];
const statusMap = {
all: "全部计划",
doing: "进行中",
done: "已完成",
};
const statusTag = {
done: <span className={styles.statusDone}></span>,
doing: <span className={styles.statusDoing}></span>,
};
const AiAnalyzer: React.FC = () => {
const [tab, setTab] = useState<"all" | "doing" | "done">("all");
const filteredPlans =
tab === "all" ? mockPlans : mockPlans.filter(p => p.status === tab);
return (
<Layout
header={
<NavCommon
title="AI数据分析"
right={
<Button type="primary" size="small" style={{ borderRadius: 6 }}>
<PlusOutlined />
</Button>
}
/>
}
>
<div className={styles.analyzerPage}>
<Tabs
activeKey={tab}
onChange={key => setTab(key as any)}
className={styles.tabs}
>
<Tabs.Tab title="全部计划" key="all" />
<Tabs.Tab title="进行中" key="doing" />
<Tabs.Tab title="已完成" key="done" />
</Tabs>
<div className={styles.planList}>
{filteredPlans.map(plan => (
<div className={styles.planCard} key={plan.id}>
<div className={styles.cardHeader}>
<span className={styles.cardTitle}>{plan.title}</span>
{statusTag[plan.status as "done" | "doing"]}
</div>
<div className={styles.cardInfo}>
<div>
<span className={styles.label}></span>
{plan.device} | : {plan.wechat}
</div>
<div>
<span className={styles.label}></span>
{plan.type}
</div>
<div>
<span className={styles.label}></span>
{plan.keywords.map(k => (
<span className={styles.keyword} key={k}>
{k}
</span>
))}
</div>
<div>
<span className={styles.label}></span>
{plan.createTime}
</div>
{plan.status === "done" && (
<div>
<span className={styles.label}></span>
{plan.finishTime}
</div>
)}
</div>
<div className={styles.cardActions}>
{plan.status === "done" ? (
<>
<Button size="small" className={styles.actionBtn}>
</Button>
<Button
size="small"
type="primary"
className={styles.actionBtn}
>
</Button>
</>
) : (
<Button
size="small"
type="primary"
className={styles.actionBtn}
>
</Button>
)}
</div>
</div>
))}
</div>
</div>
</Layout>
);
};
export default AiAnalyzer;

View File

@@ -1,145 +0,0 @@
.chatContainer {
display: flex;
flex-direction: column;
}
.messageList {
flex: 1;
overflow-y: auto;
padding: 16px 12px 80px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.userMessage {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.aiMessage {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 18px;
font-size: 15px;
line-height: 1.6;
word-break: break-word;
background: #fff;
color: #222;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.userMessage .bubble {
background: linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%);
color: #222;
border-bottom-right-radius: 6px;
}
.aiMessage .bubble {
background: #fff;
color: #222;
border-bottom-left-radius: 6px;
}
.time {
font-size: 11px;
color: #aaa;
margin: 4px 8px 0 8px;
align-self: flex-end;
}
.inputBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
background: #fff;
padding: 10px 12px 10px 12px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
z-index: 10;
}
.input {
flex: 1;
border: none;
outline: none;
background: #f3f4f6;
border-radius: 18px;
padding: 10px 14px;
font-size: 15px;
margin-right: 8px;
}
.sendButton {
background: var(
--primary-gradient,
linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)
);
color: #fff;
border: none;
border-radius: 18px;
padding: 8px 18px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.sendButton:disabled {
background: #e5e7eb;
color: #aaa;
cursor: not-allowed;
}
.iconBtn {
background: none;
border: none;
outline: none;
margin-right: 6px;
font-size: 20px;
color: #888;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition:
background 0.2s,
color 0.2s;
}
.iconBtn:hover,
.iconBtn:active {
background: #f3f4f6;
color: #5bbcff;
}
.image {
max-width: 180px;
max-height: 180px;
border-radius: 10px;
display: block;
}
.fileLink {
color: #5bbcff;
text-decoration: none;
font-size: 15px;
word-break: break-all;
display: flex;
align-items: center;
}
.nav-title {
color: var(--primary-color);
font-weight: 700;
font-size: 18px;
text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2);
}

View File

@@ -1,264 +0,0 @@
import React, { useRef, useState, useEffect } from "react";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import {
PictureOutlined,
PaperClipOutlined,
AudioOutlined,
} from "@ant-design/icons";
import styles from "./AIAssistant.module.scss";
interface Message {
id: string;
content: string;
from: "user" | "ai";
time: string;
type?: "text" | "image" | "file" | "audio";
fileName?: string;
fileUrl?: string;
}
const initialMessages: Message[] = [
{
id: "1",
content: "你好我是你的AI助手有什么可以帮助你的吗?",
from: "ai",
time: "15:29",
type: "text",
},
];
const AIAssistant: React.FC = () => {
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const [recognizing, setRecognizing] = useState(false);
const recognitionRef = useRef<any>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// 语音识别初始化
useEffect(() => {
if (!("webkitSpeechRecognition" in window)) return;
const SpeechRecognition = (window as any).webkitSpeechRecognition;
recognitionRef.current = new SpeechRecognition();
recognitionRef.current.continuous = false;
recognitionRef.current.interimResults = false;
recognitionRef.current.lang = "zh-CN";
recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript;
setInput(prev => prev + transcript);
setRecognizing(false);
};
recognitionRef.current.onerror = () => setRecognizing(false);
recognitionRef.current.onend = () => setRecognizing(false);
}, []);
const handleSend = async () => {
if (!input.trim()) return;
const userMsg: Message = {
id: Date.now().toString(),
content: input,
from: "user",
time: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "text",
};
setMessages(prev => [...prev, userMsg]);
setInput("");
setLoading(true);
setTimeout(() => {
setMessages(prev => [
...prev,
{
id: Date.now().toString() + "-ai",
content: "AI正在思考...此处可接入真实API",
from: "ai",
time: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "text",
},
]);
setLoading(false);
}, 1200);
};
// 图片上传
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setMessages(prev => [
...prev,
{
id: Date.now().toString(),
content: url,
from: "user",
time: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "image",
fileName: file.name,
fileUrl: url,
},
]);
}
e.target.value = "";
};
// 文件上传
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setMessages(prev => [
...prev,
{
id: Date.now().toString(),
content: file.name,
from: "user",
time: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "file",
fileName: file.name,
fileUrl: url,
},
]);
}
e.target.value = "";
};
// 语音输入
const handleVoiceInput = () => {
if (!recognitionRef.current) return alert("当前浏览器不支持语音输入");
if (recognizing) {
recognitionRef.current.stop();
setRecognizing(false);
} else {
recognitionRef.current.start();
setRecognizing(true);
}
};
return (
<Layout header={<NavCommon title="AI助手" />} loading={false}>
<div className={styles.chatContainer}>
<div className={styles.messageList}>
{messages.map(msg => (
<div
key={msg.id}
className={
msg.from === "user" ? styles.userMessage : styles.aiMessage
}
>
{msg.type === "text" && (
<div className={styles.bubble}>{msg.content}</div>
)}
{msg.type === "image" && (
<div className={styles.bubble}>
<img
src={msg.fileUrl}
alt={msg.fileName}
className={styles.image}
/>
</div>
)}
{msg.type === "file" && (
<div className={styles.bubble}>
<a
href={msg.fileUrl}
download={msg.fileName}
className={styles.fileLink}
>
<PaperClipOutlined style={{ marginRight: 6 }} />
{msg.fileName}
</a>
</div>
)}
{/* 语音消息可后续扩展 */}
<div className={styles.time}>{msg.time}</div>
</div>
))}
{loading && (
<div className={styles.aiMessage}>
<div className={styles.bubble}>AI正在输入...</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className={styles.inputBar}>
<button
className={styles.iconBtn}
onClick={() => imageInputRef.current?.click()}
title="图片"
type="button"
>
<PictureOutlined />
</button>
<input
ref={imageInputRef}
type="file"
accept="image/*"
style={{ display: "none" }}
onChange={handleImageChange}
/>
<button
className={styles.iconBtn}
onClick={() => fileInputRef.current?.click()}
title="文件"
type="button"
>
<PaperClipOutlined />
</button>
<input
ref={fileInputRef}
type="file"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<button
className={styles.iconBtn}
onClick={handleVoiceInput}
title="语音输入"
type="button"
style={{ color: recognizing ? "#5bbcff" : undefined }}
>
<AudioOutlined />
</button>
<input
className={styles.input}
type="text"
placeholder="输入消息..."
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter") handleSend();
}}
disabled={loading}
/>
<button
className={styles.sendButton}
onClick={handleSend}
disabled={loading || !input.trim()}
>
</button>
</div>
</div>
</Layout>
);
};
export default AIAssistant;

View File

@@ -1,6 +0,0 @@
import request from "@/api/request";
// 获取自动建群任务详情
export function getAutoGroupDetail(id: string) {
return request(`/api/auto-group/detail/${id}`);
}

View File

@@ -1,149 +0,0 @@
.autoGroupDetail {
padding: 16px 0 80px 0;
background: #f7f8fa;
min-height: 100vh;
}
.headerBar {
display: flex;
align-items: center;
height: 48px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
font-size: 18px;
font-weight: 600;
padding: 0 16px;
}
.title {
font-size: 18px;
font-weight: 600;
color: #222;
flex: 1;
text-align: center;
}
.infoCard {
margin-bottom: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none;
background: #fff;
padding: 16px;
}
.infoGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.infoTitle {
font-size: 14px;
font-weight: 500;
color: #1677ff;
margin-bottom: 4px;
}
.infoItem {
font-size: 13px;
color: #444;
margin-bottom: 2px;
}
.progressSection {
margin-top: 16px;
}
.progressCard {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none;
background: #fff;
padding: 16px;
margin-bottom: 16px;
}
.progressHeader {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 15px;
font-weight: 500;
margin-bottom: 8px;
}
.groupList {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
.groupCard {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none;
background: #fff;
padding: 12px 16px;
}
.groupHeader {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 15px;
font-weight: 500;
margin-bottom: 8px;
}
.memberGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-top: 8px;
}
.memberItem {
background: #f5f7fa;
border-radius: 8px;
padding: 4px 8px;
font-size: 13px;
color: #333;
display: flex;
align-items: center;
}
.warnText {
color: #faad14;
font-size: 13px;
margin-top: 8px;
display: flex;
align-items: center;
}
.successText {
color: #389e0d;
font-size: 13px;
margin-top: 8px;
display: flex;
align-items: center;
}
.successAlert {
color: #389e0d;
background: #f6ffed;
border-radius: 8px;
padding: 8px 0;
text-align: center;
margin-top: 12px;
font-size: 14px;
}
.emptyCard {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-top: 32px;
}
.emptyTitle {
font-size: 16px;
color: #888;
margin: 12px 0 4px 0;
}
.emptyDesc {
font-size: 13px;
color: #bbb;
margin-bottom: 16px;
}

View File

@@ -1,348 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Card, Button, Toast, ProgressBar, Tag } from "antd-mobile";
import { TeamOutline, LeftOutline } from "antd-mobile-icons";
import { AlertOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon/index";
import style from "./index.module.scss";
interface GroupMember {
id: string;
nickname: string;
wechatId: string;
tags: string[];
}
interface Group {
id: string;
members: GroupMember[];
}
interface GroupTaskDetail {
id: string;
name: string;
status: "preparing" | "creating" | "completed" | "paused";
totalGroups: number;
currentGroupIndex: number;
groups: Group[];
createTime: string;
lastUpdateTime: string;
creator: string;
deviceCount: number;
targetFriends: number;
groupSize: { min: number; max: number };
timeRange: { start: string; end: string };
targetTags: string[];
groupNameTemplate: string;
groupDescription: string;
}
const mockTaskDetail: GroupTaskDetail = {
id: "1",
name: "VIP客户建群",
status: "creating",
totalGroups: 5,
currentGroupIndex: 2,
groups: Array.from({ length: 5 }).map((_, index) => ({
id: `group-${index}`,
members: Array.from({ length: Math.floor(Math.random() * 10) + 30 }).map(
(_, mIndex) => ({
id: `member-${index}-${mIndex}`,
nickname: `用户${mIndex + 1}`,
wechatId: `wx_${mIndex}`,
tags: [`标签${(mIndex % 3) + 1}`],
}),
),
})),
createTime: "2024-11-20 19:04:14",
lastUpdateTime: "2025-02-06 13:12:35",
creator: "admin",
deviceCount: 2,
targetFriends: 156,
groupSize: { min: 20, max: 50 },
timeRange: { start: "09:00", end: "21:00" },
targetTags: ["VIP客户", "高价值"],
groupNameTemplate: "VIP客户交流群{序号}",
groupDescription: "VIP客户专属交流群提供优质服务",
};
const GroupPreview: React.FC<{
groupIndex: number;
members: GroupMember[];
isCreating: boolean;
isCompleted: boolean;
onRetry?: () => void;
}> = ({ groupIndex, members, isCreating, isCompleted, onRetry }) => {
const [expanded, setExpanded] = useState(false);
const targetSize = 38;
return (
<Card className={style.groupCard}>
<div className={style.groupHeader}>
<div>
{groupIndex + 1}
<Tag
color={isCompleted ? "success" : isCreating ? "warning" : "default"}
style={{ marginLeft: 8 }}
>
{isCompleted ? "已完成" : isCreating ? "创建中" : "等待中"}
</Tag>
</div>
<div style={{ color: "#888", fontSize: 12 }}>
<TeamOutline style={{ marginRight: 4 }} />
{members.length}/{targetSize}
</div>
</div>
{isCreating && !isCompleted && (
<ProgressBar
percent={Math.round((members.length / targetSize) * 100)}
style={{ margin: "8px 0" }}
/>
)}
{expanded ? (
<>
<div className={style.memberGrid}>
{members.map(member => (
<div key={member.id} className={style.memberItem}>
<span>{member.nickname}</span>
{member.tags.length > 0 && (
<Tag color="primary" style={{ marginLeft: 4 }}>
{member.tags[0]}
</Tag>
)}
</div>
))}
</div>
<Button
size="mini"
fill="none"
block
onClick={() => setExpanded(false)}
style={{ marginTop: 8 }}
>
</Button>
</>
) : (
<Button
size="mini"
fill="none"
block
onClick={() => setExpanded(true)}
style={{ marginTop: 8 }}
>
({members.length})
</Button>
)}
{!isCompleted && members.length < targetSize && (
<div className={style.warnText}>
<AlertOutlined style={{ marginRight: 4 }} />
{targetSize}
{onRetry && (
<Button
size="mini"
fill="none"
color="primary"
style={{ marginLeft: 8 }}
onClick={onRetry}
>
</Button>
)}
</div>
)}
{isCompleted && <div className={style.successText}></div>}
</Card>
);
};
const GroupCreationProgress: React.FC<{
taskDetail: GroupTaskDetail;
onComplete: () => void;
}> = ({ taskDetail, onComplete }) => {
const [groups, setGroups] = useState<Group[]>(taskDetail.groups);
const [currentGroupIndex, setCurrentGroupIndex] = useState(
taskDetail.currentGroupIndex,
);
const [status, setStatus] = useState<GroupTaskDetail["status"]>(
taskDetail.status,
);
useEffect(() => {
if (status === "creating" && currentGroupIndex < groups.length) {
const timer = setTimeout(() => {
if (currentGroupIndex === groups.length - 1) {
setStatus("completed");
onComplete();
} else {
setCurrentGroupIndex(prev => prev + 1);
}
}, 3000);
return () => clearTimeout(timer);
}
}, [status, currentGroupIndex, groups.length, onComplete]);
const handleRetryGroup = (groupIndex: number) => {
setGroups(prev =>
prev.map((group, index) => {
if (index === groupIndex) {
return {
...group,
members: [
...group.members,
{
id: `retry-member-${Date.now()}`,
nickname: `补充用户${group.members.length + 1}`,
wechatId: `wx_retry_${Date.now()}`,
tags: ["新加入"],
},
],
};
}
return group;
}),
);
};
return (
<div className={style.progressSection}>
<Card className={style.progressCard}>
<div className={style.progressHeader}>
<div>
<Tag
color={
status === "completed"
? "success"
: status === "creating"
? "warning"
: "default"
}
style={{ marginLeft: 8 }}
>
{status === "preparing"
? "准备中"
: status === "creating"
? "创建中"
: "已完成"}
</Tag>
</div>
<div style={{ color: "#888", fontSize: 12 }}>
{currentGroupIndex + 1}/{groups.length}
</div>
</div>
<ProgressBar
percent={Math.round(((currentGroupIndex + 1) / groups.length) * 100)}
style={{ marginTop: 8 }}
/>
</Card>
<div className={style.groupList}>
{groups.map((group, index) => (
<GroupPreview
key={group.id}
groupIndex={index}
members={group.members}
isCreating={status === "creating" && index === currentGroupIndex}
isCompleted={status === "completed" || index < currentGroupIndex}
onRetry={() => handleRetryGroup(index)}
/>
))}
</div>
{status === "completed" && (
<div className={style.successAlert}></div>
)}
</div>
);
};
const AutoGroupDetail: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [taskDetail, setTaskDetail] = useState<GroupTaskDetail | null>(null);
useEffect(() => {
setLoading(true);
setTimeout(() => {
setTaskDetail(mockTaskDetail);
setLoading(false);
}, 800);
}, [id]);
const handleComplete = () => {
Toast.show({ content: "所有群组已创建完成" });
};
if (!taskDetail) {
return (
<Layout
header={<NavCommon title="建群详情" backFn={() => navigate(-1)} />}
>
<Card className={style.emptyCard}>
<AlertOutlined style={{ fontSize: 48, color: "#ccc" }} />
<div className={style.emptyTitle}></div>
<div className={style.emptyDesc}>ID是否正确</div>
<Button
color="primary"
onClick={() => navigate("/workspace/auto-group")}
>
</Button>
</Card>
</Layout>
);
}
return (
<Layout
header={
<NavCommon
title={taskDetail.name + " - 建群详情"}
backFn={() => navigate(-1)}
/>
}
loading={loading}
>
<div className={style.autoGroupDetail}>
<Card className={style.infoCard}>
<div className={style.infoGrid}>
<div>
<div className={style.infoTitle}></div>
<div className={style.infoItem}>{taskDetail.name}</div>
<div className={style.infoItem}>
{taskDetail.createTime}
</div>
<div className={style.infoItem}>{taskDetail.creator}</div>
<div className={style.infoItem}>
{taskDetail.deviceCount}
</div>
</div>
<div>
<div className={style.infoTitle}></div>
<div className={style.infoItem}>
{taskDetail.groupSize.min}-{taskDetail.groupSize.max}{" "}
</div>
<div className={style.infoItem}>
{taskDetail.timeRange.start} -{" "}
{taskDetail.timeRange.end}
</div>
<div className={style.infoItem}>
{taskDetail.targetTags.join(", ")}
</div>
<div className={style.infoItem}>
{taskDetail.groupNameTemplate}
</div>
</div>
</div>
</Card>
<GroupCreationProgress
taskDetail={taskDetail}
onComplete={handleComplete}
/>
</div>
</Layout>
);
};
export default AutoGroupDetail;

View File

@@ -1,17 +0,0 @@
import request from "@/api/request";
// 创建朋友圈同步任务
export const createAutoGroup = (params: any) =>
request("/v1/workbench/create", params, "POST");
// 更新朋友圈同步任务
export const updateAutoGroup = (params: any) =>
request("/v1/workbench/update", params, "POST");
// 获取朋友圈同步任务详情
export const getAutoGroupDetail = (id: string) =>
request("/v1/workbench/detail", { id }, "GET");
// 获取朋友圈同步任务列表
export const getAutoGroupList = (params: any) =>
request("/v1/workbench/list", params, "GET");

View File

@@ -1,336 +0,0 @@
import React, { useImperativeHandle, forwardRef, useEffect } from "react";
import { Button, Card, Switch, Form, InputNumber } from "antd";
import { Input } from "antd";
const { TextArea } = Input;
interface BasicSettingsProps {
initialValues?: {
name: string;
startTime: string;
endTime: string;
groupSizeMin: number;
groupSizeMax: number;
maxGroupsPerDay: number;
groupNameTemplate: string;
groupDescription: string;
status: number;
};
}
export interface BasicSettingsRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
(
{
initialValues = {
name: "",
startTime: "06:00",
endTime: "23:59",
groupSizeMin: 20,
groupSizeMax: 50,
maxGroupsPerDay: 10,
groupNameTemplate: "",
groupDescription: "",
status: 1,
},
},
ref,
) => {
const [form] = Form.useForm();
// 当initialValues变化时重新设置表单值
useEffect(() => {
form.setFieldsValue(initialValues);
}, [form, initialValues]);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("BasicSettings 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
key={JSON.stringify(initialValues)}
onValuesChange={(changedValues, allValues) => {
// 可以在这里处理表单值变化
}}
>
{/* 任务名称 */}
<Form.Item
label="任务名称"
name="name"
rules={[
{ required: true, message: "请输入任务名称" },
{ min: 2, max: 50, message: "任务名称长度在2-50个字符之间" },
]}
>
<Input placeholder="请输入任务名称" />
</Form.Item>
{/* 允许建群的时间段 */}
<Form.Item label="允许建群的时间段">
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<Form.Item
name="startTime"
noStyle
rules={[{ required: true, message: "请选择开始时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
<span style={{ color: "#888" }}></span>
<Form.Item
name="endTime"
noStyle
rules={[{ required: true, message: "请选择结束时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
</div>
</Form.Item>
{/* 每日最大建群数 */}
<Form.Item
label="每日最大建群数"
name="maxGroupsPerDay"
rules={[
{ required: true, message: "请输入每日最大建群数" },
{
validator: (_, value) => {
const numValue = Number(value);
if (value && (numValue < 1 || numValue > 100)) {
return Promise.reject(
new Error("每日最大建群数在1-100之间"),
);
}
return Promise.resolve();
},
},
]}
>
<InputNumber
min={1}
max={100}
placeholder="请输入最大建群数"
step={1}
style={{ width: "100%" }}
value={form.getFieldValue("maxGroupsPerDay")}
onChange={value => form.setFieldValue("maxGroupsPerDay", value)}
addonBefore={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("maxGroupsPerDay") || 1;
const newValue = Math.max(1, currentValue - 1);
form.setFieldValue("maxGroupsPerDay", newValue);
}}
>
-
</Button>
}
addonAfter={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("maxGroupsPerDay") || 1;
const newValue = Math.min(100, currentValue + 1);
form.setFieldValue("maxGroupsPerDay", newValue);
}}
>
+
</Button>
}
/>
</Form.Item>
{/* 群组最小人数 */}
<Form.Item
label="群组最小人数"
name="groupSizeMin"
rules={[
{ required: true, message: "请输入群组最小人数" },
{
validator: (_, value) => {
const numValue = Number(value);
if (value && (numValue < 1 || numValue > 500)) {
return Promise.reject(
new Error("群组最小人数在1-500之间"),
);
}
return Promise.resolve();
},
},
]}
>
<InputNumber
min={1}
max={500}
placeholder="请输入最小人数"
step={1}
style={{ width: "100%" }}
value={form.getFieldValue("groupSizeMin")}
onChange={value => form.setFieldValue("groupSizeMin", value)}
addonBefore={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("groupSizeMin") || 1;
const newValue = Math.max(1, currentValue - 1);
form.setFieldValue("groupSizeMin", newValue);
}}
>
-
</Button>
}
addonAfter={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("groupSizeMin") || 1;
const newValue = Math.min(500, currentValue + 1);
form.setFieldValue("groupSizeMin", newValue);
}}
>
+
</Button>
}
/>
</Form.Item>
{/* 群组最大人数 */}
<Form.Item
label="群组最大人数"
name="groupSizeMax"
rules={[
{ required: true, message: "请输入群组最大人数" },
{
validator: (_, value) => {
const numValue = Number(value);
if (value && (numValue < 1 || numValue > 500)) {
return Promise.reject(
new Error("群组最大人数在1-500之间"),
);
}
return Promise.resolve();
},
},
]}
>
<InputNumber
min={1}
max={500}
placeholder="请输入最大人数"
step={1}
style={{ width: "100%" }}
value={form.getFieldValue("groupSizeMax")}
onChange={value => form.setFieldValue("groupSizeMax", value)}
addonBefore={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("groupSizeMax") || 1;
const newValue = Math.max(1, currentValue - 1);
form.setFieldValue("groupSizeMax", newValue);
}}
>
-
</Button>
}
addonAfter={
<Button
type="text"
onClick={() => {
const currentValue =
form.getFieldValue("groupSizeMax") || 1;
const newValue = Math.min(500, currentValue + 1);
form.setFieldValue("groupSizeMax", newValue);
}}
>
+
</Button>
}
/>
</Form.Item>
{/* 群名称模板 */}
<Form.Item
label="群名称模板"
name="groupNameTemplate"
rules={[
{ required: true, message: "请输入群名称模板" },
{
min: 2,
max: 100,
message: "群名称模板长度在2-100个字符之间",
},
]}
>
<Input placeholder="请输入群名称模板" />
</Form.Item>
{/* 群描述 */}
<Form.Item
label="群描述"
name="groupDescription"
rules={[{ max: 200, message: "群描述不能超过200个字符" }]}
>
<TextArea
placeholder="请输入群描述"
rows={3}
maxLength={200}
showCount
/>
</Form.Item>
{/* 是否启用 */}
<Form.Item
label="是否启用"
name="status"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<Switch />
</div>
</Form.Item>
</Form>
</Card>
</div>
);
},
);
BasicSettings.displayName = "BasicSettings";
export default BasicSettings;

View File

@@ -1,93 +0,0 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
interface DeviceSelectorProps {
selectedDevices: DeviceSelectionItem[];
onNext: (data: {
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
}) => void;
}
export interface DeviceSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const DeviceSelector = forwardRef<DeviceSelectorRef, DeviceSelectorProps>(
({ selectedDevices, onNext }, ref) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("DeviceSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
// 设备选择
const handleDeviceSelect = (deviceGroupsOptions: DeviceSelectionItem[]) => {
const deviceGroups = deviceGroupsOptions.map(item => item.id);
form.setFieldValue("deviceGroups", deviceGroups);
// 通知父组件数据变化
onNext({
deviceGroups: deviceGroups.map(id => String(id)),
deviceGroupsOptions,
});
};
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{
deviceGroups: selectedDevices.map(item => item.id),
}}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="deviceGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个设备组",
},
{ type: "array", max: 20, message: "最多只能选择20个设备组" },
]}
>
<DeviceSelection
selectedOptions={selectedDevices}
onSelect={handleDeviceSelect}
/>
</Form.Item>
</Form>
</Card>
);
},
);
DeviceSelector.displayName = "DeviceSelector";
export default DeviceSelector;

View File

@@ -1,98 +0,0 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import PoolSelection from "@/components/PoolSelection";
import {
PoolSelectionItem,
PoolPackageItem,
} from "@/components/PoolSelection/data";
interface PoolSelectorProps {
selectedPools: PoolSelectionItem[];
onNext: (data: {
poolGroups: string[];
poolGroupsOptions: PoolSelectionItem[];
}) => void;
}
export interface PoolSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const PoolSelector = forwardRef<PoolSelectorRef, PoolSelectorProps>(
({ selectedPools, onNext }, ref) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("PoolSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
// 处理选择变化
const handlePoolChange = (poolGroupsOptions: PoolSelectionItem[]) => {
const poolGroups = poolGroupsOptions.map(c => c.id.toString());
form.setFieldValue("poolGroups", poolGroups);
onNext({
poolGroups,
poolGroupsOptions,
});
};
// 处理详细选择数据
const handleSelectDetail = (poolPackages: PoolPackageItem[]) => {
// 如果需要处理原始流量池包数据,可以在这里添加逻辑
};
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
initialValues={{
poolGroups: selectedPools.map(c => c.id.toString()),
}}
>
<div style={{ marginBottom: 16 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="poolGroups"
rules={[
{ required: true, message: "请选择至少一个流量池包" },
{ type: "array", min: 1, message: "请选择至少一个流量池包" },
{ type: "array", max: 20, message: "最多只能选择20个流量池包" },
]}
>
<PoolSelection
selectedOptions={selectedPools}
onSelect={handlePoolChange}
/>
</Form.Item>
</Form>
</Card>
</div>
);
},
);
PoolSelector.displayName = "PoolSelector";
export default PoolSelector;

View File

@@ -1,25 +0,0 @@
.autoGroupForm {
padding: 10px;
background: #f7f8fa;
}
.headerBar {
display: flex;
align-items: center;
height: 48px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
font-size: 18px;
font-weight: 600;
padding: 0 16px;
}
.title {
font-size: 18px;
font-weight: 600;
color: #222;
flex: 1;
text-align: center;
}

View File

@@ -1,283 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Toast } from "antd-mobile";
import { Button } from "antd";
import Layout from "@/components/Layout/Layout";
import { createAutoGroup, updateAutoGroup, getAutoGroupDetail } from "./api";
import { AutoGroupFormData, StepItem } from "./types";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
import DeviceSelector, { DeviceSelectorRef } from "./components/DeviceSelector";
import PoolSelector, { PoolSelectorRef } from "./components/PoolSelector";
import NavCommon from "@/components/NavCommon/index";
import dayjs from "dayjs";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
const steps: StepItem[] = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择设备" },
{ id: 3, title: "步骤 3", subtitle: "选择流量池包" },
];
const defaultForm: AutoGroupFormData = {
name: "",
type: 4,
deviceGroups: [], // 设备组
deviceGroupsOptions: [], // 设备组选项
poolGroups: [], // 内容库
poolGroupsOptions: [], // 内容库选项
startTime: dayjs().format("HH:mm"), // 开始时间 (HH:mm)
endTime: dayjs().add(1, "hour").format("HH:mm"), // 结束时间 (HH:mm)
groupSizeMin: 20, // 群组最小人数
groupSizeMax: 50, // 群组最大人数
maxGroupsPerDay: 10, // 每日最大建群数
groupNameTemplate: "", // 群名称模板
groupDescription: "", // 群描述
status: 1, // 是否启用 (1: 启用, 0: 禁用)
};
const AutoGroupForm: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams();
const isEdit = Boolean(id);
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(!isEdit); // 非编辑模式直接标记为已加载
const [formData, setFormData] = useState<AutoGroupFormData>(defaultForm);
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<
DeviceSelectionItem[]
>([]);
const [poolGroupsOptions, setpoolGroupsOptions] = useState<
PoolSelectionItem[]
>([]);
// 创建子组件的ref
const basicSettingsRef = useRef<BasicSettingsRef>(null);
const deviceSelectorRef = useRef<DeviceSelectorRef>(null);
const poolSelectorRef = useRef<PoolSelectorRef>(null);
useEffect(() => {
if (!id) return;
// 这里应请求详情接口回填表单演示用mock
getAutoGroupDetail(id).then(res => {
const updatedForm = {
...defaultForm,
name: res.name,
deviceGroups: res.config.deviceGroups || [],
deviceGroupsOptions: res.config.deviceGroupsOptions || [],
poolGroups: res.config.poolGroups || [],
poolGroupsOptions: res.config.poolGroupsOptions || [],
startTime: res.config.startTime,
endTime: res.config.endTime,
groupSizeMin: res.config.groupSizeMin,
groupSizeMax: res.config.groupSizeMax,
maxGroupsPerDay: res.config.maxGroupsPerDay,
groupNameTemplate: res.config.groupNameTemplate,
groupDescription: res.config.groupDescription,
status: res.status,
type: res.type,
id: res.id,
};
setFormData(updatedForm);
setDeviceGroupsOptions(res.config.deviceGroupsOptions || []);
setpoolGroupsOptions(res.config.poolGroupsOptions || []);
setDataLoaded(true); // 标记数据已加载
});
}, [id]);
const handleBasicSettingsChange = (values: Partial<AutoGroupFormData>) => {
setFormData(prev => ({ ...prev, ...values }));
};
// 设备组选择
const handleDevicesChange = (data: {
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
}) => {
setFormData(prev => ({
...prev,
deviceGroups: data.deviceGroups,
}));
setDeviceGroupsOptions(data.deviceGroupsOptions);
};
// 流量池包选择
const handlePoolsChange = (data: {
poolGroups: string[];
poolGroupsOptions: PoolSelectionItem[];
}) => {
setFormData(prev => ({ ...prev, poolGroups: data.poolGroups }));
setpoolGroupsOptions(data.poolGroupsOptions);
};
const handleSave = async () => {
if (!formData.name.trim()) {
Toast.show({ content: "请输入任务名称" });
return;
}
if (formData.deviceGroups.length === 0) {
Toast.show({ content: "请选择至少一个设备组" });
return;
}
if (formData.poolGroups.length === 0) {
Toast.show({ content: "请选择至少一个流量池包" });
return;
}
setLoading(true);
try {
const submitData = {
...formData,
deviceGroupsOptions: deviceGroupsOptions,
poolGroupsOptions: poolGroupsOptions,
};
if (isEdit) {
await updateAutoGroup(submitData);
Toast.show({ content: "编辑成功" });
} else {
await createAutoGroup(submitData);
Toast.show({ content: "创建成功" });
}
navigate("/workspace/auto-group");
} catch (e) {
Toast.show({ content: "提交失败" });
} finally {
setLoading(false);
}
};
const handlePrevious = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleNext = async () => {
if (currentStep < 3) {
try {
let isValid = false;
switch (currentStep) {
case 1:
// 调用 BasicSettings 的表单校验
isValid = (await basicSettingsRef.current?.validate()) || false;
if (isValid) {
const values = basicSettingsRef.current?.getValues();
if (values) {
handleBasicSettingsChange(values);
}
setCurrentStep(2);
}
break;
case 2:
// 调用 DeviceSelector 的表单校验
isValid = (await deviceSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
}
break;
default:
setCurrentStep(currentStep + 1);
}
} catch (error) {
console.log("表单验证失败:", error);
}
}
};
const renderCurrentStep = () => {
// 编辑模式下,等待数据加载完成后再渲染
if (isEdit && !dataLoaded) {
return (
<div style={{ textAlign: "center", padding: "50px" }}>...</div>
);
}
switch (currentStep) {
case 1:
return (
<BasicSettings
ref={basicSettingsRef}
initialValues={{
name: formData.name,
startTime: formData.startTime,
endTime: formData.endTime,
groupSizeMin: formData.groupSizeMin,
groupSizeMax: formData.groupSizeMax,
maxGroupsPerDay: formData.maxGroupsPerDay,
groupNameTemplate: formData.groupNameTemplate,
groupDescription: formData.groupDescription,
status: formData.status,
}}
/>
);
case 2:
return (
<DeviceSelector
ref={deviceSelectorRef}
selectedDevices={deviceGroupsOptions}
onNext={handleDevicesChange}
/>
);
case 3:
return (
<PoolSelector
ref={poolSelectorRef}
selectedPools={poolGroupsOptions}
onNext={handlePoolsChange}
/>
);
default:
return null;
}
};
const renderFooter = () => {
return (
<div className="footer-btn-group">
{currentStep > 1 && (
<Button size="large" onClick={handlePrevious}>
</Button>
)}
{currentStep === 3 ? (
<Button
size="large"
type="primary"
loading={loading}
onClick={handleSave}
>
{isEdit ? "保存修改" : "创建任务"}
</Button>
) : (
<Button size="large" type="primary" onClick={handleNext}>
</Button>
)}
</div>
);
};
return (
<Layout
header={
<>
<NavCommon
title={isEdit ? "编辑建群任务" : "新建建群任务"}
backFn={() => navigate(-1)}
/>
<StepIndicator currentStep={currentStep} steps={steps} />
</>
}
footer={renderFooter()}
>
<div style={{ padding: 12 }}>{renderCurrentStep()}</div>
</Layout>
);
};
export default AutoGroupForm;

View File

@@ -1,69 +0,0 @@
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
// 自动建群表单数据类型定义
export interface AutoGroupFormData {
id?: string; // 任务ID
type: number; // 任务类型
name: string; // 任务名称
deviceGroups: string[]; // 设备组
deviceGroupsOptions: DeviceSelectionItem[]; // 设备组选项
poolGroups: string[]; // 流量池
poolGroupsOptions: PoolSelectionItem[]; // 流量池选项
startTime: string; // 开始时间 (YYYY-MM-DD HH:mm:ss)
endTime: string; // 结束时间 (YYYY-MM-DD HH:mm:ss)
groupSizeMin: number; // 群组最小人数
groupSizeMax: number; // 群组最大人数
maxGroupsPerDay: number; // 每日最大建群数
groupNameTemplate: string; // 群名称模板
groupDescription: string; // 群描述
status: number; // 是否启用 (1: 启用, 0: 禁用)
[key: string]: any;
}
// 步骤定义
export interface StepItem {
id: number;
title: string;
subtitle: string;
}
// 表单验证规则
export const formValidationRules = {
name: [
{ required: true, message: "请输入任务名称" },
{ min: 2, max: 50, message: "任务名称长度应在2-50个字符之间" },
],
deviceGroups: [
{ required: true, message: "请选择设备组" },
{ type: "array", min: 1, message: "至少选择一个设备组" },
],
poolGroups: [
{ required: true, message: "请选择内容库" },
{ type: "array", min: 1, message: "至少选择一个内容库" },
],
startTime: [{ required: true, message: "请选择开始时间" }],
endTime: [{ required: true, message: "请选择结束时间" }],
groupSizeMin: [
{ required: true, message: "请输入群组最小人数" },
{ type: "number", min: 1, max: 500, message: "群组最小人数应在1-500之间" },
],
groupSizeMax: [
{ required: true, message: "请输入群组最大人数" },
{ type: "number", min: 1, max: 500, message: "群组最大人数应在1-500之间" },
],
maxGroupsPerDay: [
{ required: true, message: "请输入每日最大建群数" },
{
type: "number",
min: 1,
max: 100,
message: "每日最大建群数应在1-100之间",
},
],
groupNameTemplate: [
{ required: true, message: "请输入群名称模板" },
{ min: 2, max: 100, message: "群名称模板长度应在2-100个字符之间" },
],
groupDescription: [{ max: 200, message: "群描述不能超过200个字符" }],
};

View File

@@ -1,16 +0,0 @@
import request from "@/api/request";
// 获取自动建群任务列表
// 获取朋友圈同步任务列表
export const getAutoGroupList = (params: any) =>
request("/v1/workbench/list", params, "GET");
// 复制自动建群任务
export function copyAutoGroupTask(id: string): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 删除自动建群任务
export function deleteAutoGroupTask(id: string): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}

View File

@@ -1,173 +0,0 @@
.autoGroupList {
padding: 0 12px;
}
.taskList {
}
.taskCard {
margin-bottom: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none;
background: #fff;
padding: 16px;
}
.taskHeader {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
}
.taskTitle {
flex: 1;
font-size: 16px;
font-weight: 500;
color: #222;
}
.statusRunning {
background: #e6f7e6;
color: #389e0d;
border-radius: 8px;
padding: 2px 8px;
font-size: 12px;
margin-left: 8px;
}
.statusPaused {
background: #f5f5f5;
color: #888;
border-radius: 8px;
padding: 2px 8px;
font-size: 12px;
margin-left: 8px;
}
.statusCompleted {
background: #e6f4ff;
color: #1677ff;
border-radius: 8px;
padding: 2px 8px;
font-size: 12px;
margin-left: 8px;
}
.taskInfoGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 16px;
margin-bottom: 8px;
font-size: 13px;
}
.infoLabel {
color: #888;
font-size: 12px;
}
.infoValue {
color: #222;
font-weight: 500;
font-size: 14px;
}
.taskFooter {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px solid #f0f0f0;
padding-top: 8px;
margin-top: 8px;
}
.footerLeft {
display: flex;
align-items: center;
}
.footerRight {
display: flex;
align-items: center;
}
.expandPanel {
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed #e0e0e0;
}
.expandGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.expandTitle {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
display: flex;
align-items: center;
color: #1677ff;
}
.expandInfo {
font-size: 13px;
color: #444;
margin-bottom: 2px;
}
.expandTags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
background: #f0f5ff;
color: #1677ff;
border-radius: 8px;
padding: 2px 8px;
font-size: 12px;
}
.menuItem {
padding: 8px 12px;
font-size: 14px;
color: #222;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s;
}
.menuItem:hover {
background: #f5f5f5;
}
.menuItemDanger {
padding: 8px 12px;
font-size: 14px;
color: #e53e3e;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s;
}
.menuItemDanger:hover {
background: #fff1f0;
}
.emptyCard {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-top: 32px;
}
.emptyTitle {
font-size: 16px;
color: #888;
margin: 12px 0 4px 0;
}
.emptyDesc {
font-size: 13px;
color: #bbb;
margin-bottom: 16px;
}

View File

@@ -1,342 +0,0 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Card, Popover, Toast } from "antd-mobile";
import { Input, Switch, Pagination } from "antd";
import {
MoreOutline,
AddCircleOutline,
UserAddOutline,
ClockCircleOutline,
} from "antd-mobile-icons";
import {
ReloadOutlined,
PlusOutlined,
SearchOutlined,
} from "@ant-design/icons";
import {
getAutoGroupList,
copyAutoGroupTask,
deleteAutoGroupTask,
} from "./api";
import { comfirm } from "@/utils/common";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import NavCommon from "@/components/NavCommon";
interface GroupTask {
id: string;
name: string;
status: number; // 1 开启, 0 关闭
deviceCount?: number;
targetFriends?: number;
createdGroups?: number;
lastCreateTime?: string;
createTime?: string;
creator?: string;
createInterval?: number;
maxGroupsPerDay?: number;
timeRange?: { start: string; end: string };
groupSize?: { min: number; max: number };
targetTags?: string[];
groupNameTemplate?: string;
groupDescription?: string;
}
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return style.statusRunning;
case 0:
return style.statusPaused;
default:
return style.statusPaused;
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "开启";
case 0:
return "关闭";
default:
return "关闭";
}
};
const AutoGroupList: React.FC = () => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState("");
const [tasks, setTasks] = useState<GroupTask[]>([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const refreshTasks = async (p = page, ps = pageSize) => {
setLoading(true);
try {
const res: any = await getAutoGroupList({ type: 4, page: p, limit: ps });
// 兼容不同返回结构
const list = res?.list || res?.records || res?.data || [];
const totalCount = res?.total || res?.totalCount || list.length;
const normalized: GroupTask[] = (list as any[]).map(item => ({
id: String(item.id),
name: item.name,
status: Number(item.status) === 1 ? 1 : 0,
deviceCount: Array.isArray(item.config?.devices)
? item.config.devices.length
: 0,
maxGroupsPerDay: item.config?.maxGroupsPerDay ?? 0,
timeRange: {
start: item.config?.startTime ?? "-",
end: item.config?.endTime ?? "-",
},
groupSize: {
min: item.config?.groupSizeMin ?? 0,
max: item.config?.groupSizeMax ?? 0,
},
creator: item.creatorName ?? "",
createTime: item.createTime ?? "",
lastCreateTime: item.updateTime ?? "",
}));
setTasks(normalized);
setTotal(totalCount);
} catch (e) {
Toast.show({ content: "获取列表失败" });
} finally {
setLoading(false);
}
};
useEffect(() => {
refreshTasks(1, pageSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleDelete = async (taskId: string) => {
const taskToDelete = tasks.find(task => task.id === taskId);
if (!taskToDelete) return;
try {
await comfirm("确定要删除吗?", {
title: "删除确认",
cancelText: "取消",
confirmText: "删除",
});
await deleteAutoGroupTask(taskId);
setTasks(tasks.filter(task => task.id !== taskId));
Toast.show({ content: "删除成功" });
} catch (error) {
// 用户取消删除或删除失败
if (error !== "cancel") {
Toast.show({ content: "删除失败" });
}
}
};
const handleEdit = (taskId: string) => {
navigate(`/workspace/auto-group/${taskId}/edit`);
};
const handleView = (taskId: string) => {
navigate(`/workspace/auto-group/${taskId}`);
};
// 复制任务
const handleCopy = async (id: string) => {
try {
await copyAutoGroupTask(id);
Toast.show({
content: "复制成功",
position: "top",
});
refreshTasks(); // 重新获取列表
} catch (error) {
Toast.show({
content: "复制失败",
position: "top",
});
}
};
const toggleTaskStatus = (taskId: string) => {
setTasks(prev =>
prev.map(task =>
task.id === taskId
? {
...task,
status: task.status === 1 ? 0 : 1,
}
: task,
),
);
Toast.show({ content: "状态已切换" });
};
const handleCreateNew = () => {
navigate("/workspace/auto-group/new");
};
const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Layout
header={
<>
<NavCommon
title="自动建群"
backFn={() => navigate("/workspace")}
right={
<Button size="small" color="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索计划名称"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={() => refreshTasks()}
loading={false}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
footer={
<div className="pagination-container">
<Pagination
current={page}
pageSize={pageSize}
total={total}
onChange={(p, ps) => {
setPage(p);
setPageSize(ps);
refreshTasks(p, ps);
}}
showSizeChanger
showTotal={t => `${t}`}
/>
</div>
}
loading={loading}
>
<div className={style.autoGroupList}>
<div className={style.taskList}>
{filteredTasks.length === 0 ? (
<Card className={style.emptyCard}>
<UserAddOutline style={{ fontSize: 48, color: "#ccc" }} />
<div className={style.emptyTitle}></div>
<div className={style.emptyDesc}></div>
<Button color="primary" onClick={handleCreateNew}>
<AddCircleOutline />
</Button>
</Card>
) : (
filteredTasks.map(task => (
<Card key={task.id} className={style.taskCard}>
<div className={style.taskHeader}>
<div className={style.taskTitle}>{task.name}</div>
<span className={getStatusColor(task.status)}>
{getStatusText(task.status)}
</span>
<Switch
checked={task.status === 1}
onChange={() => toggleTaskStatus(task.id)}
disabled={false}
style={{ marginLeft: 8 }}
/>
<Popover
content={
<div>
<div
className={style.menuItem}
onClick={() => handleView(task.id)}
>
</div>
<div
className={style.menuItem}
onClick={() => handleEdit(task.id)}
>
</div>
<div
className={style.menuItem}
onClick={() => handleCopy(task.id)}
>
</div>
<div
className={style.menuItemDanger}
onClick={() => handleDelete(task.id)}
>
</div>
</div>
}
trigger="click"
>
<MoreOutline style={{ fontSize: 20, marginLeft: 8 }} />
</Popover>
</div>
<div className={style.taskInfoGrid}>
<div>
<div className={style.infoLabel}></div>
<div className={style.infoValue}>
{task.deviceCount ?? 0}
</div>
</div>
{/* 该字段暂无,预留位 */}
<div>
<div className={style.infoLabel}></div>
<div className={style.infoValue}>
{task.timeRange?.start} - {task.timeRange?.end}
</div>
</div>
<div>
<div className={style.infoLabel}></div>
<div className={style.infoValue}>
{task.maxGroupsPerDay ?? 0}
</div>
</div>
<div>
<div className={style.infoLabel}></div>
<div className={style.infoValue}>{task.creator ?? ""}</div>
</div>
</div>
<div className={style.taskFooter}>
<div className={style.footerLeft}>
<ClockCircleOutline style={{ marginRight: 4 }} />
{task.lastCreateTime}
</div>
<div className={style.footerRight}>
{task.createTime}
</div>
</div>
</Card>
))
)}
</div>
</div>
</Layout>
);
};
export default AutoGroupList;

View File

@@ -1,63 +0,0 @@
import request from "@/api/request";
import {
LikeTask,
CreateLikeTaskData,
UpdateLikeTaskData,
LikeRecord,
PaginatedResponse,
} from "@/pages/mobile/workspace/auto-like/record/data";
// 获取自动点赞任务列表
export function fetchAutoLikeTasks(
params = { type: 1, page: 1, limit: 100 },
): Promise<LikeTask[]> {
return request("/v1/workbench/list", params, "GET");
}
// 获取单个任务详情
export function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建自动点赞任务
export function createAutoLikeTask(data: CreateLikeTaskData): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 1 }, "POST");
}
// 更新自动点赞任务
export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 1 }, "POST");
}
// 删除自动点赞任务
export function deleteAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleAutoLikeTask(data): Promise<any> {
return request("/v1/workbench/update-status", { ...data }, "POST");
}
// 复制自动点赞任务
export function copyAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 获取点赞记录
export function fetchLikeRecords(
workbenchId: string,
page: number = 1,
limit: number = 20,
keyword?: string,
): Promise<PaginatedResponse<LikeRecord>> {
const params: any = {
workbenchId,
page: page.toString(),
limit: limit.toString(),
};
if (keyword) {
params.keyword = keyword;
}
return request("/v1/workbench/records", params, "GET");
}

View File

@@ -1,119 +0,0 @@
// 自动点赞任务状态
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 内容类型
export type ContentType = "text" | "image" | "video" | "link";
// 设备信息
export interface Device {
id: string;
name: string;
status: "online" | "offline";
lastActive: string;
}
// 好友信息
export interface Friend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
tags: string[];
region: string;
source: string;
}
// 点赞记录
export interface LikeRecord {
id: string;
workbenchId: string;
momentsId: string;
snsId: string;
wechatAccountId: string;
wechatFriendId: string;
likeTime: string;
content: string;
resUrls: string[];
momentTime: string;
userName: string;
operatorName: string;
operatorAvatar: string;
friendName: string;
friendAvatar: string;
}
// 自动点赞任务
export interface LikeTask {
id: string;
name: string;
status: LikeTaskStatus;
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: ContentType[];
targetTags: string[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
todayLikeCount: number;
totalLikeCount: number;
updateTime: string;
}
// 创建任务数据
export interface CreateLikeTaskData {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends?: string[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
}
// 更新任务数据
export interface UpdateLikeTaskData extends CreateLikeTaskData {
id: string;
}
// 任务配置
export interface TaskConfig {
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
}
// API响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应类型
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}

View File

@@ -1,278 +0,0 @@
.task-list {
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.task-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
.task-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.task-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.task-name {
font-size: 18px;
font-weight: 600;
color: #333;
}
.task-status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.active {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
&.inactive {
background: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
}
}
.task-controls {
display: flex;
align-items: center;
gap: 8px;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
&:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
}
input:checked + .slider {
background-color: #1890ff;
}
input:checked + .slider:before {
transform: translateX(20px);
}
}
.menu-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
border-radius: 4px;
color: #666;
&:hover {
background: #f5f5f5;
}
}
.menu-dropdown {
position: absolute;
right: 0;
top: 28px;
background: white;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 100;
min-width: 120px;
padding: 4px;
border: 1px solid #e5e5e5;
}
.menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 4px;
font-size: 14px;
gap: 8px;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.danger {
color: #ff4d4f;
&:hover {
background: #fff2f0;
}
}
}
.task-info {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.info-section {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.info-item {
display: flex;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.task-stats {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #666;
border-top: 1px solid #f0f0f0;
padding-top: 10px;
}
.stats-item {
display: flex;
align-items: center;
gap: 8px;
}
.stats-icon {
font-size: 16px;
&.blue {
color: #1890ff;
}
&.green {
color: #52c41a;
}
}
.stats-label {
font-weight: 500;
}
.stats-value {
color: #333;
font-weight: 600;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 16px;
}
.loading-text {
color: #666;
font-size: 14px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-icon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
margin-bottom: 8px;
}
.empty-subtext {
font-size: 14px;
color: #999;
}
// 移动端适配
@media (max-width: 768px) {
.task-info {
grid-template-columns: 1fr;
gap: 16px;
}
.task-stats {
gap: 12px;
align-items: flex-start;
}
.header-content {
padding: 12px 16px;
}
.search-section {
padding: 12px 16px;
}
.task-list {
padding: 0 12px;
}
}

View File

@@ -1,380 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Toast, SpinLoading, Dialog, Card } from "antd-mobile";
import NavCommon from "@/components/NavCommon";
import { Input } from "antd";
import {
PlusOutlined,
CopyOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
MoreOutlined,
LikeOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import {
fetchAutoLikeTasks,
deleteAutoLikeTask,
toggleAutoLikeTask,
copyAutoLikeTask,
} from "./api";
import { LikeTask } from "./data";
import style from "./index.module.scss";
// 卡片菜单组件
interface CardMenuProps {
onView: () => void;
onEdit: () => void;
onCopy: () => void;
onDelete: () => void;
}
const CardMenu: React.FC<CardMenuProps> = ({
onView,
onEdit,
onCopy,
onDelete,
}) => {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setOpen(v => !v)} className={style["menu-btn"]}>
<MoreOutlined />
</button>
{open && (
<div ref={menuRef} className={style["menu-dropdown"]}>
<div
onClick={() => {
onView();
setOpen(false);
}}
className={style["menu-item"]}
>
<EyeOutlined />
</div>
<div
onClick={() => {
onEdit();
setOpen(false);
}}
className={style["menu-item"]}
>
<EditOutlined />
</div>
<div
onClick={() => {
onCopy();
setOpen(false);
}}
className={style["menu-item"]}
>
<CopyOutlined />
</div>
<div
onClick={() => {
onDelete();
setOpen(false);
}}
className={`${style["menu-item"]} ${style["danger"]}`}
>
<DeleteOutlined />
</div>
</div>
)}
</div>
);
};
const AutoLike: React.FC = () => {
const navigate = useNavigate();
const [tasks, setTasks] = useState<LikeTask[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
// 获取任务列表
const fetchTasks = async () => {
setLoading(true);
try {
const Res: any = await fetchAutoLikeTasks();
// 直接就是任务数组,无需再解包
const mappedTasks = Res?.list?.map((task: any) => ({
...task,
status: task.status || 2, // 默认为关闭状态
deviceCount: task.deviceCount || 0,
targetGroup: task.targetGroup || "全部好友",
likeInterval: task.likeInterval || 60,
maxLikesPerDay: task.maxLikesPerDay || 100,
lastLikeTime: task.lastLikeTime || "暂无",
createTime: task.createTime || "",
updateTime: task.updateTime || "",
todayLikeCount: task.todayLikeCount || 0,
totalLikeCount: task.totalLikeCount || 0,
}));
setTasks(mappedTasks);
} catch (error) {
console.error("获取自动点赞任务失败:", error);
Toast.show({
content: "获取任务列表失败",
position: "top",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
// 删除任务
const handleDelete = async (id: string) => {
const result = await Dialog.confirm({
content: "确定要删除这个任务吗?",
confirmText: "删除",
cancelText: "取消",
});
if (result) {
try {
await deleteAutoLikeTask(id);
Toast.show({
content: "删除成功",
position: "top",
});
fetchTasks(); // 重新获取列表
} catch (error) {
Toast.show({
content: "删除失败",
position: "top",
});
}
}
};
// 编辑任务
const handleEdit = (taskId: string) => {
navigate(`/workspace/auto-like/edit/${taskId}`);
};
// 查看任务
const handleView = (taskId: string) => {
navigate(`/workspace/auto-like/record/${taskId}`);
};
// 复制任务
const handleCopy = async (id: string) => {
try {
await copyAutoLikeTask(id);
Toast.show({
content: "复制成功",
position: "top",
});
fetchTasks(); // 重新获取列表
} catch (error) {
Toast.show({
content: "复制失败",
position: "top",
});
}
};
// 切换任务状态
const toggleTaskStatus = async (id: string, status: number) => {
try {
await toggleAutoLikeTask({ id });
Toast.show({
content: status === 1 ? "已暂停" : "已启动",
position: "top",
});
fetchTasks(); // 重新获取列表
} catch (error) {
Toast.show({
content: "操作失败",
position: "top",
});
}
};
// 创建新任务
const handleCreateNew = () => {
navigate("/workspace/auto-like/new");
};
// 过滤任务
const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Layout
header={
<>
<NavCommon
title="自动点赞"
backFn={() => navigate("/workspace")}
right={
<Button size="small" color="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索计划名称"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={fetchTasks}
loading={loading}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
>
<div className={style["auto-like-page"]}>
{/* 任务列表 */}
<div className={style["task-list"]}>
{loading ? (
<div className={style["loading"]}>
<SpinLoading color="primary" />
<div className={style["loading-text"]}>...</div>
</div>
) : filteredTasks.length === 0 ? (
<div className={style["empty-state"]}>
<div className={style["empty-icon"]}>
<LikeOutlined />
</div>
<div className={style["empty-text"]}></div>
<div className={style["empty-subtext"]}>
</div>
</div>
) : (
filteredTasks.map(task => (
<Card key={task.id} className={style["task-card"]}>
<div className={style["task-header"]}>
<div className={style["task-title-section"]}>
<h3 className={style["task-name"]}>{task.name}</h3>
<span
className={`${style["task-status"]} ${
Number(task.status) === 1
? style["active"]
: style["inactive"]
}`}
>
{Number(task.status) === 1 ? "进行中" : "已暂停"}
</span>
</div>
<div className={style["task-controls"]}>
<label className={style["switch"]}>
<input
type="checkbox"
checked={Number(task.status) === 1}
onChange={() =>
toggleTaskStatus(task.id, Number(task.status))
}
/>
<span className={style["slider"]}></span>
</label>
<CardMenu
onView={() => handleView(task.id)}
onEdit={() => handleEdit(task.id)}
onCopy={() => handleCopy(task.id)}
onDelete={() => handleDelete(task.id)}
/>
</div>
</div>
<div className={style["task-info"]}>
<div className={style["info-section"]}>
<div className={style["info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{task.deviceCount}
</span>
</div>
<div className={style["info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{task.targetGroup}
</span>
</div>
</div>
<div className={style["info-section"]}>
<div className={style["info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{task.likeInterval}
</span>
</div>
<div className={style["info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{task.maxLikesPerDay}
</span>
</div>
</div>
</div>
<div className={style["task-stats"]}>
<div className={style["stats-item"]}>
<LikeOutlined
className={`${style["stats-icon"]} ${style["blue"]}`}
/>
<span className={style["stats-label"]}></span>
<span className={style["stats-value"]}>
{task.lastLikeTime}
</span>
</div>
<div className={style["stats-item"]}>
<LikeOutlined
className={`${style["stats-icon"]} ${style["green"]}`}
/>
<span className={style["stats-label"]}></span>
<span className={style["stats-value"]}>
{task.totalLikeCount || 0}
</span>
</div>
</div>
</Card>
))
)}
</div>
</div>
</Layout>
);
};
export default AutoLike;

View File

@@ -1,21 +0,0 @@
import request from "@/api/request";
import {
CreateLikeTaskData,
UpdateLikeTaskData,
LikeTask,
} from "@/pages/workspace/auto-like/record/data";
// 获取自动点赞任务详情
export function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建自动点赞任务
export function createAutoLikeTask(data: CreateLikeTaskData): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 1 }, "POST");
}
// 更新自动点赞任务
export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 1 }, "POST");
}

View File

@@ -1,125 +0,0 @@
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
// 自动点赞任务状态
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 内容类型
export type ContentType = "text" | "image" | "video" | "link";
// 设备信息
export interface Device {
id: string;
name: string;
status: "online" | "offline";
lastActive: string;
}
// 好友信息
export interface Friend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
tags: string[];
region: string;
source: string;
}
// 点赞记录
export interface LikeRecord {
id: string;
workbenchId: string;
momentsId: string;
snsId: string;
wechatAccountId: string;
wechatFriendId: string;
likeTime: string;
content: string;
resUrls: string[];
momentTime: string;
userName: string;
operatorName: string;
operatorAvatar: string;
friendName: string;
friendAvatar: string;
}
// 自动点赞任务
export interface LikeTask {
id: string;
name: string;
status: LikeTaskStatus;
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: ContentType[];
targetTags: string[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
todayLikeCount: number;
totalLikeCount: number;
updateTime: string;
}
// 创建任务数据
export interface CreateLikeTaskData {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
deviceGroups: number[];
deviceGroupsOptions: DeviceSelectionItem[];
friendsGroups: number[];
friendsGroupsOptions: FriendSelectionItem[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
[key: string]: any;
}
// 更新任务数据
export interface UpdateLikeTaskData extends CreateLikeTaskData {
id: string;
}
// 任务配置
export interface TaskConfig {
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
}
// API响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应类型
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}

View File

@@ -1,427 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
import { Button, Input, Switch, Spin, message } from "antd";
import Layout from "@/components/Layout/Layout";
import DeviceSelection from "@/components/DeviceSelection";
import FriendSelection from "@/components/FriendSelection";
import StepIndicator from "@/components/StepIndicator";
import NavCommon from "@/components/NavCommon";
import {
createAutoLikeTask,
updateAutoLikeTask,
fetchAutoLikeTaskDetail,
} from "./api";
import { CreateLikeTaskData, ContentType } from "./data";
import style from "./new.module.scss";
const contentTypeLabels: Record<ContentType, string> = {
text: "文字",
image: "图片",
video: "视频",
link: "链接",
};
const steps = [
{ id: 1, title: "基础设置", subtitle: "设置点赞规则" },
{ id: 2, title: "设备选择", subtitle: "选择执行设备" },
{ id: 3, title: "人群选择", subtitle: "选择目标人群" },
];
const NewAutoLike: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEditMode = !!id;
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(isEditMode);
const [autoEnabled, setAutoEnabled] = useState(false);
const [formData, setFormData] = useState<CreateLikeTaskData>({
name: "",
interval: 5,
maxLikes: 200,
startTime: "08:00",
endTime: "22:00",
contentTypes: ["text", "image", "video"],
deviceGroups: [],
deviceGroupsOptions: [],
friendsGroups: [],
friendsGroupsOptions: [],
targetTags: [],
friendMaxLikes: 10,
enableFriendTags: false,
friendTags: "",
});
useEffect(() => {
if (id) {
fetchTaskDetail();
}
}, [id]);
const fetchTaskDetail = async () => {
setIsLoading(true);
try {
const taskDetail = await fetchAutoLikeTaskDetail(id!);
if (taskDetail) {
const config = (taskDetail as any).config || taskDetail;
setFormData({
name: taskDetail.name || "",
interval: config.likeInterval || config.interval || 5,
maxLikes: config.maxLikesPerDay || config.maxLikes || 200,
startTime: config.timeRange?.start || config.startTime || "08:00",
endTime: config.timeRange?.end || config.endTime || "22:00",
contentTypes: config.contentTypes || ["text", "image", "video"],
deviceGroups: config.deviceGroups || [],
deviceGroupsOptions: config.deviceGroupsOptions || [],
friendsGroups: config.friendsgroups || [],
friendsGroupsOptions: config.friendsGroupsOptions || [],
targetTags: config.targetTags || [],
friendMaxLikes: config.friendMaxLikes || 10,
enableFriendTags: config.enableFriendTags || false,
friendTags: config.friendTags || "",
});
setAutoEnabled(
(taskDetail as any).status === 1 ||
(taskDetail as any).status === "running",
);
}
} catch (error) {
message.error("获取任务详情失败");
navigate("/workspace/auto-like");
} finally {
setIsLoading(false);
}
};
const handleUpdateFormData = (data: Partial<CreateLikeTaskData>) => {
setFormData(prev => ({ ...prev, ...data }));
};
const handleNext = () => {
setCurrentStep(prev => Math.min(prev + 1, 3));
// 滚动到顶部
const mainElement = document.querySelector("main");
if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: "smooth" });
}
};
const handlePrev = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
// 滚动到顶部
const mainElement = document.querySelector("main");
if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: "smooth" });
}
};
const handleComplete = async () => {
if (!formData.name.trim()) {
message.warning("请输入任务名称");
return;
}
if (!formData.deviceGroups || formData.deviceGroups.length === 0) {
message.warning("请选择执行设备");
return;
}
setIsSubmitting(true);
try {
if (isEditMode) {
await updateAutoLikeTask({ ...formData, id });
message.success("更新成功");
} else {
await createAutoLikeTask(formData);
message.success("创建成功");
}
navigate("/workspace/auto-like");
} catch (error) {
message.error(isEditMode ? "更新失败" : "创建失败");
} finally {
setIsSubmitting(false);
}
};
// 步骤器
const renderStepIndicator = () => (
<StepIndicator steps={steps} currentStep={currentStep} />
);
// 步骤1基础设置
const renderBasicSettings = () => (
<div className={style.basicSection}>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Input
placeholder="请输入任务名称"
value={formData.name}
onChange={e => handleUpdateFormData({ name: e.target.value })}
className={style.input}
/>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.counterRow}>
<Button
icon={<MinusOutlined />}
onClick={() =>
handleUpdateFormData({
interval: Math.max(1, formData.interval - 1),
})
}
className={style.counterBtn}
/>
<div className={style.counterInputWrapper}>
<Input
type="number"
min={1}
max={60}
value={formData.interval}
onChange={e =>
handleUpdateFormData({
interval: Number.parseInt(e.target.value) || 1,
})
}
className={style.counterInput}
/>
</div>
<Button
icon={<PlusOutlined />}
onClick={() =>
handleUpdateFormData({ interval: formData.interval + 1 })
}
className={style.counterBtn}
/>
<span className={style.counterUnit}></span>
</div>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.counterRow}>
<Button
icon={<MinusOutlined />}
onClick={() =>
handleUpdateFormData({
maxLikes: Math.max(1, formData.maxLikes - 10),
})
}
className={style.counterBtn}
/>
<div className={style.counterInputWrapper}>
<Input
type="number"
min={1}
max={500}
value={formData.maxLikes}
onChange={e =>
handleUpdateFormData({
maxLikes: Number.parseInt(e.target.value) || 1,
})
}
className={style.counterInput}
/>
</div>
<Button
icon={<PlusOutlined />}
onClick={() =>
handleUpdateFormData({ maxLikes: formData.maxLikes + 10 })
}
className={style.counterBtn}
/>
<span className={style.counterUnit}>/</span>
</div>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.timeRow}>
<Input
type="time"
value={formData.startTime}
onChange={e => handleUpdateFormData({ startTime: e.target.value })}
className={style.inputTime}
/>
<span className={style.timeSeparator}></span>
<Input
type="time"
value={formData.endTime}
onChange={e => handleUpdateFormData({ endTime: e.target.value })}
className={style.inputTime}
/>
</div>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.contentTypes}>
{(["text", "image", "video"] as ContentType[]).map(type => (
<Button
key={type}
type={
formData.contentTypes.includes(type) ? "primary" : "default"
}
ghost={!formData.contentTypes.includes(type)}
className={style.contentTypeBtn}
onClick={() => {
const newTypes = formData.contentTypes.includes(type)
? formData.contentTypes.filter(t => t !== type)
: [...formData.contentTypes, type];
handleUpdateFormData({ contentTypes: newTypes });
}}
>
{contentTypeLabels[type]}
</Button>
))}
</div>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.switchRow}>
<span className={style.switchLabel}></span>
<Switch
checked={formData.enableFriendTags}
onChange={checked =>
handleUpdateFormData({ enableFriendTags: checked })
}
className={style.switch}
/>
</div>
{formData.enableFriendTags && (
<div className={style.formItem}>
<Input
placeholder="请输入标签"
value={formData.friendTags}
onChange={e =>
handleUpdateFormData({ friendTags: e.target.value })
}
className={style.input}
/>
<div className={style.counterTip}></div>
</div>
)}
</div>
<div className={style.formItem}>
<div className={style.switchRow}>
<span className={style.switchLabel}></span>
<Switch
checked={autoEnabled}
onChange={setAutoEnabled}
className={style.switch}
/>
</div>
</div>
<Button
type="primary"
block
onClick={handleNext}
size="large"
className={style.mainBtn}
disabled={!formData.name.trim()}
>
</Button>
</div>
);
// 步骤2设备选择
const renderDeviceSelection = () => (
<div className={style.basicSection}>
<div className={style.formItem}>
<DeviceSelection
selectedOptions={formData.deviceGroupsOptions}
onSelect={devices =>
handleUpdateFormData({
deviceGroups: devices.map(v => v.id),
deviceGroupsOptions: devices,
})
}
showInput={true}
showSelectedList={true}
/>
</div>
<Button
onClick={handlePrev}
className={style.prevBtn}
size="large"
style={{ marginRight: 16 }}
>
</Button>
<Button
type="primary"
onClick={handleNext}
className={style.nextBtn}
size="large"
disabled={formData.deviceGroups.length === 0}
>
</Button>
</div>
);
// 步骤3好友设置
const renderFriendSettings = () => (
<div className={style.basicSection}>
<div className={style.formItem}>
<FriendSelection
selectedOptions={formData.friendsGroupsOptions || []}
onSelect={friends =>
handleUpdateFormData({
friendsGroups: friends.map(f => f.id),
friendsGroupsOptions: friends,
})
}
deviceIds={formData.deviceGroups}
/>
</div>
<Button
onClick={handlePrev}
className={style.prevBtn}
size="large"
style={{ marginRight: 16 }}
>
</Button>
<Button
type="primary"
onClick={handleComplete}
className={style.completeBtn}
size="large"
loading={isSubmitting}
disabled={
!formData.friendsGroups || formData.friendsGroups.length === 0
}
>
{isEditMode ? "更新任务" : "创建任务"}
</Button>
</div>
);
return (
<Layout
header={
<>
<NavCommon title={isEditMode ? "编辑自动点赞" : "新建自动点赞"} />
{renderStepIndicator()}
</>
}
>
<div className={style.formBg}>
{isLoading ? (
<div className={style.formLoading}>
<Spin />
</div>
) : (
<>
{currentStep === 1 && renderBasicSettings()}
{currentStep === 2 && renderDeviceSelection()}
{currentStep === 3 && renderFriendSettings()}
</>
)}
</div>
</Layout>
);
};
export default NewAutoLike;

View File

@@ -1,232 +0,0 @@
.formBg {
background: #f8f6f3;
min-height: 100vh;
padding: 0 0 80px 0;
position: relative;
}
.basicSection {
background: none;
border-radius: 0;
box-shadow: none;
padding: 24px 16px 0 16px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.formItem {
margin-bottom: 24px;
}
.formLabel {
font-size: 15px;
color: #222;
font-weight: 500;
margin-bottom: 10px;
}
.input {
height: 44px;
border-radius: 8px;
font-size: 15px;
}
.timeRow {
display: flex;
align-items: center;
}
.inputTime {
width: 100px;
height: 40px;
border-radius: 8px;
font-size: 15px;
}
.timeTo {
margin: 0 8px;
color: #888;
}
.counterRow {
display: flex;
align-items: center;
gap: 0;
}
.counterBtn {
width: 40px;
height: 40px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
font-size: 16px;
color: #188eee;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border 0.2s;
}
.counterBtn:hover {
border: 1px solid #188eee;
}
.counterValue {
width: 48px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #222;
}
.counterInputWrapper {
position: relative;
width: 80px;
display: flex;
align-items: center;
}
.counterInput {
width: 100%;
height: 40px;
border-radius: 0;
border: 1px solid #e5e7eb;
border-left: none;
border-right: none;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #222;
padding: 0 8px;
}
.counterUnit {
color: #888;
font-size: 14px;
margin-left: 8px;
}
.timeSeparator {
margin: 0 8px;
color: #888;
font-size: 14px;
}
.counterTip {
font-size: 12px;
color: #aaa;
margin-top: 4px;
}
.contentTypes {
display: flex;
gap: 8px;
}
.contentTypeTag {
padding: 8px 16px;
border-radius: 6px;
background: #f5f5f5;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.contentTypeTag:hover {
background: #e5e7eb;
}
.contentTypeTagActive {
padding: 8px 16px;
border-radius: 6px;
background: #e6f7ff;
color: #188eee;
border: 1px solid #91d5ff;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.switchRow {
display: flex;
align-items: center;
justify-content: space-between;
}
.switchLabel {
font-size: 15px;
color: #222;
font-weight: 500;
}
.switch {
margin-top: 0;
}
.selectedTip {
font-size: 13px;
color: #888;
margin-top: 8px;
}
.formStepBtnRow {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
}
.prevBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.nextBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.completeBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.formLoading {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.footerBtnBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 30;
background: #fff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 24px 24px 24px;
display: flex;
justify-content: center;
gap: 16px;
}
.prevBtn,
.nextBtn,
.completeBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 120px;
}

View File

@@ -1,63 +0,0 @@
import request from "@/api/request";
import {
LikeTask,
CreateLikeTaskData,
UpdateLikeTaskData,
LikeRecord,
PaginatedResponse,
} from "@/pages/workspace/auto-like/record/data";
// 获取自动点赞任务列表
export function fetchAutoLikeTasks(
params = { type: 1, page: 1, limit: 100 },
): Promise<LikeTask[]> {
return request("/v1/workbench/list", params, "GET");
}
// 获取单个任务详情
export function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建自动点赞任务
export function createAutoLikeTask(data: CreateLikeTaskData): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 1 }, "POST");
}
// 更新自动点赞任务
export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 1 }, "POST");
}
// 删除自动点赞任务
export function deleteAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleAutoLikeTask(id: string, status: string): Promise<any> {
return request("/v1/workbench/update-status", { id, status }, "POST");
}
// 复制自动点赞任务
export function copyAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 获取点赞记录
export function fetchLikeRecords(
workbenchId: string,
page: number = 1,
limit: number = 20,
keyword?: string,
): Promise<PaginatedResponse<LikeRecord>> {
const params: any = {
workbenchId,
page: page.toString(),
limit: limit.toString(),
};
if (keyword) {
params.keyword = keyword;
}
return request("/v1/workbench/like-records", params, "GET");
}

View File

@@ -1,119 +0,0 @@
// 自动点赞任务状态
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 内容类型
export type ContentType = "text" | "image" | "video" | "link";
// 设备信息
export interface Device {
id: string;
name: string;
status: "online" | "offline";
lastActive: string;
}
// 好友信息
export interface Friend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
tags: string[];
region: string;
source: string;
}
// 点赞记录
export interface LikeRecord {
id: string;
workbenchId: string;
momentsId: string;
snsId: string;
wechatAccountId: string;
wechatFriendId: string;
likeTime: string;
content: string;
resUrls: string[];
momentTime: string;
userName: string;
operatorName: string;
operatorAvatar: string;
friendName: string;
friendAvatar: string;
}
// 自动点赞任务
export interface LikeTask {
id: string;
name: string;
status: LikeTaskStatus;
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: ContentType[];
targetTags: string[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
todayLikeCount: number;
totalLikeCount: number;
updateTime: string;
}
// 创建任务数据
export interface CreateLikeTaskData {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends?: string[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
}
// 更新任务数据
export interface UpdateLikeTaskData extends CreateLikeTaskData {
id: string;
}
// 任务配置
export interface TaskConfig {
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
}
// API响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应类型
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}

View File

@@ -1,306 +0,0 @@
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import {
Button,
Input,
Card,
Badge,
Avatar,
Skeleton,
message,
Spin,
Divider,
Pagination,
} from "antd";
import {
LikeOutlined,
ReloadOutlined,
SearchOutlined,
UserOutlined,
} from "@ant-design/icons";
import styles from "./record.module.scss";
import NavCommon from "@/components/NavCommon";
import { fetchLikeRecords } from "./api";
import Layout from "@/components/Layout/Layout";
// 格式化日期
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch (error) {
return dateString;
}
};
export default function AutoLikeRecord() {
const { id } = useParams<{ id: string }>();
const [records, setRecords] = useState<any[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
useEffect(() => {
if (!id) return;
setRecordsLoading(true);
fetchLikeRecords(id, 1, pageSize)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
message.error("获取点赞记录失败,请稍后重试");
})
.finally(() => setRecordsLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleSearch = () => {
setCurrentPage(1);
fetchLikeRecords(id!, 1, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
message.error("获取点赞记录失败,请稍后重试");
});
};
const handleRefresh = () => {
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
})
.catch(() => {
message.error("获取点赞记录失败,请稍后重试");
});
};
const handlePageChange = (newPage: number) => {
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(newPage);
})
.catch(() => {
message.error("获取点赞记录失败,请稍后重试");
});
};
return (
<Layout
header={
<>
<NavCommon title="点赞记录" />
<div className={styles.headerSearchBar}>
<div className={styles.headerSearchInputWrap}>
<Input
prefix={<SearchOutlined className={styles.headerSearchIcon} />}
placeholder="搜索好友昵称或内容"
className={styles.headerSearchInput}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onPressEnter={handleSearch}
allowClear
/>
</div>
<Button
icon={<ReloadOutlined spin={recordsLoading} />}
onClick={handleRefresh}
loading={recordsLoading}
type="default"
shape="circle"
/>
</div>
</>
}
footer={
<>
<div className={styles.footerPagination}>
<Pagination
current={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
size="default"
className={styles.pagination}
/>
</div>
</>
}
>
<div className={styles.bgWrap}>
<div className={styles.contentWrap}>
{recordsLoading ? (
<div className={styles.skeletonWrap}>
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className={styles.skeletonCard}>
<div className={styles.skeletonCardHeader}>
<Skeleton.Avatar
active
size={40}
className={styles.skeletonAvatar}
/>
<div className={styles.skeletonNameWrap}>
<Skeleton.Input
active
size="small"
className={styles.skeletonName}
style={{ width: 96 }}
/>
<Skeleton.Input
active
size="small"
className={styles.skeletonSub}
style={{ width: 64 }}
/>
</div>
</div>
<Divider className={styles.skeletonSep} />
<div className={styles.skeletonContentWrap}>
<Skeleton.Input
active
size="small"
className={styles.skeletonContent1}
style={{ width: "100%" }}
/>
<Skeleton.Input
active
size="small"
className={styles.skeletonContent2}
style={{ width: "75%" }}
/>
<div className={styles.skeletonImgWrap}>
<Skeleton.Image
active
className={styles.skeletonImg}
style={{ width: 80, height: 80 }}
/>
<Skeleton.Image
active
className={styles.skeletonImg}
style={{ width: 80, height: 80 }}
/>
</div>
</div>
</div>
))}
</div>
) : records.length === 0 ? (
<div className={styles.emptyWrap}>
<LikeOutlined className={styles.emptyIcon} />
<p className={styles.emptyText}></p>
</div>
) : (
<>
{records.map(record => (
<div key={record.id} className={styles.recordCard}>
<div className={styles.recordCardHeader}>
<div className={styles.recordCardHeaderLeft}>
<Avatar
src={record.friendAvatar || undefined}
icon={<UserOutlined />}
size={40}
className={styles.avatarImg}
/>
<div className={styles.friendInfo}>
<div
className={styles.friendName}
title={record.friendName}
>
{record.friendName}
</div>
<div className={styles.friendSub}></div>
</div>
</div>
<Badge
className={styles.timeBadge}
count={formatDate(record.momentTime || record.likeTime)}
style={{
background: "#e8f0fe",
color: "#333",
fontWeight: 400,
}}
/>
</div>
<Divider className={styles.cardSep} />
<div className={styles.cardContent}>
{record.content && (
<p className={styles.contentText}>{record.content}</p>
)}
{Array.isArray(record.resUrls) &&
record.resUrls.length > 0 && (
<div
className={
`${styles.imgGrid} ` +
(record.resUrls.length === 1
? styles.grid1
: record.resUrls.length === 2
? styles.grid2
: record.resUrls.length <= 3
? styles.grid3
: record.resUrls.length <= 6
? styles.grid6
: styles.grid9)
}
>
{record.resUrls
.slice(0, 9)
.map((image: string, idx: number) => (
<div key={idx} className={styles.imgItem}>
<img
src={image}
alt={`内容图片 ${idx + 1}`}
className={styles.img}
/>
</div>
))}
</div>
)}
</div>
<div className={styles.operatorWrap}>
<Avatar
src={record.operatorAvatar || undefined}
icon={<UserOutlined />}
size={32}
className={styles.operatorAvatar}
/>
<div className={styles.operatorInfo}>
<span
className={styles.operatorName}
title={record.operatorName}
>
{record.operatorName}
</span>
<span className={styles.operatorAction}>
<LikeOutlined
style={{ color: "red", marginRight: 4 }}
/>
</span>
</div>
</div>
</div>
))}
</>
)}
</div>
</div>
</Layout>
);
}

View File

@@ -1,268 +0,0 @@
// 搜索栏
.headerSearchBar {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
.headerSearchInputWrap {
position: relative;
flex: 1;
}
.headerSearchIcon {
position: absolute;
left: 12px;
top: 10px;
width: 16px;
height: 16px;
color: #a3a3a3;
}
.headerSearchInput {
padding-left: 32px !important;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
// 分页
.footerPagination {
display: flex;
justify-content: center;
align-items: center;
padding: 12px 0;
background: #fff;
}
.pagination {
:global(.ant-pagination-item) {
border-radius: 6px;
}
:global(.ant-pagination-item-active) {
background: #1890ff;
border-color: #1890ff;
}
:global(.ant-pagination-prev),
:global(.ant-pagination-next) {
border-radius: 6px;
}
:global(.ant-pagination-jump-prev),
:global(.ant-pagination-jump-next) {
border-radius: 6px;
}
}
// 背景和内容
.bgWrap {
background: #f7f7fa;
min-height: 100vh;
padding-bottom: 80px;
}
.contentWrap {
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
// 骨架屏
.skeletonWrap {
display: flex;
flex-direction: column;
gap: 16px;
}
.skeletonCard {
padding: 0px;
}
.skeletonCardHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.skeletonAvatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.skeletonNameWrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeletonName {
width: 96px;
height: 16px;
}
.skeletonSub {
width: 64px;
height: 12px;
}
.skeletonSep {
margin: 12px 0;
}
.skeletonContentWrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeletonContent1 {
width: 100%;
height: 16px;
}
.skeletonContent2 {
width: 75%;
height: 16px;
}
.skeletonImgWrap {
display: flex;
gap: 8px;
margin-top: 12px;
}
.skeletonImg {
width: 80px;
height: 80px;
border-radius: 8px;
}
// 空状态
.emptyWrap {
text-align: center;
padding: 48px 0;
}
.emptyIcon {
width: 48px;
height: 48px;
color: #e5e7eb;
margin: 0 auto 12px auto;
}
.emptyText {
color: #888;
font-size: 16px;
}
// 记录卡片
.recordCard {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px;
}
.recordCardHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.recordCardHeaderLeft {
display: flex;
align-items: center;
gap: 12px;
max-width: 65%;
}
.avatarImg {
width: 40px;
height: 40px;
border-radius: 50%;
}
.friendInfo {
min-width: 0;
}
.friendName {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.friendSub {
font-size: 13px;
color: #888;
}
.timeBadge {
background: #e8f0fe;
white-space: nowrap;
flex-shrink: 0;
}
.cardSep {
margin: 12px 0;
}
.cardContent {
margin-bottom: 12px;
}
.contentText {
color: #444;
margin-bottom: 12px;
white-space: pre-line;
}
.imgGrid {
display: grid;
gap: 8px;
}
.grid1 {
grid-template-columns: 1fr;
}
.grid2 {
grid-template-columns: 1fr 1fr;
}
.grid3 {
grid-template-columns: 1fr 1fr 1fr;
}
.grid6 {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.grid9 {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
.imgItem {
position: relative;
aspect-ratio: 1/1;
border-radius: 8px;
overflow: hidden;
}
.img {
width: 100%;
height: 100%;
object-fit: cover;
}
// 操作人
.operatorWrap {
display: flex;
align-items: center;
margin-top: 16px;
padding: 8px;
background: #f3f4f6;
border-radius: 8px;
}
.operatorAvatar {
width: 32px !important;
height: 32px !important;
margin-right: 8px;
flex-shrink: 0;
}
.operatorInfo {
font-size: 14px;
position: relative;
flex: 1;
position: relative;
}
.operatorName {
font-weight: 500;
max-width: 100%;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.operatorAction {
color: #888;
margin-left: 8px;
font-size: 12px;
position: absolute;
right: 0;
top: 2px;
}

View File

@@ -1,325 +0,0 @@
.container {
padding: 12px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
font-size: 14px;
gap: 8px;
}
.empty {
display: flex;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.actionCard {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:global(.adm-card-body) {
padding: 16px;
}
}
.taskHeader {
margin-bottom: 16px;
}
.taskInfo {
display: flex;
align-items: center;
justify-content: space-between;
}
.taskName {
font-size: 18px;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 12px;
}
.taskStatus {
font-size: 14px;
font-weight: 500;
padding: 4px 12px;
border-radius: 12px;
background-color: rgba(82, 196, 26, 0.1);
white-space: nowrap;
}
.actions {
display: flex;
gap: 20px;
:global(.adm-button) {
flex: 1;
border-radius: 6px;
font-size: 14px;
}
}
.tabs {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
:global(.adm-tabs-header) {
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
:global(.adm-tabs-tab) {
font-size: 15px;
font-weight: 500;
&.adm-tabs-tab-active {
color: #1890ff;
}
}
:global(.adm-tabs-tab-line) {
background: #1890ff;
}
}
.tabContent {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.infoCard {
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
:global(.adm-card-body) {
padding: 16px;
}
}
.cardTitle {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.infoList {
display: flex;
flex-direction: column;
gap: 12px;
}
.infoItem {
display: flex;
align-items: center;
font-size: 14px;
line-height: 1.5;
}
.label {
color: #666;
margin-right: 12px;
min-width: 80px;
flex-shrink: 0;
}
.value {
color: #333;
flex: 1;
word-break: break-all;
}
.recordList {
:global(.adm-list-body) {
border: none;
}
}
.recordItem {
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
margin-bottom: 12px;
:global(.adm-list-item-content) {
padding: 16px;
}
&:last-child {
margin-bottom: 0;
}
}
.recordHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.recordInfo {
flex: 1;
}
.recordDevice {
font-size: 14px;
font-weight: 500;
color: #333;
}
.recordTime {
font-size: 12px;
color: #999;
white-space: nowrap;
}
.recordContent {
display: flex;
flex-direction: column;
gap: 8px;
}
.recordDetail {
display: flex;
align-items: center;
font-size: 13px;
.label {
min-width: 70px;
color: #666;
}
.value {
color: #333;
}
}
.recordError {
font-size: 13px;
color: #ff4d4f;
background: #fff2f0;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ffccc7;
}
.loadingMore {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
font-size: 14px;
gap: 8px;
}
// 状态标签样式覆盖
:global {
.adm-tag {
border-radius: 12px;
font-size: 12px;
padding: 2px 8px;
&.adm-tag-success {
background: #f6ffed;
border-color: #b7eb8f;
color: #52c41a;
}
&.adm-tag-danger {
background: #fff2f0;
border-color: #ffccc7;
color: #ff4d4f;
}
&.adm-tag-warning {
background: #fff7e6;
border-color: #ffd591;
color: #fa8c16;
}
}
}
// 下拉刷新样式
:global(.adm-pull-to-refresh) {
.adm-pull-to-refresh-indicator {
color: #1890ff;
}
}
// 响应式设计
@media (max-width: 480px) {
.taskName {
font-size: 16px;
}
.taskStatus {
font-size: 13px;
padding: 3px 10px;
}
.cardTitle {
font-size: 15px;
}
.infoItem {
font-size: 13px;
}
.label {
min-width: 70px;
}
.recordDevice {
font-size: 13px;
}
.recordTime {
font-size: 11px;
}
.recordDetail {
font-size: 12px;
.label {
min-width: 60px;
}
}
.recordError {
font-size: 12px;
padding: 6px 10px;
}
}
// 空状态样式
:global(.adm-empty) {
padding: 40px 20px;
.adm-empty-image {
width: 60px;
height: 60px;
opacity: 0.3;
}
.adm-empty-description {
color: #999;
font-size: 14px;
margin-top: 12px;
}
}

View File

@@ -1,467 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Button,
Toast,
Card,
Tabs,
List,
Tag,
Space,
InfiniteScroll,
PullToRefresh,
Empty,
SpinLoading,
} from "antd-mobile";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import {
fetchContactImportTaskDetail,
fetchImportRecords,
triggerImport,
toggleContactImportTask,
} from "../list/api";
import { ContactImportRecord } from "../list/data";
import {
PlayCircleOutlined,
PauseCircleOutlined,
EditOutlined,
ReloadOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
} from "@ant-design/icons";
import style from "./index.module.scss";
const ContactImportDetail: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const [task, setTask] = useState<any>(null);
const [records, setRecords] = useState<ContactImportRecord[]>([]);
const [loading, setLoading] = useState(false);
const [recordsLoading, setRecordsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [activeTab, setActiveTab] = useState("info");
// 获取任务详情
const loadTaskDetail = async () => {
if (!id) return;
setLoading(true);
try {
const response = await fetchContactImportTaskDetail(id);
const data = response?.data || response;
setTask(data);
} catch (error) {
Toast.show({
content: "获取任务详情失败",
icon: "fail",
});
navigate("/workspace/contact-import/list");
} finally {
setLoading(false);
}
};
// 获取导入记录
const loadRecords = async (pageNum: number = 1, reset: boolean = false) => {
if (!id) return;
setRecordsLoading(true);
try {
const response = await fetchImportRecords(id, pageNum, 20);
const data = response?.data || response;
if (reset) {
setRecords(data.list || []);
} else {
setRecords(prev => [...prev, ...(data.list || [])]);
}
setHasMore(data.list.length === 20);
setPage(pageNum);
} catch (error) {
Toast.show({
content: "获取记录失败",
icon: "fail",
});
} finally {
setRecordsLoading(false);
}
};
// 切换任务状态
const handleToggleStatus = async () => {
if (!task) return;
try {
await toggleContactImportTask({
id: task.id,
status: task.status === 1 ? 2 : 1,
});
Toast.show({
content: task.status === 1 ? "任务已暂停" : "任务已启动",
icon: "success",
});
loadTaskDetail();
} catch (error) {
Toast.show({
content: "操作失败",
icon: "fail",
});
}
};
// 手动触发导入
const handleTriggerImport = async () => {
if (!task) return;
try {
await triggerImport(task.id);
Toast.show({
content: "导入任务已触发",
icon: "success",
});
setTimeout(() => {
loadTaskDetail();
loadRecords(1, true);
}, 1000);
} catch (error) {
Toast.show({
content: "触发失败",
icon: "fail",
});
}
};
// 刷新数据
const handleRefresh = async () => {
await loadTaskDetail();
if (activeTab === "records") {
await loadRecords(1, true);
}
};
// 加载更多记录
const loadMoreRecords = async () => {
await loadRecords(page + 1);
};
// 获取状态文本和颜色
const getStatusInfo = (status: number) => {
return status === 1
? { text: "运行中", color: "#52c41a" }
: { text: "已暂停", color: "#faad14" };
};
// 获取记录状态图标
const getRecordStatusIcon = (status: string) => {
switch (status) {
case "success":
return <CheckCircleOutlined style={{ color: "#52c41a" }} />;
case "failed":
return <CloseCircleOutlined style={{ color: "#ff4d4f" }} />;
case "pending":
return <ClockCircleOutlined style={{ color: "#faad14" }} />;
default:
return null;
}
};
// 获取记录状态标签
const getRecordStatusTag = (status: string) => {
switch (status) {
case "success":
return <Tag color="success"></Tag>;
case "failed":
return <Tag color="danger"></Tag>;
case "pending":
return <Tag color="warning"></Tag>;
default:
return <Tag color="default"></Tag>;
}
};
useEffect(() => {
loadTaskDetail();
}, [id]);
useEffect(() => {
// if (activeTab === "records" && records.length === 0) {
// loadRecords(1, true);
// }
}, [activeTab]);
if (loading) {
return (
<Layout
header={
<NavCommon
left={
<Button
fill="none"
size="small"
onClick={() => navigate("/workspace/contact-import/list")}
>
</Button>
}
title="任务详情"
/>
}
>
<div className={style.loading}>
<SpinLoading /> ...
</div>
</Layout>
);
}
if (!task) {
return (
<Layout
header={
<NavCommon
left={
<Button
fill="none"
size="small"
onClick={() => navigate("/workspace/contact-import/list")}
>
</Button>
}
title="任务详情"
/>
}
>
<div className={style.empty}>
<Empty description="任务不存在" />
</div>
</Layout>
);
}
const statusInfo = getStatusInfo(task.status);
return (
<Layout
header={
<>
<NavCommon
title="任务详情"
right={
<Button
color="primary"
onClick={() =>
navigate(`/workspace/contact-import/form/${task.id}`)
}
>
<EditOutlined />
</Button>
}
/>
{/* 任务操作栏 */}
<Card className={style.actionCard}>
<div className={style.taskHeader}>
<div className={style.taskInfo}>
<div className={style.taskName}>{task.name}</div>
<div
className={style.taskStatus}
style={{ color: statusInfo.color }}
>
{statusInfo.text}
</div>
</div>
</div>
<div className={style.actions}>
<Button
onClick={handleToggleStatus}
color={task.status === 1 ? "warning" : "primary"}
>
{task.status === 1 ? (
<>
<PauseCircleOutlined />
</>
) : (
<>
<PlayCircleOutlined />
</>
)}
</Button>
<Button
size="small"
onClick={handleTriggerImport}
disabled={task.status !== 1}
>
<ReloadOutlined />
</Button>
</div>
</Card>
</>
}
>
<PullToRefresh onRefresh={handleRefresh}>
<div className={style.container}>
{/* 标签页 */}
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
className={style.tabs}
>
<Tabs.Tab title="任务信息" key="info">
<div className={style.tabContent}>
{/* 基本信息 */}
<Card className={style.infoCard}>
<div className={style.cardTitle}></div>
<div className={style.infoList}>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.name}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.deviceGroups?.length || 0}
</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.num}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>ID:</span>
<span className={style.value}>{task.clientId}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.remarkType}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.remarkValue}</span>
</div>
</div>
</Card>
{/* 时间配置 */}
<Card className={style.infoCard}>
<div className={style.cardTitle}></div>
<div className={style.infoList}>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.startTime}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.endTime}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.maxImportsPerDay}
</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.importInterval}
</span>
</div>
</div>
</Card>
{/* 统计信息 */}
<Card className={style.infoCard}>
<div className={style.cardTitle}></div>
<div className={style.infoList}>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.todayImportCount}
</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.totalImportCount}
</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.createTime}</span>
</div>
<div className={style.infoItem}>
<span className={style.label}>:</span>
<span className={style.value}>{task.updateTime}</span>
</div>
</div>
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="导入记录" key="records">
<div className={style.tabContent}>
{records.length === 0 && !recordsLoading ? (
<Empty description="暂无导入记录" />
) : (
<List className={style.recordList}>
{records.map(record => (
<List.Item key={record.id} className={style.recordItem}>
<div className={style.recordHeader}>
<div className={style.recordInfo}>
<Space>
{getRecordStatusIcon(record.importStatus)}
<span className={style.recordDevice}>
{record.deviceName}
</span>
{getRecordStatusTag(record.importStatus)}
</Space>
</div>
<div className={style.recordTime}>
{record.createTime}
</div>
</div>
<div className={style.recordContent}>
<div className={style.recordDetail}>
<span className={style.label}>:</span>
<span className={style.value}>{record.num}</span>
</div>
<div className={style.recordDetail}>
<span className={style.label}>:</span>
<span className={style.value}>
{record.remarkType}: {record.remarkValue}
</span>
</div>
{record.errorMessage && (
<div className={style.recordError}>
: {record.errorMessage}
</div>
)}
</div>
</List.Item>
))}
</List>
)}
<InfiniteScroll
loadMore={loadMoreRecords}
hasMore={hasMore}
threshold={10}
>
{recordsLoading && (
<div className={style.loadingMore}>
<SpinLoading /> ...
</div>
)}
</InfiniteScroll>
</div>
</Tabs.Tab>
</Tabs>
</div>
</PullToRefresh>
</Layout>
);
};
export default ContactImportDetail;

View File

@@ -1,91 +0,0 @@
import request from "@/api/request";
import {
Allocation,
CreateContactImportTaskData,
UpdateContactImportTaskData,
ContactImportRecord,
PaginatedResponse,
ImportStats,
} from "./data";
// 获取通讯录导入任务列表
export function fetchContactImportTasks(
params = { type: 6, page: 1, limit: 10 },
) {
return request("/v1/workbench/list", params, "GET");
}
// 获取单个任务详情
export function fetchContactImportTaskDetail(id: number) {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建通讯录导入任务
export function createContactImportTask(data: Allocation): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 6 }, "POST");
}
// 更新通讯录导入任务
export function updateContactImportTask(data: Allocation): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 6 }, "POST");
}
// 删除通讯录导入任务
export function deleteContactImportTask(id: number): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleContactImportTask(data: {
id: number;
status: number;
}): Promise<any> {
return request("/v1/workbench/update-status", { ...data }, "POST");
}
// 复制通讯录导入任务
export function copyContactImportTask(id: number): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 获取导入记录
export function fetchImportRecords(
workbenchId: number,
page: number = 1,
limit: number = 20,
keyword?: string,
): Promise<PaginatedResponse<ContactImportRecord>> {
return request(
"/v1/workbench/import-records",
{
workbenchId,
page,
limit,
keyword,
},
"GET",
);
}
// 获取统计数据
export function fetchImportStats(): Promise<ImportStats> {
return request("/v1/workbench/import-stats", {}, "GET");
}
// 获取设备组列表
export function fetchDeviceGroups(): Promise<any[]> {
return request("/v1/device/groups", {}, "GET");
}
// 手动触发导入
export function triggerImport(taskId: number): Promise<any> {
return request("/v1/workbench/trigger-import", { taskId }, "POST");
}
// 批量操作任务
export function batchOperateTasks(data: {
taskIds: number[];
operation: "start" | "stop" | "delete";
}): Promise<any> {
return request("/v1/workbench/batch-operate", data, "POST");
}

View File

@@ -1,168 +0,0 @@
// 分配接口类型
export interface Allocation {
/** 主键ID */
id?: number;
/** 任务名称 */
name: string;
//是否启用0关闭1启用
status: number;
//任务类型固定为6
type: number;
/** 工作台ID */
workbenchId: number;
/** 设备id */
deviceGroups: number[];
/** 流量池 */
pools?: JSON | null;
/** 分配数量 */
num?: number | null;
/** 是否清除现有联系人默认0 */
clearContact?: number;
/** 备注类型 0不备注 1年月日 2月日 3自定义默认0 */
remarkType: number;
/** 备注 */
remark?: string | null;
/** 开始时间 */
startTime?: string | null;
/** 结束时间 */
endTime?: string | null;
[key: string]: any;
}
// 通讯录导入任务状态
export type ContactImportTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 设备组信息
export interface DeviceGroup {
id: string;
name: string;
deviceCount: number;
status: "online" | "offline";
lastActive: string;
}
// 通讯录导入记录
export interface ContactImportRecord {
id: string;
workbenchId: string;
wechatAccountId: string;
deviceId: string;
num: number;
clientId: string;
remarkType: string;
remarkValue: string;
startTime: string;
endTime: string;
createTime: string;
operatorName: string;
operatorAvatar: string;
deviceName: string;
importStatus: "success" | "failed" | "pending";
errorMessage?: string;
}
// 通讯录导入任务配置
export interface ContactImportTaskConfig {
id: number;
workbenchId: number;
devices: number[];
pools: number[];
num: number;
clearContact: number;
remarkType: number;
remark: string;
startTime: string;
endTime: string;
createTime: string;
}
// 通讯录导入任务
export interface ContactImportTask {
id: number;
companyId: number;
name: string;
type: number;
status: ContactImportTaskStatus;
autoStart: number;
userId: number;
createTime: string;
updateTime: string;
config: ContactImportTaskConfig;
creatorName: string;
auto_like: any;
moments_sync: any;
traffic_config: any;
group_push: any;
group_create: any;
// 计算属性,用于向后兼容
deviceGroups?: string[];
todayImportCount?: number;
totalImportCount?: number;
maxImportsPerDay?: number;
importInterval?: number;
}
// 创建通讯录导入任务数据
export interface CreateContactImportTaskData {
name: string;
type: number;
config: {
devices: number[];
pools: number[];
num: number;
clearContact: number;
remarkType: number;
remark: string;
startTime: string;
endTime: string;
};
}
// 更新通讯录导入任务数据
export interface UpdateContactImportTaskData
extends CreateContactImportTaskData {
id: number;
}
// 任务配置
export interface TaskConfig {
deviceGroups: string[];
num: number;
clientId: string;
remarkType: string;
remarkValue: string;
startTime: string;
endTime: string;
maxImportsPerDay: number;
importInterval: number;
}
// API响应
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}
// 统计数据
export interface ImportStats {
totalTasks: number;
activeTasks: number;
todayImports: number;
totalImports: number;
successRate: number;
}

View File

@@ -1,146 +0,0 @@
.basicSection {
background: none;
border-radius: 0;
box-shadow: none;
padding: 24px 16px 0 16px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.formItem {
margin-bottom: 24px;
}
.formLabel {
font-size: 15px;
color: #222;
font-weight: 500;
margin-bottom: 10px;
display: block;
}
.input {
height: 44px;
border-radius: 8px;
font-size: 15px;
width: 100%;
}
.select {
width: 100%;
height: 44px;
:global(.ant-select-selector) {
height: 44px !important;
border-radius: 8px !important;
font-size: 15px !important;
}
:global(.ant-select-selection-item) {
line-height: 42px !important;
}
}
.timePicker {
width: 100%;
height: 44px;
:global(.ant-picker) {
width: 100%;
height: 44px;
border-radius: 8px;
font-size: 15px;
}
}
.counterRow {
display: flex;
align-items: center;
gap: 0;
}
.stepperContainer {
display: flex;
align-items: center;
gap: 0;
}
.stepperButton {
width: 40px;
height: 40px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
font-size: 16px;
color: #188eee;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border 0.2s;
&:hover {
border: 1px solid #188eee;
}
}
.stepperInput {
width: 80px;
height: 40px;
border-radius: 0;
border: 1px solid #e5e7eb;
border-left: none;
border-right: none;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #222;
padding: 0 8px;
}
.counterTip {
font-size: 12px;
color: #aaa;
margin-top: 4px;
}
.buttonGroup {
display: flex;
gap: 12px;
padding: 14px;
background: #fff;
}
.submitButton {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 120px;
flex: 1;
}
.resetButton {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.deviceSelection {
width: 100%;
:global {
.ant-input {
border-radius: 8px;
border: 1px solid #e8e8e8;
padding: 12px 16px;
font-size: 14px;
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}

View File

@@ -1,355 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import DeviceSelection from "@/components/DeviceSelection";
import {
createContactImportTask,
updateContactImportTask,
fetchContactImportTaskDetail,
} from "./api";
import { Allocation } from "./data";
import style from "./index.module.scss";
import dayjs from "dayjs";
const ContactImportForm: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id?: string }>();
const isEdit = !!id;
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: "", // 任务名称
status: 1, // 是否启用,默认启用
type: 6, // 任务类型固定为6
workbenchId: 1, // 默认工作台ID
deviceGroups: [] as number[],
pools: [] as any[],
num: 50,
clearContact: 0,
remarkType: 0,
remark: "",
startTime: dayjs("09:00", "HH:mm"),
endTime: dayjs("21:00", "HH:mm"),
// 保留原有字段用于UI显示
deviceGroupsOptions: [] as any[],
});
// 处理设备选择
const handleDeviceSelect = (selectedDevices: any[]) => {
setFormData(prev => ({
...prev,
deviceGroupsOptions: selectedDevices,
deviceGroups: selectedDevices.map(device => device.id), // 提取设备ID存储到deviceGroups数组
}));
};
// 获取任务详情(编辑模式)
const loadTaskDetail = async () => {
if (!id) return;
try {
setLoading(true);
const data = await fetchContactImportTaskDetail(Number(id));
if (data) {
const config = data.config || {};
// 构造设备选择组件需要的数据格式
const deviceGroupsOptions = config.deviceGroupsOptions || [];
setFormData({
name: data.name || "",
status: data.status || 1,
type: data.type || 6,
workbenchId: config.workbenchId || 1,
deviceGroups:
deviceGroupsOptions.map((device: any) => device.id) || [],
pools: config.pools ? JSON.parse(JSON.stringify(config.pools)) : [],
num: config.num || 50,
clearContact: config.clearContact || 0,
remarkType: config.remarkType || 0,
remark: config.remark || "",
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
deviceGroupsOptions,
});
}
} catch (error) {
console.error("Failed to load task detail:", error);
message.error("获取任务详情失败");
navigate("/workspace/contact-import/list");
} finally {
setLoading(false);
}
};
// 更新表单数据
const handleUpdateFormData = (data: Partial<typeof formData>) => {
setFormData(prev => ({ ...prev, ...data }));
};
// 提交表单
const handleSubmit = async () => {
// 表单验证
if (!formData.name.trim()) {
message.error("请输入任务名称");
return;
}
if (formData.deviceGroups.length === 0) {
message.error("请选择设备");
return;
}
if (!formData.num || formData.num <= 0) {
message.error("请输入有效的分配数量");
return;
}
// 验证开始时间不得大于结束时间
if (formData.startTime && formData.endTime) {
if (formData.startTime.isAfter(formData.endTime)) {
message.error("开始时间不得大于结束时间");
return;
}
}
setLoading(true);
try {
const submitData: Partial<Allocation> = {
name: formData.name,
status: formData.status,
type: formData.type,
workbenchId: formData.workbenchId,
deviceGroups: formData.deviceGroups,
pools: JSON.parse(JSON.stringify(formData.pools)),
num: formData.num,
clearContact: formData.clearContact,
remarkType: formData.remarkType,
remark: formData.remark || null,
startTime: formData.startTime?.format("HH:mm") || null,
endTime: formData.endTime?.format("HH:mm") || null,
};
if (isEdit && id) {
await updateContactImportTask({
...submitData,
id: Number(id),
} as Allocation);
message.success("更新成功");
} else {
await createContactImportTask(submitData as Allocation);
message.success("创建成功");
}
navigate("/workspace/contact-import/list");
} catch (error) {
message.error(isEdit ? "更新失败" : "创建失败");
} finally {
setLoading(false);
}
};
// 重置表单
const handleReset = () => {
setFormData({
name: "",
status: 1,
type: 6,
workbenchId: 1,
deviceGroups: [],
pools: [],
num: 50,
clearContact: 0,
remarkType: 0,
remark: "",
startTime: dayjs("09:00", "HH:mm"),
endTime: dayjs("21:00", "HH:mm"),
deviceGroupsOptions: [],
});
};
useEffect(() => {
if (isEdit) {
loadTaskDetail();
}
}, [id, isEdit]);
return (
<Layout
header={<NavCommon title={isEdit ? "编辑导入任务" : "新建导入任务"} />}
footer={
<div className={style.buttonGroup}>
<Button
type="primary"
size="large"
loading={loading}
onClick={handleSubmit}
className={style.submitButton}
>
{isEdit ? "更新" : "创建"}
</Button>
<Button
size="large"
onClick={handleReset}
className={style.resetButton}
>
</Button>
</div>
}
loading={loading}
>
<div className={style.formBg}>
<div className={style.basicSection}>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Input
placeholder="请输入任务名称"
value={formData.name}
onChange={e => handleUpdateFormData({ name: e.target.value })}
className={style.input}
/>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<DeviceSelection
selectedOptions={formData.deviceGroupsOptions}
onSelect={handleDeviceSelect}
placeholder="请选择设备"
className={style.deviceSelection}
/>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.stepperContainer}>
<Button
icon={<MinusOutlined />}
onClick={() =>
handleUpdateFormData({ num: Math.max(1, formData.num - 1) })
}
disabled={formData.num <= 1}
className={style.stepperButton}
/>
<Input
value={formData.num}
onChange={e => {
const value = parseInt(e.target.value) || 1;
handleUpdateFormData({
num: Math.min(1000, Math.max(1, value)),
});
}}
className={style.stepperInput}
/>
<Button
icon={<PlusOutlined />}
onClick={() =>
handleUpdateFormData({
num: Math.min(1000, formData.num + 1),
})
}
disabled={formData.num >= 1000}
className={style.stepperButton}
/>
</div>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Switch
checked={formData.clearContact === 1}
onChange={checked =>
handleUpdateFormData({ clearContact: checked ? 1 : 0 })
}
/>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Select
placeholder="请选择备注类型"
value={formData.remarkType}
onChange={value => handleUpdateFormData({ remarkType: value })}
className={style.select}
>
<Select.Option value={0}></Select.Option>
<Select.Option value={1}></Select.Option>
<Select.Option value={2}></Select.Option>
<Select.Option value={3}></Select.Option>
</Select>
<div className={style.counterTip}></div>
</div>
{formData.remarkType === 3 && (
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Input
placeholder="请输入备注内容"
value={formData.remark}
onChange={e => handleUpdateFormData({ remark: e.target.value })}
className={style.input}
/>
<div className={style.counterTip}></div>
</div>
)}
<div className={style.formItem}>
<div className={style.formLabel}></div>
<TimePicker
value={formData.startTime}
onChange={time =>
handleUpdateFormData({
startTime: time,
})
}
format="HH:mm"
placeholder="请选择开始时间"
className={style.timePicker}
/>
<div className={style.counterTip}></div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<TimePicker
value={formData.endTime}
onChange={time =>
handleUpdateFormData({
endTime: time,
})
}
format="HH:mm"
placeholder="请选择结束时间"
className={style.timePicker}
/>
<div className={style.counterTip}></div>
</div>
<div
className={style.formItem}
style={{ display: "flex", justifyContent: "space-between" }}
>
<span></span>
<Switch
checked={formData.status === 1}
onChange={check =>
handleUpdateFormData({ status: check ? 1 : 0 })
}
/>
</div>
</div>
</div>
</Layout>
);
};
export default ContactImportForm;

View File

@@ -1,97 +0,0 @@
import request from "@/api/request";
import {
ContactImportTask,
CreateContactImportTaskData,
UpdateContactImportTaskData,
ContactImportRecord,
PaginatedResponse,
ImportStats,
} from "./data";
// 获取通讯录导入任务列表
export function fetchContactImportTasks(
params = { type: 6, page: 1, limit: 10 },
) {
return request("/v1/workbench/list", params, "GET");
}
// 获取单个任务详情
export function fetchContactImportTaskDetail(
id: number,
): Promise<ContactImportTask | null> {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建通讯录导入任务
export function createContactImportTask(
data: CreateContactImportTaskData,
): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 6 }, "POST");
}
// 更新通讯录导入任务
export function updateContactImportTask(
data: UpdateContactImportTaskData,
): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 6 }, "POST");
}
// 删除通讯录导入任务
export function deleteContactImportTask(id: number): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleContactImportTask(data: {
id: number;
status: number;
}): Promise<any> {
return request("/v1/workbench/update-status", { ...data }, "POST");
}
// 复制通讯录导入任务
export function copyContactImportTask(id: number): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 获取导入记录
export function fetchImportRecords(
workbenchId: number,
page: number = 1,
limit: number = 20,
keyword?: string,
): Promise<PaginatedResponse<ContactImportRecord>> {
return request(
"/v1/workbench/import-records",
{
workbenchId,
page,
limit,
keyword,
},
"GET",
);
}
// 获取统计数据
export function fetchImportStats(): Promise<ImportStats> {
return request("/v1/workbench/import-stats", {}, "GET");
}
// 获取设备组列表
export function fetchDeviceGroups(): Promise<any[]> {
return request("/v1/device/groups", {}, "GET");
}
// 手动触发导入
export function triggerImport(taskId: number): Promise<any> {
return request("/v1/workbench/trigger-import", { taskId }, "POST");
}
// 批量操作任务
export function batchOperateTasks(data: {
taskIds: number[];
operation: "start" | "stop" | "delete";
}): Promise<any> {
return request("/v1/workbench/batch-operate", data, "POST");
}

View File

@@ -1,131 +0,0 @@
// 通讯录导入任务状态
export type ContactImportTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 设备组信息
export interface DeviceGroup {
id: string;
name: string;
deviceCount: number;
status: "online" | "offline";
lastActive: string;
}
// 通讯录导入记录
export interface ContactImportRecord {
id: string;
workbenchId: string;
wechatAccountId: string;
deviceId: string;
num: number;
clientId: string;
remarkType: string;
remarkValue: string;
startTime: string;
endTime: string;
createTime: string;
operatorName: string;
operatorAvatar: string;
deviceName: string;
importStatus: "success" | "failed" | "pending";
errorMessage?: string;
}
// 通讯录导入任务配置
export interface ContactImportTaskConfig {
id: number;
workbenchId: number;
devices: number[];
pools: number[];
num: number;
clearContact: number;
remarkType: number;
remark: string;
startTime: string;
endTime: string;
createTime: string;
}
// 通讯录导入任务
export interface ContactImportTask {
id: number;
companyId: number;
name: string;
type: number;
status: ContactImportTaskStatus;
autoStart: number;
userId: number;
createTime: string;
updateTime: string;
config: ContactImportTaskConfig;
creatorName: string;
auto_like: any;
moments_sync: any;
traffic_config: any;
group_push: any;
group_create: any;
// 计算属性,用于向后兼容
deviceGroups?: string[];
todayImportCount?: number;
totalImportCount?: number;
maxImportsPerDay?: number;
importInterval?: number;
}
// 创建通讯录导入任务数据
export interface CreateContactImportTaskData {
name: string;
type: number;
config: {
devices: number[];
pools: number[];
num: number;
clearContact: number;
remarkType: number;
remark: string;
startTime: string;
endTime: string;
};
}
// 更新通讯录导入任务数据
export interface UpdateContactImportTaskData
extends CreateContactImportTaskData {
id: number;
}
// 任务配置
export interface TaskConfig {
deviceGroups: string[];
num: number;
clientId: string;
remarkType: string;
remarkValue: string;
startTime: string;
endTime: string;
maxImportsPerDay: number;
importInterval: number;
}
// API响应
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}
// 统计数据
export interface ImportStats {
totalTasks: number;
activeTasks: number;
todayImports: number;
totalImports: number;
successRate: number;
}

View File

@@ -1,241 +0,0 @@
.container {
padding: 0 14px;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.searchBox {
flex: 1;
:global(.ant-input-affix-wrapper) {
border-radius: 6px;
border: 1px solid #d9d9d9;
&:hover {
border-color: #40a9ff;
}
&:focus-within {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
.actions {
display: flex;
gap: 8px;
:global(.adm-button) {
border-radius: 6px;
}
}
.taskList {
display: flex;
flex-direction: column;
gap: 12px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: #666;
font-size: 14px;
gap: 8px;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.emptyIcon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
.emptyText {
color: #666;
font-size: 14px;
margin-bottom: 16px;
}
.taskCard {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
:global(.adm-card-body) {
padding: 16px;
}
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.taskInfo {
flex: 1;
}
.taskName {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
line-height: 1.4;
}
.taskStatus {
font-size: 12px;
font-weight: 500;
padding: 2px 8px;
border-radius: 12px;
background-color: rgba(82, 196, 26, 0.1);
display: inline-block;
}
.cardMenu {
position: relative;
}
.menuButton {
padding: 4px 8px;
color: #666;
&:hover {
color: #1890ff;
background-color: #f0f8ff;
}
}
.menuDropdown {
position: absolute;
top: 100%;
right: 0;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 100px;
overflow: hidden;
}
.menuItem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
&:last-child {
color: #ff4d4f;
&:hover {
background-color: #fff2f0;
}
}
}
.cardContent {
margin-bottom: 16px;
}
.taskDetail {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
}
.label {
color: #666;
margin-right: 8px;
min-width: 70px;
flex-shrink: 0;
}
.value {
color: #333;
flex: 1;
word-break: break-all;
}
.cardActions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
:global(.adm-button) {
flex: 1;
border-radius: 6px;
font-size: 14px;
}
}
// 响应式设计
@media (max-width: 480px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.actions {
justify-content: space-between;
}
.taskName {
font-size: 15px;
}
.taskDetail {
font-size: 13px;
}
.label {
min-width: 60px;
}
}

View File

@@ -1,345 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Toast, SpinLoading, Dialog, Card } from "antd-mobile";
import NavCommon from "@/components/NavCommon";
import { Input } from "antd";
import {
PlusOutlined,
CopyOutlined,
DeleteOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
MoreOutlined,
ContactsOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import {
fetchContactImportTasks,
deleteContactImportTask,
toggleContactImportTask,
copyContactImportTask,
} from "./api";
import { ContactImportTask } from "./data";
import style from "./index.module.scss";
// 卡片菜单组件
interface CardMenuProps {
onView: () => void;
onEdit: () => void;
onCopy: () => void;
onDelete: () => void;
}
const CardMenu: React.FC<CardMenuProps> = ({
onView,
onEdit,
onCopy,
onDelete,
}) => {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className={style.cardMenu} ref={menuRef}>
<Button
size="small"
fill="none"
onClick={() => setOpen(!open)}
className={style.menuButton}
>
<MoreOutlined />
</Button>
{open && (
<div className={style.menuDropdown}>
<div className={style.menuItem} onClick={onView}>
<EyeOutlined />
</div>
<div className={style.menuItem} onClick={onEdit}>
<EditOutlined />
</div>
<div className={style.menuItem} onClick={onCopy}>
<CopyOutlined />
</div>
<div className={style.menuItem} onClick={onDelete}>
<DeleteOutlined />
</div>
</div>
)}
</div>
);
};
const ContactImport: React.FC = () => {
const navigate = useNavigate();
const [tasks, setTasks] = useState<ContactImportTask[]>([]);
const [loading, setLoading] = useState(false);
const [searchKeyword, setSearchKeyword] = useState("");
const [filteredTasks, setFilteredTasks] = useState<ContactImportTask[]>([]);
// 获取任务列表
const loadTasks = async () => {
setLoading(true);
try {
const response = await fetchContactImportTasks();
const data = response?.list || [];
setTasks(data);
setFilteredTasks(data);
} catch (error) {
Toast.show({
content: "获取任务列表失败",
icon: "fail",
});
} finally {
setLoading(false);
}
};
// 搜索过滤
const handleSearch = (keyword: string) => {
setSearchKeyword(keyword);
if (!keyword.trim()) {
setFilteredTasks(tasks);
} else {
const filtered = tasks.filter(
task =>
task.name.toLowerCase().includes(keyword.toLowerCase()) ||
(task.config?.remark || "")
.toLowerCase()
.includes(keyword.toLowerCase()) ||
task.creatorName.toLowerCase().includes(keyword.toLowerCase()),
);
setFilteredTasks(filtered);
}
};
// 删除任务
const handleDelete = async (id: number) => {
const result = await Dialog.confirm({
content: "确定要删除这个通讯录导入任务吗?",
});
if (result) {
try {
await deleteContactImportTask(id);
Toast.show({
content: "删除成功",
icon: "success",
});
loadTasks();
} catch (error) {
Toast.show({
content: "删除失败",
icon: "fail",
});
}
}
};
// 切换任务状态
const handleToggleStatus = async (task: ContactImportTask) => {
try {
await toggleContactImportTask({
id: task.id,
status: task.status === 1 ? 2 : 1,
});
Toast.show({
content: task.status === 1 ? "任务已暂停" : "任务已启动",
icon: "success",
});
loadTasks();
} catch (error) {
Toast.show({
content: "操作失败",
icon: "fail",
});
}
};
// 复制任务
const handleCopy = async (id: number) => {
try {
await copyContactImportTask(id);
Toast.show({
content: "复制成功",
icon: "success",
});
loadTasks();
} catch (error) {
Toast.show({
content: "复制失败",
icon: "fail",
});
}
};
// 查看详情
const handleView = (id: number) => {
navigate(`/workspace/contact-import/detail/${id}`);
};
// 编辑任务
const handleEdit = (id: number) => {
navigate(`/workspace/contact-import/form/${id}`);
};
// 格式化状态文本
const getStatusText = (status: number) => {
return status === 1 ? "运行中" : "已暂停";
};
// 格式化状态颜色
const getStatusColor = (status: number) => {
return status === 1 ? "#52c41a" : "#faad14";
};
useEffect(() => {
loadTasks();
}, []);
return (
<Layout
header={
<>
<NavCommon
backFn={() => navigate("/workspace")}
title="通讯录导入"
right={
<Button
size="small"
color="primary"
onClick={() => navigate("/workspace/contact-import/form")}
>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索计划名称"
value={searchKeyword}
onChange={e => setSearchKeyword(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={() => handleSearch(searchKeyword)}
loading={loading}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
>
<div className={style.container}>
{/* 任务列表 */}
<div className={style.taskList}>
{loading ? (
<div className={style.loading}>
<SpinLoading /> ...
</div>
) : filteredTasks.length === 0 ? (
<div className={style.empty}>
<ContactsOutlined className={style.emptyIcon} />
<div className={style.emptyText}>
{searchKeyword ? "未找到相关任务" : "暂无通讯录导入任务"}
</div>
{!searchKeyword && (
<Button
color="primary"
size="small"
onClick={() => navigate("/workspace/contact-import/form")}
>
<PlusOutlined />
</Button>
)}
</div>
) : (
filteredTasks.map(task => (
<Card key={task.id} className={style.taskCard}>
<div className={style.cardHeader}>
<div className={style.taskInfo}>
<div className={style.taskName}>{task.name}</div>
<div
className={style.taskStatus}
style={{ color: getStatusColor(task.status) }}
>
{getStatusText(task.status)}
</div>
</div>
<CardMenu
onView={() => handleView(task.id)}
onEdit={() => handleEdit(task.id)}
onCopy={() => handleCopy(task.id)}
onDelete={() => handleDelete(task.id)}
/>
</div>
<div className={style.cardContent}>
<div className={style.taskDetail}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.config?.remarkType === 1 ? "自定义备注" : "其他"}
</span>
</div>
<div className={style.taskDetail}>
<span className={style.label}>:</span>
<span className={style.value}>
{task.config?.devices?.length || 0}
</span>
</div>
<div className={style.taskDetail}>
<span className={style.label}>:</span>
<span className={style.value}>{task.config?.num || 0}</span>
</div>
<div className={style.taskDetail}>
<span className={style.label}>:</span>
<span className={style.value}>{task.createTime}</span>
</div>
</div>
<div className={style.cardActions}>
<Button
size="small"
fill="none"
onClick={() => handleToggleStatus(task)}
>
{task.status === 1 ? "暂停" : "启动"}
</Button>
<Button
size="small"
fill="none"
onClick={() => handleView(task.id)}
>
</Button>
</div>
</Card>
))
)}
</div>
</div>
</Layout>
);
};
export default ContactImport;

View File

@@ -1,74 +0,0 @@
import request from "@/api/request";
export interface GroupPushTask {
id: string;
name: string;
status: number; // 1: 运行中, 2: 已暂停
deviceCount: number;
targetGroups: string[];
pushCount: number;
successCount: number;
lastPushTime: string;
createTime: string;
creator: string;
pushInterval: number;
maxPerDay: number;
startTime: string; // 允许推送的开始时间
endTime: string; // 允许推送的结束时间
messageType: "text" | "image" | "video" | "link";
messageContent: string;
targetTags: string[];
pushMode: "immediate" | "scheduled";
scheduledTime?: string;
}
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
export async function fetchGroupPushTasks(): Promise<GroupPushTask[]> {
const response = await request("/v1/workbench/list", { type: 3 }, "GET");
if (Array.isArray(response)) return response;
if (response && Array.isArray(response.data)) return response.data;
return [];
}
export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
return request(`/v1/workspace/group-push/tasks/${id}`, {}, "DELETE");
}
export async function toggleGroupPushTask(
id: string,
status: string,
): Promise<ApiResponse> {
return request(
`/v1/workspace/group-push/tasks/${id}/toggle`,
{ status },
"POST",
);
}
export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
return request(`/v1/workspace/group-push/tasks/${id}/copy`, {}, "POST");
}
export async function createGroupPushTask(
taskData: Partial<GroupPushTask>,
): Promise<ApiResponse> {
return request("/v1/workspace/group-push/tasks", taskData, "POST");
}
export async function updateGroupPushTask(
id: string,
taskData: Partial<GroupPushTask>,
): Promise<ApiResponse> {
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
}
export async function getGroupPushTaskDetail(
id: string,
): Promise<GroupPushTask> {
return request(`/v1/workspace/group-push/tasks/${id}`);
}

View File

@@ -1,98 +0,0 @@
.searchBar {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 0 8px 0;
}
.taskList {
display: flex;
flex-direction: column;
gap: 16px;
}
.emptyCard {
text-align: center;
padding: 48px 0;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.taskCard {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 20px 16px 12px 16px;
}
.taskHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.taskTitle {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.taskActions {
display: flex;
align-items: center;
gap: 8px;
}
.taskInfoGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 16px;
font-size: 13px;
color: #666;
margin-bottom: 12px;
}
.progressBlock {
margin-bottom: 12px;
}
.progressLabel {
font-size: 13px;
color: #888;
margin-bottom: 4px;
}
.taskFooter {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px dashed #eee;
padding-top: 8px;
margin-top: 8px;
}
.expandedPanel {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #eee;
}
.expandedGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
@media (max-width: 600px) {
.taskCard {
padding: 12px 6px 8px 6px;
}
.expandedGrid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,254 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Card, Badge, Button, Progress, Spin } from "antd";
import {
ArrowLeftOutlined,
SettingOutlined,
TeamOutlined,
MessageOutlined,
CalendarOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import { getGroupPushTaskDetail, GroupPushTask } from "./groupPush";
import styles from "./index.module.scss";
const Detail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [task, setTask] = useState<GroupPushTask | null>(null);
useEffect(() => {
if (!id) return;
setLoading(true);
getGroupPushTaskDetail(id)
.then(res => {
setTask(res.data || res); // 兼容两种返回格式
})
.finally(() => setLoading(false));
}, [id]);
if (loading) {
return (
<Layout
header={
<div
style={{
background: "#fff",
padding: "0 16px",
fontWeight: 600,
fontSize: 18,
}}
>
<ArrowLeftOutlined
onClick={() => navigate(-1)}
style={{ marginRight: 12, cursor: "pointer" }}
/>
</div>
}
>
<div style={{ padding: 48, textAlign: "center" }}>
<Spin />
</div>
</Layout>
);
}
if (!task) {
return (
<Layout
header={
<div
style={{
background: "#fff",
padding: "0 16px",
fontWeight: 600,
fontSize: 18,
}}
>
<ArrowLeftOutlined
onClick={() => navigate(-1)}
style={{ marginRight: 12, cursor: "pointer" }}
/>
</div>
}
footer={<MeauMobile />}
>
<div style={{ padding: 48, textAlign: "center", color: "#888" }}>
</div>
</Layout>
);
}
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return "green";
case 2:
return "gray";
default:
return "gray";
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
const getMessageTypeText = (type: string) => {
switch (type) {
case "text":
return "文字";
case "image":
return "图片";
case "video":
return "视频";
case "link":
return "链接";
default:
return "未知";
}
};
const getSuccessRate = (pushCount: number, successCount: number) => {
if (pushCount === 0) return 0;
return Math.round((successCount / pushCount) * 100);
};
return (
<Layout
header={
<div
style={{
background: "#fff",
padding: "0 16px",
fontWeight: 600,
fontSize: 18,
}}
>
<ArrowLeftOutlined
onClick={() => navigate(-1)}
style={{ marginRight: 12, cursor: "pointer" }}
/>
</div>
}
>
<div className={styles.bg}>
<Card className={styles.taskCard}>
<div className={styles.taskHeader}>
<div className={styles.taskTitle}>
<span>{task.name}</span>
<Badge
color={getStatusColor(task.status)}
text={getStatusText(task.status)}
style={{ marginLeft: 8 }}
/>
</div>
</div>
<div className={styles.taskInfoGrid}>
<div>{task.deviceCount} </div>
<div>{task.targetGroups.length} </div>
<div>
{task.successCount}/{task.pushCount}
</div>
<div>{task.creator}</div>
</div>
<div className={styles.progressBlock}>
<div className={styles.progressLabel}></div>
<Progress
percent={getSuccessRate(task.pushCount, task.successCount)}
size="small"
/>
</div>
<div className={styles.taskFooter}>
<div>
<CalendarOutlined /> {task.lastPushTime}
</div>
<div>{task.createTime}</div>
</div>
<div className={styles.expandedPanel}>
<div className={styles.expandedGrid}>
<div>
<SettingOutlined /> <b></b>
<div>{task.pushInterval} </div>
<div>{task.maxPerDay} </div>
<div>
{task.timeRange.start} - {task.timeRange.end}
</div>
<div>
{task.pushMode === "immediate" ? "立即推送" : "定时推送"}
</div>
{task.scheduledTime && (
<div>{task.scheduledTime}</div>
)}
</div>
<div>
<TeamOutlined /> <b></b>
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{task.targetGroups.map(group => (
<Badge
key={group}
color="blue"
text={group}
style={{ background: "#f0f5ff", marginRight: 4 }}
/>
))}
</div>
</div>
<div>
<MessageOutlined /> <b></b>
<div>{getMessageTypeText(task.messageType)}</div>
<div
style={{
background: "#f5f5f5",
padding: 8,
borderRadius: 4,
marginTop: 4,
}}
>
{task.messageContent}
</div>
</div>
<div>
<CalendarOutlined /> <b></b>
<div>
{task.pushCount} / {task.maxPerDay}
</div>
<Progress
percent={Math.round((task.pushCount / task.maxPerDay) * 100)}
size="small"
/>
{task.targetTags.length > 0 && (
<div style={{ marginTop: 8 }}>
<div></div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{task.targetTags.map(tag => (
<Badge
key={tag}
color="purple"
text={tag}
style={{ background: "#f9f0ff", marginRight: 4 }}
/>
))}
</div>
</div>
)}
</div>
</div>
</div>
</Card>
</div>
</Layout>
);
};
export default Detail;

View File

@@ -1,333 +0,0 @@
import React, {
useImperativeHandle,
forwardRef,
useState,
useEffect,
} from "react";
import {
Input,
Button,
Card,
Switch,
Form,
InputNumber,
Select,
Radio,
} from "antd";
import { fetchSocialMediaList, fetchPromotionSiteList } from "../index.api";
interface BasicSettingsProps {
defaultValues?: {
name: string;
startTime: string; // 允许推送的开始时间
endTime: string; // 允许推送的结束时间
maxPerDay: number;
pushOrder: number; // 1: 按最早, 2: 按最新
isLoop: number; // 0: 否, 1: 是
pushType: number; // 0: 定时推送, 1: 立即推送
status: number; // 0: 否, 1: 是
socialMediaId?: string;
promotionSiteId?: string;
};
onNext: (values: any) => void;
onSave: (values: any) => void;
loading?: boolean;
}
export interface BasicSettingsRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
(
{
defaultValues = {
name: "",
startTime: "06:00", // 允许推送的开始时间
endTime: "23:59", // 允许推送的结束时间
maxPerDay: 20,
pushOrder: 1,
isLoop: 0, // 0: 否, 1: 是
pushType: 0, // 0: 定时推送, 1: 立即推送
status: 0, // 0: 否, 1: 是
socialMediaId: undefined,
promotionSiteId: undefined,
},
},
ref,
) => {
const [form] = Form.useForm();
const [, forceUpdate] = useState({});
const [socialMediaList, setSocialMediaList] = useState([]);
const [promotionSiteList, setPromotionSiteList] = useState([]);
const [loadingSocialMedia, setLoadingSocialMedia] = useState(false);
const [loadingPromotionSite, setLoadingPromotionSite] = useState(false);
// 确保组件初始化时能正确显示按钮状态
useEffect(() => {
forceUpdate({});
}, []);
// 组件挂载时获取社交媒体列表
useEffect(() => {
setLoadingSocialMedia(true);
fetchSocialMediaList()
.then(res => {
setSocialMediaList(res);
})
.finally(() => {
setLoadingSocialMedia(false);
});
}, []);
// 监听社交媒体选择变化
const handleSocialMediaChange = value => {
form.setFieldsValue({ socialMediaId: value });
// 清空推广站点选择
form.setFieldsValue({ promotionSiteId: undefined });
setPromotionSiteList([]);
if (value) {
setLoadingPromotionSite(true);
fetchPromotionSiteList(value)
.then(res => {
setPromotionSiteList(res);
})
.finally(() => {
setLoadingPromotionSite(false);
});
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("BasicSettings 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
const handlePushOrderChange = (value: number) => {
form.setFieldsValue({ pushOrder: value });
forceUpdate({}); // 强制组件重新渲染
};
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
initialValues={defaultValues}
onValuesChange={(changedValues, allValues) => {
// 当pushOrder值变化时强制更新组件
if ("pushOrder" in changedValues) {
forceUpdate({});
}
}}
>
{/* 任务名称 */}
<Form.Item
label="任务名称"
name="name"
rules={[
{ required: true, message: "请输入任务名称" },
{ min: 2, max: 50, message: "任务名称长度在2-50个字符之间" },
]}
>
<Input placeholder="请输入任务名称" />
</Form.Item>
{/* 推送类型 */}
<Form.Item
label="推送类型"
name="pushType"
rules={[{ required: true, message: "请选择推送类型" }]}
>
<Radio.Group>
<Radio value={0}></Radio>
<Radio value={1}></Radio>
</Radio.Group>
</Form.Item>
{/* 允许推送的时间段 - 只在定时推送时显示 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.pushType !== currentValues.pushType
}
>
{({ getFieldValue }) => {
// 只在pushType为0定时推送时显示时间段设置
return getFieldValue("pushType") === 0 ? (
<Form.Item label="允许推送的时间段">
<div
style={{ display: "flex", gap: 8, alignItems: "center" }}
>
<Form.Item
name="startTime"
noStyle
rules={[{ required: true, message: "请选择开始时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
<span style={{ color: "#888" }}></span>
<Form.Item
name="endTime"
noStyle
rules={[{ required: true, message: "请选择结束时间" }]}
>
<Input type="time" style={{ width: 120 }} />
</Form.Item>
</div>
</Form.Item>
) : null;
}}
</Form.Item>
{/* 每日推送 */}
<Form.Item
label="每日推送"
name="maxPerDay"
rules={[
{ required: true, message: "请输入每日推送数量" },
{
type: "number",
min: 1,
max: 100,
message: "每日推送数量在1-100之间",
},
]}
>
<InputNumber
min={1}
max={100}
style={{ width: 120 }}
addonAfter="条内容"
/>
</Form.Item>
{/* 推送顺序 */}
<Form.Item
label="推送顺序"
name="pushOrder"
rules={[{ required: true, message: "请选择推送顺序" }]}
>
<div style={{ display: "flex" }}>
<Button
type={
form.getFieldValue("pushOrder") == 1 ? "primary" : "default"
}
style={{ borderRadius: "6px 0 0 6px" }}
onClick={() => handlePushOrderChange(1)}
>
</Button>
<Button
type={
form.getFieldValue("pushOrder") == 2 ? "primary" : "default"
}
style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }}
onClick={() => handlePushOrderChange(2)}
>
</Button>
</div>
</Form.Item>
{/* 京东联盟 */}
<Form.Item label="京东联盟" style={{ marginBottom: 16 }}>
<div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
<Form.Item name="socialMediaId" noStyle>
<Select
placeholder="请选择社交媒体"
style={{ width: 200 }}
loading={loadingSocialMedia}
onChange={handleSocialMediaChange}
options={socialMediaList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
<Form.Item name="promotionSiteId" noStyle>
<Select
placeholder="请选择推广站点"
style={{ width: 200 }}
loading={loadingPromotionSite}
disabled={!form.getFieldValue("socialMediaId")}
options={promotionSiteList.map(item => ({
label: item.name,
value: item.id,
}))}
/>
</Form.Item>
</div>
</Form.Item>
{/* 是否循环推送 */}
<Form.Item
label="是否循环推送"
name="isLoop"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
>
<Switch />
</Form.Item>
{/* 是否启用 */}
<Form.Item
label="是否启用"
name="status"
valuePropName="checked"
getValueFromEvent={checked => (checked ? 1 : 0)}
getValueProps={value => ({ checked: value === 1 })}
>
<Switch />
</Form.Item>
{/* 推送类型提示 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.pushType !== currentValues.pushType
}
>
{({ getFieldValue }) => {
const pushType = getFieldValue("pushType");
if (pushType === 1) {
return (
<div
style={{
background: "#fffbe6",
border: "1px solid #ffe58f",
borderRadius: 4,
padding: 8,
color: "#ad8b00",
marginBottom: 16,
}}
>
</div>
);
}
return null;
}}
</Form.Item>
</Form>
</Card>
</div>
);
},
);
BasicSettings.displayName = "BasicSettings";
export default BasicSettings;

View File

@@ -1,96 +0,0 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import ContentSelection from "@/components/ContentSelection";
import { ContentItem } from "@/components/ContentSelection/data";
interface ContentSelectorProps {
selectedOptions: ContentItem[];
onPrevious: () => void;
onNext: (data: {
contentGroups: string[];
contentGroupsOptions: ContentItem[];
}) => void;
}
export interface ContentSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const ContentSelector = forwardRef<ContentSelectorRef, ContentSelectorProps>(
({ selectedOptions, onNext }, ref) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
await form.validateFields();
return true;
} catch (error) {
console.log("ContentSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
// 处理选择变化
const handleLibrariesChange = (contentGroupsOptions: ContentItem[]) => {
const contentGroups = contentGroupsOptions.map(c => c.id.toString());
onNext({
contentGroups,
contentGroupsOptions,
});
form.setFieldValue("contentGroups", contentGroups);
};
return (
<div style={{ marginBottom: 24 }}>
<Card>
<Form
form={form}
layout="vertical"
initialValues={{
contentGroups: selectedOptions.map(c => Number(c.id)),
}}
>
<div style={{ marginBottom: 16 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="contentGroups"
rules={[
{ required: true, message: "请选择至少一个内容库" },
{ type: "array", min: 1, message: "请选择至少一个内容库" },
{ type: "array", max: 20, message: "最多只能选择20个内容库" },
]}
>
<ContentSelection
selectedOptions={selectedOptions}
onSelect={handleLibrariesChange}
placeholder="选择内容库"
showInput={true}
showSelectedList={true}
readonly={false}
selectedListMaxHeight={320}
/>
</Form.Item>
</Form>
</Card>
</div>
);
},
);
ContentSelector.displayName = "ContentSelector";
export default ContentSelector;

View File

@@ -1,95 +0,0 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card } from "antd";
import GroupSelection from "@/components/GroupSelection";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
interface GroupSelectorProps {
selectedGroups: GroupSelectionItem[];
onPrevious: () => void;
onNext: (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
}) => void;
}
export interface GroupSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const GroupSelector = forwardRef<GroupSelectorRef, GroupSelectorProps>(
({ selectedGroups, onNext }, ref) => {
const [form] = Form.useForm();
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
try {
form.setFieldsValue({
wechatGroups: selectedGroups.map(item => item.id),
});
await form.validateFields();
return true;
} catch (error) {
console.log("GroupSelector 表单验证失败:", error);
return false;
}
},
getValues: () => {
return form.getFieldsValue();
},
}));
// 群组选择
const handleGroupSelect = (wechatGroupsOptions: GroupSelectionItem[]) => {
const wechatGroups = wechatGroupsOptions.map(item => item.id);
form.setFieldValue("wechatGroups", wechatGroups);
onNext({ wechatGroups, wechatGroupsOptions });
};
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{ groups: selectedGroups }}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="wechatGroups"
rules={[
{
required: true,
type: "array",
min: 1,
message: "请选择至少一个群组",
},
{ type: "array", max: 50, message: "最多只能选择50个群组" },
]}
>
<GroupSelection
selectedOptions={selectedGroups}
onSelect={handleGroupSelect}
placeholder="选择要推送的群组"
readonly={false}
showSelectedList={true}
selectedListMaxHeight={300}
/>
</Form.Item>
</Form>
</Card>
);
},
);
GroupSelector.displayName = "GroupSelector";
export default GroupSelector;

View File

@@ -1,22 +0,0 @@
import request from "@/api/request";
export function createGroupPushTask(data) {
return request("/v1/workbench/create", { ...data, type: 3 }, "POST");
}
// 获取自动点赞任务详情
export function fetchGroupPushTaskDetail(id: string) {
return request("/v1/workbench/detail", { id }, "GET");
}
export function updateGroupPushTask(data) {
return request("/v1/workbench/update", { ...data, type: 3 }, "POST");
}
// 获取京东社交媒体列表
export const fetchSocialMediaList = async () => {
return request("/v1/workbench/getJdSocialMedia", {}, "GET");
};
// 获取京东推广站点列表
export const fetchPromotionSiteList = async (id: number) => {
return request("/v1/workbench/getJdPromotionSite", { id }, "GET");
};

View File

@@ -1,37 +0,0 @@
import { ContentItem } from "@/components/ContentSelection/data";
export interface WechatGroup {
id: string;
name: string;
avatar: string;
serviceAccount: {
id: string;
name: string;
avatar: string;
};
}
export interface ContentLibrary {
id: string;
name: string;
targets: Array<{
id: string;
avatar: string;
}>;
}
export interface FormData {
name: string;
startTime: string; // 允许推送的开始时间
endTime: string; // 允许推送的结束时间
dailyPushCount: number;
pushOrder: number; // 1: 按最早, 2: 按最新
isLoop: number; // 0: 否, 1: 是
pushType: number; // 0: 定时推送, 1: 立即推送
status: number; // 0: 否, 1: 是
contentGroups: string[];
wechatGroups: string[];
// 京东联盟相关字段
socialMediaId?: string;
promotionSiteId?: string;
[key: string]: any;
}

View File

@@ -1,257 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button } from "antd";
import { Toast } from "antd-mobile";
import { createGroupPushTask, fetchGroupPushTaskDetail } from "./index.api";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
import GroupSelector, { GroupSelectorRef } from "./components/GroupSelector";
import ContentSelector, {
ContentSelectorRef,
} from "./components/ContentSelector";
import type { FormData } from "./index.data";
import NavCommon from "@/components/NavCommon";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { ContentItem } from "@/components/ContentSelection/data";
const steps = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择社群" },
{ id: 3, title: "步骤 3", subtitle: "选择内容库" },
];
const NewGroupPush: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [wechatGroupsOptions, setWechatGroupsOptions] = useState<
GroupSelectionItem[]
>([]);
const [contentGroupsOptions, setContentGroupsOptions] = useState<
ContentItem[]
>([]);
const [formData, setFormData] = useState<FormData>({
name: "",
startTime: "06:00", // 允许推送的开始时间
dailyPushCount: 0, // 每日已推送次数
endTime: "23:59", // 允许推送的结束时间
maxPerDay: 20,
pushOrder: 2, // 2: 按最新
isLoop: 0, // 0: 否, 1: 是
pushType: 0, // 0: 定时推送, 1: 立即推送
status: 0, // 0: 否, 1: 是
wechatGroups: [],
contentGroups: [],
});
const [isEditMode, setIsEditMode] = useState(false);
// 创建子组件的ref
const basicSettingsRef = useRef<BasicSettingsRef>(null);
const groupSelectorRef = useRef<GroupSelectorRef>(null);
const contentSelectorRef = useRef<ContentSelectorRef>(null);
useEffect(() => {
if (!id) return;
setIsEditMode(true);
}, [id]);
const handleBasicSettingsChange = (values: Partial<FormData>) => {
setFormData(prev => ({ ...prev, ...values }));
};
//群组选择
const handleGroupsChange = (data: {
wechatGroups: string[];
wechatGroupsOptions: GroupSelectionItem[];
}) => {
setFormData(prev => ({
...prev,
wechatGroups: data.wechatGroups,
}));
setWechatGroupsOptions(data.wechatGroupsOptions);
};
//内容库选择
const handleLibrariesChange = (data: {
contentGroups: string[];
contentGroupsOptions: ContentItem[];
}) => {
setFormData(prev => ({ ...prev, contentGroups: data.contentGroups }));
setContentGroupsOptions(data.contentGroupsOptions);
};
const handleSave = async () => {
try {
// 调用 ContentSelector 的表单校验
const isValid = (await contentSelectorRef.current?.validate()) || false;
if (!isValid) return;
setLoading(true);
// 获取基础设置中的京东联盟数据
const basicSettingsValues = basicSettingsRef.current?.getValues() || {};
// 构建 API 请求数据
const apiData = {
name: formData.name,
startTime: formData.startTime, // 允许推送的开始时间
endTime: formData.endTime, // 允许推送的结束时间
maxPerDay: formData.maxPerDay,
pushOrder: formData.pushOrder,
isLoop: formData.isLoop, // 0: 否, 1: 是
pushType: formData.pushType, // 0: 定时推送, 1: 立即推送
status: formData.status, // 0: 否, 1: 是
wechatGroups: formData.wechatGroups,
contentGroups: formData.contentGroups,
// 京东联盟数据从基础设置中获取
socialMediaId: basicSettingsValues.socialMediaId,
promotionSiteId: basicSettingsValues.promotionSiteId,
pushMode:
formData.pushType === 1
? ("immediate" as const)
: ("scheduled" as const),
messageType: "text" as const,
messageContent: "",
targetTags: [],
pushInterval: 60,
};
// 打印API请求数据用于调试
console.log("发送到API的数据:", apiData);
// 调用创建或更新 API
if (id) {
// 更新逻辑将在这里实现
Toast.show({ content: "更新成功", position: "top" });
navigate("/workspace/group-push");
} else {
createGroupPushTask(apiData)
.then(() => {
Toast.show({ content: "创建成功", position: "top" });
navigate("/workspace/group-push");
})
.catch(() => {
Toast.show({ content: "创建失败,请稍后重试", position: "top" });
});
}
} catch (error) {
Toast.show({ content: "保存失败,请稍后重试", position: "top" });
} finally {
setLoading(false);
}
};
const handlePrevious = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleNext = async () => {
if (currentStep < 3) {
try {
let isValid = false;
switch (currentStep) {
case 1:
// 调用 BasicSettings 的表单校验
isValid = (await basicSettingsRef.current?.validate()) || false;
if (isValid) {
const values = basicSettingsRef.current?.getValues();
if (values) {
handleBasicSettingsChange(values);
}
setCurrentStep(2);
}
break;
case 2:
// 调用 GroupSelector 的表单校验
isValid = (await groupSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
}
break;
default:
setCurrentStep(currentStep + 1);
}
} catch (error) {
console.log("表单验证失败:", error);
}
}
};
const renderFooter = () => {
return (
<div className="footer-btn-group">
{currentStep > 1 && (
<Button size="large" onClick={handlePrevious}>
</Button>
)}
{currentStep === 3 ? (
<Button size="large" type="primary" onClick={handleSave}>
</Button>
) : (
<Button size="large" type="primary" onClick={handleNext}>
</Button>
)}
</div>
);
};
return (
<Layout
header={<NavCommon title={isEditMode ? "编辑任务" : "新建任务"} />}
footer={renderFooter()}
>
<div style={{ padding: 12 }}>
<div style={{ marginBottom: 12 }}>
<StepIndicator currentStep={currentStep} steps={steps} />
</div>
<div>
{currentStep === 1 && (
<BasicSettings
ref={basicSettingsRef}
defaultValues={{
name: formData.name,
startTime: formData.startTime,
endTime: formData.endTime,
maxPerDay: formData.maxPerDay,
pushOrder: formData.pushOrder,
isLoop: formData.isLoop,
status: formData.status,
pushType: formData.pushType,
}}
onNext={handleBasicSettingsChange}
onSave={handleSave}
loading={loading}
/>
)}
{currentStep === 2 && (
<GroupSelector
ref={groupSelectorRef}
selectedGroups={wechatGroupsOptions}
onPrevious={() => setCurrentStep(1)}
onNext={handleGroupsChange}
/>
)}
{currentStep === 3 && (
<ContentSelector
ref={contentSelectorRef}
selectedOptions={contentGroupsOptions}
onPrevious={() => setCurrentStep(2)}
onNext={handleLibrariesChange}
/>
)}
</div>
</div>
</Layout>
);
};
export default NewGroupPush;

View File

@@ -1,44 +0,0 @@
import request from "@/api/request";
import { GroupPushTask } from "../detail/groupPush";
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
export async function fetchGroupPushTasks() {
return request("/v1/workbench/list", { type: 3 }, "GET");
}
export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleGroupPushTask(data): Promise<any> {
return request("/v1/workbench/update-status", { ...data, type: 3 }, "POST");
}
export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
return request("/v1/workbench/copy", { id }, "POST");
}
export async function createGroupPushTask(
taskData: Partial<GroupPushTask>,
): Promise<ApiResponse> {
return request("/v1/workspace/group-push/tasks", taskData, "POST");
}
export async function updateGroupPushTask(
id: string,
taskData: Partial<GroupPushTask>,
): Promise<ApiResponse> {
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
}
export async function getGroupPushTaskDetail(
id: string,
): Promise<GroupPushTask> {
return request(`/v1/workspace/group-push/tasks/${id}`);
}

View File

@@ -1,164 +0,0 @@
.nav-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.searchBar {
display: flex;
gap: 8px;
padding: 16px;
}
.refresh-btn {
// 只针对当前模块的refresh-btn按钮进行样式设置
&.ant-btn {
height: 38px !important;
width: 40px !important;
padding: 0 !important;
border-radius: 8px !important;
min-width: 40px !important;
flex-shrink: 0 !important;
}
}
.taskList {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 16px;
}
.emptyCard {
text-align: center;
padding: 48px 0;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.taskCard {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 20px 16px 12px 16px;
}
.taskHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.taskTitle {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.taskActions {
display: flex;
align-items: center;
gap: 8px;
}
.taskInfoGrid {
font-size: 13px;
color: #666;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
}
.progressBlock {
margin-bottom: 12px;
}
.progressLabel {
font-size: 13px;
color: #888;
margin-bottom: 4px;
}
.taskFooter {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px dashed #eee;
padding-top: 8px;
margin-top: 8px;
}
.expandedPanel {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #eee;
}
.expandedGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
// CardMenu 样式
.menu-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
border-radius: 4px;
color: #666;
&:hover {
background: #f5f5f5;
}
}
.menu-dropdown {
position: absolute;
right: 0;
top: 28px;
background: white;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 100;
min-width: 120px;
padding: 4px;
border: 1px solid #e5e5e5;
}
.menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 4px;
font-size: 14px;
gap: 8px;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.danger {
color: #ff4d4f;
&:hover {
background: #fff2f0;
}
}
}
@media (max-width: 600px) {
.taskCard {
padding: 12px 6px 8px 6px;
}
.expandedGrid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,282 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import {
TeamOutlined,
PlusOutlined,
SearchOutlined,
ReloadOutlined,
MoreOutlined,
ClockCircleOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined,
SendOutlined,
CarryOutOutlined,
} from "@ant-design/icons";
import { Card, Button, Input, Badge, Switch } from "antd";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import {
fetchGroupPushTasks,
deleteGroupPushTask,
toggleGroupPushTask,
copyGroupPushTask,
} from "./index.api";
import styles from "./index.module.scss";
// 卡片菜单组件
interface CardMenuProps {
onView: () => void;
onEdit: () => void;
onCopy: () => void;
onDelete: () => void;
}
const CardMenu: React.FC<CardMenuProps> = ({ onEdit, onCopy, onDelete }) => {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setOpen(v => !v)} className={styles["menu-btn"]}>
<MoreOutlined />
</button>
{open && (
<div ref={menuRef} className={styles["menu-dropdown"]}>
<div
onClick={() => {
onEdit();
setOpen(false);
}}
className={styles["menu-item"]}
>
<EditOutlined />
</div>
<div
onClick={() => {
onCopy();
setOpen(false);
}}
className={styles["menu-item"]}
>
<CopyOutlined />
</div>
<div
onClick={() => {
onDelete();
setOpen(false);
}}
className={`${styles["menu-item"]} ${styles["danger"]}`}
>
<DeleteOutlined />
</div>
</div>
)}
</div>
);
};
const GroupPush: React.FC = () => {
const navigate = useNavigate();
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [tasks, setTasks] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const fetchTasks = async () => {
setLoading(true);
try {
const result = await fetchGroupPushTasks();
setTasks(result.list);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
const handleDelete = async (taskId: string) => {
if (!window.confirm("确定要删除该任务吗?")) return;
await deleteGroupPushTask(taskId);
fetchTasks();
};
const handleEdit = (taskId: string) => {
navigate(`/workspace/group-push/edit/${taskId}`);
};
const handleView = (taskId: string) => {
navigate(`/workspace/group-push/${taskId}`);
};
const handleCopy = async (taskId: string) => {
await copyGroupPushTask(taskId);
fetchTasks();
};
const toggleTaskStatus = async (taskId: string) => {
const task = tasks.find(t => t.id === taskId);
if (!task) return;
const newStatus = task.status === 1 ? 2 : 1;
await toggleGroupPushTask({ id: taskId, status: newStatus });
fetchTasks();
};
const handleCreateNew = () => {
navigate("/workspace/group-push/new");
};
const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const getStatusColor = (status: number) => {
switch (status) {
case 1:
return "green";
case 2:
return "gray";
default:
return "gray";
}
};
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
return (
<Layout
loading={loading}
header={
<>
<NavCommon
title="群消息推送"
backFn={() => navigate("/workspace")}
right={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreateNew}
>
</Button>
}
/>
<div className={styles.searchBar}>
<Input
placeholder="搜索计划名称"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
<Button
onClick={fetchTasks}
size="large"
className={styles["refresh-btn"]}
>
<ReloadOutlined />
</Button>
</div>
</>
}
>
<div className={styles.bg}>
<div className={styles.taskList}>
{filteredTasks.length === 0 ? (
<Card className={styles.emptyCard}>
<SendOutlined
style={{ fontSize: 48, color: "#ccc", marginBottom: 12 }}
/>
<div style={{ color: "#888", fontSize: 16, marginBottom: 8 }}>
</div>
<div style={{ color: "#bbb", fontSize: 13, marginBottom: 16 }}>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreateNew}
>
</Button>
</Card>
) : (
filteredTasks.map(task => (
<Card key={task.id} className={styles.taskCard}>
<div className={styles.taskHeader}>
<div className={styles.taskTitle}>
<span>{task.name}</span>
<Badge
color={getStatusColor(task.status)}
text={getStatusText(task.status)}
style={{ marginLeft: 8 }}
/>
</div>
<div className={styles.taskActions}>
<Switch
checked={task.status === 1}
onChange={() => toggleTaskStatus(task.id)}
/>
<CardMenu
onView={() => handleView(task.id)}
onEdit={() => handleEdit(task.id)}
onCopy={() => handleCopy(task.id)}
onDelete={() => handleDelete(task.id)}
/>
</div>
</div>
<div className={styles.taskInfoGrid}>
<div>
<TeamOutlined />
{task.config?.groups?.length || 0}
</div>
<div>
<CarryOutOutlined />
{task.config?.content || 0}
</div>
</div>
<div className={styles.taskFooter}>
<div>
<ClockCircleOutlined />
{task.config?.lastPushTime || "暂无"}
</div>
<div>{task.createTime}</div>
</div>
</Card>
))
)}
</div>
</div>
</Layout>
);
};
export default GroupPush;

View File

@@ -1,14 +0,0 @@
import request from "@/api/request";
// 设备统计
export function getDeviceStats() {
return request("/v1/dashboard/device-stats", {}, "GET");
}
// 微信号统计
export function getWechatStats() {
return request("/v1/dashboard/wechat-stats", {}, "GET");
}
// 你可以根据需要继续添加其他接口
// 例如:场景获客统计、今日数据统计等

View File

@@ -1,115 +0,0 @@
.workspace {
padding: 12px;
}
.section {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-left: 4px;
}
.featuresGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.featureLink {
text-decoration: none;
color: inherit;
}
.featureCard {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
:global(.adm-card-body) {
padding: 0;
}
}
.featureIcon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.icon {
font-size: 20px;
}
.featureHeader {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.featureName {
font-size: 16px;
font-weight: 600;
color: #333;
}
.newBadge {
margin-left: 8px;
:global(.adm-badge-content) {
background-color: var(--primary-color);
color: #fff;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
}
}
.featureDescription {
font-size: 12px;
color: #666;
line-height: 1.4;
}
// 响应式设计
@media (max-width: 375px) {
.workspace {
padding: 12px;
}
.statsGrid {
gap: 8px;
}
.featuresGrid {
gap: 8px;
}
.featureCard {
padding: 10px;
}
.featureIcon {
width: 36px;
height: 36px;
}
.icon {
font-size: 18px;
}
}

View File

@@ -1,160 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
import { Card, Badge } from "antd-mobile";
import {
LikeOutlined,
SendOutlined,
TeamOutlined,
LinkOutlined,
ClockCircleOutlined,
ContactsOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import styles from "./index.module.scss";
import NavCommon from "@/components/NavCommon";
const Workspace: React.FC = () => {
// 常用功能
const commonFeatures = [
{
id: "auto-like",
name: "自动点赞",
description: "智能自动点赞互动",
icon: (
<LikeOutlined className={styles.icon} style={{ color: "#ff4d4f" }} />
),
path: "/workspace/auto-like",
bgColor: "#fff2f0",
isNew: true,
},
{
id: "moments-sync",
name: "朋友圈同步",
description: "自动同步朋友圈内容",
icon: (
<ClockCircleOutlined
className={styles.icon}
style={{ color: "#722ed1" }}
/>
),
path: "/workspace/moments-sync",
bgColor: "#f9f0ff",
},
{
id: "group-push",
name: "群消息推送",
description: "智能群发助手",
icon: (
<SendOutlined className={styles.icon} style={{ color: "#fa8c16" }} />
),
path: "/workspace/group-push",
bgColor: "#fff7e6",
},
{
id: "auto-group",
name: "自动建群",
description: "智能拉好友建群",
icon: (
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
),
path: "/workspace/auto-group",
bgColor: "#f6ffed",
},
{
id: "traffic-distribution",
name: "流量分发",
description: "管理流量分发和分配",
icon: (
<LinkOutlined className={styles.icon} style={{ color: "#1890ff" }} />
),
path: "/workspace/traffic-distribution",
bgColor: "#e6f7ff",
},
{
id: "contact-import",
name: "通讯录导入",
description: "批量导入通讯录联系人",
icon: (
<ContactsOutlined className={styles.icon} style={{ color: "#722ed1" }} />
),
path: "/workspace/contact-import/list",
bgColor: "#f9f0ff",
isNew: true,
},
];
return (
<Layout
header={<NavCommon left={<></>} title="工作台" />}
footer={<MeauMobile activeKey="workspace" />}
>
<div className={styles.workspace}>
{/* 常用功能 */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}></h2>
<div className={styles.featuresGrid}>
{commonFeatures.map(feature => (
<Link
to={feature.path}
key={feature.id}
className={styles.featureLink}
>
<Card className={styles.featureCard}>
<div
className={styles.featureIcon}
style={{ backgroundColor: feature.bgColor }}
>
{feature.icon}
</div>
<div className={styles.featureHeader}>
<div className={styles.featureName}>{feature.name}</div>
{feature.isNew && (
<Badge content="New" className={styles.newBadge} />
)}
</div>
<div className={styles.featureDescription}>
{feature.description}
</div>
</Card>
</Link>
))}
</div>
</div>
{/* AI智能助手 */}
{/* <div className={styles.section}>
<h2 className={styles.sectionTitle}>AI 智能助手</h2>
<div className={styles.featuresGrid}>
{aiFeatures.map(feature => (
<Link
to={feature.path}
key={feature.id}
className={styles.featureLink}
>
<Card className={styles.featureCard}>
<div
className={styles.featureIcon}
style={{ backgroundColor: feature.bgColor }}
>
{feature.icon}
</div>
<div className={styles.featureHeader}>
<div className={styles.featureName}>{feature.name}</div>
{feature.isNew && (
<Badge content="New" className={styles.newBadge} />
)}
</div>
<div className={styles.featureDescription}>
{feature.description}
</div>
</Card>
</Link>
))}
</div>
</div> */}
</div>
</Layout>
);
};
export default Workspace;

View File

@@ -1,198 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Button, Switch, message, Spin } from "antd";
import NavCommon from "@/components/NavCommon";
import { EditOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import request from "@/api/request";
interface MomentsSyncTask {
id: string;
name: string;
status: 1 | 2;
deviceCount: number;
syncCount: number;
lastSyncTime: string;
createTime: string;
creatorName: string;
updateTime?: string;
maxSyncPerDay?: number;
syncInterval?: number;
timeRange?: { start: string; end: string };
contentTypes?: string[];
targetTags?: string[];
todaySyncCount?: number;
totalSyncCount?: number;
syncMode?: string;
config?: {
devices?: string[];
contentLibraryNames?: string[];
syncCount?: number;
};
}
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
const MomentsSyncDetail: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const [task, setTask] = useState<MomentsSyncTask | null>(null);
const [loading, setLoading] = useState(false);
const fetchTaskDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await request("/v1/workbench/detail", { id }, "GET");
if (res) setTask(res);
} catch {
message.error("获取任务详情失败");
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
if (id) fetchTaskDetail();
}, [id, fetchTaskDetail]);
const handleToggleStatus = async () => {
if (!task || !id) return;
try {
const newStatus = task.status === 1 ? 2 : 1;
await request(
"/v1/workbench/update-status",
{ id, status: newStatus },
"POST",
);
setTask({ ...task, status: newStatus });
message.success(newStatus === 1 ? "任务已开启" : "任务已暂停");
} catch {
message.error("操作失败");
}
};
const handleEdit = () => {
if (id) navigate(`/workspace/moments-sync/edit/${id}`);
};
if (loading) {
return (
<Layout>
<div className={style.detailLoading}>
<Spin size="large" />
</div>
</Layout>
);
}
if (!task) {
return (
<Layout>
<div className={style.detailLoading}>
<div></div>
<Button onClick={() => navigate("/workspace/moments-sync")}>
</Button>
</div>
</Layout>
);
}
return (
<Layout
header={
<NavCommon
title="查看朋友圈同步任务"
right={
<Button
icon={<EditOutlined />}
onClick={handleEdit}
className={style.editBtn}
type="primary"
>
</Button>
}
/>
}
>
<div className={style.detailBg}>
<div className={style.detailCard}>
<div className={style.detailTop}>
<div className={style.detailTitle}>{task.name}</div>
<span
className={
task.status === 1
? style.statusPill + " " + style.statusActive
: style.statusPill + " " + style.statusPaused
}
>
{getStatusText(task.status)}
</span>
<Switch
checked={task.status === 1}
onChange={handleToggleStatus}
className={style.switchBtn}
size="small"
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: 16,
}}
>
<div>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ fontSize: 14, color: "#222", marginBottom: 4 }}>
{task.config?.devices?.length || 0}
</div>
<div style={{ fontSize: 14, color: "#222", marginBottom: 4 }}>
{task.config?.contentLibraryNames?.join("") || "-"}
</div>
<div style={{ fontSize: 14, color: "#222", marginBottom: 4 }}>
{task.syncCount || 0}
</div>
<div style={{ fontSize: 14, color: "#222" }}>
{task.creatorName}
</div>
</div>
<div>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ fontSize: 14, color: "#222", marginBottom: 4 }}>
{task.createTime}
</div>
<div style={{ fontSize: 14, color: "#222" }}>
{task.lastSyncTime || "无"}
</div>
</div>
</div>
<div
style={{
borderTop: "1px solid #f0f0f0",
margin: "16px 0 0 0",
paddingTop: 12,
}}
>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ color: "#888", fontSize: 14 }}></div>
</div>
</div>
</div>
</Layout>
);
};
export default MomentsSyncDetail;

View File

@@ -1,356 +0,0 @@
.title {
font-size: 18px;
font-weight: bold;
color: #188eee;
}
.backBtn {
border: none;
background: none;
box-shadow: none;
color: #666;
font-size: 18px;
margin-right: 8px;
}
.addBtn {
margin-left: 8px;
}
.taskList {
padding: 0 16px;
}
.taskCard {
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
padding: 16px;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 16px rgba(24, 142, 238, 0.1);
}
}
.taskCardTop {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.taskName {
font-size: 16px;
font-weight: 500;
color: #222;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.switchBtn {
margin-left: 8px;
}
.actionBtn {
margin-left: 4px;
}
.taskCardInfo {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
font-size: 13px;
color: #666;
margin-top: 8px;
}
.emptyBox {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 40px 0 32px 0;
text-align: center;
color: #bbb;
margin-top: 40px;
}
.emptyText {
font-size: 16px;
color: #888;
margin: 16px 0 20px 0;
}
.itemCard {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
padding: 16px 16px 12px 16px;
transition: box-shadow 0.2s;
position: relative;
&:hover {
box-shadow: 0 4px 16px rgba(24, 142, 238, 0.1);
}
}
.itemTop {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.itemTitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 500;
color: #222;
}
.itemName {
font-size: 15px;
font-weight: 500;
color: #222;
margin-right: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.statusBadge {
margin-left: 2px;
.ant-badge-status-dot {
width: 8px;
height: 8px;
}
.ant-badge-status-success {
background: #19c37d;
}
.ant-badge-status-default {
background: #bdbdbd;
}
.ant-badge-status-text {
font-size: 12px;
font-weight: 400;
padding: 0 6px;
border-radius: 8px;
background: #f5f5f5;
color: #222;
}
}
.itemActions {
display: flex;
align-items: center;
gap: 6px;
}
.switchBtn {
margin-right: 2px;
}
.moreBtn {
margin-left: 2px;
color: #888;
font-size: 18px;
background: none;
border: none;
box-shadow: none;
}
.itemInfoRow {
display: flex;
font-size: 13px;
color: #666;
margin-bottom: 2px;
}
.infoCol {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itemBottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px solid #f3f3f3;
margin-top: 8px;
padding-top: 6px;
}
.bottomLeft {
display: flex;
align-items: center;
gap: 4px;
}
.clockIcon {
color: #bdbdbd;
font-size: 14px;
margin-right: 2px;
}
.bottomRight {
text-align: right;
}
// 覆盖Antd Dropdown菜单样式
.ant-dropdown-menu {
border-radius: 10px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
min-width: 110px !important;
padding: 6px 0 !important;
}
.ant-dropdown-menu-item {
font-size: 14px !important;
padding: 7px 16px !important;
border-radius: 6px !important;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
}
.ant-dropdown-menu-item:hover {
background: #f5f5f5 !important;
}
.ant-dropdown-menu-item-danger {
color: #e53e3e !important;
}
.detailBg {
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.detailCard {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 20px 20px 12px 20px;
width: 100%;
max-width: 480px;
margin-bottom: 24px;
}
.detailTop {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.detailTitle {
font-size: 18px;
font-weight: 600;
color: #222;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusBadge {
margin-left: 2px;
.ant-badge-status-dot {
width: 8px;
height: 8px;
}
.ant-badge-status-success {
background: #19c37d;
}
.ant-badge-status-default {
background: #bdbdbd;
}
.ant-badge-status-text {
font-size: 12px;
font-weight: 400;
padding: 0 6px;
border-radius: 8px;
background: #f5f5f5;
color: #222;
}
}
.switchBtn {
margin-left: 8px;
}
.detailInfoRow {
display: flex;
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.infoCol {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.detailBottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #888;
border-top: 1px solid #f3f3f3;
margin-top: 10px;
padding-top: 6px;
}
.bottomLeft {
display: flex;
align-items: center;
gap: 4px;
}
.clockIcon {
color: #bdbdbd;
font-size: 14px;
margin-right: 2px;
}
.bottomRight {
text-align: right;
}
.detailLoading {
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-size: 16px;
gap: 16px;
}
.statusPill {
display: inline-block;
min-width: 48px;
height: 20px;
line-height: 20px;
font-size: 10px;
border-radius: 12px;
text-align: center;
margin-left: 6px;
box-sizing: border-box;
}
.statusActive {
background: #19c37d;
color: #fff;
}
.statusPaused {
background: #e5e7eb;
color: #888;
}

View File

@@ -1,293 +0,0 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Switch, Input, message, Dropdown, Menu } from "antd";
import { Button } from "antd-mobile";
import NavCommon from "@/components/NavCommon";
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined,
MoreOutlined,
ClockCircleOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import request from "@/api/request";
interface MomentsSyncTask {
id: string;
name: string;
status: 1 | 2;
deviceCount: number;
syncCount: number;
lastSyncTime: string;
createTime: string;
creatorName: string;
contentLib?: string;
config?: {
devices?: string[];
contentGroups: number[];
contentGroupsOptions?: {
id: number;
name: string;
[key: string]: any;
}[];
};
}
const getStatusText = (status: number) => {
switch (status) {
case 1:
return "进行中";
case 2:
return "已暂停";
default:
return "未知";
}
};
const MomentsSync: React.FC = () => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [tasks, setTasks] = useState<MomentsSyncTask[]>([]);
const fetchTasks = async () => {
setLoading(true);
try {
const res = await request(
"/v1/workbench/list",
{ type: 2, page: 1, limit: 100 },
"GET",
);
setTasks(res.list || []);
} catch (e) {
message.error("获取任务失败");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
const handleDelete = async (id: string) => {
if (!window.confirm("确定要删除该任务吗?")) return;
try {
await request("/v1/workbench/delete", { id }, "DELETE");
message.success("删除成功");
fetchTasks();
} catch {
message.error("删除失败");
}
};
const handleCopy = async (id: string) => {
try {
await request("/v1/workbench/copy", { id }, "POST");
message.success("复制成功");
fetchTasks();
} catch {
message.error("复制失败");
}
};
const handleToggle = async (id: string, status: number) => {
const newStatus = status === 1 ? 2 : 1;
try {
await request(
"/v1/workbench/update-status",
{ id, status: newStatus },
"POST",
);
setTasks(prev =>
prev.map(t => (t.id === id ? { ...t, status: newStatus } : t)),
);
message.success("操作成功");
} catch {
message.error("操作失败");
}
};
const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
// 菜单
const getMenu = (task: MomentsSyncTask) => (
<Menu>
<Menu.Item
key="view"
icon={<EyeOutlined />}
onClick={() => navigate(`/workspace/moments-sync/record/${task.id}`)}
>
</Menu.Item>
<Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={() => navigate(`/workspace/moments-sync/edit/${task.id}`)}
>
</Menu.Item>
<Menu.Item
key="copy"
icon={<CopyOutlined />}
onClick={() => handleCopy(task.id)}
>
</Menu.Item>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={() => handleDelete(task.id)}
danger
>
</Menu.Item>
</Menu>
);
return (
<Layout
header={
<>
<NavCommon
title="朋友圈同步"
right={
<Button
size="small"
color="primary"
onClick={() => navigate("/workspace/moments-sync/new")}
>
<PlusOutlined />
</Button>
}
/>
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索任务名称"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
size="small"
onClick={fetchTasks}
loading={loading}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
loading={loading}
>
<div className={style.pageBg}>
<div className={style.taskList}>
{filteredTasks.length === 0 ? (
<div className={style.emptyBox}>
<span style={{ fontSize: 40, color: "#ddd" }}>
<ClockCircleOutlined />
</span>
<div className={style.emptyText}></div>
<Button
color="primary"
onClick={() => navigate("/workspace/moments-sync/new")}
>
</Button>
</div>
) : (
filteredTasks.map(task => (
<div key={task.id} className={style.itemCard}>
<div className={style.itemTop}>
<div className={style.itemTitle}>
<span className={style.itemName}>{task.name}</span>
<span
className={
task.status === 1
? style.statusPill + " " + style.statusActive
: style.statusPill + " " + style.statusPaused
}
>
{getStatusText(task.status)}
</span>
</div>
<div className={style.itemActions}>
<Switch
checked={task.status === 1}
onChange={() => handleToggle(task.id, task.status)}
className={style.switchBtn}
size="small"
/>
<Dropdown
overlay={getMenu(task)}
trigger={["click"]}
placement="bottomRight"
>
<button
className={style.moreBtn}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
tabIndex={0}
aria-label="更多操作"
>
<MoreOutlined />
</button>
</Dropdown>
</div>
</div>
<div className={style.itemInfoRow}>
<div className={style.infoCol}>
{task.config?.devices?.length || 0}
</div>
<div className={style.infoCol}>
{task.syncCount || 0}
</div>
</div>
<div className={style.itemInfoRow}>
<div className={style.infoCol}>
{task.config?.contentGroupsOptions
?.map(c => c.name)
.join(",") || "默认内容库"}
</div>
<div className={style.infoCol}>
{task.creatorName}
</div>
</div>
<div className={style.itemBottom}>
<div className={style.bottomLeft}>
<ClockCircleOutlined className={style.clockIcon} />
{task.lastSyncTime || "无"}
</div>
<div className={style.bottomRight}>
{task.createTime}
</div>
</div>
</div>
))
)}
</div>
</div>
</Layout>
);
};
export default MomentsSync;

View File

@@ -1,13 +0,0 @@
import request from "@/api/request";
// 创建朋友圈同步任务
export const createMomentsSync = (params: any) =>
request("/v1/workbench/create", params, "POST");
// 更新朋友圈同步任务
export const updateMomentsSync = (params: any) =>
request("/v1/workbench/update", params, "POST");
// 获取朋友圈同步任务详情
export const getMomentsSyncDetail = (id: string) =>
request("/v1/workbench/detail", { id }, "GET");

View File

@@ -1,249 +0,0 @@
.formBg {
padding: 16px;
}
.formStepBtnRow {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
padding: 12px;
}
.formSteps {
display: flex;
justify-content: center;
margin-bottom: 32px;
gap: 32px;
}
.formStepIndicator {
display: flex;
flex-direction: column;
align-items: center;
color: #bbb;
font-size: 13px;
font-weight: 400;
transition: color 0.2s;
}
.formStepActive {
color: #188eee;
font-weight: 600;
}
.formStepDone {
color: #19c37d;
}
.formStepNum {
width: 28px;
height: 28px;
border-radius: 50%;
background: #e5e7eb;
color: #888;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
margin-bottom: 4px;
}
.formStepActive .formStepNum {
background: #188eee;
color: #fff;
}
.formStepDone .formStepNum {
background: #19c37d;
color: #fff;
}
.formStep {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 32px 24px 24px 24px;
width: 100%;
max-width: 420px;
margin: 0 auto 24px auto;
}
.formItem {
margin-bottom: 24px;
}
.formLabel {
font-size: 15px;
color: #222;
font-weight: 500;
margin-bottom: 10px;
}
.input {
height: 44px;
border-radius: 8px;
font-size: 15px;
}
.timeRow {
display: flex;
align-items: center;
}
.inputTime {
width: 100px;
height: 40px;
border-radius: 8px;
font-size: 15px;
}
.timeTo {
margin: 0 8px;
color: #888;
}
.counterRow {
display: flex;
align-items: center;
gap: 0;
}
.counterBtn {
width: 40px;
height: 40px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
font-size: 20px;
color: #188eee;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border 0.2s;
}
.counterBtn:hover {
border: 1px solid #188eee;
}
.counterValue {
width: 48px;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #222;
}
.counterUnit {
margin-left: 8px;
color: #888;
font-size: 14px;
}
.accountTypeRow {
display: flex;
gap: 12px;
}
.accountTypeBtn {
flex: 1;
height: 44px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
font-size: 15px;
color: #666;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.accountTypeBtn:hover {
border: 1px solid #188eee;
}
.accountTypeActive {
background: #f0f8ff;
border: 1px solid #188eee;
color: #188eee;
}
.questionIcon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: #999;
}
.switchRow {
display: flex;
align-items: center;
justify-content: space-between;
}
.switchLabel {
font-size: 15px;
color: #222;
font-weight: 500;
}
.switch {
margin-top: 0;
}
.searchInput {
height: 44px;
border-radius: 8px;
font-size: 15px;
}
.searchIcon {
color: #999;
font-size: 16px;
}
.selectedTip {
font-size: 13px;
color: #888;
margin-top: 8px;
}
.formStepBtnRow {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
}
.prevBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.nextBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.completeBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
min-width: 100px;
}
.formLoading {
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,372 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button, Input, Switch, message, Spin } from "antd";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import StepIndicator from "@/components/StepIndicator";
import {
createMomentsSync,
updateMomentsSync,
getMomentsSyncDetail,
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import ContentSelection from "@/components/ContentSelection";
import NavCommon from "@/components/NavCommon";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { ContentItem } from "@/components/ContentSelection/data";
const steps = [
{ id: 1, title: "基础设置", subtitle: "基础设置" },
{ id: 2, title: "设备选择", subtitle: "设备选择" },
{ id: 3, title: "内容库选择", subtitle: "内容库选择" },
];
const defaultForm = {
taskName: "",
startTime: "06:00",
endTime: "23:59",
syncCount: 5,
syncInterval: 30,
syncType: 1, // 1=业务号 2=人设号
accountType: "business" as "business" | "personal", // 仅UI用
enabled: true,
deviceGroups: [] as any[],
contentGroups: [] as any[], // 存完整内容库对象数组
contentTypes: ["text", "image", "video"],
targetTags: [] as string[],
filterKeywords: [] as string[],
};
const NewMomentsSync: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEditMode = !!id;
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({ ...defaultForm });
const [deviceGroupsOptions, setSelectedDevicesOptions] = useState<
DeviceSelectionItem[]
>([]);
const [contentGroupsOptions, setContentGroupsOptions] = useState<
ContentItem[]
>([]);
// 获取详情(编辑)
const fetchDetail = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const res = await getMomentsSyncDetail(id);
if (res) {
setFormData({
taskName: res.name,
startTime: res.timeRange?.start || "06:00",
endTime: res.timeRange?.end || "23:59",
syncCount: res.config?.syncCount || res.syncCount || 5,
syncInterval: res.config?.syncInterval || res.syncInterval || 30,
syncType: res.accountType === 1 ? 1 : 2,
accountType: res.accountType === 1 ? "business" : "personal",
enabled: res.status === 1,
deviceGroups: res.config?.deviceGroups || [],
// 关键用id字符串数组回填
contentGroups: res.config?.contentGroups || [], // 直接用对象数组
contentTypes: res.config?.contentTypes || ["text", "image", "video"],
targetTags: res.config?.targetTags || [],
filterKeywords: res.config?.filterKeywords || [],
});
setSelectedDevicesOptions(res.config?.deviceGroupsOptions || []);
setContentGroupsOptions(res.config?.contentGroupsOptions || []);
}
} catch {
message.error("获取详情失败");
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
if (isEditMode) fetchDetail();
}, [isEditMode, fetchDetail]);
// 步骤切换
const next = () => setCurrentStep(s => Math.min(s + 1, steps.length - 1));
const prev = () => setCurrentStep(s => Math.max(s - 1, 0));
// 表单数据更新
const updateForm = (data: Partial<typeof formData>) => {
setFormData(prev => ({ ...prev, ...data }));
};
// UI选择账号类型时同步syncType和accountType
const handleAccountTypeChange = (type: "business" | "personal") => {
setFormData(prev => ({
...prev,
accountType: type,
syncType: type === "business" ? 1 : 2,
}));
};
const handleDevicesChange = (devices: DeviceSelectionItem[]) => {
setSelectedDevicesOptions(devices);
updateForm({ deviceGroups: devices.map(d => d.id) });
};
const handleContentChange = (libs: ContentItem[]) => {
setContentGroupsOptions(libs);
updateForm({ contentGroups: libs });
};
// 提交
const handleSubmit = async () => {
if (!formData.taskName.trim()) {
message.error("请输入任务名称");
return;
}
if (formData.deviceGroups.length === 0) {
message.error("请选择设备");
return;
}
if (formData.contentGroups.length === 0) {
message.error("请选择内容库");
return;
}
setLoading(true);
try {
const params = {
name: formData.taskName,
deviceGroups: formData.deviceGroups,
contentGroups: formData.contentGroups.map((lib: any) => lib.id),
syncInterval: formData.syncInterval,
syncCount: formData.syncCount,
syncType: formData.syncType, // 账号类型真实传参
accountType: formData.accountType === "business" ? 1 : 2, // 也要传
startTime: formData.startTime,
endTime: formData.endTime,
contentTypes: formData.contentTypes,
targetTags: formData.targetTags,
filterKeywords: formData.filterKeywords,
type: 2,
status: formData.enabled ? 1 : 0,
};
if (isEditMode && id) {
await updateMomentsSync({ id, ...params });
message.success("更新成功");
navigate(`/workspace/moments-sync`);
} else {
await createMomentsSync(params);
message.success("创建成功");
navigate("/workspace/moments-sync");
}
} catch {
message.error(isEditMode ? "更新失败" : "创建失败");
} finally {
setLoading(false);
}
};
// 步骤内容(去除按钮)
const renderStep = () => {
if (currentStep === 0) {
return (
<div className={style.formStep}>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<Input
value={formData.taskName}
onChange={e => updateForm({ taskName: e.target.value })}
placeholder="请输入任务名称"
maxLength={30}
className={style.input}
/>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.timeRow}>
<Input
type="time"
value={formData.startTime}
onChange={e => updateForm({ startTime: e.target.value })}
className={style.inputTime}
/>
<span className={style.timeTo}></span>
<Input
type="time"
value={formData.endTime}
onChange={e => updateForm({ endTime: e.target.value })}
className={style.inputTime}
/>
</div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.counterRow}>
<button
className={style.counterBtn}
onClick={() =>
updateForm({ syncCount: Math.max(1, formData.syncCount - 1) })
}
>
<MinusOutlined />
</button>
<span className={style.counterValue}>{formData.syncCount}</span>
<button
className={style.counterBtn}
onClick={() =>
updateForm({ syncCount: formData.syncCount + 1 })
}
>
<PlusOutlined />
</button>
<span className={style.counterUnit}></span>
</div>
</div>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<div className={style.accountTypeRow}>
<button
className={`${style.accountTypeBtn} ${formData.accountType === "business" ? style.accountTypeActive : ""}`}
onClick={() => handleAccountTypeChange("business")}
>
</button>
<button
className={`${style.accountTypeBtn} ${formData.accountType === "personal" ? style.accountTypeActive : ""}`}
onClick={() => handleAccountTypeChange("personal")}
>
</button>
</div>
</div>
<div className={style.formItem}>
<div className={style.switchRow}>
<span className={style.switchLabel}></span>
<Switch
checked={formData.enabled}
onChange={checked => updateForm({ enabled: checked })}
className={style.switch}
/>
</div>
</div>
</div>
);
}
if (currentStep === 1) {
return (
<div className={style.formStep}>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<DeviceSelection
selectedOptions={deviceGroupsOptions}
onSelect={handleDevicesChange}
placeholder="请选择设备"
showSelectedList={true}
selectedListMaxHeight={200}
/>
</div>
</div>
);
}
if (currentStep === 2) {
return (
<div className={style.formStep}>
<div className={style.formItem}>
<div className={style.formLabel}></div>
<ContentSelection
selectedOptions={contentGroupsOptions}
onSelect={handleContentChange}
placeholder="请选择内容库"
showSelectedList={true}
selectedListMaxHeight={200}
/>
{formData.contentGroups.length > 0 && (
<div className={style.selectedTip}>
: {formData.contentGroups.length}
</div>
)}
</div>
</div>
);
}
return null;
};
// 统一底部按钮
const renderFooter = () => {
if (loading) return null;
if (currentStep === 0) {
return (
<div className={style.formStepBtnRow}>
<Button
type="primary"
disabled={!formData.taskName.trim()}
onClick={next}
className={style.nextBtn}
block
>
</Button>
</div>
);
}
if (currentStep === 1) {
return (
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn} block>
</Button>
<Button type="primary" onClick={next} className={style.nextBtn} block>
</Button>
</div>
);
}
if (currentStep === 2) {
return (
<div className={style.formStepBtnRow}>
<Button onClick={prev} className={style.prevBtn} block>
</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={loading}
className={style.completeBtn}
block
>
</Button>
</div>
);
}
return null;
};
return (
<Layout
header={
<NavCommon title={isEditMode ? "编辑朋友圈同步" : "新建朋友圈同步"} />
}
footer={renderFooter()}
>
<div className={style.formBg}>
<div style={{ marginBottom: "15px" }}>
<StepIndicator currentStep={currentStep + 1} steps={steps} />
</div>
{loading ? (
<div className={style.formLoading}>
<Spin />
</div>
) : (
renderStep()
)}
</div>
</Layout>
);
};
export default NewMomentsSync;

View File

@@ -1,63 +0,0 @@
import request from "@/api/request";
import {
LikeTask,
CreateLikeTaskData,
UpdateLikeTaskData,
LikeRecord,
PaginatedResponse,
} from "@/pages/workspace/auto-like/record/data";
// 获取自动点赞任务列表
export function fetchAutoLikeTasks(
params = { type: 1, page: 1, limit: 100 },
): Promise<LikeTask[]> {
return request("/v1/workbench/list", params, "GET");
}
// 获取单个任务详情
export function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
return request("/v1/workbench/detail", { id }, "GET");
}
// 创建自动点赞任务
export function createAutoLikeTask(data: CreateLikeTaskData): Promise<any> {
return request("/v1/workbench/create", { ...data, type: 1 }, "POST");
}
// 更新自动点赞任务
export function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 1 }, "POST");
}
// 删除自动点赞任务
export function deleteAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/delete", { id }, "DELETE");
}
// 切换任务状态
export function toggleAutoLikeTask(id: string, status: string): Promise<any> {
return request("/v1/workbench/update-status", { id, status }, "POST");
}
// 复制自动点赞任务
export function copyAutoLikeTask(id: string): Promise<any> {
return request("/v1/workbench/copy", { id }, "POST");
}
// 获取点赞记录
export function fetchLikeRecords(
workbenchId: string,
page: number = 1,
limit: number = 20,
keyword?: string,
): Promise<PaginatedResponse<LikeRecord>> {
const params: any = {
workbenchId,
page: page.toString(),
limit: limit.toString(),
};
if (keyword) {
params.keyword = keyword;
}
return request("/v1/workbench/moments-records", params, "GET");
}

View File

@@ -1,119 +0,0 @@
// 自动点赞任务状态
export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭
// 内容类型
export type ContentType = "text" | "image" | "video" | "link";
// 设备信息
export interface Device {
id: string;
name: string;
status: "online" | "offline";
lastActive: string;
}
// 好友信息
export interface Friend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
tags: string[];
region: string;
source: string;
}
// 点赞记录
export interface LikeRecord {
id: string;
workbenchId: string;
momentsId: string;
snsId: string;
wechatAccountId: string;
wechatFriendId: string;
likeTime: string;
content: string;
resUrls: string[];
momentTime: string;
userName: string;
operatorName: string;
operatorAvatar: string;
friendName: string;
friendAvatar: string;
}
// 自动点赞任务
export interface LikeTask {
id: string;
name: string;
status: LikeTaskStatus;
deviceCount: number;
targetGroup: string;
likeCount: number;
lastLikeTime: string;
createTime: string;
creator: string;
likeInterval: number;
maxLikesPerDay: number;
timeRange: { start: string; end: string };
contentTypes: ContentType[];
targetTags: string[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
todayLikeCount: number;
totalLikeCount: number;
updateTime: string;
}
// 创建任务数据
export interface CreateLikeTaskData {
name: string;
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends?: string[];
friendMaxLikes: number;
friendTags?: string;
enableFriendTags: boolean;
targetTags: string[];
}
// 更新任务数据
export interface UpdateLikeTaskData extends CreateLikeTaskData {
id: string;
}
// 任务配置
export interface TaskConfig {
interval: number;
maxLikes: number;
startTime: string;
endTime: string;
contentTypes: ContentType[];
devices: string[];
friends: string[];
friendMaxLikes: number;
friendTags: string;
enableFriendTags: boolean;
}
// API响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 分页响应类型
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
limit: number;
}

View File

@@ -1,278 +0,0 @@
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import {
Button,
Input,
Card,
Badge,
Avatar,
Skeleton,
message,
Spin,
Divider,
Pagination,
} from "antd";
import {
LikeOutlined,
ReloadOutlined,
SearchOutlined,
UserOutlined,
} from "@ant-design/icons";
import styles from "./record.module.scss";
import NavCommon from "@/components/NavCommon";
import { fetchLikeRecords } from "./api";
import Layout from "@/components/Layout/Layout";
// 格式化日期
const formatDate = (timestamp: number) => {
timestamp = timestamp * 1000;
try {
const date = new Date(timestamp);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch (error) {
return timestamp;
}
};
export default function AutoLikeRecord() {
const { id } = useParams<{ id: string }>();
const [records, setRecords] = useState<any[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 10;
useEffect(() => {
if (!id) return;
setRecordsLoading(true);
fetchLikeRecords(id, 1, pageSize)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
message.error("获取发表记录失败,请稍后重试");
})
.finally(() => setRecordsLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleSearch = () => {
setCurrentPage(1);
fetchLikeRecords(id!, 1, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(1);
})
.catch(() => {
message.error("获取发表记录失败,请稍后重试");
});
};
const handleRefresh = () => {
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
})
.catch(() => {
message.error("获取发表记录失败,请稍后重试");
});
};
const handlePageChange = (newPage: number) => {
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
.then((response: any) => {
setRecords(response.list || []);
setTotal(response.total || 0);
setCurrentPage(newPage);
})
.catch(() => {
message.error("获取发表记录失败,请稍后重试");
});
};
return (
<Layout
header={
<>
<NavCommon title="发表记录" />
<div className={styles.headerSearchBar}>
<div className={styles.headerSearchInputWrap}>
<Input
prefix={<SearchOutlined className={styles.headerSearchIcon} />}
placeholder="搜索好友昵称或内容"
className={styles.headerSearchInput}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onPressEnter={handleSearch}
allowClear
/>
</div>
<Button
icon={<ReloadOutlined spin={recordsLoading} />}
onClick={handleRefresh}
loading={recordsLoading}
type="default"
shape="circle"
/>
</div>
</>
}
footer={
<>
<div className={styles.footerPagination}>
<Pagination
current={currentPage}
total={total}
pageSize={pageSize}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper
size="default"
className={styles.pagination}
/>
</div>
</>
}
>
<div className={styles.bgWrap}>
<div className={styles.contentWrap}>
{recordsLoading ? (
<div className={styles.skeletonWrap}>
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className={styles.skeletonCard}>
<div className={styles.skeletonCardHeader}>
<Skeleton.Avatar
active
size={40}
className={styles.skeletonAvatar}
/>
<div className={styles.skeletonNameWrap}>
<Skeleton.Input
active
size="small"
className={styles.skeletonName}
style={{ width: 96 }}
/>
<Skeleton.Input
active
size="small"
className={styles.skeletonSub}
style={{ width: 64 }}
/>
</div>
</div>
<Divider className={styles.skeletonSep} />
<div className={styles.skeletonContentWrap}>
<Skeleton.Input
active
size="small"
className={styles.skeletonContent1}
style={{ width: "100%" }}
/>
<Skeleton.Input
active
size="small"
className={styles.skeletonContent2}
style={{ width: "75%" }}
/>
<div className={styles.skeletonImgWrap}>
<Skeleton.Image
active
className={styles.skeletonImg}
style={{ width: 80, height: 80 }}
/>
<Skeleton.Image
active
className={styles.skeletonImg}
style={{ width: 80, height: 80 }}
/>
</div>
</div>
</div>
))}
</div>
) : records.length === 0 ? (
<div className={styles.emptyWrap}>
<LikeOutlined className={styles.emptyIcon} />
<p className={styles.emptyText}></p>
</div>
) : (
<>
{records.map(record => (
<div key={record.id} className={styles.recordCard}>
<div className={styles.recordCardHeader}>
<div className={styles.recordCardHeaderLeft}>
<Avatar
src={record.operatorAvatar || undefined}
icon={<UserOutlined />}
size={40}
className={styles.avatarImg}
/>
<div className={styles.friendInfo}>
<div
className={styles.friendName}
title={record.friendName}
>
{record.operatorName}
</div>
<div className={styles.friendSub}>
{formatDate(record.publishTime)}
</div>
</div>
</div>
</div>
<Divider className={styles.cardSep} />
<div className={styles.cardContent}>
{record.content && (
<p className={styles.contentText}>{record.content}</p>
)}
{Array.isArray(record.resUrls) &&
record.resUrls.length > 0 && (
<div
className={
`${styles.imgGrid} ` +
(record.resUrls.length === 1
? styles.grid1
: record.resUrls.length === 2
? styles.grid2
: record.resUrls.length <= 3
? styles.grid3
: record.resUrls.length <= 6
? styles.grid6
: styles.grid9)
}
>
{record.resUrls
.slice(0, 9)
.map((image: string, idx: number) => (
<div key={idx} className={styles.imgItem}>
<img
src={image}
alt={`内容图片 ${idx + 1}`}
className={styles.img}
/>
</div>
))}
</div>
)}
</div>
</div>
))}
</>
)}
</div>
</div>
</Layout>
);
}

View File

@@ -1,267 +0,0 @@
// 搜索栏
.headerSearchBar {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
.headerSearchInputWrap {
position: relative;
flex: 1;
}
.headerSearchIcon {
position: absolute;
left: 12px;
top: 10px;
width: 16px;
height: 16px;
color: #a3a3a3;
}
.headerSearchInput {
padding-left: 32px !important;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
// 分页
.footerPagination {
display: flex;
justify-content: center;
align-items: center;
padding: 12px 0;
background: #fff;
}
.pagination {
:global(.ant-pagination-item) {
border-radius: 6px;
}
:global(.ant-pagination-item-active) {
background: #1890ff;
border-color: #1890ff;
}
:global(.ant-pagination-prev),
:global(.ant-pagination-next) {
border-radius: 6px;
}
:global(.ant-pagination-jump-prev),
:global(.ant-pagination-jump-next) {
border-radius: 6px;
}
}
// 背景和内容
.bgWrap {
background: #f7f7fa;
min-height: 100vh;
padding-bottom: 80px;
}
.contentWrap {
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
// 骨架屏
.skeletonWrap {
display: flex;
flex-direction: column;
gap: 16px;
}
.skeletonCard {
padding: 0px;
}
.skeletonCardHeader {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.skeletonAvatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.skeletonNameWrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeletonName {
width: 96px;
height: 16px;
}
.skeletonSub {
width: 64px;
height: 12px;
}
.skeletonSep {
margin: 12px 0;
}
.skeletonContentWrap {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeletonContent1 {
width: 100%;
height: 16px;
}
.skeletonContent2 {
width: 75%;
height: 16px;
}
.skeletonImgWrap {
display: flex;
gap: 8px;
margin-top: 12px;
}
.skeletonImg {
width: 80px;
height: 80px;
border-radius: 8px;
}
// 空状态
.emptyWrap {
text-align: center;
padding: 48px 0;
}
.emptyIcon {
width: 48px;
height: 48px;
color: #e5e7eb;
margin: 0 auto 12px auto;
}
.emptyText {
color: #888;
font-size: 16px;
}
// 记录卡片
.recordCard {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px;
}
.recordCardHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.recordCardHeaderLeft {
display: flex;
align-items: center;
gap: 12px;
}
.avatarImg {
width: 40px;
height: 40px;
border-radius: 50%;
}
.friendInfo {
min-width: 0;
}
.friendName {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.friendSub {
font-size: 13px;
color: #888;
}
.timeBadge {
background: #e8f0fe;
white-space: nowrap;
flex-shrink: 0;
}
.cardSep {
margin: 12px 0;
}
.cardContent {
margin-bottom: 12px;
}
.contentText {
color: #444;
margin-bottom: 12px;
white-space: pre-line;
}
.imgGrid {
display: grid;
gap: 8px;
}
.grid1 {
grid-template-columns: 1fr;
}
.grid2 {
grid-template-columns: 1fr 1fr;
}
.grid3 {
grid-template-columns: 1fr 1fr 1fr;
}
.grid6 {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.grid9 {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
.imgItem {
position: relative;
aspect-ratio: 1/1;
border-radius: 8px;
overflow: hidden;
}
.img {
width: 100%;
height: 100%;
object-fit: cover;
}
// 操作人
.operatorWrap {
display: flex;
align-items: center;
margin-top: 16px;
padding: 8px;
background: #f3f4f6;
border-radius: 8px;
}
.operatorAvatar {
width: 32px !important;
height: 32px !important;
margin-right: 8px;
flex-shrink: 0;
}
.operatorInfo {
font-size: 14px;
position: relative;
flex: 1;
position: relative;
}
.operatorName {
font-weight: 500;
max-width: 100%;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.operatorAction {
color: #888;
margin-left: 8px;
font-size: 12px;
position: absolute;
right: 0;
top: 2px;
}

View File

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

View File

@@ -1,21 +0,0 @@
import request from "@/api/request";
import type { TrafficDistributionFormData } from "./data";
// 获取流量分发详情
export const getTrafficDistributionDetail = (id: string) => {
return request("/v1/workbench/detail", { id });
};
// 更新流量分发
export const updateTrafficDistribution = (
data: TrafficDistributionFormData,
) => {
return request("/v1/workbench/update", data, "POST");
};
// 创建流量分发
export const createTrafficDistribution = (
data: TrafficDistributionFormData,
) => {
return request("/v1/workbench/create", data, "POST");
};

View File

@@ -1,77 +0,0 @@
// 流量分发详情接口
export interface TrafficDistributionDetail {
id: number;
name: string;
type: number;
status: number;
autoStart: number;
createTime: string;
updateTime: string;
companyId: number;
config: TrafficDistributionConfig;
auto_like: any;
moments_sync: any;
group_push: any;
}
// 流量分发配置接口
export interface TrafficDistributionConfig {
id: number;
workbenchId: number;
distributeType: number;
maxPerDay: number;
timeType: number;
startTime: string;
endTime: string;
accountGroups: any[];
accountGroupsOptions: any[];
deviceGroups: any[];
deviceGroupsOptions: any[];
pools: any[];
exp: number;
createTime: string;
updateTime: string;
lastUpdated: string;
total: {
dailyAverage: number;
totalAccounts: number;
deviceCount: number;
poolCount: number;
totalUsers: number;
};
}
// 流量分发表单数据接口
export interface TrafficDistributionFormData {
id?: string;
type: number;
name: string;
source: string;
sourceIcon: string;
description: string;
distributeType: number;
maxPerDay: number;
timeType: number;
startTime: string;
endTime: string;
deviceGroups: any[];
deviceGroupsOptions: any[];
accountGroups: any[];
accountGroupsOptions: any[];
pools: any[];
enabled: boolean;
}
// 流量池接口
export interface TrafficPool {
id: string;
name: string;
userCount: number;
tags: string[];
}
// 获客场景接口
export interface Scenario {
label: string;
value: string;
}

View File

@@ -1,265 +0,0 @@
.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;
}
.deviceSelectItem {
margin-bottom: 0 !important;
}
.searchWrapper {
margin-bottom: 16px;
}
.tabWrapper {
margin-bottom: 16px;
}
.tabList {
display: flex;
background: #f5f5f5;
border-radius: 8px;
padding: 4px;
}
.tabItem {
flex: 1;
text-align: center;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.tabItem:hover {
color: #1890ff;
}
.tabActive {
background: #fff;
color: #1890ff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tabContent {
min-height: 200px;
}
.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;
}
.formFooter {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
border-top: 1px solid #f0f0f0;
}
.footerBtn {
min-width: 80px;
}
// 步骤条美化
.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;
}

View File

@@ -1,470 +0,0 @@
import React, { useState, useEffect } from "react";
import {
Form,
Input,
Button,
Radio,
Slider,
TimePicker,
message,
Checkbox,
} from "antd";
import { SearchOutlined } from "@ant-design/icons";
import { useNavigate, useParams } 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 { AccountItem } from "@/components/AccountSelection/data";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import {
getTrafficDistributionDetail,
updateTrafficDistribution,
createTrafficDistribution,
} from "./api";
import type {
TrafficDistributionDetail,
TrafficDistributionFormData,
} from "./data";
import dayjs from "dayjs";
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 { id } = useParams<{ id: string }>();
const isEdit = !!id;
const [current, setCurrent] = useState(0);
const [selectedAccounts, setSelectedAccounts] = useState<AccountItem[]>([]);
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
[],
);
// 设备组和账号组数据
const [deviceGroups, setDeviceGroups] = useState<any[]>([]);
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<any[]>([]);
const [accountGroups, setAccountGroups] = useState<any[]>([]);
const [accountGroupsOptions, setAccountGroupsOptions] = useState<any[]>([]);
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 [detailLoading, setDetailLoading] = useState(false);
const [selectedPools, setSelectedPools] = useState<string[]>([]);
const [poolSearch, setPoolSearch] = useState("");
const [targetSelectionTab, setTargetSelectionTab] = useState<
"device" | "account"
>("device");
// 编辑时的详情数据
const [detailData, setDetailData] =
useState<TrafficDistributionDetail | null>(null);
// 生成默认名称
const generateDefaultName = () => {
const now = new Date();
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
const timeStr = now.toTimeString().slice(0, 5).replace(/:/g, "");
return `流量分发 ${dateStr} ${timeStr}`;
};
// 获取详情数据
useEffect(() => {
if (isEdit && id) {
fetchDetail();
}
}, [isEdit, id]);
const fetchDetail = async () => {
if (!id) return;
setDetailLoading(true);
try {
const detail = await getTrafficDistributionDetail(id);
setDetailData(detail);
// 回填表单数据
const config = detail.config;
form.setFieldsValue({
name: detail.name,
distributeType: config.distributeType,
maxPerDay: config.maxPerDay,
timeType: config.timeType,
});
// 设置状态
setDistributeType(config.distributeType);
setMaxPerDay(config.maxPerDay);
setTimeType(config.timeType);
// 设置账号组数据
setAccountGroups(config.accountGroups || []);
setAccountGroupsOptions(config.accountGroupsOptions || []);
setSelectedAccounts(config.accountGroupsOptions || []);
// 设置设备组数据
setDeviceGroups(config.deviceGroups || []);
setDeviceGroupsOptions(config.deviceGroupsOptions || []);
setSelectedDevices(config.deviceGroupsOptions || []);
// 设置时间范围 - 使用dayjs格式
if (config.timeType === 2 && config.startTime && config.endTime) {
const [startHour, startMinute] = config.startTime
.split(":")
.map(Number);
const [endHour, endMinute] = config.endTime.split(":").map(Number);
// 使用dayjs创建时间对象
const startTime = dayjs().hour(startHour).minute(startMinute).second(0);
const endTime = dayjs().hour(endHour).minute(endMinute).second(0);
setTimeRange([startTime, endTime]);
}
// 设置流量池
setSelectedPools(config.pools.map((pool: any) => pool.id || pool));
} catch (error) {
console.error("获取详情失败:", error);
message.error("获取详情失败");
} finally {
setDetailLoading(false);
}
};
const handleFinish = async (values?: any) => {
setLoading(true);
try {
// 如果没有传递values参数从表单中获取
const formValues = values || form.getFieldsValue();
const formData: TrafficDistributionFormData = {
id: id,
type: 5, // 流量分发类型
name: formValues.name,
source: "",
sourceIcon: "",
description: "",
distributeType: distributeType,
maxPerDay: maxPerDay,
timeType: timeType,
startTime:
timeType === 2 && timeRange?.[0] ? timeRange[0].format("HH:mm") : "",
endTime:
timeType === 2 && timeRange?.[1] ? timeRange[1].format("HH:mm") : "",
deviceGroups: deviceGroups,
deviceGroupsOptions: deviceGroupsOptions,
accountGroups: accountGroups,
accountGroupsOptions: accountGroupsOptions,
pools: selectedPools,
enabled: true,
};
if (isEdit) {
await updateTrafficDistribution(formData);
message.success("更新流量分发成功");
} else {
await createTrafficDistribution(formData);
message.success("新建流量分发成功");
}
navigate(-1);
} catch (e) {
message.error(isEdit ? "更新失败" : "新建失败");
} finally {
setLoading(false);
}
};
// 步骤切换
const next = () => {
if (current === 0) {
// 第一步需要验证表单
form
.validateFields()
.then(() => {
setCurrent(cur => cur + 1);
})
.catch(() => {
// 验证失败,不进行下一步
});
} else {
setCurrent(cur => cur + 1);
}
};
const prev = () => setCurrent(cur => cur - 1);
// 过滤流量池
const filteredPools = poolList.filter(pool => pool.name.includes(poolSearch));
return (
<Layout
header={
<>
<NavCommon title={isEdit ? "编辑流量分发" : "新建流量分发"} />
<div className={style.formStepsWrap}>
<StepIndicator currentStep={current + 1} steps={stepList} />
</div>
</>
}
loading={detailLoading}
footer={
<div className="footer-btn-group">
{current > 0 && (
<Button size="large" onClick={prev}>
</Button>
)}
{current < 2 ? (
<Button size="large" type="primary" onClick={next}>
</Button>
) : (
<Button
type="primary"
size="large"
onClick={handleFinish}
loading={loading}
>
</Button>
)}
</div>
}
>
<div className={style.formPage}>
<div className={style.formBody}>
{current === 0 && (
<Form
form={form}
layout="vertical"
initialValues={{
name: isEdit ? "" : generateDefaultName(),
distributeType: 1,
maxPerDay: 50,
timeType: 1,
}}
disabled={detailLoading}
>
<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="分配方式" 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>
)}
{current === 1 && (
<div>
<div className={style.sectionTitle}></div>
{/* Tab 切换 */}
<div className={style.tabWrapper}>
<div className={style.tabList}>
<div
className={`${style.tabItem} ${
targetSelectionTab === "device" ? style.tabActive : ""
}`}
onClick={() => setTargetSelectionTab("device")}
>
</div>
<div
className={`${style.tabItem} ${
targetSelectionTab === "account" ? style.tabActive : ""
}`}
onClick={() => setTargetSelectionTab("account")}
>
</div>
</div>
</div>
{/* Tab 内容 */}
<div className={style.tabContent}>
{targetSelectionTab === "device" && (
<div className={style.formBlock}>
<DeviceSelection
selectedOptions={selectedDevices}
onSelect={devices => {
setSelectedDevices(devices);
setDeviceGroupsOptions(devices);
}}
placeholder="请选择设备"
showSelectedList={true}
selectedListMaxHeight={300}
deviceGroups={deviceGroups}
/>
</div>
)}
{targetSelectionTab === "account" && (
<div className={style.formBlock}>
<AccountSelection
selectedOptions={selectedAccounts}
onSelect={accounts => {
setSelectedAccounts(accounts);
setAccountGroupsOptions(accounts);
}}
placeholder="请选择客服"
showSelectedList={true}
selectedListMaxHeight={300}
accountGroups={accountGroups}
/>
</div>
)}
</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>
)}
</div>
</div>
</Layout>
);
};
export default TrafficDistributionForm;

View File

@@ -1,43 +0,0 @@
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");
}
// 编辑计划(更新)
export function updateDistributionRule(data: any): Promise<any> {
return request("/v1/workbench/update", { ...data, type: 5 }, "POST");
}
// 暂停/启用计划
export function toggleDistributionRuleStatus(
id: number,
status: 0 | 1,
): Promise<any> {
return request("/v1/workbench/update-status", { id, status }, "POST");
}
// 删除计划
export function deleteDistributionRule(id: number): Promise<any> {
return request("/v1/workbench/delete", { id }, "POST");
}
// 获取流量分发规则详情
export function fetchDistributionRuleDetail(id: number): Promise<any> {
return request(`/v1/workbench/detail?id=${id}`, {}, "GET");
}
//流量分发记录
export function fetchTransferFriends(params: {
page?: number;
limit?: number;
keyword?: string;
workbenchId: number;
}) {
return request("/v1/workbench/transfer-friends", params, "GET");
}

View File

@@ -1,166 +0,0 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "../index.module.scss";
import { fetchDistributionRuleDetail } from "../api";
interface AccountItem {
id: string | number;
nickname?: string;
wechatId?: string;
avatar?: string;
status?: string;
}
interface AccountListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const AccountListModal: React.FC<AccountListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取账号数据
const fetchAccounts = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await fetchDistributionRuleDetail(ruleId);
const accountData = detailRes?.config?.accountGroupsOptions || [];
setAccounts(accountData);
} catch (error) {
console.error("获取账号详情失败:", error);
message.error("获取账号详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchAccounts();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 分发账号列表` : "分发账号列表";
const getStatusColor = (status?: string) => {
switch (status) {
case "normal":
return "#52c41a";
case "limited":
return "#faad14";
case "blocked":
return "#ff4d4f";
default:
return "#d9d9d9";
}
};
const getStatusText = (status?: string) => {
switch (status) {
case "normal":
return "正常";
case "limited":
return "受限";
case "blocked":
return "封禁";
default:
return "未知";
}
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.accountModal}>
{/* 头部 */}
<div className={style.accountModalHeader}>
<h3 className={style.accountModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.accountModalClose}
/>
</div>
{/* 账号列表 */}
<div className={style.accountList}>
{loading ? (
<div className={style.accountLoading}>
<SpinLoading color="primary" />
<div className={style.accountLoadingText}>
...
</div>
</div>
) : accounts.length > 0 ? (
accounts.map((account, index) => (
<div key={account.id || index} className={style.accountItem}>
<div className={style.accountAvatar}>
<Avatar
src={account.avatar}
style={{ "--size": "48px" }}
fallback={
(account.nickname || account.wechatId || "账号")[0]
}
/>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>
{account.nickname ||
account.wechatId ||
`账号${account.id}`}
</div>
<div className={style.accountWechatId}>
{account.wechatId || "未绑定微信号"}
</div>
</div>
<div className={style.accountStatus}>
<span
className={style.statusDot}
style={{ backgroundColor: getStatusColor(account.status) }}
/>
<span className={style.statusText}>
{getStatusText(account.status)}
</span>
</div>
</div>
))
) : (
<div className={style.accountEmpty}>
<div className={style.accountEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.accountModalFooter}>
<div className={style.accountStats}>
<span> {accounts.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default AccountListModal;

Some files were not shown because too many files have changed in this diff Show More