feat(微信聊天): 添加聊天记录搜索和客户转接功能

- 在微信聊天界面新增聊天记录搜索组件,支持按时间和关键词搜索
- 添加客户转接功能组件,支持选择目标客服和添加附言
- 实现一键转回功能
- 添加相关API接口和状态管理
- 配置antd中文语言环境和dayjs本地化
This commit is contained in:
超级老白兔
2025-09-10 16:52:28 +08:00
parent 46d6641259
commit 51b1918f72
7 changed files with 510 additions and 10 deletions

View File

@@ -1,10 +1,17 @@
// main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import App from "./App";
import "./styles/global.scss";
import { db } from "@/utils/db"; // 引入数据库实例
// 设置dayjs为中文
dayjs.locale("zh-cn");
// 数据库初始化
async function initializeApp() {
try {
@@ -26,7 +33,11 @@ async function initializeApp() {
// 渲染应用
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
root.render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>,
);
}
// 启动应用

View File

@@ -13,7 +13,48 @@ import {
//读取聊天信息
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
function jsonToQueryString(json) {
const params = new URLSearchParams();
for (const key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
params.append(key, json[key]);
}
}
return params.toString();
}
//转移客户
export function WechatFriendAllot(params: {
wechatFriendId?: number;
wechatChatroomId?: number;
toAccountId: number;
notifyReceiver: boolean;
comment: string;
}) {
return request(
"/api/wechatFriend/allot?" + jsonToQueryString(params),
undefined,
"PUT",
);
}
//获取可转移客服列表
export function getTransferableAgentList() {
return request("/api/account/myDepartmentAccountsForTransfer", {}, "GET");
}
// 微信好友列表
export function WechatFriendRebackAllot(params: {
wechatFriendId?: number;
wechatChatroomId?: number;
}) {
return request(
"/api/wechatFriend/rebackAllot?" + jsonToQueryString(params),
undefined,
"PUT",
);
}
// 微信群列表
export function WechatGroup(params) {
return request("/api/WechatGroup/list", params, "GET");
}

View File

@@ -0,0 +1,161 @@
import React, { useState } from "react";
import { Button, Modal, Input, DatePicker, message } from "antd";
import { MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
const { RangePicker } = DatePicker;
interface ChatRecordProps {
onSearch?: (data: { dateRange: [string, string]; content: string }) => void;
className?: string;
disabled?: boolean;
}
const ChatRecord: React.FC<ChatRecordProps> = ({
onSearch,
className,
disabled = false,
}) => {
const [visible, setVisible] = useState(false);
const [searchContent, setSearchContent] = useState<string>("");
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(
null,
);
const [loading, setLoading] = useState(false);
// 打开弹窗
const openModal = () => {
setVisible(true);
};
// 关闭弹窗并重置状态
const closeModal = () => {
setVisible(false);
setSearchContent("");
setDateRange(null);
setLoading(false);
};
// 执行查找
const handleSearch = async () => {
if (!dateRange) {
message.warning("请选择时间范围");
return;
}
if (!searchContent.trim()) {
message.warning("请输入查找内容");
return;
}
try {
setLoading(true);
const searchData = {
dateRange: [
dateRange[0].format("YYYY-MM-DD 00:00:00"),
dateRange[1].format("YYYY-MM-DD 23:59:59"),
] as [string, string],
content: searchContent.trim(),
};
// 调用回调函数
if (onSearch) {
await onSearch(searchData);
}
message.success("查找完成");
closeModal();
} catch (error) {
console.error("查找失败:", error);
message.error("查找失败,请重试");
} finally {
setLoading(false);
}
};
return (
<>
<div
className={className}
onClick={openModal}
style={{
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.5 : 1,
}}
>
<MessageOutlined />
</div>
<Modal
title="查找聊天记录"
open={visible}
onCancel={closeModal}
width={450}
centered
maskClosable={!loading}
footer={[
<div
key="footer"
style={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<Button
onClick={closeModal}
disabled={loading}
style={{ marginRight: "8px" }}
>
</Button>
<Button type="primary" loading={loading} onClick={handleSearch}>
</Button>
</div>,
]}
>
<div style={{ padding: "20px 0" }}>
{/* 时间范围选择 */}
<div style={{ marginBottom: "20px" }}>
<div
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
>
</div>
<RangePicker
value={dateRange}
onChange={setDateRange}
style={{ width: "100%" }}
size="large"
disabled={loading}
placeholder={["开始日期", "结束日期"]}
/>
</div>
{/* 查找内容输入 */}
<div>
<div
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
>
</div>
<Input
placeholder="请输入要查找的关键词或内容"
value={searchContent}
onChange={e => setSearchContent(e.target.value)}
size="large"
maxLength={100}
showCount
disabled={loading}
/>
</div>
</div>
</Modal>
</>
);
};
export default ChatRecord;

View File

@@ -0,0 +1,226 @@
import React, { useState } from "react";
import { Button, Modal, Select, Input, message } from "antd";
import { ShareAltOutlined } from "@ant-design/icons";
import {
getTransferableAgentList,
WechatFriendAllot,
WechatFriendRebackAllot,
} from "@/pages/pc/ckbox/weChat/api";
import { useCurrentContact } from "@/store/module/weChat/weChat";
const { TextArea } = Input;
const { Option } = Select;
interface ToContractProps {
className?: string;
disabled?: boolean;
}
interface DepartItem {
id: number;
userName: string;
realName: string;
nickname: string;
avatar: string;
memo: string;
departmentId: number;
alive: boolean;
}
const ToContract: React.FC<ToContractProps> = ({
className,
disabled = false,
}) => {
const currentContact = useCurrentContact();
const [visible, setVisible] = useState(false);
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
const [comment, setComment] = useState<string>("");
const [loading, setLoading] = useState(false);
const [customerServiceList, setCustomerServiceList] = useState<DepartItem[]>(
[],
);
// 打开弹窗
const openModal = () => {
setVisible(true);
getTransferableAgentList().then(data => {
setCustomerServiceList(data);
});
};
// 关闭弹窗并重置状态
const closeModal = () => {
setVisible(false);
setSelectedTarget(null);
setComment("");
setLoading(false);
};
// 确定转给他人
const handleConfirm = async () => {
if (!selectedTarget) {
message.warning("请选择目标客服");
return;
}
try {
setLoading(true);
console.log(currentContact);
// 调用转接接口
if (currentContact) {
if ("chatroomId" in currentContact && currentContact.chatroomId) {
await WechatFriendAllot({
wechatChatroomId: currentContact.id,
toAccountId: selectedTarget as number,
notifyReceiver: true,
comment: comment.trim(),
});
} else {
await WechatFriendAllot({
wechatFriendId: currentContact.id,
toAccountId: selectedTarget as number,
notifyReceiver: true,
comment: comment.trim(),
});
}
}
message.success("转接成功");
closeModal();
} catch (error) {
console.error("转接失败:", error);
message.error("转接失败,请重试");
} finally {
setLoading(false);
}
};
// 一键转回
const handleReturn = async () => {
try {
setLoading(true);
// 调用转回接口
if (currentContact) {
if ("chatroomId" in currentContact && currentContact.chatroomId) {
await WechatFriendRebackAllot({
wechatChatroomId: currentContact.id,
});
} else {
await WechatFriendRebackAllot({
wechatFriendId: currentContact.id,
});
}
}
message.success("转回成功");
closeModal();
} catch (error) {
console.error("转回失败:", error);
message.error("转回失败,请重试");
} finally {
setLoading(false);
}
};
return (
<>
<div
className={className}
onClick={openModal}
style={{
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.5 : 1,
}}
>
<ShareAltOutlined />
</div>
<Modal
title="转给他人"
open={visible}
onCancel={closeModal}
width={400}
centered
maskClosable={!loading}
footer={[
<div
key="footer"
style={{
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<Button onClick={handleReturn} disabled={loading}>
</Button>
<div>
<Button
onClick={closeModal}
disabled={loading}
style={{ marginRight: "8px" }}
>
</Button>
<Button
type="primary"
loading={loading}
onClick={handleConfirm}
disabled={!selectedTarget}
>
</Button>
</div>
</div>,
]}
>
<div style={{ padding: "20px 0" }}>
{/* 目标客服选择 */}
<div style={{ marginBottom: "20px" }}>
<div
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
>
</div>
<Select
placeholder="请选择目标客服"
value={selectedTarget}
onChange={setSelectedTarget}
style={{ width: "100%" }}
size="large"
disabled={loading}
>
{customerServiceList.map(item => (
<Option key={item.id} value={item.id}>
{item.nickname || item.realName} - {item.userName}
</Option>
))}
</Select>
</div>
{/* 附言输入 */}
<div>
<div
style={{ marginBottom: "8px", fontSize: "14px", color: "#333" }}
>
</div>
<TextArea
placeholder="请输入附言内容"
value={comment}
onChange={e => setComment(e.target.value)}
rows={4}
maxLength={300}
showCount
style={{ resize: "none" }}
disabled={loading}
/>
</div>
</div>
</Modal>
</>
);
};
export default ToContract;

View File

@@ -14,6 +14,8 @@ import { EmojiPicker } from "@/components/EmojiSeclection";
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
import AudioRecorder from "@/components/Upload/AudioRecorder";
import ToContract from "./components/toContract";
import ChatRecord from "./components/chatRecord";
import styles from "./MessageEnter.module.scss";
const { Footer } = Layout;
@@ -174,14 +176,24 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
/>
</div>
<div className={styles.rightTool}>
<div className={styles.rightToolItem}>
<ShareAltOutlined />
</div>
<div className={styles.rightToolItem}>
<MessageOutlined />
</div>
<ToContract
className={styles.rightToolItem}
onTransfer={data => {
console.log("转接数据:", data);
// 这里可以添加实际的转接逻辑
}}
onReturn={() => {
console.log("执行一键转回操作");
// 这里可以添加实际的转回逻辑
}}
/>
<ChatRecord
className={styles.rightToolItem}
onSearch={data => {
console.log("查找数据:", data);
// 这里可以添加实际的查找逻辑
}}
/>
</div>
</div>
<div className={styles.inputArea}>

View File

@@ -18,7 +18,12 @@ export interface WeChatState {
isExist?: boolean,
) => void;
loadChatMessages: (Init: boolean, To?: number) => Promise<void>;
SearchMessage: (params: {
From: number;
To: number;
keyword: string;
Count?: number;
}) => Promise<void>;
// 视频消息处理方法
setVideoLoading: (messageId: number, isLoading: boolean) => void;
setVideoUrl: (messageId: number, videoUrl: string) => void;

View File

@@ -98,6 +98,50 @@ export const useWeChatStore = create<WeChatState>()(
set({ messagesLoading: false });
}
},
SearchMessage: async ({
From = 1,
To = 4704624000000,
keyword = "",
Count = 20,
}: {
From: number;
To: number;
keyword: string;
Count?: number;
}) => {
const state = useWeChatStore.getState();
const contact = state.currentContract;
set({ messagesLoading: true });
try {
const params: any = {
wechatAccountId: contact.wechatAccountId,
From,
To,
keyword,
Count,
olderData: true,
};
if ("chatroomId" in contact && contact.chatroomId) {
params.wechatChatroomId = contact.id;
const messages = await getChatroomMessages(params);
const currentGroupMembers = await getGroupMembers({
id: contact.id,
});
set({ currentMessages: messages || [], currentGroupMembers });
} else {
params.wechatFriendId = contact.id;
const messages = await getChatMessages(params);
set({ currentMessages: messages || [] });
}
set({ messagesLoading: false });
} catch (error) {
console.error("获取聊天消息失败:", error);
} finally {
set({ messagesLoading: false });
}
},
setMessageLoading: loading => {
set({ messagesLoading: Boolean(loading) });