Add dropdown menu in SidebarMenu for adding friends and creating group chats, including modals for each action

This commit is contained in:
超级老白兔
2025-11-08 17:00:40 +08:00
parent d722c03744
commit daf504d5fd
6 changed files with 777 additions and 3 deletions

View File

@@ -0,0 +1,4 @@
import request from "@/api/request2";
export const getWechatAccountInfo = (params: { id?: string }) => {
return request("/api/wechataccount", params, "GET");
};

View File

@@ -0,0 +1,102 @@
.addFriendModal {
.ant-modal-body {
padding: 24px;
}
.ant-modal-header {
display: none;
}
}
.modalContent {
display: flex;
flex-direction: column;
gap: 16px;
}
.searchInputWrapper {
.ant-input {
height: 40px;
border-radius: 4px;
}
}
.tipText {
font-size: 14px;
color: #666;
margin-top: 8px;
margin-bottom: 8px;
}
.greetingWrapper {
margin-bottom: 20px;
.ant-input {
resize: none;
border-radius: 4px;
}
}
.formRow {
display: flex;
align-items: center;
margin-bottom: 16px;
.label {
width: 60px;
font-size: 14px;
color: #333;
flex-shrink: 0;
}
.inputField {
flex: 1;
height: 36px;
border-radius: 4px;
}
.selectField {
flex: 1;
border-radius: 4px;
.ant-select-selector {
height: 36px;
border-radius: 4px;
}
}
}
.buttonGroup {
display: flex;
gap: 12px;
margin-top: 8px;
justify-content: flex-start;
.addButton {
background-color: #52c41a;
border-color: #52c41a;
color: #fff;
height: 36px;
padding: 0 24px;
border-radius: 4px;
font-size: 14px;
&:hover {
background-color: #73d13d;
border-color: #73d13d;
}
}
.cancelButton {
height: 36px;
padding: 0 24px;
border-radius: 4px;
font-size: 14px;
border-color: #d9d9d9;
color: #333;
&:hover {
border-color: #40a9ff;
color: #40a9ff;
}
}
}

View File

@@ -0,0 +1,174 @@
import React, { useState } from "react";
import { Modal, Input, Button, message, Select } from "antd";
import { SearchOutlined, DownOutlined } from "@ant-design/icons";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { useCustomerStore } from "@/store/module/weChat/customer";
import styles from "./index.module.scss";
interface AddFriendsProps {
visible: boolean;
onCancel: () => void;
}
const AddFriends: React.FC<AddFriendsProps> = ({ visible, onCancel }) => {
const [searchValue, setSearchValue] = useState("");
const [greeting, setGreeting] = useState("我是老坑爹-解放双手,释放时间");
const [remark, setRemark] = useState("");
const [selectedTag, setSelectedTag] = useState<string | undefined>(undefined);
const [loading, setLoading] = useState(false);
const { sendCommand } = useWebSocketStore();
const currentCustomer = useCustomerStore(state => state.currentCustomer);
// 获取标签列表(从 currentCustomer.labels 字符串数组)
const tags = currentCustomer?.labels || [];
// 重置表单
const handleReset = () => {
setSearchValue("");
setGreeting("我是老坑爹-解放双手,释放时间");
setRemark("");
setSelectedTag(undefined);
};
// 处理取消
const handleCancel = () => {
handleReset();
onCancel();
};
// 判断是否为手机号11位数字
const isPhoneNumber = (value: string): boolean => {
return /^1[3-9]\d{9}$/.test(value.trim());
};
// 处理添加好友
const handleAddFriend = async () => {
if (!searchValue.trim()) {
message.warning("请输入微信号或手机号");
return;
}
if (!currentCustomer?.id) {
message.error("请先选择客服账号");
return;
}
setLoading(true);
try {
const trimmedValue = searchValue.trim();
const isPhone = isPhoneNumber(trimmedValue);
// 发送添加好友命令
sendCommand("CmdSendFriendRequest", {
WechatAccountId: currentCustomer.id,
TargetWechatId: isPhone ? "" : trimmedValue,
Phone: isPhone ? trimmedValue : "",
Message: greeting.trim() || "我是老坑爹-解放双手,释放时间",
Remark: remark.trim() || "",
Labels: selectedTag || "",
});
message.success("好友请求已发送");
handleCancel();
} catch (error) {
console.error("添加好友失败:", error);
message.error("添加好友失败,请重试");
} finally {
setLoading(false);
}
};
return (
<Modal
title={null}
open={visible}
onCancel={handleCancel}
footer={null}
className={styles.addFriendModal}
width={480}
closable={false}
>
<div className={styles.modalContent}>
{/* 搜索输入框 */}
<div className={styles.searchInputWrapper}>
<Input
placeholder="请输入微信号/手机号"
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
onPressEnter={handleAddFriend}
disabled={loading}
allowClear
/>
</div>
{/* 提示文字 */}
<div className={styles.tipText}>,</div>
{/* 验证消息文本区域 */}
<div className={styles.greetingWrapper}>
<Input.TextArea
value={greeting}
onChange={e => setGreeting(e.target.value)}
rows={4}
disabled={loading}
maxLength={200}
/>
</div>
{/* 备注输入框 */}
<div className={styles.formRow}>
<span className={styles.label}>:</span>
<Input
value={remark}
onChange={e => setRemark(e.target.value)}
placeholder="请输入备注"
disabled={loading}
className={styles.inputField}
/>
</div>
{/* 标签选择器 */}
<div className={styles.formRow}>
<span className={styles.label}>:</span>
<Select
value={selectedTag}
onChange={setSelectedTag}
placeholder="请选择标签"
disabled={loading}
className={styles.selectField}
suffixIcon={<DownOutlined />}
allowClear
>
{tags.map(tag => (
<Select.Option key={tag} value={tag}>
{tag}
</Select.Option>
))}
</Select>
</div>
{/* 底部按钮 */}
<div className={styles.buttonGroup}>
<Button
type="primary"
onClick={handleAddFriend}
loading={loading}
className={styles.addButton}
>
</Button>
<Button
onClick={handleCancel}
disabled={loading}
className={styles.cancelButton}
>
</Button>
</div>
</div>
</Modal>
);
};
export default AddFriends;

