Merge branch 'yongpxu-dev' into yongpxu-dev2

# Conflicts:
#	Cunkebao/dist/.vite/manifest.json
#	Cunkebao/dist/index.html
This commit is contained in:
2025-08-28 17:39:40 +08:00
8 changed files with 444 additions and 107 deletions

View File

@@ -1,9 +1,9 @@
{ {
"_charts-62ymwWYF.js": { "_charts-M0qaf_ew.js": {
"file": "assets/charts-62ymwWYF.js", "file": "assets/charts-M0qaf_ew.js",
"name": "charts", "name": "charts",
"imports": [ "imports": [
"_ui-DUe_gloh.js", "_ui-D5qYGnLz.js",
"_vendor-2vc8h_ct.js" "_vendor-2vc8h_ct.js"
] ]
}, },
@@ -11,8 +11,8 @@
"file": "assets/ui-D0C0OGrH.css", "file": "assets/ui-D0C0OGrH.css",
"src": "_ui-D0C0OGrH.css" "src": "_ui-D0C0OGrH.css"
}, },
"_ui-DUe_gloh.js": { "_ui-D5qYGnLz.js": {
"file": "assets/ui-DUe_gloh.js", "file": "assets/ui-D5qYGnLz.js",
"name": "ui", "name": "ui",
"imports": [ "imports": [
"_vendor-2vc8h_ct.js" "_vendor-2vc8h_ct.js"
@@ -33,18 +33,18 @@
"name": "vendor" "name": "vendor"
}, },
"index.html": { "index.html": {
"file": "assets/index-BQZfedpY.js", "file": "assets/index-BQxyt58_.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_vendor-2vc8h_ct.js", "_vendor-2vc8h_ct.js",
"_utils-6WF66_dS.js", "_utils-6WF66_dS.js",
"_ui-DUe_gloh.js", "_ui-D5qYGnLz.js",
"_charts-62ymwWYF.js" "_charts-M0qaf_ew.js"
], ],
"css": [ "css": [
"assets/index-ejYsXKTB.css" "assets/index-B6B8u-1D.css"
] ]
} }
} }

View File

