refactor(wechat): 重构微信聊天界面组件结构,将相关文件移动到weChat子目录下

重构微信聊天界面的组件结构,将原有分散的组件文件整理到weChat子目录中,包括消息列表、联系人列表、垂直用户列表等组件。同时优化了样式文件和类型定义,保持代码结构清晰。修改路由配置以支持新的目录结构。
This commit is contained in:
超级老白兔
2025-09-08 18:07:12 +08:00
parent 6c6d0f328e
commit fe5e7b1633
32 changed files with 1912 additions and 102 deletions

View File

@@ -1,105 +1,14 @@
import React, { useState, useEffect } from "react";
import { Layout, Button, Space, message, Tooltip } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList";
import PageSkeleton from "./components/Skeleton";
import React from "react";
import { Layout } from "antd";
import { Outlet } from "react-router-dom";
import NavCommon from "./components/NavCommon";
import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat/ckchat";
const { Content, Sider } = Layout;
import { chatInitAPIdata, initSocket } from "./main";
import { useWeChatStore } from "@/store/module/weChat/weChat";
import { KfUserListData } from "@/pages/pc/ckbox/data";
const CkboxPage: React.FC = () => {
// 不要在组件初始化时获取sendCommand而是在需要时动态获取
const [loading, setLoading] = useState(false);
const currentContract = useWeChatStore(state => state.currentContract);
useEffect(() => {
// 方法一:使用 Promise 链式调用处理异步函数
setLoading(true);
chatInitAPIdata()
.then(response => {
const data = response as {
contractList: any[];
groupList: any[];
kfUserList: KfUserListData[];
newContractList: { groupName: string; contacts: any[] }[];
};
const { contractList } = data;
//找出已经在聊天的
const isChatList = contractList.filter(
v => (v?.config && v.config?.chat) || false,
);
isChatList.forEach(v => {
addChatSession(v);
});
// 数据加载完成后初始化WebSocket连接
initSocket();
})
.catch(error => {
console.error("获取联系人列表失败:", error);
})
.finally(() => {
setLoading(false);
});
}, []);
return (
<PageSkeleton loading={loading}>
<Layout className={styles.ckboxLayout}>
<NavCommon title="AI自动聊天懂业务会引导客户不停地聊不停" />
<Layout>
{/* 垂直侧边栏 */}
<Sider width={80} className={styles.verticalSider}>
<VerticalUserList />
</Sider>
{/* 左侧联系人边栏 */}
<Sider width={280} className={styles.sider}>
<SidebarMenu loading={loading} />
</Sider>
{/* 主内容区 */}
<Content className={styles.mainContent}>
{currentContract ? (
<div className={styles.chatContainer}>
{/* <div className={styles.chatToolbar}>
<Space>
<Tooltip title={showProfile ? "隐藏资料" : "显示资料"}>
<Button
type={showProfile ? "primary" : "default"}
icon={<InfoCircleOutlined />}
onClick={() => setShowProfile(!showProfile)}
size="small"
>
{showProfile ? "隐藏资料" : "显示资料"}
</Button>
</Tooltip>
</Space>
</div> */}
<ChatWindow contract={currentContract} />
</div>
) : (
<div className={styles.welcomeScreen}>
<div className={styles.welcomeContent}>
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
<h2>使</h2>
<p></p>
</div>
</div>
)}
</Content>
</Layout>
</Layout>
</PageSkeleton>
<Layout className={styles.ckboxLayout}>
<NavCommon title="AI自动聊天懂业务会引导客户不停地聊不停" />
<Outlet />
</Layout>
);
};

View File

