自动建群提交 + 触客宝发布朋友圈优化
This commit is contained in:
@@ -219,6 +219,48 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.searchInputWrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clearSearchBtn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
padding: 0;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchBtn {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.memberList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { SearchOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Popup } from "antd-mobile";
|
||||
import { Avatar } from "antd-mobile";
|
||||
import style from "./index.module.scss";
|
||||
@@ -82,6 +82,9 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
const [allMembers, setAllMembers] = useState<Record<string, GroupMember[]>>({});
|
||||
const [selectedMembers, setSelectedMembers] = useState<Record<string, GroupMember[]>>({});
|
||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||
const [memberSearchKeyword, setMemberSearchKeyword] = useState("");
|
||||
// 存储完整成员列表(用于搜索时切换回完整列表)
|
||||
const [fullMembersCache, setFullMembersCache] = useState<Record<string, GroupMember[]>>({});
|
||||
|
||||
// 处理群选择
|
||||
const handleGroupSelect = (groups: GroupSelectionItem[]) => {
|
||||
@@ -110,24 +113,66 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
const handleOpenMemberSelection = async (groupId: string) => {
|
||||
if (readonly) return;
|
||||
setMemberSelectionVisible({ visible: true, groupId });
|
||||
setMemberSearchKeyword(""); // 重置搜索关键词
|
||||
|
||||
// 如果还没有加载过该群的成员列表,则加载
|
||||
if (!allMembers[groupId]) {
|
||||
// 如果还没有加载过该群的成员列表,则加载所有成员(不使用搜索关键词)
|
||||
if (!allMembers[groupId] && !fullMembersCache[groupId]) {
|
||||
setLoadingMembers(true);
|
||||
try {
|
||||
const members = await getGroupMembers(groupId);
|
||||
const members = await getGroupMembers(groupId, 1, 100, "");
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: members }));
|
||||
setFullMembersCache(prev => ({ ...prev, [groupId]: members })); // 缓存完整列表
|
||||
} catch (error) {
|
||||
console.error("加载群成员失败:", error);
|
||||
} finally {
|
||||
setLoadingMembers(false);
|
||||
}
|
||||
} else if (fullMembersCache[groupId] && !allMembers[groupId]) {
|
||||
// 如果有缓存但没有显示列表,恢复完整列表
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭成员选择弹窗
|
||||
const handleCloseMemberSelection = () => {
|
||||
setMemberSelectionVisible({ visible: false, groupId: "" });
|
||||
setMemberSearchKeyword(""); // 重置搜索关键词
|
||||
};
|
||||
|
||||
// 手动触发搜索
|
||||
const handleSearchMembers = async () => {
|
||||
const groupId = memberSelectionVisible.groupId;
|
||||
if (!groupId) return;
|
||||
|
||||
const keyword = memberSearchKeyword.trim();
|
||||
|
||||
// 如果搜索关键词为空,使用缓存的完整列表
|
||||
if (!keyword) {
|
||||
if (fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 有搜索关键词时,调用 API 搜索
|
||||
setLoadingMembers(true);
|
||||
try {
|
||||
const members = await getGroupMembers(groupId, 1, 100, keyword);
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: members }));
|
||||
} catch (error) {
|
||||
console.error("搜索群成员失败:", error);
|
||||
} finally {
|
||||
setLoadingMembers(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空搜索
|
||||
const handleClearSearch = () => {
|
||||
setMemberSearchKeyword("");
|
||||
const groupId = memberSelectionVisible.groupId;
|
||||
if (groupId && fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
};
|
||||
|
||||
// 选择成员
|
||||
@@ -328,6 +373,36 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style.searchBox}>
|
||||
<div className={style.searchInputWrapper}>
|
||||
<Input
|
||||
placeholder="搜索成员昵称或微信号"
|
||||
value={memberSearchKeyword}
|
||||
onChange={val => setMemberSearchKeyword(val)}
|
||||
onEnterPress={handleSearchMembers}
|
||||
className={style.searchInput}
|
||||
/>
|
||||
{memberSearchKeyword && (
|
||||
<Button
|
||||
fill="none"
|
||||
size="small"
|
||||
className={style.clearSearchBtn}
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={handleSearchMembers}
|
||||
loading={loadingMembers}
|
||||
className={style.searchBtn}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style.memberList}>
|
||||
{loadingMembers ? (
|
||||
<div className={style.loadingBox}>
|
||||
|
||||
@@ -99,13 +99,18 @@ export default function ContentForm() {
|
||||
: []);
|
||||
setSelectedDevices(deviceOptions || []);
|
||||
setSelectedFriends(data.sourceFriends || []);
|
||||
setSelectedGroups(data.selectedGroups || []);
|
||||
// 使用 wechatGroupsOptions 作为群列表数据
|
||||
setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []);
|
||||
const groupsOptions = data.wechatGroupsOptions || data.selectedGroupsOptions || [];
|
||||
setSelectedGroupsOptions(groupsOptions);
|
||||
// 从 groupsOptions 中提取群 ID 列表,如果没有 selectedGroups 的话
|
||||
const groupIds = data.selectedGroups && data.selectedGroups.length > 0
|
||||
? data.selectedGroups
|
||||
: groupsOptions.map((g: any) => String(g.id));
|
||||
setSelectedGroups(groupIds);
|
||||
// 处理带成员的群数据
|
||||
// groupMembersOptions 是一个对象,key是群ID(字符串),value是成员数组
|
||||
const groupMembersMap = data.groupMembersOptions || {};
|
||||
const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map(
|
||||
const groupsWithMembers: GroupWithMembers[] = groupsOptions.map(
|
||||
(group: any) => {
|
||||
const groupIdStr = String(group.id);
|
||||
const members = groupMembersMap[groupIdStr] || [];
|
||||
|
||||
@@ -75,7 +75,7 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
useEffect(() => {
|
||||
if (!visible || !ruleId) return;
|
||||
|
||||
setLoading(true);
|
||||
setLoading(true);
|
||||
const params: { startTime?: string; endTime?: string } = {};
|
||||
if (startTime) {
|
||||
params.startTime = formatDate(startTime);
|
||||
@@ -85,26 +85,26 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
}
|
||||
|
||||
getFriendRequestTaskStats(ruleId.toString(), params)
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
setXData(res.dateArray);
|
||||
setYData([
|
||||
res.allNumArray,
|
||||
res.errorNumArray,
|
||||
res.passNumArray,
|
||||
res.passRateArray,
|
||||
res.successNumArray,
|
||||
res.successRateArray,
|
||||
]);
|
||||
setStatistics(res.totalStats);
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
setXData(res.dateArray);
|
||||
setYData([
|
||||
res.allNumArray,
|
||||
res.errorNumArray,
|
||||
res.passNumArray,
|
||||
res.passRateArray,
|
||||
res.successNumArray,
|
||||
res.successRateArray,
|
||||
]);
|
||||
setStatistics(res.totalStats);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取统计数据失败:", error);
|
||||
message.error("获取统计数据失败");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [visible, ruleId, startTime, endTime, formatDate]);
|
||||
|
||||
const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据";
|
||||
|
||||
@@ -221,7 +221,7 @@ const AddChannelModal: React.FC<AddChannelModalProps> = ({
|
||||
setQrCodeData(null);
|
||||
setScanning(false);
|
||||
const timer = setTimeout(() => {
|
||||
handleGenerateQRCode();
|
||||
handleGenerateQRCode();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.syncOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.syncContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
padding: 32px 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.syncText {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loadingContainer,
|
||||
.emptyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
padding: 8px;
|
||||
margin-right: -8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
// 群组信息卡片
|
||||
.groupInfoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.groupIconLarge {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 auto 16px;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.groupTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
text-align: center;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.createTimeInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.createTimeIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.actionButtonIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
// 基本信息
|
||||
.basicInfoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sectionTitleDot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.basicInfoList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.basicInfoItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.basicInfoLabel {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.basicInfoValue {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.planIcon {
|
||||
font-size: 16px;
|
||||
color: #3b82f6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.adminAvatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.adminAvatarText {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.basicInfoDivider {
|
||||
height: 1px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.noAnnouncement {
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chevronIcon {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
// 群成员
|
||||
.membersCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.membersHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.memberCountBadge {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
padding: 6px;
|
||||
color: #6b7280;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.membersList {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.memberItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f9fafb;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.memberInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memberInfoText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.memberNameRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memberAvatarWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memberAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.memberAvatarPlaceholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #e9d5ff;
|
||||
color: #7c3aed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.adminBadge {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #fbbf24;
|
||||
border: 2px solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.adminBadgeIcon {
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.memberInfoText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.memberNameRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memberName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.memberWechatId {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.quitTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.joinStatusTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memberExtraInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.memberJoinTime {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.memberActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.adminTag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid rgba(146, 64, 14, 0.3);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
color: #9ca3af;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:active {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyMembers {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyMembersText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.actionsSection {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.actionCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.actionCardIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.actionCard:hover .actionCardIcon {
|
||||
background: #dbeafe;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.actionCardText {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.actionCardDanger {
|
||||
&:active {
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.actionCardIconDanger {
|
||||
background: #fee2e2;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.actionCardTextDanger {
|
||||
color: #dc2626;
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Toast, SpinLoading, Dialog, Input, TextArea } from "antd-mobile";
|
||||
import {
|
||||
EditOutlined,
|
||||
QrcodeOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
LogoutOutlined,
|
||||
StarOutlined,
|
||||
CloseCircleOutlined,
|
||||
ScheduleOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getCreatedGroupDetail, syncGroupInfo, modifyGroupInfo, quitGroup } from "../form/api";
|
||||
import style from "./group-detail.module.scss";
|
||||
|
||||
interface GroupMember {
|
||||
id?: string;
|
||||
friendId?: number;
|
||||
nickname?: string;
|
||||
wechatId?: string;
|
||||
avatar?: string;
|
||||
isOwner?: number; // 1表示是群主,0表示不是
|
||||
isGroupAdmin?: boolean;
|
||||
joinStatus?: string; // "auto" | "manual" - 入群状态
|
||||
isQuit?: number; // 0/1 - 是否已退群
|
||||
joinTime?: string; // 入群时间
|
||||
alias?: string; // 成员别名
|
||||
remark?: string; // 成员备注
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GroupDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
createTime?: string;
|
||||
planName?: string;
|
||||
groupAdmin?: {
|
||||
id: string;
|
||||
nickname?: string;
|
||||
wechatId?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
announcement?: string;
|
||||
memberCount?: number;
|
||||
members?: GroupMember[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupDetailPage: React.FC = () => {
|
||||
const { id, groupId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groupDetail, setGroupDetail] = useState<GroupDetail | null>(null);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [editNameVisible, setEditNameVisible] = useState(false);
|
||||
const [editNameValue, setEditNameValue] = useState("");
|
||||
const [editAnnouncementVisible, setEditAnnouncementVisible] = useState(false);
|
||||
const [editAnnouncementValue, setEditAnnouncementValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !groupId) return;
|
||||
fetchGroupDetail();
|
||||
}, [id, groupId]);
|
||||
|
||||
const fetchGroupDetail = async () => {
|
||||
if (!id || !groupId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getCreatedGroupDetail({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
const data = res?.data || res || {};
|
||||
|
||||
// 处理成员数据,确保有id字段
|
||||
const members = (data.members || []).map((member: any) => ({
|
||||
...member,
|
||||
id: String(member.friendId || member.id || member.wechatId || ''),
|
||||
}));
|
||||
|
||||
const detailData: GroupDetail = {
|
||||
id: String(data.id || groupId),
|
||||
name: data.groupName || data.name || "未命名群组",
|
||||
createTime: data.createTime || "",
|
||||
planName: data.workbenchName || data.planName || "",
|
||||
groupAdmin: data.ownerWechatId || data.ownerNickname
|
||||
? {
|
||||
id: String(data.ownerWechatId || ""),
|
||||
nickname: data.ownerNickname,
|
||||
wechatId: data.ownerWechatId,
|
||||
avatar: data.ownerAvatar,
|
||||
}
|
||||
: undefined,
|
||||
announcement: data.announce || data.announcement, // 接口返回的字段是 announce
|
||||
memberCount: data.memberCount || members.length || 0,
|
||||
members: members,
|
||||
avatar: data.avatar || data.groupAvatar,
|
||||
groupAvatar: data.groupAvatar,
|
||||
};
|
||||
|
||||
setGroupDetail(detailData);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取群组详情失败", position: "top" });
|
||||
navigate(-1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditName = () => {
|
||||
if (!groupDetail) return;
|
||||
setEditNameValue(groupDetail.name);
|
||||
setEditNameVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmEditName = async () => {
|
||||
if (!id || !groupId || !editNameValue.trim()) {
|
||||
Toast.show({ content: "群名称不能为空", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await modifyGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
chatroomName: editNameValue.trim(),
|
||||
});
|
||||
Toast.show({ content: "修改成功", position: "top" });
|
||||
setEditNameVisible(false);
|
||||
// 刷新群详情
|
||||
fetchGroupDetail();
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "修改群名称失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAnnouncement = () => {
|
||||
if (!groupDetail) return;
|
||||
setEditAnnouncementValue(groupDetail.announcement || "");
|
||||
setEditAnnouncementVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmEditAnnouncement = async () => {
|
||||
if (!id || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await modifyGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
announce: editAnnouncementValue.trim() || undefined,
|
||||
});
|
||||
Toast.show({ content: "修改成功", position: "top" });
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
// 刷新群详情
|
||||
fetchGroupDetail();
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "修改群公告失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowQRCode = () => {
|
||||
Toast.show({ content: "群二维码功能待实现", position: "top" });
|
||||
};
|
||||
|
||||
const handleSyncGroup = async () => {
|
||||
if (!id || !groupId) {
|
||||
Toast.show({ content: "参数错误", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncing(true);
|
||||
try {
|
||||
const res = await syncGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
const data = res?.data || res || {};
|
||||
|
||||
const successMessages: string[] = [];
|
||||
if (data.groupInfoSynced) {
|
||||
successMessages.push("群信息同步成功");
|
||||
}
|
||||
if (data.memberInfoSynced) {
|
||||
successMessages.push("群成员信息同步成功");
|
||||
}
|
||||
|
||||
if (successMessages.length > 0) {
|
||||
Toast.show({
|
||||
content: successMessages.join(","),
|
||||
position: "top"
|
||||
});
|
||||
// 同步成功后刷新群详情
|
||||
await fetchGroupDetail();
|
||||
} else {
|
||||
Toast.show({
|
||||
content: res?.msg || "同步完成",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "同步群信息失败",
|
||||
position: "top"
|
||||
});
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisbandGroup = () => {
|
||||
Dialog.confirm({
|
||||
content: "确定要退出该群组吗?",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
onConfirm: async () => {
|
||||
if (!id || !groupId) {
|
||||
Toast.show({ content: "参数错误", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await quitGroup({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
Toast.show({ content: "退出群组成功", position: "top" });
|
||||
// 退出成功后返回上一页
|
||||
navigate(-1);
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "退出群组失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveMember = (memberId: string) => {
|
||||
Dialog.confirm({
|
||||
content: "确定要移除此成员吗?",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {
|
||||
Toast.show({ content: "移除成员功能待实现", position: "top" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤成员
|
||||
const filteredMembers = groupDetail?.members?.filter((member) => {
|
||||
if (!searchKeyword) return true;
|
||||
const keyword = searchKeyword.toLowerCase();
|
||||
return (
|
||||
member.nickname?.toLowerCase().includes(keyword) ||
|
||||
member.wechatId?.toLowerCase().includes(keyword)
|
||||
);
|
||||
}) || [];
|
||||
|
||||
// 群组图标颜色
|
||||
const iconColors = {
|
||||
from: "#3b82f6",
|
||||
to: "#4f46e5",
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!groupDetail) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyText}>未找到该群组</div>
|
||||
<button
|
||||
className={style.backButton}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="群详情"
|
||||
backFn={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 同步遮罩层 */}
|
||||
{syncing && (
|
||||
<div className={style.syncOverlay}>
|
||||
<div className={style.syncContent}>
|
||||
<SpinLoading style={{ "--size": "32px" }} />
|
||||
<div className={style.syncText}>同步中...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 群组信息卡片 */}
|
||||
<section className={style.groupInfoCard}>
|
||||
{groupDetail.avatar || groupDetail.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconLarge}
|
||||
src={groupDetail.avatar || groupDetail.groupAvatar}
|
||||
alt={groupDetail.name || ""}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.nextElementSibling as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconLarge}
|
||||
style={{
|
||||
display: groupDetail.avatar || groupDetail.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
|
||||
}}
|
||||
>
|
||||
{(groupDetail.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
<h2 className={style.groupTitle}>{groupDetail.name}</h2>
|
||||
<div className={style.createTimeInfo}>
|
||||
<ScheduleOutlined className={style.createTimeIcon} />
|
||||
创建于 {groupDetail.createTime || "-"}
|
||||
</div>
|
||||
<div className={style.actionButtons}>
|
||||
<button className={style.actionButton} onClick={handleEditName}>
|
||||
<EditOutlined className={style.actionButtonIcon} />
|
||||
修改名称
|
||||
</button>
|
||||
{/* 群二维码功能暂时隐藏 */}
|
||||
{/* <button className={style.actionButton} onClick={handleShowQRCode}>
|
||||
<QrcodeOutlined className={style.actionButtonIcon} />
|
||||
群二维码
|
||||
</button> */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<section className={style.basicInfoCard}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
基本信息
|
||||
</h3>
|
||||
<div className={style.basicInfoList}>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>所属计划</span>
|
||||
<div className={style.basicInfoValue}>
|
||||
<span className={style.planIcon}>📋</span>
|
||||
{groupDetail.planName || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.basicInfoDivider}></div>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>群主</span>
|
||||
<div className={style.basicInfoValue}>
|
||||
{groupDetail.groupAdmin ? (
|
||||
<>
|
||||
<span className={style.adminAvatar}>
|
||||
{groupDetail.groupAdmin.avatar ? (
|
||||
<img
|
||||
src={groupDetail.groupAdmin.avatar}
|
||||
alt={groupDetail.groupAdmin.nickname || ""}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
|
||||
style.hidden
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span className={style.adminAvatarText}>
|
||||
{groupDetail.groupAdmin.nickname?.charAt(0)?.toUpperCase() || "A"}
|
||||
</span>
|
||||
</span>
|
||||
{groupDetail.groupAdmin.wechatId || "-"}
|
||||
</>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.basicInfoDivider}></div>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>群公告</span>
|
||||
<div
|
||||
className={style.basicInfoValue}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={handleEditAnnouncement}
|
||||
>
|
||||
{groupDetail.announcement ? (
|
||||
groupDetail.announcement
|
||||
) : (
|
||||
<span className={style.noAnnouncement}>
|
||||
未设置
|
||||
<span className={style.chevronIcon}>›</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 群成员 */}
|
||||
<section className={style.membersCard}>
|
||||
<div className={style.membersHeader}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
群成员
|
||||
<span className={style.memberCountBadge}>
|
||||
{groupDetail.memberCount || 0}人
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
className={style.searchButton}
|
||||
onClick={() => setShowSearch(!showSearch)}
|
||||
title="搜索成员"
|
||||
>
|
||||
<SearchOutlined />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<div className={style.searchBox}>
|
||||
<input
|
||||
className={style.searchInput}
|
||||
placeholder="搜索成员..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style.membersList}>
|
||||
{filteredMembers.length > 0 ? (
|
||||
filteredMembers.map((member) => {
|
||||
const isGroupAdmin = member.isOwner === 1 || member.isGroupAdmin || member.id === groupDetail.groupAdmin?.id;
|
||||
return (
|
||||
<div key={member.friendId || member.id || member.wechatId} className={style.memberItem}>
|
||||
<div className={style.memberInfo}>
|
||||
<div className={style.memberAvatarWrapper}>
|
||||
{member.avatar ? (
|
||||
<img
|
||||
className={style.memberAvatar}
|
||||
src={member.avatar}
|
||||
alt={member.nickname || ""}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={style.memberAvatarPlaceholder}>
|
||||
{member.nickname?.charAt(0)?.toUpperCase() || "?"}
|
||||
</div>
|
||||
)}
|
||||
{isGroupAdmin && (
|
||||
<div className={style.adminBadge}>
|
||||
<StarOutlined className={style.adminBadgeIcon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.memberInfoText}>
|
||||
<div className={style.memberNameRow}>
|
||||
<span className={style.memberName}>
|
||||
{member.nickname || "-"}
|
||||
</span>
|
||||
{member.isQuit === 1 && (
|
||||
<span className={style.quitTag}>已退群</span>
|
||||
)}
|
||||
{member.joinStatus && (
|
||||
<span className={style.joinStatusTag}>
|
||||
{member.joinStatus === "auto" ? "自动" : "手动"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.memberWechatId}>
|
||||
{member.wechatId || "-"}
|
||||
</div>
|
||||
{(member.alias || member.remark) && (
|
||||
<div className={style.memberExtraInfo}>
|
||||
{member.alias && <span>别名:{member.alias}</span>}
|
||||
{member.alias && member.remark && <span className={style.divider}>|</span>}
|
||||
{member.remark && <span>备注:{member.remark}</span>}
|
||||
</div>
|
||||
)}
|
||||
{member.joinTime && (
|
||||
<div className={style.memberJoinTime}>
|
||||
入群时间:{member.joinTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.memberActions}>
|
||||
{isGroupAdmin ? (
|
||||
<span className={style.adminTag}>群主</span>
|
||||
) : (
|
||||
// 移除成员功能暂时隐藏
|
||||
// <button
|
||||
// className={style.removeButton}
|
||||
// onClick={() => handleRemoveMember(String(member.friendId || member.id || member.wechatId || ''))}
|
||||
// >
|
||||
// <CloseCircleOutlined />
|
||||
// </button>
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={style.emptyMembers}>
|
||||
<div className={style.emptyMembersText}>
|
||||
{searchKeyword ? "未找到匹配的成员" : "暂无成员"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<section className={style.actionsSection}>
|
||||
<button className={style.actionCard} onClick={handleSyncGroup}>
|
||||
<div className={style.actionCardIcon}>
|
||||
<SyncOutlined />
|
||||
</div>
|
||||
<span className={style.actionCardText}>同步群信息</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${style.actionCard} ${style.actionCardDanger}`}
|
||||
onClick={handleDisbandGroup}
|
||||
>
|
||||
<div className={`${style.actionCardIcon} ${style.actionCardIconDanger}`}>
|
||||
<LogoutOutlined />
|
||||
</div>
|
||||
<span className={`${style.actionCardText} ${style.actionCardTextDanger}`}>
|
||||
解散/退出群组
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 修改群名称弹窗 */}
|
||||
<Dialog
|
||||
visible={editNameVisible}
|
||||
content={
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
|
||||
群名称
|
||||
</div>
|
||||
<Input
|
||||
placeholder="请输入群名称"
|
||||
value={editNameValue}
|
||||
onChange={(val) => setEditNameValue(val)}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
padding: "10px 12px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#fff",
|
||||
}}
|
||||
maxLength={30}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
closeOnAction
|
||||
onClose={() => {
|
||||
setEditNameVisible(false);
|
||||
setEditNameValue("");
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
key: "cancel",
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
setEditNameVisible(false);
|
||||
setEditNameValue("");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "confirm",
|
||||
text: "确定",
|
||||
primary: true,
|
||||
onClick: handleConfirmEditName,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 修改群公告弹窗 */}
|
||||
<Dialog
|
||||
visible={editAnnouncementVisible}
|
||||
content={
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
|
||||
群公告
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder="请输入群公告"
|
||||
value={editAnnouncementValue}
|
||||
onChange={(val) => setEditAnnouncementValue(val)}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
padding: "10px 12px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#fff",
|
||||
minHeight: "120px",
|
||||
}}
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
closeOnAction
|
||||
onClose={() => {
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
key: "cancel",
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "confirm",
|
||||
text: "确定",
|
||||
primary: true,
|
||||
onClick: handleConfirmEditAnnouncement,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDetailPage;
|
||||
@@ -0,0 +1,275 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
padding: 8px;
|
||||
margin-right: -8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索和筛选区域
|
||||
.filterSection {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
background: #f3f4f6;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
font-size: 18px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
&:focus + .searchIcon,
|
||||
&:focus ~ .searchIcon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #4b5563;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButtonActive {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 1px 2px 0 rgba(59, 130, 246, 0.3);
|
||||
|
||||
&:active {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 群组列表
|
||||
.groupsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.groupCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.groupCardContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.groupIconWrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupIconImg,
|
||||
.groupIconFallback {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.groupIconImg {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.groupIconFallback {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.groupInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.groupName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.groupMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memberCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #4b5563;
|
||||
background: #f3f4f6;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.createTime {
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.footerText {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Toast, SpinLoading, InfiniteScroll } from "antd-mobile";
|
||||
import { SearchOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getCreatedGroupsList } from "../form/api";
|
||||
import style from "./groups-list.module.scss";
|
||||
|
||||
interface Group {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
groupName?: string;
|
||||
memberCount: number;
|
||||
memberCountText?: string;
|
||||
createTime: string;
|
||||
avatar?: string;
|
||||
groupAvatar?: string;
|
||||
memberAvatars?: Array<{
|
||||
avatar?: string;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
ownerNickname?: string;
|
||||
ownerWechatId?: string;
|
||||
ownerAvatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GroupListPageProps {
|
||||
planId?: string;
|
||||
}
|
||||
|
||||
const GroupListPage: React.FC<GroupListPageProps> = ({ planId: propPlanId }) => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const planId = propPlanId || id;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [sortBy, setSortBy] = useState<"all" | "createTime" | "memberCount">("all");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!planId) return;
|
||||
fetchGroups(true);
|
||||
}, [planId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!planId) return;
|
||||
const timer = setTimeout(() => {
|
||||
fetchGroups(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchKeyword]);
|
||||
|
||||
const fetchGroups = async (reset = false) => {
|
||||
if (!planId) return;
|
||||
const page = reset ? 1 : currentPage;
|
||||
if (reset) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const res = await getCreatedGroupsList({
|
||||
workbenchId: planId,
|
||||
page,
|
||||
limit: 20,
|
||||
keyword: searchKeyword || undefined,
|
||||
});
|
||||
// 注意:request 拦截器已经提取了 data 字段,所以 res 就是 data 对象
|
||||
// 接口返回结构:{ list: [...], total: 2, page: "1", limit: "10" }
|
||||
const groupsData = res?.list || (Array.isArray(res) ? res : []);
|
||||
if (reset) {
|
||||
setGroups(groupsData);
|
||||
setCurrentPage(2); // 重置后下一页是2
|
||||
} else {
|
||||
setGroups(prev => [...prev, ...groupsData]);
|
||||
setCurrentPage(prev => prev + 1);
|
||||
}
|
||||
setHasMore(groupsData.length >= 20);
|
||||
} catch (e: any) {
|
||||
// request拦截器在code !== 200时会reject并显示Toast
|
||||
// 如果拦截器没有显示错误(比如网络错误),这里才显示
|
||||
// 注意:拦截器已经在错误时显示了Toast,所以这里通常不需要再显示
|
||||
} finally {
|
||||
if (reset) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (hasMore && !loading) {
|
||||
await fetchGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 排序(搜索和过滤由接口完成)
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
if (sortBy === "createTime") {
|
||||
const timeA = new Date(a.createTime || 0).getTime();
|
||||
const timeB = new Date(b.createTime || 0).getTime();
|
||||
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
|
||||
} else if (sortBy === "memberCount") {
|
||||
return sortOrder === "asc" ? a.memberCount - b.memberCount : b.memberCount - a.memberCount;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleSortClick = (type: "createTime" | "memberCount") => {
|
||||
if (sortBy === type) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(type);
|
||||
setSortOrder("desc");
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIcon = (type: "createTime" | "memberCount") => {
|
||||
if (sortBy !== type) return <span style={{ fontSize: '12px' }}>⇅</span>;
|
||||
return sortOrder === "asc" ? <UpOutlined /> : <DownOutlined />;
|
||||
};
|
||||
|
||||
// 群组图标颜色配置
|
||||
const iconColors = [
|
||||
{ from: "#3b82f6", to: "#4f46e5" }, // blue to indigo
|
||||
{ from: "#6366f1", to: "#8b5cf6" }, // indigo to purple
|
||||
{ from: "#a855f7", to: "#ec4899" }, // purple to pink
|
||||
{ from: "#f472b6", to: "#f43f5e" }, // pink to rose
|
||||
{ from: "#fb923c", to: "#ef4444" }, // orange to red
|
||||
{ from: "#14b8a6", to: "#10b981" }, // teal to emerald
|
||||
{ from: "#22d3ee", to: "#3b82f6" }, // cyan to blue
|
||||
];
|
||||
|
||||
const getGroupIcon = (index: number) => {
|
||||
const colors = iconColors[index % iconColors.length];
|
||||
return { from: colors.from, to: colors.to };
|
||||
};
|
||||
|
||||
const getGroupIconEmoji = (group: Group, index: number) => {
|
||||
// 可以根据群组名称或其他属性返回不同的图标
|
||||
const icons = ["groups", "star", "fiber_new", "local_fire_department", "campaign", "forum", "school"];
|
||||
return icons[index % icons.length];
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="所有群组列表" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="所有群组列表"
|
||||
backFn={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 搜索和筛选区域 */}
|
||||
<div className={style.filterSection}>
|
||||
<div className={style.searchBox}>
|
||||
<SearchOutlined className={style.searchIcon} />
|
||||
<input
|
||||
className={style.searchInput}
|
||||
placeholder="搜索群名称、群ID、群主昵称..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.filterButtons}>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "all" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => {
|
||||
setSortBy("all");
|
||||
setSortOrder("desc");
|
||||
}}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "createTime" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => handleSortClick("createTime")}
|
||||
>
|
||||
创建时间
|
||||
<span className={style.filterIcon}>{getSortIcon("createTime")}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "memberCount" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => handleSortClick("memberCount")}
|
||||
>
|
||||
成员数量
|
||||
<span className={style.filterIcon}>{getSortIcon("memberCount")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 群组列表 */}
|
||||
<div className={style.groupsList}>
|
||||
{sortedGroups.length > 0 ? (
|
||||
sortedGroups.map((group, index) => {
|
||||
const iconColors = getGroupIcon(index);
|
||||
return (
|
||||
<div
|
||||
key={String(group.id)}
|
||||
className={style.groupCard}
|
||||
onClick={() => navigate(`/workspace/group-create/${planId}/groups/${String(group.id)}`)}
|
||||
>
|
||||
<div className={style.groupCardContent}>
|
||||
<div className={style.groupIconWrapper}>
|
||||
{group.avatar || group.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconImg}
|
||||
src={group.avatar || group.groupAvatar}
|
||||
alt={group.groupName || group.name || ""}
|
||||
onError={(e) => {
|
||||
// 加载失败时显示默认图标
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconFallback}
|
||||
style={{
|
||||
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
|
||||
}}
|
||||
>
|
||||
{(group.groupName || group.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.groupInfo}>
|
||||
<h4 className={style.groupName}>{group.groupName || group.name || `群组 ${index + 1}`}</h4>
|
||||
<div className={style.groupMeta}>
|
||||
<span className={style.memberCount}>
|
||||
👤 {group.memberCountText || `${group.memberCount || 0}人`}
|
||||
</span>
|
||||
<span className={style.createTime}>{group.createTime || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={style.emptyState}>
|
||||
<div className={style.emptyText}>暂无群组</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 无限滚动加载 */}
|
||||
{sortedGroups.length > 0 && (
|
||||
<InfiniteScroll
|
||||
loadMore={handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
threshold={100}
|
||||
>
|
||||
{hasMore ? (
|
||||
<div className={style.footer}>
|
||||
<SpinLoading style={{ "--size": "24px" }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.footer}>
|
||||
<p className={style.footerText}>已显示全部 {sortedGroups.length} 个群组</p>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupListPage;
|
||||
@@ -0,0 +1,482 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loadingContainer,
|
||||
.emptyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 基本信息卡片
|
||||
.infoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infoTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.infoSubtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.statusRunning {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.statusStopped {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.infoMeta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.metaItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metaLabel {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.metaValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.metaIcon {
|
||||
font-size: 16px;
|
||||
color: #9ca3af;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
.statsSection {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sectionTitleDot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statIconBlue {
|
||||
background: #dbeafe;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.statIconGreen {
|
||||
background: #d1fae5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.statIconPurple {
|
||||
background: #f3e8ff;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.statNumber {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
// 配置信息
|
||||
.configCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.configList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.configItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.configValue {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deviceIcon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.configDivider {
|
||||
height: 1px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.wechatTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wechatTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.wechatTagAdmin {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.adminBadge {
|
||||
margin-left: 4px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
border: 1px solid rgba(146, 64, 14, 0.3);
|
||||
background: rgba(254, 243, 199, 0.5);
|
||||
}
|
||||
|
||||
// 群列表
|
||||
.groupsSection {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.groupsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.groupsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.groupCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.groupCardHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.groupCardLeft {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.groupIconWrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupIconImg,
|
||||
.groupIconFallback {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.groupIconImg {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.groupIconFallback {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.groupName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.groupMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.groupMemberCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #f3f4f6;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.groupDate {
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.chevronIcon {
|
||||
font-size: 24px;
|
||||
color: #d1d5db;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupMembers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.memberAvatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.memberAvatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
margin-left: -10px;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memberMore {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
margin-left: -10px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.viewDetailText {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.emptyGroups {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyGroupsText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Button, Toast, SpinLoading } from "antd-mobile";
|
||||
import { EditOutlined, ScheduleOutlined, HistoryOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getGroupCreateDetail, getCreatedGroupsList } from "../form/api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface GroupCreateDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
planType: number; // 0-全局计划, 1-独立计划
|
||||
status: number; // 1-启用, 0-禁用
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
createdGroupsCount?: number; // 已建群数
|
||||
totalMembersCount?: number; // 总人数
|
||||
groupSizeMax?: number; // 群组最大人数
|
||||
config?: {
|
||||
deviceGroupsOptions?: Array<{
|
||||
id: number;
|
||||
nickname?: string;
|
||||
memo?: string;
|
||||
wechatId?: string;
|
||||
}>;
|
||||
wechatGroupsOptions?: Array<{
|
||||
id: number;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
groupAdminWechatId?: number;
|
||||
groupNameTemplate?: string;
|
||||
};
|
||||
groups?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
createTime: string;
|
||||
members?: Array<{
|
||||
id: string;
|
||||
avatar?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupCreateDetail: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detail, setDetail] = useState<GroupCreateDetail | null>(null);
|
||||
const [groups, setGroups] = useState<Array<{
|
||||
id: number | string;
|
||||
name?: string;
|
||||
groupName?: string;
|
||||
memberCount: number;
|
||||
memberCountText?: string;
|
||||
createTime: string;
|
||||
avatar?: string;
|
||||
groupAvatar?: string;
|
||||
memberAvatars?: Array<{
|
||||
avatar?: string;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
ownerNickname?: string;
|
||||
ownerWechatId?: string;
|
||||
ownerAvatar?: string;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchDetail();
|
||||
fetchGroups();
|
||||
}, [id]);
|
||||
|
||||
const fetchGroups = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await getCreatedGroupsList({
|
||||
workbenchId: id,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
const groupsData = res?.list || res?.data?.list || res?.data || [];
|
||||
setGroups(groupsData);
|
||||
} catch (e: any) {
|
||||
// 静默失败,不影响主详情展示
|
||||
console.error("获取群组列表失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getGroupCreateDetail(id!);
|
||||
const config = res.config || {};
|
||||
const stats = config.stats || {};
|
||||
|
||||
const detailData: GroupCreateDetail = {
|
||||
id: String(res.id),
|
||||
name: res.name || "",
|
||||
planType: config.planType ?? res.planType ?? 1,
|
||||
status: res.status ?? 1,
|
||||
createTime: res.createTime || "",
|
||||
updateTime: res.updateTime || res.createTime || "",
|
||||
createdGroupsCount: stats.createdGroupsCount ?? res.createdGroupsCount ?? 0,
|
||||
totalMembersCount: stats.totalMembersCount ?? res.totalMembersCount ?? 0,
|
||||
groupSizeMax: config.groupSizeMax || 38,
|
||||
config: {
|
||||
deviceGroupsOptions: config.deviceGroupsOptions || [],
|
||||
wechatGroupsOptions: config.wechatGroupsOptions || [],
|
||||
groupAdminWechatId: config.groupAdminWechatId,
|
||||
groupNameTemplate: config.groupNameTemplate || "",
|
||||
},
|
||||
groups: [], // 群列表通过单独的接口获取
|
||||
};
|
||||
setDetail(detailData);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取详情失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
navigate(`/workspace/group-create/${id}/edit`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyText}>未找到该计划</div>
|
||||
<Button color="primary" onClick={() => navigate("/workspace/group-create")}>
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const isRunning = detail.status === 1;
|
||||
const planTypeText = detail.planType === 0 ? "全局计划" : "独立计划";
|
||||
const executorDevice = detail.config?.deviceGroupsOptions?.[0];
|
||||
const executorName = executorDevice?.nickname || executorDevice?.memo || executorDevice?.wechatId || "-";
|
||||
const fixedWechatIds = detail.config?.wechatGroupsOptions || [];
|
||||
const groupAdminId = detail.config?.groupAdminWechatId;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="计划详情"
|
||||
backFn={() => navigate(-1)}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleEdit}
|
||||
style={{ marginRight: "-8px" }}
|
||||
>
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 基本信息卡片 */}
|
||||
<section className={style.infoCard}>
|
||||
<div className={style.infoHeader}>
|
||||
<div>
|
||||
<h2 className={style.infoTitle}>{detail.name}</h2>
|
||||
<p className={style.infoSubtitle}>{planTypeText}</p>
|
||||
</div>
|
||||
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
|
||||
{isRunning && <span className={style.statusDot}></span>}
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.infoMeta}>
|
||||
<div className={style.metaItem}>
|
||||
<span className={style.metaLabel}>创建时间</span>
|
||||
<div className={style.metaValue}>
|
||||
<ScheduleOutlined className={style.metaIcon} />
|
||||
{detail.createTime || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.metaItem}>
|
||||
<span className={style.metaLabel}>更新时间</span>
|
||||
<div className={style.metaValue}>
|
||||
<HistoryOutlined className={style.metaIcon} />
|
||||
{detail.updateTime || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<section className={style.statsSection}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
统计信息
|
||||
</h3>
|
||||
<div className={style.statsGrid}>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconBlue}`}>
|
||||
👥
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.createdGroupsCount || 0}</span>
|
||||
<span className={style.statLabel}>已建群数</span>
|
||||
</div>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconGreen}`}>
|
||||
👥
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.totalMembersCount || 0}</span>
|
||||
<span className={style.statLabel}>总人数</span>
|
||||
</div>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconPurple}`}>
|
||||
📊
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.groupSizeMax || 38}</span>
|
||||
<span className={style.statLabel}>人/群</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 配置信息 */}
|
||||
<section className={style.configCard}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
配置信息
|
||||
</h3>
|
||||
<div className={style.configList}>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>分组方式</span>
|
||||
<span className={style.configValue}>所有好友自动分组</span>
|
||||
</div>
|
||||
<div className={style.configDivider}></div>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>执行设备</span>
|
||||
<div className={style.configValue}>
|
||||
<span className={style.deviceIcon}>📱</span>
|
||||
{executorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.configDivider}></div>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>固定微信号</span>
|
||||
<div className={style.wechatTags}>
|
||||
{fixedWechatIds.length > 0 ? (
|
||||
fixedWechatIds.map((wechat: any) => {
|
||||
const isGroupAdmin = wechat.id === groupAdminId;
|
||||
const displayText = wechat.nickname && wechat.wechatId
|
||||
? `${wechat.nickname}(${wechat.wechatId})`
|
||||
: wechat.wechatId || wechat.nickname || "-";
|
||||
return (
|
||||
<span
|
||||
key={wechat.id}
|
||||
className={`${style.wechatTag} ${isGroupAdmin ? style.wechatTagAdmin : ""}`}
|
||||
>
|
||||
{displayText}
|
||||
{isGroupAdmin && (
|
||||
<span className={style.adminBadge}>群主</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className={style.configValue}>-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 群列表 */}
|
||||
<section className={style.groupsSection}>
|
||||
<div className={style.groupsHeader}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
群列表
|
||||
</h3>
|
||||
<button
|
||||
className={style.viewAllButton}
|
||||
onClick={() => navigate(`/workspace/group-create/${id}/groups`)}
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
<div className={style.groupsList}>
|
||||
{groups.length > 0 ? (
|
||||
groups.map((group, index) => (
|
||||
<div
|
||||
key={String(group.id)}
|
||||
className={style.groupCard}
|
||||
onClick={() => navigate(`/workspace/group-create/${id}/groups/${String(group.id)}`)}
|
||||
>
|
||||
<div className={style.groupCardHeader}>
|
||||
<div className={style.groupCardLeft}>
|
||||
<div className={style.groupIconWrapper}>
|
||||
{group.avatar || group.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconImg}
|
||||
src={group.avatar || group.groupAvatar}
|
||||
alt={group.groupName || group.name || ""}
|
||||
onError={(e) => {
|
||||
// 加载失败时显示默认图标
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconFallback}
|
||||
style={{
|
||||
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${
|
||||
['#60a5fa', '#818cf8', '#a78bfa', '#f472b6'][index % 4]
|
||||
}, ${
|
||||
['#4f46e5', '#6366f1', '#8b5cf6', '#ec4899'][index % 4]
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
{(group.groupName || group.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className={style.groupName}>{group.groupName || group.name || detail.config?.groupNameTemplate || `群组 ${index + 1}`}</h4>
|
||||
<div className={style.groupMeta}>
|
||||
<span className={style.groupMemberCount}>
|
||||
👤 {group.memberCountText || `${group.memberCount || 0}人`}
|
||||
</span>
|
||||
<span className={style.groupDate}>{group.createTime || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={style.chevronIcon}>›</span>
|
||||
</div>
|
||||
{group.memberAvatars && group.memberAvatars.length > 0 && (
|
||||
<div className={style.groupMembers}>
|
||||
<div className={style.memberAvatars}>
|
||||
{group.memberAvatars.slice(0, 6).map((member, memberIndex) => (
|
||||
<img
|
||||
key={member.wechatId || memberIndex}
|
||||
className={style.memberAvatar}
|
||||
src={member.avatar || "https://via.placeholder.com/24"}
|
||||
alt={member.nickname || ""}
|
||||
style={{ zIndex: 6 - memberIndex }}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "https://via.placeholder.com/24";
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{group.memberAvatars.length > 6 && (
|
||||
<span className={style.memberMore}>
|
||||
+{group.memberAvatars.length - 6}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={style.viewDetailText}>点击查看详情</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={style.emptyGroups}>
|
||||
<div className={style.emptyGroupsText}>暂无群组</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupCreateDetail;
|
||||
@@ -16,3 +16,59 @@ export const getGroupCreateDetail = (id: string) =>
|
||||
export const getGroupCreateList = (params: any) =>
|
||||
request("/v1/workbench/list", params, "GET");
|
||||
|
||||
// 删除自动建群任务
|
||||
export const deleteGroupCreate = (id: string) =>
|
||||
request("/v1/workbench/delete", { id }, "DELETE");
|
||||
|
||||
// 切换任务状态
|
||||
export const toggleGroupCreateStatus = (data: { id: string | number; status: number }) =>
|
||||
request("/v1/workbench/update-status", { ...data }, "POST");
|
||||
|
||||
// 获取群列表
|
||||
export const getCreatedGroupsList = (params: {
|
||||
workbenchId: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
}) => {
|
||||
// 如果没有指定 limit,默认使用 20
|
||||
const finalParams = {
|
||||
...params,
|
||||
limit: params.limit ?? 20,
|
||||
};
|
||||
return request("/v1/workbench/created-groups-list", finalParams, "GET");
|
||||
};
|
||||
|
||||
// 获取群详情
|
||||
export const getCreatedGroupDetail = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/created-group-detail", params, "GET");
|
||||
};
|
||||
|
||||
// 同步群信息
|
||||
export const syncGroupInfo = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/sync-group-info", params, "POST");
|
||||
};
|
||||
|
||||
// 修改群名称/群公告
|
||||
export const modifyGroupInfo = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
chatroomName?: string;
|
||||
announce?: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/modify-group-info", params, "POST");
|
||||
};
|
||||
|
||||
// 退出群组
|
||||
export const quitGroup = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/quit-group", params, "POST");
|
||||
};
|
||||
|
||||
@@ -692,3 +692,9 @@
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.statusSwitchContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -37,12 +37,13 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
onChange({
|
||||
executor: selectedDevice,
|
||||
executorId: selectedDevice.id,
|
||||
groupName: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
|
||||
groupNameTemplate: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
executor: undefined,
|
||||
executorId: undefined,
|
||||
groupNameTemplate: "", // 清空群名称
|
||||
});
|
||||
}
|
||||
setExecutorSelectionVisible(false);
|
||||
@@ -51,7 +52,7 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
// 处理固定微信号选择(必须3个)
|
||||
const handleFixedWechatIdsSelect = (friends: FriendSelectionItem[]) => {
|
||||
// 检查总数是否超过3个(包括已添加的手动微信号)
|
||||
const currentManualCount = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual).length;
|
||||
const currentManualCount = (formData.wechatGroupsOptions || []).filter(f => f.isManual).length;
|
||||
const newSelectedCount = friends.length;
|
||||
if (currentManualCount + newSelectedCount > 3) {
|
||||
Toast.show({ content: "固定微信号最多只能选择3个", position: "top" });
|
||||
@@ -60,10 +61,10 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
// 标记为选择的(非手动添加),确保所有从选择弹窗来的都标记为非手动
|
||||
const selectedFriends = friends.map(f => ({ ...f, isManual: false }));
|
||||
// 合并已添加的手动微信号和新的选择
|
||||
const manualFriends = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
|
||||
const manualFriends = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
|
||||
onChange({
|
||||
fixedWechatIds: [...manualFriends, ...selectedFriends].map(f => f.id),
|
||||
fixedWechatIdsOptions: [...manualFriends, ...selectedFriends],
|
||||
wechatGroups: [...manualFriends, ...selectedFriends].map(f => f.id),
|
||||
wechatGroupsOptions: [...manualFriends, ...selectedFriends],
|
||||
});
|
||||
setFixedWechatIdsSelectionVisible(false);
|
||||
};
|
||||
@@ -77,6 +78,27 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
setFixedWechatIdsSelectionVisible(true);
|
||||
};
|
||||
|
||||
// 打开群管理员选择弹窗
|
||||
const handleOpenGroupAdminSelection = () => {
|
||||
if (selectedWechatIds.length === 0) {
|
||||
Toast.show({
|
||||
content: manualAddedWechatIds.length > 0
|
||||
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
|
||||
: "请先选择固定微信号",
|
||||
position: "top"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 如果当前选择的群管理员是手动添加的,清空选择
|
||||
if (formData.groupAdminWechatIdOption && formData.groupAdminWechatIdOption.isManual) {
|
||||
onChange({
|
||||
groupAdminWechatIdOption: undefined,
|
||||
groupAdminWechatId: undefined,
|
||||
});
|
||||
}
|
||||
setGroupAdminSelectionVisible(true);
|
||||
};
|
||||
|
||||
// 处理群管理员选择(单选)
|
||||
const handleGroupAdminSelect = (friends: FriendSelectionItem[]) => {
|
||||
if (friends.length > 0) {
|
||||
@@ -99,12 +121,12 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
Toast.show({ content: "请输入微信号", position: "top" });
|
||||
return;
|
||||
}
|
||||
const existingIds = formData.fixedWechatIdsOptions.map(f => f.wechatId.toLowerCase());
|
||||
const existingIds = formData.wechatGroupsOptions.map(f => f.wechatId.toLowerCase());
|
||||
if (existingIds.includes(manualWechatIdInput.trim().toLowerCase())) {
|
||||
Toast.show({ content: "该微信号已添加", position: "top" });
|
||||
return;
|
||||
}
|
||||
if (formData.fixedWechatIdsOptions.length >= 3) {
|
||||
if (formData.wechatGroupsOptions.length >= 3) {
|
||||
Toast.show({ content: "固定微信号最多只能添加3个", position: "top" });
|
||||
return;
|
||||
}
|
||||
@@ -117,18 +139,26 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
isManual: true, // 标记为手动添加
|
||||
};
|
||||
onChange({
|
||||
fixedWechatIds: [...formData.fixedWechatIds, newFriend.id],
|
||||
fixedWechatIdsOptions: [...formData.fixedWechatIdsOptions, newFriend],
|
||||
wechatGroups: [...formData.wechatGroups, newFriend.id],
|
||||
wechatGroupsOptions: [...formData.wechatGroupsOptions, newFriend],
|
||||
});
|
||||
setManualWechatIdInput("");
|
||||
};
|
||||
|
||||
// 移除固定微信号
|
||||
const handleRemoveFixedWechatId = (id: number) => {
|
||||
onChange({
|
||||
fixedWechatIds: formData.fixedWechatIds.filter(fid => fid !== id),
|
||||
fixedWechatIdsOptions: formData.fixedWechatIdsOptions.filter(f => f.id !== id),
|
||||
});
|
||||
const removedFriend = formData.wechatGroupsOptions.find(f => f.id === id);
|
||||
const newOptions = formData.wechatGroupsOptions.filter(f => f.id !== id);
|
||||
const updateData: Partial<GroupCreateFormData> = {
|
||||
wechatGroups: formData.wechatGroups.filter(fid => fid !== id),
|
||||
wechatGroupsOptions: newOptions,
|
||||
};
|
||||
// 如果移除的是群管理员,也要清除群管理员设置
|
||||
if (formData.groupAdminWechatId === id) {
|
||||
updateData.groupAdminWechatId = undefined;
|
||||
updateData.groupAdminWechatIdOption = undefined;
|
||||
}
|
||||
onChange(updateData);
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -144,14 +174,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
return false;
|
||||
}
|
||||
// 固定微信号不是必填的,移除验证
|
||||
if (!formData.groupName?.trim()) {
|
||||
Toast.show({ content: "请输入群名称", position: "top" });
|
||||
if (!formData.groupNameTemplate?.trim()) {
|
||||
Toast.show({ content: "请输入群名称模板", position: "top" });
|
||||
return false;
|
||||
}
|
||||
// 群名称长度验证(2-100个字符)
|
||||
const groupNameLength = formData.groupName.trim().length;
|
||||
if (groupNameLength < 2 || groupNameLength > 100) {
|
||||
Toast.show({ content: "群名称长度应在2-100个字符之间", position: "top" });
|
||||
// 群名称模板长度验证(2-100个字符)
|
||||
const groupNameTemplateLength = formData.groupNameTemplate.trim().length;
|
||||
if (groupNameTemplateLength < 2 || groupNameTemplateLength > 100) {
|
||||
Toast.show({ content: "群名称模板长度应在2-100个字符之间", position: "top" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -163,8 +193,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
|
||||
// 区分已选择的微信号(从下拉选择)和已添加的微信号(手动输入)
|
||||
// 如果 isManual 未定义,默认为 false(即选择的)
|
||||
const selectedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => !f.isManual);
|
||||
const manualAddedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
|
||||
const selectedWechatIds = (formData.wechatGroupsOptions || []).filter(f => !f.isManual);
|
||||
const manualAddedWechatIds = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
@@ -355,14 +385,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
</div>
|
||||
{formData.groupAdminEnabled && (
|
||||
<div
|
||||
className={`${style.wechatSelectInput} ${(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? style.disabled : ''}`}
|
||||
onClick={() => {
|
||||
if ((selectedWechatIds.length + manualAddedWechatIds.length) === 0) {
|
||||
Toast.show({ content: "请先选择固定微信号", position: "top" });
|
||||
return;
|
||||
}
|
||||
setGroupAdminSelectionVisible(true);
|
||||
}}
|
||||
className={`${style.wechatSelectInput} ${selectedWechatIds.length === 0 ? style.disabled : ''}`}
|
||||
onClick={handleOpenGroupAdminSelection}
|
||||
>
|
||||
<div className={style.selectInputWrapper}>
|
||||
<span className={style.selectInputPlaceholder}>
|
||||
@@ -383,8 +407,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="text"
|
||||
className={style.input}
|
||||
value={formData.groupName}
|
||||
onChange={e => onChange({ groupName: e.target.value })}
|
||||
value={formData.groupNameTemplate}
|
||||
onChange={e => onChange({ groupNameTemplate: e.target.value })}
|
||||
placeholder="请输入群名称"
|
||||
/>
|
||||
</div>
|
||||
@@ -448,8 +472,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="time"
|
||||
className={style.timeInput}
|
||||
value={formData.executeTime || "09:00"}
|
||||
onChange={e => onChange({ executeTime: e.target.value || "09:00" })}
|
||||
value={formData.startTime || "09:00"}
|
||||
onChange={e => onChange({ startTime: e.target.value || "09:00" })}
|
||||
/>
|
||||
<ClockCircleOutlined className={style.timeIcon} />
|
||||
</div>
|
||||
@@ -458,21 +482,25 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="time"
|
||||
className={style.timeInput}
|
||||
value={formData.executeEndTime || "18:00"}
|
||||
onChange={e => onChange({ executeEndTime: e.target.value || "18:00" })}
|
||||
value={formData.endTime || "21:00"}
|
||||
onChange={e => onChange({ endTime: e.target.value || "21:00" })}
|
||||
/>
|
||||
<ClockCircleOutlined className={style.timeIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{(selectedWechatIds.length + manualAddedWechatIds.length) !== 3 && (
|
||||
<div className={style.errorTip}>
|
||||
<ExclamationCircleOutlined className={style.errorIcon} />
|
||||
<span className={style.errorText}>固定微信号必须选择3个</span>
|
||||
{/* 是否启用 */}
|
||||
<div className={style.card}>
|
||||
<div className={style.statusSwitchContainer}>
|
||||
<label className={style.label} style={{ marginBottom: 0 }}>是否启用</label>
|
||||
<Switch
|
||||
checked={formData.status === 1}
|
||||
onChange={(checked) => onChange({ status: checked ? 1 : 0 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 隐藏的选择组件 */}
|
||||
<div style={{ display: "none" }}>
|
||||
@@ -518,12 +546,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||
{(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? (
|
||||
{selectedWechatIds.length === 0 ? (
|
||||
<div style={{ textAlign: "center", padding: "40px", color: "#94a3b8" }}>
|
||||
暂无固定微信号可选
|
||||
{manualAddedWechatIds.length > 0
|
||||
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
|
||||
: "暂无固定微信号可选"}
|
||||
</div>
|
||||
) : (
|
||||
[...selectedWechatIds, ...manualAddedWechatIds].map(friend => {
|
||||
selectedWechatIds.map(friend => {
|
||||
const isSelected = formData.groupAdminWechatIdOption?.id === friend.id;
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -10,25 +10,25 @@ export interface DeviceSelectionStepRef {
|
||||
}
|
||||
|
||||
interface DeviceSelectionStepProps {
|
||||
selectedDevices?: DeviceSelectionItem[];
|
||||
selectedDeviceIds?: number[];
|
||||
deviceGroupsOptions?: DeviceSelectionItem[];
|
||||
deviceGroups?: number[];
|
||||
onSelect: (devices: DeviceSelectionItem[]) => void;
|
||||
}
|
||||
|
||||
const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionStepProps>(
|
||||
({ selectedDevices = [], selectedDeviceIds = [], onSelect }, ref) => {
|
||||
({ deviceGroupsOptions = [], deviceGroups = [], onSelect }, ref) => {
|
||||
const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false);
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate: async () => {
|
||||
if (selectedDeviceIds.length === 0) {
|
||||
if (deviceGroups.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getValues: () => {
|
||||
return { selectedDevices, selectedDeviceIds };
|
||||
return { deviceGroupsOptions, deviceGroups };
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -48,10 +48,10 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
className={style.deviceSelector}
|
||||
onClick={() => setDeviceSelectionVisible(true)}
|
||||
>
|
||||
{selectedDevices.length > 0 ? (
|
||||
{deviceGroupsOptions.length > 0 ? (
|
||||
<div className={style.selectedDevicesInfo}>
|
||||
<span className={style.selectedCountText}>
|
||||
已选择 {selectedDevices.length} 个设备
|
||||
已选择 {deviceGroupsOptions.length} 个设备
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -64,9 +64,9 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
</div>
|
||||
|
||||
{/* 已选设备列表 */}
|
||||
{selectedDevices.length > 0 && (
|
||||
{deviceGroupsOptions.length > 0 && (
|
||||
<div className={style.selectedDevicesGrid}>
|
||||
{selectedDevices.map((device) => (
|
||||
{deviceGroupsOptions.map((device) => (
|
||||
<div key={device.id} className={style.selectedDeviceCard}>
|
||||
<div className={style.deviceCardHeader}>
|
||||
<div className={`${style.deviceCardIcon} ${device.status === "online" ? style.deviceIconOnline : style.deviceIconOffline}`}>
|
||||
@@ -88,8 +88,8 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
className={style.deviceCardDeleteButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newSelectedDevices = selectedDevices.filter(d => d.id !== device.id);
|
||||
onSelect(newSelectedDevices);
|
||||
const newDeviceGroupsOptions = deviceGroupsOptions.filter(d => d.id !== device.id);
|
||||
onSelect(newDeviceGroupsOptions);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined className={style.deviceCardDeleteIcon} />
|
||||
@@ -102,7 +102,7 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
{/* 设备选择弹窗 */}
|
||||
<div style={{ display: "none" }}>
|
||||
<DeviceSelection
|
||||
selectedOptions={selectedDevices}
|
||||
selectedOptions={deviceGroupsOptions}
|
||||
onSelect={handleDeviceSelect}
|
||||
placeholder="选择设备"
|
||||
showInput={false}
|
||||
|
||||
@@ -19,24 +19,22 @@ const steps = [
|
||||
|
||||
const defaultForm: GroupCreateFormData = {
|
||||
planType: 1, // 默认独立计划
|
||||
name: "新建群计划",
|
||||
name: "",
|
||||
executorId: undefined,
|
||||
executor: undefined,
|
||||
selectedDevices: [],
|
||||
selectedDeviceIds: [],
|
||||
fixedWechatIds: [],
|
||||
fixedWechatIdsOptions: [],
|
||||
deviceGroupsOptions: [],
|
||||
deviceGroups: [],
|
||||
wechatGroups: [],
|
||||
wechatGroupsOptions: [],
|
||||
groupAdminEnabled: false,
|
||||
groupAdminWechatId: undefined,
|
||||
groupAdminWechatIdOption: undefined,
|
||||
groupName: "卡若001",
|
||||
groupMethod: 0, // 默认所有好友自动分组
|
||||
maxGroupsPerDay: 10,
|
||||
groupNameTemplate: "",
|
||||
maxGroupsPerDay: 20,
|
||||
groupSizeMin: 3,
|
||||
groupSizeMax: 38,
|
||||
executeType: 1, // 默认定时执行
|
||||
executeTime: "09:00", // 默认开始时间
|
||||
executeEndTime: "18:00", // 默认结束时间
|
||||
startTime: "09:00", // 默认开始时间
|
||||
endTime: "21:00", // 默认结束时间
|
||||
poolGroups: [],
|
||||
poolGroupsOptions: [],
|
||||
status: 1, // 默认启用
|
||||
@@ -61,30 +59,41 @@ const GroupCreateForm: React.FC = () => {
|
||||
// 获取详情并回填表单
|
||||
getGroupCreateDetail(id)
|
||||
.then(res => {
|
||||
const config = res.config || {};
|
||||
// 转换 deviceGroups 从字符串数组到数字数组
|
||||
const deviceGroups = (config.deviceGroups || []).map((id: string | number) => Number(id));
|
||||
// 转换 poolGroups 保持字符串数组
|
||||
const poolGroups = (config.poolGroups || []).map((id: string | number) => String(id));
|
||||
// 转换 wechatGroups 到数字数组
|
||||
const wechatGroups = (config.wechatGroups || []).map((id: string | number) => Number(id));
|
||||
|
||||
// 查找群管理员选项(如果有)
|
||||
const groupAdminWechatIdOption = config.groupAdminWechatId && config.wechatGroupsOptions
|
||||
? config.wechatGroupsOptions.find((f: any) => f.id === config.groupAdminWechatId)
|
||||
: undefined;
|
||||
|
||||
const updatedForm: GroupCreateFormData = {
|
||||
...defaultForm,
|
||||
id: res.id,
|
||||
planType: res.planType ?? 1,
|
||||
id: String(res.id),
|
||||
planType: config.planType ?? res.planType ?? 1,
|
||||
name: res.name ?? "",
|
||||
executorId: res.executorId,
|
||||
executor: res.executor,
|
||||
selectedDevices: res.selectedDevices || [],
|
||||
selectedDeviceIds: res.selectedDeviceIds || [],
|
||||
fixedWechatIds: res.fixedWechatIds || [],
|
||||
fixedWechatIdsOptions: res.fixedWechatIdsOptions || [],
|
||||
groupAdminEnabled: res.groupAdminEnabled ?? false,
|
||||
groupAdminWechatId: res.groupAdminWechatId,
|
||||
groupAdminWechatIdOption: res.groupAdminWechatIdOption,
|
||||
groupName: res.groupName ?? "",
|
||||
groupMethod: res.groupMethod ?? 0,
|
||||
maxGroupsPerDay: res.maxGroupsPerDay ?? 10,
|
||||
groupSizeMin: res.groupSizeMin ?? 3,
|
||||
groupSizeMax: res.groupSizeMax ?? 38,
|
||||
executeType: res.executeType ?? 1,
|
||||
executeTime: res.executeTime ?? "09:00",
|
||||
executeEndTime: res.executeEndTime ?? "18:00",
|
||||
poolGroups: res.poolGroups || [],
|
||||
poolGroupsOptions: res.poolGroupsOptions || [],
|
||||
executorId: config.executorId,
|
||||
executor: config.deviceGroupsOptions?.[0], // executor 使用第一个设备(如果需要)
|
||||
deviceGroupsOptions: config.deviceGroupsOptions || [],
|
||||
deviceGroups: deviceGroups,
|
||||
wechatGroups: wechatGroups,
|
||||
wechatGroupsOptions: config.wechatGroupsOptions || config.wechatFriendsOptions || [],
|
||||
groupAdminEnabled: config.groupAdminEnabled === 1,
|
||||
groupAdminWechatId: config.groupAdminWechatId || undefined,
|
||||
groupAdminWechatIdOption: groupAdminWechatIdOption,
|
||||
groupNameTemplate: config.groupNameTemplate || "",
|
||||
maxGroupsPerDay: config.maxGroupsPerDay ?? 20,
|
||||
groupSizeMin: config.groupSizeMin ?? 3,
|
||||
groupSizeMax: config.groupSizeMax ?? 38,
|
||||
startTime: config.startTime || "09:00",
|
||||
endTime: config.endTime || "21:00",
|
||||
poolGroups: poolGroups,
|
||||
poolGroupsOptions: config.poolGroupsOptions || [],
|
||||
status: res.status ?? 1,
|
||||
};
|
||||
setFormData(updatedForm);
|
||||
@@ -163,9 +172,7 @@ const GroupCreateForm: React.FC = () => {
|
||||
await createGroupCreate(submitData);
|
||||
Toast.show({ content: "创建成功" });
|
||||
}
|
||||
// 注意:需要导航到列表页面,但目前列表页面还未创建
|
||||
// navigate("/workspace/group-create");
|
||||
navigate("/workspace");
|
||||
navigate("/workspace/group-create");
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "提交失败" });
|
||||
} finally {
|
||||
@@ -194,13 +201,13 @@ const GroupCreateForm: React.FC = () => {
|
||||
return (
|
||||
<DeviceSelectionStep
|
||||
ref={deviceSelectionStepRef}
|
||||
selectedDevices={formData.selectedDevices || []}
|
||||
selectedDeviceIds={formData.selectedDeviceIds || []}
|
||||
deviceGroupsOptions={formData.deviceGroupsOptions || []}
|
||||
deviceGroups={formData.deviceGroups || []}
|
||||
onSelect={(devices) => {
|
||||
const deviceIds = devices.map(d => d.id);
|
||||
handleFormDataChange({
|
||||
selectedDevices: devices,
|
||||
selectedDeviceIds: deviceIds,
|
||||
deviceGroupsOptions: devices,
|
||||
deviceGroups: deviceIds,
|
||||
// 如果只有一个设备,也设置 executor 和 executorId 用于兼容
|
||||
executor: devices.length === 1 ? devices[0] : undefined,
|
||||
executorId: devices.length === 1 ? devices[0].id : undefined,
|
||||
|
||||
@@ -9,21 +9,19 @@ export interface GroupCreateFormData {
|
||||
name: string; // 计划名称
|
||||
executor?: DeviceSelectionItem; // 执行智能体(执行者)- 单个设备(保留用于兼容)
|
||||
executorId?: number; // 执行智能体ID(设备ID)(保留用于兼容)
|
||||
selectedDevices?: DeviceSelectionItem[]; // 选中的设备列表
|
||||
selectedDeviceIds?: number[]; // 选中的设备ID列表
|
||||
fixedWechatIds: number[]; // 固定微信号ID列表(必须3个)
|
||||
fixedWechatIdsOptions: FriendSelectionItem[]; // 固定微信号选项
|
||||
deviceGroupsOptions?: DeviceSelectionItem[]; // 选中的设备列表
|
||||
deviceGroups?: number[]; // 选中的设备ID列表
|
||||
wechatGroups: number[]; // 固定微信号ID列表(必须3个)
|
||||
wechatGroupsOptions: FriendSelectionItem[]; // 固定微信号选项
|
||||
groupAdminEnabled: boolean; // 群管理员开关
|
||||
groupAdminWechatId?: number; // 群管理员微信号ID
|
||||
groupAdminWechatIdOption?: FriendSelectionItem; // 群管理员微信号选项
|
||||
groupName: string; // 群名称
|
||||
groupMethod: number; // 分组方式:0-所有好友自动分组,1-指定群数量
|
||||
maxGroupsPerDay: number; // 每日最大建群数(当groupMethod为1时使用)
|
||||
groupNameTemplate: string; // 群名称模板
|
||||
maxGroupsPerDay: number; // 每日最大建群数
|
||||
groupSizeMin: number; // 群组最小人数
|
||||
groupSizeMax: number; // 群组最大人数
|
||||
executeType: number; // 执行类型:0-立即执行,1-定时执行
|
||||
executeTime?: string; // 执行开始时间(HH:mm),默认 09:00
|
||||
executeEndTime?: string; // 执行结束时间(HH:mm),默认 18:00
|
||||
startTime?: string; // 执行开始时间(HH:mm),默认 09:00
|
||||
endTime?: string; // 执行结束时间(HH:mm),默认 21:00
|
||||
poolGroups?: string[]; // 流量池ID列表
|
||||
poolGroupsOptions?: PoolSelectionItem[]; // 流量池选项列表
|
||||
status: number; // 是否启用 (1: 启用, 0: 禁用)
|
||||
@@ -44,7 +42,7 @@ export const formValidationRules = {
|
||||
{ min: 2, max: 50, message: "计划名称长度应在2-50个字符之间" },
|
||||
],
|
||||
executorId: [{ required: true, message: "请选择执行智能体" }],
|
||||
fixedWechatIds: [
|
||||
wechatGroups: [
|
||||
{ required: true, message: "请选择固定微信号" },
|
||||
{
|
||||
validator: (_: any, value: number[]) => {
|
||||
@@ -55,9 +53,9 @@ export const formValidationRules = {
|
||||
},
|
||||
},
|
||||
],
|
||||
groupName: [
|
||||
{ required: true, message: "请输入群名称" },
|
||||
{ min: 2, max: 100, message: "群名称长度应在2-100个字符之间" },
|
||||
groupNameTemplate: [
|
||||
{ required: true, message: "请输入群名称模板" },
|
||||
{ min: 2, max: 100, message: "群名称模板长度应在2-100个字符之间" },
|
||||
],
|
||||
maxGroupsPerDay: [
|
||||
{ required: true, message: "请输入每日最大建群数" },
|
||||
@@ -76,28 +74,4 @@ export const formValidationRules = {
|
||||
{ required: true, message: "请输入群组最大人数" },
|
||||
{ type: "number", min: 1, max: 500, message: "群组最大人数应在1-500之间" },
|
||||
],
|
||||
executeDate: [
|
||||
{
|
||||
validator: (_: any, value: string, callback: any) => {
|
||||
// 如果执行类型是定时执行,则必填
|
||||
const executeType = callback?.executeType ?? 1;
|
||||
if (executeType === 1 && !value) {
|
||||
return Promise.reject(new Error("请选择执行日期"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
executeTime: [
|
||||
{
|
||||
validator: (_: any, value: string, callback: any) => {
|
||||
// 如果执行类型是定时执行,则必填
|
||||
const executeType = callback?.executeType ?? 1;
|
||||
if (executeType === 1 && !value) {
|
||||
return Promise.reject(new Error("请选择执行时间"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f8fafc;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
.infoBox {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: #2563eb;
|
||||
font-size: 20px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoText {
|
||||
font-size: 13px;
|
||||
color: #1e40af;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sectionDot {
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: #2563eb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 8px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.sectionTitleIndependent {
|
||||
.sectionDot {
|
||||
background: #fb923c;
|
||||
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.sectionDotIndependent {
|
||||
background: #fb923c;
|
||||
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
|
||||
}
|
||||
|
||||
.planList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.planCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: linear-gradient(to bottom right, rgba(239, 246, 255, 0.5), transparent);
|
||||
border-radius: 0 0 0 80px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cardTitleSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.statusRunning {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border-color: rgba(22, 101, 52, 0.1);
|
||||
}
|
||||
|
||||
.statusStopped {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border-color: rgba(100, 116, 139, 0.1);
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statusSwitch {
|
||||
--checked-color: #2563eb;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardStats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 4px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statNumber {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
font-family: "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
.statDivider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.cardDetails {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.detailTime {
|
||||
margin-left: auto;
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.emptyButton {
|
||||
min-width: 140px;
|
||||
}
|
||||
344
Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx
Normal file
344
Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Toast, Switch, Dialog } from "antd-mobile";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import { MoreOutlined, EditOutlined, DeleteOutlined, PlusOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getGroupCreateList, toggleGroupCreateStatus, deleteGroupCreate } from "../form/api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface GroupCreatePlan {
|
||||
id: string;
|
||||
name: string;
|
||||
planType: number; // 0-全局计划, 1-独立计划
|
||||
status: number; // 1-启用, 0-禁用
|
||||
groupNameTemplate?: string;
|
||||
groupSizeMax?: number;
|
||||
groupSizeMin?: number;
|
||||
updateTime?: string;
|
||||
createTime?: string;
|
||||
createdGroupsCount?: number; // 已建群数
|
||||
totalMembersCount?: number; // 总人数
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupCreateList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [plans, setPlans] = useState<GroupCreatePlan[]>([]);
|
||||
const [menuLoadingId, setMenuLoadingId] = useState<string | null>(null);
|
||||
|
||||
// 获取列表数据
|
||||
const fetchList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getGroupCreateList({ type: 4 });
|
||||
const list = res?.list || res?.data?.list || res?.data || [];
|
||||
const normalized: GroupCreatePlan[] = (list as any[]).map((item: any) => {
|
||||
const stats = item.config?.stats || {};
|
||||
return {
|
||||
id: String(item.id),
|
||||
name: item.name || "",
|
||||
planType: item.config?.planType ?? item.planType ?? 1,
|
||||
status: item.status === 1 ? 1 : 0,
|
||||
groupNameTemplate: item.config?.groupNameTemplate || item.groupNameTemplate || item.groupName || "",
|
||||
groupSizeMax: item.config?.groupSizeMax || item.groupSizeMax || 38,
|
||||
groupSizeMin: item.config?.groupSizeMin || item.groupSizeMin || 3,
|
||||
updateTime: item.updateTime || item.createTime || "",
|
||||
createTime: item.createTime || "",
|
||||
createdGroupsCount: stats.createdGroupsCount ?? item.createdGroupsCount ?? 0,
|
||||
totalMembersCount: stats.totalMembersCount ?? item.totalMembersCount ?? 0,
|
||||
};
|
||||
});
|
||||
setPlans(normalized);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取列表失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, []);
|
||||
|
||||
// 切换状态
|
||||
const handleStatusToggle = async (plan: GroupCreatePlan) => {
|
||||
try {
|
||||
const newStatus = plan.status === 1 ? 0 : 1;
|
||||
await toggleGroupCreateStatus({
|
||||
id: plan.id,
|
||||
status: newStatus,
|
||||
});
|
||||
Toast.show({ content: newStatus === 1 ? "已启用" : "已停止", position: "top" });
|
||||
// 直接更新本地状态
|
||||
setPlans(prev =>
|
||||
prev.map(p => (p.id === plan.id ? { ...p, status: newStatus } : p))
|
||||
);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "操作失败", position: "top" });
|
||||
}
|
||||
};
|
||||
|
||||
// 删除计划
|
||||
const handleDelete = async (plan: GroupCreatePlan) => {
|
||||
const result = await Dialog.confirm({
|
||||
content: "确定要删除该计划吗?",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
setMenuLoadingId(plan.id);
|
||||
try {
|
||||
await deleteGroupCreate(plan.id);
|
||||
Toast.show({ content: "删除成功", position: "top" });
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "删除失败", position: "top" });
|
||||
} finally {
|
||||
setMenuLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 菜单点击
|
||||
const handleMenuClick = ({ key }: { key: string }, plan: GroupCreatePlan) => {
|
||||
if (key === "detail") {
|
||||
navigate(`/workspace/group-create/${plan.id}`);
|
||||
} else if (key === "edit") {
|
||||
navigate(`/workspace/group-create/${plan.id}/edit`);
|
||||
} else if (key === "delete") {
|
||||
handleDelete(plan);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新计划
|
||||
const handleCreate = () => {
|
||||
navigate("/workspace/group-create/new");
|
||||
};
|
||||
|
||||
// 分隔全局计划和独立计划
|
||||
const globalPlans = plans.filter(p => p.planType === 0);
|
||||
const independentPlans = plans.filter(p => p.planType === 1);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="自动建群"
|
||||
right={
|
||||
<div style={{ marginRight: "-16px" }}>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleCreate}
|
||||
>
|
||||
<PlusOutlined /> 新建任务
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
loading={loading}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 全局计划提示 */}
|
||||
{globalPlans.length > 0 && (
|
||||
<div className={style.infoBox}>
|
||||
<span className={style.infoIcon}>ℹ</span>
|
||||
<p className={style.infoText}>
|
||||
全局建群计划将应用与所有设备,包含新添加的设备,请确保设置合理的规则。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 全局建群计划 */}
|
||||
{globalPlans.length > 0 && (
|
||||
<section className={style.section}>
|
||||
<h2 className={style.sectionTitle}>
|
||||
<div className={style.sectionDot}></div>
|
||||
全局建群计划
|
||||
</h2>
|
||||
{globalPlans.map(plan => (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onMenuClick={handleMenuClick}
|
||||
menuLoading={menuLoadingId === plan.id}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 独立建群计划 */}
|
||||
{independentPlans.length > 0 && (
|
||||
<section className={style.section}>
|
||||
<h2 className={`${style.sectionTitle} ${style.sectionTitleIndependent}`}>
|
||||
<div className={`${style.sectionDot} ${style.sectionDotIndependent}`}></div>
|
||||
独立建群计划
|
||||
</h2>
|
||||
<div className={style.planList}>
|
||||
{independentPlans.map(plan => (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onMenuClick={handleMenuClick}
|
||||
menuLoading={menuLoadingId === plan.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{plans.length === 0 && !loading && (
|
||||
<div className={style.emptyState}>
|
||||
<div className={style.emptyIcon}>📋</div>
|
||||
<div className={style.emptyText}>暂无建群计划</div>
|
||||
<Button color="primary" onClick={handleCreate} className={style.emptyButton}>
|
||||
创建第一个计划
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
// 计划卡片组件
|
||||
interface PlanCardProps {
|
||||
plan: GroupCreatePlan;
|
||||
onStatusToggle: (plan: GroupCreatePlan) => void;
|
||||
onMenuClick: (params: { key: string }, plan: GroupCreatePlan) => void;
|
||||
menuLoading: boolean;
|
||||
}
|
||||
|
||||
const PlanCard: React.FC<PlanCardProps> = ({
|
||||
plan,
|
||||
onStatusToggle,
|
||||
onMenuClick,
|
||||
menuLoading,
|
||||
}) => {
|
||||
const isRunning = plan.status === 1;
|
||||
|
||||
return (
|
||||
<div className={style.planCard}>
|
||||
{/* 卡片头部 */}
|
||||
<div className={style.cardHeader}>
|
||||
<div className={style.cardTitleSection}>
|
||||
<h3 className={style.cardTitle}>{plan.name}</h3>
|
||||
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.cardActions}>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onChange={() => onStatusToggle(plan)}
|
||||
className={style.statusSwitch}
|
||||
/>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "detail",
|
||||
icon: <EyeOutlined />,
|
||||
label: "计划详情",
|
||||
disabled: menuLoading,
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
icon: <EditOutlined />,
|
||||
label: "编辑",
|
||||
disabled: menuLoading,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "删除",
|
||||
disabled: menuLoading,
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
onClick: (params) => onMenuClick(params, plan),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
disabled={menuLoading}
|
||||
>
|
||||
<button className={style.moreButton}>
|
||||
<MoreOutlined />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className={style.cardStats}>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statHeader}>
|
||||
<span className={style.statIcon}>👥</span>
|
||||
<p className={style.statLabel}>已建群数</p>
|
||||
</div>
|
||||
<div className={style.statValue}>
|
||||
<span className={style.statNumber}>{plan.createdGroupsCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.statDivider}></div>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statHeader}>
|
||||
<span className={style.statIcon}>👥</span>
|
||||
<p className={style.statLabel}>总人数</p>
|
||||
</div>
|
||||
<div className={style.statValue}>
|
||||
<span className={style.statNumber}>{plan.totalMembersCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<div className={style.cardDetails}>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>📝</span>
|
||||
<span className={style.detailLabel}>群名称</span>
|
||||
<span className={style.detailValue}>{plan.groupNameTemplate || "-"}</span>
|
||||
</div>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>⚙️</span>
|
||||
<span className={style.detailLabel}>群规模</span>
|
||||
<span className={style.detailValue}>
|
||||
{plan.groupSizeMax || 38}人/群
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>🕐</span>
|
||||
<span className={style.detailLabel}>更新时间</span>
|
||||
<span className={style.detailTime}>
|
||||
{plan.updateTime
|
||||
? new Date(plan.updateTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).replace(/\//g, "-")
|
||||
: plan.createTime
|
||||
? new Date(plan.createTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).replace(/\//g, "-")
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupCreateList;
|
||||
@@ -62,6 +62,17 @@ const Workspace: React.FC = () => {
|
||||
path: "/workspace/auto-group",
|
||||
bgColor: "#f6ffed",
|
||||
},
|
||||
{
|
||||
id: "group-create",
|
||||
name: "自动建群(新版)",
|
||||
description: "智能拉好友建群",
|
||||
icon: (
|
||||
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
|
||||
),
|
||||
path: "/workspace/group-create",
|
||||
bgColor: "#f6ffed",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "traffic-distribution",
|
||||
name: "流量分发",
|
||||
|
||||
@@ -52,13 +52,13 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
}
|
||||
|
||||
return {
|
||||
id: pool.id || pool.poolId,
|
||||
name: pool.name || pool.poolName || `流量池${pool.id}`,
|
||||
description: pool.description || pool.desc || "",
|
||||
id: pool.id || pool.poolId,
|
||||
name: pool.name || pool.poolName || `流量池${pool.id}`,
|
||||
description: pool.description || pool.desc || "",
|
||||
userCount: pool.num || pool.userCount || pool.count || 0,
|
||||
tags: pool.tags || [],
|
||||
tags: pool.tags || [],
|
||||
createdAt: createdAt,
|
||||
deviceIds: pool.deviceIds || [],
|
||||
deviceIds: pool.deviceIds || [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -40,8 +40,12 @@ export const routeGroups = {
|
||||
"/workspace/auto-like/:id/edit",
|
||||
"/workspace/auto-group",
|
||||
"/workspace/auto-group/:id",
|
||||
"/workspace/group-create",
|
||||
"/workspace/group-create/new",
|
||||
"/workspace/group-create/:id",
|
||||
"/workspace/group-create/:id/groups",
|
||||
"/workspace/group-create/:id/groups/:groupId",
|
||||
"/workspace/group-create/:id/edit",
|
||||
"/workspace/group-push",
|
||||
"/workspace/group-push/new",
|
||||
"/workspace/group-push/:id",
|
||||
|
||||
@@ -5,7 +5,11 @@ import RecordAutoLike from "@/pages/mobile/workspace/auto-like/record";
|
||||
import AutoGroupList from "@/pages/mobile/workspace/auto-group/list";
|
||||
import AutoGroupDetail from "@/pages/mobile/workspace/auto-group/detail";
|
||||
import AutoGroupForm from "@/pages/mobile/workspace/auto-group/form";
|
||||
import GroupCreateList from "@/pages/mobile/workspace/group-create/list";
|
||||
import GroupCreateForm from "@/pages/mobile/workspace/group-create/form";
|
||||
import GroupCreateDetail from "@/pages/mobile/workspace/group-create/detail";
|
||||
import GroupListPage from "@/pages/mobile/workspace/group-create/detail/groups-list";
|
||||
import GroupDetailPage from "@/pages/mobile/workspace/group-create/detail/group-detail";
|
||||
import GroupPush from "@/pages/mobile/workspace/group-push/list";
|
||||
import FormGroupPush from "@/pages/mobile/workspace/group-push/form";
|
||||
import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail";
|
||||
@@ -76,6 +80,11 @@ const workspaceRoutes = [
|
||||
auth: true,
|
||||
},
|
||||
// 自动建群(新版)
|
||||
{
|
||||
path: "/workspace/group-create",
|
||||
element: <GroupCreateList />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/new",
|
||||
element: <GroupCreateForm />,
|
||||
@@ -83,6 +92,21 @@ const workspaceRoutes = [
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id",
|
||||
element: <GroupCreateDetail />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/groups",
|
||||
element: <GroupListPage />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/groups/:groupId",
|
||||
element: <GroupDetailPage />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/edit",
|
||||
element: <GroupCreateForm />,
|
||||
auth: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user