@@ -11,13 +11,13 @@
</style> </style>
<!-- 引入 uni-app web-view SDK必须 --> <!-- 引入 uni-app web-view SDK必须 -->
<script type="text/javascript" src="/websdk.js"></script> <script type="text/javascript" src="/websdk.js"></script>
<script type="module" crossorigin src="/assets/index-BQZfedpY.js"></script> <script type="module" crossorigin src="/assets/index-BQxyt58_.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js"> <link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">
<link rel="modulepreload" crossorigin href="/assets/ui-DUe_gloh.js"> <link rel="modulepreload" crossorigin href="/assets/ui-D5qYGnLz.js">
<link rel="modulepreload" crossorigin href="/assets/charts-62ymwWYF.js"> <link rel="modulepreload" crossorigin href="/assets/charts-M0qaf_ew.js">
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css"> <link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
<link rel="stylesheet" crossorigin href="/assets/index-ejYsXKTB.css"> <link rel="stylesheet" crossorigin href="/assets/index-B6B8u-1D.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,50 +1,100 @@
.headerContainer { .sidebarMenu {
background: #fff; height: 100%;
border-bottom: 1px solid #f0f0f0; display: flex;
} flex-direction: column;
.searchBar {
padding: 16px 16px 8px;
background: #fff; background: #fff;
:global(.ant-input) { .headerContainer {
border-radius: 20px; padding: 16px;
background: #f5f5f5; background: #fff;
border: none; border-bottom: 1px solid #f0f0f0;
&:focus { .searchBar {
margin-bottom: 16px;
padding: 0;
background: #fff; background: #fff;
border: 1px solid #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); :global(.ant-input) {
border-radius: 20px;
background: #f5f5f5;
border: none;
&:focus {
background: #fff;
border: 1px solid #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
.tabsContainer {
display: flex;
justify-content: space-around;
border-bottom: 1px solid #f0f0f0;
padding: 0 0 8px;
.tabItem {
padding: 8px 0;
flex: 1;
text-align: center;
cursor: pointer;
color: #999;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
&.active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
span {
margin-left: 4px;
}
}
} }
} }
} }
.tabsContainer { // 骨架屏样式
.skeletonContainer {
height: 100%;
padding: 16px;
display: flex; display: flex;
padding: 0 16px 8px; flex-direction: column;
border-bottom: 1px solid #f0f0f0;
.tabItem { .searchBarSkeleton {
margin-bottom: 16px;
}
.tabsContainerSkeleton {
display: flex; display: flex;
align-items: center; justify-content: space-around;
padding: 8px 12px; margin-bottom: 16px;
cursor: pointer; padding-bottom: 8px;
border-radius: 4px; border-bottom: 1px solid #f0f0f0;
transition: all 0.3s; gap: 8px;
}
&:hover { .contactListSkeleton {
color: #1890ff; flex: 1;
background-color: rgba(24, 144, 255, 0.1); overflow-y: auto;
}
&.active { .contactItemSkeleton {
color: #1890ff; display: flex;
background-color: rgba(24, 144, 255, 0.1); align-items: center;
} padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
span { .contactInfoSkeleton {
margin-left: 4px; margin-left: 12px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
} }
} }
} }
@@ -59,3 +109,16 @@
padding: 20px; padding: 20px;
text-align: center; text-align: center;
} }
.contentContainer {
flex: 1;
overflow-y: auto;
}
.footer {
padding: 10px;
text-align: center;
border-top: 1px solid #f0f0f0;
background: #fff;
display: none; /* 默认隐藏底部,如果需要显示可以移除此行 */
}

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Input } from "antd"; import { Input, Skeleton } from "antd";
import { import {
SearchOutlined, SearchOutlined,
UserOutlined, UserOutlined,
@@ -9,7 +9,6 @@ import {
import { ContractData } from "@/pages/pc/ckbox/data"; import { ContractData } from "@/pages/pc/ckbox/data";
import WechatFriendsModule from "./WechatFriendsModule"; import WechatFriendsModule from "./WechatFriendsModule";
import MessageList from "./MessageList/index"; import MessageList from "./MessageList/index";
import LayoutFiexd from "@/components/Layout/LayoutFiexd";
import styles from "./SidebarMenu.module.scss"; import styles from "./SidebarMenu.module.scss";
import { getChatSessions } from "@/store/module/ckchat"; import { getChatSessions } from "@/store/module/ckchat";
@@ -53,6 +52,51 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
); );
}; };
// 渲染骨架屏
const renderSkeleton = () => (
<div className={styles.skeletonContainer}>
<div className={styles.searchBarSkeleton}>
<Skeleton.Input active size="small" block />
</div>
<div className={styles.tabsContainerSkeleton}>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
</div>
<div className={styles.contactListSkeleton}>
{Array(8)
.fill(null)
.map((_, index) => (
<div
key={`contact-skeleton-${index}`}
className={styles.contactItemSkeleton}
>
<Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.contactInfoSkeleton}>
<Skeleton.Input active size="small" style={{ width: "60%" }} />
<Skeleton.Input active size="small" style={{ width: "80%" }} />
</div>
</div>
))}
</div>
</div>
);
// 渲染Header部分包含搜索框和标签页切换 // 渲染Header部分包含搜索框和标签页切换
const renderHeader = () => ( const renderHeader = () => (
<div className={styles.headerContainer}> <div className={styles.headerContainer}>
@@ -125,10 +169,15 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
} }
}; };
if (loading) {
return renderSkeleton();
}
return ( return (
<LayoutFiexd header={renderHeader()} footer="底部"> <div className={styles.sidebarMenu}>
{renderContent()} {renderHeader()}
</LayoutFiexd> <div className={styles.contentContainer}>{renderContent()}</div>
</div>
); );
}; };

View File

