Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
0
Moncter/src/pages/pc/ckbox/weChat/api.ts
Normal file
0
Moncter/src/pages/pc/ckbox/weChat/api.ts
Normal file
0
Moncter/src/store/module/websocket/websocket.ts
Normal file
0
Moncter/src/store/module/websocket/websocket.ts
Normal file
@@ -17,6 +17,7 @@ const FriendListItem = memo<{
|
||||
onClick={() => onSelect(friend)}
|
||||
>
|
||||
<Checkbox checked={isSelected} />
|
||||
|
||||
<Avatar src={friend.avatar} size={40}>
|
||||
{friend.nickname?.charAt(0)}
|
||||
</Avatar>
|
||||
@@ -41,6 +42,9 @@ interface TwoColumnSelectionProps {
|
||||
deviceIds?: number[];
|
||||
enableDeviceFilter?: boolean;
|
||||
dataSource?: FriendSelectionItem[];
|
||||
onLoadMore?: () => void; // 加载更多回调
|
||||
hasMore?: boolean; // 是否有更多数据
|
||||
loading?: boolean; // 是否正在加载
|
||||
}
|
||||
|
||||
const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
@@ -51,15 +55,16 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
deviceIds = [],
|
||||
enableDeviceFilter = true,
|
||||
dataSource,
|
||||
onLoadMore,
|
||||
hasMore = false,
|
||||
loading = false,
|
||||
}) => {
|
||||
const [rawFriends, setRawFriends] = useState<FriendSelectionItem[]>([]);
|
||||
const [selectedFriends, setSelectedFriends] = useState<FriendSelectionItem[]>(
|
||||
[],
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 使用 useMemo 缓存过滤结果,避免每次渲染都重新计算
|
||||
const filteredFriends = useMemo(() => {
|
||||
@@ -76,17 +81,8 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
);
|
||||
}, [dataSource, rawFriends, searchQuery]);
|
||||
|
||||
// 分页显示好友列表,避免一次性渲染太多项目
|
||||
const ITEMS_PER_PAGE = 50;
|
||||
const [displayPage, setDisplayPage] = useState(1);
|
||||
|
||||
const friends = useMemo(() => {
|
||||
const startIndex = 0;
|
||||
const endIndex = displayPage * ITEMS_PER_PAGE;
|
||||
return filteredFriends.slice(startIndex, endIndex);
|
||||
}, [filteredFriends, displayPage]);
|
||||
|
||||
const hasMoreFriends = filteredFriends.length > friends.length;
|
||||
// 好友列表直接使用过滤后的结果
|
||||
const friends = filteredFriends;
|
||||
|
||||
// 使用 useMemo 缓存选中状态映射,避免每次渲染都重新计算
|
||||
const selectedFriendsMap = useMemo(() => {
|
||||
@@ -100,7 +96,7 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
// 获取好友列表
|
||||
const fetchFriends = useCallback(
|
||||
async (page: number, keyword: string = "") => {
|
||||
setLoading(true);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const params: any = {
|
||||
page,
|
||||
@@ -119,7 +115,6 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
|
||||
if (response.success) {
|
||||
setRawFriends(response.data.list || []);
|
||||
setTotalPages(Math.ceil((response.data.total || 0) / 20));
|
||||
} else {
|
||||
setRawFriends([]);
|
||||
message.error(response.message || "获取好友列表失败");
|
||||
@@ -128,7 +123,7 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
console.error("获取好友列表失败:", error);
|
||||
message.error("获取好友列表失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[deviceIds, enableDeviceFilter],
|
||||
@@ -139,7 +134,6 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
if (visible && !dataSource) {
|
||||
// 只有在没有外部数据源时才调用 API
|
||||
fetchFriends(1);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [visible, dataSource, fetchFriends]);
|
||||
|
||||
@@ -148,49 +142,23 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
if (visible) {
|
||||
setSearchQuery("");
|
||||
setSelectedFriends([]);
|
||||
setLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
// 防抖搜索处理
|
||||
const handleSearch = useCallback(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
return (value: string) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
setDisplayPage(1); // 重置分页
|
||||
if (!dataSource) {
|
||||
fetchFriends(1, value);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
}, [dataSource, fetchFriends])();
|
||||
|
||||
// API搜索处理(当没有外部数据源时)
|
||||
const handleApiSearch = useCallback(
|
||||
async (keyword: string) => {
|
||||
const handleSearch = useCallback(
|
||||
(value: string) => {
|
||||
if (!dataSource) {
|
||||
await fetchFriends(1, keyword);
|
||||
const timer = setTimeout(() => {
|
||||
fetchFriends(1, value);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
},
|
||||
[dataSource, fetchFriends],
|
||||
);
|
||||
|
||||
// 加载更多好友
|
||||
const handleLoadMore = useCallback(() => {
|
||||
setDisplayPage(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
// 防抖搜索
|
||||
useEffect(() => {
|
||||
if (!dataSource && searchQuery.trim()) {
|
||||
const timer = setTimeout(() => {
|
||||
handleApiSearch(searchQuery);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [searchQuery, dataSource, handleApiSearch]);
|
||||
|
||||
// 选择好友 - 使用 useCallback 优化性能
|
||||
const handleSelectFriend = useCallback((friend: FriendSelectionItem) => {
|
||||
setSelectedFriends(prev => {
|
||||
@@ -216,10 +184,8 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
setSearchQuery("");
|
||||
}, [selectedFriends, onConfirm]);
|
||||
|
||||
// 取消选择 - 使用 useCallback 优化性能
|
||||
// 取消选择
|
||||
const handleCancel = useCallback(() => {
|
||||
setSelectedFriends([]);
|
||||
setSearchQuery("");
|
||||
onCancel();
|
||||
}, [onCancel]);
|
||||
|
||||
@@ -248,8 +214,8 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
value={searchQuery}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
setSearchQuery(value); // 立即更新显示
|
||||
handleSearch(value); // 防抖处理搜索
|
||||
setSearchQuery(value);
|
||||
handleSearch(value);
|
||||
}}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
@@ -257,7 +223,7 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.friendList}>
|
||||
{loading ? (
|
||||
{isLoading && !loading ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : friends.length > 0 ? (
|
||||
// 使用 React.memo 优化列表项渲染
|
||||
@@ -280,9 +246,10 @@ const TwoColumnSelection: React.FC<TwoColumnSelectionProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMoreFriends && (
|
||||
{/* 使用外部传入的加载更多 */}
|
||||
{hasMore && (
|
||||
<div className={styles.loadMoreWrapper}>
|
||||
<Button type="link" onClick={handleLoadMore} loading={loading}>
|
||||
<Button type="link" onClick={onLoadMore} loading={loading}>
|
||||
加载更多
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,97 +7,18 @@ import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import App from "./App";
|
||||
import "./styles/global.scss";
|
||||
import { db } from "./utils/db"; // 引入数据库实例
|
||||
import { initializeDatabaseFromPersistedUser } from "./utils/db";
|
||||
|
||||
// 设置dayjs为中文
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
// 清理旧数据库
|
||||
async function cleanupOldDatabase() {
|
||||
async function bootstrap() {
|
||||
try {
|
||||
// 获取所有数据库
|
||||
const databases = await indexedDB.databases();
|
||||
|
||||
for (const dbInfo of databases) {
|
||||
if (dbInfo.name === "CunkebaoDatabase") {
|
||||
console.log("检测到旧版数据库,开始清理...");
|
||||
|
||||
// 打开数据库检查版本
|
||||
const openRequest = indexedDB.open(dbInfo.name);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
openRequest.onsuccess = async event => {
|
||||
const database = (event.target as IDBOpenDBRequest).result;
|
||||
const objectStoreNames = Array.from(database.objectStoreNames);
|
||||
|
||||
// 检查是否存在旧表
|
||||
const hasOldTables = objectStoreNames.some(name =>
|
||||
[
|
||||
"kfUsers",
|
||||
"weChatGroup",
|
||||
"contracts",
|
||||
"newContactList",
|
||||
"messageList",
|
||||
].includes(name),
|
||||
);
|
||||
|
||||
if (hasOldTables) {
|
||||
console.log("发现旧表,删除整个数据库:", objectStoreNames);
|
||||
database.close();
|
||||
|
||||
// 删除整个数据库
|
||||
const deleteRequest = indexedDB.deleteDatabase(dbInfo.name);
|
||||
deleteRequest.onsuccess = () => {
|
||||
console.log("旧数据库已删除");
|
||||
resolve();
|
||||
};
|
||||
deleteRequest.onerror = () => {
|
||||
console.error("删除旧数据库失败");
|
||||
reject();
|
||||
};
|
||||
} else {
|
||||
console.log("数据库结构正确,无需清理");
|
||||
database.close();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
openRequest.onerror = () => {
|
||||
console.error("无法打开数据库进行检查");
|
||||
reject();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
await initializeDatabaseFromPersistedUser();
|
||||
} catch (error) {
|
||||
console.warn("清理旧数据库时出错(可忽略):", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库初始化
|
||||
async function initializeApp() {
|
||||
try {
|
||||
// 1. 清理旧数据库
|
||||
await cleanupOldDatabase();
|
||||
|
||||
// 2. 打开新数据库
|
||||
await db.open();
|
||||
console.log("数据库初始化成功");
|
||||
|
||||
// 3. 开发环境清空数据(可选)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("开发环境:跳过数据清理");
|
||||
// 如需清空数据,取消下面的注释
|
||||
// await db.chatSessions.clear();
|
||||
// await db.contactsUnified.clear();
|
||||
// await db.contactLabelMap.clear();
|
||||
// await db.userLoginRecords.clear();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("数据库初始化失败:", error);
|
||||
console.warn("Failed to prepare database before app bootstrap:", error);
|
||||
}
|
||||
|
||||
// 渲染应用
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
root.render(
|
||||
<ConfigProvider locale={zhCN}>
|
||||
@@ -106,5 +27,4 @@ async function initializeApp() {
|
||||
);
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
initializeApp();
|
||||
void bootstrap();
|
||||
|
||||
@@ -35,28 +35,64 @@ export function updateConfig(params) {
|
||||
return request2("/api/WechatFriend/updateConfig", params, "PUT");
|
||||
}
|
||||
//获取聊天记录-2 获取列表
|
||||
export function getChatMessages(params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
wechatChatroomId?: number;
|
||||
From: number;
|
||||
To: number;
|
||||
Count: number;
|
||||
olderData: boolean;
|
||||
}) {
|
||||
return request2("/api/FriendMessage/SearchMessage", params, "GET");
|
||||
export interface messreocrParams {
|
||||
From?: number | string;
|
||||
To?: number | string;
|
||||
/**
|
||||
* 当前页码,从 1 开始
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* 每页条数
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* 群id
|
||||
*/
|
||||
wechatChatroomId?: number | string;
|
||||
/**
|
||||
* 好友id
|
||||
*/
|
||||
wechatFriendId?: number | string;
|
||||
/**
|
||||
* 微信账号ID
|
||||
*/
|
||||
wechatAccountId?: number | string;
|
||||
/**
|
||||
* 关键词、类型等扩展参数
|
||||
*/
|
||||
[property: string]: any;
|
||||
}
|
||||
export function getChatroomMessages(params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
wechatChatroomId?: number;
|
||||
From: number;
|
||||
To: number;
|
||||
Count: number;
|
||||
olderData: boolean;
|
||||
}) {
|
||||
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||
export function getChatMessages(params: messreocrParams) {
|
||||
return request("/v1/kefu/message/details", params, "GET");
|
||||
}
|
||||
export function getChatroomMessages(params: messreocrParams) {
|
||||
return request("/v1/kefu/message/details", params, "GET");
|
||||
}
|
||||
//=====================旧==============================
|
||||
|
||||
// export function getChatMessages(params: {
|
||||
// wechatAccountId: number;
|
||||
// wechatFriendId?: number;
|
||||
// wechatChatroomId?: number;
|
||||
// From: number;
|
||||
// To: number;
|
||||
// Count: number;
|
||||
// olderData: boolean;
|
||||
// }) {
|
||||
// return request2("/api/FriendMessage/SearchMessage", params, "GET");
|
||||
// }
|
||||
// export function getChatroomMessages(params: {
|
||||
// wechatAccountId: number;
|
||||
// wechatFriendId?: number;
|
||||
// wechatChatroomId?: number;
|
||||
// From: number;
|
||||
// To: number;
|
||||
// Count: number;
|
||||
// olderData: boolean;
|
||||
// }) {
|
||||
// return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||
// }
|
||||
|
||||
//获取群列表
|
||||
export function getGroupList(params: { prevId: number; count: number }) {
|
||||
|
||||
@@ -146,7 +146,7 @@ export interface ContractData {
|
||||
labels: string[];
|
||||
signature: string;
|
||||
accountId: number;
|
||||
extendFields: null;
|
||||
extendFields?: Record<string, any> | null;
|
||||
city?: string;
|
||||
lastUpdateTime: string;
|
||||
isPassed: boolean;
|
||||
|
||||
@@ -105,7 +105,7 @@ const PushTaskModal: React.FC<PushTaskModalProps> = ({
|
||||
};
|
||||
|
||||
const getSubtitle = () => {
|
||||
return "智能批量推送,AI智能话术改写";
|
||||
return "智能批量推送,AI智能话术改写";
|
||||
};
|
||||
|
||||
// 步骤2的标题
|
||||
|
||||
@@ -127,7 +127,7 @@ const CreatePushTask: React.FC = () => {
|
||||
}
|
||||
}, [validPushType]);
|
||||
|
||||
const subtitle = "智能批量推送,AI智能话术改写";
|
||||
const subtitle = "智能批量推送,AI智能话术改写";
|
||||
|
||||
const step2Title = useMemo(() => {
|
||||
switch (validPushType) {
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
export type PushType = "friend-message" | "group-message" | "group-announcement";
|
||||
export type PushType =
|
||||
| "friend-message"
|
||||
| "group-message"
|
||||
| "group-announcement";
|
||||
|
||||
const MessagePushAssistant: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -26,7 +29,9 @@ const MessagePushAssistant: React.FC = () => {
|
||||
icon: <UserOutlined />,
|
||||
color: "#1890ff",
|
||||
onClick: () => {
|
||||
navigate("/pc/powerCenter/message-push-assistant/create-push-task/friend-message");
|
||||
navigate(
|
||||
"/pc/powerCenter/message-push-assistant/create-push-task/friend-message",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -36,17 +41,21 @@ const MessagePushAssistant: React.FC = () => {
|
||||
icon: <MessageOutlined />,
|
||||
color: "#52c41a",
|
||||
onClick: () => {
|
||||
navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-message");
|
||||
navigate(
|
||||
"/pc/powerCenter/message-push-assistant/create-push-task/group-message",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "group-announcement",
|
||||
title: "群公告推送",
|
||||
description: "向选定的微信群发布群公告",
|
||||
description: "向选定的微信群批量发布群公告",
|
||||
icon: <SoundOutlined />,
|
||||
color: "#722ed1",
|
||||
onClick: () => {
|
||||
navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-announcement");
|
||||
navigate(
|
||||
"/pc/powerCenter/message-push-assistant/create-push-task/group-announcement",
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -81,7 +90,7 @@ const MessagePushAssistant: React.FC = () => {
|
||||
<div style={{ padding: "20px" }}>
|
||||
<PowerNavigation
|
||||
title="消息推送助手"
|
||||
subtitle="智能批量推送,AI智能话术改写"
|
||||
subtitle="智能批量推送,AI智能话术改写"
|
||||
showBackButton={true}
|
||||
backButtonText="返回"
|
||||
onBackClick={() => navigate("/pc/powerCenter")}
|
||||
|
||||
@@ -25,41 +25,13 @@ export interface GetPushHistoryResponse {
|
||||
/**
|
||||
* 获取推送历史列表
|
||||
*/
|
||||
export const getPushHistory = async (
|
||||
params: GetPushHistoryParams
|
||||
): Promise<GetPushHistoryResponse> => {
|
||||
try {
|
||||
// TODO: 替换为实际的API接口地址
|
||||
const response = await request.get("/api/push-history", { params });
|
||||
|
||||
// 如果接口返回的数据格式不同,需要在这里进行转换
|
||||
if (response.data && response.data.success !== undefined) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 兼容不同的响应格式
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
list: response.data?.list || response.data?.data || [],
|
||||
total: response.data?.total || 0,
|
||||
page: response.data?.page || params.page || 1,
|
||||
pageSize: response.data?.pageSize || params.pageSize || 10,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("获取推送历史失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: error?.message || "获取推送历史失败",
|
||||
};
|
||||
}
|
||||
export interface GetGroupPushHistoryParams {
|
||||
keyword?: string;
|
||||
limit: string;
|
||||
page: string;
|
||||
workbenchId?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
export const getPushHistory = async (params: GetGroupPushHistoryParams) => {
|
||||
return request("/v1/workbench/group-push-history", params, "GET");
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -76,18 +76,31 @@ const PushHistory: React.FC = () => {
|
||||
}
|
||||
|
||||
const response = await getPushHistory(params);
|
||||
const result = response?.data ?? response ?? {};
|
||||
|
||||
if (response.success) {
|
||||
setDataSource(response.data?.list || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: response.data?.page || page,
|
||||
total: response.data?.total || 0,
|
||||
}));
|
||||
} else {
|
||||
message.error(response.message || "获取推送历史失败");
|
||||
if (!result || typeof result !== "object") {
|
||||
message.error("获取推送历史失败");
|
||||
setDataSource([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const toNumber = (value: unknown, fallback: number) => {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
};
|
||||
|
||||
const list = Array.isArray(result.list) ? result.list : [];
|
||||
const total = toNumber(result.total, pagination.total);
|
||||
const currentPage = toNumber(result.page, page);
|
||||
const pageSize = toNumber(result.pageSize, pagination.pageSize);
|
||||
|
||||
setDataSource(list);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("获取推送历史失败:", error);
|
||||
message.error("获取推送历史失败,请稍后重试");
|
||||
@@ -211,9 +224,7 @@ const PushHistory: React.FC = () => {
|
||||
dataIndex: "pushContent",
|
||||
key: "pushContent",
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<span style={{ color: "#333" }}>{text}</span>
|
||||
),
|
||||
render: (text: string) => <span style={{ color: "#333" }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "目标数量",
|
||||
@@ -287,7 +298,9 @@ const PushHistory: React.FC = () => {
|
||||
subtitle="查看所有推送任务的历史记录"
|
||||
showBackButton={true}
|
||||
backButtonText="返回"
|
||||
onBackClick={() => navigate("/pc/powerCenter/message-push-assistant")}
|
||||
onBackClick={() =>
|
||||
navigate("/pc/powerCenter/message-push-assistant")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -369,11 +382,3 @@ const PushHistory: React.FC = () => {
|
||||
};
|
||||
|
||||
export default PushHistory;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export function WechatFriendAllot(params: {
|
||||
|
||||
//获取可转移客服列表
|
||||
export function getTransferableAgentList() {
|
||||
return request2("/api/account/myDepartmentAccountsForTransfer", {}, "GET");
|
||||
return request("/v1/kefu/accounts/list", {}, "GET");
|
||||
}
|
||||
|
||||
// 微信好友列表
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { useCurrentContact } from "@/store/module/weChat/weChat";
|
||||
import { ContactManager } from "@/utils/dbAction/contact";
|
||||
import { MessageManager } from "@/utils/dbAction/message";
|
||||
import { triggerRefresh } from "@/store/module/weChat/message";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
const { TextArea } = Input;
|
||||
@@ -49,7 +48,7 @@ const ToContract: React.FC<ToContractProps> = ({
|
||||
const openModal = () => {
|
||||
setVisible(true);
|
||||
getTransferableAgentList().then(data => {
|
||||
setCustomerServiceList(data);
|
||||
setCustomerServiceList(data.list);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -110,10 +109,7 @@ const ToContract: React.FC<ToContractProps> = ({
|
||||
await ContactManager.deleteContact(currentContact.id);
|
||||
console.log("✅ 已从联系人数据库删除");
|
||||
|
||||
// 3. 触发会话列表刷新
|
||||
triggerRefresh();
|
||||
|
||||
// 4. 清空当前选中的联系人(关闭聊天窗口)
|
||||
// 3. 清空当前选中的联系人(关闭聊天窗口)
|
||||
clearCurrentContact();
|
||||
|
||||
message.success("转接成功,已清理本地数据");
|
||||
@@ -167,10 +163,7 @@ const ToContract: React.FC<ToContractProps> = ({
|
||||
await ContactManager.deleteContact(currentContact.id);
|
||||
console.log("✅ 已从联系人数据库删除");
|
||||
|
||||
// 3. 触发会话列表刷新
|
||||
triggerRefresh();
|
||||
|
||||
// 4. 清空当前选中的联系人(关闭聊天窗口)
|
||||
// 3. 清空当前选中的联系人(关闭聊天窗口)
|
||||
clearCurrentContact();
|
||||
|
||||
message.success("转回成功,已清理本地数据");
|
||||
|
||||
@@ -167,27 +167,11 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
||||
// AI 消息处理
|
||||
useEffect(() => {
|
||||
if (quoteMessageContent) {
|
||||
console.log(
|
||||
"🤖 AI消息到达 - aiQuoteMessageContent:",
|
||||
aiQuoteMessageContent,
|
||||
);
|
||||
|
||||
// 检查:如果用户输入框已有内容(且不是之前的AI内容),不覆盖
|
||||
if (inputValue && inputValue !== quoteMessageContent) {
|
||||
console.log("⚠️ 用户正在输入,不覆盖输入内容");
|
||||
updateQuoteMessageContent(""); // 清空AI回复
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAiAssist) {
|
||||
// AI辅助模式:填充到输入框,等待人工确认
|
||||
console.log("✨ AI辅助模式:填充消息到输入框");
|
||||
setInputValue(quoteMessageContent);
|
||||
}
|
||||
|
||||
if (isAiTakeover) {
|
||||
// AI接管模式:直接发送消息(传入内容,避免 state 闭包问题)
|
||||
console.log("🚀 AI接管模式:自动发送消息");
|
||||
handleSend(quoteMessageContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
const [selectedRecords, setSelectedRecords] = useState<ChatRecord[]>([]);
|
||||
|
||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
||||
const currentMessagesHasMore = useWeChatStore(
|
||||
state => state.currentMessagesHasMore,
|
||||
);
|
||||
|
||||
const loadChatMessages = useWeChatStore(state => state.loadChatMessages);
|
||||
const messagesLoading = useWeChatStore(state => state.messagesLoading);
|
||||
@@ -552,8 +555,14 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
};
|
||||
|
||||
// 用于分组消息并添加时间戳的辅助函数
|
||||
const groupMessagesByTime = (messages: ChatRecord[]) => {
|
||||
return messages
|
||||
const groupMessagesByTime = (messages: ChatRecord[] | null | undefined) => {
|
||||
const safeMessages = Array.isArray(messages)
|
||||
? messages
|
||||
: Array.isArray((messages as any)?.list)
|
||||
? ((messages as any).list as ChatRecord[])
|
||||
: [];
|
||||
|
||||
return safeMessages
|
||||
.filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息
|
||||
.map(msg => ({
|
||||
time: formatWechatTime(String(msg?.wechatTime)),
|
||||
@@ -639,7 +648,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isOwn && (
|
||||
{!!isOwn && (
|
||||
<>
|
||||
{/* Checkbox 显示控制 */}
|
||||
{showCheckbox && (
|
||||
@@ -650,7 +659,6 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Avatar
|
||||
size={32}
|
||||
src={currentCustomer?.avatar || ""}
|
||||
@@ -680,33 +688,10 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
);
|
||||
};
|
||||
const loadMoreMessages = () => {
|
||||
// 兼容性处理:检查消息数组和时间戳
|
||||
if (!currentMessages || currentMessages.length === 0) {
|
||||
console.warn("No messages available for loading more");
|
||||
if (messagesLoading || !currentMessagesHasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMessage = currentMessages[0];
|
||||
if (!firstMessage || !firstMessage.createTime) {
|
||||
console.warn("Invalid message or createTime");
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容性处理:确保时间戳格式正确
|
||||
let timestamp;
|
||||
try {
|
||||
const date = new Date(firstMessage.createTime);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn("Invalid createTime format:", firstMessage.createTime);
|
||||
return;
|
||||
}
|
||||
timestamp = date.getTime() - 24 * 36000 * 1000;
|
||||
} catch (error) {
|
||||
console.error("Error parsing createTime:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
loadChatMessages(false, timestamp);
|
||||
loadChatMessages(false);
|
||||
};
|
||||
|
||||
const handleForwardMessage = (messageData: ChatRecord) => {
|
||||
@@ -785,8 +770,17 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
|
||||
return (
|
||||
<div className={styles.messagesContainer}>
|
||||
<div className={styles.loadMore} onClick={() => loadMoreMessages()}>
|
||||
点击加载更早的信息 {messagesLoading ? <LoadingOutlined /> : ""}
|
||||
<div
|
||||
className={styles.loadMore}
|
||||
onClick={() => loadMoreMessages()}
|
||||
style={{
|
||||
cursor:
|
||||
currentMessagesHasMore && !messagesLoading ? "pointer" : "default",
|
||||
opacity: currentMessagesHasMore ? 1 : 0.6,
|
||||
}}
|
||||
>
|
||||
{currentMessagesHasMore ? "点击加载更早的信息" : "已经没有更早的消息了"}
|
||||
{messagesLoading ? <LoadingOutlined /> : ""}
|
||||
</div>
|
||||
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
||||
<React.Fragment key={`group-${groupIndex}`}>
|
||||
|
||||
@@ -4,3 +4,51 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tabHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.tabItem {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tabItem:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tabItem:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.tabItemActive {
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tabUnderline {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: transparent;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.tabItemActive .tabUnderline {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
@@ -197,6 +197,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.footerActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.profileSider {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import request from "@/api/request";
|
||||
// 更新好友信息
|
||||
export interface UpdateFriendInfoParams {
|
||||
id: number;
|
||||
phone: string;
|
||||
company: string;
|
||||
name: string;
|
||||
position: string;
|
||||
email: string;
|
||||
address: string;
|
||||
qq: string;
|
||||
remark: string;
|
||||
}
|
||||
export function updateFriendInfo(params: UpdateFriendInfoParams): Promise<any> {
|
||||
return request("/v1/kefu/wechatFriend/updateInfo", params, "POST");
|
||||
}
|
||||
|
||||
// 更新本地数据库中的好友信息
|
||||
export interface UpdateLocalDBParams {
|
||||
wechatFriendId: number;
|
||||
extendFields: string;
|
||||
updateConversation?: boolean; // 是否同时更新会话列表
|
||||
}
|
||||
|
||||
export function updateLocalDBFriendInfo(
|
||||
params: UpdateLocalDBParams,
|
||||
): Promise<any> {
|
||||
return request("/v1/kefu/wechatFriend/updateLocalDB", params, "POST");
|
||||
}
|
||||
// 获取好友信息
|
||||
export interface GetFriendInfoParams {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface FriendDetailResponse {
|
||||
detail: {
|
||||
id: number;
|
||||
wechatAccountId: number;
|
||||
alias: string;
|
||||
wechatId: string;
|
||||
conRemark: string;
|
||||
nickname: string;
|
||||
pyInitial: string;
|
||||
quanPin: string;
|
||||
avatar: string;
|
||||
gender: number;
|
||||
region: string;
|
||||
addFrom: number;
|
||||
labels: any[];
|
||||
siteLabels: string[];
|
||||
signature: string;
|
||||
isDeleted: number;
|
||||
isPassed: number;
|
||||
deleteTime: number;
|
||||
accountId: number;
|
||||
extendFields: string;
|
||||
accountUserName: string;
|
||||
accountRealName: string;
|
||||
accountNickname: string;
|
||||
ownerAlias: string;
|
||||
ownerWechatId: string;
|
||||
ownerNickname: string;
|
||||
ownerAvatar: string;
|
||||
phone: string;
|
||||
thirdParty: string;
|
||||
groupId: number;
|
||||
passTime: string;
|
||||
additionalPicture: string;
|
||||
desc: string;
|
||||
country: string;
|
||||
privince: string;
|
||||
city: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
R: string;
|
||||
F: string;
|
||||
M: string;
|
||||
realName: null | string;
|
||||
company: null | string;
|
||||
position: null | string;
|
||||
aiType: number;
|
||||
};
|
||||
}
|
||||
export function getFriendInfo(
|
||||
params: GetFriendInfoParams,
|
||||
): Promise<FriendDetailResponse> {
|
||||
return request("/v1/kefu/wechatFriend/detail", params, "GET");
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { Input, message } from "antd";
|
||||
import { Button } from "antd-mobile";
|
||||
import { EditOutlined } from "@ant-design/icons";
|
||||
import { updateFriendInfo, UpdateFriendInfoParams } from "../api";
|
||||
|
||||
import styles from "../Person.module.scss";
|
||||
|
||||
export interface DetailValueField {
|
||||
label: string;
|
||||
key: string;
|
||||
ifEdit?: boolean;
|
||||
placeholder?: string;
|
||||
type?: "text" | "textarea";
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export interface DetailValueProps {
|
||||
fields: DetailValueField[];
|
||||
value?: Record<string, string>;
|
||||
onChange?: (next: Record<string, string>) => void;
|
||||
onSubmit?: (next: Record<string, string>, changedKeys: string[]) => void;
|
||||
submitText?: string;
|
||||
submitting?: boolean;
|
||||
renderFooter?: React.ReactNode;
|
||||
saveHandler?: (
|
||||
values: Record<string, string>,
|
||||
changedKeys: string[],
|
||||
) => Promise<void>;
|
||||
onSaveSuccess?: (
|
||||
values: Record<string, string>,
|
||||
changedKeys: string[],
|
||||
) => void;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
const DetailValue: React.FC<DetailValueProps> = ({
|
||||
fields,
|
||||
value = {},
|
||||
onChange,
|
||||
onSubmit,
|
||||
submitText = "保存",
|
||||
submitting = false,
|
||||
renderFooter,
|
||||
saveHandler,
|
||||
onSaveSuccess,
|
||||
isGroup = false,
|
||||
}) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [editingFields, setEditingFields] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [fieldValues, setFieldValues] = useState<Record<string, string>>(value);
|
||||
|
||||
const [originalValues, setOriginalValues] =
|
||||
useState<Record<string, string>>(value);
|
||||
const [changedKeys, setChangedKeys] = useState<string[]>([]);
|
||||
|
||||
// 当外部value变化时,更新内部状态
|
||||
useEffect(() => {
|
||||
setFieldValues(value);
|
||||
setOriginalValues(value);
|
||||
setChangedKeys([]);
|
||||
// 重置所有编辑状态
|
||||
const newEditingFields: Record<string, boolean> = {};
|
||||
fields.forEach(field => {
|
||||
newEditingFields[field.key] = false;
|
||||
});
|
||||
setEditingFields(newEditingFields);
|
||||
}, [value, fields]);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(fieldKey: string, nextVal: string) => {
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: nextVal,
|
||||
}));
|
||||
|
||||
// 检查值是否发生变化,更新changedKeys
|
||||
if (nextVal !== originalValues[fieldKey]) {
|
||||
if (!changedKeys.includes(fieldKey)) {
|
||||
setChangedKeys(prev => [...prev, fieldKey]);
|
||||
}
|
||||
} else {
|
||||
// 如果值恢复到原始值,从changedKeys中移除
|
||||
setChangedKeys(prev => prev.filter(key => key !== fieldKey));
|
||||
}
|
||||
|
||||
// 调用外部onChange,但不触发自动保存
|
||||
if (onChange) {
|
||||
onChange({
|
||||
...fieldValues,
|
||||
[fieldKey]: nextVal,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onChange, fieldValues, originalValues, changedKeys],
|
||||
);
|
||||
|
||||
const handleEditField = useCallback((fieldKey: string) => {
|
||||
setEditingFields(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleCancelEdit = useCallback(
|
||||
(fieldKey: string) => {
|
||||
// 恢复原始值
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: originalValues[fieldKey] || "",
|
||||
}));
|
||||
|
||||
// 从changedKeys中移除
|
||||
setChangedKeys(prev => prev.filter(key => key !== fieldKey));
|
||||
|
||||
// 关闭编辑状态
|
||||
setEditingFields(prev => ({
|
||||
...prev,
|
||||
[fieldKey]: false,
|
||||
}));
|
||||
},
|
||||
[originalValues],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (changedKeys.length === 0) {
|
||||
messageApi.info("没有需要保存的更改");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isGroup) {
|
||||
// 群组信息使用传入的saveHandler
|
||||
if (saveHandler) {
|
||||
await saveHandler(fieldValues, changedKeys);
|
||||
} else {
|
||||
onSubmit?.(fieldValues, changedKeys);
|
||||
}
|
||||
} else {
|
||||
// 个人资料信息处理
|
||||
if (changedKeys.includes("conRemark")) {
|
||||
// 微信备注是特例,使用WebSocket更新
|
||||
if (saveHandler) {
|
||||
await saveHandler(fieldValues, changedKeys);
|
||||
} else {
|
||||
onSubmit?.(fieldValues, changedKeys);
|
||||
}
|
||||
} else {
|
||||
// 其他个人资料信息使用updateFriendInfo API
|
||||
const params: UpdateFriendInfoParams = {
|
||||
id: Number(value.id) || 0,
|
||||
phone: fieldValues.phone || "",
|
||||
company: fieldValues.company || "",
|
||||
name: fieldValues.name || "",
|
||||
position: fieldValues.position || "",
|
||||
email: fieldValues.email || "",
|
||||
address: fieldValues.address || "",
|
||||
qq: fieldValues.qq || "",
|
||||
remark: fieldValues.remark || "",
|
||||
};
|
||||
await updateFriendInfo(params);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新原始值
|
||||
setOriginalValues(fieldValues);
|
||||
// 清空changedKeys
|
||||
setChangedKeys([]);
|
||||
// 关闭所有编辑状态
|
||||
const newEditingFields: Record<string, boolean> = {};
|
||||
fields.forEach(field => {
|
||||
newEditingFields[field.key] = false;
|
||||
});
|
||||
setEditingFields(newEditingFields);
|
||||
// 调用保存成功回调
|
||||
onSaveSuccess?.(fieldValues, changedKeys);
|
||||
messageApi.success("保存成功");
|
||||
} catch (error) {
|
||||
messageApi.error("保存失败");
|
||||
console.error("保存失败:", error);
|
||||
}
|
||||
}, [
|
||||
onSubmit,
|
||||
saveHandler,
|
||||
onSaveSuccess,
|
||||
fieldValues,
|
||||
changedKeys,
|
||||
fields,
|
||||
messageApi,
|
||||
isGroup,
|
||||
value.id,
|
||||
]);
|
||||
|
||||
const isEditing = Object.values(editingFields).some(Boolean);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
{fields.map(field => {
|
||||
const disabled = field.ifEdit === false;
|
||||
const fieldValue = fieldValues[field.key] ?? "";
|
||||
const isFieldEditing = editingFields[field.key];
|
||||
const InputComponent =
|
||||
field.type === "textarea" ? Input.TextArea : Input;
|
||||
|
||||
return (
|
||||
<div key={field.key} className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>{field.label}:</span>
|
||||
<div className={styles.infoValue}>
|
||||
{disabled ? (
|
||||
<span>{fieldValue || field.placeholder || ""}</span>
|
||||
) : isFieldEditing ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<InputComponent
|
||||
value={fieldValue}
|
||||
placeholder={field.placeholder}
|
||||
onChange={event =>
|
||||
handleFieldChange(field.key, event.target.value)
|
||||
}
|
||||
onPressEnter={undefined}
|
||||
autoFocus
|
||||
rows={field.type === "textarea" ? 4 : undefined}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleCancelEdit(field.key)}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" color="primary" onClick={handleSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
padding: "4px 8px",
|
||||
borderRadius: 4,
|
||||
border: "1px solid transparent",
|
||||
transition: "all 0.3s",
|
||||
width: "100%",
|
||||
}}
|
||||
onClick={() => handleEditField(field.key)}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.backgroundColor = "#f5f5f5";
|
||||
e.currentTarget.style.borderColor = "#d9d9d9";
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
e.currentTarget.style.borderColor = "transparent";
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace:
|
||||
field.type === "textarea" ? "pre-wrap" : "nowrap",
|
||||
}}
|
||||
>
|
||||
{fieldValue || field.placeholder || ""}
|
||||
</span>
|
||||
<EditOutlined style={{ color: "#1890ff", marginLeft: 8 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{(onSubmit || renderFooter) && !isEditing && changedKeys.length > 0 && (
|
||||
<div className={styles.footerActions}>
|
||||
{renderFooter}
|
||||
<Button
|
||||
loading={submitting}
|
||||
onClick={handleSubmit}
|
||||
style={{ marginLeft: renderFooter ? 8 : 0 }}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailValue;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ const GroupModal: React.FC<GroupModalProps> = ({
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
|
||||
@@ -126,7 +126,7 @@ const QuickReplyModal: React.FC<QuickReplyModalProps> = ({
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
|
||||
@@ -39,6 +39,7 @@ import QuickReplyModal from "./components/QuickReplyModal";
|
||||
import GroupModal from "./components/GroupModal";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
import { ChatRecord } from "@/pages/pc/ckbox/data";
|
||||
|
||||
// 消息类型枚举
|
||||
export enum MessageType {
|
||||
@@ -82,10 +83,12 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
|
||||
state => state.updateQuoteMessageContent,
|
||||
);
|
||||
const currentContract = useWeChatStore(state => state.currentContract);
|
||||
const addMessage = useWeChatStore(state => state.addMessage);
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
|
||||
const sendQuickReplyNow = (reply: QuickWordsReply) => {
|
||||
if (!currentContract) return;
|
||||
const messageId = Date.now();
|
||||
const params = {
|
||||
wechatAccountId: currentContract.wechatAccountId,
|
||||
wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0,
|
||||
@@ -93,7 +96,35 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
|
||||
msgSubType: 0,
|
||||
msgType: reply.msgType,
|
||||
content: reply.content,
|
||||
seq: messageId,
|
||||
} as any;
|
||||
|
||||
if (reply.msgType !== MessageType.TEXT) {
|
||||
const localMessage: ChatRecord = {
|
||||
id: messageId,
|
||||
wechatAccountId: params.wechatAccountId,
|
||||
wechatFriendId: params.wechatFriendId,
|
||||
wechatChatroomId: params.wechatChatroomId,
|
||||
tenantId: 0,
|
||||
accountId: 0,
|
||||
synergyAccountId: 0,
|
||||
content: params.content,
|
||||
msgType: reply.msgType,
|
||||
msgSubType: params.msgSubType,
|
||||
msgSvrId: "",
|
||||
isSend: true,
|
||||
createTime: new Date().toISOString(),
|
||||
isDeleted: false,
|
||||
deleteTime: "",
|
||||
sendStatus: 1,
|
||||
wechatTime: Date.now(),
|
||||
origin: 0,
|
||||
msgId: 0,
|
||||
recalled: false,
|
||||
seq: messageId,
|
||||
};
|
||||
addMessage(localMessage);
|
||||
}
|
||||
sendCommand("CmdSendMessage", params);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { Layout, Tabs } from "antd";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Layout } from "antd";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./Person.module.scss";
|
||||
import ProfileModules from "./components/ProfileModules";
|
||||
@@ -9,6 +9,8 @@ import LayoutFiexd from "@/components/Layout/LayoutFiexd";
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
interface PersonProps {
|
||||
contract: ContractData | weChatGroup;
|
||||
}
|
||||
@@ -16,50 +18,113 @@ interface PersonProps {
|
||||
const Person: React.FC<PersonProps> = ({ contract }) => {
|
||||
const [activeKey, setActiveKey] = useState("profile");
|
||||
const isGroup = "chatroomId" in contract;
|
||||
// 使用state保存当前contract的副本,确保在切换tab时不会丢失修改
|
||||
const [currentContract, setCurrentContract] = useState<
|
||||
ContractData | weChatGroup
|
||||
>(contract);
|
||||
|
||||
// 当外部contract变化时,更新内部状态
|
||||
useEffect(() => {
|
||||
setCurrentContract(contract);
|
||||
}, [contract]);
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
const baseItems = [
|
||||
{
|
||||
key: "quickwords",
|
||||
label: "快捷语录",
|
||||
children: <QuickWords onInsert={noop} />,
|
||||
},
|
||||
{
|
||||
key: "profile",
|
||||
label: isGroup ? "群资料" : "个人资料",
|
||||
children: <ProfileModules contract={currentContract} />,
|
||||
},
|
||||
];
|
||||
if (!isGroup) {
|
||||
baseItems.push({
|
||||
key: "moments",
|
||||
label: "朋友圈",
|
||||
children: <FriendsCircle wechatFriendId={currentContract.id} />,
|
||||
});
|
||||
}
|
||||
return baseItems;
|
||||
}, [currentContract, isGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveKey("profile");
|
||||
setRenderedKeys(["profile"]);
|
||||
}, [contract]);
|
||||
|
||||
const tabHeaderItems = useMemo(
|
||||
() => tabItems.map(({ key, label }) => ({ key, label })),
|
||||
[tabItems],
|
||||
);
|
||||
|
||||
const availableKeys = useMemo(
|
||||
() => tabItems.map(item => item.key),
|
||||
[tabItems],
|
||||
);
|
||||
|
||||
const [renderedKeys, setRenderedKeys] = useState<string[]>(() => ["profile"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableKeys.includes(activeKey) && availableKeys.length > 0) {
|
||||
setActiveKey(availableKeys[0]);
|
||||
}
|
||||
}, [activeKey, availableKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderedKeys(keys => {
|
||||
const filtered = keys.filter(key => availableKeys.includes(key));
|
||||
if (!filtered.includes(activeKey)) {
|
||||
filtered.push(activeKey);
|
||||
}
|
||||
const isSameLength = filtered.length === keys.length;
|
||||
const isSameOrder =
|
||||
isSameLength && filtered.every((key, index) => key === keys[index]);
|
||||
return isSameOrder ? keys : filtered;
|
||||
});
|
||||
}, [activeKey, availableKeys]);
|
||||
|
||||
return (
|
||||
<Sider width={330} className={styles.profileSider}>
|
||||
<LayoutFiexd
|
||||
header={
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={key => setActiveKey(key)}
|
||||
tabBarStyle={{
|
||||
padding: "0 30px",
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
key: "quickwords",
|
||||
label: "快捷语录",
|
||||
},
|
||||
{
|
||||
key: "profile",
|
||||
label: isGroup ? "群资料" : "个人资料",
|
||||
},
|
||||
|
||||
...(!isGroup
|
||||
? [
|
||||
{
|
||||
key: "moments",
|
||||
label: "朋友圈",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
<div className={styles.tabHeader}>
|
||||
{tabHeaderItems.map(({ key, label }) => {
|
||||
const isActive = key === activeKey;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`${styles.tabItem}${
|
||||
isActive ? ` ${styles.tabItemActive}` : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setActiveKey(key);
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<div className={styles.tabUnderline} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activeKey === "profile" && <ProfileModules contract={contract} />}
|
||||
{activeKey === "quickwords" && (
|
||||
<QuickWords
|
||||
words={[]}
|
||||
onInsert={() => {}}
|
||||
onAdd={() => {}}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
)}
|
||||
{activeKey === "moments" && !isGroup && (
|
||||
<FriendsCircle wechatFriendId={contract.id} />
|
||||
)}
|
||||
{renderedKeys.map(key => {
|
||||
const item = tabItems.find(tab => tab.key === key);
|
||||
if (!item) return null;
|
||||
const isActive = key === activeKey;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{ display: isActive ? "block" : "none", height: "100%" }}
|
||||
>
|
||||
{item.children}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</LayoutFiexd>
|
||||
</Sider>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ContractData {
|
||||
labels: string[];
|
||||
signature: string;
|
||||
accountId: number;
|
||||
extendFields: null;
|
||||
extendFields?: Record<string, any> | null;
|
||||
city?: string;
|
||||
lastUpdateTime: string;
|
||||
isPassed: boolean;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getWechatFriendDetail,
|
||||
getWechatChatroomDetail,
|
||||
} from "./api";
|
||||
import { useMessageStore, triggerRefresh } from "@weChatStore/message";
|
||||
import { useMessageStore } from "@weChatStore/message";
|
||||
import { useWebSocketStore } from "@storeModule/websocket/websocket";
|
||||
import { useCustomerStore } from "@weChatStore/customer";
|
||||
import { useContactStore } from "@weChatStore/contacts";
|
||||
@@ -39,14 +39,12 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
// Store状态
|
||||
const {
|
||||
loading,
|
||||
refreshTrigger,
|
||||
hasLoadedOnce,
|
||||
setLoading,
|
||||
setHasLoadedOnce,
|
||||
sessions,
|
||||
setSessions: setSessionState,
|
||||
} = useMessageStore();
|
||||
|
||||
// 组件内部状态:会话列表数据
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([]);
|
||||
|
||||
// 右键菜单相关状态
|
||||
@@ -74,6 +72,8 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
});
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||
const previousUserIdRef = useRef<number | null>(null);
|
||||
const loadRequestRef = useRef(0);
|
||||
|
||||
// 右键菜单事件处理
|
||||
const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => {
|
||||
@@ -105,7 +105,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
|
||||
try {
|
||||
// 1. 立即更新UI并重新排序(乐观更新)
|
||||
setSessions(prev => {
|
||||
setSessionState(prev => {
|
||||
const updatedSessions = prev.map(s =>
|
||||
s.id === session.id
|
||||
? {
|
||||
@@ -141,7 +141,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
message.success(`${newPinned === 1 ? "置顶" : "取消置顶"}成功`);
|
||||
} catch (error) {
|
||||
// 4. 失败时回滚UI
|
||||
setSessions(prev =>
|
||||
setSessionState(prev =>
|
||||
prev.map(s =>
|
||||
s.id === session.id
|
||||
? { ...s, config: { ...s.config, top: currentPinned } }
|
||||
@@ -162,7 +162,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
onOk: async () => {
|
||||
try {
|
||||
// 1. 立即从UI移除
|
||||
setSessions(prev => prev.filter(s => s.id !== session.id));
|
||||
setSessionState(prev => prev.filter(s => s.id !== session.id));
|
||||
|
||||
// 2. 后台调用API
|
||||
await updateConfig({
|
||||
@@ -180,7 +180,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
message.success("删除成功");
|
||||
} catch (error) {
|
||||
// 4. 失败时恢复UI
|
||||
setSessions(prev => [...prev, session]);
|
||||
setSessionState(prev => [...prev, session]);
|
||||
message.error("删除失败");
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
|
||||
try {
|
||||
// 1. 立即更新UI
|
||||
setSessions(prev =>
|
||||
setSessionState(prev =>
|
||||
prev.map(s =>
|
||||
s.id === session.id ? { ...s, conRemark: editRemarkModal.remark } : s,
|
||||
),
|
||||
@@ -258,7 +258,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
message.success("备注更新成功");
|
||||
} catch (error) {
|
||||
// 4. 失败时回滚UI
|
||||
setSessions(prev =>
|
||||
setSessionState(prev =>
|
||||
prev.map(s =>
|
||||
s.id === session.id ? { ...s, conRemark: oldRemark } : s,
|
||||
),
|
||||
@@ -343,9 +343,6 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
};
|
||||
});
|
||||
|
||||
console.log("群聊数据示例:", groups[0]); // 调试:查看第一个群聊数据
|
||||
console.log("好友数据示例:", friends[0]); // 调试:查看第一个好友数据
|
||||
|
||||
// 执行增量同步
|
||||
const syncResult = await MessageManager.syncSessions(currentUserId, {
|
||||
friends,
|
||||
@@ -360,112 +357,93 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
`会话同步完成: 新增${syncResult.added}, 更新${syncResult.updated}, 删除${syncResult.deleted}`,
|
||||
);
|
||||
|
||||
// 如果有数据变更,触发UI刷新
|
||||
if (
|
||||
syncResult.added > 0 ||
|
||||
syncResult.updated > 0 ||
|
||||
syncResult.deleted > 0
|
||||
) {
|
||||
triggerRefresh();
|
||||
}
|
||||
// 会话管理器会在有变更时触发订阅回调
|
||||
} catch (error) {
|
||||
console.error("同步服务器数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换账号时重置加载状态
|
||||
useEffect(() => {
|
||||
if (!currentUserId) return;
|
||||
if (previousUserIdRef.current === currentUserId) return;
|
||||
previousUserIdRef.current = currentUserId;
|
||||
setHasLoadedOnce(false);
|
||||
setSessionState([]);
|
||||
}, [currentUserId, setHasLoadedOnce, setSessionState]);
|
||||
|
||||
// 初始化加载会话列表
|
||||
useEffect(() => {
|
||||
if (!currentUserId || currentUserId === 0) {
|
||||
console.warn("currentUserId 无效,跳过加载:", currentUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
const requestId = ++loadRequestRef.current;
|
||||
|
||||
const initializeSessions = async () => {
|
||||
if (!currentUserId || currentUserId === 0) {
|
||||
console.warn("currentUserId 无效,跳过加载:", currentUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经加载过一次,只从本地数据库读取,不请求接口
|
||||
if (hasLoadedOnce) {
|
||||
console.log("已加载过,只从本地数据库读取");
|
||||
setLoading(true); // 显示骨架屏
|
||||
|
||||
try {
|
||||
const cachedSessions =
|
||||
await MessageManager.getUserSessions(currentUserId);
|
||||
console.log("从本地加载会话数:", cachedSessions.length);
|
||||
|
||||
// 如果本地数据为空,重置 hasLoadedOnce 并重新加载
|
||||
if (cachedSessions.length === 0) {
|
||||
console.warn("本地数据为空,重置加载状态并重新加载");
|
||||
setHasLoadedOnce(false);
|
||||
// 不 return,继续执行下面的首次加载逻辑
|
||||
} else {
|
||||
setSessions(cachedSessions);
|
||||
setLoading(false); // 数据加载完成,关闭骨架屏
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("从本地加载会话列表失败:", error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("首次加载,开始初始化...");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. 优先从本地数据库加载
|
||||
const cachedSessions =
|
||||
await MessageManager.getUserSessions(currentUserId);
|
||||
|
||||
console.log("本地缓存会话数:", cachedSessions.length);
|
||||
if (isCancelled || loadRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedSessions.length > 0) {
|
||||
// 有缓存数据,立即显示
|
||||
console.log("有缓存数据,立即显示");
|
||||
setSessions(cachedSessions);
|
||||
setLoading(false);
|
||||
setSessionState(cachedSessions);
|
||||
}
|
||||
|
||||
// 2. 后台静默同步(不显示同步提示)
|
||||
console.log("后台静默同步中...");
|
||||
const needsFullSync = cachedSessions.length === 0 || !hasLoadedOnce;
|
||||
|
||||
if (needsFullSync) {
|
||||
await syncWithServer();
|
||||
setHasLoadedOnce(true); // 标记已加载过
|
||||
console.log("同步完成");
|
||||
if (isCancelled || loadRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setHasLoadedOnce(true);
|
||||
} else {
|
||||
// 无缓存,直接API加载
|
||||
console.log("无缓存,从服务器加载...");
|
||||
await syncWithServer();
|
||||
const newSessions =
|
||||
await MessageManager.getUserSessions(currentUserId);
|
||||
console.log("从服务器加载会话数:", newSessions.length);
|
||||
setSessions(newSessions);
|
||||
setLoading(false);
|
||||
setHasLoadedOnce(true); // 标记已加载过
|
||||
syncWithServer().catch(error => {
|
||||
console.error("后台同步失败:", error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("初始化会话列表失败:", error);
|
||||
setLoading(false);
|
||||
if (!isCancelled) {
|
||||
console.error("初始化会话列表失败:", error);
|
||||
}
|
||||
} finally {
|
||||
if (!isCancelled && loadRequestRef.current === requestId) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeSessions();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentUserId]);
|
||||
|
||||
// 监听refreshTrigger,重新查询数据库
|
||||
// 订阅数据库变更,自动更新Store
|
||||
useEffect(() => {
|
||||
const refreshSessions = async () => {
|
||||
if (!currentUserId || refreshTrigger === 0) return;
|
||||
if (!currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedSessions =
|
||||
await MessageManager.getUserSessions(currentUserId);
|
||||
setSessions(updatedSessions);
|
||||
} catch (error) {
|
||||
console.error("刷新会话列表失败:", error);
|
||||
}
|
||||
};
|
||||
const unsubscribe = MessageManager.onSessionsUpdate(
|
||||
({ userId: ownerId, sessions: updatedSessions }) => {
|
||||
if (ownerId !== currentUserId) return;
|
||||
setSessionState(updatedSessions);
|
||||
},
|
||||
);
|
||||
|
||||
refreshSessions();
|
||||
}, [refreshTrigger, currentUserId]);
|
||||
return unsubscribe;
|
||||
}, [currentUserId, setSessionState]);
|
||||
|
||||
// 根据客服和搜索关键词筛选会话
|
||||
useEffect(() => {
|
||||
@@ -655,6 +633,8 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
top: 0,
|
||||
},
|
||||
sortKey: "",
|
||||
phone: msgData.phone || "",
|
||||
region: msgData.region || "",
|
||||
};
|
||||
|
||||
await MessageManager.addSession(newSession);
|
||||
@@ -681,6 +661,8 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
top: 0,
|
||||
},
|
||||
sortKey: "",
|
||||
phone: msgData.phone || "",
|
||||
region: msgData.region || "",
|
||||
};
|
||||
|
||||
await MessageManager.addSession(newSession);
|
||||
@@ -688,8 +670,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 触发静默刷新:通知组件从数据库重新查询
|
||||
triggerRefresh();
|
||||
// MessageManager 的回调会自动把最新数据发给 Store
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
@@ -710,7 +691,6 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
// 点击会话
|
||||
const onContactClick = async (session: ChatSession) => {
|
||||
console.log("onContactClick", session);
|
||||
console.log("session.aiType:", session.aiType); // 调试:查看 aiType 字段
|
||||
|
||||
// 设置当前会话
|
||||
setCurrentContact(session as any);
|
||||
@@ -718,7 +698,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
// 标记为已读(不更新时间和排序)
|
||||
if (session.config.unreadCount > 0) {
|
||||
// 立即更新UI(只更新未读数量)
|
||||
setSessions(prev =>
|
||||
setSessionState(prev =>
|
||||
prev.map(s =>
|
||||
s.id === session.id
|
||||
? { ...s, config: { ...s.config, unreadCount: 0 } }
|
||||
|
||||
@@ -82,6 +82,20 @@ export const getAllGroups = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const serializeExtendFields = (value: any) => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim() ? value : "{}";
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (error) {
|
||||
console.warn("序列化 extendFields 失败:", error);
|
||||
}
|
||||
}
|
||||
return "{}";
|
||||
};
|
||||
|
||||
/**
|
||||
* 将好友数据转换为统一的 Contact 格式
|
||||
*/
|
||||
@@ -95,11 +109,21 @@ export const convertFriendsToContacts = (
|
||||
id: friend.id,
|
||||
type: "friend" as const,
|
||||
wechatAccountId: friend.wechatAccountId,
|
||||
wechatFriendId: friend.id,
|
||||
wechatId: friend.wechatId,
|
||||
nickname: friend.nickname || "",
|
||||
conRemark: friend.conRemark || "",
|
||||
avatar: friend.avatar || "",
|
||||
alias: friend.alias || "",
|
||||
gender: friend.gender,
|
||||
aiType: friend.aiType ?? 0,
|
||||
phone: friend.phone ?? "",
|
||||
region: friend.region ?? "",
|
||||
quanPin: friend.quanPin || "",
|
||||
signature: friend.signature || "",
|
||||
config: friend.config || {},
|
||||
groupId: friend.groupId, // 保留标签ID
|
||||
extendFields: serializeExtendFields(friend.extendFields),
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
sortKey: "",
|
||||
searchKey: "",
|
||||
@@ -120,10 +144,19 @@ export const convertGroupsToContacts = (
|
||||
type: "group" as const,
|
||||
wechatAccountId: group.wechatAccountId,
|
||||
wechatId: group.chatroomId || "",
|
||||
chatroomId: group.chatroomId || "",
|
||||
chatroomOwner: group.chatroomOwner || "",
|
||||
nickname: group.nickname || "",
|
||||
conRemark: group.conRemark || "",
|
||||
avatar: group.chatroomAvatar || group.avatar || "",
|
||||
selfDisplayName: group.selfDisplyName || "",
|
||||
notice: group.notice || "",
|
||||
aiType: group.aiType ?? 0,
|
||||
phone: group.phone ?? "",
|
||||
region: group.region ?? "",
|
||||
config: group.config || {},
|
||||
groupId: group.groupId, // 保留标签ID
|
||||
extendFields: serializeExtendFields(group.extendFields),
|
||||
lastUpdateTime: new Date().toISOString(),
|
||||
sortKey: "",
|
||||
searchKey: "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Input, Skeleton, Button, Dropdown, MenuProps } from "antd";
|
||||
import {
|
||||
SearchOutlined,
|
||||
@@ -193,6 +193,27 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabContentCacheRef = useRef<Record<string, React.ReactNode>>({});
|
||||
|
||||
const getTabContent = (tabKey: string) => {
|
||||
if (!tabContentCacheRef.current[tabKey]) {
|
||||
switch (tabKey) {
|
||||
case "chats":
|
||||
tabContentCacheRef.current[tabKey] = <MessageList />;
|
||||
break;
|
||||
case "contracts":
|
||||
tabContentCacheRef.current[tabKey] = <WechatFriends />;
|
||||
break;
|
||||
case "friendsCicle":
|
||||
tabContentCacheRef.current[tabKey] = <FriendsCircle />;
|
||||
break;
|
||||
default:
|
||||
tabContentCacheRef.current[tabKey] = null;
|
||||
}
|
||||
}
|
||||
return tabContentCacheRef.current[tabKey];
|
||||
};
|
||||
|
||||
// 渲染内容部分
|
||||
const renderContent = () => {
|
||||
// 如果正在切换tab到聊天,显示骨架屏
|
||||
@@ -200,16 +221,27 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
||||
return renderSkeleton();
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case "chats":
|
||||
return <MessageList />;
|
||||
case "contracts":
|
||||
return <WechatFriends />;
|
||||
case "friendsCicle":
|
||||
return <FriendsCircle />;
|
||||
default:
|
||||
return null;
|
||||
const availableTabs = ["chats", "contracts"];
|
||||
if (currentCustomer && currentCustomer.id !== 0) {
|
||||
availableTabs.push("friendsCicle");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{availableTabs.map(tabKey => (
|
||||
<div
|
||||
key={tabKey}
|
||||
style={{
|
||||
display: activeTab === tabKey ? "block" : "none",
|
||||
height: "100%",
|
||||
}}
|
||||
aria-hidden={activeTab !== tabKey}
|
||||
>
|
||||
{getTabContent(tabKey)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -144,7 +144,7 @@ export interface ContractData {
|
||||
labels: string[];
|
||||
signature: string;
|
||||
accountId: number;
|
||||
extendFields: null;
|
||||
extendFields?: Record<string, any> | null;
|
||||
city?: string;
|
||||
lastUpdateTime: string;
|
||||
isPassed: boolean;
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { databaseManager } from "@/utils/db";
|
||||
|
||||
const STORE_CACHE_KEYS = [
|
||||
"user-store",
|
||||
"app-store",
|
||||
"settings-store",
|
||||
"websocket-store",
|
||||
"ckchat-store",
|
||||
"wechat-storage",
|
||||
"contacts-storage",
|
||||
"message-storage",
|
||||
"customer-storage",
|
||||
];
|
||||
|
||||
const allStorages = (): Storage[] => {
|
||||
if (typeof window === "undefined") {
|
||||
return [];
|
||||
}
|
||||
const storages: Storage[] = [];
|
||||
try {
|
||||
storages.push(window.localStorage);
|
||||
} catch (error) {
|
||||
console.warn("无法访问 localStorage:", error);
|
||||
}
|
||||
try {
|
||||
storages.push(window.sessionStorage);
|
||||
} catch (error) {
|
||||
console.warn("无法访问 sessionStorage:", error);
|
||||
}
|
||||
return storages;
|
||||
};
|
||||
|
||||
const clearStoreCaches = () => {
|
||||
const storages = allStorages();
|
||||
if (!storages.length) {
|
||||
return;
|
||||
}
|
||||
STORE_CACHE_KEYS.forEach(key => {
|
||||
storages.forEach(storage => {
|
||||
try {
|
||||
storage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.warn(`清理持久化数据失败: ${key}`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
@@ -28,7 +75,7 @@ interface UserState {
|
||||
setToken: (token: string) => void;
|
||||
setToken2: (token2: string) => void;
|
||||
clearUser: () => void;
|
||||
login: (token: string, userInfo: User) => void;
|
||||
login: (token: string, userInfo: User) => Promise<void>;
|
||||
login2: (token2: string) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
@@ -39,12 +86,27 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
token: null,
|
||||
token2: null,
|
||||
isLoggedIn: false,
|
||||
setUser: user => set({ user, isLoggedIn: true }),
|
||||
setUser: user => {
|
||||
set({ user, isLoggedIn: true });
|
||||
databaseManager.ensureDatabase(user.id).catch(error => {
|
||||
console.warn("Failed to initialize database for user:", error);
|
||||
});
|
||||
},
|
||||
setToken: token => set({ token }),
|
||||
setToken2: token2 => set({ token2 }),
|
||||
clearUser: () =>
|
||||
set({ user: null, token: null, token2: null, isLoggedIn: false }),
|
||||
login: (token, userInfo) => {
|
||||
clearUser: () => {
|
||||
databaseManager.closeCurrentDatabase().catch(error => {
|
||||
console.warn("Failed to close database on clearUser:", error);
|
||||
});
|
||||
clearStoreCaches();
|
||||
set({ user: null, token: null, token2: null, isLoggedIn: false });
|
||||
},
|
||||
login: async (token, userInfo) => {
|
||||
clearStoreCaches();
|
||||
|
||||
// 清除旧的双token缓存
|
||||
localStorage.removeItem("token2");
|
||||
|
||||
// 只将token存储到localStorage
|
||||
localStorage.setItem("token", token);
|
||||
|
||||
@@ -66,6 +128,11 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
lastLoginIp: userInfo.lastLoginIp,
|
||||
lastLoginTime: userInfo.lastLoginTime,
|
||||
};
|
||||
try {
|
||||
await databaseManager.ensureDatabase(user.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize user database:", error);
|
||||
}
|
||||
set({ user, token, isLoggedIn: true });
|
||||
|
||||
Toast.show({ content: "登录成功", position: "top" });
|
||||
@@ -80,6 +147,10 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
// 清除localStorage中的token
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("token2");
|
||||
databaseManager.closeCurrentDatabase().catch(error => {
|
||||
console.warn("Failed to close user database on logout:", error);
|
||||
});
|
||||
clearStoreCaches();
|
||||
set({ user: null, token: null, token2: null, isLoggedIn: false });
|
||||
},
|
||||
}),
|
||||
@@ -92,7 +163,11 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
isLoggedIn: state.isLoggedIn,
|
||||
}),
|
||||
onRehydrateStorage: () => state => {
|
||||
// console.log("User store hydrated:", state);
|
||||
if (state?.user?.id) {
|
||||
databaseManager.ensureDatabase(state.user!.id).catch(error => {
|
||||
console.warn("Failed to restore user database:", error);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ContractData {
|
||||
labels: string[];
|
||||
signature: string;
|
||||
accountId: number;
|
||||
extendFields: null;
|
||||
extendFields?: Record<string, any> | null;
|
||||
city?: string;
|
||||
lastUpdateTime: string;
|
||||
isPassed: boolean;
|
||||
|
||||
@@ -3,6 +3,10 @@ import { persist } from "zustand/middleware";
|
||||
import { ContactGroupByLabel } from "@/pages/pc/ckbox/data";
|
||||
import { Contact } from "@/utils/db";
|
||||
import { ContactManager } from "@/utils/dbAction";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
|
||||
const SEARCH_DEBOUNCE_DELAY = 300;
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/**
|
||||
* 联系人状态管理接口
|
||||
@@ -171,8 +175,16 @@ export const useContactStore = create<ContactState>()(
|
||||
|
||||
setSearchKeyword: (keyword: string) => {
|
||||
set({ searchKeyword: keyword });
|
||||
|
||||
if (searchDebounceTimer) {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = null;
|
||||
}
|
||||
|
||||
if (keyword.trim()) {
|
||||
get().searchContacts(keyword);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
get().searchContacts(keyword);
|
||||
}, SEARCH_DEBOUNCE_DELAY);
|
||||
} else {
|
||||
set({ isSearchMode: false, searchResults: [] });
|
||||
}
|
||||
@@ -204,8 +216,15 @@ export const useContactStore = create<ContactState>()(
|
||||
set({ loading: true, isSearchMode: true });
|
||||
|
||||
try {
|
||||
const currentUserId = useUserStore.getState().user?.id;
|
||||
|
||||
if (!currentUserId) {
|
||||
set({ searchResults: [], isSearchMode: false, loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await ContactManager.searchContacts(
|
||||
get().currentContact?.userId || 0,
|
||||
currentUserId,
|
||||
keyword,
|
||||
);
|
||||
set({ searchResults: results });
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ChatSession } from "@/utils/db";
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
wechatId: string;
|
||||
@@ -26,13 +28,15 @@ export interface Message {
|
||||
}
|
||||
|
||||
//Store State - 会话列表状态管理(不存储数据,只管理状态)
|
||||
export type SessionsUpdater =
|
||||
| ChatSession[]
|
||||
| ((previous: ChatSession[]) => ChatSession[]);
|
||||
|
||||
export interface MessageState {
|
||||
//加载状态
|
||||
loading: boolean;
|
||||
//后台同步状态
|
||||
refreshing: boolean;
|
||||
//刷新触发器(用于通知组件重新查询数据库)
|
||||
refreshTrigger: number;
|
||||
//最后刷新时间
|
||||
lastRefreshTime: string | null;
|
||||
//是否已经加载过一次(避免重复请求)
|
||||
@@ -42,8 +46,6 @@ export interface MessageState {
|
||||
setLoading: (loading: boolean) => void;
|
||||
//设置同步状态
|
||||
setRefreshing: (refreshing: boolean) => void;
|
||||
//触发刷新(通知组件重新查询)
|
||||
triggerRefresh: () => void;
|
||||
//设置已加载标识
|
||||
setHasLoadedOnce: (loaded: boolean) => void;
|
||||
//重置加载状态(用于登出或切换用户)
|
||||
@@ -60,4 +62,16 @@ export interface MessageState {
|
||||
updateMessageStatus: (messageId: number, status: string) => void;
|
||||
//更新当前选中的消息(废弃,保留兼容)
|
||||
updateCurrentMessage: (message: Message) => void;
|
||||
|
||||
// ==================== 新的会话数据接口 ====================
|
||||
// 当前会话列表
|
||||
sessions: ChatSession[];
|
||||
// 设置或更新会话列表(支持回调写法)
|
||||
setSessions: (updater: SessionsUpdater) => void;
|
||||
// 新增或替换某个会话
|
||||
upsertSession: (session: ChatSession) => void;
|
||||
// 按 ID 和类型移除会话
|
||||
removeSessionById: (sessionId: number, type: ChatSession["type"]) => void;
|
||||
// 清空所有会话(登出/切账号使用)
|
||||
clearSessions: () => void;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { Message, MessageState } from "./message.data";
|
||||
import { ChatSession } from "@/utils/db";
|
||||
import { Message, MessageState, SessionsUpdater } from "./message.data";
|
||||
|
||||
const computeSortKey = (session: ChatSession) => {
|
||||
const isTop = session.config?.top ? 1 : 0;
|
||||
const timestamp = new Date(session.lastUpdateTime || new Date()).getTime();
|
||||
const displayName = (
|
||||
session.conRemark ||
|
||||
session.nickname ||
|
||||
(session as any).wechatId ||
|
||||
""
|
||||
).toLowerCase();
|
||||
|
||||
return `${isTop}|${timestamp}|${displayName}`;
|
||||
};
|
||||
|
||||
const normalizeSessions = (sessions: ChatSession[]) => {
|
||||
if (!Array.isArray(sessions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...sessions]
|
||||
.map(session => ({
|
||||
...session,
|
||||
sortKey: computeSortKey(session),
|
||||
}))
|
||||
.sort((a, b) => b.sortKey.localeCompare(a.sortKey));
|
||||
};
|
||||
|
||||
const resolveUpdater = (
|
||||
updater: SessionsUpdater,
|
||||
previous: ChatSession[],
|
||||
): ChatSession[] => {
|
||||
if (typeof updater === "function") {
|
||||
return updater(previous);
|
||||
}
|
||||
return updater;
|
||||
};
|
||||
|
||||
/**
|
||||
* 会话列表状态管理Store
|
||||
@@ -13,24 +50,18 @@ export const useMessageStore = create<MessageState>()(
|
||||
// ==================== 新增状态管理 ====================
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
refreshTrigger: 0,
|
||||
lastRefreshTime: null,
|
||||
hasLoadedOnce: false,
|
||||
|
||||
setLoading: (loading: boolean) => set({ loading }),
|
||||
setRefreshing: (refreshing: boolean) => set({ refreshing }),
|
||||
triggerRefresh: () =>
|
||||
set({
|
||||
refreshTrigger: get().refreshTrigger + 1,
|
||||
lastRefreshTime: new Date().toISOString(),
|
||||
}),
|
||||
setHasLoadedOnce: (loaded: boolean) => set({ hasLoadedOnce: loaded }),
|
||||
resetLoadState: () =>
|
||||
set({
|
||||
hasLoadedOnce: false,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
refreshTrigger: 0,
|
||||
sessions: [],
|
||||
}),
|
||||
|
||||
// ==================== 保留原有接口(向后兼容) ====================
|
||||
@@ -45,6 +76,45 @@ export const useMessageStore = create<MessageState>()(
|
||||
message.id === messageId ? { ...message, status } : message,
|
||||
),
|
||||
}),
|
||||
|
||||
// ==================== 会话数据接口 ====================
|
||||
sessions: [],
|
||||
setSessions: (updater: SessionsUpdater) =>
|
||||
set(state => ({
|
||||
sessions: normalizeSessions(resolveUpdater(updater, state.sessions)),
|
||||
lastRefreshTime: new Date().toISOString(),
|
||||
})),
|
||||
upsertSession: (session: ChatSession) =>
|
||||
set(state => {
|
||||
const next = [...state.sessions];
|
||||
const index = next.findIndex(
|
||||
s => s.id === session.id && s.type === session.type,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
next[index] = session;
|
||||
} else {
|
||||
next.push(session);
|
||||
}
|
||||
return {
|
||||
sessions: normalizeSessions(next),
|
||||
lastRefreshTime: new Date().toISOString(),
|
||||
};
|
||||
}),
|
||||
removeSessionById: (sessionId: number, type: ChatSession["type"]) =>
|
||||
set(state => ({
|
||||
sessions: normalizeSessions(
|
||||
state.sessions.filter(
|
||||
s => !(s.id === sessionId && s.type === type),
|
||||
),
|
||||
),
|
||||
lastRefreshTime: new Date().toISOString(),
|
||||
})),
|
||||
clearSessions: () =>
|
||||
set({
|
||||
sessions: [],
|
||||
lastRefreshTime: new Date().toISOString(),
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: "message-storage",
|
||||
@@ -105,11 +175,6 @@ export const setLoading = (loading: boolean) =>
|
||||
export const setRefreshing = (refreshing: boolean) =>
|
||||
useMessageStore.getState().setRefreshing(refreshing);
|
||||
|
||||
/**
|
||||
* 触发刷新(通知组件重新查询数据库)
|
||||
*/
|
||||
export const triggerRefresh = () => useMessageStore.getState().triggerRefresh();
|
||||
|
||||
/**
|
||||
* 设置已加载标识
|
||||
* @param loaded 是否已加载
|
||||
|
||||
@@ -40,6 +40,12 @@ export interface WeChatState {
|
||||
// ==================== 聊天消息管理 ====================
|
||||
/** 当前聊天的消息列表 */
|
||||
currentMessages: ChatRecord[];
|
||||
/** 当前聊天记录分页页码 */
|
||||
currentMessagesPage: number;
|
||||
/** 单页消息条数 */
|
||||
currentMessagesPageSize: number;
|
||||
/** 是否还有更多历史消息 */
|
||||
currentMessagesHasMore: boolean;
|
||||
/** 添加新消息 */
|
||||
addMessage: (message: ChatRecord) => void;
|
||||
/** 更新指定消息 */
|
||||
@@ -83,7 +89,7 @@ export interface WeChatState {
|
||||
|
||||
// ==================== 消息加载方法 ====================
|
||||
/** 加载聊天消息 */
|
||||
loadChatMessages: (Init: boolean, To?: number) => Promise<void>;
|
||||
loadChatMessages: (Init: boolean, pageOverride?: number) => Promise<void>;
|
||||
/** 搜索消息 */
|
||||
SearchMessage: (params: {
|
||||
From: number;
|
||||
|
||||
@@ -28,6 +28,7 @@ let pendingMessages: ChatRecord[] = []; // 待处理的消息队列
|
||||
let currentAiGenerationId: string | null = null; // 当前AI生成的唯一ID
|
||||
const AI_REQUEST_DELAY = 3000; // 3秒延迟
|
||||
const FILE_MESSAGE_TYPE = "file";
|
||||
const DEFAULT_MESSAGE_PAGE_SIZE = 20;
|
||||
|
||||
type FileMessagePayload = {
|
||||
type?: string;
|
||||
@@ -120,6 +121,108 @@ const isFileLikeMessage = (msg: ChatRecord): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const normalizeMessages = (source: any): ChatRecord[] => {
|
||||
if (Array.isArray(source)) {
|
||||
return source;
|
||||
}
|
||||
if (Array.isArray(source?.list)) {
|
||||
return source.list;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const parseTimeValue = (value: unknown): number => {
|
||||
if (value === null || value === undefined) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isNaN(numeric)) {
|
||||
return numeric;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.getTime();
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getMessageTimestamp = (msg: ChatRecord): number => {
|
||||
const candidates = [
|
||||
(msg as any)?.wechatTime,
|
||||
(msg as any)?.createTime,
|
||||
(msg as any)?.msgTime,
|
||||
(msg as any)?.timestamp,
|
||||
(msg as any)?.time,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const parsed = parseTimeValue(candidate);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof msg.id === "number" ? msg.id : 0;
|
||||
};
|
||||
|
||||
const sortMessagesByTime = (messages: ChatRecord[]): ChatRecord[] => {
|
||||
return [...messages].sort(
|
||||
(a, b) => getMessageTimestamp(a) - getMessageTimestamp(b),
|
||||
);
|
||||
};
|
||||
|
||||
const resolvePaginationState = (
|
||||
source: any,
|
||||
requestedPage: number,
|
||||
requestedLimit: number,
|
||||
listLength: number,
|
||||
) => {
|
||||
const page =
|
||||
typeof source?.page === "number"
|
||||
? source.page
|
||||
: typeof source?.current === "number"
|
||||
? source.current
|
||||
: requestedPage;
|
||||
|
||||
const limit =
|
||||
typeof source?.limit === "number"
|
||||
? source.limit
|
||||
: typeof source?.pageSize === "number"
|
||||
? source.pageSize
|
||||
: requestedLimit;
|
||||
|
||||
let hasMore: boolean;
|
||||
if (typeof source?.hasNext === "boolean") {
|
||||
hasMore = source.hasNext;
|
||||
} else if (typeof source?.hasNextPage === "boolean") {
|
||||
hasMore = source.hasNextPage;
|
||||
} else if (typeof source?.pages === "number") {
|
||||
hasMore = page < source.pages;
|
||||
} else if (typeof source?.total === "number" && limit > 0) {
|
||||
hasMore = page * limit < source.total;
|
||||
} else {
|
||||
hasMore = listLength >= limit && listLength > 0;
|
||||
}
|
||||
|
||||
if (listLength === 0) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
limit: limit || requestedLimit || DEFAULT_MESSAGE_PAGE_SIZE,
|
||||
hasMore,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeFilePayload = (
|
||||
payload: FileMessagePayload | null | undefined,
|
||||
msg: ChatRecord,
|
||||
@@ -348,6 +451,10 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
currentContract: null,
|
||||
/** 当前聊天的消息列表 */
|
||||
currentMessages: [],
|
||||
/** 当前消息分页信息 */
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE,
|
||||
currentMessagesHasMore: true,
|
||||
|
||||
// ==================== 聊天消息管理方法 ====================
|
||||
/** 添加新消息到当前聊天 */
|
||||
@@ -429,7 +536,13 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
aiRequestTimer = null;
|
||||
}
|
||||
pendingMessages = [];
|
||||
set({ currentContract: null, currentMessages: [] });
|
||||
set({
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesHasMore: true,
|
||||
currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE,
|
||||
});
|
||||
},
|
||||
/** 设置当前联系人并加载相关数据 */
|
||||
setCurrentContact: (contract: ContractData | weChatGroup) => {
|
||||
@@ -443,7 +556,13 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
|
||||
const state = useWeChatStore.getState();
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [], isLoadingAiChat: false });
|
||||
set({
|
||||
currentMessages: [],
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesHasMore: true,
|
||||
currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE,
|
||||
isLoadingAiChat: false,
|
||||
});
|
||||
|
||||
const params: any = {};
|
||||
|
||||
@@ -468,62 +587,91 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
id: contract.id,
|
||||
config: { chat: true },
|
||||
});
|
||||
state.loadChatMessages(true, 4704624000000);
|
||||
state.loadChatMessages(true);
|
||||
},
|
||||
|
||||
// ==================== 消息加载方法 ====================
|
||||
/** 加载聊天消息 */
|
||||
loadChatMessages: async (Init: boolean, To?: number) => {
|
||||
loadChatMessages: async (Init: boolean, pageOverride?: number) => {
|
||||
const state = useWeChatStore.getState();
|
||||
const contact = state.currentContract;
|
||||
set({ messagesLoading: true });
|
||||
set({ isLoadingData: Init });
|
||||
|
||||
if (!contact) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Init && !state.currentMessagesHasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPage = Init
|
||||
? 1
|
||||
: (pageOverride ?? state.currentMessagesPage + 1);
|
||||
const limit =
|
||||
state.currentMessagesPageSize || DEFAULT_MESSAGE_PAGE_SIZE;
|
||||
|
||||
if (state.messagesLoading && !Init) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({
|
||||
messagesLoading: true,
|
||||
isLoadingData: Init,
|
||||
});
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
wechatAccountId: contact.wechatAccountId,
|
||||
From: 1,
|
||||
To: To || +new Date(),
|
||||
Count: 20,
|
||||
olderData: true,
|
||||
page: nextPage,
|
||||
limit,
|
||||
};
|
||||
|
||||
if ("chatroomId" in contact && contact.chatroomId) {
|
||||
// 群聊消息加载
|
||||
const isGroup =
|
||||
"chatroomId" in contact && Boolean(contact.chatroomId);
|
||||
|
||||
if (isGroup) {
|
||||
params.wechatChatroomId = contact.id;
|
||||
const messages = await getChatroomMessages(params);
|
||||
const currentGroupMembers = await getGroupMembers({
|
||||
} else {
|
||||
params.wechatFriendId = contact.id;
|
||||
}
|
||||
|
||||
const response = isGroup
|
||||
? await getChatroomMessages(params)
|
||||
: await getChatMessages(params);
|
||||
|
||||
const normalizedMessages = normalizeMessages(response);
|
||||
const sortedMessages = sortMessagesByTime(normalizedMessages);
|
||||
const paginationMeta = resolvePaginationState(
|
||||
response,
|
||||
nextPage,
|
||||
limit,
|
||||
sortedMessages.length,
|
||||
);
|
||||
|
||||
let nextGroupMembers = state.currentGroupMembers;
|
||||
if (Init && isGroup) {
|
||||
nextGroupMembers = await getGroupMembers({
|
||||
id: contact.id,
|
||||
});
|
||||
if (Init) {
|
||||
set({ currentMessages: messages || [], currentGroupMembers });
|
||||
} else {
|
||||
set({
|
||||
currentMessages: [
|
||||
...(messages || []),
|
||||
...state.currentMessages,
|
||||
],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 私聊消息加载
|
||||
params.wechatFriendId = contact.id;
|
||||
const messages = await getChatMessages(params);
|
||||
if (Init) {
|
||||
set({ currentMessages: messages || [] });
|
||||
} else {
|
||||
set({
|
||||
currentMessages: [
|
||||
...(messages || []),
|
||||
...state.currentMessages,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
set({ messagesLoading: false });
|
||||
|
||||
set(current => ({
|
||||
currentMessages: Init
|
||||
? sortedMessages
|
||||
: [...sortedMessages, ...current.currentMessages],
|
||||
currentGroupMembers:
|
||||
Init && isGroup ? nextGroupMembers : current.currentGroupMembers,
|
||||
currentMessagesPage: paginationMeta.page,
|
||||
currentMessagesPageSize: paginationMeta.limit,
|
||||
currentMessagesHasMore: paginationMeta.hasMore,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("获取聊天消息失败:", error);
|
||||
} finally {
|
||||
set({ messagesLoading: false });
|
||||
set({
|
||||
messagesLoading: false,
|
||||
isLoadingData: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -546,11 +694,11 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
try {
|
||||
const params: any = {
|
||||
wechatAccountId: contact.wechatAccountId,
|
||||
keyword,
|
||||
From,
|
||||
To,
|
||||
keyword,
|
||||
Count,
|
||||
olderData: true,
|
||||
page: 1,
|
||||
limit: Count,
|
||||
};
|
||||
|
||||
if ("chatroomId" in contact && contact.chatroomId) {
|
||||
@@ -560,12 +708,23 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
const currentGroupMembers = await getGroupMembers({
|
||||
id: contact.id,
|
||||
});
|
||||
set({ currentMessages: messages || [], currentGroupMembers });
|
||||
set({
|
||||
currentMessages: sortMessagesByTime(normalizeMessages(messages)),
|
||||
currentGroupMembers,
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesHasMore: false,
|
||||
currentMessagesPageSize: Count || state.currentMessagesPageSize,
|
||||
});
|
||||
} else {
|
||||
// 私聊消息搜索
|
||||
params.wechatFriendId = contact.id;
|
||||
const messages = await getChatMessages(params);
|
||||
set({ currentMessages: messages || [] });
|
||||
set({
|
||||
currentMessages: sortMessagesByTime(normalizeMessages(messages)),
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesHasMore: false,
|
||||
currentMessagesPageSize: Count || state.currentMessagesPageSize,
|
||||
});
|
||||
}
|
||||
set({ messagesLoading: false });
|
||||
} catch (error) {
|
||||
@@ -831,6 +990,9 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
set({
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
currentMessagesPage: 1,
|
||||
currentMessagesHasMore: true,
|
||||
currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE,
|
||||
messagesLoading: false,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
import { Toast } from "antd-mobile";
|
||||
import { useUserStore } from "../user";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { useCustomerStore } from "@/store/module/weChat/customer";
|
||||
const { getAccountId } = useCkChatStore.getState();
|
||||
import { msgManageCore } from "./msgManage";
|
||||
// WebSocket消息类型
|
||||
@@ -52,6 +52,8 @@ interface WebSocketState {
|
||||
reconnectAttempts: number;
|
||||
reconnectTimer: NodeJS.Timeout | null;
|
||||
aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器
|
||||
aliveStatusUnsubscribe: (() => void) | null;
|
||||
aliveStatusLastRequest: number | null;
|
||||
|
||||
// 方法
|
||||
connect: (config: Partial<WebSocketConfig>) => void;
|
||||
@@ -87,6 +89,8 @@ const DEFAULT_CONFIG: WebSocketConfig = {
|
||||
maxReconnectAttempts: 5,
|
||||
};
|
||||
|
||||
const ALIVE_STATUS_MIN_INTERVAL = 5 * 1000; // ms
|
||||
|
||||
export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
(set, get) => ({
|
||||
status: WebSocketStatus.DISCONNECTED,
|
||||
@@ -97,6 +101,8 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
aliveStatusTimer: null,
|
||||
aliveStatusUnsubscribe: null,
|
||||
aliveStatusLastRequest: null,
|
||||
|
||||
// 连接WebSocket
|
||||
connect: (config: Partial<WebSocketConfig>) => {
|
||||
@@ -232,11 +238,6 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
currentState.status !== WebSocketStatus.CONNECTED ||
|
||||
!currentState.ws
|
||||
) {
|
||||
// Toast.show({
|
||||
// content: "WebSocket未连接,正在重新连接...",
|
||||
// position: "top",
|
||||
// });
|
||||
|
||||
// 重置连接状态并发起重新连接
|
||||
set({ status: WebSocketStatus.DISCONNECTED });
|
||||
if (currentState.config) {
|
||||
@@ -392,7 +393,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
|
||||
set({
|
||||
messages: [...currentState.messages, newMessage],
|
||||
unreadCount: currentState.config.unreadCount + 1,
|
||||
unreadCount: (currentState.unreadCount ?? 0) + 1,
|
||||
});
|
||||
//消息处理器
|
||||
msgManageCore(data);
|
||||
@@ -405,7 +406,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
},
|
||||
|
||||
// 内部方法:处理连接关闭
|
||||
_handleClose: (event: CloseEvent) => {
|
||||
_handleClose: () => {
|
||||
const currentState = get();
|
||||
|
||||
// console.log("WebSocket连接关闭:", event.code, event.reason);
|
||||
@@ -431,7 +432,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
},
|
||||
|
||||
// 内部方法:处理连接错误
|
||||
_handleError: (event: Event) => {
|
||||
_handleError: () => {
|
||||
// console.error("WebSocket连接错误:", event);
|
||||
|
||||
set({ status: WebSocketStatus.ERROR });
|
||||
@@ -477,42 +478,97 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
// 先停止现有定时器
|
||||
currentState._stopAliveStatusTimer();
|
||||
|
||||
// 获取客服用户列表
|
||||
const { kfUserList } = useCkChatStore.getState();
|
||||
const requestAliveStatus = () => {
|
||||
const state = get();
|
||||
if (state.status !== WebSocketStatus.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有客服用户,不启动定时器
|
||||
if (!kfUserList || kfUserList.length === 0) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (
|
||||
state.aliveStatusLastRequest &&
|
||||
now - state.aliveStatusLastRequest < ALIVE_STATUS_MIN_INTERVAL
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { customerList } = useCustomerStore.getState();
|
||||
const { kfUserList } = useCkChatStore.getState();
|
||||
const targets =
|
||||
customerList && customerList.length > 0
|
||||
? customerList
|
||||
: kfUserList && kfUserList.length > 0
|
||||
? kfUserList
|
||||
: [];
|
||||
|
||||
if (targets.length > 0) {
|
||||
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: targets.map(v => v.id),
|
||||
});
|
||||
set({ aliveStatusLastRequest: now });
|
||||
}
|
||||
};
|
||||
|
||||
// 尝试立即请求一次,如果客服列表尚未加载,后续定时器会继续检查
|
||||
requestAliveStatus();
|
||||
|
||||
const unsubscribeCustomer = useCustomerStore.subscribe(state => {
|
||||
if (
|
||||
get().status === WebSocketStatus.CONNECTED &&
|
||||
state.customerList &&
|
||||
state.customerList.length > 0
|
||||
) {
|
||||
requestAliveStatus();
|
||||
}
|
||||
});
|
||||
|
||||
const unsubscribeKf = useCkChatStore.subscribe(state => {
|
||||
if (
|
||||
get().status === WebSocketStatus.CONNECTED &&
|
||||
state.kfUserList &&
|
||||
state.kfUserList.length > 0
|
||||
) {
|
||||
requestAliveStatus();
|
||||
}
|
||||
});
|
||||
|
||||
// 启动定时器,每5秒查询一次
|
||||
const timer = setInterval(() => {
|
||||
const state = get();
|
||||
// 检查连接状态
|
||||
if (state.status === WebSocketStatus.CONNECTED) {
|
||||
const { kfUserList: currentKfUserList } = useCkChatStore.getState();
|
||||
if (currentKfUserList && currentKfUserList.length > 0) {
|
||||
state.sendCommand("CmdRequestWechatAccountsAliveStatus", {
|
||||
wechatAccountIds: currentKfUserList.map(v => v.id),
|
||||
});
|
||||
}
|
||||
requestAliveStatus();
|
||||
} else {
|
||||
// 如果连接断开,停止定时器
|
||||
state._stopAliveStatusTimer();
|
||||
}
|
||||
}, 5 * 1000);
|
||||
|
||||
set({ aliveStatusTimer: timer });
|
||||
set({
|
||||
aliveStatusTimer: timer,
|
||||
aliveStatusUnsubscribe: () => {
|
||||
unsubscribeCustomer();
|
||||
unsubscribeKf();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 内部方法:停止客服状态查询定时器
|
||||
_stopAliveStatusTimer: () => {
|
||||
const currentState = get();
|
||||
|
||||
if (currentState.aliveStatusUnsubscribe) {
|
||||
currentState.aliveStatusUnsubscribe();
|
||||
}
|
||||
|
||||
if (currentState.aliveStatusTimer) {
|
||||
clearInterval(currentState.aliveStatusTimer);
|
||||
set({ aliveStatusTimer: null });
|
||||
}
|
||||
set({
|
||||
aliveStatusTimer: null,
|
||||
aliveStatusUnsubscribe: null,
|
||||
aliveStatusLastRequest: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
@@ -524,6 +580,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
||||
messages: state.messages.slice(-100), // 只保留最近100条消息
|
||||
unreadCount: state.unreadCount,
|
||||
reconnectAttempts: state.reconnectAttempts,
|
||||
aliveStatusLastRequest: state.aliveStatusLastRequest,
|
||||
// 注意:定时器不需要持久化,重新连接时会重新创建
|
||||
}),
|
||||
onRehydrateStorage: () => state => {
|
||||
|
||||
@@ -5,6 +5,12 @@ export const PERSIST_KEYS = {
|
||||
USER_STORE: "user-store",
|
||||
APP_STORE: "app-store",
|
||||
SETTINGS_STORE: "settings-store",
|
||||
CKCHAT_STORE: "ckchat-store",
|
||||
WEBSOCKET_STORE: "websocket-store",
|
||||
WECHAT_STORAGE: "wechat-storage",
|
||||
CONTACTS_STORAGE: "contacts-storage",
|
||||
MESSAGE_STORAGE: "message-storage",
|
||||
CUSTOMER_STORAGE: "customer-storage",
|
||||
} as const;
|
||||
|
||||
// 存储类型
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
*/
|
||||
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { getPersistedData, PERSIST_KEYS } from "@/store/persistUtils";
|
||||
const DB_NAME_PREFIX = "CunkebaoDatabase";
|
||||
|
||||
// ==================== 用户登录记录 ====================
|
||||
export interface UserLoginRecord {
|
||||
@@ -58,6 +60,9 @@ export interface ChatSession {
|
||||
chatroomOwner?: string; // 群主
|
||||
selfDisplayName?: string; // 群内昵称
|
||||
notice?: string; // 群公告
|
||||
phone?: string; // 联系人电话
|
||||
region?: string; // 联系人地区
|
||||
extendFields?: string; // 扩展字段(JSON 字符串)
|
||||
}
|
||||
|
||||
// ==================== 统一联系人表(兼容好友和群聊) ====================
|
||||
@@ -88,6 +93,7 @@ export interface Contact {
|
||||
signature?: string; // 个性签名
|
||||
phone?: string; // 手机号
|
||||
quanPin?: string; // 全拼
|
||||
extendFields?: string; // 扩展字段(JSON 字符串)
|
||||
|
||||
// 群聊特有字段(type='group'时有效)
|
||||
chatroomId?: string; // 群聊ID
|
||||
@@ -123,18 +129,17 @@ class CunkebaoDatabase extends Dexie {
|
||||
contactLabelMap!: Table<ContactLabelMap>; // 联系人标签映射表
|
||||
userLoginRecords!: Table<UserLoginRecord>; // 用户登录记录表
|
||||
|
||||
constructor() {
|
||||
super("CunkebaoDatabase");
|
||||
constructor(dbName: string) {
|
||||
super(dbName);
|
||||
|
||||
// 版本1:统一表结构
|
||||
this.version(1).stores({
|
||||
// 会话表索引:支持按用户、类型、时间、置顶等查询
|
||||
chatSessions:
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], sortKey, nickname, conRemark, avatar, content, lastUpdateTime",
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region",
|
||||
|
||||
// 联系人表索引:支持按用户、类型、标签、搜索等查询
|
||||
contactsUnified:
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId",
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region",
|
||||
|
||||
// 联系人标签映射表索引:支持按用户、标签、联系人、类型查询
|
||||
contactLabelMap:
|
||||
@@ -145,68 +150,200 @@ class CunkebaoDatabase extends Dexie {
|
||||
"serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime",
|
||||
});
|
||||
|
||||
// 版本2:添加 aiType 字段
|
||||
this.version(2)
|
||||
.stores({
|
||||
// 会话表索引:添加 aiType 索引
|
||||
chatSessions:
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType",
|
||||
|
||||
// 联系人表索引:添加 aiType 索引
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region, extendFields",
|
||||
contactsUnified:
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType",
|
||||
|
||||
// 联系人标签映射表索引:保持不变
|
||||
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region, extendFields",
|
||||
contactLabelMap:
|
||||
"serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime",
|
||||
|
||||
// 用户登录记录表索引:保持不变
|
||||
userLoginRecords:
|
||||
"serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime",
|
||||
})
|
||||
.upgrade(tx => {
|
||||
// 数据迁移:为现有数据添加 aiType 默认值
|
||||
return tx
|
||||
.upgrade(async tx => {
|
||||
await tx
|
||||
.table("chatSessions")
|
||||
.toCollection()
|
||||
.modify(session => {
|
||||
if (session.aiType === undefined) {
|
||||
session.aiType = 0; // 默认为普通类型
|
||||
if (!("extendFields" in session) || session.extendFields == null) {
|
||||
session.extendFields = "{}";
|
||||
} else if (typeof session.extendFields !== "string") {
|
||||
session.extendFields = JSON.stringify(session.extendFields);
|
||||
}
|
||||
});
|
||||
|
||||
await tx
|
||||
.table("contactsUnified")
|
||||
.toCollection()
|
||||
.modify(contact => {
|
||||
if (!("extendFields" in contact) || contact.extendFields == null) {
|
||||
contact.extendFields = "{}";
|
||||
} else if (typeof contact.extendFields !== "string") {
|
||||
contact.extendFields = JSON.stringify(contact.extendFields);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return tx
|
||||
.table("contactsUnified")
|
||||
.toCollection()
|
||||
.modify(contact => {
|
||||
if (contact.aiType === undefined) {
|
||||
contact.aiType = 0; // 默认为普通类型
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据库实例
|
||||
export const db = new CunkebaoDatabase();
|
||||
class DatabaseManager {
|
||||
private currentDb: CunkebaoDatabase | null = null;
|
||||
private currentUserId: number | null = null;
|
||||
|
||||
private getDatabaseName(userId: number) {
|
||||
return `${DB_NAME_PREFIX}_${userId}`;
|
||||
}
|
||||
|
||||
private async openDatabase(dbName: string) {
|
||||
const instance = new CunkebaoDatabase(dbName);
|
||||
await instance.open();
|
||||
return instance;
|
||||
}
|
||||
|
||||
async ensureDatabase(userId: number) {
|
||||
if (userId === undefined || userId === null) {
|
||||
throw new Error("Invalid userId provided for database initialization");
|
||||
}
|
||||
|
||||
if (
|
||||
this.currentDb &&
|
||||
this.currentUserId === userId &&
|
||||
this.currentDb.isOpen()
|
||||
) {
|
||||
return this.currentDb;
|
||||
}
|
||||
|
||||
await this.closeCurrentDatabase();
|
||||
|
||||
const dbName = this.getDatabaseName(userId);
|
||||
this.currentDb = await this.openDatabase(dbName);
|
||||
this.currentUserId = userId;
|
||||
|
||||
return this.currentDb;
|
||||
}
|
||||
|
||||
getCurrentDatabase(): CunkebaoDatabase {
|
||||
if (!this.currentDb) {
|
||||
throw new Error("Database has not been initialized for the current user");
|
||||
}
|
||||
return this.currentDb;
|
||||
}
|
||||
|
||||
getCurrentUserId() {
|
||||
return this.currentUserId;
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
return !!this.currentDb && this.currentDb.isOpen();
|
||||
}
|
||||
|
||||
async closeCurrentDatabase() {
|
||||
if (this.currentDb) {
|
||||
try {
|
||||
this.currentDb.close();
|
||||
} catch (error) {
|
||||
console.warn("Failed to close current database:", error);
|
||||
}
|
||||
this.currentDb = null;
|
||||
}
|
||||
this.currentUserId = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const databaseManager = new DatabaseManager();
|
||||
|
||||
let pendingDatabaseRestore: Promise<CunkebaoDatabase | null> | null = null;
|
||||
|
||||
async function restoreDatabaseFromPersistedState() {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const persistedData = getPersistedData<string | Record<string, any>>(
|
||||
PERSIST_KEYS.USER_STORE,
|
||||
"localStorage",
|
||||
);
|
||||
|
||||
if (!persistedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: any = persistedData;
|
||||
|
||||
if (typeof persistedData === "string") {
|
||||
try {
|
||||
parsed = JSON.parse(persistedData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse persisted user-store value:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const state = parsed?.state ?? parsed;
|
||||
const userId = state?.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await databaseManager.ensureDatabase(userId);
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize database from persisted user:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeDatabaseFromPersistedUser() {
|
||||
if (databaseManager.isInitialized()) {
|
||||
return databaseManager.getCurrentDatabase();
|
||||
}
|
||||
|
||||
if (!pendingDatabaseRestore) {
|
||||
pendingDatabaseRestore = restoreDatabaseFromPersistedState().finally(() => {
|
||||
pendingDatabaseRestore = null;
|
||||
});
|
||||
}
|
||||
|
||||
return pendingDatabaseRestore;
|
||||
}
|
||||
|
||||
const dbProxy = new Proxy({} as CunkebaoDatabase, {
|
||||
get(_target, prop: string | symbol) {
|
||||
const currentDb = databaseManager.getCurrentDatabase();
|
||||
const value = (currentDb as any)[prop];
|
||||
if (typeof value === "function") {
|
||||
return value.bind(currentDb);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
export const db = dbProxy;
|
||||
|
||||
// 简单的数据库操作类
|
||||
export class DatabaseService<T> {
|
||||
constructor(private table: Table<T>) {}
|
||||
constructor(private readonly tableAccessor: () => Table<T>) {}
|
||||
|
||||
private get table(): Table<T> {
|
||||
return this.tableAccessor();
|
||||
}
|
||||
|
||||
// 基础 CRUD 操作 - 使用serverId作为主键
|
||||
async create(data: Omit<T, "serverId">): Promise<string | number> {
|
||||
return await this.table.add(data as T);
|
||||
return await this.table.add(this.prepareDataForWrite(data) as T);
|
||||
}
|
||||
|
||||
// 创建数据(直接使用接口数据)
|
||||
// 接口数据的id字段直接作为serverId主键,原id字段保留
|
||||
async createWithServerId(data: any): Promise<string | number> {
|
||||
const dataToInsert = {
|
||||
const dataToInsert = this.prepareDataForWrite({
|
||||
...data,
|
||||
serverId: data.id, // 使用接口的id作为serverId主键
|
||||
};
|
||||
phone: data.phone ?? "",
|
||||
region: data.region ?? "",
|
||||
});
|
||||
return await this.table.add(dataToInsert as T);
|
||||
}
|
||||
|
||||
@@ -225,7 +362,10 @@ export class DatabaseService<T> {
|
||||
}
|
||||
|
||||
async update(serverId: string | number, data: Partial<T>): Promise<number> {
|
||||
return await this.table.update(serverId, data as any);
|
||||
return await this.table.update(
|
||||
serverId,
|
||||
this.prepareDataForWrite(data) as any,
|
||||
);
|
||||
}
|
||||
|
||||
async updateMany(
|
||||
@@ -234,7 +374,7 @@ export class DatabaseService<T> {
|
||||
return await this.table.bulkUpdate(
|
||||
dataList.map(item => ({
|
||||
key: item.serverId,
|
||||
changes: item.data as any,
|
||||
changes: this.prepareDataForWrite(item.data) as any,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -242,7 +382,8 @@ export class DatabaseService<T> {
|
||||
async createMany(
|
||||
dataList: Omit<T, "serverId">[],
|
||||
): Promise<(string | number)[]> {
|
||||
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
|
||||
const processed = dataList.map(item => this.prepareDataForWrite(item));
|
||||
return await this.table.bulkAdd(processed as T[], { allKeys: true });
|
||||
}
|
||||
|
||||
// 批量创建数据(直接使用接口数据)
|
||||
@@ -266,10 +407,14 @@ export class DatabaseService<T> {
|
||||
return [];
|
||||
}
|
||||
|
||||
const processedData = newData.map(item => ({
|
||||
...item,
|
||||
serverId: item.id, // 使用接口的id作为serverId主键
|
||||
}));
|
||||
const processedData = newData.map(item =>
|
||||
this.prepareDataForWrite({
|
||||
...item,
|
||||
serverId: item.id, // 使用接口的id作为serverId主键
|
||||
phone: item.phone ?? "",
|
||||
region: item.region ?? "",
|
||||
}),
|
||||
);
|
||||
|
||||
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
|
||||
}
|
||||
@@ -443,13 +588,42 @@ export class DatabaseService<T> {
|
||||
.equals(value)
|
||||
.count();
|
||||
}
|
||||
|
||||
private prepareDataForWrite(data: any) {
|
||||
if (!data || typeof data !== "object") {
|
||||
return data;
|
||||
}
|
||||
|
||||
const prepared = { ...data };
|
||||
|
||||
if ("extendFields" in prepared) {
|
||||
const value = prepared.extendFields;
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
prepared.extendFields = value;
|
||||
} else if (value && typeof value === "object") {
|
||||
prepared.extendFields = JSON.stringify(value);
|
||||
} else {
|
||||
prepared.extendFields = "{}";
|
||||
}
|
||||
}
|
||||
|
||||
return prepared;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建统一表的服务实例
|
||||
export const chatSessionService = new DatabaseService(db.chatSessions);
|
||||
export const contactUnifiedService = new DatabaseService(db.contactsUnified);
|
||||
export const contactLabelMapService = new DatabaseService(db.contactLabelMap);
|
||||
export const userLoginRecordService = new DatabaseService(db.userLoginRecords);
|
||||
export const chatSessionService = new DatabaseService<ChatSession>(
|
||||
() => databaseManager.getCurrentDatabase().chatSessions,
|
||||
);
|
||||
export const contactUnifiedService = new DatabaseService<Contact>(
|
||||
() => databaseManager.getCurrentDatabase().contactsUnified,
|
||||
);
|
||||
export const contactLabelMapService = new DatabaseService<ContactLabelMap>(
|
||||
() => databaseManager.getCurrentDatabase().contactLabelMap,
|
||||
);
|
||||
export const userLoginRecordService = new DatabaseService<UserLoginRecord>(
|
||||
() => databaseManager.getCurrentDatabase().userLoginRecords,
|
||||
);
|
||||
|
||||
// 默认导出数据库实例
|
||||
export default db;
|
||||
|
||||
@@ -184,7 +184,10 @@ export class ContactManager {
|
||||
local.conRemark !== server.conRemark ||
|
||||
local.avatar !== server.avatar ||
|
||||
local.wechatAccountId !== server.wechatAccountId ||
|
||||
(local.aiType ?? 0) !== (server.aiType ?? 0) // 添加 aiType 比较
|
||||
(local.aiType ?? 0) !== (server.aiType ?? 0) || // 添加 aiType 比较
|
||||
(local.phone ?? "") !== (server.phone ?? "") ||
|
||||
(local.region ?? "") !== (server.region ?? "") ||
|
||||
(local.extendFields ?? "{}") !== (server.extendFields ?? "{}")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,10 +195,12 @@ export class ContactManager {
|
||||
* 获取联系人分组列表
|
||||
*/
|
||||
static async getContactGroups(
|
||||
userId: number,
|
||||
customerId?: number,
|
||||
_userId: number,
|
||||
_customerId?: number,
|
||||
): Promise<ContactGroupByLabel[]> {
|
||||
try {
|
||||
void _userId;
|
||||
void _customerId;
|
||||
// 这里应该根据实际的标签系统来实现
|
||||
// 暂时返回空数组,实际实现需要根据标签表来查询
|
||||
return [];
|
||||
|
||||
@@ -11,8 +11,29 @@ import Dexie from "dexie";
|
||||
import { db, chatSessionService, ChatSession } from "../db";
|
||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
|
||||
const serializeExtendFields = (value: any) => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim() ? value : "{}";
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (error) {
|
||||
console.warn("序列化 extendFields 失败:", error);
|
||||
}
|
||||
}
|
||||
return "{}";
|
||||
};
|
||||
|
||||
interface SessionUpdatePayload {
|
||||
userId: number;
|
||||
sessions: ChatSession[];
|
||||
}
|
||||
|
||||
export class MessageManager {
|
||||
private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>();
|
||||
private static updateCallbacks = new Set<
|
||||
(payload: SessionUpdatePayload) => void
|
||||
>();
|
||||
|
||||
// ==================== 回调管理 ====================
|
||||
|
||||
@@ -21,9 +42,11 @@ export class MessageManager {
|
||||
* @param callback 回调函数
|
||||
* @returns 取消注册的函数
|
||||
*/
|
||||
static onSessionsUpdate(callback: (sessions: ChatSession[]) => void) {
|
||||
static onSessionsUpdate(callback: (payload: SessionUpdatePayload) => void) {
|
||||
this.updateCallbacks.add(callback);
|
||||
return () => this.updateCallbacks.delete(callback);
|
||||
return () => {
|
||||
this.updateCallbacks.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +58,7 @@ export class MessageManager {
|
||||
const sessions = await this.getUserSessions(userId);
|
||||
this.updateCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(sessions);
|
||||
callback({ userId, sessions });
|
||||
} catch (error) {
|
||||
console.error("会话更新回调执行失败:", error);
|
||||
}
|
||||
@@ -93,6 +116,8 @@ export class MessageManager {
|
||||
content: (friend as any).content || "",
|
||||
lastUpdateTime: friend.lastUpdateTime || new Date().toISOString(),
|
||||
aiType: (friend as any).aiType ?? 0, // AI类型,默认为0(普通)
|
||||
phone: (friend as any).phone ?? "",
|
||||
region: (friend as any).region ?? "",
|
||||
config: {
|
||||
unreadCount: friend.config?.unreadCount || 0,
|
||||
top: (friend.config as any)?.top || false,
|
||||
@@ -101,6 +126,7 @@ export class MessageManager {
|
||||
wechatFriendId: friend.id,
|
||||
wechatId: friend.wechatId,
|
||||
alias: friend.alias,
|
||||
extendFields: serializeExtendFields((friend as any).extendFields),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,6 +152,8 @@ export class MessageManager {
|
||||
content: (group as any).content || "",
|
||||
lastUpdateTime: (group as any).lastUpdateTime || new Date().toISOString(),
|
||||
aiType: (group as any).aiType ?? 0, // AI类型,默认为0(普通)
|
||||
phone: (group as any).phone ?? "",
|
||||
region: (group as any).region ?? "",
|
||||
config: {
|
||||
unreadCount: (group.config as any)?.unreadCount || 0,
|
||||
top: (group.config as any)?.top || false,
|
||||
@@ -135,6 +163,7 @@ export class MessageManager {
|
||||
chatroomOwner: group.chatroomOwner,
|
||||
selfDisplayName: group.selfDisplyName,
|
||||
notice: group.notice,
|
||||
extendFields: serializeExtendFields((group as any).extendFields),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -199,6 +228,9 @@ export class MessageManager {
|
||||
"avatar",
|
||||
"wechatAccountId", // 添加wechatAccountId比较
|
||||
"aiType", // 添加aiType比较
|
||||
"phone",
|
||||
"region",
|
||||
"extendFields",
|
||||
];
|
||||
|
||||
for (const field of fieldsToCompare) {
|
||||
|
||||
Reference in New Issue
Block a user