diff --git a/Cunkebao/src/components/GroupSelection/selectionPopup.tsx b/Cunkebao/src/components/GroupSelection/selectionPopup.tsx index dd5c9927..8bf39cd6 100644 --- a/Cunkebao/src/components/GroupSelection/selectionPopup.tsx +++ b/Cunkebao/src/components/GroupSelection/selectionPopup.tsx @@ -142,7 +142,11 @@ export default function SelectionPopup({ // 复制一份selectedOptions到临时变量 setTempSelectedOptions([...selectedOptions]); fetchGroups(1, ""); + } else { + // 弹窗关闭时重置状态 + setTempSelectedOptions([]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]); // 搜索防抖(只在弹窗打开且搜索词变化时执行) diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss new file mode 100644 index 00000000..75e5a2ec --- /dev/null +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss @@ -0,0 +1,329 @@ +.container { + width: 100%; +} + +.inputWrapper { + position: relative; + margin-bottom: 12px; + + .inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; + z-index: 1; + pointer-events: none; + } + + .input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; + } + + .clearBtn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #999; + font-size: 16px; + z-index: 1; + } +} + +.selectedGroupsList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.groupCard { + background: #fff; + border-radius: 12px; + padding: 16px; + border: 1px solid #e5e6eb; +} + +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.groupInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.groupAvatar { + width: 48px; + height: 48px; + border-radius: 8px; + flex-shrink: 0; +} + +.groupDetails { + flex: 1; + min-width: 0; +} + +.groupName { + font-size: 16px; + font-weight: 500; + color: #222; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.groupId { + font-size: 14px; + color: #888; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.deleteGroupBtn { + color: #ff4d4f; + font-size: 18px; + padding: 4px; + min-width: auto; + height: auto; +} + +.membersSection { + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.membersLabel { + font-size: 14px; + color: #666; + margin-bottom: 12px; + font-weight: 500; +} + +.membersList { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.memberItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: relative; + width: 70px; +} + +.memberAvatar { + width: 56px; + height: 56px; + border-radius: 50%; + position: relative; +} + +.removeMemberBtn { + position: absolute; + top: -4px; + right: -4px; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + background: #fff; + border: 1px solid #e5e6eb; + border-radius: 50%; + color: #ff4d4f; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.memberName { + font-size: 12px; + color: #222; + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.addMemberBtn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border: 1px dashed #d9d9d9; + border-radius: 50%; + background: #fafafa; + color: #999; + font-size: 20px; + cursor: pointer; + transition: all 0.2s; + gap: 4px; + + span { + font-size: 12px; + } + + &:active { + background: #f0f0f0; + border-color: #1677ff; + color: #1677ff; + } +} + +.memberSelectionPopup { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} + +.popupHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; +} + +.popupTitle { + font-size: 18px; + font-weight: 600; + color: #222; +} + +.closeBtn { + color: #1677ff; + 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; + padding: 16px 20px; +} + +.memberListItem { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + cursor: pointer; + + &:last-child { + border-bottom: none; + } + + &.selected { + .memberListItemName { + color: #1677ff; + } + } +} + +.memberListItemAvatar { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.memberListItemName { + flex: 1; + font-size: 16px; + color: #222; +} + +.checkmark { + color: #1677ff; + font-size: 18px; + font-weight: bold; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.loadingText { + font-size: 14px; + color: #999; +} + +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.emptyText { + font-size: 14px; + color: #999; +} diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx new file mode 100644 index 00000000..ec7114d1 --- /dev/null +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx @@ -0,0 +1,438 @@ +import React, { useState, useEffect } from "react"; +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"; +import GroupSelection from "../GroupSelection"; +import { GroupSelectionItem } from "../GroupSelection/data"; +import request from "@/api/request"; + +// 群成员接口 +export interface GroupMember { + id: string; + nickname: string; + wechatId: string; + avatar: string; + gender?: "male" | "female"; + role?: "owner" | "admin" | "member"; +} + +// 带成员的群选项 +export interface GroupWithMembers extends GroupSelectionItem { + members?: GroupMember[]; + groupId?: string; // 用于关联成员和群 +} + +interface GroupSelectionWithMembersProps { + selectedGroups: GroupWithMembers[]; + onSelect: (groups: GroupWithMembers[]) => void; + placeholder?: string; + className?: string; + readonly?: boolean; +} + +// 获取群成员列表 +const getGroupMembers = async ( + groupId: string, + page: number = 1, + limit: number = 100, + keyword: string = "", +): Promise => { + try { + const params: any = { + page, + limit, + groupId, + }; + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + const response = await request("/v1/kefu/wechatChatroom/members", params, "GET"); + // request 拦截器会返回 res.data.data ?? res.data + // 对于 { code: 200, data: { list: [...] } } 的返回,拦截器会返回 { list: [...] } + const memberList = response?.list || response?.data?.list || []; + + // 映射接口返回的数据结构到我们的接口 + return memberList.map((item: any) => ({ + id: String(item.id), + nickname: item.nickname || "", + wechatId: item.wechatId || "", + avatar: item.avatar || "", + gender: undefined, // 接口未返回,暂时设为 undefined + role: undefined, // 接口未返回,暂时设为 undefined + })); + } catch (error) { + console.error("获取群成员失败:", error); + return []; + } +}; + +const GroupSelectionWithMembers: React.FC = ({ + selectedGroups, + onSelect, + placeholder = "选择聊天群", + className = "", + readonly = false, +}) => { + const [groupSelectionVisible, setGroupSelectionVisible] = useState(false); + const [memberSelectionVisible, setMemberSelectionVisible] = useState<{ + visible: boolean; + groupId: string; + }>({ visible: false, groupId: "" }); + 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[]) => { + const groupsWithMembers: GroupWithMembers[] = groups.map(group => { + const existing = selectedGroups.find(g => g.id === group.id); + return { + ...group, + members: existing?.members || [], + }; + }); + onSelect(groupsWithMembers); + setGroupSelectionVisible(false); + }; + + // 删除群 + const handleRemoveGroup = (groupId: string) => { + if (readonly) return; + const newGroups = selectedGroups.filter(g => g.id !== groupId); + const newSelectedMembers = { ...selectedMembers }; + delete newSelectedMembers[groupId]; + setSelectedMembers(newSelectedMembers); + onSelect(newGroups); + }; + + // 打开成员选择弹窗 + const handleOpenMemberSelection = async (groupId: string) => { + if (readonly) return; + setMemberSelectionVisible({ visible: true, groupId }); + setMemberSearchKeyword(""); // 重置搜索关键词 + + // 如果还没有加载过该群的成员列表,则加载所有成员(不使用搜索关键词) + if (!allMembers[groupId] && !fullMembersCache[groupId]) { + setLoadingMembers(true); + try { + 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] })); + } + }; + + // 选择成员 + const handleSelectMember = (groupId: string, member: GroupMember) => { + if (readonly) return; + const currentMembers = selectedMembers[groupId] || []; + const isSelected = currentMembers.some(m => m.id === member.id); + + let newSelectedMembers = { ...selectedMembers }; + if (isSelected) { + newSelectedMembers[groupId] = currentMembers.filter(m => m.id !== member.id); + } else { + newSelectedMembers[groupId] = [...currentMembers, member]; + } + setSelectedMembers(newSelectedMembers); + + // 更新群数据 + const updatedGroups = selectedGroups.map(group => { + if (group.id === groupId) { + return { + ...group, + members: newSelectedMembers[groupId] || [], + }; + } + return group; + }); + onSelect(updatedGroups); + }; + + // 移除成员 + const handleRemoveMember = (groupId: string, memberId: string) => { + if (readonly) return; + const currentMembers = selectedMembers[groupId] || []; + const newMembers = currentMembers.filter(m => m.id !== memberId); + + const newSelectedMembers = { ...selectedMembers }; + newSelectedMembers[groupId] = newMembers; + setSelectedMembers(newSelectedMembers); + + // 更新群数据 + const updatedGroups = selectedGroups.map(group => { + if (group.id === groupId) { + return { + ...group, + members: newMembers, + }; + } + return group; + }); + onSelect(updatedGroups); + }; + + // 同步 selectedGroups 到 selectedMembers + useEffect(() => { + const membersMap: Record = {}; + selectedGroups.forEach(group => { + if (group.members && group.members.length > 0) { + membersMap[group.id] = group.members; + } + }); + setSelectedMembers(membersMap); + }, [selectedGroups.length]); + + // 获取显示文本 + const getDisplayText = () => { + if (selectedGroups.length === 0) return ""; + return `已选择${selectedGroups.length}个群聊`; + }; + + const currentGroupMembers = allMembers[memberSelectionVisible.groupId] || []; + const currentSelectedMembers = selectedMembers[memberSelectionVisible.groupId] || []; + + return ( +
+ {/* 输入框 */} +
!readonly && setGroupSelectionVisible(true)} + > + + + {!readonly && selectedGroups.length > 0 && ( + + )} +
+ + {/* 已选群列表 */} + {selectedGroups.length > 0 && ( +
+ {selectedGroups.map(group => ( +
+ {/* 群信息 */} +
+
+ +
+
{group.name}
+
ID: {group.chatroomId || group.id}
+
+
+ {!readonly && ( + + )} +
+ + {/* 成员选择区域 */} +
+
+ 采集群内指定成员 ({group.members?.length || 0}人) +
+
+ {group.members?.map(member => ( +
+ +
{member.nickname}
+ {!readonly && ( + + )} +
+ ))} + {!readonly && ( +
handleOpenMemberSelection(group.id)} + > + + 添加 +
+ )} +
+
+
+ ))} +
+ )} + + {/* 群选择弹窗 */} + + + {/* 成员选择弹窗 */} + +
+
+
选择成员
+ +
+
+
+ setMemberSearchKeyword(val)} + onEnterPress={handleSearchMembers} + className={style.searchInput} + /> + {memberSearchKeyword && ( + + )} +
+ +
+
+ {loadingMembers ? ( +
+
加载中...
+
+ ) : currentGroupMembers.length > 0 ? ( + currentGroupMembers.map(member => { + const isSelected = currentSelectedMembers.some(m => m.id === member.id); + return ( +
handleSelectMember(memberSelectionVisible.groupId, member)} + > + +
{member.nickname}
+ {isSelected &&
} +
+ ); + }) + ) : ( +
+
暂无成员数据
+
+ )} +
+
+
+
+ ); +}; + +export default GroupSelectionWithMembers; diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index 98527516..6de9a90c 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -6,12 +6,13 @@ import { DownOutlined } from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; +import GroupSelectionWithMembers from "@/components/GroupSelectionWithMembers"; import DeviceSelection from "@/components/DeviceSelection"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; -import request from "@/api/request"; -import { getContentLibraryDetail, updateContentLibrary } from "./api"; +import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; +import { GroupWithMembers } from "@/components/GroupSelectionWithMembers"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -49,6 +50,9 @@ export default function ContentForm() { const [selectedGroupsOptions, setSelectedGroupsOptions] = useState< GroupSelectionItem[] >([]); + const [selectedGroupsWithMembers, setSelectedGroupsWithMembers] = useState< + GroupWithMembers[] + >([]); const [useAI, setUseAI] = useState(false); const [aiPrompt, setAIPrompt] = useState("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除"); const [enabled, setEnabled] = useState(true); @@ -64,6 +68,7 @@ export default function ContentForm() { "text", "image", "video", + "link", ]); const [submitting, setSubmitting] = useState(false); const [loading, setLoading] = useState(false); @@ -94,12 +99,40 @@ export default function ContentForm() { : []); setSelectedDevices(deviceOptions || []); setSelectedFriends(data.sourceFriends || []); - setSelectedGroups(data.selectedGroups || []); - setSelectedGroupsOptions(data.selectedGroupsOptions || []); + // 使用 wechatGroupsOptions 作为群列表数据 + 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[] = groupsOptions.map( + (group: any) => { + const groupIdStr = String(group.id); + const members = groupMembersMap[groupIdStr] || []; + // 映射成员数据结构 + return { + ...group, + members: members.map((member: any) => ({ + id: String(member.id), + nickname: member.nickname || "", + wechatId: member.wechatId || "", + avatar: member.avatar || "", + gender: undefined, + role: undefined, + })), + }; + }, + ); + setSelectedGroupsWithMembers(groupsWithMembers); setSelectedFriendsOptions(data.friendsGroupsOptions || []); setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); - setCatchType(data.catchType || ["text", "image", "video"]); + setCatchType(data.catchType || ["text", "image", "video", "link"]); setAIPrompt(data.aiPrompt || ""); // aiEnabled 为 AI 提示词开关,1 开启 0 关闭 if (typeof data.aiEnabled !== "undefined") { @@ -141,7 +174,15 @@ export default function ContentForm() { devices: selectedDevices.map(d => d.id), friendsGroups: friendsGroups, wechatGroups: selectedGroups, - groupMembers: {}, + groupMembers: selectedGroupsWithMembers.reduce( + (acc, group) => { + if (group.members && group.members.length > 0) { + acc[group.id] = group.members.map(m => m.id); + } + return acc; + }, + {} as Record, + ), keywordInclude: keywordsInclude .split(/,|,|\n|\s+/) .map(s => s.trim()) @@ -162,7 +203,7 @@ export default function ContentForm() { await updateContentLibrary({ id, ...payload }); Toast.show({ content: "保存成功", position: "top" }); } else { - await request("/v1/content/library/create", { ...payload, formType: 0 }, "POST"); + await createContentLibrary(payload); Toast.show({ content: "创建成功", position: "top" }); } navigate("/mine/content"); @@ -181,6 +222,12 @@ export default function ContentForm() { setSelectedGroupsOptions(groups); }; + const handleGroupsWithMembersChange = (groups: GroupWithMembers[]) => { + setSelectedGroupsWithMembers(groups); + setSelectedGroups(groups.map(g => g.id.toString())); + setSelectedGroupsOptions(groups); + }; + const handleFriendsChange = (friends: FriendSelectionItem[]) => { setSelectedFriends(friends.map(f => f.id.toString())); setSelectedFriendsOptions(friends); @@ -234,7 +281,7 @@ export default function ContentForm() { 来源渠道 - 微信朋友圈 + 微信 @@ -336,9 +383,9 @@ export default function ContentForm() { /> - @@ -386,7 +433,7 @@ export default function ContentForm() { 采集内容类型
- {["text", "image", "video"].map(type => ( + {["text", "image", "video", "link"].map(type => (
diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index a985f182..54e8de40 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -692,13 +692,13 @@ const WechatAccountDetail: React.FC = () => {
{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"} - 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"} + 最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "-"}
- {overviewData?.healthScoreAssessment?.score || 67} + {overviewData?.healthScoreAssessment?.score || 0}
SCORE
@@ -810,7 +810,7 @@ const WechatAccountDetail: React.FC = () => {
- {overviewData?.healthScoreAssessment?.score || 67} + {overviewData?.healthScoreAssessment?.score || 0}
SCORE
@@ -869,7 +869,7 @@ const WechatAccountDetail: React.FC = () => {
- {record.title || record.description || "记录"} + {record.name || record.description || "记录"} {record.statusTag && ( {record.statusTag} diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts index 26377d6c..b1f5c5f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts @@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data"; export function getPlanList(params: { sceneId: string; page: number; - pageSize: number; + limit: number; }): Promise { return request(`/v1/plan/list`, params, "GET"); } @@ -36,6 +36,13 @@ export function getUserList(planId: string, type: number) { } //获客列表 -export function getFriendRequestTaskStats(taskId: string) { - return request(`/v1/dashboard/friendRequestTaskStats`, { taskId }, "GET"); +export function getFriendRequestTaskStats( + taskId: string, + params?: { startTime?: string; endTime?: string }, +) { + return request( + `/v1/dashboard/friendRequestTaskStats`, + { taskId, ...params }, + "GET", + ); } 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 a084dc3c..308bd4f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/PoolListModal.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { Popup, SpinLoading } from "antd-mobile"; -import { Button, message } from "antd"; -import { CloseOutlined } from "@ant-design/icons"; +import React, { useEffect, useState, useCallback } from "react"; +import { Popup, SpinLoading, DatePicker } from "antd-mobile"; +import { Button, message, Input } from "antd"; +import { CloseOutlined, CalendarOutlined } from "@ant-design/icons"; import style from "./Popups.module.scss"; import { getFriendRequestTaskStats } from "../api"; import LineChart2 from "@/components/LineChart2"; @@ -39,12 +39,52 @@ const PoolListModal: React.FC = ({ const [xData, setXData] = useState([]); const [yData, setYData] = useState([]); const [loading, setLoading] = useState(false); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const [showStartTimePicker, setShowStartTimePicker] = useState(false); + const [showEndTimePicker, setShowEndTimePicker] = useState(false); - // 当弹窗打开且有ruleId时,获取数据 + // 格式化日期为 YYYY-MM-DD + const formatDate = useCallback((date: Date | null): string => { + if (!date) return ""; + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + }, []); + + // 初始化默认时间(近7天) useEffect(() => { - if (visible && ruleId) { + if (visible) { + // 如果时间未设置,设置默认值为近7天 + if (!startTime || !endTime) { + const today = new Date(); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(today.getDate() - 7); + setStartTime(sevenDaysAgo); + setEndTime(today); + } + } else { + // 弹窗关闭时重置时间,下次打开时重新初始化 + setStartTime(null); + setEndTime(null); + } + }, [visible]); + + // 当弹窗打开或有ruleId或时间筛选变化时,获取数据 + useEffect(() => { + if (!visible || !ruleId) return; + setLoading(true); - getFriendRequestTaskStats(ruleId.toString()) + const params: { startTime?: string; endTime?: string } = {}; + if (startTime) { + params.startTime = formatDate(startTime); + } + if (endTime) { + params.endTime = formatDate(endTime); + } + + getFriendRequestTaskStats(ruleId.toString(), params) .then(res => { console.log(res); setXData(res.dateArray); @@ -57,13 +97,15 @@ const PoolListModal: React.FC = ({ res.successRateArray, ]); setStatistics(res.totalStats); - setLoading(false); + }) + .catch(error => { + console.error("获取统计数据失败:", error); + message.error("获取统计数据失败"); }) .finally(() => { setLoading(false); }); - } - }, [visible, ruleId]); + }, [visible, ruleId, startTime, endTime, formatDate]); const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据"; return ( @@ -89,6 +131,55 @@ const PoolListModal: React.FC = ({ />
+ {/* 时间筛选 */} +
+
+ + setShowStartTimePicker(true)} + prefix={} + className={style.dateFilterInput} + /> + setShowStartTimePicker(false)} + onConfirm={val => { + setStartTime(val); + setShowStartTimePicker(false); + }} + /> +
+
+ + setShowEndTimePicker(true)} + prefix={} + className={style.dateFilterInput} + /> + setShowEndTimePicker(false)} + onConfirm={val => { + setEndTime(val); + setShowEndTimePicker(false); + }} + /> +
+
+ {/* 统计数据表格 */}
{loading ? ( diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss index cbfce7c1..dd77f7b7 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/Popups.module.scss @@ -663,6 +663,32 @@ color: #666; } +// 日期筛选样式 +.dateFilter { + display: flex; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; + background: #fff; +} + +.dateFilterItem { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.dateFilterLabel { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.dateFilterInput { + width: 100%; +} + // 统计数据弹窗样式 .statisticsContent { flex: 1; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 367187e9..e658e26f 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -8,8 +8,9 @@ import { Popup, Card, Tag, + InfiniteScroll, } from "antd-mobile"; -import { Input, Pagination } from "antd"; +import { Input } from "antd"; import { PlusOutlined, CopyOutlined, @@ -80,7 +81,7 @@ const ScenarioList: React.FC = () => { const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [total, setTotal] = useState(0); - const pageSize = 20; + const limit = 20; // 获取计划列表数据 const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { @@ -96,7 +97,7 @@ const ScenarioList: React.FC = () => { const response = await getPlanList({ sceneId: scenarioId, page: page, - pageSize: pageSize, + limit: limit, }); if (response && response.list) { @@ -110,7 +111,7 @@ const ScenarioList: React.FC = () => { // 更新分页信息 setTotal(response.total || 0); - setHasMore(response.list.length === pageSize); + setHasMore(response.list.length === limit); setCurrentPage(page); } } catch (error) { @@ -149,10 +150,11 @@ const ScenarioList: React.FC = () => { fetchScenarioData(); }, [scenarioId]); - // 分页改变处理 - const handlePageChange = async (page: number) => { - setCurrentPage(page); - await fetchPlanList(page, false); + // 加载更多 + const handleLoadMore = async () => { + if (!hasMore || loadingMore || loadingTasks) return; + const nextPage = currentPage + 1; + await fetchPlanList(nextPage, true); }; const handleCopyPlan = async (taskId: string) => { @@ -405,18 +407,6 @@ const ScenarioList: React.FC = () => { } loading={loading} - footer={ -
- -
- } >
{/* 计划列表 */} @@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
))} + {/* 上拉加载更多 */} + + {loadingMore && ( +
+ + + 加载中... + +
+ )} + {!hasMore && filteredTasks.length > 0 && ( +
+ 没有更多了 +
+ )} +
)}
diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts index 01fec441..a6e5bade 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts @@ -60,6 +60,7 @@ export const defFormData: FormData = { enabled: true, remarkFormat: "", addFriendInterval: 1, + tips: "请注意消息,稍后加你微信", posters: [], device: [], customTags: [], diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx index ac63a6b6..87b2dfab 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -90,7 +90,7 @@ export default function NewPlan() { setFormData(prev => ({ ...prev, name: detail.name ?? "", - scenario: Number(detail.scenario) || 1, + scenario: Number(detail.sceneId || detail.scenario) || 1, scenarioTags: detail.scenarioTags ?? [], customTags: detail.customTags ?? [], customTagsOptions: detail.customTags ?? [], @@ -102,7 +102,7 @@ export default function NewPlan() { startTime: detail.startTime ?? "09:00", endTime: detail.endTime ?? "18:00", enabled: detail.enabled ?? true, - sceneId: Number(detail.scenario) || 1, + sceneId: Number(detail.sceneId || detail.scenario) || 1, remarkFormat: detail.remarkFormat ?? "", addFriendInterval: detail.addFriendInterval ?? 1, tips: detail.tips ?? "", @@ -122,10 +122,19 @@ export default function NewPlan() { distributionAddReward: detail.addFriendRewardAmount, })); } else { + // 新建时,如果是海报场景,设置默认获客成功提示 + const defaultTips = "请注意消息,稍后加你微信"; if (scenarioId) { setFormData(prev => ({ ...prev, ...{ scenario: Number(scenarioId) || 1 }, + tips: Number(scenarioId) === 1 ? defaultTips : prev.tips || "", + })); + } else { + // 如果没有 scenarioId,默认是海报场景(scenario === 1),设置默认提示 + setFormData(prev => ({ + ...prev, + tips: prev.scenario === 1 ? defaultTips : prev.tips || "", })); } } diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index f8136303..07137f67 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -40,6 +40,7 @@ const generatePosterMaterials = (): Material[] => { }; const BasicSettings: React.FC = ({ + isEdit, formData, onChange, sceneList, @@ -100,6 +101,15 @@ const BasicSettings: React.FC = ({ setTips(formData.tips || ""); }, [formData.tips]); + // 当切换到海报场景且 tips 为空时,设置默认值 + useEffect(() => { + if (formData.scenario === 1 && !formData.tips) { + const defaultTips = "请注意消息,稍后加你微信"; + onChange({ ...formData, tips: defaultTips }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formData.scenario]); + // 选中场景 const handleScenarioSelect = (sceneId: number) => { @@ -249,16 +259,27 @@ const BasicSettings: React.FC = ({ ) : (
{sceneList - .filter(scene => scene.id !== 10) + .filter(scene => { + // 编辑模式下,如果当前选中的场景 id 是 10,则显示它 + if (isEdit && formData.scenario === 10 && scene.id === 10) { + return true; + } + // 其他情况过滤掉 id 为 10 的场景 + return scene.id !== 10; + }) .map(scene => { const selected = formData.scenario === scene.id; + // 编辑模式下,如果当前场景 id 是 10,则禁用所有场景选择 + const isDisabled = isEdit && formData.scenario === 10; return (
= ({ setQrCodeData(null); setScanning(false); const timer = setTimeout(() => { - handleGenerateQRCode(); + handleGenerateQRCode(); }, 100); return () => clearTimeout(timer); } diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx index 1df858fc..57c93bb6 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx @@ -33,14 +33,12 @@ import type { } from "./data"; import styles from "./index.module.scss"; -// 格式化金额显示(后端返回的是分,需要转换为元) +// 格式化金额显示(后端返回的是元,直接格式化即可) const formatCurrency = (amount: number): string => { - // 将分转换为元 - const yuan = amount / 100; - if (yuan >= 10000) { - return "¥" + (yuan / 10000).toFixed(2) + "万"; + if (amount >= 10000) { + return "¥" + (amount / 10000).toFixed(2) + "万"; } - return "¥" + yuan.toFixed(2); + return "¥" + amount.toFixed(2); }; const ChannelDetailPage: React.FC = () => { 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, + }, + ]} + /> + + {/* 修改群公告弹窗 */} + +
+ 群公告 +
+