diff --git a/Cunkebao/src/components/FriendSelection/TwoColumnSelection.module.scss b/Cunkebao/src/components/FriendSelection/TwoColumnSelection.module.scss new file mode 100644 index 00000000..d54b8ea1 --- /dev/null +++ b/Cunkebao/src/components/FriendSelection/TwoColumnSelection.module.scss @@ -0,0 +1,153 @@ +.twoColumnModal { + .ant-modal-body { + padding: 0; + } +} + +.container { + display: flex; + height: 500px; + border: 1px solid #e8e8e8; +} + +.leftColumn { + flex: 1; + border-right: 1px solid #e8e8e8; + display: flex; + flex-direction: column; +} + +.rightColumn { + width: 300px; + display: flex; + flex-direction: column; + background: #fafafa; +} + +.searchWrapper { + padding: 16px; + border-bottom: 1px solid #e8e8e8; + + .ant-input { + border-radius: 6px; + } +} + +.friendList { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.friendItem { + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + + &.selected { + background-color: #e6f7ff; + } + + .ant-checkbox { + margin-right: 12px; + } +} + +.friendInfo { + margin-left: 12px; + flex: 1; +} + +.friendName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.friendId { + font-size: 12px; + color: #999; +} + +.selectedHeader { + padding: 16px; + border-bottom: 1px solid #e8e8e8; + font-weight: 500; + color: #333; + background: #fff; +} + +.selectedList { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.selectedItem { + display: flex; + align-items: center; + padding: 8px 16px; + background: #fff; + margin: 4px 8px; + border-radius: 6px; + border: 1px solid #e8e8e8; +} + +.selectedInfo { + margin-left: 8px; + flex: 1; +} + +.selectedName { + font-size: 13px; + color: #333; +} + +.removeBtn { + color: #999; + font-size: 16px; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: #ff4d4f; + background: #fff2f0; + } +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #999; +} + +.empty { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #999; + font-size: 14px; +} + +.emptySelected { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #999; + font-size: 14px; +} \ No newline at end of file diff --git a/Cunkebao/src/components/FriendSelection/TwoColumnSelection.tsx b/Cunkebao/src/components/FriendSelection/TwoColumnSelection.tsx new file mode 100644 index 00000000..58e99c1a --- /dev/null +++ b/Cunkebao/src/components/FriendSelection/TwoColumnSelection.tsx @@ -0,0 +1,206 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { Modal, Input, Avatar, Button, Checkbox, message } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { getFriendList } from './api'; +import type { FriendSelectionItem } from './data'; +import styles from './TwoColumnSelection.module.scss'; + +interface TwoColumnSelectionProps { + visible: boolean; + onCancel: () => void; + onConfirm: (selectedIds: string[], selectedItems: FriendSelectionItem[]) => void; + title?: string; + deviceIds?: number[]; + enableDeviceFilter?: boolean; +} + +const TwoColumnSelection: React.FC = ({ + visible, + onCancel, + onConfirm, + title = '选择好友', + deviceIds = [], + enableDeviceFilter = true, +}) => { + const [friends, setFriends] = useState([]); + const [selectedFriends, setSelectedFriends] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + // 获取好友列表 + const fetchFriends = useCallback(async (page: number, keyword: string = '') => { + setLoading(true); + try { + const params: any = { + page, + pageSize: 20, + }; + + if (keyword) { + params.keyword = keyword; + } + + if (enableDeviceFilter && deviceIds.length > 0) { + params.deviceIds = deviceIds; + } + + const response = await getFriendList(params); + + if (response.success) { + setFriends(response.data.list || []); + setTotalPages(Math.ceil((response.data.total || 0) / 20)); + } else { + message.error(response.message || '获取好友列表失败'); + } + } catch (error) { + console.error('获取好友列表失败:', error); + message.error('获取好友列表失败'); + } finally { + setLoading(false); + } + }, [deviceIds, enableDeviceFilter]); + + // 初始化和搜索 + useEffect(() => { + if (visible) { + fetchFriends(1, searchQuery); + setCurrentPage(1); + } + }, [visible, searchQuery, fetchFriends]); + + // 处理搜索 + const handleSearch = (value: string) => { + setSearchQuery(value); + }; + + // 选择好友 + const handleSelectFriend = (friend: FriendSelectionItem) => { + const isSelected = selectedFriends.some(f => f.id === friend.id); + if (isSelected) { + setSelectedFriends(selectedFriends.filter(f => f.id !== friend.id)); + } else { + setSelectedFriends([...selectedFriends, friend]); + } + }; + + // 移除已选好友 + const handleRemoveFriend = (friendId: number) => { + setSelectedFriends(selectedFriends.filter(f => f.id !== friendId)); + }; + + // 确认选择 + const handleConfirmSelection = () => { + const selectedIds = selectedFriends.map(f => f.id.toString()); + onConfirm(selectedIds, selectedFriends); + setSelectedFriends([]); + setSearchQuery(''); + }; + + // 取消选择 + const handleCancelSelection = () => { + setSelectedFriends([]); + setSearchQuery(''); + onCancel(); + }; + + return ( + + 取消 + , + , + ]} + className={styles.twoColumnModal} + > +
+ {/* 左侧:好友列表 */} +
+
+ handleSearch(e.target.value)} + prefix={} + allowClear + /> +
+ +
+ {loading ? ( +
加载中...
+ ) : friends.length > 0 ? ( + friends.map(friend => { + const isSelected = selectedFriends.some(f => f.id === friend.id); + return ( +
handleSelectFriend(friend)} + > + + + {friend.nickname?.charAt(0)} + +
+
{friend.nickname}
+
{friend.wechatId}
+
+
+ ); + }) + ) : ( +
+ {searchQuery ? `没有找到包含"${searchQuery}"的好友` : '暂无好友'} +
+ )} +
+
+ + {/* 右侧:已选好友 */} +
+
+ 已选联系人 ({selectedFriends.length}) +
+ +
+ {selectedFriends.length > 0 ? ( + selectedFriends.map(friend => ( +
+ + {friend.nickname?.charAt(0)} + +
+
{friend.nickname}
+
+ +
+ )) + ) : ( +
+ 暂无选择 +
+ )} +
+
+
+
+ ); +}; + +export default TwoColumnSelection; \ No newline at end of file diff --git a/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.module.scss b/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.module.scss new file mode 100644 index 00000000..0ff3cbe4 --- /dev/null +++ b/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.module.scss @@ -0,0 +1,154 @@ +.twoColumnModal { + .ant-modal-body { + padding: 0; + } +} + +.container { + display: flex; + height: 500px; + border: 1px solid #e8e8e8; +} + +.leftColumn { + flex: 1; + border-right: 1px solid #e8e8e8; + display: flex; + flex-direction: column; +} + +.rightColumn { + width: 300px; + display: flex; + flex-direction: column; + background: #fafafa; +} + +.searchWrapper { + padding: 16px; + border-bottom: 1px solid #e8e8e8; + + .ant-input { + border-radius: 6px; + } +} + +.memberList { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.memberItem { + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + + &.selected { + background-color: #e6f7ff; + } + + .ant-checkbox { + margin-right: 12px; + } +} + +.memberInfo { + margin-left: 12px; + flex: 1; +} + +.memberName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.memberId { + font-size: 12px; + color: #999; +} + +.selectedHeader { + padding: 16px; + border-bottom: 1px solid #e8e8e8; + font-weight: 500; + color: #333; + background: #fff; + display: flex; + align-items: center; + justify-content: space-between; +} + +.singleTip { + font-size: 12px; + color: #999; + font-weight: normal; +} + +.selectedList { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.selectedItem { + display: flex; + align-items: center; + padding: 8px 16px; + background: #fff; + margin: 4px 8px; + border-radius: 6px; + border: 1px solid #e8e8e8; +} + +.selectedInfo { + margin-left: 8px; + flex: 1; +} + +.selectedName { + font-size: 13px; + color: #333; +} + +.removeBtn { + color: #999; + font-size: 16px; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: #ff4d4f; + background: #fff2f0; + } +} + +.empty { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #999; + font-size: 14px; +} + +.emptySelected { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: #999; + font-size: 14px; +} \ No newline at end of file diff --git a/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.tsx b/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.tsx new file mode 100644 index 00000000..ef276a68 --- /dev/null +++ b/Cunkebao/src/components/MemberSelection/TwoColumnMemberSelection.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react'; +import { Modal, Input, Avatar, Button, Checkbox } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import styles from './TwoColumnMemberSelection.module.scss'; + +interface Member { + id: string; + nickname: string; + avatar: string; +} + +interface TwoColumnMemberSelectionProps { + visible: boolean; + members: Member[]; + onCancel: () => void; + onConfirm: (selectedIds: string[]) => void; + title?: string; + allowMultiple?: boolean; +} + +const TwoColumnMemberSelection: React.FC = ({ + visible, + members, + onCancel, + onConfirm, + title = '选择成员', + allowMultiple = true, +}) => { + const [selectedMembers, setSelectedMembers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + + // 过滤成员 + const filteredMembers = members.filter(member => + member.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || + member.id.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // 处理搜索 + const handleSearch = (value: string) => { + setSearchQuery(value); + }; + + // 选择成员 + const handleSelectMember = (member: Member) => { + const isSelected = selectedMembers.some(m => m.id === member.id); + + if (allowMultiple) { + if (isSelected) { + setSelectedMembers(selectedMembers.filter(m => m.id !== member.id)); + } else { + setSelectedMembers([...selectedMembers, member]); + } + } else { + // 单选模式 + if (isSelected) { + setSelectedMembers([]); + } else { + setSelectedMembers([member]); + } + } + }; + + // 移除已选成员 + const handleRemoveMember = (memberId: string) => { + setSelectedMembers(selectedMembers.filter(m => m.id !== memberId)); + }; + + // 确认选择 + const handleConfirmSelection = () => { + const selectedIds = selectedMembers.map(m => m.id); + onConfirm(selectedIds); + setSelectedMembers([]); + setSearchQuery(''); + }; + + // 取消选择 + const handleCancelSelection = () => { + setSelectedMembers([]); + setSearchQuery(''); + onCancel(); + }; + + return ( + + 取消 + , + , + ]} + className={styles.twoColumnModal} + > +
+ {/* 左侧:成员列表 */} +
+
+ handleSearch(e.target.value)} + prefix={} + allowClear + /> +
+ +
+ {filteredMembers.length > 0 ? ( + filteredMembers.map(member => { + const isSelected = selectedMembers.some(m => m.id === member.id); + return ( +
handleSelectMember(member)} + > + + + {member.nickname?.charAt(0)} + +
+
{member.nickname}
+
{member.id}
+
+
+ ); + }) + ) : ( +
+ {searchQuery ? `没有找到包含"${searchQuery}"的成员` : '暂无成员'} +
+ )} +
+
+ + {/* 右侧:已选成员 */} +
+
+ 已选成员 ({selectedMembers.length}) + {!allowMultiple && (单选)} +
+ +
+ {selectedMembers.length > 0 ? ( + selectedMembers.map(member => ( +
+ + {member.nickname?.charAt(0)} + +
+
{member.nickname}
+
+ +
+ )) + ) : ( +
+ 暂无选择 +
+ )} +
+
+
+
+ ); +}; + +export default TwoColumnMemberSelection; \ No newline at end of file