Merge branch 'yongpxu-dev' into develop
# Conflicts: # Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/ProfileCard/index.tsx resolved by develop version
This commit is contained in:
26
Cunkebao/dist/.vite/manifest.json
vendored
26
Cunkebao/dist/.vite/manifest.json
vendored
@@ -1,18 +1,14 @@
|
||||
{
|
||||
"_charts-DKSCc2_C.js": {
|
||||
"file": "assets/charts-DKSCc2_C.js",
|
||||
"_charts-BET_YNJb.js": {
|
||||
"file": "assets/charts-BET_YNJb.js",
|
||||
"name": "charts",
|
||||
"imports": [
|
||||
"_ui-DhAz00L0.js",
|
||||
"_ui-BSfOMVFg.js",
|
||||
"_vendor-2vc8h_ct.js"
|
||||
]
|
||||
},
|
||||
"_ui-D0C0OGrH.css": {
|
||||
"file": "assets/ui-D0C0OGrH.css",
|
||||
"src": "_ui-D0C0OGrH.css"
|
||||
},
|
||||
"_ui-DhAz00L0.js": {
|
||||
"file": "assets/ui-DhAz00L0.js",
|
||||
"_ui-BSfOMVFg.js": {
|
||||
"file": "assets/ui-BSfOMVFg.js",
|
||||
"name": "ui",
|
||||
"imports": [
|
||||
"_vendor-2vc8h_ct.js"
|
||||
@@ -21,6 +17,10 @@
|
||||
"assets/ui-D0C0OGrH.css"
|
||||
]
|
||||
},
|
||||
"_ui-D0C0OGrH.css": {
|
||||
"file": "assets/ui-D0C0OGrH.css",
|
||||
"src": "_ui-D0C0OGrH.css"
|
||||
},
|
||||
"_utils-6WF66_dS.js": {
|
||||
"file": "assets/utils-6WF66_dS.js",
|
||||
"name": "utils",
|
||||
@@ -33,18 +33,18 @@
|
||||
"name": "vendor"
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-BdCPAYQ7.js",
|
||||
"file": "assets/index-DX2o9_TA.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"_vendor-2vc8h_ct.js",
|
||||
"_utils-6WF66_dS.js",
|
||||
"_ui-DhAz00L0.js",
|
||||
"_charts-DKSCc2_C.js"
|
||||
"_ui-BSfOMVFg.js",
|
||||
"_charts-BET_YNJb.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/index-ChiFk16x.css"
|
||||
"assets/index-DwDrBOQB.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
Cunkebao/dist/index.html
vendored
8
Cunkebao/dist/index.html
vendored
@@ -11,13 +11,13 @@
|
||||
</style>
|
||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
||||
<script type="text/javascript" src="/websdk.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BdCPAYQ7.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DX2o9_TA.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-DhAz00L0.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-DKSCc2_C.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-BSfOMVFg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-BET_YNJb.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ChiFk16x.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwDrBOQB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
UserOutline,
|
||||
} from "antd-mobile-icons";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
|
||||
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
||||
import style from "./login.module.scss";
|
||||
@@ -75,6 +76,8 @@ const Login: React.FC = () => {
|
||||
|
||||
response.then(res => {
|
||||
const { member, kefuData, deviceTotal } = res;
|
||||
// 清空WebSocket连接状态
|
||||
useWebSocketStore.getState().clearConnectionState();
|
||||
login(res.token, member, deviceTotal);
|
||||
const { self, token } = kefuData;
|
||||
login2(token.access_token);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
UpdateLikeTaskData,
|
||||
LikeRecord,
|
||||
PaginatedResponse,
|
||||
} from "@/pages/workspace/auto-like/record/data";
|
||||
} from "@/pages/mobile/workspace/auto-like/record/data";
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export function fetchAutoLikeTasks(
|
||||
@@ -36,7 +36,7 @@ export function deleteAutoLikeTask(id: string): Promise<any> {
|
||||
|
||||
// 切换任务状态
|
||||
export function toggleAutoLikeTask(data): Promise<any> {
|
||||
return request("/v1/workbench/update-status", { ...data, type: 1 }, "POST");
|
||||
return request("/v1/workbench/update-status", { ...data }, "POST");
|
||||
}
|
||||
|
||||
// 复制自动点赞任务
|
||||
|
||||
@@ -201,8 +201,7 @@ const AutoLike: React.FC = () => {
|
||||
// 切换任务状态
|
||||
const toggleTaskStatus = async (id: string, status: number) => {
|
||||
try {
|
||||
const newStatus = status === 1 ? "2" : "1";
|
||||
await toggleAutoLikeTask(id, newStatus);
|
||||
await toggleAutoLikeTask({ id });
|
||||
Toast.show({
|
||||
content: status === 1 ? "已暂停" : "已启动",
|
||||
position: "top",
|
||||
|
||||
@@ -22,8 +22,13 @@ export function WechatGroup(params) {
|
||||
export function clearUnreadCount(params) {
|
||||
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
||||
}
|
||||
|
||||
//更新配置
|
||||
export function updateConfig(params) {
|
||||
return request("/api/WechatFriend/updateConfig", params, "PUT");
|
||||
}
|
||||
//获取聊天记录-2 获取列表
|
||||
export function getMessages(params: {
|
||||
export function getChatMessages(params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
wechatChatroomId?: number;
|
||||
@@ -73,19 +78,6 @@ export const getControlTerminalList = params => {
|
||||
return request("/api/wechataccount", params, "GET");
|
||||
};
|
||||
|
||||
// 搜索联系人
|
||||
export const getChatMessage = (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId: number;
|
||||
From: number;
|
||||
To: number;
|
||||
Count: number;
|
||||
olderData: boolean;
|
||||
keyword: string;
|
||||
}) => {
|
||||
return request("/api/FriendMessage/SearchMessage", params, "GET");
|
||||
};
|
||||
|
||||
// 获取聊天历史
|
||||
export const getChatHistory = (
|
||||
chatId: string,
|
||||
|
||||
@@ -128,128 +128,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chatFooter {
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
flex-shrink: 0;
|
||||
|
||||
.inputContainer {
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
justify-content: space-between;
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rightTool {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
color: #666;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #bae7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
padding: 12px 16px;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
background: #fff;
|
||||
|
||||
.messageInput {
|
||||
flex: 1;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
min-height: 36px;
|
||||
max-height: 120px;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
border-radius: 4px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
color: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #096dd9;
|
||||
border-color: #096dd9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
color: #bfbfbf;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
padding: 4px 16px 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧个人资料卡片
|
||||
.profileSider {
|
||||
background: #fff;
|
||||
@@ -748,19 +626,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chatFooter {
|
||||
padding: 12px;
|
||||
|
||||
.inputContainer {
|
||||
.inputArea {
|
||||
.sendButton {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageItem {
|
||||
.messageContent {
|
||||
max-width: 85%;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// MessageEnter 组件样式 - 微信风格
|
||||
.chatFooter {
|
||||
background: #f7f7f7;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inputToolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.leftTool {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: #e6e6e6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.rightTool {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rightToolItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e6e6e6;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
border: 1px solid #d1d1d1;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButtonArea {
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
min-width: 60px;
|
||||
font-size: 13px;
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
|
||||
&:hover {
|
||||
background: #06ad56;
|
||||
border-color: #06ad56;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #059748;
|
||||
border-color: #059748;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #b3b3b3;
|
||||
border-color: #b3b3b3;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.inputHint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.inputContainer {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.inputToolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rightTool {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rightToolItem {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.inputArea {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
align-self: flex-end;
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import React, { useState } from "react";
|
||||
import { Layout, Input, Button, Dropdown, Menu, Tooltip, Modal } from "antd";
|
||||
import {
|
||||
ShareAltOutlined,
|
||||
SendOutlined,
|
||||
SmileOutlined,
|
||||
FolderOutlined,
|
||||
AudioOutlined,
|
||||
AudioOutlined as AudioHoldOutlined,
|
||||
CodeSandboxOutlined,
|
||||
MessageOutlined,
|
||||
EnvironmentOutlined,
|
||||
StarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import styles from "./MessageEnter.module.scss";
|
||||
|
||||
const { Footer } = Layout;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface MessageEnterProps {
|
||||
contract: ContractData | weChatGroup;
|
||||
}
|
||||
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
|
||||
const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [showMaterialModal, setShowMaterialModal] = useState(false);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim()) return;
|
||||
console.log("发送消息", contract);
|
||||
const params = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||
wechatFriendId: contract?.chatroomId ? 0 : contract.id,
|
||||
msgSubType: 0,
|
||||
msgType: 1,
|
||||
content: inputValue,
|
||||
};
|
||||
sendCommand("CmdSendMessage", params);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
// Ctrl+Enter 换行由 TextArea 自动处理,不需要阻止默认行为
|
||||
};
|
||||
|
||||
// 素材菜单项
|
||||
const materialMenuItems = [
|
||||
{
|
||||
key: "text",
|
||||
label: "文字素材",
|
||||
icon: <span>📝</span>,
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
label: "语音素材",
|
||||
icon: <span>🎵</span>,
|
||||
},
|
||||
{
|
||||
key: "image",
|
||||
label: "图片素材",
|
||||
icon: <span>🖼️</span>,
|
||||
},
|
||||
{
|
||||
key: "video",
|
||||
label: "视频素材",
|
||||
icon: <span>🎬</span>,
|
||||
},
|
||||
{
|
||||
key: "link",
|
||||
label: "链接素材",
|
||||
icon: <span>🔗</span>,
|
||||
},
|
||||
{
|
||||
key: "card",
|
||||
label: "名片素材",
|
||||
icon: <span>📇</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const handleMaterialSelect = (key: string) => {
|
||||
console.log("选择素材类型:", key);
|
||||
setShowMaterialModal(true);
|
||||
// 这里可以根据不同的素材类型显示不同的模态框
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 聊天输入 */}
|
||||
<Footer className={styles.chatFooter}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputToolbar}>
|
||||
<div className={styles.leftTool}>
|
||||
<Tooltip title="表情">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SmileOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="上传附件">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FolderOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="收藏">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<StarOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="位置">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EnvironmentOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="语音">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="按住说话">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioHoldOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontSize: "8px",
|
||||
color: "#52c41a",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
H
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu
|
||||
items={materialMenuItems}
|
||||
onClick={({ key }) => handleMaterialSelect(key)}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger={["click"]}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CodeSandboxOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className={styles.rightTool}>
|
||||
<div className={styles.rightToolItem}>
|
||||
<ShareAltOutlined />
|
||||
转给他人
|
||||
</div>
|
||||
<div className={styles.rightToolItem}>
|
||||
<MessageOutlined />
|
||||
聊天记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputArea}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextArea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="输入消息..."
|
||||
className={styles.messageInput}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
/>
|
||||
<div className={styles.sendButtonArea}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim()}
|
||||
className={styles.sendButton}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputHint}>按下Ctrl+Enter换行,Enter发送</div>
|
||||
</div>
|
||||
</Footer>
|
||||
|
||||
{/* 素材选择模态框 */}
|
||||
<Modal
|
||||
title="选择素材"
|
||||
open={showMaterialModal}
|
||||
onCancel={() => setShowMaterialModal(false)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowMaterialModal(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
onClick={() => setShowMaterialModal(false)}
|
||||
>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<div style={{ display: "flex", height: "400px" }}>
|
||||
{/* 左侧素材分类 */}
|
||||
<div
|
||||
style={{
|
||||
width: "200px",
|
||||
background: "#f5f5f5",
|
||||
borderRight: "1px solid #e8e8e8",
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "16px", borderBottom: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>公共素材</h4>
|
||||
</div>
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
background: "#e6f7ff",
|
||||
borderLeft: "3px solid #1890ff",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
暗黑4
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
针对老客户的...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
D2辅助
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
ROS反馈演示...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
一键宏产品素...
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "16px", borderTop: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>部门素材</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Input.Search placeholder="昵称" style={{ width: "100%" }} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "300px",
|
||||
color: "#8c8c8c",
|
||||
}}
|
||||
>
|
||||
请选择左侧素材分类
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageEnter;
|
||||
|
||||
@@ -1,312 +1,112 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd";
|
||||
import {
|
||||
Layout,
|
||||
Input,
|
||||
Button,
|
||||
Avatar,
|
||||
Space,
|
||||
Dropdown,
|
||||
Menu,
|
||||
message,
|
||||
Tooltip,
|
||||
Modal,
|
||||
} from "antd";
|
||||
import {
|
||||
ShareAltOutlined,
|
||||
SendOutlined,
|
||||
SmileOutlined,
|
||||
FolderOutlined,
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
MoreOutlined,
|
||||
UserOutlined,
|
||||
AudioOutlined,
|
||||
AudioOutlined as AudioHoldOutlined,
|
||||
DownloadOutlined,
|
||||
CodeSandboxOutlined,
|
||||
MessageOutlined,
|
||||
FileOutlined,
|
||||
FilePdfOutlined,
|
||||
FileWordOutlined,
|
||||
FileExcelOutlined,
|
||||
FilePptOutlined,
|
||||
PlayCircleFilled,
|
||||
EnvironmentOutlined,
|
||||
TeamOutlined,
|
||||
StarOutlined,
|
||||
FolderOutlined,
|
||||
EnvironmentOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { getMessages } from "@/pages/pc/ckbox/api";
|
||||
import styles from "./ChatWindow.module.scss";
|
||||
import {
|
||||
useWebSocketStore,
|
||||
WebSocketMessage,
|
||||
} from "@/store/module/websocket/websocket";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
import Person from "./components/Person";
|
||||
const { Header, Content, Footer } = Layout;
|
||||
const { TextArea } = Input;
|
||||
import ProfileCard from "./components/ProfileCard";
|
||||
import MessageEnter from "./components/MessageEnter";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
interface ChatWindowProps {
|
||||
contract: ContractData | weChatGroup;
|
||||
onSendMessage: (message: string) => void;
|
||||
showProfile?: boolean;
|
||||
onToggleProfile?: () => void;
|
||||
}
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
contract,
|
||||
onSendMessage,
|
||||
showProfile = true,
|
||||
onToggleProfile,
|
||||
}) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [messages, setMessages] = useState<ChatRecord[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showMaterialModal, setShowMaterialModal] = useState(false);
|
||||
const [pendingVideoRequests, setPendingVideoRequests] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
||||
const prevMessagesRef = useRef(currentMessages);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const params: any = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
From: 1,
|
||||
To: +new Date() + 1000,
|
||||
Count: 100,
|
||||
olderData: true,
|
||||
};
|
||||
if (contract.groupId == 1) {
|
||||
params.wechatFriendId = contract.id;
|
||||
} else {
|
||||
params.wechatChatroomId = contract.id;
|
||||
}
|
||||
getMessages(params)
|
||||
.then(msg => {
|
||||
setMessages(msg);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [contract.id]);
|
||||
const prevMessages = prevMessagesRef.current;
|
||||
|
||||
// 检查是否有视频状态变化(从加载中变为已完成或开始加载)
|
||||
console.log("currentMessages", currentMessages);
|
||||
|
||||
const hasVideoStateChange = currentMessages.some((msg, index) => {
|
||||
const prevMsg = prevMessages[index];
|
||||
if (!prevMsg || prevMsg.id !== msg.id) return false;
|
||||
|
||||
useEffect(() => {
|
||||
// 只有在非视频加载操作时才自动滚动到底部
|
||||
// 检查是否有视频正在加载中
|
||||
const hasLoadingVideo = messages.some(msg => {
|
||||
try {
|
||||
const content =
|
||||
const currentContent =
|
||||
typeof msg.content === "string"
|
||||
? JSON.parse(msg.content)
|
||||
: msg.content;
|
||||
return content.isLoading === true;
|
||||
const prevContent =
|
||||
typeof prevMsg.content === "string"
|
||||
? JSON.parse(prevMsg.content)
|
||||
: prevMsg.content;
|
||||
|
||||
// 检查视频状态是否发生变化(开始加载、完成加载、获得URL)
|
||||
const currentHasVideo =
|
||||
currentContent.previewImage && currentContent.tencentUrl;
|
||||
const prevHasVideo = prevContent.previewImage && prevContent.tencentUrl;
|
||||
|
||||
if (currentHasVideo && prevHasVideo) {
|
||||
// 检查加载状态变化或视频URL变化
|
||||
return (
|
||||
currentContent.isLoading !== prevContent.isLoading ||
|
||||
currentContent.videoUrl !== prevContent.videoUrl
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasLoadingVideo) {
|
||||
// 只有在没有视频状态变化时才自动滚动到底部
|
||||
if (!hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// 添加 WebSocket 消息订阅 - 监听视频下载响应消息
|
||||
useEffect(() => {
|
||||
// 只有当有待处理的视频请求时才订阅WebSocket消息
|
||||
if (Object.keys(pendingVideoRequests).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("开始监听视频下载响应,当前待处理请求:", pendingVideoRequests);
|
||||
|
||||
// 订阅 WebSocket 消息变化
|
||||
const unsubscribe = useWebSocketStore.subscribe(state => {
|
||||
// 只处理新增的消息
|
||||
const messages = state.messages as WebSocketMessage[];
|
||||
|
||||
// 筛选出视频下载响应消息
|
||||
messages.forEach(message => {
|
||||
if (message?.content?.cmdType === "CmdDownloadVideoResult") {
|
||||
console.log("收到视频下载响应:", message.content);
|
||||
|
||||
// 检查是否是我们正在等待的视频响应
|
||||
const messageId = Object.keys(pendingVideoRequests).find(
|
||||
id => pendingVideoRequests[id] === message.content.friendMessageId,
|
||||
);
|
||||
|
||||
if (messageId) {
|
||||
console.log("找到对应的消息ID:", messageId);
|
||||
|
||||
// 从待处理队列中移除
|
||||
setPendingVideoRequests(prev => {
|
||||
const newRequests = { ...prev };
|
||||
delete newRequests[messageId];
|
||||
return newRequests;
|
||||
});
|
||||
|
||||
// 更新消息内容,将视频URL添加到对应的消息中
|
||||
setMessages(prevMessages => {
|
||||
return prevMessages.map(msg => {
|
||||
if (msg.id === Number(messageId)) {
|
||||
try {
|
||||
const msgContent =
|
||||
typeof msg.content === "string"
|
||||
? JSON.parse(msg.content)
|
||||
: msg.content;
|
||||
|
||||
// 更新消息内容,添加视频URL并移除加载状态
|
||||
return {
|
||||
...msg,
|
||||
content: JSON.stringify({
|
||||
...msgContent,
|
||||
videoUrl: message.content.url,
|
||||
isLoading: false,
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("解析消息内容失败:", e);
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载时取消订阅
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [pendingVideoRequests]); // 依赖于pendingVideoRequests,当队列变化时重新设置订阅
|
||||
// 更新上一次的消息状态
|
||||
prevMessagesRef.current = currentMessages;
|
||||
}, [currentMessages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
try {
|
||||
const newMessage: ChatRecord = {
|
||||
id: contract.id,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatFriendId: contract.id,
|
||||
tenantId: 0,
|
||||
accountId: 0,
|
||||
synergyAccountId: 0,
|
||||
content: inputValue,
|
||||
msgType: 0,
|
||||
msgSubType: 0,
|
||||
msgSvrId: "",
|
||||
isSend: false,
|
||||
createTime: "",
|
||||
isDeleted: false,
|
||||
deleteTime: "",
|
||||
sendStatus: 0,
|
||||
wechatTime: 0,
|
||||
origin: 0,
|
||||
msgId: 0,
|
||||
recalled: false,
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMessage]);
|
||||
onSendMessage(inputValue);
|
||||
setInputValue("");
|
||||
} catch (error) {
|
||||
messageApi.error("发送失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
// 素材菜单项
|
||||
const materialMenuItems = [
|
||||
{
|
||||
key: "text",
|
||||
label: "文字素材",
|
||||
icon: <span>📝</span>,
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
label: "语音素材",
|
||||
icon: <span>🎵</span>,
|
||||
},
|
||||
{
|
||||
key: "image",
|
||||
label: "图片素材",
|
||||
icon: <span>🖼️</span>,
|
||||
},
|
||||
{
|
||||
key: "video",
|
||||
label: "视频素材",
|
||||
icon: <span>🎬</span>,
|
||||
},
|
||||
{
|
||||
key: "link",
|
||||
label: "链接素材",
|
||||
icon: <span>🔗</span>,
|
||||
},
|
||||
{
|
||||
key: "card",
|
||||
label: "名片素材",
|
||||
icon: <span>📇</span>,
|
||||
},
|
||||
];
|
||||
|
||||
const handleMaterialSelect = (key: string) => {
|
||||
console.log("选择素材类型:", key);
|
||||
setShowMaterialModal(true);
|
||||
// 这里可以根据不同的素材类型显示不同的模态框
|
||||
};
|
||||
|
||||
// 处理视频播放请求,发送socket请求获取真实视频地址
|
||||
const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
|
||||
// 生成请求ID (使用当前时间戳作为唯一标识)
|
||||
const requestSeq = `${+new Date()}`;
|
||||
console.log("发送视频下载请求:", { messageId, requestSeq });
|
||||
console.log("发送视频下载请求:", { messageId, tencentUrl });
|
||||
|
||||
// 先设置加载状态
|
||||
useWeChatStore.getState().setVideoLoading(messageId, true);
|
||||
|
||||
// 构建socket请求数据
|
||||
useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
|
||||
chatroomMessageId: contract.chatroomId ? messageId : 0,
|
||||
friendMessageId: contract.chatroomId ? 0 : messageId,
|
||||
seq: requestSeq, // 使用唯一的请求ID
|
||||
seq: `${+new Date()}`, // 使用时间戳作为请求序列号
|
||||
tencentUrl: tencentUrl,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
});
|
||||
|
||||
// 将消息ID和请求序列号添加到待处理队列
|
||||
setPendingVideoRequests(prev => ({
|
||||
...prev,
|
||||
[messageId]: messageId,
|
||||
}));
|
||||
|
||||
// 更新消息状态为加载中
|
||||
setMessages(prevMessages => {
|
||||
return prevMessages.map(msg => {
|
||||
if (msg.id === messageId) {
|
||||
// 保存原始内容,添加loading状态
|
||||
const originalContent = msg.content;
|
||||
return {
|
||||
...msg,
|
||||
content: JSON.stringify({
|
||||
...JSON.parse(originalContent),
|
||||
isLoading: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 解析消息内容,判断消息类型并返回对应的渲染内容
|
||||
@@ -337,21 +137,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
content.trim().endsWith("}")
|
||||
) {
|
||||
const videoData = JSON.parse(content);
|
||||
// 处理用户提供的JSON格式 {"previewImage":"https://...", "tencentUrl":"..."}
|
||||
// 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true}
|
||||
if (videoData.previewImage && videoData.tencentUrl) {
|
||||
// 提取预览图URL,去掉可能的引号
|
||||
const previewImageUrl = videoData.previewImage.replace(/[`"']/g, "");
|
||||
|
||||
// 创建点击处理函数,调用handleVideoPlayRequest发送socket请求获取真实视频地址
|
||||
// 创建点击处理函数
|
||||
const handlePlayClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// 调用处理函数,传入tencentUrl和消息ID
|
||||
handleVideoPlayRequest(videoData.tencentUrl, msg.id);
|
||||
// 如果没有视频URL且不在加载中,则发起下载请求
|
||||
if (!videoData.videoUrl && !videoData.isLoading) {
|
||||
handleVideoPlayRequest(videoData.tencentUrl, msg.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否已下载视频URL
|
||||
// 如果已有视频URL,显示视频播放器
|
||||
if (videoData.videoUrl) {
|
||||
// 已获取到视频URL,显示视频播放器
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div className={styles.videoContainer}>
|
||||
@@ -374,30 +175,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否处于加载状态
|
||||
if (videoData.isLoading) {
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div className={styles.videoContainer}>
|
||||
<img
|
||||
src={previewImageUrl}
|
||||
alt="视频预览"
|
||||
className={styles.videoThumbnail}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
borderRadius: "8px",
|
||||
opacity: "0.7",
|
||||
}}
|
||||
/>
|
||||
<div className={styles.videoPlayIcon}>
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认显示预览图和播放按钮
|
||||
// 显示预览图,根据加载状态显示不同的图标
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div className={styles.videoContainer} onClick={handlePlayClick}>
|
||||
@@ -405,12 +183,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
src={previewImageUrl}
|
||||
alt="视频预览"
|
||||
className={styles.videoThumbnail}
|
||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
borderRadius: "8px",
|
||||
opacity: videoData.isLoading ? "0.7" : "1",
|
||||
}}
|
||||
/>
|
||||
<div className={styles.videoPlayIcon}>
|
||||
<PlayCircleFilled
|
||||
style={{ fontSize: "48px", color: "#fff" }}
|
||||
/>
|
||||
{videoData.isLoading ? (
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
) : (
|
||||
<PlayCircleFilled
|
||||
style={{ fontSize: "48px", color: "#fff" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -740,14 +526,10 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
|
||||
// 用于分组消息并添加时间戳的辅助函数
|
||||
const groupMessagesByTime = (messages: ChatRecord[]) => {
|
||||
const groups: { time: string; messages: ChatRecord[] }[] = [];
|
||||
messages.forEach(msg => {
|
||||
// 使用 formatWechatTime 函数格式化时间戳
|
||||
const formattedTime = formatWechatTime(msg.wechatTime);
|
||||
groups.push({ time: formattedTime, messages: [msg] });
|
||||
});
|
||||
|
||||
return groups;
|
||||
return messages.map(msg => ({
|
||||
time: formatWechatTime(msg?.wechatTime),
|
||||
messages: [msg],
|
||||
}));
|
||||
};
|
||||
|
||||
const renderMessage = (msg: ChatRecord) => {
|
||||
@@ -802,7 +584,6 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
|
||||
return (
|
||||
<Layout className={styles.chatWindow}>
|
||||
{contextHolder}
|
||||
{/* 聊天主体区域 */}
|
||||
<Layout className={styles.chatMain}>
|
||||
{/* 聊天头部 */}
|
||||
@@ -849,228 +630,26 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
{/* 聊天内容 */}
|
||||
<Content className={styles.chatContent}>
|
||||
<div className={styles.messagesContainer}>
|
||||
{loading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{groupMessagesByTime(messages).map((group, groupIndex) => (
|
||||
<React.Fragment key={`group-${groupIndex}`}>
|
||||
<div className={styles.messageTime}>{group.time}</div>
|
||||
{group.messages.map(renderMessage)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
||||
<React.Fragment key={`group-${groupIndex}`}>
|
||||
<div className={styles.messageTime}>{group.time}</div>
|
||||
{group.messages.map(renderMessage)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
{/* 聊天输入 */}
|
||||
<Footer className={styles.chatFooter}>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputToolbar}>
|
||||
<div className={styles.leftTool}>
|
||||
<Tooltip title="表情">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SmileOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="上传附件">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FolderOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="收藏">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<StarOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="位置">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EnvironmentOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="语音">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="按住说话">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<AudioHoldOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
fontSize: "8px",
|
||||
color: "#52c41a",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
H
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu
|
||||
items={materialMenuItems}
|
||||
onClick={({ key }) => handleMaterialSelect(key)}
|
||||
style={{
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
trigger={["click"]}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CodeSandboxOutlined />}
|
||||
className={styles.toolbarButton}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className={styles.rightTool}>
|
||||
<div className={styles.rightToolItem}>
|
||||
<ShareAltOutlined />
|
||||
转给他人
|
||||
</div>
|
||||
<div className={styles.rightToolItem}>
|
||||
<MessageOutlined />
|
||||
聊天记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.inputArea}>
|
||||
<TextArea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="输入消息..."
|
||||
className={styles.messageInput}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim()}
|
||||
className={styles.sendButton}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.inputHint}>按下Ctrl+Enter换行</div>
|
||||
</div>
|
||||
</Footer>
|
||||
{/* 消息输入组件 */}
|
||||
<MessageEnter contract={contract} />
|
||||
</Layout>
|
||||
|
||||
{/* 右侧个人资料卡片 */}
|
||||
<Person
|
||||
<ProfileCard
|
||||
contract={contract}
|
||||
showProfile={showProfile}
|
||||
onToggleProfile={onToggleProfile}
|
||||
/>
|
||||
|
||||
{/* 素材选择模态框 */}
|
||||
<Modal
|
||||
title="选择素材"
|
||||
open={showMaterialModal}
|
||||
onCancel={() => setShowMaterialModal(false)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowMaterialModal(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
onClick={() => setShowMaterialModal(false)}
|
||||
>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<div style={{ display: "flex", height: "400px" }}>
|
||||
{/* 左侧素材分类 */}
|
||||
<div
|
||||
style={{
|
||||
width: "200px",
|
||||
background: "#f5f5f5",
|
||||
borderRight: "1px solid #e8e8e8",
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: "16px", borderBottom: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>公共素材</h4>
|
||||
</div>
|
||||
<div style={{ padding: "8px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
background: "#e6f7ff",
|
||||
borderLeft: "3px solid #1890ff",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
暗黑4
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
针对老客户的...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
D2辅助
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
ROS反馈演示...
|
||||
</div>
|
||||
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
|
||||
一键宏产品素...
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "16px", borderTop: "1px solid #e8e8e8" }}>
|
||||
<h4 style={{ margin: 0, color: "#262626" }}>部门素材</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容区域 */}
|
||||
<div style={{ flex: 1, padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Input.Search placeholder="昵称" style={{ width: "100%" }} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "300px",
|
||||
color: "#8c8c8c",
|
||||
}}
|
||||
>
|
||||
请选择左侧素材分类
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
.lastMessage {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
@@ -71,7 +70,7 @@
|
||||
padding-right: 5px;
|
||||
height: 18px; // 添加固定高度
|
||||
line-height: 18px; // 设置行高与高度一致
|
||||
|
||||
|
||||
&::before {
|
||||
content: attr(data-count);
|
||||
position: absolute;
|
||||
@@ -88,7 +87,7 @@
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
&[data-count]:not([data-count=""]):not([data-count="0"]) {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
@@ -106,7 +105,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.lastDayMessage {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@@ -2,28 +2,28 @@ import React from "react";
|
||||
import { List, Avatar, Badge } from "antd";
|
||||
import { UserOutlined, TeamOutlined } from "@ant-design/icons";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
|
||||
import styles from "./MessageList.module.scss";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
interface MessageListProps {
|
||||
chatSessions: ContractData[] | weChatGroup[];
|
||||
currentChat: ContractData | weChatGroup;
|
||||
onContactClick: (chat: ContractData | weChatGroup) => void;
|
||||
}
|
||||
interface MessageListProps {}
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({
|
||||
chatSessions,
|
||||
currentChat,
|
||||
onContactClick,
|
||||
}) => {
|
||||
const MessageList: React.FC<MessageListProps> = () => {
|
||||
const { setCurrentContact, currentContract } = useWeChatStore();
|
||||
const chatSessions = useCkChatStore(state => state.chatSessions);
|
||||
const onContactClick = (session: ContractData | weChatGroup) => {
|
||||
setCurrentContact(session, true);
|
||||
};
|
||||
return (
|
||||
<div className={styles.messageList}>
|
||||
<List
|
||||
dataSource={chatSessions as ContractData[]}
|
||||
dataSource={chatSessions as (ContractData | weChatGroup)[]}
|
||||
renderItem={session => (
|
||||
<List.Item
|
||||
key={session.id}
|
||||
className={`${styles.messageItem} ${
|
||||
currentChat?.id === session.id ? styles.active : ""
|
||||
currentContract?.id === session.id ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => onContactClick(session)}
|
||||
>
|
||||
|
||||
@@ -60,6 +60,13 @@
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -2,21 +2,24 @@ import React, { useState, useCallback, useEffect } from "react";
|
||||
import { List, Avatar, Collapse, Button } from "antd";
|
||||
import type { CollapseProps } from "antd";
|
||||
import styles from "./WechatFriends.module.scss";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import {
|
||||
useCkChatStore,
|
||||
searchContactsAndGroups,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
|
||||
interface WechatFriendsProps {
|
||||
contracts: ContractData[] | weChatGroup[];
|
||||
onContactClick: (contract: ContractData | weChatGroup) => void;
|
||||
selectedContactId?: ContractData | weChatGroup;
|
||||
}
|
||||
|
||||
const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
contracts,
|
||||
onContactClick,
|
||||
selectedContactId,
|
||||
}) => {
|
||||
const [newContractList, setNewContractList] = useState<any[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
(ContractData | weChatGroup)[]
|
||||
>([]);
|
||||
const getNewContractListFn = useCkChatStore(
|
||||
state => state.getNewContractList,
|
||||
);
|
||||
@@ -26,17 +29,27 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
|
||||
// 使用useEffect来处理异步的getNewContractList调用
|
||||
useEffect(() => {
|
||||
const fetchNewContractList = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const result = await getNewContractListFn();
|
||||
setNewContractList(result || []);
|
||||
if (searchKeyword.trim()) {
|
||||
// 有搜索关键词时,获取搜索结果
|
||||
const searchResult = await searchContactsAndGroups();
|
||||
setSearchResults(searchResult || []);
|
||||
setNewContractList([]);
|
||||
} else {
|
||||
// 无搜索关键词时,获取分组列表
|
||||
const result = await getNewContractListFn();
|
||||
setNewContractList(result || []);
|
||||
setSearchResults([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取联系人分组列表失败:", error);
|
||||
console.error("获取联系人数据失败:", error);
|
||||
setNewContractList([]);
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
fetchNewContractList();
|
||||
fetchData();
|
||||
}, [getNewContractListFn, kfSelected, countLables, searchKeyword]);
|
||||
|
||||
const [activeKey, setActiveKey] = useState<string[]>([]); // 默认展开第一个分组
|
||||
@@ -48,32 +61,39 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||
const [hasMore, setHasMore] = useState<{ [key: string]: boolean }>({});
|
||||
const [page, setPage] = useState<{ [key: string]: number }>({});
|
||||
const { setCurrentContact } = useWeChatStore();
|
||||
const onContactClick = (contact: ContractData | weChatGroup) => {
|
||||
addChatSession(contact);
|
||||
setCurrentContact(contact);
|
||||
};
|
||||
|
||||
// 渲染联系人项
|
||||
const renderContactItem = (contact: ContractData) => (
|
||||
<List.Item
|
||||
key={contact.id}
|
||||
onClick={() => onContactClick(contact)}
|
||||
className={`${styles.contractItem} ${contact.id === selectedContactId?.id ? styles.selected : ""}`}
|
||||
>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
src={contact.avatar || contact.chatroomAvatar}
|
||||
icon={
|
||||
!(contact.avatar || contact.chatroomAvatar) && (
|
||||
<span>{contact.nickname.charAt(0)}</span>
|
||||
)
|
||||
}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.contractInfo}>
|
||||
<div className={styles.name}>
|
||||
{contact.conRemark || contact.nickname}
|
||||
const renderContactItem = (contact: ContractData | weChatGroup) => {
|
||||
// 判断是否为群组
|
||||
const isGroup = "chatroomId" in contact;
|
||||
const avatar = contact.avatar || contact.chatroomAvatar;
|
||||
const name = contact.conRemark || contact.nickname;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={contact.id}
|
||||
onClick={() => onContactClick(contact)}
|
||||
className={`${styles.contractItem} ${contact.id === selectedContactId?.id ? styles.selected : ""}`}
|
||||
>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
icon={!avatar && <span>{contact.nickname.charAt(0)}</span>}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
<div className={styles.contractInfo}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
{isGroup && <div className={styles.groupInfo}>群聊</div>}
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化分页数据
|
||||
useEffect(() => {
|
||||
@@ -188,22 +208,27 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
|
||||
return (
|
||||
<div className={styles.contractListSimple}>
|
||||
{newContractList && newContractList.length > 0 ? (
|
||||
{searchKeyword.trim() ? (
|
||||
// 搜索模式:直接显示搜索结果列表
|
||||
<>
|
||||
<div className={styles.header}>搜索结果</div>
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={searchResults}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
{searchResults.length === 0 && (
|
||||
<div className={styles.noResults}>未找到匹配的联系人</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 正常模式:显示分组
|
||||
<Collapse
|
||||
className={styles.groupCollapse}
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
items={getCollapseItems()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.header}>全部好友</div>
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={contracts as ContractData[]}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,26 +6,15 @@ import {
|
||||
ChromeOutlined,
|
||||
MessageOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import WechatFriends from "./WechatFriends";
|
||||
import MessageList from "./MessageList/index";
|
||||
import styles from "./SidebarMenu.module.scss";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
|
||||
interface SidebarMenuProps {
|
||||
contracts: ContractData[] | weChatGroup[];
|
||||
currentChat: ContractData | weChatGroup;
|
||||
onContactClick: (contract: ContractData | weChatGroup) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
contracts,
|
||||
currentChat,
|
||||
onContactClick,
|
||||
loading = false,
|
||||
}) => {
|
||||
const chatSessions = useCkChatStore(state => state.getChatSessions());
|
||||
const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
||||
const setSearchKeyword = useCkChatStore(state => state.setSearchKeyword);
|
||||
const clearSearchKeyword = useCkChatStore(state => state.clearSearchKeyword);
|
||||
@@ -93,7 +82,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
{/* 搜索栏 */}
|
||||
<div className={styles.searchBar}>
|
||||
<Input
|
||||
placeholder="搜索联系人、群组"
|
||||
placeholder="搜索客户..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchKeyword}
|
||||
onChange={e => handleSearch(e.target.value)}
|
||||
@@ -133,21 +122,9 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case "chats":
|
||||
return (
|
||||
<MessageList
|
||||
chatSessions={chatSessions}
|
||||
onContactClick={onContactClick}
|
||||
currentChat={currentChat}
|
||||
/>
|
||||
);
|
||||
return <MessageList />;
|
||||
case "contracts":
|
||||
return (
|
||||
<WechatFriends
|
||||
contracts={contracts as ContractData[]}
|
||||
onContactClick={onContactClick}
|
||||
selectedContactId={currentChat}
|
||||
/>
|
||||
);
|
||||
return <WechatFriends />;
|
||||
case "groups":
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
|
||||
@@ -10,38 +10,15 @@ import styles from "./index.module.scss";
|
||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
||||
const { Header, Content, Sider } = Layout;
|
||||
import { chatInitAPIdata, initSocket } from "./main";
|
||||
import { clearUnreadCount } from "@/pages/pc/ckbox/api";
|
||||
import {
|
||||
KfUserListData,
|
||||
weChatGroup,
|
||||
ContractData,
|
||||
} from "@/pages/pc/ckbox/data";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
|
||||
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
const CkboxPage: React.FC = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [contracts, setContacts] = useState<any[]>([]);
|
||||
const [currentChat, setCurrentChat] = useState<ContractData | weChatGroup>(
|
||||
null,
|
||||
);
|
||||
const status = useWebSocketStore(state => state.status);
|
||||
// 不要在组件初始化时获取sendCommand,而是在需要时动态获取
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProfile, setShowProfile] = useState(true);
|
||||
const kfUserList = useCkChatStore(state => state.kfUserList);
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
useEffect(() => {
|
||||
if (status == "connected" && kfUserList.length > 0) {
|
||||
//查询客服用户激活状态
|
||||
setInterval(() => {
|
||||
sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: kfUserList.map(v => v.id),
|
||||
});
|
||||
}, 10 * 1000);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const currentContract = useWeChatStore(state => state.currentContract);
|
||||
useEffect(() => {
|
||||
// 方法一:使用 Promise 链式调用处理异步函数
|
||||
setLoading(true);
|
||||
@@ -63,8 +40,6 @@ const CkboxPage: React.FC = () => {
|
||||
addChatSession(v);
|
||||
});
|
||||
|
||||
setContacts(isChatList);
|
||||
|
||||
// 数据加载完成后初始化WebSocket连接
|
||||
initSocket();
|
||||
})
|
||||
@@ -76,46 +51,9 @@ const CkboxPage: React.FC = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
//开始开启聊天
|
||||
const handleContactClick = (contract: ContractData | weChatGroup) => {
|
||||
clearUnreadCount([contract.id]).then(() => {
|
||||
contract.unreadCount = 0;
|
||||
addChatSession(contract);
|
||||
setCurrentChat(contract);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendMessage = async (message: string) => {
|
||||
if (!currentChat || !message.trim()) return;
|
||||
|
||||
try {
|
||||
// 更新当前聊天会话
|
||||
const updatedSession = {
|
||||
...currentChat,
|
||||
lastMessage: message,
|
||||
lastTime: dayjs().toISOString(),
|
||||
unreadCount: 0,
|
||||
};
|
||||
|
||||
setCurrentChat(updatedSession);
|
||||
|
||||
messageApi.success("消息发送成功");
|
||||
} catch (error) {
|
||||
messageApi.error("消息发送失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理垂直侧边栏用户选择
|
||||
const handleVerticalUserSelect = (userId: string) => {
|
||||
// setActiveVerticalUserId(userId);
|
||||
// 这里可以根据选择的用户类别筛选不同的联系人列表
|
||||
// 例如:根据userId加载不同分类的联系人
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSkeleton loading={loading}>
|
||||
<Layout className={styles.ckboxLayout}>
|
||||
{contextHolder}
|
||||
<Header className={styles.header}>触客宝</Header>
|
||||
<Layout>
|
||||
{/* 垂直侧边栏 */}
|
||||
@@ -126,17 +64,12 @@ const CkboxPage: React.FC = () => {
|
||||
|
||||
{/* 左侧联系人边栏 */}
|
||||
<Sider width={280} className={styles.sider}>
|
||||
<SidebarMenu
|
||||
contracts={contracts}
|
||||
currentChat={currentChat}
|
||||
onContactClick={handleContactClick}
|
||||
loading={loading}
|
||||
/>
|
||||
<SidebarMenu loading={loading} />
|
||||
</Sider>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<Content className={styles.mainContent}>
|
||||
{currentChat ? (
|
||||
{currentContract ? (
|
||||
<div className={styles.chatContainer}>
|
||||
<div className={styles.chatToolbar}>
|
||||
<Space>
|
||||
@@ -153,8 +86,7 @@ const CkboxPage: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
<ChatWindow
|
||||
contract={currentChat}
|
||||
onSendMessage={handleSendMessage}
|
||||
contract={currentContract}
|
||||
showProfile={showProfile}
|
||||
onToggleProfile={() => setShowProfile(!showProfile)}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface CkUserInfo {
|
||||
export interface CkChatState {
|
||||
userInfo: CkUserInfo | null;
|
||||
isLoggedIn: boolean;
|
||||
searchKeyword: string;
|
||||
contractList: ContractData[];
|
||||
chatSessions: any[];
|
||||
kfUserList: KfUserListData[];
|
||||
@@ -42,6 +43,8 @@ export interface CkChatState {
|
||||
newContractList: ContactGroupByLabel[];
|
||||
getContractList: () => ContractData[];
|
||||
getNewContractList: () => ContactGroupByLabel[];
|
||||
setSearchKeyword: (keyword: string) => void;
|
||||
clearSearchKeyword: () => void;
|
||||
asyncKfSelected: (data: number) => void;
|
||||
asyncWeChatGroup: (data: weChatGroup[]) => void;
|
||||
asyncCountLables: (data: ContactGroupByLabel[]) => void;
|
||||
@@ -49,13 +52,13 @@ export interface CkChatState {
|
||||
asyncKfUserList: (data: KfUserListData[]) => void;
|
||||
getKfUserInfo: (wechatAccountId: number) => KfUserListData | undefined;
|
||||
asyncContractList: (data: ContractData[]) => void;
|
||||
getChatSessions: () => any[];
|
||||
asyncChatSessions: (data: any[]) => void;
|
||||
updateChatSession: (session: ContractData | weChatGroup) => void;
|
||||
deleteCtrlUser: (userId: number) => void;
|
||||
updateCtrlUser: (user: KfUserListData) => void;
|
||||
clearkfUserList: () => void;
|
||||
getChatSessions: () => any[];
|
||||
addChatSession: (session: any) => void;
|
||||
updateChatSession: (session: any) => void;
|
||||
deleteChatSession: (sessionId: string) => void;
|
||||
setUserInfo: (userInfo: CkUserInfo) => void;
|
||||
clearUserInfo: () => void;
|
||||
|
||||
@@ -131,6 +131,71 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
return cachedResult;
|
||||
};
|
||||
})(),
|
||||
// 搜索好友和群组的新方法 - 从本地数据库查询并返回扁平化的搜索结果
|
||||
searchContactsAndGroups: (() => {
|
||||
let cachedResult: (ContractData | weChatGroup)[] = [];
|
||||
let lastKfSelected: number | null = null;
|
||||
let lastSearchKeyword: string = "";
|
||||
|
||||
return async () => {
|
||||
const state = useCkChatStore.getState();
|
||||
|
||||
// 检查是否需要重新计算缓存
|
||||
const shouldRecalculate =
|
||||
lastKfSelected !== state.kfSelected ||
|
||||
lastSearchKeyword !== state.searchKeyword;
|
||||
|
||||
if (shouldRecalculate) {
|
||||
if (state.searchKeyword.trim()) {
|
||||
const keyword = state.searchKeyword.toLowerCase();
|
||||
|
||||
// 从本地数据库查询联系人数据
|
||||
let allContacts: any[] = await contractService.findAll();
|
||||
|
||||
// 从本地数据库查询群组数据
|
||||
let allGroups: any[] = await weChatGroupService.findAll();
|
||||
|
||||
// 根据选中的客服筛选联系人
|
||||
if (state.kfSelected !== 0) {
|
||||
allContacts = allContacts.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// 根据选中的客服筛选群组
|
||||
if (state.kfSelected !== 0) {
|
||||
allGroups = allGroups.filter(
|
||||
item => item.wechatAccountId === state.kfSelected,
|
||||
);
|
||||
}
|
||||
|
||||
// 搜索匹配的联系人
|
||||
const matchedContacts = allContacts.filter(item => {
|
||||
const nickname = (item.nickname || "").toLowerCase();
|
||||
const conRemark = (item.conRemark || "").toLowerCase();
|
||||
return nickname.includes(keyword) || conRemark.includes(keyword);
|
||||
});
|
||||
|
||||
// 搜索匹配的群组
|
||||
const matchedGroups = allGroups.filter(item => {
|
||||
const nickname = (item.nickname || "").toLowerCase();
|
||||
const conRemark = (item.conRemark || "").toLowerCase();
|
||||
return nickname.includes(keyword) || conRemark.includes(keyword);
|
||||
});
|
||||
|
||||
// 合并搜索结果
|
||||
cachedResult = [...matchedContacts, ...matchedGroups];
|
||||
} else {
|
||||
cachedResult = [];
|
||||
}
|
||||
|
||||
lastKfSelected = state.kfSelected;
|
||||
lastSearchKeyword = state.searchKeyword;
|
||||
}
|
||||
|
||||
return cachedResult;
|
||||
};
|
||||
})(),
|
||||
// 异步设置联系人分组列表
|
||||
asyncNewContractList: async (data: any[]) => {
|
||||
set({ newContractList: data });
|
||||
@@ -297,6 +362,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
})(),
|
||||
// 添加聊天会话
|
||||
addChatSession: (session: ContractData | weChatGroup) => {
|
||||
session.unreadCount = 0;
|
||||
set(state => {
|
||||
// 检查是否已存在相同id的会话
|
||||
const exists = state.chatSessions.some(item => item.id === session.id);
|
||||
@@ -307,47 +373,20 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
: [...state.chatSessions, session as ContractData | weChatGroup],
|
||||
};
|
||||
});
|
||||
// 清除getChatSessions缓存
|
||||
const state = useCkChatStore.getState();
|
||||
if (
|
||||
state.getChatSessions &&
|
||||
typeof state.getChatSessions === "function"
|
||||
) {
|
||||
// 触发缓存重新计算
|
||||
state.getChatSessions();
|
||||
}
|
||||
},
|
||||
// 更新聊天会话
|
||||
updateChatSession: (session: ContractData | weChatGroup) => {
|
||||
set(state => ({
|
||||
chatSessions: state.chatSessions.map(item =>
|
||||
item.id === session.id ? session : item,
|
||||
item.id === session.id ? { ...item, ...session } : item,
|
||||
),
|
||||
}));
|
||||
// 清除getChatSessions缓存
|
||||
const state = useCkChatStore.getState();
|
||||
if (
|
||||
state.getChatSessions &&
|
||||
typeof state.getChatSessions === "function"
|
||||
) {
|
||||
// 触发缓存重新计算
|
||||
state.getChatSessions();
|
||||
}
|
||||
},
|
||||
// 删除聊天会话
|
||||
deleteChatSession: (sessionId: string) => {
|
||||
set(state => ({
|
||||
chatSessions: state.chatSessions.filter(item => item.id !== sessionId),
|
||||
}));
|
||||
// 清除getChatSessions缓存
|
||||
const state = useCkChatStore.getState();
|
||||
if (
|
||||
state.getChatSessions &&
|
||||
typeof state.getChatSessions === "function"
|
||||
) {
|
||||
// 触发缓存重新计算
|
||||
state.getChatSessions();
|
||||
}
|
||||
},
|
||||
// 设置用户信息
|
||||
setUserInfo: (userInfo: CkUserInfo) => {
|
||||
@@ -472,4 +511,6 @@ export const setSearchKeyword = (keyword: string) =>
|
||||
useCkChatStore.getState().setSearchKeyword(keyword);
|
||||
export const clearSearchKeyword = () =>
|
||||
useCkChatStore.getState().clearSearchKeyword();
|
||||
export const searchContactsAndGroups = () =>
|
||||
useCkChatStore.getState().searchContactsAndGroups();
|
||||
useCkChatStore.getState().getKfSelectedUser();
|
||||
|
||||
25
Cunkebao/src/store/module/weChat/weChat.data.ts
Normal file
25
Cunkebao/src/store/module/weChat/weChat.data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
// 微信聊天相关的类型定义
|
||||
export interface WeChatState {
|
||||
// 当前选中的联系人/群组
|
||||
currentContract: ContractData | weChatGroup | null;
|
||||
|
||||
// 当前聊天用户的消息列表(只存储当前聊天用户的消息)
|
||||
currentMessages: ChatRecord[];
|
||||
|
||||
// 消息加载状态
|
||||
messagesLoading: boolean;
|
||||
|
||||
// Actions
|
||||
setCurrentContact: (
|
||||
contract: ContractData | weChatGroup,
|
||||
isExist?: boolean,
|
||||
) => void;
|
||||
loadChatMessages: (contact: ContractData | weChatGroup) => Promise<void>;
|
||||
|
||||
// 视频消息处理方法
|
||||
setVideoLoading: (messageId: number, isLoading: boolean) => void;
|
||||
setVideoUrl: (messageId: number, videoUrl: string) => void;
|
||||
addMessage: (message: ChatRecord) => void;
|
||||
receivedMsg: (message: ChatRecord) => void;
|
||||
}
|
||||
194
Cunkebao/src/store/module/weChat/weChat.ts
Normal file
194
Cunkebao/src/store/module/weChat/weChat.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getChatMessages } from "@/pages/pc/ckbox/api";
|
||||
import { WeChatState } from "./weChat.data";
|
||||
import { clearUnreadCount, updateConfig } from "@/pages/pc/ckbox/api";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import {
|
||||
addChatSession,
|
||||
updateChatSession,
|
||||
useCkChatStore,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
|
||||
export const useWeChatStore = create<WeChatState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// 初始状态
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
messagesLoading: false,
|
||||
|
||||
// Actions
|
||||
setCurrentContact: (
|
||||
contract: ContractData | weChatGroup,
|
||||
isExist?: boolean,
|
||||
) => {
|
||||
const state = useWeChatStore.getState();
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [] });
|
||||
clearUnreadCount([contract.id]).then(() => {
|
||||
if (isExist) {
|
||||
updateChatSession({ ...contract, unreadCount: 0 });
|
||||
} else {
|
||||
addChatSession(contract);
|
||||
}
|
||||
set({ currentContract: contract });
|
||||
updateConfig({
|
||||
id: contract.id,
|
||||
config: { chat: true },
|
||||
});
|
||||
state.loadChatMessages(contract);
|
||||
});
|
||||
},
|
||||
|
||||
loadChatMessages: async contact => {
|
||||
set({ messagesLoading: true });
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
wechatAccountId: contact.wechatAccountId,
|
||||
From: 1,
|
||||
To: 4704624000000,
|
||||
Count: 10,
|
||||
olderData: true,
|
||||
};
|
||||
|
||||
if ("chatroomId" in contact && contact.chatroomId) {
|
||||
params.wechatChatroomId = contact.chatroomId;
|
||||
} else {
|
||||
params.wechatFriendId = contact.id;
|
||||
}
|
||||
|
||||
const messages = await getChatMessages(params);
|
||||
set({ currentMessages: messages || [] });
|
||||
} catch (error) {
|
||||
console.error("获取聊天消息失败:", error);
|
||||
} finally {
|
||||
set({ messagesLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
setMessageLoading: loading => {
|
||||
set({ messagesLoading: Boolean(loading) });
|
||||
},
|
||||
|
||||
addMessage: message => {
|
||||
set(state => ({
|
||||
currentMessages: [...state.currentMessages, message],
|
||||
}));
|
||||
},
|
||||
|
||||
receivedMsg: message => {
|
||||
const currentContract = useWeChatStore.getState().currentContract;
|
||||
if (
|
||||
currentContract &&
|
||||
currentContract.wechatAccountId == message.wechatAccountId &&
|
||||
currentContract.id == message.wechatFriendId
|
||||
) {
|
||||
set(state => ({
|
||||
currentMessages: [...state.currentMessages, message],
|
||||
}));
|
||||
} else {
|
||||
//更新消息列表unread数值,根据接收的++1 这样
|
||||
const chatSessions = useCkChatStore.getState().chatSessions;
|
||||
const session = chatSessions.find(
|
||||
item => item.id == message.wechatFriendId,
|
||||
);
|
||||
if (session) {
|
||||
session.unreadCount = Number(session.unreadCount) + 1;
|
||||
updateChatSession(session);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateMessage: (messageId, updates) => {
|
||||
set(state => ({
|
||||
currentMessages: state.currentMessages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, ...updates } : msg,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 便捷选择器
|
||||
getCurrentContact: () => get().currentContract,
|
||||
getCurrentMessages: () => get().currentMessages,
|
||||
getMessagesLoading: () => get().messagesLoading,
|
||||
|
||||
// 视频消息处理方法
|
||||
setVideoLoading: (messageId: number, isLoading: boolean) => {
|
||||
set(state => ({
|
||||
currentMessages: state.currentMessages.map(msg => {
|
||||
if (msg.id === messageId) {
|
||||
try {
|
||||
const content = JSON.parse(msg.content);
|
||||
// 更新加载状态
|
||||
const updatedContent = { ...content, isLoading };
|
||||
return {
|
||||
...msg,
|
||||
content: JSON.stringify(updatedContent),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("更新视频加载状态失败:", e);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}),
|
||||
}));
|
||||
},
|
||||
|
||||
setVideoUrl: (messageId: number, videoUrl: string) => {
|
||||
set(state => ({
|
||||
currentMessages: state.currentMessages.map(msg => {
|
||||
if (msg.id === messageId) {
|
||||
try {
|
||||
const content = JSON.parse(msg.content);
|
||||
// 检查视频是否已经下载完毕,避免重复更新
|
||||
if (content.videoUrl && content.videoUrl === videoUrl) {
|
||||
console.log("视频已下载,跳过重复更新:", messageId);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// 设置视频URL并清除加载状态
|
||||
const updatedContent = {
|
||||
...content,
|
||||
videoUrl,
|
||||
isLoading: false,
|
||||
};
|
||||
return {
|
||||
...msg,
|
||||
content: JSON.stringify(updatedContent),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("更新视频URL失败:", e);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}),
|
||||
}));
|
||||
},
|
||||
clearAllData: () => {
|
||||
set({
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
messagesLoading: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "wechat-storage",
|
||||
partialize: state => ({
|
||||
// currentContract 不做持久化,登录和页面刷新时直接清空
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 导出便捷的选择器函数
|
||||
export const useCurrentContact = () =>
|
||||
useWeChatStore(state => state.currentContract);
|
||||
export const useCurrentMessages = () =>
|
||||
useWeChatStore(state => state.currentMessages);
|
||||
export const useMessagesLoading = () =>
|
||||
useWeChatStore(state => state.messagesLoading);
|
||||
27
Cunkebao/src/store/module/websocket/msg.data.ts
Normal file
27
Cunkebao/src/store/module/websocket/msg.data.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface FriendMessage {
|
||||
id: number;
|
||||
wechatFriendId: number;
|
||||
wechatAccountId: number;
|
||||
tenantId: number;
|
||||
accountId: number;
|
||||
synergyAccountId: number;
|
||||
content: string;
|
||||
msgType: number;
|
||||
msgSubType: number;
|
||||
msgSvrId: string;
|
||||
isSend: boolean;
|
||||
createTime: string;
|
||||
isDeleted: boolean;
|
||||
deleteTime: string;
|
||||
sendStatus: number;
|
||||
wechatTime: number;
|
||||
origin: number;
|
||||
msgId: number;
|
||||
recalled: boolean;
|
||||
}
|
||||
export interface Messages {
|
||||
friendMessage: FriendMessage | null;
|
||||
chatroomMessage: string;
|
||||
seq: number;
|
||||
cmdType: string;
|
||||
}
|
||||
@@ -2,8 +2,14 @@
|
||||
import { deepCopy } from "@/utils/common";
|
||||
import { WebSocketMessage } from "./websocket";
|
||||
import { getkfUserList, asyncKfUserList } from "@/store/module/ckchat/ckchat";
|
||||
import { Messages } from "./msg.data";
|
||||
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
// 消息处理器类型定义
|
||||
type MessageHandler = (message: WebSocketMessage) => void;
|
||||
const setVideoUrl = useWeChatStore.getState().setVideoUrl;
|
||||
const addMessage = useWeChatStore.getState().addMessage;
|
||||
const receivedMsg = useWeChatStore.getState().receivedMsg;
|
||||
|
||||
// 消息处理器映射
|
||||
const messageHandlers: Record<string, MessageHandler> = {
|
||||
@@ -19,6 +25,31 @@ const messageHandlers: Record<string, MessageHandler> = {
|
||||
});
|
||||
asyncKfUserList(kfUserList);
|
||||
},
|
||||
// 发送消息响应
|
||||
CmdSendMessageResp: message => {
|
||||
console.log("发送消息响应", message);
|
||||
addMessage(message.friendMessage);
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
CmdSendMessageResult: message => {
|
||||
console.log("发送消息结果", message);
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
// 接收消息响应
|
||||
CmdReceiveMessageResp: message => {
|
||||
console.log("接收消息响应", message);
|
||||
addMessage(message.friendMessage);
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
//收到消息
|
||||
CmdNewMessage: (message: Messages) => {
|
||||
// 在这里添加具体的处理逻辑
|
||||
receivedMsg(message.friendMessage);
|
||||
},
|
||||
CmdFriendInfoChanged: message => {
|
||||
// console.log("好友信息变更", message);
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
|
||||
// 登录响应
|
||||
CmdSignInResp: message => {
|
||||
@@ -30,6 +61,15 @@ const messageHandlers: Record<string, MessageHandler> = {
|
||||
CmdNotify: message => {
|
||||
console.log("通知消息", message);
|
||||
// 在这里添加具体的处理逻辑
|
||||
if (message.notify == "Kicked out") {
|
||||
// 被踢出时直接跳转到登录页面
|
||||
window.location.href = "/login";
|
||||
}
|
||||
},
|
||||
|
||||
CmdDownloadVideoResult: message => {
|
||||
// 在这里添加具体的处理逻辑
|
||||
setVideoUrl(message.friendMessageId, message.url);
|
||||
},
|
||||
|
||||
// 可以继续添加更多处理器...
|
||||
|
||||
@@ -51,6 +51,7 @@ interface WebSocketState {
|
||||
// 重连相关
|
||||
reconnectAttempts: number;
|
||||
reconnectTimer: NodeJS.Timeout | null;
|
||||
aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器
|
||||
|
||||
// 方法
|
||||
connect: (config: Partial<WebSocketConfig>) => void;
|
||||
@@ -60,6 +61,7 @@ interface WebSocketState {
|
||||
clearMessages: () => void;
|
||||
markAsRead: () => void;
|
||||
reconnect: () => void;
|
||||
clearConnectionState: () => void; // 清空连接状态
|
||||
|
||||
// 内部方法
|
||||
_handleOpen: () => void;
|
||||
@@ -68,6 +70,8 @@ interface WebSocketState {
|
||||
_handleError: (event: Event) => void;
|
||||
_startReconnectTimer: () => void;
|
||||
_stopReconnectTimer: () => void;
|
||||
_startAliveStatusTimer: () => void; // 启动客服状态查询定时器
|
||||
_stopAliveStatusTimer: () => void; // 停止客服状态查询定时器
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
@@ -92,6 +96,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
unreadCount: 0,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
aliveStatusTimer: null,
|
||||
|
||||
// 连接WebSocket
|
||||
connect: (config: Partial<WebSocketConfig>) => {
|
||||
@@ -183,6 +188,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
}
|
||||
|
||||
currentState._stopReconnectTimer();
|
||||
currentState._stopAliveStatusTimer();
|
||||
|
||||
set({
|
||||
status: WebSocketStatus.DISCONNECTED,
|
||||
@@ -226,7 +232,16 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
currentState.status !== WebSocketStatus.CONNECTED ||
|
||||
!currentState.ws
|
||||
) {
|
||||
Toast.show({ content: "WebSocket未连接", position: "top" });
|
||||
Toast.show({
|
||||
content: "WebSocket未连接,正在重新连接...",
|
||||
position: "top",
|
||||
});
|
||||
|
||||
// 重置连接状态并发起重新连接
|
||||
set({ status: WebSocketStatus.DISCONNECTED });
|
||||
if (currentState.config) {
|
||||
currentState.connect(currentState.config);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,6 +257,12 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
} catch (error) {
|
||||
// console.error("命令发送失败:", error);
|
||||
Toast.show({ content: "命令发送失败", position: "top" });
|
||||
|
||||
// 发送失败时也尝试重新连接
|
||||
set({ status: WebSocketStatus.DISCONNECTED });
|
||||
if (currentState.config) {
|
||||
currentState.connect(currentState.config);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -269,6 +290,34 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
}
|
||||
},
|
||||
|
||||
// 清空连接状态(用于退出登录时)
|
||||
clearConnectionState: () => {
|
||||
const currentState = get();
|
||||
|
||||
// 断开现有连接
|
||||
if (currentState.ws) {
|
||||
currentState.ws.close();
|
||||
}
|
||||
|
||||
// 停止所有定时器
|
||||
currentState._stopReconnectTimer();
|
||||
currentState._stopAliveStatusTimer();
|
||||
|
||||
// 重置所有状态
|
||||
set({
|
||||
status: WebSocketStatus.DISCONNECTED,
|
||||
ws: null,
|
||||
config: null,
|
||||
messages: [],
|
||||
unreadCount: 0,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
aliveStatusTimer: null,
|
||||
});
|
||||
|
||||
// console.log("WebSocket连接状态已清空");
|
||||
},
|
||||
|
||||
// 内部方法:处理连接打开
|
||||
_handleOpen: () => {
|
||||
const currentState = get();
|
||||
@@ -291,6 +340,9 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
}
|
||||
|
||||
Toast.show({ content: "WebSocket连接成功", position: "top" });
|
||||
|
||||
// 启动客服状态查询定时器
|
||||
currentState._startAliveStatusTimer();
|
||||
},
|
||||
|
||||
// 内部方法:处理消息接收
|
||||
@@ -319,6 +371,9 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
});
|
||||
}
|
||||
|
||||
// 停止客服状态查询定时器
|
||||
get()._stopAliveStatusTimer();
|
||||
|
||||
// 断开连接
|
||||
get().disconnect();
|
||||
return;
|
||||
@@ -414,6 +469,51 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
set({ reconnectTimer: null });
|
||||
}
|
||||
},
|
||||
|
||||
// 内部方法:启动客服状态查询定时器
|
||||
_startAliveStatusTimer: () => {
|
||||
const currentState = get();
|
||||
|
||||
// 先停止现有定时器
|
||||
currentState._stopAliveStatusTimer();
|
||||
|
||||
// 获取客服用户列表
|
||||
const { kfUserList } = useCkChatStore.getState();
|
||||
|
||||
// 如果没有客服用户,不启动定时器
|
||||
if (!kfUserList || kfUserList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动定时器,每5秒查询一次
|
||||
const timer = setInterval(() => {
|
||||
const state = get();
|
||||
// 检查连接状态
|
||||
if (state.status === WebSocketStatus.CONNECTED) {
|
||||
const { kfUserList: currentKfUserList } = useCkChatStore.getState();
|
||||
if (currentKfUserList && currentKfUserList.length > 0) {
|
||||
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: currentKfUserList.map(v => v.id),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 如果连接断开,停止定时器
|
||||
state._stopAliveStatusTimer();
|
||||
}
|
||||
}, 5 * 1000);
|
||||
|
||||
set({ aliveStatusTimer: timer });
|
||||
},
|
||||
|
||||
// 内部方法:停止客服状态查询定时器
|
||||
_stopAliveStatusTimer: () => {
|
||||
const currentState = get();
|
||||
|
||||
if (currentState.aliveStatusTimer) {
|
||||
clearInterval(currentState.aliveStatusTimer);
|
||||
set({ aliveStatusTimer: null });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "websocket-store",
|
||||
@@ -424,6 +524,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
messages: state.messages.slice(-100), // 只保留最近100条消息
|
||||
unreadCount: state.unreadCount,
|
||||
reconnectAttempts: state.reconnectAttempts,
|
||||
// 注意:定时器不需要持久化,重新连接时会重新创建
|
||||
}),
|
||||
onRehydrateStorage: () => state => {
|
||||
// 页面刷新后,如果之前是连接状态,尝试重新连接
|
||||
|
||||
Reference in New Issue
Block a user