refactor(ckbox): 统一将contact重命名为contract

将代码中所有的contact变量和类名统一改为contract,包括API路径、组件属性、CSS类名等
更新相关路由配置和文档中的API接口说明
This commit is contained in:
超级老白兔
2025-08-26 14:19:19 +08:00
parent c7b15896ac
commit a26b465ff6
14 changed files with 181 additions and 132 deletions

View File

@@ -46,7 +46,7 @@ const About: React.FC = () => {
];
// 联系信息
const contactInfo = [
const contractInfo = [
{
id: "email",
title: "邮箱支持",
@@ -125,7 +125,7 @@ const About: React.FC = () => {
{/* <Card className={style["setting-group"]}>
<div className={style["group-title"]}>联系我们</div>
<List>
{contactInfo.map(item => (
{contractInfo.map(item => (
<List.Item
key={item.id}
prefix={item.icon}

View File

@@ -169,7 +169,7 @@ export default ckboxRoutes;
确保后端提供以下API接口
- `GET /v1/contacts` - 获取联系人列表
- `GET /v1/contracts` - 获取联系人列表
- `GET /v1/chats/sessions` - 获取聊天会话列表
- `GET /v1/chats/:id/messages` - 获取聊天历史
- `POST /v1/chats/:id/messages` - 发送消息

View File

@@ -322,8 +322,8 @@
}
}
.contactInfo {
.contactItem {
.contractInfo {
.contractItem {
align-items: center;
margin-bottom: 12px;
font-size: 14px;
@@ -339,7 +339,7 @@
width: 16px;
}
.contactItemText {
.contractItemText {
padding-left: 10px;
}
}
@@ -475,7 +475,7 @@
width: 60px;
height: 60px;
transition: transform 0.2s ease;
.loadingSpinner {
width: 32px;
height: 32px;
@@ -485,10 +485,14 @@
animation: spin 1s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
@@ -709,8 +713,8 @@
}
}
.contactInfo {
.contactItem {
.contractInfo {
.contractItem {
font-size: 13px;
margin-bottom: 10px;
}

View File

@@ -48,8 +48,6 @@ import { ChatRecord, ContractData } from "@/pages/pc/ckbox/data";
import { clearUnreadCount, getMessages } from "@/pages/pc/ckbox/api";
import styles from "./ChatWindow.module.scss";
import { useWebSocketStore } from "@/store/module/websocket";
const { sendCommand } = useWebSocketStore.getState();
const { Header, Content, Footer, Sider } = Layout;
const { TextArea } = Input;
@@ -71,6 +69,9 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
const [inputValue, setInputValue] = useState("");
const [loading, setLoading] = useState(false);
const [showMaterialModal, setShowMaterialModal] = useState(false);
const [pendingVideoRequests, setPendingVideoRequests] = useState<
Record<string, string>
>({});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -92,6 +93,64 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
scrollToBottom();
}, [messages]);
// 添加 WebSocket 消息订阅
useEffect(() => {
// 订阅 WebSocket 消息变化
const unsubscribe = useWebSocketStore.subscribe(
state => state.messages,
(messages, previousMessages) => {
// 只处理新消息
if (messages.length > previousMessages.length) {
// 获取最新的消息
const newMessages = messages.slice(previousMessages.length);
// 处理新消息
newMessages.forEach(message => {
const content = message.content;
// 检查是否是视频下载响应
if (content && content.cmdType === "CmdDownloadVideoResponse") {
// 获取视频URL
const videoUrl = content.videoUrl;
const requestId = content.requestId || content.seq;
// 查找对应的消息ID
const messageId = pendingVideoRequests[requestId];
if (messageId && videoUrl) {
// 更新消息内容,将预览图替换为实际视频
setMessages(prevMessages => {
return prevMessages.map(msg => {
if (msg.id === messageId) {
return {
...msg,
content: videoUrl,
};
}
return msg;
});
});
// 从待处理请求中移除
setPendingVideoRequests(prev => {
const newRequests = { ...prev };
delete newRequests[requestId];
return newRequests;
});
messageApi.success("视频加载成功");
}
}
});
}
},
);
// 组件卸载时取消订阅
return () => {
unsubscribe();
};
}, [pendingVideoRequests, messageApi]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
@@ -179,15 +238,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
// 处理视频播放请求发送socket请求获取真实视频地址
const handleVideoPlayRequest = (tencentUrl: string, messageId: string) => {
// 生成请求ID (可以使用 seq 或其他唯一标识)
// 构建socket请求数据
sendCommand("CmdDownloadVideo", {
useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
chatroomMessageId: contract.chatroomId ? contract.chatroomId : 0,
friendMessageId: contract.chatroomId ? 0 : contract.id,
seq: 9,
seq: 9, // 使用唯一的请求ID
tencentUrl: tencentUrl,
wechatAccountId: contract.wechatAccountId,
});
// 记录待处理的视频请求
setPendingVideoRequests(prev => ({
...prev,
}));
// 更新消息状态为加载中
setMessages(prevMessages => {
return prevMessages.map(msg => {
@@ -205,32 +271,6 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
return msg;
});
});
// TODO: 这里应该实现实际的socket请求发送逻辑
console.log("发送视频请求:", socketData);
// 模拟获取视频地址后的处理
// 实际应用中这里应该是socket响应的回调处理
setTimeout(() => {
// 模拟获取到视频地址
const videoUrl = "https://example.com/video.mp4";
// 更新消息内容,将预览图替换为实际视频
setMessages(prevMessages => {
return prevMessages.map(msg => {
if (msg.id === messageId) {
// 创建新的视频消息内容
return {
...msg,
content: videoUrl,
};
}
return msg;
});
});
messageApi.success("视频加载成功");
}, 1500);
};
// 解析消息内容,判断消息类型并返回对应的渲染内容

View File

@@ -1,8 +1,8 @@
.contactList {
.contractList {
height: 100%;
overflow-y: auto;
.contactItem {
.contractItem {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
@@ -16,17 +16,17 @@
border-bottom: none;
}
.contactInfo {
.contractInfo {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.contactDetails {
.contractDetails {
flex: 1;
min-width: 0;
.contactName {
.contractName {
font-size: 14px;
font-weight: 500;
color: #262626;
@@ -36,13 +36,13 @@
white-space: nowrap;
}
.contactPhone {
.contractPhone {
font-size: 12px;
color: #8c8c8c;
margin-bottom: 2px;
}
.contactStatus {
.contractStatus {
font-size: 11px;
color: #bfbfbf;
overflow: hidden;
@@ -56,19 +56,19 @@
// 响应式设计
@media (max-width: 768px) {
.contactList {
.contactItem {
.contractList {
.contractItem {
padding: 10px 12px;
.contactInfo {
.contractInfo {
gap: 10px;
.contactDetails {
.contactName {
.contractDetails {
.contractName {
font-size: 13px;
}
.contactPhone {
.contractPhone {
font-size: 11px;
}
}

View File

@@ -5,36 +5,36 @@ import { ContractData } from "../../data";
import styles from "./ContactList.module.scss";
interface ContactListProps {
contacts: ContractData[];
onContactClick: (contact: ContractData) => void;
contracts: ContractData[];
onContactClick: (contract: ContractData) => void;
}
const ContactList: React.FC<ContactListProps> = ({
contacts,
contracts,
onContactClick,
}) => {
return (
<div className={styles.contactList}>
<div className={styles.contractList}>
<List
dataSource={contacts}
renderItem={contact => (
dataSource={contracts}
renderItem={contract => (
<List.Item
className={styles.contactItem}
onClick={() => onContactClick(contact)}
className={styles.contractItem}
onClick={() => onContactClick(contract)}
>
<div className={styles.contactInfo}>
<Badge dot={contact.online} color="#52c41a" offset={[-2, 2]}>
<div className={styles.contractInfo}>
<Badge dot={contract.online} color="#52c41a" offset={[-2, 2]}>
<Avatar
size={40}
src={contact.avatar}
src={contract.avatar}
icon={<UserOutlined />}
/>
</Badge>
<div className={styles.contactDetails}>
<div className={styles.contactName}>{contact.name}</div>
<div className={styles.contactPhone}>{contact.phone}</div>
{contact.status && (
<div className={styles.contactStatus}>{contact.status}</div>
<div className={styles.contractDetails}>
<div className={styles.contractName}>{contract.name}</div>
<div className={styles.contractPhone}>{contract.phone}</div>
{contract.status && (
<div className={styles.contractStatus}>{contract.status}</div>
)}
</div>
</div>

View File

@@ -2,13 +2,13 @@ import React from "react";
import { List, Avatar, Badge } from "antd";
import { UserOutlined, TeamOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData } from "./data";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
import styles from "./MessageList.module.scss";
interface MessageListProps {
chatSessions: ContractData[];
currentChat: ContractData;
onChatSelect: (chat: ContractData) => void;
onChatSelect: (chat: ContractData | GroupData) => void;
}
const MessageList: React.FC<MessageListProps> = ({

View File

@@ -1,4 +1,4 @@
.contactListSimple {
.contractListSimple {
display: flex;
flex-direction: column;
height: 100%;
@@ -26,7 +26,7 @@
}
}
.contactItem {
.contractItem {
display: flex;
align-items: center;
padding: 8px 15px;
@@ -44,7 +44,7 @@
background-color: #1890ff;
}
.contactInfo {
.contractInfo {
flex: 1;
overflow: hidden;
}

View File

@@ -1,43 +1,43 @@
import React from "react";
import { List, Avatar } from "antd";
import { ContractData } from "./data";
import { ContractData } from "@/pages/pc/ckbox/data";
import styles from "./WechatFriends.module.scss";
interface WechatFriendsProps {
contacts: ContractData[];
onContactClick: (contact: ContractData) => void;
contracts: ContractData[];
onContactClick: (contract: ContractData) => void;
selectedContactId?: ContractData;
}
const ContactListSimple: React.FC<WechatFriendsProps> = ({
contacts,
contracts,
onContactClick,
selectedContactId,
}) => {
return (
<div className={styles.contactListSimple}>
<div className={styles.contractListSimple}>
<div className={styles.header}></div>
<List
className={styles.list}
dataSource={contacts}
renderItem={contact => (
dataSource={contracts}
renderItem={contract => (
<List.Item
key={contact.id}
onClick={() => onContactClick(contact)}
className={`${styles.contactItem} ${contact.id === selectedContactId?.id ? styles.selected : ""}`}
key={contract.id}
onClick={() => onContactClick(contract)}
className={`${styles.contractItem} ${contract.id === selectedContactId?.id ? styles.selected : ""}`}
>
<div className={styles.avatarContainer}>
<Avatar
src={contact.avatar}
src={contract.avatar}
icon={
!contact.avatar && <span>{contact.nickname.charAt(0)}</span>
!contract.avatar && <span>{contract.nickname.charAt(0)}</span>
}
className={styles.avatar}
/>
</div>
<div className={styles.contactInfo}>
<div className={styles.contractInfo}>
<div className={styles.name}>
{contact.conRemark || contact.nickname}
{contract.conRemark || contract.nickname}
</div>
</div>
</List.Item>

View File

@@ -14,15 +14,15 @@ import styles from "./SidebarMenu.module.scss";
import { getChatSessions } from "@/store/module/ckchat";
interface SidebarMenuProps {
contacts: ContractData[];
contracts: ContractData[];
currentChat: ContractData;
onContactClick: (contact: ContractData) => void;
onContactClick: (contract: ContractData) => void;
onChatSelect: (chat: ContractData) => void;
loading?: boolean;
}
const SidebarMenu: React.FC<SidebarMenuProps> = ({
contacts,
contracts,
currentChat,
onContactClick,
onChatSelect,
@@ -31,18 +31,18 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
const chatSessions = getChatSessions();
const [searchText, setSearchText] = useState("");
const [activeTab, setActiveTab] = useState("contacts");
const [activeTab, setActiveTab] = useState("contracts");
const handleSearch = (value: string) => {
setSearchText(value);
};
const getFilteredContacts = () => {
if (!searchText) return contacts;
return contacts.filter(
contact =>
contact.nickname.toLowerCase().includes(searchText.toLowerCase()) ||
contact.phone.includes(searchText),
if (!searchText) return contracts;
return contracts.filter(
contract =>
contract.nickname.toLowerCase().includes(searchText.toLowerCase()) ||
contract.phone.includes(searchText),
);
};
@@ -77,8 +77,8 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
<span></span>
</div>
<div
className={`${styles.tabItem} ${activeTab === "contacts" ? styles.active : ""}`}
onClick={() => setActiveTab("contacts")}
className={`${styles.tabItem} ${activeTab === "contracts" ? styles.active : ""}`}
onClick={() => setActiveTab("contracts")}
>
<UserOutlined />
<span></span>
@@ -105,10 +105,10 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
currentChat={currentChat}
/>
);
case "contacts":
case "contracts":
return (
<WechatFriendsModule
contacts={getFilteredContacts()}
contracts={getFilteredContacts()}
onContactClick={onContactClick}
selectedContactId={currentChat}
/>

View File

@@ -11,7 +11,7 @@ const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [contacts, setContacts] = useState<any[]>([]);
const [contracts, setContacts] = useState<any[]>([]);
const [currentChat, setCurrentChat] = useState<ContractData | null>(null);
const [loading, setLoading] = useState(false);
@@ -21,10 +21,10 @@ const CkboxPage: React.FC = () => {
// 方法一:使用 Promise 链式调用处理异步函数
setLoading(true);
chatInitAPIdata()
.then((response: { contactList: any[]; chatRoomList: any[] }) => {
const { contactList, chatRoomList } = response;
.then((response: { contractList: any[]; chatRoomList: any[] }) => {
const { contractList, chatRoomList } = response;
//找出已经在聊天的
const isChatList = contactList.filter(
const isChatList = contractList.filter(
v => (v?.config && v.config?.chat) || false,
);
isChatList.forEach(v => {
@@ -41,9 +41,9 @@ const CkboxPage: React.FC = () => {
});
}, []);
const handleContactClick = (contact: ContractData) => {
addChatSession(contact);
setCurrentChat(contact);
const handleContactClick = (contract: ContractData) => {
addChatSession(contract);
setCurrentChat(contract);
};
const handleSendMessage = async (message: string) => {
@@ -74,7 +74,7 @@ const CkboxPage: React.FC = () => {
{/* 左侧边栏 */}
<Sider width={280} className={styles.sider}>
<SidebarMenu
contacts={contacts}
contracts={contracts}
currentChat={currentChat}
onContactClick={handleContactClick}
onChatSelect={setCurrentChat}
@@ -101,7 +101,7 @@ const CkboxPage: React.FC = () => {
</Space>
</div>
<ChatWindow
contact={currentChat}
contract={currentChat}
onSendMessage={handleSendMessage}
showProfile={showProfile}
onToggleProfile={() => setShowProfile(!showProfile)}

View File

@@ -33,11 +33,11 @@ export const chatInitAPIdata = async () => {
seq: 1,
});
//获取联系人列表
const contactList = await getAllContactList();
const contractList = await getAllContactList();
//获取群列表
const chatRoomList = await getAllChatRoomList();
return {
contactList,
contractList,
chatRoomList,
};
} catch (error) {
@@ -56,28 +56,28 @@ export const getAllContactList = async () => {
while (hasMore) {
console.log(`获取联系人列表prevId: ${prevId}, count: ${count}`);
const contactList = await getContactList({
const contractList = await getContactList({
prevId,
count,
});
if (
!contactList ||
!Array.isArray(contactList) ||
contactList.length === 0
!contractList ||
!Array.isArray(contractList) ||
contractList.length === 0
) {
hasMore = false;
break;
}
allContacts = [...allContacts, ...contactList];
allContacts = [...allContacts, ...contractList];
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
if (contactList.length < count) {
if (contractList.length < count) {
hasMore = false;
} else {
// 获取最后一条数据的id作为下一次请求的prevId
const lastContact = contactList[contactList.length - 1];
const lastContact = contractList[contractList.length - 1];
prevId = lastContact.id;
}
}
@@ -99,28 +99,28 @@ export const getAllChatRoomList = async () => {
while (hasMore) {
console.log(`获取群列表prevId: ${prevId}, count: ${count}`);
const contactList = await getChatRoomList({
const contractList = await getChatRoomList({
prevId,
count,
});
if (
!contactList ||
!Array.isArray(contactList) ||
contactList.length === 0
!contractList ||
!Array.isArray(contractList) ||
contractList.length === 0
) {
hasMore = false;
break;
}
allContacts = [...allContacts, ...contactList];
allContacts = [...allContacts, ...contractList];
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
if (contactList.length < count) {
if (contractList.length < count) {
hasMore = false;
} else {
// 获取最后一条数据的id作为下一次请求的prevId
const lastContact = contactList[contactList.length - 1];
const lastContact = contractList[contractList.length - 1];
prevId = lastContact.id;
}
}

View File

@@ -95,7 +95,7 @@ export const routeGroups = {
"/plans",
"/plans/:planId",
"/orders",
"/contact-import",
"/contract-import",
],
},
};
@@ -126,7 +126,7 @@ export const routePermissions = {
"/plans",
"/plans/:planId",
"/orders",
"/contact-import",
"/contract-import",
],
// 访客权限
@@ -150,7 +150,7 @@ export const routeTitles: Record<string, string> = {
"/profile": "个人中心",
"/plans": "计划管理",
"/orders": "订单管理",
"/contact-import": "联系人导入",
"/contract-import": "联系人导入",
};
// 获取路由标题

View File

@@ -120,6 +120,11 @@ export const getCkTenantId = () => useCkChatStore.getState().getTenantId();
export const getCkAccountName = () =>
useCkChatStore.getState().getAccountName();
export const getCkTenantName = () => useCkChatStore.getState().getTenantName();
export const getChatSessions = () => useCkChatStore.getState().chatSessions;
export const getChatSessions = () =>
useCkChatStore.getState().getChatSessions();
export const addChatSession = (session: ContractData | GroupData) =>
useCkChatStore.getState().addChatSession(session);
export const updateChatSession = (session: ContractData | GroupData) =>
useCkChatStore.getState().updateChatSession(session);
export const deleteChatSession = (sessionId: string) =>
useCkChatStore.getState().deleteChatSession(sessionId);