@@ -0,0 +1,111 @@
.skeletonLayout {
height: 100vh;
display: flex;
flex-direction: column;
.skeletonHeader {
height: 64px;
padding: 0 24px;
display: flex;
align-items: center;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
.skeletonVerticalSider {
background-color: #fff;
border-right: 1px solid #f0f0f0;
.verticalUserList {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 0;
.verticalUserItem {
margin-bottom: 16px;
}
}
}
.skeletonSider {
background-color: #fff;
border-right: 1px solid #f0f0f0;
padding: 16px;
.searchSkeleton {
margin-bottom: 16px;
}
.tabsSkeleton {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.contactListSkeleton {
.contactItemSkeleton {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
.contactInfoSkeleton {
margin-left: 12px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
}
}
}
.skeletonMainContent {
background-color: #f5f5f5;
padding: 16px;
display: flex;
flex-direction: column;
.chatHeaderSkeleton {
background-color: #fff;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
border-radius: 8px 8px 0 0;
}
.chatContentSkeleton {
flex: 1;
background-color: #fff;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
.messageSkeleton {
display: flex;
align-items: flex-start;
gap: 8px;
&.leftMessage {
align-self: flex-start;
}
&.rightMessage {
align-self: flex-end;
flex-direction: row-reverse;
}
}
}
.inputAreaSkeleton {
background-color: #fff;
padding: 16px;
border-radius: 0 0 8px 8px;
border-top: 1px solid #f0f0f0;
}
}
}

View File

@@ -0,0 +1,91 @@
import React from 'react';
import { Skeleton, Layout } from 'antd';
import styles from './index.module.scss';
import pageStyles from '../../index.module.scss';
const { Header, Content, Sider } = Layout;
interface PageSkeletonProps {
loading: boolean;
children: React.ReactNode;
}
/**
* 页面骨架屏组件
* 在数据加载完成前显示骨架屏
*/
const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
if (!loading) return <>{children}</>;
return (
<Layout className={pageStyles.ckboxLayout}>
<Header className={pageStyles.header}>
<Skeleton.Button active size="large" shape="square" block />
</Header>
<Layout>
{/* 垂直侧边栏骨架 */}
<Sider width={60} className={pageStyles.verticalSider}>
<div className={styles.verticalUserList}>
{Array(5)
.fill(null)
.map((_, index) => (
<div key={`vertical-${index}`} className={styles.verticalUserItem}>
<Skeleton.Avatar active size="large" shape="circle" />
</div>
))}
</div>
</Sider>
{/* 左侧联系人边栏骨架 */}
<Sider width={280} className={pageStyles.sider}>
<div className={styles.searchSkeleton}>
<Skeleton.Input active size="small" block />
</div>
<div className={styles.tabsSkeleton}>
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} />
<Skeleton.Button active size="small" shape="square" style={{ width: '30%' }} />
</div>
<div className={styles.contactListSkeleton}>
{Array(8)
.fill(null)
.map((_, index) => (
<div key={`contact-${index}`} className={styles.contactItemSkeleton}>
<Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.contactInfoSkeleton}>
<Skeleton.Input active size="small" style={{ width: '60%' }} />
<Skeleton.Input active size="small" style={{ width: '80%' }} />
</div>
</div>
))}
</div>
</Sider>
{/* 主内容区骨架 */}
<Content className={styles.skeletonMainContent}>
<div className={styles.chatHeaderSkeleton}>
<Skeleton.Avatar active size="large" shape="circle" />
<Skeleton.Input active size="small" style={{ width: '30%' }} />
</div>
<div className={styles.chatContentSkeleton}>
{Array(5)
.fill(null)
.map((_, index) => (
<div
key={`message-${index}`}
className={`${styles.messageSkeleton} ${index % 2 === 0 ? styles.leftMessage : styles.rightMessage}`}
>
<Skeleton.Avatar active size="small" shape="circle" />
<Skeleton.Input active size="small" style={{ width: index % 2 === 0 ? '60%' : '40%' }} />
</div>
))}
</div>
<div className={styles.inputAreaSkeleton}>
<Skeleton.Input active size="large" block />
</div>
</Content>
</Layout>
</Layout>
);
};
export default PageSkeleton;

View File

