feat(微信聊天): 添加消息转发功能

实现消息多选转发功能,包括:
1. 新增转发模态框组件,支持联系人搜索和多选
2. 在消息记录组件中添加转发操作逻辑
3. 在消息输入区域添加转发操作栏
4. 在微信状态管理中新增转发相关状态和方法
This commit is contained in:
超级老白兔
2025-09-19 10:24:25 +08:00
parent 8a7a717801
commit 593e6c4670
9 changed files with 655 additions and 4 deletions

View File

@@ -4,7 +4,55 @@
border-top: 1px solid #e1e1e1;
padding: 0;
height: auto;
min-height: 100px;
height: 200px;
}
// 多条转发操作栏样式
.multipleForwardingBar {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
padding: 12px 16px;
background: #ffffff;
border-top: 1px solid #e1e1e1;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
height: 200px;
}
.actionButton {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 60px;
&:hover {
background: #f5f5f5;
transform: translateY(-1px);
}
&:active {
background: #e8e8e8;
transform: translateY(0);
}
}
.actionIcon {
font-size: 20px;
color: #576b95;
margin-bottom: 2px;
}
.actionText {
font-size: 12px;
color: #333;
font-weight: 400;
white-space: nowrap;
}
.inputContainer {

View File

@@ -4,6 +4,8 @@ import {
SendOutlined,
FolderOutlined,
PictureOutlined,
ExportOutlined,
CloseOutlined,
} from "@ant-design/icons";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
@@ -28,6 +30,8 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
const [inputValue, setInputValue] = useState("");
const [showMaterialModal, setShowMaterialModal] = useState(false);
const EnterModule = useWeChatStore(state => state.EnterModule);
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
const handleSend = async () => {
if (!inputValue.trim()) return;
@@ -131,6 +135,14 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
sendCommand("CmdSendMessage", params);
};
const handleCancelAction = () => {
updateShowCheckbox(false);
updateEnterModule("common");
};
const handTurnRignt = () => {
console.log("转发");
};
return (
<>
{/* 聊天输入 */}
@@ -209,7 +221,19 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
</div>
</div>
)}
{["multipleForwarding"].includes(EnterModule)}
{["multipleForwarding"].includes(EnterModule) && (
<div className={styles.multipleForwardingBar}>
<div className={styles.actionButton} onClick={handTurnRignt}>
<ExportOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
<div className={styles.actionButton} onClick={handleCancelAction}>
<CloseOutlined className={styles.actionIcon} />
<span className={styles.actionText}></span>
</div>
</div>
)}
</Footer>
{/* 素材选择模态框 */}

View File

@@ -0,0 +1,95 @@
# TransmitModal 转发模态框组件
## 功能特性
- 🔍 支持联系人搜索(姓名和拼音)
- 👥 支持个人和群组联系人
- ✅ 多选功能,可设置最大选择数量
- 📱 响应式设计,适配移动端
- 🎨 微信风格UI设计
- ♿ 无障碍支持
## 使用方法
```tsx
import TransmitModal, { Contact } from './components/TransmitModal';
const contacts: Contact[] = [
{
id: '1',
name: '张三',
avatar: 'https://example.com/avatar1.jpg',
type: 'user',
pinyin: 'zhangsan'
},
{
id: '2',
name: '开发群',
type: 'group',
pinyin: 'kaifaqun'
}
];
function App() {
const [showModal, setShowModal] = useState(false);
const handleConfirm = (selectedContacts: Contact[]) => {
console.log('选择的联系人:', selectedContacts);
// 执行转发逻辑
setShowModal(false);
};
return (
<TransmitModal
open={showModal}
onCancel={() => setShowModal(false)}
onConfirm={handleConfirm}
contacts={contacts}
title="转发"
maxSelection={9}
/>
);
}
```
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| open | boolean | - | 是否显示模态框 |
| onCancel | () => void | - | 取消回调 |
| onConfirm | (contacts: Contact[]) => void | - | 确认回调 |
| contacts | Contact[] | - | 联系人列表 |
| title | string | '转发' | 模态框标题 |
| confirmText | string | '确定' | 确认按钮文本 |
| cancelText | string | '取消' | 取消按钮文本 |
| maxSelection | number | 9 | 最大选择数量 |
## Contact 接口
```tsx
interface Contact {
id: string; // 唯一标识
name: string; // 显示名称
avatar?: string; // 头像URL
type: 'user' | 'group'; // 类型:用户或群组
pinyin?: string; // 拼音,用于搜索
}
```
## 样式定制
组件使用 CSS Modules可以通过覆盖以下类名来定制样式
- `.transmitModal` - 模态框容器
- `.contactList` - 左侧联系人列表
- `.selectedList` - 右侧已选择列表
- `.contactItem` - 联系人项
- `.selectedItem` - 已选择项
## 注意事项
1. 确保传入的 `contacts` 数组中每个联系人的 `id` 唯一
2. `pinyin` 字段可选,但建议提供以支持拼音搜索
3. 组件会自动处理选择状态,无需外部维护
4. 达到最大选择数量时,未选择的联系人会被禁用

