重构ProfileCard组件以增强选项卡管理并改善用户体验。引入基于可用键的动态选项卡呈现,并更新选项卡标题的样式。调整活动关键帧和渲染关键帧的状态管理。更新QuickWords和ProfileModules集成。修改GroupModal和QuickReplyModal以使用“destroyOnHidden”以获得更好的模式处理。

This commit is contained in:
超级老白兔
2025-11-13 16:07:52 +08:00
parent ae4a165b07
commit a6ee45f3e3
10 changed files with 157 additions and 80 deletions

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useMemo } from "react";
import {
Input,
Button,
@@ -22,7 +22,7 @@ import {
SwapOutlined,
} from "@ant-design/icons";
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { useCustomerStore } from "@/store/module/weChat/customer";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { useWeChatStore } from "@/store/module/weChat/weChat";
import { useContactStore } from "@/store/module/weChat/contacts";
@@ -209,9 +209,14 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
// 构建联系人或群聊详细信息
const kfSelectedUser = useCkChatStore(state =>
state.getKfUserInfo(contract.wechatAccountId || 0),
);
const customerList = useCustomerStore(state => state.customerList);
const kfSelectedUser = useMemo(() => {
if (!contract.wechatAccountId) return null;
const matchedCustomer = customerList.find(
customer => customer.id === contract.wechatAccountId,
);
return matchedCustomer || null;
}, [customerList, contract.wechatAccountId]);
const { getContactsByCustomer } = useContactStore();
@@ -221,16 +226,12 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
const hasGroupManagePermission = () => {
// 暂时给所有用户完整的群管理权限
return true;
// if (!kfSelectedUser || !contract) return false;
// // 当客服的wechatId与contract的chatroomOwner相同时才有完整的群管理权限
// return kfSelectedUser.nickname === (contract as any).chatroomOwnerNickname;
};
// 获取所有可用标签
useEffect(() => {
const fetchAvailableTags = async () => {
try {
// 从kfSelectedUser.labels和contract.labels合并获取所有标签
const kfTags = kfSelectedUser?.labels || [];
const contractTags = contract.labels || [];
const allTags = [...new Set([...kfTags, ...contractTags])];

View File

@@ -28,7 +28,7 @@ const GroupModal: React.FC<GroupModalProps> = ({
form.resetFields();
}}
footer={null}
destroyOnClose
destroyOnHidden
>
<Form
form={form}

View File

@@ -126,7 +126,7 @@ const QuickReplyModal: React.FC<QuickReplyModalProps> = ({
form.resetFields();
}}
footer={null}
destroyOnClose
destroyOnHidden
>
<Form
form={form}

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, 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";
@@ -16,51 +16,107 @@ interface PersonProps {
}
const Person: React.FC<PersonProps> = ({ contract }) => {
const [activeKey, setActiveKey] = useState("profile");
const [activeKey, setActiveKey] = useState("quickwords");
const isGroup = "chatroomId" in contract;
const tabItems = useMemo(() => {
const baseItems = [
{
key: "quickwords",
label: "快捷语录",
children: <QuickWords onInsert={noop} />,
},
{
key: "profile",
label: isGroup ? "群资料" : "个人资料",
children: <ProfileModules contract={contract} />,
},
];
if (!isGroup) {
baseItems.push({
key: "moments",
label: "朋友圈",
children: <FriendsCircle wechatFriendId={contract.id} />,
});
}
return baseItems;
}, [isGroup]);
}, [contract, isGroup]);
const handleTabChange = useCallback((key: string) => {
setActiveKey(key);
}, []);
useEffect(() => {
setActiveKey("quickwords");
setRenderedKeys(["quickwords"]);
}, [contract]);
const tabBarStyle = useMemo(() => ({ padding: "0 30px" }), []);
const tabHeaderItems = useMemo(
() => tabItems.map(({ key, label }) => ({ key, label })),
[tabItems],
);
const availableKeys = useMemo(
() => tabItems.map(item => item.key),
[tabItems],
);
const [renderedKeys, setRenderedKeys] = useState<string[]>(() => [
"quickwords",
]);
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={handleTabChange}
tabBarStyle={tabBarStyle}
items={tabItems}
/>
<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 onInsert={noop} />}
{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>
);

View File

@@ -343,9 +343,6 @@ const MessageList: React.FC<MessageListProps> = () => {
};
});
console.log("群聊数据示例:", groups[0]); // 调试:查看第一个群聊数据
console.log("好友数据示例:", friends[0]); // 调试:查看第一个好友数据
// 执行增量同步
const syncResult = await MessageManager.syncSessions(currentUserId, {
friends,
@@ -655,6 +652,8 @@ const MessageList: React.FC<MessageListProps> = () => {
top: 0,
},
sortKey: "",
phone: msgData.phone || "",
region: msgData.region || "",
};
await MessageManager.addSession(newSession);
@@ -681,6 +680,8 @@ const MessageList: React.FC<MessageListProps> = () => {
top: 0,
},
sortKey: "",
phone: msgData.phone || "",
region: msgData.region || "",
};
await MessageManager.addSession(newSession);
@@ -710,7 +711,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);

View File

@@ -394,7 +394,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
set({
messages: [...currentState.messages, newMessage],
unreadCount: currentState.config.unreadCount + 1,
unreadCount: (currentState.unreadCount ?? 0) + 1,
});
//消息处理器
msgManageCore(data);

View File

@@ -60,6 +60,8 @@ export interface ChatSession {
chatroomOwner?: string; // 群主
selfDisplayName?: string; // 群内昵称
notice?: string; // 群公告
phone?: string; // 联系人电话
region?: string; // 联系人地区
}
// ==================== 统一联系人表(兼容好友和群聊) ====================
@@ -128,15 +130,14 @@ class CunkebaoDatabase extends Dexie {
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:
@@ -146,47 +147,6 @@ class CunkebaoDatabase extends Dexie {
userLoginRecords:
"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 索引
contactsUnified:
"serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType",
// 联系人标签映射表索引:保持不变
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
.table("chatSessions")
.toCollection()
.modify(session => {
if (session.aiType === undefined) {
session.aiType = 0; // 默认为普通类型
}
})
.then(() => {
return tx
.table("contactsUnified")
.toCollection()
.modify(contact => {
if (contact.aiType === undefined) {
contact.aiType = 0; // 默认为普通类型
}
});
});
});
}
}
@@ -344,6 +304,8 @@ export class DatabaseService<T> {
const dataToInsert = {
...data,
serverId: data.id, // 使用接口的id作为serverId主键
phone: data.phone ?? "",
region: data.region ?? "",
};
return await this.table.add(dataToInsert as T);
}
@@ -407,6 +369,8 @@ export class DatabaseService<T> {
const processedData = newData.map(item => ({
...item,
serverId: item.id, // 使用接口的id作为serverId主键
phone: item.phone ?? "",
region: item.region ?? "",
}));
return await this.table.bulkAdd(processedData as T[], { allKeys: true });

View File

@@ -184,7 +184,9 @@ 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 ?? "")
);
}

View File

@@ -93,6 +93,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,
@@ -126,6 +128,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,
@@ -199,6 +203,8 @@ export class MessageManager {
"avatar",
"wechatAccountId", // 添加wechatAccountId比较
"aiType", // 添加aiType比较
"phone",
"region",
];
for (const field of fieldsToCompare) {