View File

@@ -0,0 +1,153 @@
.popChatRoomModal {
.ant-modal-content {
height: 500px;
}
.ant-modal-body {
height: calc(500px - 110px);
overflow: hidden;
padding: 16px;
}
}
.modalContent {
height: 100%;
display: flex;
flex-direction: column;
}
.searchContainer {
margin-bottom: 16px;
.searchInput {
width: 100%;
}
}
.contentBody {
flex: 1;
display: flex;
gap: 16px;
min-height: 0;
}
.contactList,
.selectedList {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.listHeader {
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #d9d9d9;
font-weight: 500;
font-size: 14px;
color: #262626;
}
.listContent {
flex: 1;
overflow-y: auto;
padding: 8px;
min-height: 0;
}
.contactItem,
.selectedItem {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
&:last-child {
margin-bottom: 0;
}
}
.paginationContainer {
padding: 12px;
border-top: 1px solid #d9d9d9;
background-color: #fafafa;
}
.selectedItem {
justify-content: space-between;
}
.contactInfo {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.contactName {
font-size: 14px;
color: #262626;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conRemark {
font-size: 12px;
color: #8c8c8c;
}
.removeIcon {
color: #8c8c8c;
cursor: pointer;
padding: 4px;
border-radius: 2px;
transition: all 0.2s;
&:hover {
color: #ff4d4f;
background-color: #fff2f0;
}
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
flex-direction: column;
gap: 12px;
color: #8c8c8c;
}
// 响应式设计
@media (max-width: 768px) {
.popChatRoomModal {
.ant-modal-content {
max-height: 600px;
}
.ant-modal-body {
max-height: calc(600px - 110px);
}
}
.contentBody {
flex-direction: column;
gap: 12px;
}
.contactList,
.selectedList {
min-height: 200px;
}
}

View File

@@ -0,0 +1,293 @@
import React, { useState, useEffect, useMemo } from "react";
import {
Modal,
Input,
Button,
Avatar,
Checkbox,
Empty,
Spin,
message,
Pagination,
} from "antd";
import { SearchOutlined, CloseOutlined, UserOutlined } from "@ant-design/icons";
import styles from "./index.module.scss";
import { ContactManager } from "@/utils/dbAction";
import { useUserStore } from "@/store/module/user";
import { useCustomerStore } from "@/store/module/weChat/customer";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { Contact } from "@/utils/db";
interface PopChatRoomProps {
visible: boolean;
onCancel: () => void;
}
const PopChatRoom: React.FC<PopChatRoomProps> = ({ visible, onCancel }) => {
const [searchValue, setSearchValue] = useState("");
const [allContacts, setAllContacts] = useState<Contact[]>([]);
const [selectedContacts, setSelectedContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const pageSize = 20;
const { sendCommand } = useWebSocketStore();
const currentUserId = useUserStore(state => state.user?.id) || 0;
const currentCustomer = useCustomerStore(state => state.currentCustomer);
// 加载联系人数据(只加载好友,不包含群聊)
const loadContacts = async () => {
setLoading(true);
try {
const allContactsData =
await ContactManager.getUserContacts(currentUserId);
// 过滤出好友类型,排除群聊
const friendsOnly = (allContactsData as Contact[]).filter(
contact => contact.type === "friend",
);
setAllContacts(friendsOnly);
} catch (err) {
console.error("加载联系人数据失败:", err);
message.error("加载联系人数据失败");
} finally {
setLoading(false);
}
};
// 重置状态
useEffect(() => {
if (visible) {
setSearchValue("");
setSelectedContacts([]);
setPage(1);
loadContacts();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
// 过滤联系人 - 支持名称和拼音搜索
const filteredContacts = useMemo(() => {
if (!searchValue.trim()) return allContacts;
const keyword = searchValue.toLowerCase();
return allContacts.filter(contact => {
const name = (contact.nickname || "").toLowerCase();
const remark = (contact.conRemark || "").toLowerCase();
const quanPin = (contact as any).quanPin?.toLowerCase?.() || "";
const pinyin = (contact as any).pinyin?.toLowerCase?.() || "";
return (
name.includes(keyword) ||
remark.includes(keyword) ||
quanPin.includes(keyword) ||
pinyin.includes(keyword)
);
});
}, [allContacts, searchValue]);
const paginatedContacts = useMemo(() => {
const start = (page - 1) * pageSize;
const end = start + pageSize;
return filteredContacts.slice(start, end);
}, [filteredContacts, page]);
// 处理联系人选择
const handleContactSelect = (contact: Contact) => {
setSelectedContacts(prev => {
if (isContactSelected(contact.id)) {
return prev.filter(item => item.id !== contact.id);
}
return [...prev, contact];
});
};
// 移除已选择的联系人
const handleRemoveSelected = (contactId: number) => {
setSelectedContacts(prev =>
prev.filter(contact => contact.id !== contactId),
);
};
// 检查联系人是否已选择
const isContactSelected = (contactId: number) => {
return selectedContacts.some(contact => contact.id === contactId);
};
// 处理取消
const handleCancel = () => {
setSearchValue("");
setSelectedContacts([]);
setPage(1);
onCancel();
};
// 创建群聊
const handleCreateGroup = () => {
if (selectedContacts.length === 0) {
message.warning("请至少选择一个联系人");
return;
}
if (!currentCustomer?.id) {
message.error("请先选择客服账号");
return;
}
// 获取选中的好友ID列表
const friendIds = selectedContacts.map(contact => contact.id);
try {
// 发送创建群聊命令
sendCommand("CmdCreateChatroom", {
wechatAccountId: currentCustomer.id,
wechatFriendIds: friendIds,
});
message.success("群聊创建请求已发送");
handleCancel();
} catch (error) {
console.error("创建群聊失败:", error);
message.error("创建群聊失败,请重试");
}
};
return (
<Modal
title="发起群聊"
open={visible}
onCancel={handleCancel}
width="60%"
className={styles.popChatRoomModal}
footer={[
<Button key="cancel" onClick={handleCancel}>
</Button>,
<Button
key="create"
type="primary"
onClick={handleCreateGroup}
disabled={selectedContacts.length === 0}
loading={loading}
>
{selectedContacts.length > 0 && ` (${selectedContacts.length})`}
</Button>,
]}
>
<div className={styles.modalContent}>
{/* 搜索框 */}
<div className={styles.searchContainer}>
<Input
placeholder="请输入昵称/微信号 搜索好友"
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
className={styles.searchInput}
disabled={loading}
allowClear
/>
</div>
<div className={styles.contentBody}>
{/* 左侧联系人列表 */}
<div className={styles.contactList}>
<div className={styles.listHeader}>
<span> ({filteredContacts.length})</span>
</div>
<div className={styles.listContent}>
{loading ? (
<div className={styles.loadingContainer}>
<Spin size="large" />
<span>...</span>
</div>
) : filteredContacts.length > 0 ? (
paginatedContacts.map(contact => (
<div key={contact.id} className={styles.contactItem}>
<Checkbox
checked={isContactSelected(contact.id)}
onChange={() => handleContactSelect(contact)}
>
<div className={styles.contactInfo}>
<Avatar
size={32}
src={contact.avatar}
icon={<UserOutlined />}
/>
<div className={styles.contactName}>
<div>{contact.nickname}</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
</div>
</Checkbox>
</div>
))
) : (
<Empty
description={
searchValue ? "未找到匹配的联系人" : "暂无联系人"
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
{filteredContacts.length > 0 && (
<div className={styles.paginationContainer}>
<Pagination
size="small"
current={page}
pageSize={pageSize}
total={filteredContacts.length}
onChange={p => setPage(p)}
showSizeChanger={false}
/>
</div>
)}
</div>
{/* 右侧已选择列表 */}
<div className={styles.selectedList}>
<div className={styles.listHeader}>
<span> ({selectedContacts.length})</span>
</div>
<div className={styles.listContent}>
{selectedContacts.length > 0 ? (
selectedContacts.map(contact => (
<div key={contact.id} className={styles.selectedItem}>
<div className={styles.contactInfo}>
<Avatar
size={32}
src={contact.avatar}
icon={<UserOutlined />}
/>
<div className={styles.contactName}>
<div>{contact.nickname}</div>
{contact.conRemark && (
<div className={styles.conRemark}>
{contact.conRemark}
</div>
)}
</div>
</div>
<CloseOutlined
className={styles.removeIcon}
onClick={() => handleRemoveSelected(contact.id)}
/>
</div>
))
) : (
<Empty
description="请选择联系人"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</div>
</div>
</div>
</Modal>
);
};
export default PopChatRoom;

View File

@@ -1,9 +1,16 @@
import React, { useState, useEffect } from "react";
import { Input, Skeleton, Button } from "antd";
import { SearchOutlined, PlusOutlined } from "@ant-design/icons";
import { Input, Skeleton, Button, Dropdown, MenuProps } from "antd";
import {
SearchOutlined,
PlusOutlined,
UserAddOutlined,
TeamOutlined,
} from "@ant-design/icons";
import WechatFriends from "./WechatFriends";
import MessageList from "./MessageList/index";
import FriendsCircle from "./FriendsCicle";
import AddFriends from "./AddFriends";
import PopChatRoom from "./PopChatRoom";
import styles from "./SidebarMenu.module.scss";
import { useContactStore } from "@/store/module/weChat/contacts";
import { useCustomerStore } from "@/store/module/weChat/customer";
@@ -28,6 +35,9 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
const [activeTab, setActiveTab] = useState("chats");
const [switchingTab, setSwitchingTab] = useState(false); // tab切换加载状态
const [isAddFriendModalVisible, setIsAddFriendModalVisible] = useState(false);
const [isCreateGroupModalVisible, setIsCreateGroupModalVisible] =
useState(false);
// 监听 currentContact 变化自动切换到聊天tab并选中会话
useEffect(() => {
@@ -68,6 +78,26 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
clearSearchKeyword();
};
// 下拉菜单项
const menuItems: MenuProps["items"] = [
{
key: "addFriend",
label: "添加好友",
icon: <UserAddOutlined />,
onClick: () => {
setIsAddFriendModalVisible(true);
},
},
{
key: "createGroup",
label: "发起群聊",
icon: <TeamOutlined />,
onClick: () => {
setIsCreateGroupModalVisible(true);
},
},
];
// 渲染骨架屏
const renderSkeleton = () => (
<div className={styles.skeletonContainer}>
@@ -126,7 +156,15 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
onClear={handleClearSearch}
allowClear
/>
<Button type="primary" icon={<PlusOutlined />}></Button>
{currentCustomer && (
<Dropdown
menu={{ items: menuItems }}
trigger={["click"]}
placement="bottomRight"
>
<Button type="primary" icon={<PlusOutlined />}></Button>
</Dropdown>
)}
</div>
{/* 标签页切换 */}
@@ -182,6 +220,16 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
<div className={styles.sidebarMenu}>
{renderHeader()}
<div className={styles.contentContainer}>{renderContent()}</div>
{/* 添加好友弹窗 */}
<AddFriends
visible={isAddFriendModalVisible}
onCancel={() => setIsAddFriendModalVisible(false)}
/>
{/* 发起群聊弹窗 */}
<PopChatRoom
visible={isCreateGroupModalVisible}
onCancel={() => setIsCreateGroupModalVisible(false)}
/>
</div>
);
};