代码提交
This commit is contained in:
@@ -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;
|
||||
}
|
||||
206
Cunkebao/src/components/FriendSelection/TwoColumnSelection.tsx
Normal file
206
Cunkebao/src/components/FriendSelection/TwoColumnSelection.tsx
Normal file
@@ -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<TwoColumnSelectionProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
title = '选择好友',
|
||||
deviceIds = [],
|
||||
enableDeviceFilter = true,
|
||||
}) => {
|
||||
const [friends, setFriends] = useState<FriendSelectionItem[]>([]);
|
||||
const [selectedFriends, setSelectedFriends] = useState<FriendSelectionItem[]>([]);
|
||||
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 (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleCancelSelection}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancelSelection}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="confirm" type="primary" onClick={handleConfirmSelection}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
className={styles.twoColumnModal}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
{/* 左侧:好友列表 */}
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
placeholder="请输入昵称或微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.friendList}>
|
||||
{loading ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : friends.length > 0 ? (
|
||||
friends.map(friend => {
|
||||
const isSelected = selectedFriends.some(f => f.id === friend.id);
|
||||
return (
|
||||
<div
|
||||
key={friend.id}
|
||||
className={`${styles.friendItem} ${isSelected ? styles.selected : ''}`}
|
||||
onClick={() => handleSelectFriend(friend)}
|
||||
>
|
||||
<Checkbox checked={isSelected} />
|
||||
<Avatar src={friend.avatar} size={40}>
|
||||
{friend.nickname?.charAt(0)}
|
||||
</Avatar>
|
||||
<div className={styles.friendInfo}>
|
||||
<div className={styles.friendName}>{friend.nickname}</div>
|
||||
<div className={styles.friendId}>{friend.wechatId}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
{searchQuery ? `没有找到包含"${searchQuery}"的好友` : '暂无好友'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:已选好友 */}
|
||||
<div className={styles.rightColumn}>
|
||||
<div className={styles.selectedHeader}>
|
||||
已选联系人 ({selectedFriends.length})
|
||||
</div>
|
||||
|
||||
<div className={styles.selectedList}>
|
||||
{selectedFriends.length > 0 ? (
|
||||
selectedFriends.map(friend => (
|
||||
<div key={friend.id} className={styles.selectedItem}>
|
||||
<Avatar src={friend.avatar} size={32}>
|
||||
{friend.nickname?.charAt(0)}
|
||||
</Avatar>
|
||||
<div className={styles.selectedInfo}>
|
||||
<div className={styles.selectedName}>{friend.nickname}</div>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleRemoveFriend(friend.id)}
|
||||
className={styles.removeBtn}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.emptySelected}>
|
||||
暂无选择
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoColumnSelection;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TwoColumnMemberSelectionProps> = ({
|
||||
visible,
|
||||
members,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
title = '选择成员',
|
||||
allowMultiple = true,
|
||||
}) => {
|
||||
const [selectedMembers, setSelectedMembers] = useState<Member[]>([]);
|
||||
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 (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={handleCancelSelection}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancelSelection}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
onClick={handleConfirmSelection}
|
||||
disabled={selectedMembers.length === 0}
|
||||
>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
className={styles.twoColumnModal}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
{/* 左侧:成员列表 */}
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
placeholder="请输入昵称或微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.memberList}>
|
||||
{filteredMembers.length > 0 ? (
|
||||
filteredMembers.map(member => {
|
||||
const isSelected = selectedMembers.some(m => m.id === member.id);
|
||||
return (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`${styles.memberItem} ${isSelected ? styles.selected : ''}`}
|
||||
onClick={() => handleSelectMember(member)}
|
||||
>
|
||||
<Checkbox checked={isSelected} />
|
||||
<Avatar src={member.avatar} size={40}>
|
||||
{member.nickname?.charAt(0)}
|
||||
</Avatar>
|
||||
<div className={styles.memberInfo}>
|
||||
<div className={styles.memberName}>{member.nickname}</div>
|
||||
<div className={styles.memberId}>{member.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
{searchQuery ? `没有找到包含"${searchQuery}"的成员` : '暂无成员'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:已选成员 */}
|
||||
<div className={styles.rightColumn}>
|
||||
<div className={styles.selectedHeader}>
|
||||
已选成员 ({selectedMembers.length})
|
||||
{!allowMultiple && <span className={styles.singleTip}>(单选)</span>}
|
||||
</div>
|
||||
|
||||
<div className={styles.selectedList}>
|
||||
{selectedMembers.length > 0 ? (
|
||||
selectedMembers.map(member => (
|
||||
<div key={member.id} className={styles.selectedItem}>
|
||||
<Avatar src={member.avatar} size={32}>
|
||||
{member.nickname?.charAt(0)}
|
||||
</Avatar>
|
||||
<div className={styles.selectedInfo}>
|
||||
<div className={styles.selectedName}>{member.nickname}</div>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleRemoveMember(member.id)}
|
||||
className={styles.removeBtn}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.emptySelected}>
|
||||
暂无选择
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoColumnMemberSelection;
|
||||
Reference in New Issue
Block a user