Merge branch 'yongpxu-dev' into develop
This commit is contained in:
@@ -2,14 +2,12 @@ import React from "react";
|
||||
import { NavBar, Button } from "antd-mobile";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
interface PlaceholderPageProps {
|
||||
title: string;
|
||||
showBack?: boolean;
|
||||
showAddButton?: boolean;
|
||||
addButtonText?: string;
|
||||
showFooter?: boolean;
|
||||
}
|
||||
|
||||
const PlaceholderPage: React.FC<PlaceholderPageProps> = ({
|
||||
@@ -17,13 +15,12 @@ const PlaceholderPage: React.FC<PlaceholderPageProps> = ({
|
||||
showBack = true,
|
||||
showAddButton = false,
|
||||
addButtonText = "新建",
|
||||
showFooter = true,
|
||||
}) => {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
backArrow={showBack}
|
||||
back={showBack}
|
||||
style={{ background: "#fff" }}
|
||||
onBack={showBack ? () => window.history.back() : undefined}
|
||||
left={
|
||||
@@ -43,7 +40,6 @@ const PlaceholderPage: React.FC<PlaceholderPageProps> = ({
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={showFooter ? <MeauMobile /> : undefined}
|
||||
>
|
||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
||||
<h3>{title}页面</h3>
|
||||
|
||||
@@ -90,7 +90,7 @@ const Mine: React.FC = () => {
|
||||
description: "触客宝",
|
||||
icon: <PhoneOutlined />,
|
||||
count: 0,
|
||||
path: "/mine/ckbox",
|
||||
path: "/ckbox/weChat",
|
||||
bgColor: "#fff7e6",
|
||||
iconColor: "#fa8c16",
|
||||
},
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# 触客宝模块 (CKBox)
|
||||
|
||||
## 功能概述
|
||||
|
||||
触客宝模块是一个类似微信的聊天系统,提供实时消息、联系人管理、群组聊天等功能,专为企业客户服务场景设计。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 聊天功能
|
||||
|
||||
- 实时消息发送和接收
|
||||
- 支持文本、图片、文件等多种消息类型
|
||||
- 消息状态显示(发送中、已发送、已读)
|
||||
- 消息撤回和转发
|
||||
- 表情包和快捷回复
|
||||
|
||||
### 2. 联系人管理
|
||||
|
||||
- 联系人列表展示
|
||||
- 在线状态显示
|
||||
- 联系人搜索和筛选
|
||||
- 联系人资料查看
|
||||
- 添加/删除联系人
|
||||
|
||||
### 3. 群组功能
|
||||
|
||||
- 群组创建和管理
|
||||
- 群成员管理
|
||||
- 群组消息
|
||||
- 群组设置
|
||||
|
||||
### 4. 聊天会话
|
||||
|
||||
- 聊天会话列表
|
||||
- 未读消息提醒
|
||||
- 会话置顶和静音
|
||||
- 聊天记录管理
|
||||
- 会话搜索
|
||||
|
||||
### 5. 通话功能
|
||||
|
||||
- 语音通话
|
||||
- 视频通话
|
||||
- 通话记录
|
||||
|
||||
## 界面布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [搜索栏] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ [聊天] [联系人] [群组] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 聊天列表 │ │ │ │
|
||||
│ │ • 张三 (2) │ │ │ │
|
||||
│ │ • 技术支持群 │ │ 聊天窗口 │ │
|
||||
│ │ • 李四 │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
ckbox/
|
||||
├── index.tsx # 主页面
|
||||
├── data.ts # 数据类型定义
|
||||
├── api.ts # API接口
|
||||
├── index.module.scss # 主页面样式
|
||||
├── test.tsx # 测试页面
|
||||
├── README.md # 说明文档
|
||||
└── components/ # 组件目录
|
||||
├── ChatWindow.tsx # 聊天窗口
|
||||
├── ContactList.tsx # 联系人列表
|
||||
├── MessageList.tsx # 消息列表
|
||||
└── *.module.scss # 组件样式文件
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **React 18** - 前端框架
|
||||
- **TypeScript** - 类型安全
|
||||
- **Ant Design** - UI组件库
|
||||
- **SCSS** - 样式预处理器
|
||||
- **Day.js** - 日期处理
|
||||
- **WebSocket** - 实时通信
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 联系人 (ContractData)
|
||||
|
||||
```typescript
|
||||
interface ContractData {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
avatar?: string;
|
||||
online: boolean;
|
||||
lastSeen?: string;
|
||||
status?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 消息 (MessageData)
|
||||
|
||||
```typescript
|
||||
interface MessageData {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
replyTo?: string;
|
||||
forwardFrom?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 聊天会话 (ChatSession)
|
||||
|
||||
```typescript
|
||||
interface ChatSession {
|
||||
id: string;
|
||||
type: "private" | "group";
|
||||
name: string;
|
||||
avatar?: string;
|
||||
lastMessage: string;
|
||||
lastTime: string;
|
||||
unreadCount: number;
|
||||
online: boolean;
|
||||
members?: string[];
|
||||
pinned?: boolean;
|
||||
muted?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 路由配置
|
||||
|
||||
在路由配置中添加触客宝模块:
|
||||
|
||||
```typescript
|
||||
// router/module/ckbox.tsx
|
||||
import React from "react";
|
||||
import { lazy } from "react";
|
||||
import type { RouteObject } from "react-router-dom";
|
||||
|
||||
const CkboxPage = lazy(() => import("@/pages/pc/ckbox"));
|
||||
|
||||
const ckboxRoutes: (RouteObject & { auth?: boolean; requiredRole?: string })[] = [
|
||||
{
|
||||
path: "/ckbox",
|
||||
element: <CkboxPage />,
|
||||
auth: true,
|
||||
requiredRole: "user",
|
||||
},
|
||||
];
|
||||
|
||||
export default ckboxRoutes;
|
||||
```
|
||||
|
||||
### 2. API接口
|
||||
|
||||
确保后端提供以下API接口:
|
||||
|
||||
- `GET /v1/contracts` - 获取联系人列表
|
||||
- `GET /v1/chats/sessions` - 获取聊天会话列表
|
||||
- `GET /v1/chats/:id/messages` - 获取聊天历史
|
||||
- `POST /v1/chats/:id/messages` - 发送消息
|
||||
- `GET /v1/groups` - 获取群组列表
|
||||
- `POST /v1/groups` - 创建群组
|
||||
|
||||
### 3. 访问页面
|
||||
|
||||
访问 `/ckbox` 路径即可使用触客宝模块。
|
||||
|
||||
## 组件说明
|
||||
|
||||
### ChatWindow
|
||||
|
||||
聊天窗口组件,包含:
|
||||
|
||||
- 聊天头部(联系人信息、通话按钮、更多操作)
|
||||
- 消息内容区域(消息气泡、时间显示)
|
||||
- 输入区域(文本输入、表情、附件、发送按钮)
|
||||
|
||||
### ContactList
|
||||
|
||||
联系人列表组件,包含:
|
||||
|
||||
- 联系人头像和在线状态
|
||||
- 联系人姓名和状态
|
||||
- 点击进入聊天
|
||||
|
||||
### MessageList
|
||||
|
||||
消息列表组件,包含:
|
||||
|
||||
- 聊天会话列表
|
||||
- 最后消息和时间
|
||||
- 未读消息数量
|
||||
- 在线状态显示
|
||||
|
||||
## 样式特点
|
||||
|
||||
- 类似微信的界面设计
|
||||
- 响应式布局,支持移动端
|
||||
- 现代化的UI风格
|
||||
- 良好的用户体验
|
||||
|
||||
## 开发规范
|
||||
|
||||
- 使用TypeScript进行类型检查
|
||||
- 遵循ESLint代码规范
|
||||
- 组件按功能拆分,保持单一职责
|
||||
- API接口统一管理
|
||||
- 错误处理和用户反馈
|
||||
|
||||
## 扩展功能
|
||||
|
||||
可以根据需要扩展以下功能:
|
||||
|
||||
- 消息加密
|
||||
- 语音消息
|
||||
- 视频消息
|
||||
- 消息搜索
|
||||
- 消息备份
|
||||
- 多端同步
|
||||
- 消息提醒
|
||||
- 自动回复
|
||||
@@ -0,0 +1,44 @@
|
||||
// 菜单项接口
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// 菜单列表数据
|
||||
export const menuList: MenuItem[] = [
|
||||
{
|
||||
id: "dashboard",
|
||||
title: "数据面板",
|
||||
icon: "📊",
|
||||
path: "/ckbox/dashboard",
|
||||
},
|
||||
{
|
||||
id: "wechat",
|
||||
title: "微信管理",
|
||||
icon: "💬",
|
||||
path: "/ckbox/weChat",
|
||||
},
|
||||
];
|
||||
|
||||
// 抽屉菜单配置数据
|
||||
export const drawerMenuData = {
|
||||
header: {
|
||||
logoIcon: "✨",
|
||||
appName: "触客宝",
|
||||
appDesc: "AI智能营销系统",
|
||||
},
|
||||
primaryButton: {
|
||||
title: "AI智能客服",
|
||||
icon: "🔒",
|
||||
},
|
||||
footer: {
|
||||
balanceIcon: "⚡",
|
||||
balanceLabel: "算力余额",
|
||||
balanceValue: "9307.423",
|
||||
},
|
||||
};
|
||||
|
||||
// 导出默认配置
|
||||
export default drawerMenuData;
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import type { MenuProps } from "antd";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { drawerMenuData, menuList } from "./index.data";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header } = Layout;
|
||||
@@ -16,16 +17,14 @@ const { Header } = Layout;
|
||||
interface NavCommonProps {
|
||||
title?: string;
|
||||
onMenuClick?: () => void;
|
||||
drawerContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
const NavCommon: React.FC<NavCommonProps> = ({
|
||||
title = "触客宝",
|
||||
onMenuClick,
|
||||
drawerContent,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { userInfo } = useCkChatStore();
|
||||
|
||||
// 处理菜单图标点击
|
||||
@@ -44,31 +43,55 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
||||
<div className={styles.drawerContent}>
|
||||
<div className={styles.drawerHeader}>
|
||||
<div className={styles.logoSection}>
|
||||
<div className={styles.logoIcon}>✨</div>
|
||||
<div className={styles.logoIcon}>
|
||||
{drawerMenuData.header.logoIcon}
|
||||
</div>
|
||||
<div className={styles.logoText}>
|
||||
<div className={styles.appName}>触客宝</div>
|
||||
<div className={styles.appDesc}>AI智能营销系统</div>
|
||||
<div className={styles.appName}>
|
||||
{drawerMenuData.header.appName}
|
||||
</div>
|
||||
<div className={styles.appDesc}>
|
||||
{drawerMenuData.header.appDesc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.drawerBody}>
|
||||
<div className={styles.primaryButton}>
|
||||
<div className={styles.buttonIcon}>🔒</div>
|
||||
<span>AI智能客服</span>
|
||||
<div className={styles.buttonIcon}>
|
||||
{drawerMenuData.primaryButton.icon}
|
||||
</div>
|
||||
<span>{drawerMenuData.primaryButton.title}</span>
|
||||
</div>
|
||||
<div className={styles.menuSection}>
|
||||
<div className={styles.menuItem}>
|
||||
<div className={styles.menuIcon}>📊</div>
|
||||
<span>功能中心</span>
|
||||
</div>
|
||||
{menuList.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.menuItem}
|
||||
onClick={() => {
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
setDrawerVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.menuIcon}>{item.icon}</div>
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.drawerFooter}>
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceIcon}>
|
||||
<span className={styles.suanliIcon}>⚡</span>算力余额
|
||||
<span className={styles.suanliIcon}>
|
||||
{drawerMenuData.footer.balanceIcon}
|
||||
</span>
|
||||
{drawerMenuData.footer.balanceLabel}
|
||||
</div>
|
||||
<div className={styles.balanceText}>
|
||||
{drawerMenuData.footer.balanceValue}
|
||||
</div>
|
||||
<div className={styles.balanceText}>9307.423</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +134,7 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
||||
width={300}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent || defaultDrawerContent}
|
||||
{defaultDrawerContent}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
190
Cunkebao/src/pages/pc/ckbox/dashboard/index.module.scss
Normal file
190
Cunkebao/src/pages/pc/ckbox/dashboard/index.module.scss
Normal file
@@ -0,0 +1,190 @@
|
||||
.monitoring {
|
||||
padding: 24px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
|
||||
.header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #262626;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.statsRow {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.statCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progressRow {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.progressCard,
|
||||
.metricsCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
height: 280px;
|
||||
|
||||
.ant-card-body {
|
||||
height: calc(100% - 57px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.progressItem {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.metricItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chartsRow {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.chartCard {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// ECharts容器样式
|
||||
.echarts-for-react {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tableRow {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
.ant-card-head {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
|
||||
.ant-card-head-title {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr {
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.monitoring {
|
||||
padding: 16px;
|
||||
|
||||
.header {
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.progressRow {
|
||||
.progressCard,
|
||||
.metricsCard {
|
||||
height: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.monitoring {
|
||||
padding: 12px;
|
||||
|
||||
.statsRow {
|
||||
.statCard {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
473
Cunkebao/src/pages/pc/ckbox/dashboard/index.tsx
Normal file
473
Cunkebao/src/pages/pc/ckbox/dashboard/index.tsx
Normal file
@@ -0,0 +1,473 @@
|
||||
import React from "react";
|
||||
import { Card, Row, Col, Statistic, Progress, Table, Tag } from "antd";
|
||||
import {
|
||||
UserOutlined,
|
||||
MessageOutlined,
|
||||
TeamOutlined,
|
||||
TrophyOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import * as echarts from "echarts";
|
||||
import ReactECharts from "echarts-for-react";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface DashboardProps {
|
||||
// 预留接口属性
|
||||
}
|
||||
|
||||
const Dashboard: React.FC<DashboardProps> = () => {
|
||||
// 模拟数据
|
||||
const statsData = [
|
||||
{
|
||||
title: "在线设备数",
|
||||
value: 128,
|
||||
prefix: <UserOutlined />,
|
||||
suffix: "台",
|
||||
valueStyle: { color: "#3f8600" },
|
||||
},
|
||||
{
|
||||
title: "今日消息量",
|
||||
value: 2456,
|
||||
prefix: <MessageOutlined />,
|
||||
suffix: "条",
|
||||
valueStyle: { color: "#1890ff" },
|
||||
},
|
||||
{
|
||||
title: "活跃群组",
|
||||
value: 89,
|
||||
prefix: <TeamOutlined />,
|
||||
suffix: "个",
|
||||
valueStyle: { color: "#722ed1" },
|
||||
},
|
||||
{
|
||||
title: "成功率",
|
||||
value: 98.5,
|
||||
prefix: <TrophyOutlined />,
|
||||
suffix: "%",
|
||||
valueStyle: { color: "#f5222d" },
|
||||
},
|
||||
];
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
title: "设备名称",
|
||||
dataIndex: "deviceName",
|
||||
key: "deviceName",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (status: string) => (
|
||||
<Tag color={status === "在线" ? "green" : "red"}>{status}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "消息数",
|
||||
dataIndex: "messageCount",
|
||||
key: "messageCount",
|
||||
},
|
||||
{
|
||||
title: "最后活跃时间",
|
||||
dataIndex: "lastActive",
|
||||
key: "lastActive",
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
key: "1",
|
||||
deviceName: "设备001",
|
||||
status: "在线",
|
||||
messageCount: 245,
|
||||
lastActive: "2024-01-15 14:30:25",
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
deviceName: "设备002",
|
||||
status: "离线",
|
||||
messageCount: 156,
|
||||
lastActive: "2024-01-15 12:15:10",
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
deviceName: "设备003",
|
||||
status: "在线",
|
||||
messageCount: 389,
|
||||
lastActive: "2024-01-15 14:28:45",
|
||||
},
|
||||
];
|
||||
|
||||
// 图表数据
|
||||
const lineData = [
|
||||
{ time: "00:00", value: 120 },
|
||||
{ time: "02:00", value: 132 },
|
||||
{ time: "04:00", value: 101 },
|
||||
{ time: "06:00", value: 134 },
|
||||
{ time: "08:00", value: 190 },
|
||||
{ time: "10:00", value: 230 },
|
||||
{ time: "12:00", value: 210 },
|
||||
{ time: "14:00", value: 220 },
|
||||
{ time: "16:00", value: 165 },
|
||||
{ time: "18:00", value: 127 },
|
||||
{ time: "20:00", value: 82 },
|
||||
{ time: "22:00", value: 91 },
|
||||
];
|
||||
|
||||
const columnData = [
|
||||
{ type: "消息发送", value: 27 },
|
||||
{ type: "消息接收", value: 25 },
|
||||
{ type: "群组管理", value: 18 },
|
||||
{ type: "设备监控", value: 15 },
|
||||
{ type: "数据同步", value: 10 },
|
||||
{ type: "其他", value: 5 },
|
||||
];
|
||||
|
||||
const pieData = [
|
||||
{ type: "在线设备", value: 128 },
|
||||
{ type: "离线设备", value: 32 },
|
||||
{ type: "维护中", value: 8 },
|
||||
];
|
||||
|
||||
const areaData = [
|
||||
{ time: "1月", value: 3000 },
|
||||
{ time: "2月", value: 4000 },
|
||||
{ time: "3月", value: 3500 },
|
||||
{ time: "4月", value: 5000 },
|
||||
{ time: "5月", value: 4900 },
|
||||
{ time: "6月", value: 6000 },
|
||||
];
|
||||
|
||||
// ECharts配置
|
||||
const lineOption = {
|
||||
title: {
|
||||
text: "24小时消息趋势",
|
||||
left: "center",
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: lineData.map(item => item.time),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: lineData.map(item => item.value),
|
||||
type: "line",
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: "#1890ff",
|
||||
width: 3,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#1890ff",
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "rgba(24, 144, 255, 0.3)" },
|
||||
{ offset: 1, color: "rgba(24, 144, 255, 0.1)" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const columnOption = {
|
||||
title: {
|
||||
text: "功能使用分布",
|
||||
left: "center",
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: columnData.map(item => item.type),
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: columnData.map(item => item.value),
|
||||
type: "bar",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#52c41a" },
|
||||
{ offset: 1, color: "#389e0d" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const pieOption = {
|
||||
title: {
|
||||
text: "设备状态分布",
|
||||
left: "center",
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
left: "left",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "设备状态",
|
||||
type: "pie",
|
||||
radius: "50%",
|
||||
data: pieData.map(item => ({ name: item.type, value: item.value })),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const areaOption = {
|
||||
title: {
|
||||
text: "月度数据趋势",
|
||||
left: "center",
|
||||
textStyle: {
|
||||
color: "#333",
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
textStyle: {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: areaData.map(item => item.time),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#ccc",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: areaData.map(item => item.value),
|
||||
type: "line",
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: "#722ed1",
|
||||
width: 3,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#722ed1",
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "rgba(114, 46, 209, 0.6)" },
|
||||
{ offset: 1, color: "rgba(114, 46, 209, 0.1)" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.monitoring}>
|
||||
<div className={styles.header}>
|
||||
<h2>数据监控看板</h2>
|
||||
<p>实时监控系统运行状态和数据指标</p>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]} className={styles.statsRow}>
|
||||
{statsData.map((stat, index) => (
|
||||
<Col xs={24} sm={12} md={6} key={index}>
|
||||
<Card className={styles.statCard}>
|
||||
<Statistic
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
prefix={stat.prefix}
|
||||
suffix={stat.suffix}
|
||||
valueStyle={stat.valueStyle}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 进度指标 */}
|
||||
<Row gutter={[16, 16]} className={styles.progressRow}>
|
||||
<Col xs={24} md={12}>
|
||||
<Card title="系统负载" className={styles.progressCard}>
|
||||
<div className={styles.progressItem}>
|
||||
<span>CPU使用率</span>
|
||||
<Progress percent={65} status="active" />
|
||||
</div>
|
||||
<div className={styles.progressItem}>
|
||||
<span>内存使用率</span>
|
||||
<Progress percent={45} />
|
||||
</div>
|
||||
<div className={styles.progressItem}>
|
||||
<span>磁盘使用率</span>
|
||||
<Progress percent={30} />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Card title="实时指标" className={styles.metricsCard}>
|
||||
<div className={styles.metricItem}>
|
||||
<span>消息处理速度</span>
|
||||
<div className={styles.metricValue}>
|
||||
<span>1,245</span>
|
||||
<ArrowUpOutlined style={{ color: "#3f8600" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.metricItem}>
|
||||
<span>错误率</span>
|
||||
<div className={styles.metricValue}>
|
||||
<span>0.2%</span>
|
||||
<ArrowDownOutlined style={{ color: "#3f8600" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.metricItem}>
|
||||
<span>响应时间</span>
|
||||
<div className={styles.metricValue}>
|
||||
<span>125ms</span>
|
||||
<ArrowDownOutlined style={{ color: "#3f8600" }} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 - 四栏布局 */}
|
||||
<Row gutter={[16, 16]} className={styles.chartsRow}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className={styles.chartCard}>
|
||||
<ReactECharts option={lineOption} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className={styles.chartCard}>
|
||||
<ReactECharts option={columnOption} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className={styles.chartCard}>
|
||||
<ReactECharts option={pieOption} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className={styles.chartCard}>
|
||||
<ReactECharts option={areaOption} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 设备状态表格 */}
|
||||
<Row className={styles.tableRow}>
|
||||
<Col span={24}>
|
||||
<Card title="设备状态" className={styles.tableCard}>
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,105 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Layout, Button, Space, message, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import ChatWindow from "./components/ChatWindow/index";
|
||||
import SidebarMenu from "./components/SidebarMenu/index";
|
||||
import VerticalUserList from "./components/VerticalUserList";
|
||||
import PageSkeleton from "./components/Skeleton";
|
||||
import React from "react";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import NavCommon from "./components/NavCommon";
|
||||
import styles from "./index.module.scss";
|
||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
||||
const { Content, Sider } = Layout;
|
||||
import { chatInitAPIdata, initSocket } from "./main";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
|
||||
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
const CkboxPage: React.FC = () => {
|
||||
// 不要在组件初始化时获取sendCommand,而是在需要时动态获取
|
||||
const [loading, setLoading] = useState(false);
|
||||
const currentContract = useWeChatStore(state => state.currentContract);
|
||||
useEffect(() => {
|
||||
// 方法一:使用 Promise 链式调用处理异步函数
|
||||
setLoading(true);
|
||||
chatInitAPIdata()
|
||||
.then(response => {
|
||||
const data = response as {
|
||||
contractList: any[];
|
||||
groupList: any[];
|
||||
kfUserList: KfUserListData[];
|
||||
newContractList: { groupName: string; contacts: any[] }[];
|
||||
};
|
||||
const { contractList } = data;
|
||||
|
||||
//找出已经在聊天的
|
||||
const isChatList = contractList.filter(
|
||||
v => (v?.config && v.config?.chat) || false,
|
||||
);
|
||||
isChatList.forEach(v => {
|
||||
addChatSession(v);
|
||||
});
|
||||
|
||||
// 数据加载完成后初始化WebSocket连接
|
||||
initSocket();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取联系人列表失败:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageSkeleton loading={loading}>
|
||||
<Layout className={styles.ckboxLayout}>
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon title="AI自动聊天,懂业务,会引导,客户不停地聊不停" />
|
||||
<Layout>
|
||||
{/* 垂直侧边栏 */}
|
||||
|
||||
<Sider width={80} className={styles.verticalSider}>
|
||||
<VerticalUserList />
|
||||
</Sider>
|
||||
|
||||
{/* 左侧联系人边栏 */}
|
||||
<Sider width={280} className={styles.sider}>
|
||||
<SidebarMenu loading={loading} />
|
||||
</Sider>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<Content className={styles.mainContent}>
|
||||
{currentContract ? (
|
||||
<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={currentContract} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.welcomeScreen}>
|
||||
<div className={styles.welcomeContent}>
|
||||
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
|
||||
<h2>欢迎使用触客宝</h2>
|
||||
<p>选择一个联系人开始聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</PageSkeleton>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import React from "react";
|
||||
import { Card, Typography, Space, Button, Tag } from "antd";
|
||||
import {
|
||||
MessageOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const CkboxTestPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: "1200px", margin: "0 auto" }}>
|
||||
<Title level={2}>
|
||||
<MessageOutlined style={{ marginRight: "8px" }} />
|
||||
触客宝模块测试页面
|
||||
</Title>
|
||||
|
||||
<Paragraph>
|
||||
这是一个类似微信的聊天系统,提供实时消息、联系人管理、群组聊天等功能。
|
||||
</Paragraph>
|
||||
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
{/* 功能特性 */}
|
||||
<Card title="功能特性" size="small">
|
||||
<Space wrap>
|
||||
<Tag color="blue" icon={<MessageOutlined />}>
|
||||
实时聊天
|
||||
</Tag>
|
||||
<Tag color="green" icon={<UserOutlined />}>
|
||||
联系人管理
|
||||
</Tag>
|
||||
<Tag color="orange" icon={<TeamOutlined />}>
|
||||
群组聊天
|
||||
</Tag>
|
||||
<Tag color="purple" icon={<PhoneOutlined />}>
|
||||
语音通话
|
||||
</Tag>
|
||||
<Tag color="red" icon={<VideoCameraOutlined />}>
|
||||
视频通话
|
||||
</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 界面布局 */}
|
||||
<Card title="界面布局" size="small">
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #d9d9d9",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
backgroundColor: "#fafafa",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: "16px", height: "400px" }}>
|
||||
{/* 左侧边栏 */}
|
||||
<div
|
||||
style={{
|
||||
width: "300px",
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e8e8e8",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<strong>左侧边栏</strong>
|
||||
</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 搜索栏</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 聊天标签页</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 联系人标签页</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 群组标签页</div>
|
||||
</div>
|
||||
|
||||
{/* 主聊天区域 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e8e8e8",
|
||||
borderRadius: "8px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* 聊天头部 */}
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #e8e8e8",
|
||||
backgroundColor: "#fafafa",
|
||||
}}
|
||||
>
|
||||
<strong>聊天头部</strong>
|
||||
</div>
|
||||
|
||||
{/* 聊天内容 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "16px",
|
||||
backgroundColor: "#f5f5f5",
|
||||
}}
|
||||
>
|
||||
<strong>聊天内容区域</strong>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "16px",
|
||||
fontSize: "12px",
|
||||
color: "#666",
|
||||
}}
|
||||
>
|
||||
• 消息气泡显示
|
||||
<br />
|
||||
• 支持文本、图片、文件
|
||||
<br />
|
||||
• 消息时间显示
|
||||
<br />• 在线状态显示
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderTop: "1px solid #e8e8e8",
|
||||
backgroundColor: "#fafafa",
|
||||
}}
|
||||
>
|
||||
<strong>输入区域</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧个人资料 */}
|
||||
<div
|
||||
style={{
|
||||
width: "280px",
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #e8e8e8",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<strong>右侧个人资料</strong>
|
||||
</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 基本信息</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 联系信息</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 标签</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 个人简介</div>
|
||||
<div style={{ marginBottom: "8px" }}>• 操作按钮</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<Card title="使用说明" size="small">
|
||||
<Paragraph>
|
||||
<strong>1. 开始聊天:</strong>
|
||||
在左侧联系人列表中选择一个联系人,点击进入聊天界面。
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<strong>2. 发送消息:</strong>
|
||||
在聊天窗口底部的输入框中输入消息,按Enter键或点击发送按钮发送消息。
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<strong>3. 查看资料:</strong>
|
||||
聊天时右侧会显示联系人的详细资料,包括基本信息、联系方式等。
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<strong>4. 搜索功能:</strong>
|
||||
使用顶部搜索栏可以快速搜索联系人或群组。
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<strong>5. 切换标签:</strong>
|
||||
在左侧可以切换聊天、联系人、群组等不同标签页。
|
||||
</Paragraph>
|
||||
</Card>
|
||||
|
||||
{/* 技术特点 */}
|
||||
<Card title="技术特点" size="small">
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<strong>• React 18 + TypeScript:</strong>
|
||||
现代化的前端开发技术栈
|
||||
</div>
|
||||
<div>
|
||||
<strong>• Ant Design:</strong>
|
||||
企业级UI组件库,提供丰富的组件
|
||||
</div>
|
||||
<div>
|
||||
<strong>• SCSS Modules:</strong>
|
||||
样式隔离和模块化,避免样式冲突
|
||||
</div>
|
||||
<div>
|
||||
<strong>• 响应式设计:</strong>
|
||||
支持不同屏幕尺寸,移动端友好
|
||||
</div>
|
||||
<div>
|
||||
<strong>• 组件化架构:</strong>
|
||||
功能模块独立成组件,便于维护和扩展
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Card title="操作" size="small">
|
||||
<Space>
|
||||
<Button type="primary" icon={<MessageOutlined />}>
|
||||
进入聊天
|
||||
</Button>
|
||||
<Button icon={<UserOutlined />}>查看联系人</Button>
|
||||
<Button icon={<TeamOutlined />}>创建群组</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CkboxTestPage;
|
||||
246
Cunkebao/src/pages/pc/ckbox/weChat/api.ts
Normal file
246
Cunkebao/src/pages/pc/ckbox/weChat/api.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import request from "@/api/request2";
|
||||
import {
|
||||
MessageData,
|
||||
ChatHistoryResponse,
|
||||
MessageType,
|
||||
OnlineStatus,
|
||||
MessageStatus,
|
||||
FileUploadResponse,
|
||||
EmojiData,
|
||||
QuickReply,
|
||||
ChatSettings,
|
||||
} from "./data";
|
||||
|
||||
//读取聊天信息
|
||||
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
|
||||
|
||||
export function WechatGroup(params) {
|
||||
return request("/api/WechatGroup/list", params, "GET");
|
||||
}
|
||||
|
||||
//获取聊天记录-1 清除未读
|
||||
export function clearUnreadCount(params) {
|
||||
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
||||
}
|
||||
|
||||
//更新配置
|
||||
export function updateConfig(params) {
|
||||
return request("/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 request("/api/FriendMessage/SearchMessage", params, "GET");
|
||||
}
|
||||
export function getChatroomMessages(params: {
|
||||
wechatAccountId: number;
|
||||
wechatFriendId?: number;
|
||||
wechatChatroomId?: number;
|
||||
From: number;
|
||||
To: number;
|
||||
Count: number;
|
||||
olderData: boolean;
|
||||
}) {
|
||||
return request("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||
}
|
||||
|
||||
//获取群列表
|
||||
export function getGroupList(params: { prevId: number; count: number }) {
|
||||
return request(
|
||||
"/api/wechatChatroom/listExcludeMembersByPage?",
|
||||
params,
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
|
||||
//获取群成员
|
||||
export function getGroupMembers(params: { id: number }) {
|
||||
return request(
|
||||
"/api/WechatChatroom/listMembersByWechatChatroomId",
|
||||
params,
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
|
||||
//触客宝登陆
|
||||
export function loginWithToken(params: any) {
|
||||
return request(
|
||||
"/token",
|
||||
params,
|
||||
"POST",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
},
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
// 获取触客宝用户信息
|
||||
export function getChuKeBaoUserInfo() {
|
||||
return request("/api/account/self", {}, "GET");
|
||||
}
|
||||
|
||||
// 获取联系人列表
|
||||
export const getContactList = (params: { prevId: number; count: number }) => {
|
||||
return request("/api/wechatFriend/list", params, "GET");
|
||||
};
|
||||
|
||||
//获取控制终端列表
|
||||
export const getControlTerminalList = params => {
|
||||
return request("/api/wechataccount", params, "GET");
|
||||
};
|
||||
|
||||
// 获取聊天历史
|
||||
export const getChatHistory = (
|
||||
chatId: string,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
): Promise<ChatHistoryResponse> => {
|
||||
return request(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
|
||||
};
|
||||
|
||||
// 发送消息
|
||||
export const sendMessage = (
|
||||
chatId: string,
|
||||
content: string,
|
||||
type: MessageType = MessageType.TEXT,
|
||||
): Promise<MessageData> => {
|
||||
return request(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
|
||||
};
|
||||
|
||||
// 发送文件消息
|
||||
export const sendFileMessage = (
|
||||
chatId: string,
|
||||
file: File,
|
||||
type: MessageType,
|
||||
): Promise<MessageData> => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("type", type);
|
||||
return request(`/v1/chats/${chatId}/messages/file`, formData, "POST");
|
||||
};
|
||||
|
||||
// 标记消息为已读
|
||||
export const markMessageAsRead = (messageId: string): Promise<void> => {
|
||||
return request(`/v1/messages/${messageId}/read`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 标记聊天为已读
|
||||
export const markChatAsRead = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}/read`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 添加群组成员
|
||||
export const addGroupMembers = (
|
||||
groupId: string,
|
||||
memberIds: string[],
|
||||
): Promise<void> => {
|
||||
return request(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
|
||||
};
|
||||
|
||||
// 移除群组成员
|
||||
export const removeGroupMembers = (
|
||||
groupId: string,
|
||||
memberIds: string[],
|
||||
): Promise<void> => {
|
||||
return request(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
|
||||
};
|
||||
|
||||
// 获取在线状态
|
||||
export const getOnlineStatus = (userId: string): Promise<OnlineStatus> => {
|
||||
return request(`/v1/users/${userId}/status`, {}, "GET");
|
||||
};
|
||||
|
||||
// 获取消息状态
|
||||
export const getMessageStatus = (messageId: string): Promise<MessageStatus> => {
|
||||
return request(`/v1/messages/${messageId}/status`, {}, "GET");
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
export const uploadFile = (file: File): Promise<FileUploadResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return request("/v1/upload", formData, "POST");
|
||||
};
|
||||
|
||||
// 获取表情包列表
|
||||
export const getEmojiList = (): Promise<EmojiData[]> => {
|
||||
return request("/v1/emojis", {}, "GET");
|
||||
};
|
||||
|
||||
// 获取快捷回复列表
|
||||
export const getQuickReplies = (): Promise<QuickReply[]> => {
|
||||
return request("/v1/quick-replies", {}, "GET");
|
||||
};
|
||||
|
||||
// 添加快捷回复
|
||||
export const addQuickReply = (data: {
|
||||
content: string;
|
||||
category: string;
|
||||
}): Promise<QuickReply> => {
|
||||
return request("/v1/quick-replies", data, "POST");
|
||||
};
|
||||
|
||||
// 删除快捷回复
|
||||
export const deleteQuickReply = (id: string): Promise<void> => {
|
||||
return request(`/v1/quick-replies/${id}`, {}, "DELETE");
|
||||
};
|
||||
|
||||
// 获取聊天设置
|
||||
export const getChatSettings = (): Promise<ChatSettings> => {
|
||||
return request("/v1/chat/settings", {}, "GET");
|
||||
};
|
||||
|
||||
// 更新聊天设置
|
||||
export const updateChatSettings = (
|
||||
settings: Partial<ChatSettings>,
|
||||
): Promise<ChatSettings> => {
|
||||
return request("/v1/chat/settings", settings, "PUT");
|
||||
};
|
||||
|
||||
// 删除聊天会话
|
||||
export const deleteChatSession = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}`, {}, "DELETE");
|
||||
};
|
||||
|
||||
// 置顶聊天会话
|
||||
export const pinChatSession = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}/pin`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 取消置顶聊天会话
|
||||
export const unpinChatSession = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}/unpin`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 静音聊天会话
|
||||
export const muteChatSession = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}/mute`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 取消静音聊天会话
|
||||
export const unmuteChatSession = (chatId: string): Promise<void> => {
|
||||
return request(`/v1/chats/${chatId}/unmute`, {}, "PUT");
|
||||
};
|
||||
|
||||
// 转发消息
|
||||
export const forwardMessage = (
|
||||
messageId: string,
|
||||
targetChatIds: string[],
|
||||
): Promise<void> => {
|
||||
return request("/v1/messages/forward", { messageId, targetChatIds }, "POST");
|
||||
};
|
||||
|
||||
// 撤回消息
|
||||
export const recallMessage = (messageId: string): Promise<void> => {
|
||||
return request(`/v1/messages/${messageId}/recall`, {}, "PUT");
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.suanli {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
.suanliIcon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 2px solid #f0f0f0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
// 抽屉样式
|
||||
.drawer {
|
||||
:global(.ant-drawer-header) {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:global(.ant-drawer-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.drawerContent {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.drawerHeader {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.logoSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logoIcon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logoText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.appName {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.appDesc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.drawerBody {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.menuSection {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
font-size: 20px;
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drawerFooter {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
.balanceSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.balanceIcon {
|
||||
color: #666;
|
||||
.suanliIcon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.balanceText {
|
||||
color: #3d9c0d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.balanceLabel {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.balanceAmount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 280px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useState } from "react";
|
||||
import { Layout, Drawer, Avatar, Dropdown, Space, Button } from "antd";
|
||||
import {
|
||||
MenuOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { MenuProps } from "antd";
|
||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
interface NavCommonProps {
|
||||
title?: string;
|
||||
onMenuClick?: () => void;
|
||||
drawerContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
const NavCommon: React.FC<NavCommonProps> = ({
|
||||
title = "触客宝",
|
||||
onMenuClick,
|
||||
drawerContent,
|
||||
}) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
|
||||
const { userInfo } = useCkChatStore();
|
||||
|
||||
// 处理菜单图标点击
|
||||
const handleMenuClick = () => {
|
||||
setDrawerVisible(true);
|
||||
onMenuClick?.();
|
||||
};
|
||||
|
||||
// 处理抽屉关闭
|
||||
const handleDrawerClose = () => {
|
||||
setDrawerVisible(false);
|
||||
};
|
||||
|
||||
// 默认抽屉内容
|
||||
const defaultDrawerContent = (
|
||||
<div className={styles.drawerContent}>
|
||||
<div className={styles.drawerHeader}>
|
||||
<div className={styles.logoSection}>
|
||||
<div className={styles.logoIcon}>✨</div>
|
||||
<div className={styles.logoText}>
|
||||
<div className={styles.appName}>触客宝</div>
|
||||
<div className={styles.appDesc}>AI智能营销系统</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.drawerBody}>
|
||||
<div className={styles.primaryButton}>
|
||||
<div className={styles.buttonIcon}>🔒</div>
|
||||
<span>AI智能客服</span>
|
||||
</div>
|
||||
<div className={styles.menuSection}>
|
||||
<div className={styles.menuItem}>
|
||||
<div className={styles.menuIcon}>📊</div>
|
||||
<span>功能中心</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.drawerFooter}>
|
||||
<div className={styles.balanceSection}>
|
||||
<div className={styles.balanceIcon}>
|
||||
<span className={styles.suanliIcon}>⚡</span>算力余额
|
||||
</div>
|
||||
<div className={styles.balanceText}>9307.423</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={handleMenuClick}
|
||||
className={styles.menuButton}
|
||||
/>
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.headerRight}>
|
||||
<Space className={styles.userInfo}>
|
||||
<span className={styles.suanli}>
|
||||
<span className={styles.suanliIcon}>⚡</span>
|
||||
9307.423
|
||||
</span>
|
||||
<Avatar
|
||||
size={40}
|
||||
icon={<UserOutlined />}
|
||||
src={userInfo?.account?.avatar}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Drawer
|
||||
title="菜单"
|
||||
placement="left"
|
||||
onClose={handleDrawerClose}
|
||||
open={drawerVisible}
|
||||
width={300}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent || defaultDrawerContent}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavCommon;
|
||||
323
Cunkebao/src/pages/pc/ckbox/weChat/data.ts
Normal file
323
Cunkebao/src/pages/pc/ckbox/weChat/data.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
// 消息列表数据接口 - 支持weChatGroup和contracts两种数据类型
|
||||
export interface MessageListData {
|
||||
serverId: number | string; // 服务器ID作为主键
|
||||
id?: number; // 接口数据的原始ID字段
|
||||
|
||||
// 数据类型标识
|
||||
dataType: "weChatGroup" | "contracts"; // 数据类型:微信群组或联系人
|
||||
|
||||
// 通用字段(两种类型都有的字段)
|
||||
wechatAccountId: number; // 微信账号ID
|
||||
tenantId: number; // 租户ID
|
||||
accountId: number; // 账号ID
|
||||
nickname: string; // 昵称
|
||||
avatar?: string; // 头像
|
||||
groupId: number; // 分组ID
|
||||
config?: {
|
||||
chat: boolean;
|
||||
}; // 配置信息
|
||||
labels?: string[]; // 标签列表
|
||||
unreadCount: number; // 未读消息数
|
||||
|
||||
// 联系人特有字段(当dataType为'contracts'时使用)
|
||||
wechatId?: string; // 微信ID
|
||||
alias?: string; // 别名
|
||||
conRemark?: string; // 备注
|
||||
quanPin?: string; // 全拼
|
||||
gender?: number; // 性别
|
||||
region?: string; // 地区
|
||||
addFrom?: number; // 添加来源
|
||||
phone?: string; // 电话
|
||||
signature?: string; // 签名
|
||||
extendFields?: any; // 扩展字段
|
||||
city?: string; // 城市
|
||||
lastUpdateTime?: string; // 最后更新时间
|
||||
isPassed?: boolean; // 是否通过
|
||||
thirdParty?: any; // 第三方
|
||||
additionalPicture?: string; // 附加图片
|
||||
desc?: string; // 描述
|
||||
lastMessageTime?: number; // 最后消息时间
|
||||
duplicate?: boolean; // 是否重复
|
||||
|
||||
// 微信群组特有字段(当dataType为'weChatGroup'时使用)
|
||||
chatroomId?: string; // 群聊ID
|
||||
chatroomOwner?: string; // 群主
|
||||
chatroomAvatar?: string; // 群头像
|
||||
notice?: string; // 群公告
|
||||
selfDisplyName?: string; // 自己在群里的显示名称
|
||||
|
||||
[key: string]: any; // 兼容其他字段
|
||||
}
|
||||
|
||||
//联系人标签分组
|
||||
export interface ContactGroupByLabel {
|
||||
id: number;
|
||||
accountId?: number;
|
||||
groupName: string;
|
||||
tenantId?: number;
|
||||
count: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
//终端用户数据接口
|
||||
export interface KfUserListData {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
wechatId: string;
|
||||
nickname: string;
|
||||
alias: string;
|
||||
avatar: string;
|
||||
gender: number;
|
||||
region: string;
|
||||
signature: string;
|
||||
bindQQ: string;
|
||||
bindEmail: string;
|
||||
bindMobile: string;
|
||||
createTime: string;
|
||||
currentDeviceId: number;
|
||||
isDeleted: boolean;
|
||||
deleteTime: string;
|
||||
groupId: number;
|
||||
memo: string;
|
||||
wechatVersion: string;
|
||||
labels: string[];
|
||||
lastUpdateTime: string;
|
||||
isOnline?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 账户信息接口
|
||||
export interface CkAccount {
|
||||
id: number;
|
||||
realName: string;
|
||||
nickname: string | null;
|
||||
memo: string | null;
|
||||
avatar: string;
|
||||
userName: string;
|
||||
secret: string;
|
||||
accountType: number;
|
||||
departmentId: number;
|
||||
useGoogleSecretKey: boolean;
|
||||
hasVerifyGoogleSecret: boolean;
|
||||
}
|
||||
|
||||
//群聊数据接口
|
||||
export interface weChatGroup {
|
||||
id?: number;
|
||||
wechatAccountId: number;
|
||||
tenantId: number;
|
||||
accountId: number;
|
||||
chatroomId: string;
|
||||
chatroomOwner: string;
|
||||
conRemark: string;
|
||||
nickname: string;
|
||||
chatroomAvatar: string;
|
||||
groupId: number;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
};
|
||||
labels?: string[];
|
||||
unreadCount: number;
|
||||
notice: string;
|
||||
selfDisplyName: string;
|
||||
wechatChatroomId: number;
|
||||
serverId?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 联系人数据接口
|
||||
export interface ContractData {
|
||||
id?: number;
|
||||
serverId?: number;
|
||||
wechatAccountId: number;
|
||||
wechatId: string;
|
||||
alias: string;
|
||||
conRemark: string;
|
||||
nickname: string;
|
||||
quanPin: string;
|
||||
avatar?: string;
|
||||
gender: number;
|
||||
region: string;
|
||||
addFrom: number;
|
||||
phone: string;
|
||||
labels: string[];
|
||||
signature: string;
|
||||
accountId: number;
|
||||
extendFields: null;
|
||||
city?: string;
|
||||
lastUpdateTime: string;
|
||||
isPassed: boolean;
|
||||
tenantId: number;
|
||||
groupId: number;
|
||||
thirdParty: null;
|
||||
additionalPicture: string;
|
||||
desc: string;
|
||||
config?: {
|
||||
chat: boolean;
|
||||
};
|
||||
lastMessageTime: number;
|
||||
unreadCount: number;
|
||||
duplicate: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
//聊天记录接口
|
||||
export interface ChatRecord {
|
||||
id: number;
|
||||
wechatFriendId: number;
|
||||
wechatAccountId: number;
|
||||
tenantId: number;
|
||||
accountId: number;
|
||||
synergyAccountId: number;
|
||||
content: string;
|
||||
msgType: number;
|
||||
msgSubType: number;
|
||||
msgSvrId: string;
|
||||
isSend: boolean;
|
||||
createTime: string;
|
||||
isDeleted: boolean;
|
||||
deleteTime: string;
|
||||
sendStatus: number;
|
||||
wechatTime: number;
|
||||
origin: number;
|
||||
msgId: number;
|
||||
recalled: boolean;
|
||||
sender?: {
|
||||
chatroomNickname: string;
|
||||
isAdmin: boolean;
|
||||
isDeleted: boolean;
|
||||
nickname: string;
|
||||
ownerWechatId: string;
|
||||
wechatId: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信好友基本信息接口
|
||||
* 包含主要字段和兼容性字段
|
||||
*/
|
||||
export interface WechatFriend {
|
||||
// 主要字段
|
||||
id: number; // 好友ID
|
||||
wechatAccountId: number; // 微信账号ID
|
||||
wechatId: string; // 微信ID
|
||||
nickname: string; // 昵称
|
||||
conRemark: string; // 备注名
|
||||
avatar: string; // 头像URL
|
||||
gender: number; // 性别:1-男,2-女,0-未知
|
||||
region: string; // 地区
|
||||
phone: string; // 电话
|
||||
labels: string[]; // 标签列表
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 消息类型枚举
|
||||
export enum MessageType {
|
||||
TEXT = "text",
|
||||
IMAGE = "image",
|
||||
VOICE = "voice",
|
||||
VIDEO = "video",
|
||||
FILE = "file",
|
||||
LOCATION = "location",
|
||||
}
|
||||
|
||||
// 消息数据接口
|
||||
export interface MessageData {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
replyTo?: string;
|
||||
forwardFrom?: string;
|
||||
}
|
||||
|
||||
// 聊天会话类型
|
||||
export type ChatType = "private" | "group";
|
||||
|
||||
// 聊天会话接口
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
type: ChatType;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
lastMessage: string;
|
||||
lastTime: string;
|
||||
unreadCount: number;
|
||||
online: boolean;
|
||||
members?: string[];
|
||||
pinned?: boolean;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
// 聊天历史响应接口
|
||||
export interface ChatHistoryResponse {
|
||||
messages: MessageData[];
|
||||
hasMore: boolean;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// 发送消息请求接口
|
||||
export interface SendMessageRequest {
|
||||
chatId: string;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
replyTo?: string;
|
||||
}
|
||||
|
||||
// 搜索联系人请求接口
|
||||
export interface SearchContactRequest {
|
||||
keyword: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// 在线状态接口
|
||||
export interface OnlineStatus {
|
||||
userId: string;
|
||||
online: boolean;
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
// 消息状态接口
|
||||
export interface MessageStatus {
|
||||
messageId: string;
|
||||
status: "sending" | "sent" | "delivered" | "read" | "failed";
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// 文件上传响应接口
|
||||
export interface FileUploadResponse {
|
||||
url: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
// 表情包接口
|
||||
export interface EmojiData {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
// 快捷回复接口
|
||||
export interface QuickReply {
|
||||
id: string;
|
||||
content: string;
|
||||
category: string;
|
||||
useCount: number;
|
||||
}
|
||||
|
||||
// 聊天设置接口
|
||||
export interface ChatSettings {
|
||||
autoReply: boolean;
|
||||
autoReplyMessage: string;
|
||||
notification: boolean;
|
||||
sound: boolean;
|
||||
theme: "light" | "dark";
|
||||
fontSize: "small" | "medium" | "large";
|
||||
}
|
||||
198
Cunkebao/src/pages/pc/ckbox/weChat/index.module.scss
Normal file
198
Cunkebao/src/pages/pc/ckbox/weChat/index.module.scss
Normal file
@@ -0,0 +1,198 @@
|
||||
.ckboxLayout {
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
padding: 0 24px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.verticalSider {
|
||||
background: #2e2e2e;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sider {
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.searchBar {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
|
||||
: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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:global(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-tabs-nav) {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:global(.ant-tabs-tab) {
|
||||
padding: 12px 16px;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
.ant-tabs-tab-btn {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-tabs-ink-bar) {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #8c8c8c;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
.chatContainer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chatToolbar {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcomeScreen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
|
||||
.welcomeContent {
|
||||
text-align: center;
|
||||
color: #8c8c8c;
|
||||
|
||||
h2 {
|
||||
margin: 24px 0 12px 0;
|
||||
color: #262626;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.ckboxLayout {
|
||||
.sidebar {
|
||||
.searchBar {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
:global(.ant-tabs-nav) {
|
||||
padding: 0 12px;
|
||||
|
||||
:global(.ant-tabs-tab) {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
.chatContainer {
|
||||
.chatToolbar {
|
||||
padding: 6px 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcomeScreen {
|
||||
.welcomeContent {
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Cunkebao/src/pages/pc/ckbox/weChat/index.tsx
Normal file
103
Cunkebao/src/pages/pc/ckbox/weChat/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Layout } from "antd";
|
||||
import { MessageOutlined } from "@ant-design/icons";
|
||||
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/ckchat";
|
||||
const { Content, Sider } = Layout;
|
||||
import { chatInitAPIdata, initSocket } from "./main";
|
||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||
|
||||
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||
|
||||
const CkboxPage: React.FC = () => {
|
||||
// 不要在组件初始化时获取sendCommand,而是在需要时动态获取
|
||||
const [loading, setLoading] = useState(false);
|
||||
const currentContract = useWeChatStore(state => state.currentContract);
|
||||
useEffect(() => {
|
||||
// 方法一:使用 Promise 链式调用处理异步函数
|
||||
setLoading(true);
|
||||
chatInitAPIdata()
|
||||
.then(response => {
|
||||
const data = response as {
|
||||
contractList: any[];
|
||||
groupList: any[];
|
||||
kfUserList: KfUserListData[];
|
||||
newContractList: { groupName: string; contacts: any[] }[];
|
||||
};
|
||||
const { contractList } = data;
|
||||
|
||||
//找出已经在聊天的
|
||||
const isChatList = contractList.filter(
|
||||
v => (v?.config && v.config?.chat) || false,
|
||||
);
|
||||
isChatList.forEach(v => {
|
||||
addChatSession(v);
|
||||
});
|
||||
|
||||
// 数据加载完成后初始化WebSocket连接
|
||||
initSocket();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取联系人列表失败:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageSkeleton loading={loading}>
|
||||
<Layout className={styles.ckboxLayout}>
|
||||
<Layout>
|
||||
{/* 垂直侧边栏 */}
|
||||
|
||||
<Sider width={80} className={styles.verticalSider}>
|
||||
<VerticalUserList />
|
||||
</Sider>
|
||||
|
||||
{/* 左侧联系人边栏 */}
|
||||
<Sider width={280} className={styles.sider}>
|
||||
<SidebarMenu loading={loading} />
|
||||
</Sider>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<Content className={styles.mainContent}>
|
||||
{currentContract ? (
|
||||
<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={currentContract} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.welcomeScreen}>
|
||||
<div className={styles.welcomeContent}>
|
||||
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
|
||||
<h2>欢迎使用触客宝</h2>
|
||||
<p>选择一个联系人开始聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</PageSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default CkboxPage;
|
||||
307
Cunkebao/src/pages/pc/ckbox/weChat/main.ts
Normal file
307
Cunkebao/src/pages/pc/ckbox/weChat/main.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import {
|
||||
asyncKfUserList,
|
||||
asyncContractList,
|
||||
asyncChatSessions,
|
||||
asyncWeChatGroup,
|
||||
asyncCountLables,
|
||||
useCkChatStore,
|
||||
} from "@/store/module/ckchat/ckchat";
|
||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||
|
||||
import {
|
||||
loginWithToken,
|
||||
getControlTerminalList,
|
||||
getContactList,
|
||||
getGroupList,
|
||||
} from "./api";
|
||||
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
|
||||
import {
|
||||
KfUserListData,
|
||||
ContractData,
|
||||
weChatGroup,
|
||||
} from "@/pages/pc/ckbox/data";
|
||||
|
||||
import { WechatGroup } from "./api";
|
||||
const { login2 } = useUserStore.getState();
|
||||
//获取触客宝基础信息
|
||||
export const chatInitAPIdata = async () => {
|
||||
try {
|
||||
//获取联系人列表
|
||||
const contractList = await getAllContactList();
|
||||
|
||||
//获取联系人列表
|
||||
asyncContractList(contractList);
|
||||
|
||||
//获取群列表
|
||||
const groupList = await getAllGroupList();
|
||||
|
||||
await asyncWeChatGroup(groupList);
|
||||
|
||||
// 提取不重复的wechatAccountId组
|
||||
const uniqueWechatAccountIds: number[] = getUniqueWechatAccountIds(
|
||||
contractList,
|
||||
groupList,
|
||||
);
|
||||
|
||||
//获取控制终端列表
|
||||
const kfUserList: KfUserListData[] =
|
||||
await getControlTerminalListByWechatAccountIds(uniqueWechatAccountIds);
|
||||
|
||||
//获取用户列表
|
||||
await asyncKfUserList(kfUserList);
|
||||
|
||||
//获取标签列表
|
||||
const countLables = await getCountLables();
|
||||
await asyncCountLables(countLables);
|
||||
|
||||
//获取消息会话列表并按lastUpdateTime排序
|
||||
const filterUserSessions = contractList?.filter(
|
||||
v => v?.config && v.config?.chat,
|
||||
);
|
||||
const filterGroupSessions = groupList?.filter(
|
||||
v => v?.config && v.config?.chat,
|
||||
);
|
||||
//排序功能
|
||||
const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
|
||||
(a, b) => {
|
||||
// 如果lastUpdateTime不存在,则将其排在最后
|
||||
if (!a.lastUpdateTime) return 1;
|
||||
if (!b.lastUpdateTime) return -1;
|
||||
|
||||
// 首先按时间降序排列(最新的在前面)
|
||||
const timeCompare =
|
||||
new Date(b.lastUpdateTime).getTime() -
|
||||
new Date(a.lastUpdateTime).getTime();
|
||||
|
||||
// 如果时间相同,则按未读消息数量降序排列
|
||||
if (timeCompare === 0) {
|
||||
// 如果unreadCount不存在,则将其排在后面
|
||||
const aUnread = a.unreadCount || 0;
|
||||
const bUnread = b.unreadCount || 0;
|
||||
return bUnread - aUnread; // 未读消息多的排在前面
|
||||
}
|
||||
|
||||
return timeCompare;
|
||||
},
|
||||
);
|
||||
//会话数据同步
|
||||
asyncChatSessions(sortedSessions);
|
||||
|
||||
return {
|
||||
contractList,
|
||||
groupList,
|
||||
kfUserList,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("获取联系人列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
//发起soket连接
|
||||
export const initSocket = () => {
|
||||
// 检查WebSocket是否已经连接
|
||||
const { status } = useWebSocketStore.getState();
|
||||
|
||||
// 如果已经连接或正在连接,则不重复连接
|
||||
if (["connected", "connecting"].includes(status)) {
|
||||
console.log("WebSocket已连接或正在连接,跳过重复连接", { status });
|
||||
return;
|
||||
}
|
||||
|
||||
// 从store获取token和accountId
|
||||
const { token2 } = useUserStore.getState();
|
||||
const { getAccountId } = useCkChatStore.getState();
|
||||
const Token = token2;
|
||||
const accountId = getAccountId();
|
||||
// 使用WebSocket store初始化连接
|
||||
const { connect } = useWebSocketStore.getState();
|
||||
|
||||
// 连接WebSocket
|
||||
connect({
|
||||
accessToken: Token,
|
||||
accountId: Number(accountId),
|
||||
client: "kefu-client",
|
||||
cmdType: "CmdSignIn",
|
||||
seq: +new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getCountLables = async () => {
|
||||
const LablesRes = await Promise.all(
|
||||
[1, 2].map(item =>
|
||||
WechatGroup({
|
||||
groupType: item,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const [friend, group] = LablesRes;
|
||||
const countLables = [
|
||||
...[
|
||||
{
|
||||
id: 0,
|
||||
groupName: "默认群分组",
|
||||
groupType: 2,
|
||||
},
|
||||
],
|
||||
...group,
|
||||
...friend,
|
||||
...[
|
||||
{
|
||||
id: 0,
|
||||
groupName: "未分组",
|
||||
groupType: 1,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return countLables;
|
||||
};
|
||||
/**
|
||||
* 根据标签组织联系人
|
||||
* @param contractList 联系人列表
|
||||
* @param countLables 标签列表
|
||||
* @returns 按标签分组的联系人
|
||||
*/
|
||||
|
||||
//获取控制终端列表
|
||||
export const getControlTerminalListByWechatAccountIds = (
|
||||
WechatAccountIds: number[],
|
||||
) => {
|
||||
return Promise.all(
|
||||
WechatAccountIds.map(id => getControlTerminalList({ id: id })),
|
||||
);
|
||||
};
|
||||
// 递归获取所有联系人列表
|
||||
export const getAllContactList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let prevId = 0;
|
||||
const count = 1000;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const contractList = await getContactList({
|
||||
prevId,
|
||||
count,
|
||||
});
|
||||
|
||||
if (
|
||||
!contractList ||
|
||||
!Array.isArray(contractList) ||
|
||||
contractList.length === 0
|
||||
) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
allContacts = [...allContacts, ...contractList];
|
||||
|
||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||
if (contractList.length < count) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 获取最后一条数据的id作为下一次请求的prevId
|
||||
const lastContact = contractList[contractList.length - 1];
|
||||
prevId = lastContact.id;
|
||||
}
|
||||
}
|
||||
return allContacts;
|
||||
} catch (error) {
|
||||
console.error("获取所有联系人列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 提取不重复的wechatAccountId组
|
||||
export const getUniqueWechatAccountIds = (
|
||||
contacts: ContractData[],
|
||||
groupList: weChatGroup[],
|
||||
) => {
|
||||
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 使用Set来存储不重复的wechatAccountId
|
||||
const uniqueAccountIdsSet = new Set<number>();
|
||||
|
||||
// 遍历联系人列表,将每个wechatAccountId添加到Set中
|
||||
contacts.forEach(contact => {
|
||||
if (contact && contact.wechatAccountId) {
|
||||
uniqueAccountIdsSet.add(contact.wechatAccountId);
|
||||
}
|
||||
});
|
||||
|
||||
// 遍历联系人列表,将每个wechatAccountId添加到Set中
|
||||
groupList.forEach(group => {
|
||||
if (group && group.wechatAccountId) {
|
||||
uniqueAccountIdsSet.add(group.wechatAccountId);
|
||||
}
|
||||
});
|
||||
|
||||
// 将Set转换为数组并返回
|
||||
return Array.from(uniqueAccountIdsSet);
|
||||
};
|
||||
// 递归获取所有群列表
|
||||
export const getAllGroupList = async () => {
|
||||
try {
|
||||
let allContacts = [];
|
||||
let prevId = 0;
|
||||
const count = 1000;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const contractList = await getGroupList({
|
||||
prevId,
|
||||
count,
|
||||
});
|
||||
|
||||
if (
|
||||
!contractList ||
|
||||
!Array.isArray(contractList) ||
|
||||
contractList.length === 0
|
||||
) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
allContacts = [...allContacts, ...contractList];
|
||||
|
||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||
if (contractList.length < count) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 获取最后一条数据的id作为下一次请求的prevId
|
||||
const lastContact = contractList[contractList.length - 1];
|
||||
prevId = lastContact.id;
|
||||
}
|
||||
}
|
||||
|
||||
return allContacts;
|
||||
} catch (error) {
|
||||
console.error("获取所有群列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
//获取token
|
||||
const getToken = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {
|
||||
grant_type: "password",
|
||||
password: "kr123456",
|
||||
username: "kr_xf3",
|
||||
// username: "karuo",
|
||||
// password: "zhiqun1984",
|
||||
};
|
||||
loginWithToken(params)
|
||||
.then(res => {
|
||||
login2(res.access_token);
|
||||
resolve(res.access_token);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,17 +1,27 @@
|
||||
import React from "react";
|
||||
import { lazy } from "react";
|
||||
import type { RouteObject } from "react-router-dom";
|
||||
import CkboxPage from "@/pages/pc/ckbox";
|
||||
import WeChatPage from "@/pages/pc/ckbox/weChat";
|
||||
import Dashboard from "@/pages/pc/ckbox/dashboard";
|
||||
|
||||
const CkboxPage = lazy(() => import("@/pages/pc/ckbox"));
|
||||
|
||||
const ckboxRoutes: (RouteObject & { auth?: boolean; requiredRole?: string })[] =
|
||||
[
|
||||
{
|
||||
path: "/ckbox",
|
||||
element: <CkboxPage />,
|
||||
auth: true,
|
||||
requiredRole: "user",
|
||||
},
|
||||
];
|
||||
const ckboxRoutes = [
|
||||
{
|
||||
path: "/ckbox",
|
||||
element: <CkboxPage />,
|
||||
auth: true,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
element: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: "dashboard",
|
||||
element: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: "weChat",
|
||||
element: <WeChatPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default ckboxRoutes;
|
||||
|
||||
Reference in New Issue
Block a user