Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>存客宝</title>
|
||||
<title>触客宝</title>
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from "@/api/request2";
|
||||
import request from "@/api/request";
|
||||
import request2 from "@/api/request2";
|
||||
import {
|
||||
MessageData,
|
||||
ChatHistoryResponse,
|
||||
@@ -11,21 +12,27 @@ import {
|
||||
ChatSettings,
|
||||
} from "./data";
|
||||
|
||||
//获取好友接待配置
|
||||
export function getFriendInjectConfig(params) {
|
||||
return request("/v1/kefu/ai/friend/get", params, "GET");
|
||||
}
|
||||
//读取聊天信息
|
||||
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
|
||||
|
||||
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");
|
||||
export function clearUnreadCount1(params) {
|
||||
return request("/v1/kefu/message/readMessage", params, "GET");
|
||||
}
|
||||
export function clearUnreadCount2(params) {
|
||||
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: {
|
||||
@@ -37,7 +44,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;
|
||||
@@ -48,12 +55,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",
|
||||
@@ -62,7 +69,7 @@ export function getGroupList(params: { prevId: number; count: number }) {
|
||||
|
||||
//获取群成员
|
||||
export function getGroupMembers(params: { id: number }) {
|
||||
return request(
|
||||
return request2(
|
||||
"/api/WechatChatroom/listMembersByWechatChatroomId",
|
||||
params,
|
||||
"GET",
|
||||
@@ -71,7 +78,7 @@ export function getGroupMembers(params: { id: number }) {
|
||||
|
||||
//触客宝登陆
|
||||
export function loginWithToken(params: any) {
|
||||
return request(
|
||||
return request2(
|
||||
"/token",
|
||||
params,
|
||||
"POST",
|
||||
@@ -86,17 +93,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");
|
||||
};
|
||||
|
||||
// 获取聊天历史
|
||||
@@ -105,7 +112,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");
|
||||
};
|
||||
|
||||
// 发送消息
|
||||
@@ -114,7 +121,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");
|
||||
};
|
||||
|
||||
// 发送文件消息
|
||||
@@ -126,17 +133,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");
|
||||
};
|
||||
|
||||
// 添加群组成员
|
||||
@@ -144,7 +151,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");
|
||||
};
|
||||
|
||||
// 移除群组成员
|
||||
@@ -152,34 +159,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");
|
||||
};
|
||||
|
||||
// 添加快捷回复
|
||||
@@ -187,49 +194,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");
|
||||
};
|
||||
|
||||
// 转发消息
|
||||
@@ -237,10 +244,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");
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LogoutOutlined,
|
||||
UserSwitchOutlined,
|
||||
ThunderboltOutlined,
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
@@ -22,10 +23,7 @@ interface NavCommonProps {
|
||||
onMenuClick?: () => void;
|
||||
}
|
||||
|
||||
const NavCommon: React.FC<NavCommonProps> = ({
|
||||
title = "触客宝",
|
||||
onMenuClick,
|
||||
}) => {
|
||||
const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
|
||||
const [messageCount] = useState(3); // 模拟消息数量
|
||||
@@ -33,10 +31,14 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
||||
const location = useLocation();
|
||||
const { user, logout } = useUserStore();
|
||||
|
||||
// 处理菜单图标点击
|
||||
// 处理菜单图标点击:在两个路由之间切换
|
||||
const handleMenuClick = () => {
|
||||
setDrawerVisible(true);
|
||||
onMenuClick?.();
|
||||
const current = location.pathname;
|
||||
if (current.startsWith("/pc/weChat")) {
|
||||
navigate("/pc/powerCenter");
|
||||
} else {
|
||||
navigate("/pc/weChat");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理抽屉关闭
|
||||
@@ -165,13 +167,16 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
||||
<span className={styles.suanliIcon}>
|
||||
<ThunderboltOutlined size={20} />
|
||||
</span>
|
||||
9307.423
|
||||
{user?.tokens}
|
||||
</span>
|
||||
<div className={styles.messageButton} onClick={handleMessageClick}>
|
||||
<Badge count={messageCount} size="small">
|
||||
<BellOutlined style={{ fontSize: 20 }} />
|
||||
</Badge>
|
||||
</div>
|
||||
<div className={styles.messageButton}>
|
||||
<SettingOutlined style={{ fontSize: 20 }} />
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{ items: userMenuItems }}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -14,10 +14,10 @@ export interface MessageListData {
|
||||
avatar?: string; // 头像
|
||||
groupId: number; // 分组ID
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number; // 未读消息数
|
||||
}; // 配置信息
|
||||
labels?: string[]; // 标签列表
|
||||
unreadCount: number; // 未读消息数
|
||||
|
||||
// 联系人特有字段(当dataType为'contracts'时使用)
|
||||
wechatId?: string; // 微信ID
|
||||
@@ -113,10 +113,10 @@ export interface weChatGroup {
|
||||
chatroomAvatar: string;
|
||||
groupId: number;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number;
|
||||
};
|
||||
labels?: string[];
|
||||
unreadCount: number;
|
||||
notice: string;
|
||||
selfDisplyName: string;
|
||||
wechatChatroomId: number;
|
||||
@@ -152,10 +152,11 @@ export interface ContractData {
|
||||
additionalPicture: string;
|
||||
desc: string;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number;
|
||||
};
|
||||
lastMessageTime: number;
|
||||
unreadCount: number;
|
||||
|
||||
duplicate: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -246,7 +247,9 @@ export interface ChatSession {
|
||||
avatar?: string;
|
||||
lastMessage: string;
|
||||
lastTime: string;
|
||||
unreadCount: number;
|
||||
config: {
|
||||
unreadCount: number;
|
||||
};
|
||||
online: boolean;
|
||||
members?: string[];
|
||||
pinned?: boolean;
|
||||
|
||||
@@ -11,16 +11,46 @@ import {
|
||||
QuickReply,
|
||||
ChatSettings,
|
||||
} from "./data";
|
||||
//流量池
|
||||
|
||||
//好友接待配置
|
||||
export function setFriendInjectConfig(params) {
|
||||
return request("/v1/kefu/ai/friend/set", params, "POST");
|
||||
}
|
||||
|
||||
export function getTrafficPoolList() {
|
||||
return request(
|
||||
"/v1/traffic/pool/getPackage",
|
||||
{
|
||||
page: 1,
|
||||
limit: 9999,
|
||||
},
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
// 好友列表
|
||||
export function getWechatFriendList(params) {
|
||||
export function getContactList(params) {
|
||||
return request("/v1/kefu/wechatFriend/list", params, "GET");
|
||||
}
|
||||
|
||||
// 群列表
|
||||
export function getWechatChatroomList(params) {
|
||||
export function getGroupList(params) {
|
||||
return request("/v1/kefu/wechatChatroom/list", params, "GET");
|
||||
}
|
||||
//==============-原接口=================
|
||||
// 获取联系人列表
|
||||
// export const getContactList = (params: { prevId: number; count: number }) => {
|
||||
// return request2("/api/wechatFriend/list", params, "GET");
|
||||
// };
|
||||
|
||||
// //获取群列表
|
||||
// export function getGroupList(params: { prevId: number; count: number }) {
|
||||
// return request2(
|
||||
// "/api/wechatChatroom/listExcludeMembersByPage?",
|
||||
// params,
|
||||
// "GET",
|
||||
// );
|
||||
// }
|
||||
|
||||
//群、好友聊天记录列表
|
||||
export function getMessageList() {
|
||||
@@ -33,7 +63,6 @@ export function getAgentList() {
|
||||
}
|
||||
|
||||
//读取聊天信息
|
||||
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
|
||||
function jsonToQueryString(json) {
|
||||
const params = new URLSearchParams();
|
||||
for (const key in json) {
|
||||
@@ -80,11 +109,6 @@ export function WechatGroup(params) {
|
||||
return request2("/api/WechatGroup/list", params, "GET");
|
||||
}
|
||||
|
||||
//获取聊天记录-1 清除未读
|
||||
export function clearUnreadCount(params) {
|
||||
return request2("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
||||
}
|
||||
|
||||
//更新配置
|
||||
export function updateConfig(params) {
|
||||
return request2("/api/WechatFriend/updateConfig", params, "PUT");
|
||||
@@ -113,15 +137,6 @@ export function getChatroomMessages(params: {
|
||||
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||
}
|
||||
|
||||
//获取群列表
|
||||
export function getGroupList(params: { prevId: number; count: number }) {
|
||||
return request2(
|
||||
"/api/wechatChatroom/listExcludeMembersByPage?",
|
||||
params,
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
|
||||
//获取群成员
|
||||
export function getGroupMembers(params: { id: number }) {
|
||||
return request2(
|
||||
@@ -151,11 +166,6 @@ export function getChuKeBaoUserInfo() {
|
||||
return request2("/api/account/self", {}, "GET");
|
||||
}
|
||||
|
||||
// 获取联系人列表
|
||||
export const getContactList = (params: { prevId: number; count: number }) => {
|
||||
return request2("/api/wechatFriend/list", params, "GET");
|
||||
};
|
||||
|
||||
//获取控制终端列表
|
||||
export const getControlTerminalList = params => {
|
||||
return request2("/api/wechataccount", params, "GET");
|
||||
|
||||
@@ -48,7 +48,6 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim()) return;
|
||||
console.log("发送消息", contract);
|
||||
const params = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||
|
||||
@@ -3,241 +3,4 @@
|
||||
border-left: 1px solid #e8e8e8;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.profileContainer {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profileHeader {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.closeButton {
|
||||
color: #8c8c8c;
|
||||
|
||||
&:hover {
|
||||
color: #262626;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileBasic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.profileInfo {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
.profileNickname {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profileRemark {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.remarkText {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-head) {
|
||||
padding: 0 16px;
|
||||
min-height: 40px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:global(.ant-card-head-title) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: #8c8c8c;
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
|
||||
// 备注编辑区域样式
|
||||
:global(.ant-input) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:global(.ant-btn) {
|
||||
font-size: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tagsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
:global(.ant-tag) {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bioText {
|
||||
margin: 0;
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.groupManagement {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.groupMemberList {
|
||||
.groupMember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileActions {
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
|
||||
:global(.ant-btn) {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.profileSider {
|
||||
width: 280px !important;
|
||||
|
||||
.profileContainer {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.profileBasic {
|
||||
.profileInfo {
|
||||
.profileNickname {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.infoLabel {
|
||||
width: 50px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 朋友圈相关的API接口
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
|
||||
// 朋友圈请求参数接口
|
||||
export interface FetchMomentParams {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
createTimeSec?: number;
|
||||
prevSnsId?: number;
|
||||
count?: number;
|
||||
isTimeline?: boolean;
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
// 获取朋友圈数据
|
||||
export const fetchFriendsCircleData = async (params: FetchMomentParams) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
sendCommand("CmdFetchMoment", params);
|
||||
};
|
||||
|
||||
// 点赞朋友圈
|
||||
export const likeMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentInteract",
|
||||
momentInteractType: 1,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentInteract", requestData);
|
||||
};
|
||||
|
||||
// 取消点赞
|
||||
export const cancelLikeMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentCancelInteract",
|
||||
optType: 1,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
CommentId2: "",
|
||||
CommentTime: 0,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentCancelInteract", requestData);
|
||||
};
|
||||
|
||||
// 评论朋友圈
|
||||
export const commentMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
sendWord: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentInteract",
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
snsId: params.snsId,
|
||||
sendWord: params.sendWord,
|
||||
momentInteractType: 2,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentInteract", requestData);
|
||||
};
|
||||
|
||||
// 撤销评论
|
||||
export const cancelCommentMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
CommentTime: number;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentCancelInteract",
|
||||
optType: 2,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
CommentId2: "",
|
||||
CommentTime: params.CommentTime,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentCancelInteract", requestData);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
// 朋友圈相关的API接口
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
|
||||
// 朋友圈请求参数接口
|
||||
export interface FetchMomentParams {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
createTimeSec?: number;
|
||||
prevSnsId?: number;
|
||||
count?: number;
|
||||
isTimeline?: boolean;
|
||||
seq?: number;
|
||||
}
|
||||
|
||||
// 获取朋友圈数据
|
||||
export const fetchFriendsCircleData = async (params: FetchMomentParams) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
sendCommand("CmdFetchMoment", params);
|
||||
};
|
||||
|
||||
// 点赞朋友圈
|
||||
export const likeMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentInteract",
|
||||
momentInteractType: 1,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentInteract", requestData);
|
||||
};
|
||||
|
||||
// 取消点赞
|
||||
export const cancelLikeMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentCancelInteract",
|
||||
optType: 1,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
CommentId2: "",
|
||||
CommentTime: 0,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentCancelInteract", requestData);
|
||||
};
|
||||
|
||||
// 评论朋友圈
|
||||
export const commentMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
sendWord: string;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentInteract",
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
snsId: params.snsId,
|
||||
sendWord: params.sendWord,
|
||||
momentInteractType: 2,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentInteract", requestData);
|
||||
};
|
||||
|
||||
// 撤销评论
|
||||
export const cancelCommentMoment = async (params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
snsId: string;
|
||||
commentTime: number;
|
||||
commentId2: number;
|
||||
seq?: number;
|
||||
}) => {
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
const requestData = {
|
||||
cmdType: "CmdMomentCancelInteract",
|
||||
optType: 2,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId || 0,
|
||||
CommentId2: params.commentId2,
|
||||
CommentTime: params.commentTime,
|
||||
snsId: params.snsId,
|
||||
seq: params.seq || Date.now(),
|
||||
};
|
||||
|
||||
sendCommand("CmdMomentCancelInteract", requestData);
|
||||
};
|
||||
@@ -0,0 +1,320 @@
|
||||
import React, { useState } from "react";
|
||||
import { Avatar, Button, Image, Spin, Input, message } from "antd";
|
||||
import {
|
||||
HeartOutlined,
|
||||
MessageOutlined,
|
||||
LoadingOutlined,
|
||||
SendOutlined,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
CommentItem,
|
||||
likeListItem,
|
||||
FriendCardProps,
|
||||
MomentListProps,
|
||||
FriendsCircleItem,
|
||||
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
|
||||
import styles from "../index.module.scss";
|
||||
import {
|
||||
likeMoment,
|
||||
cancelLikeMoment,
|
||||
commentMoment,
|
||||
cancelCommentMoment,
|
||||
} from "./api";
|
||||
import { comfirm } from "@/utils/common";
|
||||
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
// 单个朋友圈项目组件
|
||||
export const FriendCard: React.FC<FriendCardProps> = ({
|
||||
monent,
|
||||
isNotMy = false,
|
||||
currentKf,
|
||||
wechatFriendId,
|
||||
formatTime,
|
||||
}) => {
|
||||
const content = monent?.momentEntity?.content || "";
|
||||
const images = monent?.momentEntity?.resUrls || [];
|
||||
const time = formatTime(monent.createTime);
|
||||
const likesCount = monent?.likeList?.length || 0;
|
||||
const commentsCount = monent?.commentList?.length || 0;
|
||||
const { updateLikeMoment, updateComment } = useWeChatStore();
|
||||
|
||||
// 评论相关状态
|
||||
const [showCommentInput, setShowCommentInput] = useState(false);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
|
||||
const handleLike = (moment: FriendsCircleItem) => {
|
||||
console.log(currentKf);
|
||||
|
||||
//判断是否已经点赞了
|
||||
const isLiked = moment?.likeList?.some(
|
||||
(item: likeListItem) => item.wechatId === currentKf?.wechatId,
|
||||
);
|
||||
if (isLiked) {
|
||||
cancelLikeMoment({
|
||||
wechatAccountId: currentKf?.id || 0,
|
||||
wechatFriendId: wechatFriendId || 0,
|
||||
snsId: moment.snsId,
|
||||
seq: Date.now(),
|
||||
});
|
||||
// 更新点赞
|
||||
updateLikeMoment(
|
||||
moment.snsId,
|
||||
moment.likeList.filter(v => v.wechatId !== currentKf?.wechatId),
|
||||
);
|
||||
} else {
|
||||
likeMoment({
|
||||
wechatAccountId: currentKf?.id || 0,
|
||||
wechatFriendId: wechatFriendId || 0,
|
||||
snsId: moment.snsId,
|
||||
seq: Date.now(),
|
||||
});
|
||||
// 更新点赞
|
||||
updateLikeMoment(moment.snsId, [
|
||||
...moment.likeList,
|
||||
{
|
||||
createTime: Date.now(),
|
||||
nickName: currentKf?.nickname || "",
|
||||
wechatId: currentKf?.wechatId || "",
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendComment = monent => {
|
||||
if (!commentText.trim()) {
|
||||
message.warning("请输入评论内容");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 调用发送评论的API
|
||||
commentMoment({
|
||||
wechatAccountId: currentKf?.id || 0,
|
||||
wechatFriendId: wechatFriendId || 0,
|
||||
snsId: monent.snsId,
|
||||
sendWord: commentText,
|
||||
seq: Date.now(),
|
||||
});
|
||||
// 更新评论
|
||||
updateComment(monent.snsId, [
|
||||
...monent.commentList,
|
||||
{
|
||||
commentArg: 0,
|
||||
commentId1: Date.now(),
|
||||
commentId2: Date.now(),
|
||||
commentTime: Date.now(),
|
||||
content: commentText,
|
||||
nickName: currentKf?.nickname || "",
|
||||
wechatId: currentKf?.wechatId || "",
|
||||
},
|
||||
]);
|
||||
// 清空输入框并隐藏
|
||||
setCommentText("");
|
||||
setShowCommentInput(false);
|
||||
message.success("评论发送成功");
|
||||
};
|
||||
|
||||
const handleDeleteComment = (snsId: string, comment: CommentItem) => {
|
||||
// TODO: 调用删除评论的API
|
||||
comfirm("确定删除评论吗?").then(() => {
|
||||
cancelCommentMoment({
|
||||
wechatAccountId: currentKf?.id || 0,
|
||||
wechatFriendId: wechatFriendId || 0,
|
||||
snsId: snsId,
|
||||
seq: Date.now(),
|
||||
commentId2: comment.commentId2,
|
||||
commentTime: comment.commentTime,
|
||||
});
|
||||
// 更新评论
|
||||
const commentList = monent.commentList.filter(v => {
|
||||
return !(
|
||||
v.commentId2 == comment.commentId2 &&
|
||||
v.wechatId == currentKf?.wechatId
|
||||
);
|
||||
});
|
||||
updateComment(snsId, commentList);
|
||||
message.success("评论删除成功");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.circleItem}>
|
||||
{isNotMy && (
|
||||
<div className={styles.avatar}>
|
||||
<Avatar size={36} shape="square" src="/public/assets/face/1.png" />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.itemWrap}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.userInfo}>
|
||||
{/* <div className={styles.username}>{nickName}</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemContent}>
|
||||
<div className={styles.contentText}>{content}</div>
|
||||
{images && images.length > 0 && (
|
||||
<div className={styles.imageContainer}>
|
||||
{images.map((image, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
src={image}
|
||||
className={styles.contentImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.itemFooter}>
|
||||
<div className={styles.timeInfo}>{time}</div>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<HeartOutlined />}
|
||||
onClick={() => handleLike(monent)}
|
||||
className={styles.actionButton}
|
||||
>
|
||||
{likesCount > 0 && <span>{likesCount}</span>}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MessageOutlined />}
|
||||
onClick={() => setShowCommentInput(!showCommentInput)}
|
||||
className={styles.actionButton}
|
||||
>
|
||||
{commentsCount > 0 && <span>{commentsCount}</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 点赞和评论区域 */}
|
||||
{(monent?.likeList?.length > 0 || monent?.commentList?.length > 0) && (
|
||||
<div className={styles.interactionArea}>
|
||||
{/* 点赞列表 */}
|
||||
{monent?.likeList?.length > 0 && (
|
||||
<div className={styles.likeArea}>
|
||||
<HeartOutlined className={styles.likeIcon} />
|
||||
<span className={styles.likeList}>
|
||||
{monent?.likeList?.map((like, index) => (
|
||||
<span key={`${like.wechatId}-${like.createTime}-${index}`}>
|
||||
{like.nickName}
|
||||
{index < (monent?.likeList?.length || 0) - 1 && "、"}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 评论列表 */}
|
||||
{monent?.commentList?.length > 0 && (
|
||||
<div className={styles.commentArea}>
|
||||
{monent?.commentList?.map(comment => (
|
||||
<div
|
||||
key={`${comment.wechatId}-${comment.commentTime}`}
|
||||
className={styles.commentItem}
|
||||
>
|
||||
<span className={styles.commentUser}>
|
||||
{comment.nickName}
|
||||
</span>
|
||||
<span className={styles.commentSeparator}>: </span>
|
||||
<span className={styles.commentContent}>
|
||||
{comment.content}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteComment(monent.snsId, comment)}
|
||||
className={styles.deleteCommentBtn}
|
||||
danger
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 评论输入框 */}
|
||||
{showCommentInput && (
|
||||
<div className={styles.commentInputArea}>
|
||||
<Input.TextArea
|
||||
value={commentText}
|
||||
onChange={e => setCommentText(e.target.value)}
|
||||
placeholder="写评论..."
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
className={styles.commentInput}
|
||||
/>
|
||||
<div className={styles.commentInputActions}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<SendOutlined />}
|
||||
onClick={() => handleSendComment(monent)}
|
||||
className={styles.sendCommentBtn}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 朋友圈列表组件
|
||||
export const MomentList: React.FC<MomentListProps> = ({
|
||||
MomentCommon,
|
||||
MomentCommonLoading,
|
||||
formatTime,
|
||||
currentKf,
|
||||
loadMomentData,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.myCircleContent}>
|
||||
{MomentCommon.length > 0 ? (
|
||||
<>
|
||||
{MomentCommon.map((v, index) => (
|
||||
<div
|
||||
key={`${v.snsId}-${v.createTime}-${index}`}
|
||||
className={styles.itemWrapper}
|
||||
>
|
||||
<FriendCard
|
||||
monent={v}
|
||||
isNotMy={false}
|
||||
formatTime={formatTime}
|
||||
currentKf={currentKf}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{MomentCommonLoading && (
|
||||
<div className={styles.loadingMore}>
|
||||
<Spin indicator={<LoadingOutlined spin />} /> 加载中...
|
||||
</div>
|
||||
)}
|
||||
{!MomentCommonLoading && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => loadMomentData(true)}
|
||||
>
|
||||
加载更多...
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : MomentCommonLoading ? (
|
||||
<div className={styles.loadingMore}>
|
||||
<Spin indicator={<LoadingOutlined spin />} /> 加载中...
|
||||
</div>
|
||||
) : (
|
||||
<p className={styles.emptyText}>暂无我的朋友圈内容</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
// 评论数据类型
|
||||
export interface CommentItem {
|
||||
commentArg: number;
|
||||
commentId1: number;
|
||||
commentId2: number;
|
||||
commentTime: number;
|
||||
content: string;
|
||||
nickName: string;
|
||||
wechatId: string;
|
||||
}
|
||||
|
||||
// 点赞数据类型
|
||||
export interface LikeItem {
|
||||
createTime: number;
|
||||
nickName: string;
|
||||
wechatId: string;
|
||||
}
|
||||
|
||||
// 朋友圈实体数据类型
|
||||
export interface MomentEntity {
|
||||
content: string;
|
||||
createTime: number;
|
||||
lat: number;
|
||||
lng: number;
|
||||
location: string;
|
||||
objectType: number;
|
||||
picSize: number;
|
||||
resUrls: string[];
|
||||
snsId: string;
|
||||
urls: string[];
|
||||
userName: string;
|
||||
}
|
||||
|
||||
// 朋友圈数据类型定义
|
||||
export interface FriendsCircleItem {
|
||||
commentList: CommentItem[];
|
||||
createTime: number;
|
||||
likeList: LikeItem[];
|
||||
momentEntity: MomentEntity;
|
||||
snsId: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse {
|
||||
list: FriendsCircleItem[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface FriendCardProps {
|
||||
monent: FriendsCircleItem;
|
||||
isNotMy?: boolean;
|
||||
currentKf?: any;
|
||||
wechatFriendId?: number;
|
||||
formatTime: (time: number) => string;
|
||||
}
|
||||
|
||||
export interface MomentListProps {
|
||||
MomentCommon: FriendsCircleItem[];
|
||||
MomentCommonLoading: boolean;
|
||||
currentKf?: any;
|
||||
wechatFriendId?: number;
|
||||
formatTime: (time: number) => string;
|
||||
loadMomentData: (loadMore: boolean) => void;
|
||||
}
|
||||
export interface likeListItem {
|
||||
createTime: number;
|
||||
nickName: string;
|
||||
wechatId: string;
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
/* ===== 组件根容器 ===== */
|
||||
.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;
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
/* 特殊头像样式 */
|
||||
.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;
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.contentImage {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
float: left;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 蓝色链接 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞和评论交互区域
|
||||
.interactionArea {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
.likeArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.likeIcon {
|
||||
color: #ff6b6b;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.likeList {
|
||||
color: #576b95;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentArea {
|
||||
.commentItem {
|
||||
margin-bottom: 2px;
|
||||
|
||||
.commentUser {
|
||||
color: #576b95;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.commentSeparator {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.commentContent {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deleteCommentBtn {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 评论输入框样式
|
||||
.commentInputArea {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
.commentInput {
|
||||
border: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.commentInputActions {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10px;
|
||||
.sendCommentBtn {
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 图片预览Modal样式 ===== */
|
||||
.imagePreviewModal {
|
||||
:global(.ant-modal-content) {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.ant-modal-close) {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1001;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.previewContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
|
||||
.previewImage {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.navButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
z-index: 1000;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
&.prevButton {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&.nextButton {
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.imageCounter {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Collapse } from "antd";
|
||||
import { ChromeOutlined } from "@ant-design/icons";
|
||||
import { MomentList } from "./components/friendCard";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import styles from "./index.module.scss";
|
||||
import { fetchFriendsCircleData } from "./api";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
|
||||
interface FriendsCircleProps {
|
||||
wechatFriendId?: number;
|
||||
}
|
||||
|
||||
const FriendsCircle: React.FC<FriendsCircleProps> = ({ wechatFriendId }) => {
|
||||
const currentKf = useCkChatStore(state =>
|
||||
state.kfUserList.find(kf => kf.id === state.kfSelected),
|
||||
);
|
||||
const { clearMomentCommon, updateMomentCommonLoading } = useWeChatStore();
|
||||
const MomentCommon = useWeChatStore(state => state.MomentCommon);
|
||||
const MomentCommonLoading = useWeChatStore(
|
||||
state => state.MomentCommonLoading,
|
||||
);
|
||||
|
||||
// 页面重新渲染时重置MomentCommonLoading状态
|
||||
useEffect(() => {
|
||||
updateMomentCommonLoading(false);
|
||||
}, []);
|
||||
|
||||
// 状态管理
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
// 加载更多我的朋友圈
|
||||
const loadMomentData = async (loadMore: boolean = false) => {
|
||||
updateMomentCommonLoading(true);
|
||||
// 加载数据;
|
||||
const requestData = {
|
||||
cmdType: "CmdFetchMoment",
|
||||
wechatAccountId: currentKf?.id || 0,
|
||||
wechatFriendId: wechatFriendId || 0,
|
||||
createTimeSec: Math.floor(dayjs().subtract(2, "month").valueOf() / 1000),
|
||||
prevSnsId: loadMore
|
||||
? Number(MomentCommon[MomentCommon.length - 1]?.snsId) || 0
|
||||
: 0,
|
||||
count: 10,
|
||||
isTimeline: expandedKeys.includes("1"),
|
||||
seq: Date.now(),
|
||||
};
|
||||
await fetchFriendsCircleData(requestData);
|
||||
};
|
||||
|
||||
// 处理折叠面板展开/收起
|
||||
const handleCollapseChange = (keys: string | string[]) => {
|
||||
const keyArray = Array.isArray(keys) ? keys : [keys];
|
||||
setExpandedKeys(keyArray);
|
||||
if (!MomentCommonLoading && keys.length > 0) {
|
||||
clearMomentCommon();
|
||||
loadMomentData(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间戳
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const collapseItems = [
|
||||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<div className={styles.collapseHeader}>
|
||||
<ChromeOutlined style={{ fontSize: 20 }} />
|
||||
<span className={styles.specialText}>好友朋友圈</span>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<MomentList
|
||||
currentKf={currentKf}
|
||||
MomentCommon={MomentCommon}
|
||||
MomentCommonLoading={MomentCommonLoading}
|
||||
loadMomentData={loadMomentData}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.friendsCircle}>
|
||||
{/* 可折叠的特殊模块,包含所有朋友圈数据 */}
|
||||
<Collapse
|
||||
items={collapseItems}
|
||||
className={styles.collapseContainer}
|
||||
ghost
|
||||
accordion
|
||||
activeKey={expandedKeys}
|
||||
onChange={handleCollapseChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FriendsCircle;
|
||||
@@ -0,0 +1,236 @@
|
||||
.profileContainer {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profileHeader {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.closeButton {
|
||||
color: #8c8c8c;
|
||||
|
||||
&:hover {
|
||||
color: #262626;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileBasic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.profileInfo {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
.profileNickname {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profileRemark {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.remarkText {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||
|
||||
:global(.ant-card-head) {
|
||||
padding: 0 16px;
|
||||
min-height: 40px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:global(.ant-card-head-title) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: #8c8c8c;
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
|
||||
// 备注编辑区域样式
|
||||
:global(.ant-input) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:global(.ant-btn) {
|
||||
font-size: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tagsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
:global(.ant-tag) {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bioText {
|
||||
margin: 0;
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.groupManagement {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.groupMemberList {
|
||||
.groupMember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileActions {
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
|
||||
:global(.ant-btn) {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.profileSider {
|
||||
width: 280px !important;
|
||||
|
||||
.profileContainer {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.profileBasic {
|
||||
.profileInfo {
|
||||
.profileNickname {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profileCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.infoLabel {
|
||||
width: 50px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Card, Input, Button, Space, List, Tag } from "antd";
|
||||
|
||||
export interface QuickWordItem {
|
||||
id: string | number;
|
||||
text?: string; // 兼容旧结构
|
||||
title?: string;
|
||||
content?: string;
|
||||
tag?: string; // 分类/标签
|
||||
usageCount?: number;
|
||||
}
|
||||
|
||||
export interface QuickWordsProps {
|
||||
title?: string;
|
||||
words: QuickWordItem[];
|
||||
onInsert?: (text: string) => void;
|
||||
onAdd?: (text: string) => void;
|
||||
onRemove?: (id: string | number) => void;
|
||||
}
|
||||
|
||||
const QuickWords: React.FC<QuickWordsProps> = ({
|
||||
title = "快捷语录",
|
||||
words,
|
||||
onInsert,
|
||||
|
||||
onRemove,
|
||||
}) => {
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
[...(words || [])].sort((a, b) =>
|
||||
String(a.id).localeCompare(String(b.id)),
|
||||
),
|
||||
[words],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={title} style={{ marginTop: 12 }}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Input.Search
|
||||
placeholder="搜索快捷语录..."
|
||||
allowClear
|
||||
value={keyword}
|
||||
onChange={e => setKeyword(e.target.value)}
|
||||
onSearch={v => setKeyword(v)}
|
||||
/>
|
||||
|
||||
<List
|
||||
itemLayout="vertical"
|
||||
split={false}
|
||||
dataSource={sorted.filter(item => {
|
||||
const text = `${item.title || ""}${item.content || ""}${item.text || ""}`;
|
||||
return text.toLowerCase().includes(keyword.trim().toLowerCase());
|
||||
})}
|
||||
renderItem={item => {
|
||||
const displayTitle = item.title || item.text || "未命名";
|
||||
const displayContent = item.content || item.text || "";
|
||||
return (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: "12px 8px",
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{item.tag && <Tag color="blue">{item.tag}</Tag>}
|
||||
<span style={{ fontWeight: 600, color: "#262626" }}>
|
||||
{displayTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
color: "#8c8c8c",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{displayContent}
|
||||
</div>
|
||||
{typeof item.usageCount === "number" && (
|
||||
<div
|
||||
style={{ color: "#bfbfbf", fontSize: 12, marginTop: 6 }}
|
||||
>
|
||||
使用 {item.usageCount} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||
>
|
||||
{onRemove && (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
onClick={() => onRemove(item.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => onInsert?.(displayContent || displayTitle)}
|
||||
>
|
||||
使用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickWords;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,669 +0,0 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd";
|
||||
import {
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
MoreOutlined,
|
||||
UserOutlined,
|
||||
DownloadOutlined,
|
||||
FileOutlined,
|
||||
FilePdfOutlined,
|
||||
FileWordOutlined,
|
||||
FileExcelOutlined,
|
||||
FilePptOutlined,
|
||||
PlayCircleFilled,
|
||||
TeamOutlined,
|
||||
FolderOutlined,
|
||||
EnvironmentOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./ChatWindow.module.scss";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
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;
|
||||
showProfile?: boolean;
|
||||
onToggleProfile?: () => void;
|
||||
}
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
contract,
|
||||
showProfile = true,
|
||||
onToggleProfile,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
||||
const prevMessagesRef = useRef(currentMessages);
|
||||
|
||||
useEffect(() => {
|
||||
const prevMessages = prevMessagesRef.current;
|
||||
|
||||
const hasVideoStateChange = currentMessages.some((msg, index) => {
|
||||
// 首先检查消息对象本身是否为null或undefined
|
||||
if (!msg || !msg.content) return false;
|
||||
|
||||
const prevMsg = prevMessages[index];
|
||||
if (!prevMsg || !prevMsg.content || prevMsg.id !== msg.id) return false;
|
||||
|
||||
try {
|
||||
const currentContent =
|
||||
typeof msg.content === "string"
|
||||
? JSON.parse(msg.content)
|
||||
: msg.content;
|
||||
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 (!hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 更新上一次的消息状态
|
||||
prevMessagesRef.current = currentMessages;
|
||||
}, [currentMessages]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// 处理视频播放请求,发送socket请求获取真实视频地址
|
||||
const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
|
||||
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: `${+new Date()}`, // 使用时间戳作为请求序列号
|
||||
tencentUrl: tencentUrl,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
});
|
||||
};
|
||||
|
||||
// 解析消息内容,判断消息类型并返回对应的渲染内容
|
||||
const parseMessageContent = (
|
||||
content: string | null | undefined,
|
||||
msg: ChatRecord,
|
||||
) => {
|
||||
// 处理null或undefined的内容
|
||||
if (content === null || content === undefined) {
|
||||
return <div className={styles.messageText}>消息内容不可用</div>;
|
||||
}
|
||||
// 检查是否为表情包
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") &&
|
||||
content.includes("#")
|
||||
) {
|
||||
return (
|
||||
<div className={styles.emojiMessage}>
|
||||
<img
|
||||
src={content}
|
||||
alt="表情包"
|
||||
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
||||
onClick={() => window.open(content, "_blank")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为带预览图的视频消息
|
||||
try {
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
content.trim().startsWith("{") &&
|
||||
content.trim().endsWith("}")
|
||||
) {
|
||||
const videoData = JSON.parse(content);
|
||||
// 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true}
|
||||
if (videoData.previewImage && videoData.tencentUrl) {
|
||||
// 提取预览图URL,去掉可能的引号
|
||||
const previewImageUrl = videoData.previewImage.replace(/[`"']/g, "");
|
||||
|
||||
// 创建点击处理函数
|
||||
const handlePlayClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// 如果没有视频URL且不在加载中,则发起下载请求
|
||||
if (!videoData.videoUrl && !videoData.isLoading) {
|
||||
handleVideoPlayRequest(videoData.tencentUrl, msg.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果已有视频URL,显示视频播放器
|
||||
if (videoData.videoUrl) {
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div className={styles.videoContainer}>
|
||||
<video
|
||||
controls
|
||||
src={videoData.videoUrl}
|
||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
||||
/>
|
||||
<a
|
||||
href={videoData.videoUrl}
|
||||
download
|
||||
className={styles.downloadButton}
|
||||
style={{ display: "flex" }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示预览图,根据加载状态显示不同的图标
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div className={styles.videoContainer} onClick={handlePlayClick}>
|
||||
<img
|
||||
src={previewImageUrl}
|
||||
alt="视频预览"
|
||||
className={styles.videoThumbnail}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
borderRadius: "8px",
|
||||
opacity: videoData.isLoading ? "0.7" : "1",
|
||||
}}
|
||||
/>
|
||||
<div className={styles.videoPlayIcon}>
|
||||
{videoData.isLoading ? (
|
||||
<div className={styles.loadingSpinner}></div>
|
||||
) : (
|
||||
<PlayCircleFilled
|
||||
style={{ fontSize: "48px", color: "#fff" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 保留原有的视频处理逻辑
|
||||
else if (
|
||||
videoData.type === "video" &&
|
||||
videoData.url &&
|
||||
videoData.thumb
|
||||
) {
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<div
|
||||
className={styles.videoContainer}
|
||||
onClick={() => window.open(videoData.url, "_blank")}
|
||||
>
|
||||
<img
|
||||
src={videoData.thumb}
|
||||
alt="视频预览"
|
||||
className={styles.videoThumbnail}
|
||||
/>
|
||||
<div className={styles.videoPlayIcon}>
|
||||
<VideoCameraOutlined
|
||||
style={{ fontSize: "32px", color: "#fff" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={videoData.url}
|
||||
download
|
||||
className={styles.downloadButton}
|
||||
style={{ display: "flex" }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析JSON失败,不是视频消息
|
||||
console.log("解析视频消息失败:", e);
|
||||
}
|
||||
|
||||
// 检查是否为图片链接
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
(content.match(/\.(jpg|jpeg|png|gif)$/i) ||
|
||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
||||
content.includes(".jpg")))
|
||||
) {
|
||||
return (
|
||||
<div className={styles.imageMessage}>
|
||||
<img
|
||||
src={content}
|
||||
alt="图片消息"
|
||||
onClick={() => window.open(content, "_blank")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为视频链接
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
(content.match(/\.(mp4|avi|mov|wmv|flv)$/i) ||
|
||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
||||
content.includes(".mp4")))
|
||||
) {
|
||||
return (
|
||||
<div className={styles.videoMessage}>
|
||||
<video
|
||||
controls
|
||||
src={content}
|
||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
||||
/>
|
||||
<a
|
||||
href={content}
|
||||
download
|
||||
className={styles.downloadButton}
|
||||
style={{ display: "flex" }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为音频链接
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
(content.match(/\.(mp3|wav|ogg|m4a)$/i) ||
|
||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
||||
content.includes(".mp3")))
|
||||
) {
|
||||
return (
|
||||
<div className={styles.audioMessage}>
|
||||
<audio controls src={content} style={{ maxWidth: "100%" }} />
|
||||
<a
|
||||
href={content}
|
||||
download
|
||||
className={styles.downloadButton}
|
||||
style={{ display: "flex" }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为Office文件链接
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
content.match(/\.(doc|docx|xls|xlsx|ppt|pptx|pdf)$/i)
|
||||
) {
|
||||
const fileName = content.split("/").pop() || "文件";
|
||||
const fileExt = fileName.split(".").pop()?.toLowerCase();
|
||||
|
||||
// 根据文件类型选择不同的图标
|
||||
let fileIcon = (
|
||||
<FileOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (fileExt === "pdf") {
|
||||
fileIcon = (
|
||||
<FilePdfOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
||||
fileIcon = (
|
||||
<FileWordOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#2f54eb" }}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
||||
fileIcon = (
|
||||
<FileExcelOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#52c41a" }}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
||||
fileIcon = (
|
||||
<FilePptOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#fa8c16" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.fileMessage}>
|
||||
{fileIcon}
|
||||
<div className={styles.fileInfo}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{fileName}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={content}
|
||||
download={fileExt !== "pdf" ? fileName : undefined}
|
||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
||||
className={styles.downloadButton}
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{ display: "flex" }}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否为文件消息(JSON格式)
|
||||
try {
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
content.trim().startsWith("{") &&
|
||||
content.trim().endsWith("}")
|
||||
) {
|
||||
const fileData = JSON.parse(content);
|
||||
if (fileData.type === "file" && fileData.title) {
|
||||
// 检查是否为Office文件
|
||||
const fileExt = fileData.title.split(".").pop()?.toLowerCase();
|
||||
let fileIcon = (
|
||||
<FolderOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (fileExt === "pdf") {
|
||||
fileIcon = (
|
||||
<FilePdfOutlined
|
||||
style={{
|
||||
fontSize: "24px",
|
||||
marginRight: "8px",
|
||||
color: "#ff4d4f",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
||||
fileIcon = (
|
||||
<FileWordOutlined
|
||||
style={{
|
||||
fontSize: "24px",
|
||||
marginRight: "8px",
|
||||
color: "#2f54eb",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
||||
fileIcon = (
|
||||
<FileExcelOutlined
|
||||
style={{
|
||||
fontSize: "24px",
|
||||
marginRight: "8px",
|
||||
color: "#52c41a",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
||||
fileIcon = (
|
||||
<FilePptOutlined
|
||||
style={{
|
||||
fontSize: "24px",
|
||||
marginRight: "8px",
|
||||
color: "#fa8c16",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.fileMessage}>
|
||||
{fileIcon}
|
||||
<div className={styles.fileInfo}>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{fileData.title}
|
||||
</div>
|
||||
{fileData.totalLen && (
|
||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
||||
{Math.round(fileData.totalLen / 1024)} KB
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={fileData.url || "#"}
|
||||
download={fileExt !== "pdf" ? fileData.title : undefined}
|
||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
||||
className={styles.downloadButton}
|
||||
style={{ display: "flex" }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!fileData.url) {
|
||||
console.log("文件URL不存在");
|
||||
}
|
||||
}}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析JSON失败,不是文件消息
|
||||
}
|
||||
|
||||
// 检查是否为位置信息
|
||||
if (
|
||||
typeof content === "string" &&
|
||||
(content.includes("<location") || content.includes("<msg><location"))
|
||||
) {
|
||||
// 提取位置信息
|
||||
const labelMatch = content.match(/label="([^"]*)"/i);
|
||||
const poiNameMatch = content.match(/poiname="([^"]*)"/i);
|
||||
const xMatch = content.match(/x="([^"]*)"/i);
|
||||
const yMatch = content.match(/y="([^"]*)"/i);
|
||||
|
||||
const label = labelMatch
|
||||
? labelMatch[1]
|
||||
: poiNameMatch
|
||||
? poiNameMatch[1]
|
||||
: "位置信息";
|
||||
const coordinates = xMatch && yMatch ? `${yMatch[1]}, ${xMatch[1]}` : "";
|
||||
|
||||
return (
|
||||
<div className={styles.locationMessage}>
|
||||
<EnvironmentOutlined
|
||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontWeight: "bold" }}>{label}</div>
|
||||
{coordinates && (
|
||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
||||
{coordinates}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认为文本消息
|
||||
return <div className={styles.messageText}>{content}</div>;
|
||||
};
|
||||
|
||||
// 用于分组消息并添加时间戳的辅助函数
|
||||
const groupMessagesByTime = (messages: ChatRecord[]) => {
|
||||
return messages
|
||||
.filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息
|
||||
.map(msg => ({
|
||||
time: formatWechatTime(msg?.wechatTime),
|
||||
messages: [msg],
|
||||
}));
|
||||
};
|
||||
|
||||
const renderMessage = (msg: ChatRecord) => {
|
||||
// 添加null检查,防止访问null对象的属性
|
||||
if (!msg) return null;
|
||||
|
||||
const isOwn = msg?.isSend;
|
||||
return (
|
||||
<div
|
||||
key={msg.id || `msg-${Date.now()}`}
|
||||
className={`${styles.messageItem} ${
|
||||
isOwn ? styles.ownMessage : styles.otherMessage
|
||||
}`}
|
||||
>
|
||||
<div className={styles.messageContent}>
|
||||
{!isOwn && (
|
||||
<Avatar
|
||||
size={32}
|
||||
src={contract.avatar}
|
||||
icon={<UserOutlined />}
|
||||
className={styles.messageAvatar}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.messageBubble}>
|
||||
{!isOwn && (
|
||||
<div className={styles.messageSender}>{msg?.senderName}</div>
|
||||
)}
|
||||
{parseMessageContent(msg?.content, msg)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const chatMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
||||
查看资料
|
||||
</Menu.Item>
|
||||
<Menu.Item key="call" icon={<PhoneOutlined />}>
|
||||
语音通话
|
||||
</Menu.Item>
|
||||
<Menu.Item key="video" icon={<VideoCameraOutlined />}>
|
||||
视频通话
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="pin">置顶聊天</Menu.Item>
|
||||
<Menu.Item key="mute">消息免打扰</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="clear" danger>
|
||||
清空聊天记录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout className={styles.chatWindow}>
|
||||
{/* 聊天主体区域 */}
|
||||
<Layout className={styles.chatMain}>
|
||||
{/* 聊天头部 */}
|
||||
<Header className={styles.chatHeader}>
|
||||
<div className={styles.chatHeaderInfo}>
|
||||
<Avatar
|
||||
size={40}
|
||||
src={contract.avatar || contract.chatroomAvatar}
|
||||
icon={
|
||||
contract.type === "group" ? <TeamOutlined /> : <UserOutlined />
|
||||
}
|
||||
/>
|
||||
<div className={styles.chatHeaderDetails}>
|
||||
<div className={styles.chatHeaderName}>
|
||||
{contract.nickname || contract.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Space>
|
||||
<Tooltip title="语音通话">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PhoneOutlined />}
|
||||
className={styles.headerButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="视频通话">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<VideoCameraOutlined />}
|
||||
className={styles.headerButton}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Dropdown overlay={chatMenu} trigger={["click"]}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MoreOutlined />}
|
||||
className={styles.headerButton}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
|
||||
{/* 聊天内容 */}
|
||||
<Content className={styles.chatContent}>
|
||||
<div className={styles.messagesContainer}>
|
||||
{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>
|
||||
|
||||
{/* 消息输入组件 */}
|
||||
<MessageEnter contract={contract} />
|
||||
</Layout>
|
||||
|
||||
{/* 右侧个人资料卡片 */}
|
||||
<ProfileCard
|
||||
contract={contract}
|
||||
showProfile={showProfile}
|
||||
onToggleProfile={onToggleProfile}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatWindow;
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Layout, Button, Avatar, Space, Tooltip, Dropdown } from "antd";
|
||||
import {
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
MoreOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
InfoCircleOutlined,
|
||||
RobotOutlined,
|
||||
DownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./ChatWindow.module.scss";
|
||||
@@ -14,7 +13,8 @@ import styles from "./ChatWindow.module.scss";
|
||||
import ProfileCard from "./components/ProfileCard";
|
||||
import MessageEnter from "./components/MessageEnter";
|
||||
import MessageRecord from "./components/MessageRecord";
|
||||
|
||||
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
interface ChatWindowProps {
|
||||
@@ -22,11 +22,50 @@ interface ChatWindowProps {
|
||||
}
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
||||
const updateAiQuoteMessageContent = useWeChatStore(
|
||||
state => state.updateAiQuoteMessageContent,
|
||||
);
|
||||
const aiQuoteMessageContent = useWeChatStore(
|
||||
state => state.aiQuoteMessageContent,
|
||||
);
|
||||
const [showProfile, setShowProfile] = useState(true);
|
||||
const onToggleProfile = () => {
|
||||
setShowProfile(!showProfile);
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 0, label: "人工接待" },
|
||||
{ value: 1, label: "AI辅助" },
|
||||
{ value: 2, label: "AI接管" },
|
||||
];
|
||||
|
||||
const [currentConfig, setCurrentConfig] = useState(
|
||||
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentConfig(
|
||||
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
||||
);
|
||||
}, [aiQuoteMessageContent]);
|
||||
|
||||
// 处理配置选择
|
||||
const handleConfigChange = option => {
|
||||
setCurrentConfig({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
});
|
||||
|
||||
// 保存配置到后端
|
||||
setFriendInjectConfig({
|
||||
type: option.value,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
friendId: contract.id,
|
||||
}).then(() => {
|
||||
updateAiQuoteMessageContent(option.value);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className={styles.chatWindow}>
|
||||
{/* 聊天主体区域 */}
|
||||
@@ -48,6 +87,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
||||
</div>
|
||||
</div>
|
||||
<Space>
|
||||
{!contract.chatroomId && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: typeOptions.map(option => ({
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
onClick: () => handleConfigChange(option),
|
||||
})),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button type="default" icon={<RobotOutlined />}>
|
||||
{currentConfig.label}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<Tooltip title="个人资料">
|
||||
<Button
|
||||
onClick={onToggleProfile}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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;
|
||||
@@ -24,9 +24,10 @@ export interface ContractData {
|
||||
thirdParty: null;
|
||||
additionalPicture: string;
|
||||
desc: string;
|
||||
config: null;
|
||||
lastMessageTime: number;
|
||||
unreadCount: number;
|
||||
config: {
|
||||
unreadCount: number;
|
||||
};
|
||||
duplicate: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -40,7 +41,9 @@ export interface ChatSession {
|
||||
avatar?: string;
|
||||
lastMessage: string;
|
||||
lastTime: string;
|
||||
unreadCount: number;
|
||||
config: {
|
||||
unreadCount: number;
|
||||
};
|
||||
online: boolean;
|
||||
members?: string[];
|
||||
pinned?: boolean;
|
||||
|
||||
@@ -79,7 +79,9 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
<div
|
||||
className={styles.lastMessage}
|
||||
data-count={
|
||||
session.unreadCount > 0 ? session.unreadCount : ""
|
||||
session.config.unreadCount > 0
|
||||
? session.config.unreadCount
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{session?.lastMessage}
|
||||
|
||||
@@ -15,9 +15,9 @@ const VerticalUserList: React.FC = () => {
|
||||
const session = chatSessions.filter(
|
||||
v => v.wechatAccountId === wechatAccountId,
|
||||
);
|
||||
return session.reduce((pre, cur) => pre + cur.unreadCount, 0);
|
||||
return session.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
|
||||
} else {
|
||||
return chatSessions.reduce((pre, cur) => pre + cur.unreadCount, 0);
|
||||
return chatSessions.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ export interface MessageListData {
|
||||
avatar?: string; // 头像
|
||||
groupId: number; // 分组ID
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number; // 未读消息数
|
||||
}; // 配置信息
|
||||
labels?: string[]; // 标签列表
|
||||
unreadCount: number; // 未读消息数
|
||||
|
||||
// 联系人特有字段(当dataType为'contracts'时使用)
|
||||
wechatId?: string; // 微信ID
|
||||
@@ -113,10 +113,11 @@ export interface weChatGroup {
|
||||
chatroomAvatar: string;
|
||||
groupId: number;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number;
|
||||
};
|
||||
labels?: string[];
|
||||
unreadCount: number;
|
||||
|
||||
notice: string;
|
||||
selfDisplyName: string;
|
||||
wechatChatroomId: number;
|
||||
@@ -152,10 +153,10 @@ export interface ContractData {
|
||||
additionalPicture: string;
|
||||
desc: string;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
chat?: boolean;
|
||||
unreadCount: number;
|
||||
};
|
||||
lastMessageTime: number;
|
||||
unreadCount: number;
|
||||
duplicate: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -246,7 +247,9 @@ export interface ChatSession {
|
||||
avatar?: string;
|
||||
lastMessage: string;
|
||||
lastTime: string;
|
||||
unreadCount: number;
|
||||
config: {
|
||||
unreadCount: number;
|
||||
};
|
||||
online: boolean;
|
||||
members?: string[];
|
||||
pinned?: boolean;
|
||||
|
||||
@@ -67,23 +67,24 @@ export const chatInitAPIdata = async () => {
|
||||
//排序功能
|
||||
const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
|
||||
(a, b) => {
|
||||
// 获取未读消息数量
|
||||
const aUnread = a.config?.unreadCount || 0;
|
||||
const bUnread = b.config?.unreadCount || 0;
|
||||
|
||||
// 首先按未读消息数量降序排列(未读消息多的排在前面)
|
||||
if (aUnread !== bUnread) {
|
||||
return bUnread - aUnread;
|
||||
}
|
||||
|
||||
// 如果未读消息数量相同,则按时间降序排列(最新的在前面)
|
||||
// 如果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;
|
||||
},
|
||||
);
|
||||
@@ -102,15 +103,6 @@ export const chatInitAPIdata = async () => {
|
||||
};
|
||||
//发起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();
|
||||
@@ -178,16 +170,16 @@ export const getControlTerminalListByWechatAccountIds = (
|
||||
export const getAllContactList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let prevId = 0;
|
||||
const count = 1000;
|
||||
let page = 1;
|
||||
const limit = 1000;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const contractList = await getContactList({
|
||||
prevId,
|
||||
count,
|
||||
const Result = await getContactList({
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
const contractList = Result.list;
|
||||
if (
|
||||
!contractList ||
|
||||
!Array.isArray(contractList) ||
|
||||
@@ -199,15 +191,11 @@ export const getAllContactList = async () => {
|
||||
|
||||
allContacts = [...allContacts, ...contractList];
|
||||
|
||||
// console.log(contractList.length == 0);
|
||||
|
||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||
if (contractList.length == 0) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 获取最后一条数据的id作为下一次请求的prevId
|
||||
const lastContact = contractList[contractList.length - 1];
|
||||
prevId = lastContact.id;
|
||||
page = page + 1;
|
||||
}
|
||||
}
|
||||
return allContacts;
|
||||
@@ -250,16 +238,16 @@ export const getUniqueWechatAccountIds = (
|
||||
export const getAllGroupList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let prevId = 0;
|
||||
const count = 1000;
|
||||
let page = 1;
|
||||
const limit = 1000;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const contractList = await getGroupList({
|
||||
prevId,
|
||||
count,
|
||||
const Result = await getGroupList({
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
const contractList = Result.list;
|
||||
if (
|
||||
!contractList ||
|
||||
!Array.isArray(contractList) ||
|
||||
@@ -272,12 +260,12 @@ export const getAllGroupList = async () => {
|
||||
allContacts = [...allContacts, ...contractList];
|
||||
|
||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||
if (contractList.length < count) {
|
||||
if (contractList.length < limit) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 获取最后一条数据的id作为下一次请求的prevId
|
||||
// 获取最后一条数据的id作为下一次请求的page
|
||||
const lastContact = contractList[contractList.length - 1];
|
||||
prevId = lastContact.id;
|
||||
page = lastContact.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface CkChatState {
|
||||
clearkfUserList: () => void;
|
||||
addChatSession: (session: any) => void;
|
||||
deleteChatSession: (sessionId: number) => void;
|
||||
pinChatSessionToTop: (sessionId: number) => void;
|
||||
setUserInfo: (userInfo: CkUserInfo) => void;
|
||||
clearUserInfo: () => void;
|
||||
updateAccount: (account: Partial<CkAccount>) => void;
|
||||
|
||||
@@ -375,11 +375,11 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
set(state => {
|
||||
// 检查是否已存在相同id的会话
|
||||
const exists = state.chatSessions.some(item => item.id === session.id);
|
||||
// 如果已存在则不添加,否则添加到列表中
|
||||
// 如果已存在则不添加,否则添加到列表顶部
|
||||
return {
|
||||
chatSessions: exists
|
||||
? state.chatSessions
|
||||
: [...state.chatSessions, session as ContractData | weChatGroup],
|
||||
: [session as ContractData | weChatGroup, ...state.chatSessions],
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -399,6 +399,24 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
//当前选中的客户清空
|
||||
getClearCurrentContact();
|
||||
},
|
||||
// 置顶聊天会话到列表顶部
|
||||
pinChatSessionToTop: (sessionId: number) => {
|
||||
set(state => {
|
||||
const sessionIndex = state.chatSessions.findIndex(
|
||||
item => item.id === sessionId,
|
||||
);
|
||||
if (sessionIndex === -1) return state; // 会话不存在
|
||||
|
||||
const session = state.chatSessions[sessionIndex];
|
||||
const otherSessions = state.chatSessions.filter(
|
||||
item => item.id !== sessionId,
|
||||
);
|
||||
|
||||
return {
|
||||
chatSessions: [session, ...otherSessions],
|
||||
};
|
||||
});
|
||||
},
|
||||
// 设置用户信息
|
||||
setUserInfo: (userInfo: CkUserInfo) => {
|
||||
set({ userInfo, isLoggedIn: true });
|
||||
@@ -524,4 +542,6 @@ export const clearSearchKeyword = () =>
|
||||
useCkChatStore.getState().clearSearchKeyword();
|
||||
export const searchContactsAndGroups = () =>
|
||||
useCkChatStore.getState().searchContactsAndGroups();
|
||||
export const pinChatSessionToTop = (sessionId: number) =>
|
||||
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
||||
useCkChatStore.getState().getKfSelectedUser();
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface User {
|
||||
s2_accountId: string;
|
||||
createTime: string;
|
||||
updateTime: string | null;
|
||||
tokens: number;
|
||||
lastLoginIp: string;
|
||||
lastLoginTime: number;
|
||||
}
|
||||
@@ -61,6 +62,7 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
s2_accountId: userInfo.s2_accountId,
|
||||
createTime: userInfo.createTime,
|
||||
updateTime: userInfo.updateTime,
|
||||
tokens: userInfo.tokens,
|
||||
lastLoginIp: userInfo.lastLoginIp,
|
||||
lastLoginTime: userInfo.lastLoginTime,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
* 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
|
||||
*/
|
||||
export interface WeChatState {
|
||||
aiQuoteMessageContent: number;
|
||||
updateAiQuoteMessageContent: (message: number) => void;
|
||||
quoteMessageContent: string;
|
||||
updateQuoteMessageContent: (value: string) => void;
|
||||
// ==================== Transmit Module =========Start===========
|
||||
|
||||
@@ -11,13 +11,19 @@ import {
|
||||
likeListItem,
|
||||
CommentItem,
|
||||
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
|
||||
import { clearUnreadCount, updateConfig } from "@/pages/pc/ckbox/api";
|
||||
import {
|
||||
clearUnreadCount1,
|
||||
clearUnreadCount2,
|
||||
updateConfig,
|
||||
getFriendInjectConfig,
|
||||
} from "@/pages/pc/ckbox/api";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { weChatGroupService, contractService } from "@/utils/db";
|
||||
import {
|
||||
addChatSession,
|
||||
updateChatSession,
|
||||
useCkChatStore,
|
||||
pinChatSessionToTop,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
|
||||
/**
|
||||
@@ -27,6 +33,11 @@ import {
|
||||
export const useWeChatStore = create<WeChatState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
//当前用户的ai接管状态
|
||||
aiQuoteMessageContent: 0,
|
||||
updateAiQuoteMessageContent: (message: number) => {
|
||||
set({ aiQuoteMessageContent: message });
|
||||
},
|
||||
quoteMessageContent: "",
|
||||
updateQuoteMessageContent: (message: string) => {
|
||||
set({ quoteMessageContent: message });
|
||||
@@ -140,19 +151,37 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
const state = useWeChatStore.getState();
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [], openTransmitModal: false });
|
||||
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(true, 4704624000000);
|
||||
|
||||
const params: any = {};
|
||||
|
||||
if (!contract.chatroomId) {
|
||||
params.wechatFriendId = contract.id;
|
||||
} else {
|
||||
params.wechatChatroomId = contract.id;
|
||||
}
|
||||
|
||||
clearUnreadCount1(params);
|
||||
clearUnreadCount2([contract.id]);
|
||||
getFriendInjectConfig({
|
||||
friendId: contract.id,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
}).then(result => {
|
||||
set({ aiQuoteMessageContent: result });
|
||||
});
|
||||
if (isExist) {
|
||||
updateChatSession({
|
||||
...contract,
|
||||
config: { unreadCount: 0 },
|
||||
});
|
||||
} else {
|
||||
addChatSession(contract);
|
||||
}
|
||||
set({ currentContract: contract });
|
||||
updateConfig({
|
||||
id: contract.id,
|
||||
config: { chat: true },
|
||||
});
|
||||
state.loadChatMessages(true, 4704624000000);
|
||||
},
|
||||
|
||||
// ==================== 消息加载方法 ====================
|
||||
@@ -285,6 +314,8 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
if (session) {
|
||||
session.unreadCount = Number(session.unreadCount) + 1;
|
||||
updateChatSession(session);
|
||||
// 将接收到新消息的会话置顶到列表顶部
|
||||
pinChatSessionToTop(getMessageId);
|
||||
} else {
|
||||
// 如果会话不存在,创建新会话
|
||||
if (isWechatGroup) {
|
||||
@@ -294,6 +325,7 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
...group,
|
||||
unreadCount: 1,
|
||||
});
|
||||
// 新创建的会话会自动添加到列表顶部,无需额外置顶
|
||||
}
|
||||
} else {
|
||||
const [user] = await contractService.findByIds(getMessageId);
|
||||
@@ -301,6 +333,7 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
...user,
|
||||
unreadCount: 1,
|
||||
});
|
||||
// 新创建的会话会自动添加到列表顶部,无需额外置顶
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
|
||||
set({
|
||||
messages: [...currentState.messages, newMessage],
|
||||
unreadCount: currentState.unreadCount + 1,
|
||||
unreadCount: currentState.config.unreadCount + 1,
|
||||
});
|
||||
//消息处理器
|
||||
msgManageCore(data);
|
||||
|
||||
@@ -64,12 +64,12 @@ class CunkebaoDatabase extends Dexie {
|
||||
kfUsers:
|
||||
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
||||
weChatGroup:
|
||||
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, unreadCount, notice, selfDisplyName",
|
||||
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName",
|
||||
contracts:
|
||||
"serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
|
||||
"serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate",
|
||||
newContractList: "serverId, id, groupName, contacts",
|
||||
messageList:
|
||||
"serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, unreadCount, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
|
||||
"serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user