Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop
This commit is contained in:
@@ -10,6 +10,13 @@ const { token } = useUserStore.getState();
|
||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||
const debounceMap = new Map<string, number>();
|
||||
|
||||
// 需要高频轮询、不走截流的接口白名单(按实际接口路径调整)
|
||||
const NO_DEBOUNCE_URLS = [
|
||||
"/wechat/friend/list", // 好友列表
|
||||
"/wechat/group/list", // 群组列表
|
||||
"/wechat/message/list", // 消息列表
|
||||
];
|
||||
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
||||
timeout: 20000,
|
||||
@@ -64,21 +71,31 @@ export function request(
|
||||
url: string,
|
||||
data?: any,
|
||||
method: Method = "GET",
|
||||
config?: AxiosRequestConfig,
|
||||
// 允许通过 config.debounce 控制是否开启截流,默认开启
|
||||
config?: AxiosRequestConfig & { debounce?: boolean },
|
||||
debounceGap?: number,
|
||||
): Promise<any> {
|
||||
const gap =
|
||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
const enableDebounce = config?.debounce !== false;
|
||||
const isInNoDebounceList = NO_DEBOUNCE_URLS.some(pattern =>
|
||||
url.includes(pattern),
|
||||
);
|
||||
const shouldDebounce = enableDebounce && !isInNoDebounceList;
|
||||
|
||||
if (shouldDebounce) {
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
}
|
||||
|
||||
const axiosConfig: AxiosRequestConfig & { debounce?: boolean } = {
|
||||
url,
|
||||
method,
|
||||
...config,
|
||||
|
||||
@@ -9,11 +9,20 @@ import { useUserStore } from "@/store/module/user";
|
||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||
const debounceMap = new Map<string, number>();
|
||||
|
||||
// 需要高频轮询、不走截流的接口白名单(按实际接口路径调整)
|
||||
const NO_DEBOUNCE_URLS = [
|
||||
"/wechat/friend/list",
|
||||
"/wechat/group/list",
|
||||
"/wechat/message/list",
|
||||
];
|
||||
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
headers: {
|
||||
headers?: {
|
||||
Client?: string;
|
||||
"Content-Type"?: string;
|
||||
};
|
||||
// 是否开启截流,默认开启
|
||||
debounce?: boolean;
|
||||
}
|
||||
|
||||
const instance: AxiosInstance = axios.create({
|
||||
@@ -63,14 +72,23 @@ export function request(
|
||||
): Promise<any> {
|
||||
const gap =
|
||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
|
||||
const enableDebounce = config?.debounce !== false;
|
||||
const isInNoDebounceList = NO_DEBOUNCE_URLS.some(pattern =>
|
||||
url.includes(pattern),
|
||||
);
|
||||
const shouldDebounce = enableDebounce && !isInNoDebounceList;
|
||||
|
||||
if (shouldDebounce) {
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject("请求过于频繁,请稍后再试");
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
|
||||
const axiosConfig: RequestConfig = {
|
||||
url,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.redPacketMessage {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
max-width: 300px;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.redPacketCard {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from "react";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./RedPacketMessage.module.scss";
|
||||
|
||||
interface RedPacketData {
|
||||
@@ -10,9 +12,15 @@ interface RedPacketData {
|
||||
|
||||
interface RedPacketMessageProps {
|
||||
content: string;
|
||||
msg?: ChatRecord;
|
||||
contract?: ContractData | weChatGroup;
|
||||
}
|
||||
|
||||
const RedPacketMessage: React.FC<RedPacketMessageProps> = ({ content }) => {
|
||||
const RedPacketMessage: React.FC<RedPacketMessageProps> = ({
|
||||
content,
|
||||
msg,
|
||||
contract,
|
||||
}) => {
|
||||
const renderErrorMessage = (fallbackText: string) => (
|
||||
<div className={styles.messageText}>{fallbackText}</div>
|
||||
);
|
||||
@@ -39,10 +47,42 @@ const RedPacketMessage: React.FC<RedPacketMessageProps> = ({ content }) => {
|
||||
|
||||
const title = jsonData.sendertitle || "恭喜发财,大吉大利";
|
||||
const paymsgid = jsonData.paymsgid || "";
|
||||
const nativeurl = jsonData.nativeurl || "";
|
||||
|
||||
// 处理红包点击事件,发送 socket 请求
|
||||
const handleRedPacketClick = () => {
|
||||
if (!contract || !paymsgid || !nativeurl) {
|
||||
console.warn("红包点击失败:缺少必要参数", {
|
||||
contract,
|
||||
paymsgid,
|
||||
nativeurl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isGroup = !!contract.chatroomId;
|
||||
const wechatFriendId = isGroup ? 0 : contract.id;
|
||||
|
||||
// 发送 socket 请求
|
||||
useWebSocketStore.getState().sendCommand("CmdOpenLuckyMoney", {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatFriendId: wechatFriendId,
|
||||
paymsgid: paymsgid,
|
||||
nativeurl: nativeurl,
|
||||
});
|
||||
|
||||
console.log("发送红包打开请求:", {
|
||||
cmdType: "CmdOpenLuckyMoney",
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatFriendId: wechatFriendId,
|
||||
paymsgid: paymsgid,
|
||||
nativeurl: nativeurl,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.redPacketMessage}>
|
||||
<div className={styles.redPacketCard}>
|
||||
<div className={styles.redPacketCard} onClick={handleRedPacketClick}>
|
||||
<div className={styles.redPacketHeader}>
|
||||
<div className={styles.redPacketIcon}>🧧</div>
|
||||
<div className={styles.redPacketTitle}>{title}</div>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// 转账消息样式
|
||||
.transferMessage {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
width: 260px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.transferCard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
background: #ff9500; // 橙色背景
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 149, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.transferDisabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 2px 8px rgba(255, 149, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transferHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transferIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.transferAmount {
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.transferStatus {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
margin-bottom: 5px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.transferDivider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.transferFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.transferLabel {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 消息文本样式(用于错误提示)
|
||||
.messageText {
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.transferMessage {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.transferCard {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.transferIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.transferAmount {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.transferStatus {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.transferLabel {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from "react";
|
||||
import { SwapOutlined } from "@ant-design/icons";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./TransferMessage.module.scss";
|
||||
|
||||
interface TransferData {
|
||||
title?: string;
|
||||
feedesc?: string;
|
||||
payMemo?: string;
|
||||
transferid?: string;
|
||||
transcationid?: string;
|
||||
invalidtime?: string;
|
||||
paysubtype?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface TransferMessageProps {
|
||||
content: string;
|
||||
msg?: ChatRecord;
|
||||
contract?: ContractData | weChatGroup;
|
||||
}
|
||||
|
||||
const TransferMessage: React.FC<TransferMessageProps> = ({
|
||||
content,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
msg, // 保留参数以保持与 RedPacketMessage 组件的一致性,未来可能会用到
|
||||
contract,
|
||||
}) => {
|
||||
const renderErrorMessage = (fallbackText: string) => (
|
||||
<div className={styles.messageText}>{fallbackText}</div>
|
||||
);
|
||||
|
||||
if (typeof content !== "string" || !content.trim()) {
|
||||
return renderErrorMessage("[转账消息 - 无效内容]");
|
||||
}
|
||||
|
||||
try {
|
||||
const trimmedContent = content.trim();
|
||||
const jsonData: TransferData = JSON.parse(trimmedContent);
|
||||
|
||||
// 验证是否为转账消息
|
||||
const isTransfer =
|
||||
jsonData.title === "微信转账" ||
|
||||
(jsonData.transferid && jsonData.feedesc);
|
||||
|
||||
if (!isTransfer) {
|
||||
return renderErrorMessage("[转账消息 - 格式错误]");
|
||||
}
|
||||
|
||||
const amount = jsonData.feedesc || "¥0.00";
|
||||
|
||||
// 判断转账状态
|
||||
const getTransferStatus = (
|
||||
data: TransferData,
|
||||
): { text: string; canClick: boolean } => {
|
||||
const paySubType = data.paysubtype || "";
|
||||
|
||||
switch (paySubType) {
|
||||
case "1":
|
||||
return { text: "待朋友确认收钱", canClick: true };
|
||||
case "2":
|
||||
return { text: "已过期", canClick: false };
|
||||
case "3":
|
||||
return { text: "已领取", canClick: false };
|
||||
case "4":
|
||||
return { text: "已退回", canClick: false };
|
||||
default:
|
||||
// 默认情况:可能是待领取
|
||||
return { text: "待朋友确认收钱", canClick: true };
|
||||
}
|
||||
};
|
||||
|
||||
const { text: statusText, canClick } = getTransferStatus(jsonData);
|
||||
|
||||
// 处理转账点击事件,发送 socket 请求
|
||||
const handleTransferClick = () => {
|
||||
// 如果状态不允许点击,直接返回
|
||||
if (!canClick) {
|
||||
console.log("转账状态不允许点击:", statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!contract ||
|
||||
!jsonData.transferid ||
|
||||
!jsonData.transcationid ||
|
||||
!jsonData.invalidtime ||
|
||||
!jsonData.paysubtype
|
||||
) {
|
||||
console.warn("转账点击失败:缺少必要参数", {
|
||||
contract,
|
||||
transferid: jsonData.transferid,
|
||||
transcationid: jsonData.transcationid,
|
||||
invalidtime: jsonData.invalidtime,
|
||||
paysubtype: jsonData.paysubtype,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isGroup = !!contract.chatroomId;
|
||||
const wechatFriendId = isGroup ? 0 : contract.id;
|
||||
|
||||
// 发送 socket 请求
|
||||
useWebSocketStore.getState().sendCommand("CmdReceiveTransMoney", {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatFriendId: wechatFriendId,
|
||||
transcationid: jsonData.transcationid,
|
||||
transferid: jsonData.transferid,
|
||||
invalidtime: jsonData.invalidtime,
|
||||
paysubtype: jsonData.paysubtype,
|
||||
});
|
||||
|
||||
console.log("发送转账接收请求:", {
|
||||
cmdType: "CmdReceiveTransMoney",
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatFriendId: wechatFriendId,
|
||||
transcationid: jsonData.transcationid,
|
||||
transferid: jsonData.transferid,
|
||||
invalidtime: jsonData.invalidtime,
|
||||
paysubtype: jsonData.paysubtype,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.transferMessage}>
|
||||
<div
|
||||
className={`${styles.transferCard} ${!canClick ? styles.transferDisabled : ""}`}
|
||||
onClick={handleTransferClick}
|
||||
>
|
||||
<div className={styles.transferHeader}>
|
||||
<div className={styles.transferIcon}>
|
||||
<SwapOutlined style={{ fontSize: 20 }} />
|
||||
</div>
|
||||
<div className="destion">
|
||||
<div className={styles.transferAmount}>{amount}</div>
|
||||
<div className={styles.transferStatus}>{statusText}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.transferDivider}></div>
|
||||
<div className={styles.transferFooter}>
|
||||
<span className={styles.transferLabel}>微信转账</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("转账消息解析失败:", e);
|
||||
return renderErrorMessage("[转账消息 - 解析失败]");
|
||||
}
|
||||
};
|
||||
|
||||
export default TransferMessage;
|
||||
@@ -8,9 +8,11 @@ import ClickMenu from "./components/ClickMeau";
|
||||
import LocationMessage from "./components/LocationMessage";
|
||||
import SystemRecommendRemarkMessage from "./components/SystemRecommendRemarkMessage/index";
|
||||
import RedPacketMessage from "./components/RedPacketMessage";
|
||||
import TransferMessage from "./components/TransferMessage";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
||||
import { parseSystemMessage } from "@/utils/filter";
|
||||
import styles from "./com.module.scss";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||
@@ -148,6 +150,7 @@ type GroupRenderItem = {
|
||||
|
||||
const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
// 右键菜单状态
|
||||
const [contextMenu, setContextMenu] = useState({
|
||||
visible: false,
|
||||
@@ -169,6 +172,8 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
const isLoadingData = useWeChatStore(state => state.isLoadingData);
|
||||
const showCheckbox = useWeChatStore(state => state.showCheckbox);
|
||||
const prevMessagesRef = useRef(currentMessages);
|
||||
const isLoadingMoreRef = useRef(false);
|
||||
const scrollPositionRef = useRef<number>(0);
|
||||
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
|
||||
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
|
||||
const currentCustomer = useCustomerStore(state =>
|
||||
@@ -255,7 +260,6 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
msg?: ChatRecord,
|
||||
contract?: ContractData | weChatGroup,
|
||||
) => {
|
||||
console.log("红包");
|
||||
if (isLegacyEmojiContent(trimmedContent)) {
|
||||
return renderEmojiContent(rawContent);
|
||||
}
|
||||
@@ -271,7 +275,23 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
"wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
|
||||
)
|
||||
) {
|
||||
return <RedPacketMessage content={rawContent} />;
|
||||
return (
|
||||
<RedPacketMessage
|
||||
content={rawContent}
|
||||
msg={msg}
|
||||
contract={contract}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 判断是否为转账消息
|
||||
if (
|
||||
jsonData.title === "微信转账" ||
|
||||
(jsonData.transferid && jsonData.feedesc)
|
||||
) {
|
||||
return (
|
||||
<TransferMessage content={rawContent} msg={msg} contract={contract} />
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonData.type === "file" && msg && contract) {
|
||||
@@ -443,7 +463,10 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (currentMessages.length > prevLength && !hasVideoStateChange) {
|
||||
// 如果正在加载更早的消息,不自动滚动到底部
|
||||
if (isLoadingMoreRef.current && currentMessages.length > prevLength) {
|
||||
// 不滚动,等待加载完成后在另一个 useEffect 中恢复滚动位置
|
||||
} else if (currentMessages.length > prevLength && !hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
} else if (isLoadingData && !hasVideoStateChange) {
|
||||
scrollToBottom();
|
||||
@@ -453,6 +476,22 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
prevMessagesRef.current = currentMessages;
|
||||
}, [currentMessages, isLoadingData]);
|
||||
|
||||
// 监听加载状态,当加载完成时恢复滚动位置
|
||||
useEffect(() => {
|
||||
if (!messagesLoading && isLoadingMoreRef.current) {
|
||||
// 等待DOM更新后恢复滚动位置
|
||||
requestAnimationFrame(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const newScrollTop = scrollHeight - scrollPositionRef.current;
|
||||
container.scrollTop = newScrollTop;
|
||||
}
|
||||
isLoadingMoreRef.current = false;
|
||||
});
|
||||
}
|
||||
}, [messagesLoading]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
@@ -745,6 +784,12 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
if (messagesLoading || !currentMessagesHasMore) {
|
||||
return;
|
||||
}
|
||||
// 保存当前滚动位置(距离底部的距离)
|
||||
const container = messagesContainerRef.current;
|
||||
if (container) {
|
||||
scrollPositionRef.current = container.scrollHeight - container.scrollTop;
|
||||
isLoadingMoreRef.current = true;
|
||||
}
|
||||
loadChatMessages(false);
|
||||
};
|
||||
|
||||
@@ -823,7 +868,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.messagesContainer}>
|
||||
<div ref={messagesContainerRef} className={styles.messagesContainer}>
|
||||
<div
|
||||
className={styles.loadMore}
|
||||
onClick={() => loadMoreMessages()}
|
||||
@@ -839,14 +884,16 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
||||
<React.Fragment key={`group-${groupIndex}`}>
|
||||
{group.messages
|
||||
.filter(v => [10000].includes(v.msgType))
|
||||
.map(msg => (
|
||||
<div
|
||||
key={`divider-${msg.id}`}
|
||||
className={styles.messageTime}
|
||||
dangerouslySetInnerHTML={{ __html: msg.content }}
|
||||
></div>
|
||||
))}
|
||||
.filter(v => [10000, -10001].includes(v.msgType))
|
||||
.map(msg => {
|
||||
// 解析系统消息,提取纯文本(移除img标签和_wc_custom_link_标签)
|
||||
const parsedText = parseSystemMessage(msg.content);
|
||||
return (
|
||||
<div key={`divider-${msg.id}`} className={styles.messageTime}>
|
||||
{parsedText}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{group.messages
|
||||
.filter(v => [570425393, 90000].includes(v.msgType))
|
||||
@@ -874,7 +921,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
})}
|
||||
<div className={styles.messageTime}>{group.time}</div>
|
||||
{group.messages
|
||||
.filter(v => ![10000, 570425393, 90000].includes(v.msgType))
|
||||
.filter(v => ![10000, 570425393, 90000, -10001].includes(v.msgType))
|
||||
.map(msg => {
|
||||
return renderMessage(msg);
|
||||
})}
|
||||
|
||||
@@ -47,6 +47,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
} = useMessageStore();
|
||||
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([]);
|
||||
const [syncing, setSyncing] = useState(false); // 同步状态
|
||||
const hasEnrichedRef = useRef(false); // 是否已做过未知联系人补充
|
||||
|
||||
// 右键菜单相关状态
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
@@ -296,7 +297,130 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
};
|
||||
}, [contextMenu.visible]);
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
// ==================== 数据加载 & 未知联系人补充 ====================
|
||||
|
||||
// 同步完成后,检查是否存在“未知联系人”或缺失头像/昵称的会话,并异步补充详情
|
||||
const enrichUnknownContacts = async () => {
|
||||
if (!currentUserId) return;
|
||||
if (hasEnrichedRef.current) return; // 避免重复执行
|
||||
|
||||
// 只在会话有数据时执行
|
||||
if (!sessions || sessions.length === 0) return;
|
||||
|
||||
const needEnrich = sessions.filter(s => {
|
||||
const noName = !s.conRemark && !s.nickname && !s.wechatId;
|
||||
const isUnknownNickname = s.nickname === "未知联系人";
|
||||
const noAvatar = !s.avatar;
|
||||
return noName || isUnknownNickname || noAvatar;
|
||||
});
|
||||
|
||||
if (needEnrich.length === 0) {
|
||||
hasEnrichedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
hasEnrichedRef.current = true;
|
||||
|
||||
// 逐个异步拉取详情,失败不打断整体流程
|
||||
for (const session of needEnrich) {
|
||||
try {
|
||||
let detailResult: any = null;
|
||||
if (session.type === "friend") {
|
||||
detailResult = await getWechatFriendDetail({ id: session.id });
|
||||
} else {
|
||||
detailResult = await getWechatChatroomDetail({ id: session.id });
|
||||
}
|
||||
|
||||
const detail = detailResult?.detail;
|
||||
if (!detail) continue;
|
||||
|
||||
// 更新会话列表 UI
|
||||
setSessionState(prev =>
|
||||
prev.map(s =>
|
||||
s.id === session.id && s.type === session.type
|
||||
? {
|
||||
...s,
|
||||
avatar:
|
||||
session.type === "group"
|
||||
? detail.chatroomAvatar || s.avatar
|
||||
: detail.avatar || s.avatar,
|
||||
nickname: detail.nickname || s.nickname,
|
||||
conRemark: detail.conRemark || s.conRemark,
|
||||
wechatId: detail.wechatId || s.wechatId,
|
||||
}
|
||||
: s,
|
||||
),
|
||||
);
|
||||
|
||||
// 同步到会话数据库
|
||||
await MessageManager.updateSession({
|
||||
userId: currentUserId,
|
||||
id: session.id,
|
||||
type: session.type,
|
||||
avatar:
|
||||
session.type === "group"
|
||||
? detail.chatroomAvatar || session.avatar
|
||||
: detail.avatar || session.avatar,
|
||||
nickname: detail.nickname || session.nickname,
|
||||
conRemark: detail.conRemark || session.conRemark,
|
||||
wechatId: detail.wechatId || session.wechatId,
|
||||
});
|
||||
|
||||
// 同步到联系人数据库(方便后续搜索、其它页面使用)
|
||||
const contactBase: any = {
|
||||
serverId: `${session.type}_${session.id}`,
|
||||
userId: currentUserId,
|
||||
id: session.id,
|
||||
type: session.type,
|
||||
wechatAccountId: detail.wechatAccountId,
|
||||
nickname: detail.nickname || "",
|
||||
conRemark: detail.conRemark || "",
|
||||
avatar:
|
||||
session.type === "group"
|
||||
? detail.chatroomAvatar || ""
|
||||
: detail.avatar || "",
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
sortKey: "",
|
||||
searchKey: (detail.conRemark || detail.nickname || "").toLowerCase(),
|
||||
};
|
||||
|
||||
if (session.type === "group") {
|
||||
Object.assign(contactBase, {
|
||||
chatroomId: detail.chatroomId,
|
||||
chatroomOwner: detail.chatroomOwner,
|
||||
selfDisplayName: detail.selfDisplyName,
|
||||
notice: detail.notice,
|
||||
});
|
||||
} else {
|
||||
Object.assign(contactBase, {
|
||||
wechatFriendId: detail.id,
|
||||
wechatId: detail.wechatId,
|
||||
alias: detail.alias,
|
||||
gender: detail.gender,
|
||||
region: detail.region,
|
||||
signature: detail.signature,
|
||||
phone: detail.phone,
|
||||
quanPin: detail.quanPin,
|
||||
groupId: detail.groupId,
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 upsert 逻辑:如果已存在就更新,不存在则新增
|
||||
const existContact = await ContactManager.getContactByIdAndType(
|
||||
currentUserId,
|
||||
session.id,
|
||||
session.type,
|
||||
);
|
||||
if (existContact) {
|
||||
await ContactManager.updateContact(contactBase);
|
||||
} else {
|
||||
await ContactManager.addContact(contactBase);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("补拉未知联系人详情失败:", error, session);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 与服务器同步数据(优化版:逐页同步,立即更新UI)
|
||||
const syncWithServer = async () => {
|
||||
@@ -379,6 +503,8 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
console.log(
|
||||
`会话同步完成: 成功${successCount}页, 失败${failCount}页, 共处理${totalProcessed}条数据`,
|
||||
);
|
||||
// 同步完成后,异步补充未知联系人信息
|
||||
enrichUnknownContacts();
|
||||
} catch (error) {
|
||||
console.error("同步服务器数据失败:", error);
|
||||
} finally {
|
||||
|
||||
@@ -40,6 +40,25 @@ export const messageFilter = (message: string) => {
|
||||
case !!parsed.linkUrl:
|
||||
return "[链接]";
|
||||
|
||||
// 微信红包消息:包含 paymsgid 或 nativeurl 中包含红包链接
|
||||
case !!(
|
||||
parsed.paymsgid ||
|
||||
(parsed.nativeurl &&
|
||||
parsed.nativeurl.includes(
|
||||
"wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
|
||||
))
|
||||
):
|
||||
return parsed.sendertitle
|
||||
? `[微信红包] ${parsed.sendertitle}`
|
||||
: "[微信红包]";
|
||||
|
||||
// 微信转账消息:包含 title="微信转账" 或 transferid + feedesc
|
||||
case !!(
|
||||
parsed.title === "微信转账" ||
|
||||
(parsed.transferid && parsed.feedesc)
|
||||
):
|
||||
return parsed.feedesc ? `[微信转账] ${parsed.feedesc}` : "[微信转账]";
|
||||
|
||||
// 文本消息:包含 text 或 content
|
||||
case !!(parsed.text || parsed.content):
|
||||
return parsed.text || parsed.content;
|
||||
@@ -67,3 +86,37 @@ export const messageFilter = (message: string) => {
|
||||
return message;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析系统消息中的HTML标签,提取纯文本
|
||||
* 例如:<img src="SystemMessages_HongbaoIcon.png"/> 你领取了许老板的<_wc_custom_link_>红包</_wc_custom_link_>。
|
||||
* 提取为:🧧 你领取了许老板的红包
|
||||
*/
|
||||
export const parseSystemMessage = (html: string): string => {
|
||||
if (!html) return "";
|
||||
|
||||
let text = html;
|
||||
|
||||
// 移除所有 <img> 标签
|
||||
text = text.replace(/<img[^>]*>/gi, "");
|
||||
|
||||
// 提取 <_wc_custom_link_> 标签内的文本内容
|
||||
// 匹配 <_wc_custom_link_ ...>内容</_wc_custom_link_>
|
||||
text = text.replace(
|
||||
/<_wc_custom_link_[^>]*>(.*?)<\/_wc_custom_link_>/gi,
|
||||
"$1",
|
||||
);
|
||||
|
||||
// 清理多余的空格(将多个连续空格替换为单个空格)
|
||||
text = text.replace(/\s+/g, " ");
|
||||
|
||||
// 去除首尾空格
|
||||
text = text.trim();
|
||||
|
||||
// 如果消息内容包含红包相关关键词,在前面添加🧧图标
|
||||
if (/红包|hongbao/i.test(text)) {
|
||||
text = `🧧 ${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user