内容库群选择功能 + 场景获客分页修复

This commit is contained in:
wong
2025-12-25 15:39:57 +08:00
parent 302617cd81
commit b8754f6174
11 changed files with 985 additions and 53 deletions

View File

@@ -142,7 +142,11 @@ export default function SelectionPopup({
// 复制一份selectedOptions到临时变量 // 复制一份selectedOptions到临时变量
setTempSelectedOptions([...selectedOptions]); setTempSelectedOptions([...selectedOptions]);
fetchGroups(1, ""); fetchGroups(1, "");
} else {
// 弹窗关闭时重置状态
setTempSelectedOptions([]);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]); }, [visible]);
// 搜索防抖(只在弹窗打开且搜索词变化时执行) // 搜索防抖(只在弹窗打开且搜索词变化时执行)

View File

@@ -0,0 +1,287 @@
.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;
}
.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;
}

View File

@@ -0,0 +1,363 @@
import React, { useState, useEffect } from "react";
import { SearchOutlined, DeleteOutlined, PlusOutlined } 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<GroupMember[]> => {
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<GroupSelectionWithMembersProps> = ({
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<Record<string, GroupMember[]>>({});
const [selectedMembers, setSelectedMembers] = useState<Record<string, GroupMember[]>>({});
const [loadingMembers, setLoadingMembers] = useState(false);
// 处理群选择
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 });
// 如果还没有加载过该群的成员列表,则加载
if (!allMembers[groupId]) {
setLoadingMembers(true);
try {
const members = await getGroupMembers(groupId);
setAllMembers(prev => ({ ...prev, [groupId]: members }));
} catch (error) {
console.error("加载群成员失败:", error);
} finally {
setLoadingMembers(false);
}
}
};
// 关闭成员选择弹窗
const handleCloseMemberSelection = () => {
setMemberSelectionVisible({ visible: false, 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<string, GroupMember[]> = {};
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 (
<div className={`${style.container} ${className}`}>
{/* 输入框 */}
<div
className={style.inputWrapper}
onClick={() => !readonly && setGroupSelectionVisible(true)}
>
<SearchOutlined className={style.inputIcon} />
<Input
placeholder={placeholder}
value={getDisplayText()}
readOnly
className={style.input}
/>
{!readonly && selectedGroups.length > 0 && (
<Button
fill="none"
size="small"
className={style.clearBtn}
onClick={e => {
e.stopPropagation();
setSelectedMembers({});
onSelect([]);
}}
>
<DeleteOutlined />
</Button>
)}
</div>
{/* 已选群列表 */}
{selectedGroups.length > 0 && (
<div className={style.selectedGroupsList}>
{selectedGroups.map(group => (
<div key={group.id} className={style.groupCard}>
{/* 群信息 */}
<div className={style.groupHeader}>
<div className={style.groupInfo}>
<Avatar src={group.avatar} className={style.groupAvatar} />
<div className={style.groupDetails}>
<div className={style.groupName}>{group.name}</div>
<div className={style.groupId}>ID: {group.chatroomId || group.id}</div>
</div>
</div>
{!readonly && (
<Button
fill="none"
size="small"
className={style.deleteGroupBtn}
onClick={() => handleRemoveGroup(group.id)}
>
<DeleteOutlined />
</Button>
)}
</div>
{/* 成员选择区域 */}
<div className={style.membersSection}>
<div className={style.membersLabel}>
({group.members?.length || 0})
</div>
<div className={style.membersList}>
{group.members?.map(member => (
<div key={member.id} className={style.memberItem}>
<Avatar src={member.avatar} className={style.memberAvatar} />
<div className={style.memberName}>{member.nickname}</div>
{!readonly && (
<Button
fill="none"
size="small"
className={style.removeMemberBtn}
onClick={() => handleRemoveMember(group.id, member.id)}
>
<DeleteOutlined />
</Button>
)}
</div>
))}
{!readonly && (
<div
className={style.addMemberBtn}
onClick={() => handleOpenMemberSelection(group.id)}
>
<PlusOutlined />
<span></span>
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* 群选择弹窗 */}
<GroupSelection
selectedOptions={selectedGroups as GroupSelectionItem[]}
onSelect={handleGroupSelect}
placeholder={placeholder}
visible={groupSelectionVisible}
onVisibleChange={setGroupSelectionVisible}
showInput={false}
showSelectedList={false}
/>
{/* 成员选择弹窗 */}
<Popup
visible={memberSelectionVisible.visible}
onMaskClick={handleCloseMemberSelection}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.memberSelectionPopup}>
<div className={style.popupHeader}>
<div className={style.popupTitle}></div>
<Button
fill="none"
size="small"
onClick={handleCloseMemberSelection}
className={style.closeBtn}
>
</Button>
</div>
<div className={style.memberList}>
{loadingMembers ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : currentGroupMembers.length > 0 ? (
currentGroupMembers.map(member => {
const isSelected = currentSelectedMembers.some(m => m.id === member.id);
return (
<div
key={member.id}
className={`${style.memberListItem} ${isSelected ? style.selected : ""}`}
onClick={() => handleSelectMember(memberSelectionVisible.groupId, member)}
>
<Avatar src={member.avatar} className={style.memberListItemAvatar} />
<div className={style.memberListItemName}>{member.nickname}</div>
{isSelected && <div className={style.checkmark}></div>}
</div>
);
})
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}></div>
</div>
)}
</div>
</div>
</Popup>
</div>
);
};
export default GroupSelectionWithMembers;

View File

@@ -6,11 +6,13 @@ import { DownOutlined } from "@ant-design/icons";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import FriendSelection from "@/components/FriendSelection"; import FriendSelection from "@/components/FriendSelection";
import GroupSelection from "@/components/GroupSelection"; import GroupSelection from "@/components/GroupSelection";
import GroupSelectionWithMembers from "@/components/GroupSelectionWithMembers";
import DeviceSelection from "@/components/DeviceSelection"; import DeviceSelection from "@/components/DeviceSelection";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss"; import style from "./index.module.scss";
import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api"; import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api";
import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { GroupWithMembers } from "@/components/GroupSelectionWithMembers";
import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { FriendSelectionItem } from "@/components/FriendSelection/data";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
@@ -48,6 +50,9 @@ export default function ContentForm() {
const [selectedGroupsOptions, setSelectedGroupsOptions] = useState< const [selectedGroupsOptions, setSelectedGroupsOptions] = useState<
GroupSelectionItem[] GroupSelectionItem[]
>([]); >([]);
const [selectedGroupsWithMembers, setSelectedGroupsWithMembers] = useState<
GroupWithMembers[]
>([]);
const [useAI, setUseAI] = useState(false); const [useAI, setUseAI] = useState(false);
const [aiPrompt, setAIPrompt] = useState("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除"); const [aiPrompt, setAIPrompt] = useState("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除");
const [enabled, setEnabled] = useState(true); const [enabled, setEnabled] = useState(true);
@@ -94,7 +99,30 @@ export default function ContentForm() {
setSelectedDevices(deviceOptions || []); setSelectedDevices(deviceOptions || []);
setSelectedFriends(data.sourceFriends || []); setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []); setSelectedGroups(data.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []); // 使用 wechatGroupsOptions 作为群列表数据
setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []);
// 处理带成员的群数据
// groupMembersOptions 是一个对象key是群ID字符串value是成员数组
const groupMembersMap = data.groupMembersOptions || {};
const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).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 || []); setSelectedFriendsOptions(data.friendsGroupsOptions || []);
setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsInclude((data.keywordInclude || []).join(","));
setKeywordsExclude((data.keywordExclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(","));
@@ -140,7 +168,15 @@ export default function ContentForm() {
devices: selectedDevices.map(d => d.id), devices: selectedDevices.map(d => d.id),
friendsGroups: friendsGroups, friendsGroups: friendsGroups,
wechatGroups: selectedGroups, 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<string, string[]>,
),
keywordInclude: keywordsInclude keywordInclude: keywordsInclude
.split(/,||\n|\s+/) .split(/,||\n|\s+/)
.map(s => s.trim()) .map(s => s.trim())
@@ -180,6 +216,12 @@ export default function ContentForm() {
setSelectedGroupsOptions(groups); setSelectedGroupsOptions(groups);
}; };
const handleGroupsWithMembersChange = (groups: GroupWithMembers[]) => {
setSelectedGroupsWithMembers(groups);
setSelectedGroups(groups.map(g => g.id.toString()));
setSelectedGroupsOptions(groups);
};
const handleFriendsChange = (friends: FriendSelectionItem[]) => { const handleFriendsChange = (friends: FriendSelectionItem[]) => {
setSelectedFriends(friends.map(f => f.id.toString())); setSelectedFriends(friends.map(f => f.id.toString()));
setSelectedFriendsOptions(friends); setSelectedFriendsOptions(friends);
@@ -335,9 +377,9 @@ export default function ContentForm() {
/> />
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups"> <Tabs.Tab title="选择聊天群" key="groups">
<GroupSelection <GroupSelectionWithMembers
selectedOptions={selectedGroupsOptions} selectedGroups={selectedGroupsWithMembers}
onSelect={handleGroupsChange} onSelect={handleGroupsWithMembersChange}
placeholder="选择聊天群" placeholder="选择聊天群"
/> />
</Tabs.Tab> </Tabs.Tab>

View File

@@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
export function getPlanList(params: { export function getPlanList(params: {
sceneId: string; sceneId: string;
page: number; page: number;
pageSize: number; limit: number;
}): Promise<PlanListResponse> { }): Promise<PlanListResponse> {
return request(`/v1/plan/list`, params, "GET"); return request(`/v1/plan/list`, params, "GET");
} }

View File

@@ -8,8 +8,9 @@ import {
Popup, Popup,
Card, Card,
Tag, Tag,
InfiniteScroll,
} from "antd-mobile"; } from "antd-mobile";
import { Input, Pagination } from "antd"; import { Input } from "antd";
import { import {
PlusOutlined, PlusOutlined,
CopyOutlined, CopyOutlined,
@@ -80,7 +81,7 @@ const ScenarioList: React.FC = () => {
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const pageSize = 20; const limit = 20;
// 获取计划列表数据 // 获取计划列表数据
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
@@ -96,7 +97,7 @@ const ScenarioList: React.FC = () => {
const response = await getPlanList({ const response = await getPlanList({
sceneId: scenarioId, sceneId: scenarioId,
page: page, page: page,
pageSize: pageSize, limit: limit,
}); });
if (response && response.list) { if (response && response.list) {
@@ -110,7 +111,7 @@ const ScenarioList: React.FC = () => {
// 更新分页信息 // 更新分页信息
setTotal(response.total || 0); setTotal(response.total || 0);
setHasMore(response.list.length === pageSize); setHasMore(response.list.length === limit);
setCurrentPage(page); setCurrentPage(page);
} }
} catch (error) { } catch (error) {
@@ -149,10 +150,11 @@ const ScenarioList: React.FC = () => {
fetchScenarioData(); fetchScenarioData();
}, [scenarioId]); }, [scenarioId]);
// 分页改变处理 // 加载更多
const handlePageChange = async (page: number) => { const handleLoadMore = async () => {
setCurrentPage(page); if (!hasMore || loadingMore || loadingTasks) return;
await fetchPlanList(page, false); const nextPage = currentPage + 1;
await fetchPlanList(nextPage, true);
}; };
const handleCopyPlan = async (taskId: string) => { const handleCopyPlan = async (taskId: string) => {
@@ -405,18 +407,6 @@ const ScenarioList: React.FC = () => {
</> </>
} }
loading={loading} loading={loading}
footer={
<div className="pagination-container">
<Pagination
total={total}
pageSize={pageSize}
current={currentPage}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper={false}
/>
</div>
}
> >
<div className={style["scenario-list-page"]}> <div className={style["scenario-list-page"]}>
{/* 计划列表 */} {/* 计划列表 */}
@@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
</div> </div>
</Card> </Card>
))} ))}
{/* 上拉加载更多 */}
<InfiniteScroll
loadMore={handleLoadMore}
hasMore={hasMore}
threshold={100}
>
{loadingMore && (
<div style={{ padding: "20px", textAlign: "center" }}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
...
</span>
</div>
)}
{!hasMore && filteredTasks.length > 0 && (
<div
style={{
padding: "20px",
textAlign: "center",
color: "#999",
fontSize: 12,
}}
>
</div>
)}
</InfiniteScroll>
</> </>
)} )}
</div> </div>