View File

@@ -0,0 +1,236 @@
// TransmitModal 组件样式 - 微信风格
.transmitModal {
:global(.ant-modal-content) {
padding: 0;
border-radius: 8px;
overflow: hidden;
}
:global(.ant-modal-header) {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
margin: 0;
}
:global(.ant-modal-body) {
padding: 0;
height: 400px;
}
:global(.ant-modal-footer) {
padding: 12px 20px;
border-top: 1px solid #e8e8e8;
text-align: right;
}
}
.modalContent {
height: 100%;
display: flex;
flex-direction: column;
}
.searchContainer {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
background: #fafafa;
}
.searchInput {
border-radius: 20px;
:global(.ant-input) {
border-radius: 20px;
background: #ffffff;
}
}
.contentBody {
flex: 1;
display: flex;
height: calc(100% - 60px);
}
.contactList {
flex: 1;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
}
.selectedList {
width: 200px;
display: flex;
flex-direction: column;
background: #f8f8f8;
}
.listHeader {
padding: 12px 16px;
background: #f0f0f0;
border-bottom: 1px solid #e8e8e8;
font-size: 14px;
font-weight: 500;
color: #333;
}
.listContent {
flex: 1;
overflow-y: auto;
padding: 8px 0;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
}
.contactItem {
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #f5f5f5;
}
:global(.ant-checkbox-wrapper) {
width: 100%;
margin: 0;
}
:global(.ant-checkbox) {
margin-right: 12px;
}
:global(.ant-checkbox-disabled) {
:global(.ant-checkbox-inner) {
background-color: #f5f5f5;
border-color: #d9d9d9;
}
}
}
.contactInfo {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.contactName {
font-size: 14px;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.groupIcon {
font-size: 12px;
color: #999;
margin-left: auto;
}
.selectedItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 4px 8px;
background: #ffffff;
border-radius: 6px;
border: 1px solid #e8e8e8;
transition: all 0.2s ease;
&:hover {
border-color: #d9d9d9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
.removeIcon {
font-size: 12px;
color: #999;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: all 0.2s ease;
&:hover {
color: #ff4d4f;
background: #fff2f0;
}
}
// 空状态样式
:global(.ant-empty) {
margin: 40px 0;
:global(.ant-empty-description) {
color: #999;
font-size: 13px;
}
}
// 响应式设计
@media (max-width: 768px) {
.transmitModal {
:global(.ant-modal) {
width: 90% !important;
max-width: none;
}
}
.selectedList {
width: 160px;
}
.contactName {
font-size: 13px;
}
}
// 按钮样式优化
:global(.ant-btn-primary) {
background: #07c160;
border-color: #07c160;
&:hover {
background: #06ad56;
border-color: #06ad56;
}
&:disabled {
background: #f5f5f5;
border-color: #d9d9d9;
color: #bfbfbf;
}
}
// 复选框样式优化
:global(.ant-checkbox-checked) {
:global(.ant-checkbox-inner) {
background-color: #07c160;
border-color: #07c160;
}
}
:global(.ant-checkbox-wrapper:hover) {
:global(.ant-checkbox-inner) {
border-color: #07c160;
}
}

View File

@@ -0,0 +1,218 @@
import React, { useState, useEffect, useMemo } from "react";
import { Modal, Input, Button, Avatar, Checkbox, Empty } from "antd";
import {
SearchOutlined,
CloseOutlined,
UserOutlined,
TeamOutlined,
} from "@ant-design/icons";
import styles from "./TransmitModal.module.scss";
export interface Contact {
id: string;
name: string;
avatar?: string;
type: "user" | "group";
pinyin?: string; // 用于搜索
}
export interface TransmitModalProps {
open: boolean;
onCancel: () => void;
onConfirm: (selectedContacts: Contact[]) => void;
contacts: Contact[];
title?: string;
confirmText?: string;
cancelText?: string;
maxSelection?: number;
}
const TransmitModal: React.FC<TransmitModalProps> = ({
open,
onCancel,
onConfirm,
contacts,
title = "转发",
confirmText = "确定",
cancelText = "取消",
maxSelection = 9,
}) => {
const [searchValue, setSearchValue] = useState("");
const [selectedContacts, setSelectedContacts] = useState<Contact[]>([]);
// 重置状态
useEffect(() => {
if (!open) {
setSearchValue("");
setSelectedContacts([]);
}
}, [open]);
// 过滤联系人
const filteredContacts = useMemo(() => {
if (!searchValue.trim()) return contacts;
const keyword = searchValue.toLowerCase();
return contacts.filter(
contact =>
contact.name.toLowerCase().includes(keyword) ||
contact.pinyin?.toLowerCase().includes(keyword),
);
}, [contacts, searchValue]);
// 处理联系人选择
const handleContactSelect = (contact: Contact, checked: boolean) => {
if (checked) {
if (selectedContacts.length >= maxSelection) {
return; // 达到最大选择数量
}
setSelectedContacts(prev => [...prev, contact]);
} else {
setSelectedContacts(prev => prev.filter(item => item.id !== contact.id));
}
};
// 移除已选择的联系人
const handleRemoveSelected = (contactId: string) => {
setSelectedContacts(prev => prev.filter(item => item.id !== contactId));
};
// 确认转发
const handleConfirm = () => {
onConfirm(selectedContacts);
};
// 检查联系人是否已选择
const isContactSelected = (contactId: string) => {
return selectedContacts.some(contact => contact.id === contactId);
};
return (
<Modal
title={title}
open={open}
onCancel={onCancel}
width={600}
height={500}
className={styles.transmitModal}
footer={[
<Button key="cancel" onClick={onCancel}>
{cancelText}
</Button>,
<Button
key="confirm"
type="primary"
onClick={handleConfirm}
disabled={selectedContacts.length === 0}
>
{confirmText}
</Button>,
]}
>
<div className={styles.modalContent}>
{/* 搜索框 */}
<div className={styles.searchContainer}>
<Input
placeholder="输入联系人或群名"
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
className={styles.searchInput}
/>
</div>
<div className={styles.contentBody}>
{/* 左侧联系人列表 */}
<div className={styles.contactList}>
<div className={styles.listHeader}>
<span></span>
</div>
<div className={styles.listContent}>
{filteredContacts.length > 0 ? (
filteredContacts.map(contact => (
<div key={contact.id} className={styles.contactItem}>
<Checkbox
checked={isContactSelected(contact.id)}
onChange={e =>
handleContactSelect(contact, e.target.checked)
}
disabled={
!isContactSelected(contact.id) &&
selectedContacts.length >= maxSelection
}
>
<div className={styles.contactInfo}>
<Avatar
size={32}
src={contact.avatar}
icon={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<span className={styles.contactName}>
{contact.name}
</span>
{contact.type === "group" && (
<TeamOutlined className={styles.groupIcon} />
)}
</div>
</Checkbox>
</div>
))
) : (
<Empty
description="暂无联系人"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</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={
contact.type === "group" ? (
<TeamOutlined />
) : (
<UserOutlined />
)
}
/>
<span className={styles.contactName}>{contact.name}</span>
</div>
<CloseOutlined
className={styles.removeIcon}
onClick={() => handleRemoveSelected(contact.id)}
/>
</div>
))
) : (
<Empty
description="请选择联系人"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</div>
</div>
</div>
</div>
</Modal>
);
};
export default TransmitModal;

View File

@@ -0,0 +1,2 @@
export { default } from "./TransmitModal";
export type { TransmitModalProps, Contact } from "./TransmitModal";

View File

@@ -686,8 +686,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
break;
case "multipleForwarding":
// 多条转发逻辑
updateEnterModule(!showCheckbox ? "multipleForwarding" : "common");
updateShowCheckbox(!showCheckbox);
updateEnterModule(!showCheckbox ? "common" : "multipleForwarding");
break;
case "quote":
// 引用逻辑

View File

@@ -6,6 +6,15 @@ import {
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
// 微信聊天相关的类型定义
export interface WeChatState {
//选择聊天记录
selectedMessage: ChatRecord[];
updateSelectedMessage: (message: ChatRecord[]) => void;
//选择用户或群
selectedContact: ContractData[] | weChatGroup[];
updateSelectedContact: (contact: ContractData[] | weChatGroup[]) => void;
openTransmitModal: boolean;
updateTransmitModal: (open: boolean) => void;
// 当前选中的联系人/群组
currentContract: ContractData | weChatGroup | null;
// CurrentContact 相关方法

View File

@@ -6,12 +6,13 @@ import {
getGroupMembers,
} from "@/pages/pc/ckbox/api";
import { WeChatState } from "./weChat.data";
import {
likeListItem,
CommentItem,
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
import { clearUnreadCount, updateConfig } from "@/pages/pc/ckbox/api";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import { weChatGroupService, contractService } from "@/utils/db";
import {
addChatSession,
@@ -22,6 +23,23 @@ import {
export const useWeChatStore = create<WeChatState>()(
persist(
(set, get) => ({
//选择聊天记录
selectedMessage: [],
updateSelectedMessage: (message: ChatRecord[]) => {
set({ selectedMessage: message });
},
//选择用户或群
selectedContact: [],
updateSelectedContact: (contact: ContractData[] | weChatGroup[]) => {
set({ selectedContact: contact });
},
//打开转发弹窗
openTransmitModal: false,
updateTransmitModal: (open: boolean) => {
set({ openTransmitModal: open });
},
// 初始状态
currentContract: null,
currentMessages: [],