Refactor cache clearing functionality in settings page to improve user feedback and error handling. Update WeChat API functions to support debounce options for contact and group list retrieval. Enhance message rendering logic to include red packet messages and improve user identification in chat records.
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
LockOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
@@ -16,7 +15,7 @@ import { useSettingsStore } from "@/store/module/settings";
|
||||
import style from "./index.module.scss";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp";
|
||||
import { updateChecker } from "@/utils/updateChecker";
|
||||
import { clearApplicationCache } from "@/utils/cacheCleaner";
|
||||
|
||||
interface SettingItem {
|
||||
id: string;
|
||||
@@ -58,13 +57,35 @@ const Setting: React.FC = () => {
|
||||
const handleClearCache = () => {
|
||||
Dialog.confirm({
|
||||
content: "确定要清除缓存吗?这将清除所有本地数据。",
|
||||
onConfirm: () => {
|
||||
sendMessageToParent(
|
||||
{
|
||||
action: "clearCache",
|
||||
},
|
||||
TYPE_EMUE.FUNCTION,
|
||||
);
|
||||
onConfirm: async () => {
|
||||
const handler = Toast.show({
|
||||
icon: "loading",
|
||||
content: "正在清理缓存...",
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await clearApplicationCache();
|
||||
sendMessageToParent(
|
||||
{
|
||||
action: "clearCache",
|
||||
},
|
||||
TYPE_EMUE.FUNCTION,
|
||||
);
|
||||
handler.close();
|
||||
Toast.show({
|
||||
icon: "success",
|
||||
content: "缓存清理完成",
|
||||
position: "top",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("clear cache failed", error);
|
||||
handler.close();
|
||||
Toast.show({
|
||||
icon: "fail",
|
||||
content: "缓存清理失败,请稍后再试",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
70
Cunkebao/src/utils/cacheCleaner.ts
Normal file
70
Cunkebao/src/utils/cacheCleaner.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// 全局缓存清理工具:浏览器存储 + IndexedDB + Zustand store
|
||||
import { clearAllPersistedData } from "@/store";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { useAppStore } from "@/store/module/app";
|
||||
import { useSettingsStore } from "@/store/module/settings";
|
||||
|
||||
const isBrowser = typeof window !== "undefined";
|
||||
|
||||
const safeStorageClear = (storage?: Storage) => {
|
||||
if (!storage) return;
|
||||
try {
|
||||
storage.clear();
|
||||
} catch (error) {
|
||||
console.warn("清理存储失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearBrowserStorage = () => {
|
||||
if (!isBrowser) return;
|
||||
safeStorageClear(window.localStorage);
|
||||
safeStorageClear(window.sessionStorage);
|
||||
try {
|
||||
clearAllPersistedData();
|
||||
} catch (error) {
|
||||
console.warn("清理持久化 store 失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllIndexedDB = async (): Promise<void> => {
|
||||
if (!isBrowser || !window.indexedDB || !indexedDB.databases) return;
|
||||
|
||||
const databases = await indexedDB.databases();
|
||||
const deleteJobs = databases
|
||||
.map(db => db.name)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
.map(
|
||||
name =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase(name);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(new Error(`删除数据库 ${name} 失败`));
|
||||
request.onblocked = () => {
|
||||
setTimeout(() => {
|
||||
const retry = indexedDB.deleteDatabase(name);
|
||||
retry.onsuccess = () => resolve();
|
||||
retry.onerror = () =>
|
||||
reject(new Error(`删除数据库 ${name} 失败`));
|
||||
}, 100);
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.allSettled(deleteJobs);
|
||||
};
|
||||
|
||||
export const resetAllStores = () => {
|
||||
const userStore = useUserStore.getState();
|
||||
const appStore = useAppStore.getState();
|
||||
const settingsStore = useSettingsStore.getState();
|
||||
|
||||
userStore?.clearUser?.();
|
||||
appStore?.resetAppState?.();
|
||||
settingsStore?.resetSettings?.();
|
||||
};
|
||||
|
||||
export const clearApplicationCache = async () => {
|
||||
clearBrowserStorage();
|
||||
await clearAllIndexedDB();
|
||||
resetAllStores();
|
||||
};
|
||||
73
Moncter/src/utils/cacheCleaner.ts
Normal file
73
Moncter/src/utils/cacheCleaner.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// 缓存清理工具,统一处理浏览器存储与 Zustand store
|
||||
import { clearAllPersistedData } from "@/store";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { useAppStore } from "@/store/module/app";
|
||||
import { useSettingsStore } from "@/store/module/settings";
|
||||
|
||||
const isBrowser = typeof window !== "undefined";
|
||||
|
||||
const safeStorageClear = (storage?: Storage) => {
|
||||
if (!storage) return;
|
||||
try {
|
||||
storage.clear();
|
||||
} catch (error) {
|
||||
console.warn("清理存储失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearBrowserStorage = () => {
|
||||
if (!isBrowser) return;
|
||||
safeStorageClear(window.localStorage);
|
||||
safeStorageClear(window.sessionStorage);
|
||||
// 清理自定义持久化数据
|
||||
try {
|
||||
clearAllPersistedData();
|
||||
} catch (error) {
|
||||
console.warn("清理持久化 store 失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllIndexedDB = async (): Promise<void> => {
|
||||
if (!isBrowser || !window.indexedDB || !indexedDB.databases) return;
|
||||
|
||||
const databases = await indexedDB.databases();
|
||||
const deleteJobs = databases
|
||||
.map(db => db.name)
|
||||
.filter((name): name is string => Boolean(name))
|
||||
.map(
|
||||
name =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase(name);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () =>
|
||||
reject(new Error(`删除数据库 ${name} 失败`));
|
||||
request.onblocked = () => {
|
||||
setTimeout(() => {
|
||||
const retry = indexedDB.deleteDatabase(name);
|
||||
retry.onsuccess = () => resolve();
|
||||
retry.onerror = () =>
|
||||
reject(new Error(`删除数据库 ${name} 失败`));
|
||||
}, 100);
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.allSettled(deleteJobs);
|
||||
};
|
||||
|
||||
export const resetAllStores = () => {
|
||||
const userStore = useUserStore.getState();
|
||||
const appStore = useAppStore.getState();
|
||||
const settingsStore = useSettingsStore.getState();
|
||||
|
||||
userStore?.clearUser?.();
|
||||
appStore?.resetAppState?.();
|
||||
settingsStore?.resetSettings?.();
|
||||
};
|
||||
|
||||
export const clearApplicationCache = async () => {
|
||||
clearBrowserStorage();
|
||||
await clearAllIndexedDB();
|
||||
resetAllStores();
|
||||
};
|
||||
|
||||
@@ -28,14 +28,30 @@ export function getTrafficPoolList() {
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
type ListRequestOptions = {
|
||||
debounceGap?: number;
|
||||
};
|
||||
|
||||
// 好友列表
|
||||
export function getContactList(params) {
|
||||
return request("/v1/kefu/wechatFriend/list", params, "GET");
|
||||
export function getContactList(params, options?: ListRequestOptions) {
|
||||
return request(
|
||||
"/v1/kefu/wechatFriend/list",
|
||||
params,
|
||||
"GET",
|
||||
undefined,
|
||||
options?.debounceGap,
|
||||
);
|
||||
}
|
||||
|
||||
// 群列表
|
||||
export function getGroupList(params) {
|
||||
return request("/v1/kefu/wechatChatroom/list", params, "GET");
|
||||
export function getGroupList(params, options?: ListRequestOptions) {
|
||||
return request(
|
||||
"/v1/kefu/wechatChatroom/list",
|
||||
params,
|
||||
"GET",
|
||||
undefined,
|
||||
options?.debounceGap,
|
||||
);
|
||||
}
|
||||
// 分组列表
|
||||
export function getLabelsListByGroup(params) {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// 红包消息样式
|
||||
.redPacketMessage {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.redPacketCard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
|
||||
overflow: hidden;
|
||||
|
||||
// 红包装饰背景
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 215, 0, 0.15) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
// 金色装饰边框
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 2px solid rgba(255, 215, 0, 0.4);
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 107, 107, 0.4);
|
||||
background: linear-gradient(135deg, #ff7b7b 0%, #ff6b7f 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.redPacketHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.redPacketIcon {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
.redPacketTitle {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.redPacketFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.redPacketLabel {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&::before {
|
||||
content: "💰";
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 消息文本样式(用于错误提示)
|
||||
.messageText {
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.redPacketMessage {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.redPacketCard {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.redPacketIcon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.redPacketTitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.redPacketLabel {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import styles from "./RedPacketMessage.module.scss";
|
||||
|
||||
interface RedPacketData {
|
||||
nativeurl?: string;
|
||||
paymsgid?: string;
|
||||
sendertitle?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RedPacketMessageProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const RedPacketMessage: React.FC<RedPacketMessageProps> = ({ content }) => {
|
||||
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: RedPacketData = JSON.parse(trimmedContent);
|
||||
|
||||
// 验证是否为红包消息
|
||||
const isRedPacket =
|
||||
jsonData.nativeurl &&
|
||||
typeof jsonData.nativeurl === "string" &&
|
||||
jsonData.nativeurl.includes(
|
||||
"wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
|
||||
);
|
||||
|
||||
if (!isRedPacket) {
|
||||
return renderErrorMessage("[红包消息 - 格式错误]");
|
||||
}
|
||||
|
||||
const title = jsonData.sendertitle || "恭喜发财,大吉大利";
|
||||
const paymsgid = jsonData.paymsgid || "";
|
||||
|
||||
return (
|
||||
<div className={styles.redPacketMessage}>
|
||||
<div className={styles.redPacketCard}>
|
||||
<div className={styles.redPacketHeader}>
|
||||
<div className={styles.redPacketIcon}>🧧</div>
|
||||
<div className={styles.redPacketTitle}>{title}</div>
|
||||
</div>
|
||||
<div className={styles.redPacketFooter}>
|
||||
<span className={styles.redPacketLabel}>微信红包</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("红包消息解析失败:", e);
|
||||
return renderErrorMessage("[红包消息 - 解析失败]");
|
||||
}
|
||||
};
|
||||
|
||||
export default RedPacketMessage;
|
||||
@@ -7,6 +7,7 @@ import VideoMessage from "./components/VideoMessage";
|
||||
import ClickMenu from "./components/ClickMeau";
|
||||
import LocationMessage from "./components/LocationMessage";
|
||||
import SystemRecommendRemarkMessage from "./components/SystemRecommendRemarkMessage/index";
|
||||
import RedPacketMessage from "./components/RedPacketMessage";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
||||
@@ -254,6 +255,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
msg?: ChatRecord,
|
||||
contract?: ContractData | weChatGroup,
|
||||
) => {
|
||||
console.log("红包");
|
||||
if (isLegacyEmojiContent(trimmedContent)) {
|
||||
return renderEmojiContent(rawContent);
|
||||
}
|
||||
@@ -261,6 +263,17 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
const jsonData = tryParseContentJson(trimmedContent);
|
||||
|
||||
if (jsonData && typeof jsonData === "object") {
|
||||
// 判断是否为红包消息
|
||||
if (
|
||||
jsonData.nativeurl &&
|
||||
typeof jsonData.nativeurl === "string" &&
|
||||
jsonData.nativeurl.includes(
|
||||
"wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
|
||||
)
|
||||
) {
|
||||
return <RedPacketMessage content={rawContent} />;
|
||||
}
|
||||
|
||||
if (jsonData.type === "file" && msg && contract) {
|
||||
return (
|
||||
<SmallProgramMessage
|
||||
@@ -378,13 +391,15 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
if (!msg) {
|
||||
return { avatar: "", nickname: "" };
|
||||
}
|
||||
const member =
|
||||
groupRender.find(user => user?.identifier === msg?.sender?.wechatId) ||
|
||||
groupRender.find(user => user?.wechatId === msg?.sender?.wechatId);
|
||||
|
||||
const member = groupRender.find(
|
||||
user => user?.identifier === msg?.senderWechatId,
|
||||
);
|
||||
console.log(member, "member");
|
||||
|
||||
return {
|
||||
avatar: member?.avatar || msg?.sender?.avatar || "",
|
||||
nickname: member?.nickname || msg?.sender?.nickname || "",
|
||||
avatar: member?.avatar || msg?.avatar,
|
||||
nickname: member?.nickname || msg?.senderNickname,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -615,7 +630,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
|
||||
const isOwn = msg?.isSend;
|
||||
const isGroup = !!contract.chatroomId;
|
||||
const groupUser = isGroup ? renderGroupUser(msg) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={msg.id || `msg-${Date.now()}`}
|
||||
@@ -667,14 +682,14 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
)}
|
||||
<Avatar
|
||||
size={32}
|
||||
src={groupUser?.avatar}
|
||||
src={renderGroupUser(msg)?.avatar}
|
||||
icon={<UserOutlined />}
|
||||
className={styles.messageAvatar}
|
||||
/>
|
||||
<div>
|
||||
{!isOwn && (
|
||||
<div className={styles.messageSender}>
|
||||
{groupUser?.nickname}
|
||||
{renderGroupUser(msg)?.nickname}
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const getAllFriends = async () => {
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const result = await getContactList({ page, limit });
|
||||
const result = await getContactList({ page, limit }, { debounceGap: 0 });
|
||||
const friendList = result?.list || [];
|
||||
|
||||
if (
|
||||
@@ -56,7 +56,7 @@ export const getAllGroups = async () => {
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const result = await getGroupList({ page, limit });
|
||||
const result = await getGroupList({ page, limit }, { debounceGap: 0 });
|
||||
const groupList = result?.list || [];
|
||||
|
||||
if (!groupList || !Array.isArray(groupList) || groupList.length === 0) {
|
||||
|
||||
@@ -659,7 +659,7 @@ export class MessageManager {
|
||||
updatedSession.sortKey = this.generateSortKey(updatedSession);
|
||||
|
||||
await chatSessionService.update(serverId, updatedSession);
|
||||
console.log(`会话时间已更新: ${serverId} -> ${newTime}`);
|
||||
await this.triggerCallbacks(userId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("更新会话时间失败:", error);
|
||||
@@ -830,7 +830,7 @@ export class MessageManager {
|
||||
};
|
||||
|
||||
await chatSessionService.create(sessionWithSortKey);
|
||||
console.log(`创建新会话: ${session.nickname || session.wechatId}`);
|
||||
await this.triggerCallbacks(userId);
|
||||
} catch (error) {
|
||||
console.error("创建会话失败:", error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user