@@ -0,0 +1,153 @@
.monitoring {
padding: 24px;
background-color: #f5f5f5;
min-height: 100vh;
.header {
margin-bottom: 24px;
h2 {
margin: 0 0 8px 0;
color: #262626;
font-size: 24px;
font-weight: 600;
}
p {
margin: 0;
color: #8c8c8c;
font-size: 14px;
}
}
.statsRow {
margin-bottom: 24px;
.statCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
}
}
.progressRow {
margin-bottom: 24px;
.progressCard,
.metricsCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
height: 280px;
.ant-card-body {
height: calc(100% - 57px);
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
.progressItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
span {
display: block;
margin-bottom: 8px;
color: #595959;
font-size: 14px;
}
}
.metricItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
span {
color: #595959;
font-size: 14px;
}
.metricValue {
display: flex;
align-items: center;
gap: 8px;
span {
font-weight: 600;
font-size: 16px;
color: #262626;
}
}
}
}
.tableRow {
.tableCard {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.ant-table {
.ant-table-thead > tr > th {
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
}
.ant-table-tbody > tr {
&:hover {
background-color: #f5f5f5;
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.monitoring {
padding: 16px;
.header {
h2 {
font-size: 20px;
}
}
.progressRow {
.progressCard,
.metricsCard {
height: auto;
margin-bottom: 16px;
}
}
}
}
@media (max-width: 576px) {
.monitoring {
padding: 12px;
.statsRow {
.statCard {
margin-bottom: 12px;
}
}
}
}

View File

@@ -0,0 +1,186 @@
import React from 'react';
import { Card, Row, Col, Statistic, Progress, Table, Tag } from 'antd';
import {
UserOutlined,
MessageOutlined,
TeamOutlined,
TrophyOutlined,
ArrowUpOutlined,
ArrowDownOutlined
} from '@ant-design/icons';
import styles from './index.module.scss';
interface MonitoringProps {
// 预留接口属性
}
const Monitoring: React.FC<MonitoringProps> = () => {
// 模拟数据
const statsData = [
{
title: '在线设备数',
value: 128,
prefix: <UserOutlined />,
suffix: '台',
valueStyle: { color: '#3f8600' },
},
{
title: '今日消息量',
value: 2456,
prefix: <MessageOutlined />,
suffix: '条',
valueStyle: { color: '#1890ff' },
},
{
title: '活跃群组',
value: 89,
prefix: <TeamOutlined />,
suffix: '个',
valueStyle: { color: '#722ed1' },
},
{
title: '成功率',
value: 98.5,
prefix: <TrophyOutlined />,
suffix: '%',
valueStyle: { color: '#f5222d' },
},
];
const tableColumns = [
{
title: '设备名称',
dataIndex: 'deviceName',
key: 'deviceName',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === '在线' ? 'green' : 'red'}>{status}</Tag>
),
},
{
title: '消息数',
dataIndex: 'messageCount',
key: 'messageCount',
},
{
title: '最后活跃时间',
dataIndex: 'lastActive',
key: 'lastActive',
},
];
const tableData = [
{
key: '1',
deviceName: '设备001',
status: '在线',
messageCount: 245,
lastActive: '2024-01-15 14:30:25',
},
{
key: '2',
deviceName: '设备002',
status: '离线',
messageCount: 156,
lastActive: '2024-01-15 12:15:10',
},
{
key: '3',
deviceName: '设备003',
status: '在线',
messageCount: 389,
lastActive: '2024-01-15 14:28:45',
},
];
return (
<div className={styles.monitoring}>
<div className={styles.header}>
<h2></h2>
<p></p>
</div>
{/* 统计卡片 */}
<Row gutter={[16, 16]} className={styles.statsRow}>
{statsData.map((stat, index) => (
<Col xs={24} sm={12} md={6} key={index}>
<Card className={styles.statCard}>
<Statistic
title={stat.title}
value={stat.value}
prefix={stat.prefix}
suffix={stat.suffix}
valueStyle={stat.valueStyle}
/>
</Card>
</Col>
))}
</Row>
{/* 进度指标 */}
<Row gutter={[16, 16]} className={styles.progressRow}>
<Col xs={24} md={12}>
<Card title="系统负载" className={styles.progressCard}>
<div className={styles.progressItem}>
<span>CPU使用率</span>
<Progress percent={65} status="active" />
</div>
<div className={styles.progressItem}>
<span>使</span>
<Progress percent={45} />
</div>
<div className={styles.progressItem}>
<span>使</span>
<Progress percent={30} />
</div>
</Card>
</Col>
<Col xs={24} md={12}>
<Card title="实时指标" className={styles.metricsCard}>
<div className={styles.metricItem}>
<span></span>
<div className={styles.metricValue}>
<span>1,245</span>
<ArrowUpOutlined style={{ color: '#3f8600' }} />
</div>
</div>
<div className={styles.metricItem}>
<span></span>
<div className={styles.metricValue}>
<span>0.2%</span>
<ArrowDownOutlined style={{ color: '#3f8600' }} />
</div>
</div>
<div className={styles.metricItem}>
<span></span>
<div className={styles.metricValue}>
<span>125ms</span>
<ArrowDownOutlined style={{ color: '#3f8600' }} />
</div>
</div>
</Card>
</Col>
</Row>
{/* 设备状态表格 */}
<Row className={styles.tableRow}>
<Col span={24}>
<Card title="设备状态" className={styles.tableCard}>
<Table
columns={tableColumns}
dataSource={tableData}
pagination={false}
size="middle"
/>
</Card>
</Col>
</Row>
</div>
);
};
export default Monitoring;

View File

@@ -0,0 +1,246 @@
import request from "@/api/request2";
import {
MessageData,
ChatHistoryResponse,
MessageType,
OnlineStatus,
MessageStatus,
FileUploadResponse,
EmojiData,
QuickReply,
ChatSettings,
} from "./data";
//读取聊天信息
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
export function WechatGroup(params) {
return request("/api/WechatGroup/list", params, "GET");
}
//获取聊天记录-1 清除未读
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 getChatMessages(params: {
wechatAccountId: number;
wechatFriendId?: number;
wechatChatroomId?: number;
From: number;
To: number;
Count: number;
olderData: boolean;
}) {
return request("/api/FriendMessage/SearchMessage", params, "GET");
}
export function getChatroomMessages(params: {
wechatAccountId: number;
wechatFriendId?: number;
wechatChatroomId?: number;
From: number;
To: number;
Count: number;
olderData: boolean;
}) {
return request("/api/ChatroomMessage/SearchMessage", params, "GET");
}
//获取群列表
export function getGroupList(params: { prevId: number; count: number }) {
return request(
"/api/wechatChatroom/listExcludeMembersByPage?",
params,
"GET",
);
}
//获取群成员
export function getGroupMembers(params: { id: number }) {
return request(
"/api/WechatChatroom/listMembersByWechatChatroomId",
params,
"GET",
);
}
//触客宝登陆
export function loginWithToken(params: any) {
return request(
"/token",
params,
"POST",
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
1000,
);
}
// 获取触客宝用户信息
export function getChuKeBaoUserInfo() {
return request("/api/account/self", {}, "GET");
}
// 获取联系人列表
export const getContactList = (params: { prevId: number; count: number }) => {
return request("/api/wechatFriend/list", params, "GET");
};
//获取控制终端列表
export const getControlTerminalList = params => {
return request("/api/wechataccount", params, "GET");
};
// 获取聊天历史
export const getChatHistory = (
chatId: string,
page: number = 1,
pageSize: number = 50,
): Promise<ChatHistoryResponse> => {
return request(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
};
// 发送消息
export const sendMessage = (
chatId: string,
content: string,
type: MessageType = MessageType.TEXT,
): Promise<MessageData> => {
return request(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
};
// 发送文件消息
export const sendFileMessage = (
chatId: string,
file: File,
type: MessageType,
): Promise<MessageData> => {
const formData = new FormData();
formData.append("file", file);
formData.append("type", type);
return request(`/v1/chats/${chatId}/messages/file`, formData, "POST");
};
// 标记消息为已读
export const markMessageAsRead = (messageId: string): Promise<void> => {
return request(`/v1/messages/${messageId}/read`, {}, "PUT");
};
// 标记聊天为已读
export const markChatAsRead = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/read`, {}, "PUT");
};
// 添加群组成员
export const addGroupMembers = (
groupId: string,
memberIds: string[],
): Promise<void> => {
return request(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
};
// 移除群组成员
export const removeGroupMembers = (
groupId: string,
memberIds: string[],
): Promise<void> => {
return request(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
};
// 获取在线状态
export const getOnlineStatus = (userId: string): Promise<OnlineStatus> => {
return request(`/v1/users/${userId}/status`, {}, "GET");
};
// 获取消息状态
export const getMessageStatus = (messageId: string): Promise<MessageStatus> => {
return request(`/v1/messages/${messageId}/status`, {}, "GET");
};
// 上传文件
export const uploadFile = (file: File): Promise<FileUploadResponse> => {
const formData = new FormData();
formData.append("file", file);
return request("/v1/upload", formData, "POST");
};
// 获取表情包列表
export const getEmojiList = (): Promise<EmojiData[]> => {
return request("/v1/emojis", {}, "GET");
};
// 获取快捷回复列表
export const getQuickReplies = (): Promise<QuickReply[]> => {
return request("/v1/quick-replies", {}, "GET");
};
// 添加快捷回复
export const addQuickReply = (data: {
content: string;
category: string;
}): Promise<QuickReply> => {
return request("/v1/quick-replies", data, "POST");
};
// 删除快捷回复
export const deleteQuickReply = (id: string): Promise<void> => {
return request(`/v1/quick-replies/${id}`, {}, "DELETE");
};
// 获取聊天设置
export const getChatSettings = (): Promise<ChatSettings> => {
return request("/v1/chat/settings", {}, "GET");
};
// 更新聊天设置
export const updateChatSettings = (
settings: Partial<ChatSettings>,
): Promise<ChatSettings> => {
return request("/v1/chat/settings", settings, "PUT");
};
// 删除聊天会话
export const deleteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}`, {}, "DELETE");
};
// 置顶聊天会话
export const pinChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/pin`, {}, "PUT");
};
// 取消置顶聊天会话
export const unpinChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/unpin`, {}, "PUT");
};
// 静音聊天会话
export const muteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/mute`, {}, "PUT");
};
// 取消静音聊天会话
export const unmuteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/unmute`, {}, "PUT");
};
// 转发消息
export const forwardMessage = (
messageId: string,
targetChatIds: string[],
): Promise<void> => {
return request("/v1/messages/forward", { messageId, targetChatIds }, "POST");
};
// 撤回消息
export const recallMessage = (messageId: string): Promise<void> => {
return request(`/v1/messages/${messageId}/recall`, {}, "PUT");
};

