内容库群选择功能 + 场景获客分页修复
This commit is contained in:
@@ -142,7 +142,11 @@ export default function SelectionPopup({
|
||||
// 复制一份selectedOptions到临时变量
|
||||
setTempSelectedOptions([...selectedOptions]);
|
||||
fetchGroups(1, "");
|
||||
} else {
|
||||
// 弹窗关闭时重置状态
|
||||
setTempSelectedOptions([]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible]);
|
||||
|
||||
// 搜索防抖(只在弹窗打开且搜索词变化时执行)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
363
Cunkebao/src/components/GroupSelectionWithMembers/index.tsx
Normal file
363
Cunkebao/src/components/GroupSelectionWithMembers/index.tsx
Normal 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;
|
||||
@@ -6,11 +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 { 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";
|
||||
|
||||
@@ -48,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);
|
||||
@@ -94,7 +99,30 @@ export default function ContentForm() {
|
||||
setSelectedDevices(deviceOptions || []);
|
||||
setSelectedFriends(data.sourceFriends || []);
|
||||
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 || []);
|
||||
setKeywordsInclude((data.keywordInclude || []).join(","));
|
||||
setKeywordsExclude((data.keywordExclude || []).join(","));
|
||||
@@ -140,7 +168,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<string, string[]>,
|
||||
),
|
||||
keywordInclude: keywordsInclude
|
||||
.split(/,|,|\n|\s+/)
|
||||
.map(s => s.trim())
|
||||
@@ -180,6 +216,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);
|
||||
@@ -335,9 +377,9 @@ export default function ContentForm() {
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="选择聊天群" key="groups">
|
||||
<GroupSelection
|
||||
selectedOptions={selectedGroupsOptions}
|
||||
onSelect={handleGroupsChange}
|
||||
<GroupSelectionWithMembers
|
||||
selectedGroups={selectedGroupsWithMembers}
|
||||
onSelect={handleGroupsWithMembersChange}
|
||||
placeholder="选择聊天群"
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
|
||||
export function getPlanList(params: {
|
||||
sceneId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
limit: number;
|
||||
}): Promise<PlanListResponse> {
|
||||
return request(`/v1/plan/list`, params, "GET");
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
<div className="pagination-container">
|
||||
<Pagination
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
current={currentPage}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
showQuickJumper={false}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style["scenario-list-page"]}>
|
||||
{/* 计划列表 */}
|
||||
@@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user