@@ -1,15 +1,17 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Layout, Button, Space, message, Tooltip, Spin } from "antd"; import { Layout, Button, Space, message, Tooltip } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons"; import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ContractData } from "./data"; import { ContractData } from "./data";
import ChatWindow from "./components/ChatWindow/index"; import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index"; import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList"; import VerticalUserList from "./components/VerticalUserList";
import PageSkeleton from "./components/Skeleton";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat"; import { addChatSession } from "@/store/module/ckchat";
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main"; import { chatInitAPIdata } from "./main";
import { KfUserListData } from "@/store/module/ckchat.data";
const CkboxPage: React.FC = () => { const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
@@ -25,7 +27,17 @@ const CkboxPage: React.FC = () => {
setLoading(true); setLoading(true);
chatInitAPIdata() chatInitAPIdata()
.then(response => { .then(response => {
const { contractList, chatRoomList, kfUserList } = response; const data = response as {
contractList: any[];
groupList: any[];
kfUserList: KfUserListData[];
newContractList: { groupName: string; contacts: any[] }[];
};
const { contractList, groupList, kfUserList, newContractList } = data;
response;
console.log(contractList, groupList, kfUserList, newContractList);
//找出已经在聊天的 //找出已经在聊天的
const isChatList = contractList.filter( const isChatList = contractList.filter(
v => (v?.config && v.config?.chat) || false, v => (v?.config && v.config?.chat) || false,
@@ -77,66 +89,69 @@ const CkboxPage: React.FC = () => {
}; };
return ( return (
<Layout className={styles.ckboxLayout}> <PageSkeleton loading={loading}>
{contextHolder} <Layout className={styles.ckboxLayout}>
<Header className={styles.header}></Header> {contextHolder}
<Layout> <Header className={styles.header}></Header>
{/* 垂直侧边栏 */} <Layout>
{/* 垂直侧边栏 */}
<Sider width={60} className={styles.verticalSider}> <Sider width={60} className={styles.verticalSider}>
<VerticalUserList <VerticalUserList
activeKfUserId={activeVerticalUserId} activeKfUserId={activeVerticalUserId}
onUserSelect={handleVerticalUserSelect} onUserSelect={handleVerticalUserSelect}
/> />
</Sider> </Sider>
{/* 左侧联系人边栏 */} {/* 左侧联系人边栏 */}
<Sider width={280} className={styles.sider}> <Sider width={280} className={styles.sider}>
<SidebarMenu <SidebarMenu
contracts={contracts} contracts={contracts}
currentChat={currentChat} currentChat={currentChat}
onContactClick={handleContactClick} onContactClick={handleContactClick}
onChatSelect={setCurrentChat} onChatSelect={setCurrentChat}
/> loading={loading}
</Sider> />
</Sider>
{/* 主内容区 */} {/* 主内容区 */}
<Content className={styles.mainContent}> <Content className={styles.mainContent}>
{currentChat ? ( {currentChat ? (
<div className={styles.chatContainer}> <div className={styles.chatContainer}>
<div className={styles.chatToolbar}> <div className={styles.chatToolbar}>
<Space> <Space>
<Tooltip title={showProfile ? "隐藏资料" : "显示资料"}> <Tooltip title={showProfile ? "隐藏资料" : "显示资料"}>
<Button <Button
type={showProfile ? "primary" : "default"} type={showProfile ? "primary" : "default"}
icon={<InfoCircleOutlined />} icon={<InfoCircleOutlined />}
onClick={() => setShowProfile(!showProfile)} onClick={() => setShowProfile(!showProfile)}
size="small" size="small"
> >
{showProfile ? "隐藏资料" : "显示资料"} {showProfile ? "隐藏资料" : "显示资料"}
</Button> </Button>
</Tooltip> </Tooltip>
</Space> </Space>
</div>
<ChatWindow
contract={currentChat}
onSendMessage={handleSendMessage}
showProfile={showProfile}
onToggleProfile={() => setShowProfile(!showProfile)}
/>
</div> </div>
<ChatWindow ) : (
contract={currentChat} <div className={styles.welcomeScreen}>
onSendMessage={handleSendMessage} <div className={styles.welcomeContent}>
showProfile={showProfile} <MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
onToggleProfile={() => setShowProfile(!showProfile)} <h2>使</h2>
/> <p></p>
</div> </div>
) : (
<div className={styles.welcomeScreen}>
<div className={styles.welcomeContent}>
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
<h2>使</h2>
<p></p>
</div> </div>
</div> )}
)} </Content>
</Content> </Layout>
</Layout> </Layout>
</Layout> </PageSkeleton>
); );
}; };

View File

@@ -15,6 +15,11 @@ export const useCkChatStore = createPersistStore<CkChatState>(
contractList: [], //联系人列表 contractList: [], //联系人列表
chatSessions: [], //聊天会话 chatSessions: [], //聊天会话
kfUserList: [], //客服列表 kfUserList: [], //客服列表
newContractList: [], //联系人分组
// 异步设置会话列表
asyncNewContractList: data => {
set({ newContractList: data });
},
// 异步设置会话列表 // 异步设置会话列表
asyncChatSessions: data => { asyncChatSessions: data => {
set({ chatSessions: data }); set({ chatSessions: data });
@@ -182,3 +187,6 @@ export const asyncContractList = (data: ContractData[]) =>
useCkChatStore.getState().asyncContractList(data); useCkChatStore.getState().asyncContractList(data);
export const asyncChatSessions = (data: ContractData[]) => export const asyncChatSessions = (data: ContractData[]) =>
useCkChatStore.getState().asyncChatSessions(data); useCkChatStore.getState().asyncChatSessions(data);
export const asyncNewContractList = (
data: { groupName: string; contacts: any[] }[],
) => useCkChatStore.getState().asyncNewContractList(data);