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

View File

@@ -11,13 +11,13 @@
</style>
<!-- 引入 uni-app web-view SDK必须 -->
<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/utils-6WF66_dS.js">
<link rel="modulepreload" crossorigin href="/assets/ui-DUe_gloh.js">
<link rel="modulepreload" crossorigin href="/assets/charts-62ymwWYF.js">
<link rel="modulepreload" crossorigin href="/assets/ui-D5qYGnLz.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/index-ejYsXKTB.css">
<link rel="stylesheet" crossorigin href="/assets/index-B6B8u-1D.css">
</head>
<body>
<div id="root"></div>

View File

@@ -1,50 +1,100 @@
.headerContainer {
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.searchBar {
padding: 16px 16px 8px;
.sidebarMenu {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
:global(.ant-input) {
border-radius: 20px;
background: #f5f5f5;
border: none;
.headerContainer {
padding: 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
&:focus {
.searchBar {
margin-bottom: 16px;
padding: 0;
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;
padding: 0 16px 8px;
border-bottom: 1px solid #f0f0f0;
flex-direction: column;
.tabItem {
.searchBarSkeleton {
margin-bottom: 16px;
}
.tabsContainerSkeleton {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s;
justify-content: space-around;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
gap: 8px;
}
&:hover {
color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
.contactListSkeleton {
flex: 1;
overflow-y: auto;
&.active {
color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
.contactItemSkeleton {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
span {
margin-left: 4px;
.contactInfoSkeleton {
margin-left: 12px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
}
}
}
@@ -59,3 +109,16 @@
padding: 20px;
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 { Input } from "antd";
import { Input, Skeleton } from "antd";
import {
SearchOutlined,
UserOutlined,
@@ -9,7 +9,6 @@ import {
import { ContractData } from "@/pages/pc/ckbox/data";
import WechatFriendsModule from "./WechatFriendsModule";
import MessageList from "./MessageList/index";
import LayoutFiexd from "@/components/Layout/LayoutFiexd";
import styles from "./SidebarMenu.module.scss";
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部分包含搜索框和标签页切换
const renderHeader = () => (
<div className={styles.headerContainer}>
@@ -125,10 +169,15 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
}
};
if (loading) {
return renderSkeleton();
}
return (
<LayoutFiexd header={renderHeader()} footer="底部">
{renderContent()}
</LayoutFiexd>
<div className={styles.sidebarMenu}>
{renderHeader()}
<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 { Layout, Button, Space, message, Tooltip, Spin } from "antd";
import { Layout, Button, Space, message, Tooltip } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData } from "./data";
import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList";
import PageSkeleton from "./components/Skeleton";
import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat";
const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
import { KfUserListData } from "@/store/module/ckchat.data";
const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
@@ -25,7 +27,17 @@ const CkboxPage: React.FC = () => {
setLoading(true);
chatInitAPIdata()
.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(
v => (v?.config && v.config?.chat) || false,
@@ -77,66 +89,69 @@ const CkboxPage: React.FC = () => {
};
return (
<Layout className={styles.ckboxLayout}>
{contextHolder}
<Header className={styles.header}></Header>
<Layout>
{/* 垂直侧边栏 */}
<PageSkeleton loading={loading}>
<Layout className={styles.ckboxLayout}>
{contextHolder}
<Header className={styles.header}></Header>
<Layout>
{/* 垂直侧边栏 */}
<Sider width={60} className={styles.verticalSider}>
<VerticalUserList
activeKfUserId={activeVerticalUserId}
onUserSelect={handleVerticalUserSelect}
/>
</Sider>
<Sider width={60} className={styles.verticalSider}>
<VerticalUserList
activeKfUserId={activeVerticalUserId}
onUserSelect={handleVerticalUserSelect}
/>
</Sider>
{/* 左侧联系人边栏 */}
<Sider width={280} className={styles.sider}>
<SidebarMenu
contracts={contracts}
currentChat={currentChat}
onContactClick={handleContactClick}
onChatSelect={setCurrentChat}
/>
</Sider>
{/* 左侧联系人边栏 */}
<Sider width={280} className={styles.sider}>
<SidebarMenu
contracts={contracts}
currentChat={currentChat}
onContactClick={handleContactClick}
onChatSelect={setCurrentChat}
loading={loading}
/>
</Sider>
{/* 主内容区 */}
<Content className={styles.mainContent}>
{currentChat ? (
<div className={styles.chatContainer}>
<div className={styles.chatToolbar}>
<Space>
<Tooltip title={showProfile ? "隐藏资料" : "显示资料"}>
<Button
type={showProfile ? "primary" : "default"}
icon={<InfoCircleOutlined />}
onClick={() => setShowProfile(!showProfile)}
size="small"
>
{showProfile ? "隐藏资料" : "显示资料"}
</Button>
</Tooltip>
</Space>
{/* 主内容区 */}
<Content className={styles.mainContent}>
{currentChat ? (
<div className={styles.chatContainer}>
<div className={styles.chatToolbar}>
<Space>
<Tooltip title={showProfile ? "隐藏资料" : "显示资料"}>
<Button
type={showProfile ? "primary" : "default"}
icon={<InfoCircleOutlined />}
onClick={() => setShowProfile(!showProfile)}
size="small"
>
{showProfile ? "隐藏资料" : "显示资料"}
</Button>
</Tooltip>
</Space>
</div>
<ChatWindow
contract={currentChat}
onSendMessage={handleSendMessage}
showProfile={showProfile}
onToggleProfile={() => setShowProfile(!showProfile)}
/>
</div>
<ChatWindow
contract={currentChat}
onSendMessage={handleSendMessage}
showProfile={showProfile}
onToggleProfile={() => setShowProfile(!showProfile)}
/>
</div>
) : (
<div className={styles.welcomeScreen}>
<div className={styles.welcomeContent}>
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
<h2>使</h2>
<p></p>
) : (
<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>
</PageSkeleton>
);
};

View File

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