View File

@@ -21,6 +21,7 @@ Route::group('v1/', function () {
Route::group('wechatChatroom/', function () { Route::group('wechatChatroom/', function () {
Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表 Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表
Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情 Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情
Route::get('members', 'app\chukebao\controller\WechatChatroomController@getMembers'); // 获取群成员列表
Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告 Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告
}); });

View File

@@ -151,6 +151,68 @@ class WechatChatroomController extends BaseController
return ResponseHelper::success($detail); return ResponseHelper::success($detail);
} }
public function getMembers()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$groupId = $this->request->param('groupId', '');
$keyword = $this->request->param('keyword', '');
$accountId = $this->getUserInfo('s2_accountId');
if (empty($accountId)) {
return ResponseHelper::error('请先登录');
}
// 验证群组ID必填
if (empty($groupId)) {
return ResponseHelper::error('群组ID不能为空');
}
// 验证群组是否属于当前账号
$chatroom = Db::table('s2_wechat_chatroom')
->where(['id' => $groupId, 'isDeleted' => 0])
->find();
if (!$chatroom) {
return ResponseHelper::error('群组不存在或无权限访问');
}
// 获取群组的chatroomId微信群聊ID
$chatroomId = $chatroom['chatroomId'] ?? $chatroom['id'];
// 如果chatroomId为空使用id作为chatroomId
if (empty($chatroomId)) {
$chatroomId = $chatroom['id'];
}
// 构建查询
$query = Db::table('s2_wechat_chatroom_member')
->where('chatroomId', $chatroomId);
// 关键字搜索:昵称、备注、别名
if ($keyword !== '' && $keyword !== null) {
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->whereLike('nickname', $like)
->whereOr('conRemark', 'like', $like)
->whereOr('alias', 'like', $like);
});
}
$query->order('id desc');
$total = $query->count();
$list = $query->page($page, $limit)->select();
// 处理时间格式
foreach ($list as $k => &$v) {
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
}
unset($v);
return ResponseHelper::success(['list' => $list, 'total' => $total]);
}
public function aiAnnouncement() public function aiAnnouncement()
{ {
$userId = $this->getUserInfo('id'); $userId = $this->getUserInfo('id');

View File

@@ -19,7 +19,7 @@ return [
'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √ 'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √
'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √ 'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √
'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √ 'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √
'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 XXXXXXXX
'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表 'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表
// 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友 // 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友
// 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊 // 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊

View File

@@ -357,6 +357,7 @@ class ContentLibraryController extends Controller
// 初始化选项数组 // 初始化选项数组
$library['friendsGroupsOptions'] = []; $library['friendsGroupsOptions'] = [];
$library['wechatGroupsOptions'] = []; $library['wechatGroupsOptions'] = [];
$library['groupMembersOptions'] = [];
// 批量查询好友信息 // 批量查询好友信息
if (!empty($library['friendsGroups'])) { if (!empty($library['friendsGroups'])) {
@@ -385,6 +386,61 @@ class ContentLibraryController extends Controller
} }
} }
// 批量查询群成员信息
if (!empty($library['groupMembers'])) {
// groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]}
// 键是群组ID值是成员ID数组
$allMemberIds = [];
$groupMembersMap = [];
if (is_array($library['groupMembers'])) {
foreach ($library['groupMembers'] as $groupId => $memberIds) {
if (is_array($memberIds) && !empty($memberIds)) {
$allMemberIds = array_merge($allMemberIds, $memberIds);
// 保存群组ID和成员ID的映射关系
$groupMembersMap[$groupId] = $memberIds;
}
}
}
if (!empty($allMemberIds)) {
// 去重
$allMemberIds = array_unique($allMemberIds);
// 查询群成员信息
$members = Db::table('s2_wechat_chatroom_member')
->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime')
->whereIn('id', $allMemberIds)
->select();
// 将成员数据按ID建立索引
$membersById = [];
foreach ($members as $member) {
// 格式化时间字段
$member['createTime'] = !empty($member['createTime']) ? date('Y-m-d H:i:s', $member['createTime']) : '';
$member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : '';
$membersById[$member['id']] = $member;
}
// 按照群组ID分组返回
$groupMembersOptions = [];
foreach ($groupMembersMap as $groupId => $memberIds) {
$groupMembersOptions[$groupId] = [];
foreach ($memberIds as $memberId) {
if (isset($membersById[$memberId])) {
$groupMembersOptions[$groupId][] = $membersById[$memberId];
}
}
}
$library['groupMembersOptions'] = $groupMembersOptions;
} else {
$library['groupMembersOptions'] = [];
}
} else {
$library['groupMembersOptions'] = [];
}
//获取设备信息 //获取设备信息
if (!empty($library['deviceGroups'])) { if (!empty($library['deviceGroups'])) {
$deviceList = DeviceModel::alias('d') $deviceList = DeviceModel::alias('d')
@@ -921,7 +977,7 @@ class ContentLibraryController extends Controller
// 如果有发送者信息,也获取发送者详情 // 如果有发送者信息,也获取发送者详情
if (!empty($item['wechatId'])) { if (!empty($item['wechatId'])) {
$senderInfo = Db::name('wechat_chatroom_member') $senderInfo = Db::table('s2_wechat_chatroom_member')
->where([ ->where([
'chatroomId' => $groupInfo['chatroomId'], 'chatroomId' => $groupInfo['chatroomId'],
'wechatId' => $item['wechatId'] 'wechatId' => $item['wechatId']
@@ -1477,8 +1533,8 @@ class ContentLibraryController extends Controller
try { try {
// 查询群组信息 // 查询群组信息
$groups = Db::name('wechat_group')->alias('g') $groups = Db::table('s2_wechat_chatroom')->alias('g')
->field('g.id, g.chatroomId, g.name, g.ownerWechatId') ->field('g.id, g.chatroomId, g.nickname as name, g.wechatAccountWechatId as ownerWechatId')
->whereIn('g.id', $groupIds) ->whereIn('g.id', $groupIds)
->where('g.deleteTime', 0) ->where('g.deleteTime', 0)
->select(); ->select();
@@ -1500,12 +1556,59 @@ class ContentLibraryController extends Controller
]; ];
} }
// groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]}
// 键是群组ID值是该群组的成员ID数组
// 需要按群组分组处理,确保每个群组只采集该群组配置的成员
// 建立群组ID到成员ID数组的映射
$groupIdToMemberIds = [];
if (is_array($groupMembers)) {
foreach ($groupMembers as $groupId => $memberIds) {
if (is_array($memberIds) && !empty($memberIds)) {
$groupIdToMemberIds[$groupId] = $memberIds;
}
}
}
if (empty($groupIdToMemberIds)) {
return [
'status' => 'failed',
'message' => '未找到有效的群成员ID'
];
}
// 为每个群组查询成员信息建立群组ID到成员wechatId数组的映射
$groupIdToMemberWechatIds = [];
foreach ($groupIdToMemberIds as $groupId => $memberIds) {
// 查询该群组的成员信息获取wechatId
$members = Db::table('s2_wechat_chatroom_member')
->field('id, wechatId')
->whereIn('id', $memberIds)
->select();
$wechatIds = [];
foreach ($members as $member) {
if (!empty($member['wechatId'])) {
$wechatIds[] = $member['wechatId'];
}
}
if (!empty($wechatIds)) {
$groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds);
}
}
if (empty($groupIdToMemberWechatIds)) {
return [
'status' => 'failed',
'message' => '未找到有效的群成员微信ID'
];
}
// 从群组采集内容 // 从群组采集内容
$collectedData = []; $collectedData = [];
$totalMessagesCount = 0; $totalMessagesCount = 0;
$chatroomIds = array_column($groups, 'id'); $chatroomIds = array_column($groups, 'id');
// 获取群消息 - 支持时间范围过滤 // 获取群消息 - 支持时间范围过滤(先不添加群成员过滤,后面按群组分别过滤)
$messageWhere = [ $messageWhere = [
['wechatChatroomId', 'in', $chatroomIds], ['wechatChatroomId', 'in', $chatroomIds],
['type', '=', 2] ['type', '=', 2]
@@ -1516,7 +1619,7 @@ class ContentLibraryController extends Controller
$messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]]; $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]];
} }
// 查询群消息 // 查询群消息(先查询所有消息,后面按群组和成员过滤)
$groupMessages = Db::table('s2_wechat_message') $groupMessages = Db::table('s2_wechat_message')
->where($messageWhere) ->where($messageWhere)
->order('createTime', 'desc') ->order('createTime', 'desc')
@@ -1532,6 +1635,34 @@ class ContentLibraryController extends Controller
$groupedMessages = []; $groupedMessages = [];
foreach ($groupMessages as $message) { foreach ($groupMessages as $message) {
$chatroomId = $message['wechatChatroomId']; $chatroomId = $message['wechatChatroomId'];
$senderWechatId = $message['senderWechatId'] ?? '';
// 找到对应的群组信息
$groupInfo = null;
foreach ($groups as $group) {
if ($group['id'] == $chatroomId) {
$groupInfo = $group;
break;
}
}
if (!$groupInfo) {
continue;
}
// 检查该消息的发送者是否在该群组的配置成员列表中
$groupId = $groupInfo['id'];
if (!isset($groupIdToMemberWechatIds[$groupId])) {
// 该群组没有配置成员,跳过
continue;
}
// 检查发送者是否在配置的成员列表中
if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) {
// 发送者不在该群组的配置成员列表中,跳过
continue;
}
if (!isset($groupedMessages[$chatroomId])) { if (!isset($groupedMessages[$chatroomId])) {
$groupedMessages[$chatroomId] = [ $groupedMessages[$chatroomId] = [
'count' => 0, 'count' => 0,
@@ -1579,27 +1710,14 @@ class ContentLibraryController extends Controller
continue; continue;
} }
// 找到对应的群组信息
$groupInfo = null;
foreach ($groups as $group) {
if ($group['id'] == $chatroomId) {
$groupInfo = $group;
break;
}
}
if (!$groupInfo) {
continue;
}
// 如果启用了AI处理 // 如果启用了AI处理
if (!empty($library['aiEnabled']) && !empty($content)) { if (!empty($library['aiEnabled']) && !empty($content)) {
$contentAi = $this->aiRewrite($library, $content); $contentAi = $this->aiRewrite($library, $content);
if (!empty($content)) { if (!empty($contentAi)) {
$moment['contentAi'] = $contentAi; $message['contentAi'] = $contentAi;
} else { } else {
$moment['contentAi'] = ''; $message['contentAi'] = '';
} }
} }
@@ -1968,7 +2086,38 @@ class ContentLibraryController extends Controller
return true; return true;
} }
// 提取消息内容中的链接 $resUrls = [];
$content = '';
switch ($message['msgType']) {
case 1: // 文字
$content = $message['content'];
$contentType = 4;
break;
case 3: //图片
$resUrls[] = $message['content'];
$contentType = 1;
break;
case 47: //动态图片
$resUrls[] = $message['content'];
$contentType = 1;
break;
case 34: //语言
return false;
case 43: //视频
$resUrls[] = $message['content'];
$contentType = 3;
break;
case 42: //名片
return false;
case 49: //文件
$links = json_decode($message['content'],true);
return false;
default:
return false;
}
/*// 提取消息内容中的链接
$content = $message['content'] ?? ''; $content = $message['content'] ?? '';
$links = []; $links = [];
$pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; $pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/';
@@ -1986,6 +2135,8 @@ class ContentLibraryController extends Controller
// 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文)
$contentType = $this->determineContentType($content, $resUrls, $links); $contentType = $this->determineContentType($content, $resUrls, $links);
*/
// 创建新的内容项目 // 创建新的内容项目
$item = new ContentItem(); $item = new ContentItem();
@@ -1993,7 +2144,7 @@ class ContentLibraryController extends Controller
$item->type = 'group_message'; // 群消息类型 $item->type = 'group_message'; // 群消息类型
$item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息'; $item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息';
$item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE); $item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE);
$item->msgId = $message['msgId'] ?? ''; // 存储msgId便于后续查询 $item->msgId = $message['msgSvrId'] ?? ''; // 存储msgSvrId便于后续查询
$item->createTime = time(); $item->createTime = time();
$item->content = $content; $item->content = $content;
$item->contentType = $contentType; // 设置内容类型 $item->contentType = $contentType; // 设置内容类型
@@ -2011,13 +2162,17 @@ class ContentLibraryController extends Controller
if (!empty($resUrls[0])) { if (!empty($resUrls[0])) {
$item->coverImage = $resUrls[0]; $item->coverImage = $resUrls[0];
} }
}else{
$item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE);
} }
// 处理链接 // 处理链接
if (!empty($links)) { if (!empty($links)) {
$item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE);
}else{
$item->urls = json_encode([], JSON_UNESCAPED_UNICODE);
} }
$item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE);
// 设置商品信息(需根据消息内容解析) // 设置商品信息(需根据消息内容解析)
$this->extractProductInfo($item, $content); $this->extractProductInfo($item, $content);

View File

@@ -39,10 +39,10 @@ class GetChatroomListV1Controller extends BaseController
$where = []; $where = [];
if ($this->getUserInfo('isAdmin') == 1) { if ($this->getUserInfo('isAdmin') == 1) {
$where[] = ['g.deleteTime', '=', 0]; $where[] = ['gg.isDeleted', '=', 0];
$where[] = ['g.ownerWechatId', 'in', $wechatIds]; $where[] = ['g.ownerWechatId', 'in', $wechatIds];
} else { } else {
$where[] = ['g.deleteTime', '=', 0]; $where[] = ['gg.isDeleted', '=', 0];
$where[] = ['g.ownerWechatId', 'in', $wechatIds]; $where[] = ['g.ownerWechatId', 'in', $wechatIds];
//$where[] = ['g.userId', '=', $this->getUserInfo('id')]; //$where[] = ['g.userId', '=', $this->getUserInfo('id')];
} }
@@ -55,6 +55,7 @@ class GetChatroomListV1Controller extends BaseController
->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime', ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime',
'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias']) 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias'])
->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT')
->join(['s2_wechat_chatroom' => 'gg'], 'g.id = gg.id', 'LEFT')
->where($where); ->where($where);
$total = $data->count(); $total = $data->count();