From b4c813311b842e395d229b1fd934c380fbd93e4b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Sun, 4 Jan 2026 17:03:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=BB=BA=E7=BE=A4=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=20=20+=20=20=E8=A7=A6=E5=AE=A2=E5=AE=9D=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=9C=8B=E5=8F=8B=E5=9C=88=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index.module.scss | 42 + .../GroupSelectionWithMembers/index.tsx | 83 +- .../pages/mobile/mine/content/form/index.tsx | 11 +- .../plan/list/components/PoolListModal.tsx | 34 +- .../components/AddChannelModal.tsx | 2 +- .../detail/group-detail.module.scss | 598 +++ .../group-create/detail/group-detail.tsx | 675 +++ .../detail/groups-list.module.scss | 275 ++ .../group-create/detail/groups-list.tsx | 290 ++ .../group-create/detail/index.module.scss | 482 ++ .../workspace/group-create/detail/index.tsx | 401 ++ .../mobile/workspace/group-create/form/api.ts | 56 + .../form/components/BasicSettings.module.scss | 6 + .../form/components/BasicSettings.tsx | 118 +- .../form/components/DeviceSelectionStep.tsx | 24 +- .../workspace/group-create/form/index.tsx | 85 +- .../workspace/group-create/form/types.ts | 50 +- .../group-create/list/index.module.scss | 303 ++ .../workspace/group-create/list/index.tsx | 344 ++ .../src/pages/mobile/workspace/main/index.tsx | 11 + .../list/components/PoolListModal.tsx | 10 +- Cunkebao/src/router/config.ts | 4 + Cunkebao/src/router/module/workspace.tsx | 24 + .../api/controller/WebSocketController.php | 4 +- .../controller/WechatChatroomController.php | 2 +- Server/application/cunkebao/config/route.php | 43 +- .../controller/ContentLibraryController.php | 2 +- .../distribution/ChannelController.php | 6 +- .../workbench/WorkbenchAutoLikeController.php | 108 + .../workbench/WorkbenchController.php | 3353 ++++++++++++++ .../workbench/WorkbenchController_new.php | 3006 +++++++++++++ .../WorkbenchGroupCreateController.php | 3895 +++++++++++++++++ .../WorkbenchGroupPushController.php} | 1072 ++++- .../workbench/WorkbenchHelperController.php | 313 ++ .../WorkbenchImportContactController.php | 69 + .../workbench/WorkbenchMomentsController.php | 125 + .../workbench/WorkbenchTrafficController.php | 309 ++ Server/application/job/SyncContentJob.php | 12 + .../job/WorkbenchGroupCreateJob.php | 521 ++- 39 files changed, 16264 insertions(+), 504 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/groups-list.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/groups-list.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/detail/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/list/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchController_new.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php rename Server/application/cunkebao/controller/{WorkbenchController.php => workbench/WorkbenchGroupPushController.php} (77%) create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchHelperController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchImportContactController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchMomentsController.php create mode 100644 Server/application/cunkebao/controller/workbench/WorkbenchTrafficController.php diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss index 344a93ee..75e5a2ec 100644 --- a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss @@ -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; diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx index f1f73acd..ec7114d1 100644 --- a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx @@ -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 = ({ const [allMembers, setAllMembers] = useState>({}); const [selectedMembers, setSelectedMembers] = useState>({}); const [loadingMembers, setLoadingMembers] = useState(false); + const [memberSearchKeyword, setMemberSearchKeyword] = useState(""); + // 存储完整成员列表(用于搜索时切换回完整列表) + const [fullMembersCache, setFullMembersCache] = useState>({}); // 处理群选择 const handleGroupSelect = (groups: GroupSelectionItem[]) => { @@ -110,24 +113,66 @@ const GroupSelectionWithMembers: React.FC = ({ 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 = ({ 完成 +
+
+ setMemberSearchKeyword(val)} + onEnterPress={handleSearchMembers} + className={style.searchInput} + /> + {memberSearchKeyword && ( + + )} +
+ +
{loadingMembers ? (
diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index 7ad12d64..6de9a90c 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -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] || []; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx index 4871d2ba..308bd4f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx @@ -75,7 +75,7 @@ const PoolListModal: React.FC = ({ 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 = ({ } 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} - 累计统计数据` : "累计统计数据"; diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx index 26ab1b35..2863e768 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx @@ -221,7 +221,7 @@ const AddChannelModal: React.FC = ({ setQrCodeData(null); setScanning(false); const timer = setTimeout(() => { - handleGenerateQRCode(); + handleGenerateQRCode(); }, 100); return () => clearTimeout(timer); } diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss new file mode 100644 index 00000000..dd4ee0d5 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.module.scss @@ -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; +} diff --git a/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx new file mode 100644 index 00000000..c70a1d73 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/group-create/detail/group-detail.tsx @@ -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(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 ( + navigate(-1)} />} + > +
+ +
+
+ ); + } + + if (!groupDetail) { + return ( + navigate(-1)} />} + > +
+
未找到该群组
+ +
+
+ ); + } + + return ( + navigate(-1)} + /> + } + > +
+ {/* 同步遮罩层 */} + {syncing && ( +
+
+ +
同步中...
+
+
+ )} + {/* 群组信息卡片 */} +
+ {groupDetail.avatar || groupDetail.groupAvatar ? ( + {groupDetail.name { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const fallback = target.nextElementSibling as HTMLElement; + if (fallback) { + fallback.style.display = 'flex'; + } + }} + /> + ) : null} +
+ {(groupDetail.name || '').charAt(0) || '👥'} +
+

{groupDetail.name}

+
+ + 创建于 {groupDetail.createTime || "-"} +
+
+ + {/* 群二维码功能暂时隐藏 */} + {/* */} +
+
+ + {/* 基本信息 */} +
+

+ + 基本信息 +

+
+
+ 所属计划 +
+ 📋 + {groupDetail.planName || "-"} +
+
+
+
+ 群主 +
+ {groupDetail.groupAdmin ? ( + <> + + {groupDetail.groupAdmin.avatar ? ( + {groupDetail.groupAdmin.nickname { + (e.target as HTMLImageElement).style.display = "none"; + (e.target as HTMLImageElement).nextElementSibling?.classList.remove( + style.hidden + ); + }} + /> + ) : null} + + {groupDetail.groupAdmin.nickname?.charAt(0)?.toUpperCase() || "A"} + + + {groupDetail.groupAdmin.wechatId || "-"} + + ) : ( + "-" + )} +
+
+
+
+ 群公告 +
+ {groupDetail.announcement ? ( + groupDetail.announcement + ) : ( + + 未设置 + + + )} +
+
+
+
+ + {/* 群成员 */} +
+
+

+ + 群成员 + + {groupDetail.memberCount || 0}人 + +

+ +
+ + {showSearch && ( +
+ setSearchKeyword(e.target.value)} + /> +
+ )} + +
+ {filteredMembers.length > 0 ? ( + filteredMembers.map((member) => { + const isGroupAdmin = member.isOwner === 1 || member.isGroupAdmin || member.id === groupDetail.groupAdmin?.id; + return ( +
+
+
+ {member.avatar ? ( + {member.nickname { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + ) : ( +
+ {member.nickname?.charAt(0)?.toUpperCase() || "?"} +
+ )} + {isGroupAdmin && ( +
+ +
+ )} +
+
+
+ + {member.nickname || "-"} + + {member.isQuit === 1 && ( + 已退群 + )} + {member.joinStatus && ( + + {member.joinStatus === "auto" ? "自动" : "手动"} + + )} +
+
+ {member.wechatId || "-"} +
+ {(member.alias || member.remark) && ( +
+ {member.alias && 别名:{member.alias}} + {member.alias && member.remark && |} + {member.remark && 备注:{member.remark}} +
+ )} + {member.joinTime && ( +
+ 入群时间:{member.joinTime} +
+ )} +
+
+
+ {isGroupAdmin ? ( + 群主 + ) : ( + // 移除成员功能暂时隐藏 + // + null + )} +
+
+ ); + }) + ) : ( +
+
+ {searchKeyword ? "未找到匹配的成员" : "暂无成员"} +
+
+ )} +
+
+ + {/* 操作按钮 */} +
+ + +
+
+ + {/* 修改群名称弹窗 */} + +
+ 群名称 +
+ setEditNameValue(val)} + style={{ + fontSize: "14px", + padding: "10px 12px", + border: "1px solid #e5e5e5", + borderRadius: "6px", + backgroundColor: "#fff", + }} + maxLength={30} + /> +
+ } + closeOnAction + onClose={() => { + setEditNameVisible(false); + setEditNameValue(""); + }} + actions={[ + { + key: "cancel", + text: "取消", + onClick: () => { + setEditNameVisible(false); + setEditNameValue(""); + }, + }, + { + key: "confirm", + text: "确定", + primary: true, + onClick: handleConfirmEditName, + }, + ]} + /> + + {/* 修改群公告弹窗 */} + +
+ 群公告 +
+