View File

@@ -0,0 +1,258 @@
.header {
background: #fff;
padding: 0 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-bottom: 1px solid #f0f0f0;
}
.headerLeft {
display: flex;
align-items: center;
gap: 12px;
}
.menuButton {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 6px;
transition: all 0.3s;
&:hover {
background-color: #f5f5f5;
}
.anticon {
font-size: 18px;
color: #666;
}
}
.title {
font-size: 18px;
color: #333;
margin: 0;
}
.headerRight {
display: flex;
align-items: center;
}
.userInfo {
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.3s;
&:hover {
background-color: #f5f5f5;
}
.suanli {
font-size: 16px;
color: #666;
.suanliIcon {
font-size: 24px;
}
}
}
.avatar {
border: 2px solid #f0f0f0;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
}
}
.username {
font-size: 14px;
color: #333;
font-weight: 500;
margin-left: 8px;
}
// 抽屉样式
.drawer {
:global(.ant-drawer-header) {
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
:global(.ant-drawer-body) {
padding: 0;
}
}
.drawerContent {
height: 100%;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
.drawerHeader {
padding: 20px;
background: #fff;
border-bottom: none;
}
.logoSection {
display: flex;
align-items: center;
gap: 12px;
}
.logoIcon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.logoText {
display: flex;
flex-direction: column;
}
.appName {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.appDesc {
font-size: 12px;
color: #999;
margin: 2px 0 0 0;
}
.drawerBody {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.primaryButton {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.buttonIcon {
font-size: 20px;
}
span {
font-size: 16px;
font-weight: 600;
color: white;
}
}
.menuSection {
margin-top: 8px;
}
.menuItem {
display: flex;
align-items: center;
padding: 16px 20px;
cursor: pointer;
transition: all 0.3s;
border-radius: 8px;
&:hover {
background-color: #f0f0f0;
}
span {
font-size: 16px;
color: #333;
font-weight: 500;
}
}
.menuIcon {
font-size: 20px;
margin-right: 12px;
width: 20px;
text-align: center;
}
.drawerFooter {
padding: 20px;
background: #fff;
border-top: 1px solid #f0f0f0;
.balanceSection {
display: flex;
justify-content: space-between;
align-items: center;
.balanceIcon {
color: #666;
.suanliIcon {
font-size: 20px;
}
}
.balanceText {
color: #3d9c0d;
}
}
}
.balanceLabel {
font-size: 12px;
color: #999;
margin: 0;
}
.balanceAmount {
font-size: 18px;
font-weight: 600;
color: #52c41a;
margin: 2px 0 0 0;
}
// 响应式设计
@media (max-width: 768px) {
.header {
padding: 0 12px;
}
.title {
font-size: 16px;
}
.username {
display: none;
}
.drawer {
width: 280px !important;
}
}

View File

@@ -0,0 +1,120 @@
import React, { useState } from "react";
import { Layout, Drawer, Avatar, Dropdown, Space, Button } from "antd";
import {
MenuOutlined,
UserOutlined,
LogoutOutlined,
SettingOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { useNavigate } from "react-router-dom";
import styles from "./index.module.scss";
const { Header } = Layout;
interface NavCommonProps {
title?: string;
onMenuClick?: () => void;
drawerContent?: React.ReactNode;
}
const NavCommon: React.FC<NavCommonProps> = ({
title = "触客宝",
onMenuClick,
drawerContent,
}) => {
const [drawerVisible, setDrawerVisible] = useState(false);
const { userInfo } = useCkChatStore();
// 处理菜单图标点击
const handleMenuClick = () => {
setDrawerVisible(true);
onMenuClick?.();
};
// 处理抽屉关闭
const handleDrawerClose = () => {
setDrawerVisible(false);
};
// 默认抽屉内容
const defaultDrawerContent = (
<div className={styles.drawerContent}>
<div className={styles.drawerHeader}>
<div className={styles.logoSection}>
<div className={styles.logoIcon}></div>
<div className={styles.logoText}>
<div className={styles.appName}></div>
<div className={styles.appDesc}>AI智能营销系统</div>
</div>
</div>
</div>
<div className={styles.drawerBody}>
<div className={styles.primaryButton}>
<div className={styles.buttonIcon}>🔒</div>
<span>AI智能客服</span>
</div>
<div className={styles.menuSection}>
<div className={styles.menuItem}>
<div className={styles.menuIcon}>📊</div>
<span></span>
</div>
</div>
</div>
<div className={styles.drawerFooter}>
<div className={styles.balanceSection}>
<div className={styles.balanceIcon}>
<span className={styles.suanliIcon}></span>
</div>
<div className={styles.balanceText}>9307.423</div>
</div>
</div>
</div>
);
return (
<>
<Header className={styles.header}>
<div className={styles.headerLeft}>
<Button
type="text"
icon={<MenuOutlined />}
onClick={handleMenuClick}
className={styles.menuButton}
/>
<span className={styles.title}>{title}</span>
</div>
<div className={styles.headerRight}>
<Space className={styles.userInfo}>
<span className={styles.suanli}>
<span className={styles.suanliIcon}></span>
9307.423
</span>
<Avatar
size={40}
icon={<UserOutlined />}
src={userInfo?.account?.avatar}
className={styles.avatar}
/>
</Space>
</div>
</Header>
<Drawer
title="菜单"
placement="left"
onClose={handleDrawerClose}
open={drawerVisible}
width={300}
className={styles.drawer}
>
{drawerContent || defaultDrawerContent}
</Drawer>
</>
);
};
export default NavCommon;

View File

@@ -0,0 +1,323 @@
// 消息列表数据接口 - 支持weChatGroup和contracts两种数据类型
export interface MessageListData {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
// 数据类型标识
dataType: "weChatGroup" | "contracts"; // 数据类型:微信群组或联系人
// 通用字段(两种类型都有的字段)
wechatAccountId: number; // 微信账号ID
tenantId: number; // 租户ID
accountId: number; // 账号ID
nickname: string; // 昵称
avatar?: string; // 头像
groupId: number; // 分组ID
config?: {
chat: boolean;
}; // 配置信息
labels?: string[]; // 标签列表
unreadCount: number; // 未读消息数
// 联系人特有字段当dataType为'contracts'时使用)
wechatId?: string; // 微信ID
alias?: string; // 别名
conRemark?: string; // 备注
quanPin?: string; // 全拼
gender?: number; // 性别
region?: string; // 地区
addFrom?: number; // 添加来源
phone?: string; // 电话
signature?: string; // 签名
extendFields?: any; // 扩展字段
city?: string; // 城市
lastUpdateTime?: string; // 最后更新时间
isPassed?: boolean; // 是否通过
thirdParty?: any; // 第三方
additionalPicture?: string; // 附加图片
desc?: string; // 描述
lastMessageTime?: number; // 最后消息时间
duplicate?: boolean; // 是否重复
// 微信群组特有字段当dataType为'weChatGroup'时使用)
chatroomId?: string; // 群聊ID
chatroomOwner?: string; // 群主
chatroomAvatar?: string; // 群头像
notice?: string; // 群公告
selfDisplyName?: string; // 自己在群里的显示名称
[key: string]: any; // 兼容其他字段
}
//联系人标签分组
export interface ContactGroupByLabel {
id: number;
accountId?: number;
groupName: string;
tenantId?: number;
count: number;
[key: string]: any;
}
//终端用户数据接口
export interface KfUserListData {
id: number;
tenantId: number;
wechatId: string;
nickname: string;
alias: string;
avatar: string;
gender: number;
region: string;
signature: string;
bindQQ: string;
bindEmail: string;
bindMobile: string;
createTime: string;
currentDeviceId: number;
isDeleted: boolean;
deleteTime: string;
groupId: number;
memo: string;
wechatVersion: string;
labels: string[];
lastUpdateTime: string;
isOnline?: boolean;
[key: string]: any;
}
// 账户信息接口
export interface CkAccount {
id: number;
realName: string;
nickname: string | null;
memo: string | null;
avatar: string;
userName: string;
secret: string;
accountType: number;
departmentId: number;
useGoogleSecretKey: boolean;
hasVerifyGoogleSecret: boolean;
}
//群聊数据接口
export interface weChatGroup {
id?: number;
wechatAccountId: number;
tenantId: number;
accountId: number;
chatroomId: string;
chatroomOwner: string;
conRemark: string;
nickname: string;
chatroomAvatar: string;
groupId: number;
config?: {
chat: boolean;
};
labels?: string[];
unreadCount: number;
notice: string;
selfDisplyName: string;
wechatChatroomId: number;
serverId?: number;
[key: string]: any;
}
// 联系人数据接口
export interface ContractData {
id?: number;
serverId?: number;
wechatAccountId: number;
wechatId: string;
alias: string;
conRemark: string;
nickname: string;
quanPin: string;
avatar?: string;
gender: number;
region: string;
addFrom: number;
phone: string;
labels: string[];
signature: string;
accountId: number;
extendFields: null;
city?: string;
lastUpdateTime: string;
isPassed: boolean;
tenantId: number;
groupId: number;
thirdParty: null;
additionalPicture: string;
desc: string;
config?: {
chat: boolean;
};
lastMessageTime: number;
unreadCount: number;
duplicate: boolean;
[key: string]: any;
}
//聊天记录接口
export interface ChatRecord {
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;
sender?: {
chatroomNickname: string;
isAdmin: boolean;
isDeleted: boolean;
nickname: string;
ownerWechatId: string;
wechatId: string;
[key: string]: any;
};
[key: string]: any;
}
/**
* 微信好友基本信息接口
* 包含主要字段和兼容性字段
*/
export interface WechatFriend {
// 主要字段
id: number; // 好友ID
wechatAccountId: number; // 微信账号ID
wechatId: string; // 微信ID
nickname: string; // 昵称
conRemark: string; // 备注名
avatar: string; // 头像URL
gender: number; // 性别1-男2-女0-未知
region: string; // 地区
phone: string; // 电话
labels: string[]; // 标签列表
[key: string]: any;
}
// 消息类型枚举
export enum MessageType {
TEXT = "text",
IMAGE = "image",
VOICE = "voice",
VIDEO = "video",
FILE = "file",
LOCATION = "location",
}
// 消息数据接口
export interface MessageData {
id: string;
senderId: string;
senderName: string;
content: string;
type: MessageType;
timestamp: string;
isRead: boolean;
replyTo?: string;
forwardFrom?: string;
}
// 聊天会话类型
export type ChatType = "private" | "group";
// 聊天会话接口
export interface ChatSession {
id: string;
type: ChatType;
name: string;
avatar?: string;
lastMessage: string;
lastTime: string;
unreadCount: number;
online: boolean;
members?: string[];
pinned?: boolean;
muted?: boolean;
}
// 聊天历史响应接口
export interface ChatHistoryResponse {
messages: MessageData[];
hasMore: boolean;
total: number;
}
// 发送消息请求接口
export interface SendMessageRequest {
chatId: string;
content: string;
type: MessageType;
replyTo?: string;
}
// 搜索联系人请求接口
export interface SearchContactRequest {
keyword: string;
limit?: number;
}
// 在线状态接口
export interface OnlineStatus {
userId: string;
online: boolean;
lastSeen: string;
}
// 消息状态接口
export interface MessageStatus {
messageId: string;
status: "sending" | "sent" | "delivered" | "read" | "failed";
timestamp: string;
}
// 文件上传响应接口
export interface FileUploadResponse {
url: string;
filename: string;
size: number;
type: string;
}
// 表情包接口
export interface EmojiData {
id: string;
name: string;
url: string;
category: string;
}
// 快捷回复接口
export interface QuickReply {
id: string;
content: string;
category: string;
useCount: number;
}
// 聊天设置接口
export interface ChatSettings {
autoReply: boolean;
autoReplyMessage: string;
notification: boolean;
sound: boolean;
theme: "light" | "dark";
fontSize: "small" | "medium" | "large";
}

View File

@@ -0,0 +1,198 @@
.ckboxLayout {
height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
.header {
background: #1890ff;
color: #fff;
height: 64px;
line-height: 64px;
padding: 0 24px;
font-size: 18px;
font-weight: bold;
}
.verticalSider {
background: #2e2e2e;
border-right: 1px solid #f0f0f0;
overflow: hidden;
}
.sider {
background: #fff;
border-right: 1px solid #f0f0f0;
overflow: auto;
}
.sidebar {
height: 100%;
display: flex;
flex-direction: column;
.searchBar {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
:global(.ant-input) {
border-radius: 20px;
background: #f5f5f5;
border: none;
&:focus {
background: #fff;
border: 1px solid #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
.tabs {
flex: 1;
display: flex;
flex-direction: column;
:global(.ant-tabs-content) {
flex: 1;
overflow: hidden;
}
:global(.ant-tabs-tabpane) {
height: 100%;
overflow: hidden;
}
:global(.ant-tabs-nav) {
margin: 0;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
:global(.ant-tabs-tab) {
padding: 12px 16px;
margin: 0;
&:hover {
color: #1890ff;
}
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #1890ff;
}
}
}
:global(.ant-tabs-ink-bar) {
background: #1890ff;
}
}
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #8c8c8c;
p {
margin-top: 16px;
font-size: 14px;
}
}
}
.mainContent {
background: #f5f5f5;
display: flex;
flex-direction: column;
overflow: auto;
.chatContainer {
height: 100%;
display: flex;
flex-direction: column;
.chatToolbar {
background: #fff;
border-bottom: 1px solid #f0f0f0;
padding: 8px 16px;
display: flex;
justify-content: flex-end;
align-items: center;
min-height: 48px;
}
}
.welcomeScreen {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
.welcomeContent {
text-align: center;
color: #8c8c8c;
h2 {
margin: 24px 0 12px 0;
color: #262626;
font-size: 24px;
font-weight: 600;
}
p {
font-size: 16px;
margin: 0;
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.ckboxLayout {
.sidebar {
.searchBar {
padding: 12px;
}
.tabs {
:global(.ant-tabs-nav) {
padding: 0 12px;
:global(.ant-tabs-tab) {
padding: 10px 12px;
}
}
}
}
.mainContent {
.chatContainer {
.chatToolbar {
padding: 6px 12px;
min-height: 40px;
}
}
.welcomeScreen {
.welcomeContent {
h2 {
font-size: 20px;
}
p {
font-size: 14px;
}
}
}
}
}
}

View File

@@ -0,0 +1,106 @@
import React, { useState, useEffect } from "react";
import { Layout, Button, Space, message, Tooltip } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList";
import PageSkeleton from "./components/Skeleton";
import NavCommon from "./components/NavCommon";
import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat/ckchat";
const { Content, Sider } = Layout;
import { chatInitAPIdata, initSocket } from "./main";
import { useWeChatStore } from "@/store/module/weChat/weChat";
import { KfUserListData } from "@/pages/pc/ckbox/data";
const CkboxPage: React.FC = () => {
// 不要在组件初始化时获取sendCommand而是在需要时动态获取
const [loading, setLoading] = useState(false);
const currentContract = useWeChatStore(state => state.currentContract);
useEffect(() => {
// 方法一:使用 Promise 链式调用处理异步函数
setLoading(true);
chatInitAPIdata()
.then(response => {
const data = response as {
contractList: any[];
groupList: any[];
kfUserList: KfUserListData[];
newContractList: { groupName: string; contacts: any[] }[];
};
const { contractList } = data;
//找出已经在聊天的
const isChatList = contractList.filter(
v => (v?.config && v.config?.chat) || false,
);
isChatList.forEach(v => {
addChatSession(v);
});
// 数据加载完成后初始化WebSocket连接
initSocket();
})
.catch(error => {
console.error("获取联系人列表失败:", error);
})
.finally(() => {
setLoading(false);
});
}, []);
return (
<PageSkeleton loading={loading}>
<Layout className={styles.ckboxLayout}>
<NavCommon title="AI自动聊天懂业务会引导客户不停地聊不停" />
<Layout>
{/* 垂直侧边栏 */}
<Sider width={80} className={styles.verticalSider}>
<VerticalUserList />
</Sider>
{/* 左侧联系人边栏 */}
<Sider width={280} className={styles.sider}>
<SidebarMenu loading={loading} />
</Sider>
{/* 主内容区 */}
<Content className={styles.mainContent}>
{currentContract ? (
<div className={styles.chatContainer}>
{/* <div className={styles.chatToolbar}>
<Space>
<Tooltip title={showProfile ? "隐藏资料" : "显示资料"}>
<Button
type={showProfile ? "primary" : "default"}
icon={<InfoCircleOutlined />}
onClick={() => setShowProfile(!showProfile)}
size="small"
>
{showProfile ? "隐藏资料" : "显示资料"}
</Button>
</Tooltip>
</Space>
</div> */}
<ChatWindow contract={currentContract} />
</div>
) : (
<div className={styles.welcomeScreen}>
<div className={styles.welcomeContent}>
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
<h2>使</h2>
<p></p>
</div>
</div>
)}
</Content>
</Layout>
</Layout>
</PageSkeleton>
);
};
export default CkboxPage;

View File

@@ -0,0 +1,307 @@
import {
asyncKfUserList,
asyncContractList,
asyncChatSessions,
asyncWeChatGroup,
asyncCountLables,
useCkChatStore,
} from "@/store/module/ckchat/ckchat";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import {
loginWithToken,
getControlTerminalList,
getContactList,
getGroupList,
} from "./api";
import { useUserStore } from "@/store/module/user";
import {
KfUserListData,
ContractData,
weChatGroup,
} from "@/pages/pc/ckbox/data";
import { WechatGroup } from "./api";
const { login2 } = useUserStore.getState();
//获取触客宝基础信息
export const chatInitAPIdata = async () => {
try {
//获取联系人列表
const contractList = await getAllContactList();
//获取联系人列表
asyncContractList(contractList);
//获取群列表
const groupList = await getAllGroupList();
await asyncWeChatGroup(groupList);
// 提取不重复的wechatAccountId组
const uniqueWechatAccountIds: number[] = getUniqueWechatAccountIds(
contractList,
groupList,
);
//获取控制终端列表
const kfUserList: KfUserListData[] =
await getControlTerminalListByWechatAccountIds(uniqueWechatAccountIds);
//获取用户列表
await asyncKfUserList(kfUserList);
//获取标签列表
const countLables = await getCountLables();
await asyncCountLables(countLables);
//获取消息会话列表并按lastUpdateTime排序
const filterUserSessions = contractList?.filter(
v => v?.config && v.config?.chat,
);
const filterGroupSessions = groupList?.filter(
v => v?.config && v.config?.chat,
);
//排序功能
const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
(a, b) => {
// 如果lastUpdateTime不存在则将其排在最后
if (!a.lastUpdateTime) return 1;
if (!b.lastUpdateTime) return -1;
// 首先按时间降序排列(最新的在前面)
const timeCompare =
new Date(b.lastUpdateTime).getTime() -
new Date(a.lastUpdateTime).getTime();
// 如果时间相同,则按未读消息数量降序排列
if (timeCompare === 0) {
// 如果unreadCount不存在则将其排在后面
const aUnread = a.unreadCount || 0;
const bUnread = b.unreadCount || 0;
return bUnread - aUnread; // 未读消息多的排在前面
}
return timeCompare;
},
);
//会话数据同步
asyncChatSessions(sortedSessions);
return {
contractList,
groupList,
kfUserList,
};
} catch (error) {
console.error("获取联系人列表失败:", error);
return [];
}
};
//发起soket连接
export const initSocket = () => {
// 检查WebSocket是否已经连接
const { status } = useWebSocketStore.getState();
// 如果已经连接或正在连接,则不重复连接
if (["connected", "connecting"].includes(status)) {
console.log("WebSocket已连接或正在连接跳过重复连接", { status });
return;
}
// 从store获取token和accountId
const { token2 } = useUserStore.getState();
const { getAccountId } = useCkChatStore.getState();
const Token = token2;
const accountId = getAccountId();
// 使用WebSocket store初始化连接
const { connect } = useWebSocketStore.getState();
// 连接WebSocket
connect({
accessToken: Token,
accountId: Number(accountId),
client: "kefu-client",
cmdType: "CmdSignIn",
seq: +new Date(),
});
};
export const getCountLables = async () => {
const LablesRes = await Promise.all(
[1, 2].map(item =>
WechatGroup({
groupType: item,
}),
),
);
const [friend, group] = LablesRes;
const countLables = [
...[
{
id: 0,
groupName: "默认群分组",
groupType: 2,
},
],
...group,
...friend,
...[
{
id: 0,
groupName: "未分组",
groupType: 1,
},
],
];
return countLables;
};
/**
* 根据标签组织联系人
* @param contractList 联系人列表
* @param countLables 标签列表
* @returns 按标签分组的联系人
*/
//获取控制终端列表
export const getControlTerminalListByWechatAccountIds = (
WechatAccountIds: number[],
) => {
return Promise.all(
WechatAccountIds.map(id => getControlTerminalList({ id: id })),
);
};
// 递归获取所有联系人列表
export const getAllContactList = async () => {
try {
let allContacts = [];
let prevId = 0;
const count = 1000;
let hasMore = true;
while (hasMore) {
const contractList = await getContactList({
prevId,
count,
});
if (
!contractList ||
!Array.isArray(contractList) ||
contractList.length === 0
) {
hasMore = false;
break;
}
allContacts = [...allContacts, ...contractList];
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
if (contractList.length < count) {
hasMore = false;
} else {
// 获取最后一条数据的id作为下一次请求的prevId
const lastContact = contractList[contractList.length - 1];
prevId = lastContact.id;
}
}
return allContacts;
} catch (error) {
console.error("获取所有联系人列表失败:", error);
return [];
}
};
// 提取不重复的wechatAccountId组
export const getUniqueWechatAccountIds = (
contacts: ContractData[],
groupList: weChatGroup[],
) => {
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
return [];
}
// 使用Set来存储不重复的wechatAccountId
const uniqueAccountIdsSet = new Set<number>();
// 遍历联系人列表将每个wechatAccountId添加到Set中
contacts.forEach(contact => {
if (contact && contact.wechatAccountId) {
uniqueAccountIdsSet.add(contact.wechatAccountId);
}
});
// 遍历联系人列表将每个wechatAccountId添加到Set中
groupList.forEach(group => {
if (group && group.wechatAccountId) {
uniqueAccountIdsSet.add(group.wechatAccountId);
}
});
// 将Set转换为数组并返回
return Array.from(uniqueAccountIdsSet);
};
// 递归获取所有群列表
export const getAllGroupList = async () => {
try {
let allContacts = [];
let prevId = 0;
const count = 1000;
let hasMore = true;
while (hasMore) {
const contractList = await getGroupList({
prevId,
count,
});
if (
!contractList ||
!Array.isArray(contractList) ||
contractList.length === 0
) {
hasMore = false;
break;
}
allContacts = [...allContacts, ...contractList];
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
if (contractList.length < count) {
hasMore = false;
} else {
// 获取最后一条数据的id作为下一次请求的prevId
const lastContact = contractList[contractList.length - 1];
prevId = lastContact.id;
}
}
return allContacts;
} catch (error) {
console.error("获取所有群列表失败:", error);
return [];
}
};
//获取token
const getToken = () => {
return new Promise((resolve, reject) => {
const params = {
grant_type: "password",
password: "kr123456",
username: "kr_xf3",
// username: "karuo",
// password: "zhiqun1984",
};
loginWithToken(params)
.then(res => {
login2(res.access_token);
resolve(res.access_token);
})
.catch(err => {
reject(err);
});
});
};

View File

@@ -1,8 +1,6 @@
import React from "react";
import { lazy } from "react";
import type { RouteObject } from "react-router-dom";
const CkboxPage = lazy(() => import("@/pages/pc/ckbox"));
import CkboxPage from "@/pages/pc/ckbox";
import WeChatPage from "@/pages/pc/ckbox/weChat";
const ckboxRoutes: (RouteObject & { auth?: boolean; requiredRole?: string })[] =
[
@@ -11,6 +9,12 @@ const ckboxRoutes: (RouteObject & { auth?: boolean; requiredRole?: string })[] =
element: <CkboxPage />,
auth: true,
requiredRole: "user",
children: [
{
path: "weChat",
element: <WeChatPage />,
},
],
},
];