feat(聊天窗口重構與個人資料卡片優化): 刪除冗餘的ChatWindow組件代碼,重構ProfileCard以支持選項卡功能,並整合快捷語錄及朋友圈模塊,提升用戶界面友好性和功能性。

This commit is contained in:
超级老白兔
2025-09-23 19:03:55 +08:00
parent fe82be5f21
commit 424fc518ff
13 changed files with 2932 additions and 2287 deletions

View File

@@ -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;
}
}
}
}
}

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}
}
}

View File

@@ -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;

View File

@@ -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;