Merge branch 'yongpxu-dev' into yongpxu-dev2

This commit is contained in:
超级老白兔
2025-09-16 16:55:52 +08:00
89 changed files with 1512 additions and 11631 deletions

View File

@@ -1,4 +1,8 @@
import { BarChartOutlined, RobotOutlined } from "@ant-design/icons";
import {
BarChartOutlined,
RobotOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
// 菜单项接口
export interface MenuItem {
id: string;
@@ -31,7 +35,7 @@ export const drawerMenuData = {
appDesc: "AI智能营销系统",
},
footer: {
balanceIcon: "⚡",
balanceIcon: <ThunderboltOutlined size={20} />,
balanceLabel: "算力余额",
balanceValue: "9307.423",
},

View File

@@ -89,8 +89,7 @@
cursor: pointer;
.suanliIcon {
font-size: 20px;
color: #ffc107;
color: #52c41a;
}
&:hover {
@@ -330,7 +329,7 @@
color: #666;
.suanliIcon {
font-size: 20px;
color: #3d9c0d;
}
}

View File

@@ -7,10 +7,10 @@ import {
CloseOutlined,
LogoutOutlined,
UserSwitchOutlined,
ThunderboltOutlined,
} from "@ant-design/icons";
import { useUserStore } from "@/store/module/user";
// import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { useNavigate, useLocation } from "react-router-dom";
import { drawerMenuData, menuList } from "./index.data";
import styles from "./index.module.scss";
@@ -31,7 +31,6 @@ const NavCommon: React.FC<NavCommonProps> = ({
const [messageCount] = useState(3); // 模拟消息数量
const navigate = useNavigate();
const location = useLocation();
// const { userInfo } = useCkChatStore();
const { user, logout } = useUserStore();
// 处理菜单图标点击
@@ -163,9 +162,16 @@ const NavCommon: React.FC<NavCommonProps> = ({
<div className={styles.headerRight}>
<Space className={styles.userInfo}>
<span className={styles.suanli}>
<span className={styles.suanliIcon}></span>
<span className={styles.suanliIcon}>
<ThunderboltOutlined size={20} />
</span>
9307.423
</span>
<div className={styles.messageButton} onClick={handleMessageClick}>
<Badge count={messageCount} size="small">
<BellOutlined style={{ fontSize: 20 }} />
</Badge>
</div>
<Dropdown
menu={{ items: userMenuItems }}
placement="bottomRight"
@@ -180,11 +186,6 @@ const NavCommon: React.FC<NavCommonProps> = ({
/>
</div>
</Dropdown>
<div className={styles.messageButton} onClick={handleMessageClick}>
<Badge count={messageCount} size="small">
<BellOutlined style={{ fontSize: 20 }} />
</Badge>
</div>
</Space>
</div>
</Header>

View File

@@ -1,4 +1,5 @@
import request from "@/api/request2";
import request2 from "@/api/request2";
import request from "@/api/request";
import {
MessageData,
ChatHistoryResponse,
@@ -11,6 +12,26 @@ import {
ChatSettings,
} from "./data";
// 好友列表
export function getWechatFriendList(params) {
return request("/v1/kefu/wechatFriend/list", params, "GET");
}
// 群列表
export function getWechatChatroomList(params) {
return request("/v1/kefu/wechatChatroom/list", params, "GET");
}
//群、好友聊天记录列表
export function getMessageList() {
return request("/v1/kefu/message/list", {}, "GET");
}
//获取客服列表
export function getAgentList() {
return request("/v1/kefu/customerService/list", {}, "GET");
}
//读取聊天信息
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
function jsonToQueryString(json) {
@@ -30,7 +51,7 @@ export function WechatFriendAllot(params: {
notifyReceiver: boolean;
comment: string;
}) {
return request(
return request2(
"/api/wechatFriend/allot?" + jsonToQueryString(params),
undefined,
"PUT",
@@ -39,7 +60,7 @@ export function WechatFriendAllot(params: {
//获取可转移客服列表
export function getTransferableAgentList() {
return request("/api/account/myDepartmentAccountsForTransfer", {}, "GET");
return request2("/api/account/myDepartmentAccountsForTransfer", {}, "GET");
}
// 微信好友列表
@@ -47,7 +68,7 @@ export function WechatFriendRebackAllot(params: {
wechatFriendId?: number;
wechatChatroomId?: number;
}) {
return request(
return request2(
"/api/wechatFriend/rebackAllot?" + jsonToQueryString(params),
undefined,
"PUT",
@@ -56,17 +77,17 @@ export function WechatFriendRebackAllot(params: {
// 微信群列表
export function WechatGroup(params) {
return request("/api/WechatGroup/list", params, "GET");
return request2("/api/WechatGroup/list", params, "GET");
}
//获取聊天记录-1 清除未读
export function clearUnreadCount(params) {
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
return request2("/api/WechatFriend/clearUnreadCount", params, "PUT");
}
//更新配置
export function updateConfig(params) {
return request("/api/WechatFriend/updateConfig", params, "PUT");
return request2("/api/WechatFriend/updateConfig", params, "PUT");
}
//获取聊天记录-2 获取列表
export function getChatMessages(params: {
@@ -78,7 +99,7 @@ export function getChatMessages(params: {
Count: number;
olderData: boolean;
}) {
return request("/api/FriendMessage/SearchMessage", params, "GET");
return request2("/api/FriendMessage/SearchMessage", params, "GET");
}
export function getChatroomMessages(params: {
wechatAccountId: number;
@@ -89,12 +110,12 @@ export function getChatroomMessages(params: {
Count: number;
olderData: boolean;
}) {
return request("/api/ChatroomMessage/SearchMessage", params, "GET");
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
}
//获取群列表
export function getGroupList(params: { prevId: number; count: number }) {
return request(
return request2(
"/api/wechatChatroom/listExcludeMembersByPage?",
params,
"GET",
@@ -103,7 +124,7 @@ export function getGroupList(params: { prevId: number; count: number }) {
//获取群成员
export function getGroupMembers(params: { id: number }) {
return request(
return request2(
"/api/WechatChatroom/listMembersByWechatChatroomId",
params,
"GET",
@@ -112,7 +133,7 @@ export function getGroupMembers(params: { id: number }) {
//触客宝登陆
export function loginWithToken(params: any) {
return request(
return request2(
"/token",
params,
"POST",
@@ -127,17 +148,17 @@ export function loginWithToken(params: any) {
// 获取触客宝用户信息
export function getChuKeBaoUserInfo() {
return request("/api/account/self", {}, "GET");
return request2("/api/account/self", {}, "GET");
}
// 获取联系人列表
export const getContactList = (params: { prevId: number; count: number }) => {
return request("/api/wechatFriend/list", params, "GET");
return request2("/api/wechatFriend/list", params, "GET");
};
//获取控制终端列表
export const getControlTerminalList = params => {
return request("/api/wechataccount", params, "GET");
return request2("/api/wechataccount", params, "GET");
};
// 获取聊天历史
@@ -146,7 +167,7 @@ export const getChatHistory = (
page: number = 1,
pageSize: number = 50,
): Promise<ChatHistoryResponse> => {
return request(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
return request2(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
};
// 发送消息
@@ -155,7 +176,7 @@ export const sendMessage = (
content: string,
type: MessageType = MessageType.TEXT,
): Promise<MessageData> => {
return request(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
return request2(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
};
// 发送文件消息
@@ -167,17 +188,17 @@ export const sendFileMessage = (
const formData = new FormData();
formData.append("file", file);
formData.append("type", type);
return request(`/v1/chats/${chatId}/messages/file`, formData, "POST");
return request2(`/v1/chats/${chatId}/messages/file`, formData, "POST");
};
// 标记消息为已读
export const markMessageAsRead = (messageId: string): Promise<void> => {
return request(`/v1/messages/${messageId}/read`, {}, "PUT");
return request2(`/v1/messages/${messageId}/read`, {}, "PUT");
};
// 标记聊天为已读
export const markChatAsRead = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/read`, {}, "PUT");
return request2(`/v1/chats/${chatId}/read`, {}, "PUT");
};
// 添加群组成员
@@ -185,7 +206,7 @@ export const addGroupMembers = (
groupId: string,
memberIds: string[],
): Promise<void> => {
return request(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
return request2(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
};
// 移除群组成员
@@ -193,34 +214,34 @@ export const removeGroupMembers = (
groupId: string,
memberIds: string[],
): Promise<void> => {
return request(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
return request2(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
};
// 获取在线状态
export const getOnlineStatus = (userId: string): Promise<OnlineStatus> => {
return request(`/v1/users/${userId}/status`, {}, "GET");
return request2(`/v1/users/${userId}/status`, {}, "GET");
};
// 获取消息状态
export const getMessageStatus = (messageId: string): Promise<MessageStatus> => {
return request(`/v1/messages/${messageId}/status`, {}, "GET");
return request2(`/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");
return request2("/v1/upload", formData, "POST");
};
// 获取表情包列表
export const getEmojiList = (): Promise<EmojiData[]> => {
return request("/v1/emojis", {}, "GET");
return request2("/v1/emojis", {}, "GET");
};
// 获取快捷回复列表
export const getQuickReplies = (): Promise<QuickReply[]> => {
return request("/v1/quick-replies", {}, "GET");
return request2("/v1/quick-replies", {}, "GET");
};
// 添加快捷回复
@@ -228,49 +249,49 @@ export const addQuickReply = (data: {
content: string;
category: string;
}): Promise<QuickReply> => {
return request("/v1/quick-replies", data, "POST");
return request2("/v1/quick-replies", data, "POST");
};
// 删除快捷回复
export const deleteQuickReply = (id: string): Promise<void> => {
return request(`/v1/quick-replies/${id}`, {}, "DELETE");
return request2(`/v1/quick-replies/${id}`, {}, "DELETE");
};
// 获取聊天设置
export const getChatSettings = (): Promise<ChatSettings> => {
return request("/v1/chat/settings", {}, "GET");
return request2("/v1/chat/settings", {}, "GET");
};
// 更新聊天设置
export const updateChatSettings = (
settings: Partial<ChatSettings>,
): Promise<ChatSettings> => {
return request("/v1/chat/settings", settings, "PUT");
return request2("/v1/chat/settings", settings, "PUT");
};
// 删除聊天会话
export const deleteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}`, {}, "DELETE");
return request2(`/v1/chats/${chatId}`, {}, "DELETE");
};
// 置顶聊天会话
export const pinChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/pin`, {}, "PUT");
return request2(`/v1/chats/${chatId}/pin`, {}, "PUT");
};
// 取消置顶聊天会话
export const unpinChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/unpin`, {}, "PUT");
return request2(`/v1/chats/${chatId}/unpin`, {}, "PUT");
};
// 静音聊天会话
export const muteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/mute`, {}, "PUT");
return request2(`/v1/chats/${chatId}/mute`, {}, "PUT");
};
// 取消静音聊天会话
export const unmuteChatSession = (chatId: string): Promise<void> => {
return request(`/v1/chats/${chatId}/unmute`, {}, "PUT");
return request2(`/v1/chats/${chatId}/unmute`, {}, "PUT");
};
// 转发消息
@@ -278,10 +299,10 @@ export const forwardMessage = (
messageId: string,
targetChatIds: string[],
): Promise<void> => {
return request("/v1/messages/forward", { messageId, targetChatIds }, "POST");
return request2("/v1/messages/forward", { messageId, targetChatIds }, "POST");
};
// 撤回消息
export const recallMessage = (messageId: string): Promise<void> => {
return request(`/v1/messages/${messageId}/recall`, {}, "PUT");
return request2(`/v1/messages/${messageId}/recall`, {}, "PUT");
};

View File

@@ -113,6 +113,8 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
msgType = 3;
} else if ([FileType.AUDIO].includes(fileType)) {
msgType = 34;
} else if ([FileType.FILE].includes(fileType)) {
msgType = 49;
}
const params = {

View File

@@ -55,6 +55,7 @@
.messageContent {
flex-direction: row-reverse;
width: 70%;
}
.messageBubble {

View File

@@ -36,6 +36,26 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
);
};
// 提取content字符串中冒号后面的JSON部分
const extractJsonFromContent = (content: string): object | null => {
try {
// 查找第一个冒号的位置
const colonIndex = content.indexOf(":");
if (colonIndex === -1) {
return null;
}
// 提取冒号后面的部分并去除前后空格
const jsonStr = content.substring(colonIndex + 1).trim();
// 尝试解析JSON
return JSON.parse(jsonStr);
} catch (error) {
console.error("解析JSON失败:", error);
return null;
}
};
// 解析表情包文字格式[表情名称]并替换为img标签
const parseEmojiText = (text: string): React.ReactNode[] => {
const emojiRegex = /\[([^\]]+)\]/g;
@@ -155,6 +175,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
<div className={styles.messageText}>{fallbackText}</div>
);
// 添加调试信息
console.log("MessageRecord - msgType:", msgType, "content:", content);
// 根据msgType进行消息类型判断
switch (msgType) {
case 1: // 文本消息
@@ -193,6 +216,14 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
</div>
);
case 34: // 语音消息
if (typeof content !== "string" || !content.trim()) {
return renderErrorMessage("[语音消息 - 无效内容]");
}
// content直接是音频URL字符串
return <AudioMessage audioUrl={content} msgId={String(msg.id)} />;
case 43: // 视频消息
return (
<VideoMessage content={content || ""} msg={msg} contract={contract} />
@@ -225,14 +256,6 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
}
return renderErrorMessage("[表情包]");
case 34: // 语音消息
if (typeof content !== "string" || !content.trim()) {
return renderErrorMessage("[语音消息 - 无效内容]");
}
// content直接是音频URL字符串
return <AudioMessage audioUrl={content} msgId={String(msg.id)} />;
case 49: // 小程序/文章/其他:图文、文件
return <SmallProgramMessage content={content || ""} />;
@@ -577,7 +600,10 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
<div className={styles.messageTime}>{group.time}</div>
{group.messages
.filter(v => ![10000].includes(v.msgType))
.map(renderMessage)}
.map(msg => {
console.log("Rendering message with msgType:", msg.msgType);
return renderMessage(msg);
})}
</React.Fragment>
))}
<div ref={messagesEndRef} />

View File

@@ -0,0 +1,6 @@
import request2 from "@/api/request2";
import request from "@/api/request";
// 静音聊天会话
// export const muteChatSession = (chatId: string): Promise<void> => {
// return request2(`/v1/chats/${chatId}/mute`, {}, "PUT");
// };

View File

@@ -0,0 +1,248 @@
/* ===== 组件根容器 ===== */
.friendsCircle {
height: 100%;
overflow-y: auto;
padding: 0;
background-color: #f5f5f5;
/* 滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
/* ===== 折叠面板样式 ===== */
.collapseContainer {
margin-bottom: 1px;
:global(.ant-collapse-item) {
border-bottom: 1px solid #e8e8e8;
&:last-child {
border-bottom: 1px solid #e8e8e8;
}
}
:global(.ant-collapse-header) {
padding: 12px 16px !important;
background-color: #ffffff;
&:hover {
background-color: #f8f8f8;
}
}
:global(.ant-collapse-content-box) {
padding: 16px;
background-color: #ffffff;
}
/* 折叠面板头部 */
.collapseHeader {
display: flex;
align-items: center;
gap: 6px;
/* 特殊头像样式 */
.specialAvatar {
background-color: #1890ff;
}
/* 群组头像样式 */
.groupAvatars {
display: flex;
position: relative;
width: 32px;
height: 32px;
.groupAvatar {
position: absolute;
border: 1px solid #fff;
background-color: #52c41a;
&:nth-child(1) {
top: 0;
left: 0;
z-index: 4;
}
&:nth-child(2) {
top: 0;
right: 0;
z-index: 3;
}
&:nth-child(3) {
bottom: 0;
left: 0;
z-index: 2;
}
&:nth-child(4) {
bottom: 0;
right: 0;
z-index: 1;
}
}
}
/* 特殊文本样式 */
.specialText {
font-size: 16px;
color: #333;
font-weight: 400;
}
}
}
/* ===== 内容区域样式 ===== */
.myCircleContent,
.squareContent {
padding: 0;
/* 项目包装器 */
.itemWrapper {
margin-bottom: 1px;
&:last-child {
margin-bottom: 0;
}
/* ===== 朋友圈项目样式 ===== */
.circleItem {
background-color: #ffffff;
margin-bottom: 20px;
display: flex;
/* 头像样式 */
.avatar {
margin-right: 10px;
}
/* 项目头部 */
.itemHeader {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
/* 用户信息 */
.userInfo {
flex: 1;
.username {
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
}
}
/* 项目内容 */
.itemContent {
margin-bottom: 12px;
font-size: 12px;
.contentText {
color: #333;
line-height: 1.6;
margin-bottom: 8px;
word-wrap: break-word;
}
/* 图片容器 */
.imageContainer {
margin: 8px 0;
.contentImage {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
margin-bottom: 8px;
}
}
/* 蓝色链接 */
.blueLink {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
/* 项目底部 */
.itemFooter {
display: flex;
align-items: center;
justify-content: space-between;
/* 时间信息 */
.timeInfo {
font-size: 12px;
color: #999;
}
/* 操作按钮区域 */
.actions {
display: flex;
align-items: center;
gap: 8px;
.actionButton {
padding: 4px 8px;
color: #666;
&:hover {
color: #1890ff;
background-color: #f0f8ff;
}
.anticon {
font-size: 14px;
}
}
}
}
}
}
/* 空状态样式 */
.emptyText {
padding: 20px;
text-align: center;
color: #999;
font-size: 14px;
margin: 0;
}
/* 加载更多样式 */
.loadingMore {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
color: #666;
font-size: 14px;
gap: 8px;
.anticon {
margin-right: 4px;
}
}
}
}

View File

@@ -0,0 +1,384 @@
import React, { useState, useEffect } from "react";
import { Avatar, Button, Collapse, Spin } from "antd";
import {
HeartOutlined,
ChromeOutlined,
MessageOutlined,
LoadingOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import { InfiniteScroll } from "antd-mobile";
import styles from "./index.module.scss";
// 朋友圈数据类型定义
interface FriendsCircleItem {
id: string;
avatar: string;
username: string;
content: string;
images?: string[];
time: string;
likes?: number;
comments?: number;
}
// API响应类型
interface ApiResponse {
list: FriendsCircleItem[];
total: number;
hasMore: boolean;
}
// 分页参数类型
interface PaginationParams {
pageNum: number;
pageSize: number;
type: "my" | "square";
}
// 模拟朋友圈数据
const mockFriendsCircleData: FriendsCircleItem[] = [
{
id: "1",
avatar: "",
username: "我的朋友圈",
content: "",
time: "",
},
{
id: "2",
avatar: "",
username: "朋友圈广场",
content: "",
time: "",
},
{
id: "3",
avatar: "/public/assets/face/1.png",
username: "老坑爹-解放双手,释放时间",
content:
"🎉🎊🎈欢迎小伙伴加入单群聊客宝地盘思慕斯蛋糕的小伙伴们的支持与信任!!!",
images: ["/public/assets/face/1.png"],
time: "2025年9月16日 13:48",
likes: 0,
comments: 0,
},
{
id: "4",
avatar: "/public/assets/face/1.png",
username: "老坑爹-解放双手,释放时间",
content: "一整年卡1好的产品有用户的好评是买卖说的再多不如用户的有说服力",
images: ["/public/assets/face/1.png"],
time: "2025年9月16日 11:33",
likes: 0,
comments: 0,
},
{
id: "5",
avatar: "/public/assets/face/1.png",
username: "老坑爹-解放双手,释放时间",
content:
"两个小时637朵卡今天的努力也是给我自己最好的礼物🎁坚持就是胜利第二年开干。",
images: ["/public/assets/face/1.png"],
time: "2025年9月16日 11:03",
likes: 0,
comments: 0,
},
{
id: "6",
avatar: "/public/assets/face/1.png",
username: "老坑爹-解放双手,释放时间",
content: "老坑爹如果不幸苦,没有品质保障,客户会无限复购?",
images: ["/public/assets/face/1.png"],
time: "2025年9月16日 10:33",
likes: 0,
comments: 0,
},
];
const FriendsCircle: React.FC = () => {
// 状态管理
const [myCircleData, setMyCircleData] = useState<FriendsCircleItem[]>([]);
const [squareData, setSquareData] = useState<FriendsCircleItem[]>([]);
const [myCircleLoading, setMyCircleLoading] = useState(false);
const [squareLoading, setSquareLoading] = useState(false);
const [myCircleHasMore, setMyCircleHasMore] = useState(true);
const [squareHasMore, setSquareHasMore] = useState(true);
const [myCirclePage, setMyCirclePage] = useState(1);
const [squarePage, setSquarePage] = useState(1);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// 模拟API调用函数
const fetchFriendsCircleData = async (
params: PaginationParams,
): Promise<ApiResponse> => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
const { pageNum, pageSize, type } = params;
const startIndex = (pageNum - 1) * pageSize;
// 使用原有的模拟数据
const allData = mockFriendsCircleData.slice(2); // 排除前两个特殊项
const typeData = type === "my" ? allData.slice(0, 2) : allData.slice(2);
const paginatedData = typeData.slice(startIndex, startIndex + pageSize);
return {
list: paginatedData,
total: typeData.length,
hasMore: startIndex + pageSize < typeData.length,
};
};
// 加载我的朋友圈数据
const loadMyCircleData = async (
pageNum: number = 1,
reset: boolean = false,
) => {
setMyCircleLoading(true);
try {
const response = await fetchFriendsCircleData({
pageNum,
pageSize: 10,
type: "my",
});
if (reset) {
setMyCircleData(response.list);
} else {
setMyCircleData(prev => [...prev, ...response.list]);
}
setMyCircleHasMore(response.hasMore);
setMyCirclePage(pageNum);
} catch (error) {
console.error("加载我的朋友圈失败:", error);
} finally {
setMyCircleLoading(false);
}
};
// 加载朋友圈广场数据
const loadSquareData = async (
pageNum: number = 1,
reset: boolean = false,
) => {
setSquareLoading(true);
try {
const response = await fetchFriendsCircleData({
pageNum,
pageSize: 10,
type: "square",
});
if (reset) {
setSquareData(response.list);
} else {
setSquareData(prev => [...prev, ...response.list]);
}
setSquareHasMore(response.hasMore);
setSquarePage(pageNum);
} catch (error) {
console.error("加载朋友圈广场失败:", error);
} finally {
setSquareLoading(false);
}
};
// 加载更多我的朋友圈
const loadMoreMyCircle = async () => {
if (!myCircleHasMore || myCircleLoading) return;
await loadMyCircleData(myCirclePage + 1);
};
// 加载更多朋友圈广场
const loadMoreSquare = async () => {
if (!squareHasMore || squareLoading) return;
await loadSquareData(squarePage + 1);
};
// 处理折叠面板展开/收起
const handleCollapseChange = (keys: string | string[]) => {
const keyArray = Array.isArray(keys) ? keys : [keys];
setExpandedKeys(keyArray);
// 当展开时加载数据
keyArray.forEach(key => {
if (key === "1" && myCircleData.length === 0) {
loadMyCircleData(1, true);
}
if (key === "2" && squareData.length === 0) {
loadSquareData(1, true);
}
});
};
const handleLike = (id: string) => {
console.log("点赞:", id);
};
const handleComment = (id: string) => {
console.log("评论:", id);
};
const renderNormalItem = (item: FriendsCircleItem, isNotMy?: boolean) => {
return (
<div className={styles.circleItem}>
{isNotMy && (
<div className={styles.avatar}>
<Avatar size={36} shape="square" src={item.avatar} />
</div>
)}
<div className={styles.itemWrap}>
<div className={styles.itemHeader}>
<div className={styles.userInfo}>
<div className={styles.username}>{item.username}</div>
</div>
</div>
<div className={styles.itemContent}>
<div className={styles.contentText}>{item.content}</div>
{item.images && item.images.length > 0 && (
<div className={styles.imageContainer}>
{item.images.map((image, index) => (
<img
key={index}
src={image}
className={styles.contentImage}
/>
))}
</div>
)}
<div className={styles.blueLink}></div>
</div>
<div className={styles.itemFooter}>
<div className={styles.timeInfo}>{item.time}</div>
<div className={styles.actions}>
<Button
type="text"
size="small"
icon={<HeartOutlined />}
onClick={() => handleLike(item.id)}
className={styles.actionButton}
/>
<Button
type="text"
size="small"
icon={<MessageOutlined />}
onClick={() => handleComment(item.id)}
className={styles.actionButton}
/>
</div>
</div>
</div>
</div>
);
};
const renderMyFriendsCircle = () => {
return (
<div className={styles.myCircleContent}>
{myCircleData.length > 0 ? (
<>
{myCircleData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item, false)}
</div>
))}
<InfiniteScroll
loadMore={loadMoreMyCircle}
hasMore={myCircleHasMore}
threshold={10}
>
{myCircleLoading && (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
)}
</InfiniteScroll>
</>
) : myCircleLoading ? (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
) : (
<p className={styles.emptyText}></p>
)}
</div>
);
};
const renderFriendsSquare = () => {
return (
<div className={styles.squareContent}>
{squareData.length > 0 ? (
<>
{squareData.map(item => (
<div key={item.id} className={styles.itemWrapper}>
{renderNormalItem(item, true)}
</div>
))}
<InfiniteScroll
loadMore={loadMoreSquare}
hasMore={squareHasMore}
threshold={10}
>
{squareLoading && (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
)}
</InfiniteScroll>
</>
) : squareLoading ? (
<div className={styles.loadingMore}>
<Spin indicator={<LoadingOutlined spin />} /> ...
</div>
) : (
<p className={styles.emptyText}>广</p>
)}
</div>
);
};
const collapseItems = [
{
key: "1",
label: (
<div className={styles.collapseHeader}>
<ChromeOutlined style={{ fontSize: 20 }} />
<span className={styles.specialText}></span>
</div>
),
children: renderMyFriendsCircle(),
},
{
key: "2",
label: (
<div className={styles.collapseHeader}>
<AppstoreOutlined style={{ fontSize: 20 }} />
<span className={styles.specialText}>广</span>
</div>
),
children: renderFriendsSquare(),
},
];
return (
<div className={styles.friendsCircle}>
{/* 可折叠的特殊模块,包含所有朋友圈数据 */}
<Collapse
items={collapseItems}
className={styles.collapseContainer}
ghost
activeKey={expandedKeys}
onChange={handleCollapseChange}
/>
</div>
);
};
export default FriendsCircle;

View File

@@ -1,13 +1,9 @@
import React, { useState } from "react";
import { Input, Skeleton } from "antd";
import {
SearchOutlined,
UserOutlined,
ChromeOutlined,
MessageOutlined,
} from "@ant-design/icons";
import { SearchOutlined, ChromeOutlined } from "@ant-design/icons";
import WechatFriends from "./WechatFriends";
import MessageList from "./MessageList/index";
import FriendsCircle from "./FriendsCicle";
import styles from "./SidebarMenu.module.scss";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
interface SidebarMenuProps {
@@ -18,6 +14,7 @@ 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);
const kfSelected = useCkChatStore(state => state.kfSelected);
const [activeTab, setActiveTab] = useState("chats");
@@ -97,23 +94,22 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
className={`${styles.tabItem} ${activeTab === "chats" ? styles.active : ""}`}
onClick={() => setActiveTab("chats")}
>
<MessageOutlined />
<span></span>
</div>
<div
className={`${styles.tabItem} ${activeTab === "contracts" ? styles.active : ""}`}
onClick={() => setActiveTab("contracts")}
>
<UserOutlined />
<span></span>
</div>
<div
className={`${styles.tabItem} ${activeTab === "groups" ? styles.active : ""}`}
onClick={() => setActiveTab("groups")}
>
<ChromeOutlined />
<span></span>
</div>
{kfSelected != 0 && (
<div
className={`${styles.tabItem} ${activeTab === "friendsCicle" ? styles.active : ""}`}
onClick={() => setActiveTab("friendsCicle")}
>
<span></span>
</div>
)}
</div>
</div>
);
@@ -125,13 +121,8 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
return <MessageList />;
case "contracts":
return <WechatFriends />;
case "groups":
return (
<div className={styles.emptyState}>
<ChromeOutlined style={{ fontSize: 48, color: "#ccc" }} />
<p></p>
</div>
);
case "friendsCicle":
return <FriendsCircle />;
default:
return null;
}

View File

@@ -9,6 +9,7 @@
text-align: center;
cursor: pointer;
.allFriends {
padding-top: 15px;
font-size: 12px;
color: #333333;
}
@@ -48,6 +49,16 @@
}
}
}
.allUser {
width: 50px;
height: 50px;
border-radius: 50%;
border: 6px #1890ff solid;
display: flex;
align-items: center;
color: #333;
justify-content: center;
}
}
.messageBadge {

View File

@@ -1,61 +1,68 @@
import React from "react";
import { Avatar, Badge, Tooltip } from "antd";
import { Avatar, Badge } from "antd";
import styles from "./VerticalUserList.module.scss";
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat/ckchat";
import { TeamOutlined } from "@ant-design/icons";
const VerticalUserList: React.FC = () => {
// 格式化消息数量显示
const formatMessageCount = (count: number) => {
if (count > 99) return "99+";
return count.toString();
};
const handleUserSelect = (userId: number) => {
asyncKfSelected(userId);
};
const kfUserList = useCkChatStore(state => state.kfUserList);
const kfSelected = useCkChatStore(state => state.kfSelected);
const chatSessions = useCkChatStore(state => state.chatSessions);
const getUnreadCount = (wechatAccountId: number) => {
if (wechatAccountId != 0) {
const session = chatSessions.filter(
v => v.wechatAccountId === wechatAccountId,
);
return session.reduce((pre, cur) => pre + cur.unreadCount, 0);
} else {
return chatSessions.reduce((pre, cur) => pre + cur.unreadCount, 0);
}
};
return (
<div className={styles.verticalUserList}>
<div
className={styles.userListHeader}
onClick={() => handleUserSelect(0)}
>
<TeamOutlined style={{ fontSize: "26px" }} />
<div className={styles.allFriends}></div>
<div className={styles.userListHeader}>
<div className={styles.allFriends}></div>
</div>
<div className={styles.userList}>
<div className={styles.userItem} onClick={() => handleUserSelect(0)}>
<Badge
count={getUnreadCount(0)}
overflowCount={99}
className={styles.messageBadge}
>
<div className={styles.allUser}></div>
</Badge>
<div className={`${styles.onlineIndicator} ${styles.online}`} />
</div>
{kfUserList.map(user => (
<Tooltip key={user.id} title={user.name} placement="right">
<div
className={`${styles.userItem} ${kfSelected === user.id ? styles.active : ""}`}
onClick={() => handleUserSelect(user.id)}
<div
key={user.id}
className={`${styles.userItem} ${kfSelected === user.id ? styles.active : ""}`}
onClick={() => handleUserSelect(user.id)}
>
<Badge
count={getUnreadCount(user.id)}
overflowCount={99}
className={styles.messageBadge}
>
<Badge
count={
user.messageCount ? formatMessageCount(user.messageCount) : 0
}
overflowCount={99}
className={styles.messageBadge}
<Avatar
src={user.avatar}
size={50}
className={styles.userAvatar}
style={{
backgroundColor: !user.avatar ? "#1890ff" : undefined,
}}
>
<Avatar
src={user.avatar}
size={50}
className={styles.userAvatar}
style={{
backgroundColor: !user.avatar ? "#1890ff" : undefined,
}}
>
{!user.avatar && user.name.charAt(0)}
</Avatar>
</Badge>
<div
className={`${styles.onlineIndicator} ${user.isOnline ? styles.online : styles.offline}`}
/>
</div>
</Tooltip>
{!user.avatar && user.name.charAt(0)}
</Avatar>
</Badge>
<div
className={`${styles.onlineIndicator} ${user.isOnline ? styles.online : styles.offline}`}
/>
</div>
))}
</div>
</div>

View File

@@ -22,21 +22,21 @@ const CkboxPage: React.FC = () => {
setLoading(true);
chatInitAPIdata()
.then(response => {
const data = response as {
contractList: any[];
groupList: any[];
kfUserList: KfUserListData[];
newContractList: { groupName: string; contacts: any[] }[];
};
const { contractList } = data;
// 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);
});
// //找出已经在聊天的
// const isChatList = contractList.filter(
// v => (v?.config && v.config?.chat) || false,
// );
// isChatList.forEach(v => {
// addChatSession(v);
// });
// 数据加载完成后初始化WebSocket连接
initSocket();

View File

@@ -13,6 +13,7 @@ import {
getControlTerminalList,
getContactList,
getGroupList,
getMessageList,
} from "./api";
import { useUserStore } from "@/store/module/user";
@@ -106,7 +107,7 @@ export const initSocket = () => {
// 如果已经连接或正在连接,则不重复连接
if (["connected", "connecting"].includes(status)) {
// console.log("WebSocket已连接或正在连接跳过重复连接", { status });
console.log("WebSocket已连接或正在连接跳过重复连接", { status });
return;
}

View File

@@ -23,6 +23,14 @@ const messageHandlers: Record<string, MessageHandler> = {
kfUserList.forEach(kfUser => {
kfUser.isOnline = wechatAccountsAliveStatus[kfUser.id];
});
// 按在线状态排序,在线的排在前面
kfUserList.sort((a, b) => {
if (a.isOnline && !b.isOnline) return -1;
if (!a.isOnline && b.isOnline) return 1;
return 0;
});
asyncKfUserList(kfUserList);
},
// 发送消息响应