夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
    • Nest 项目如何编写 Dockerfile
    • 提升 Dockerfile 水平的 5 个技巧
    • Docker 是怎么实现的
    • 为什么 Node 应用要用 PM2 来跑?
    • 快速入门 MySQL
    • SQL 查询语句的所有语法和函数
    • 一对一、join 查询、级联方式
    • 一对多、多对多关系的表设计
    • 子查询和 EXISTS
    • SQL 综合练习
    • MySQL 的事务和隔离级别
    • MySQL 的视图、存储过程和函数
    • Node 操作 MySQL 的两种方式
    • 快速掌握 TypeORM
    • TypeORM 一对一的映射和关联 CRUD
    • TypeORM 一对多的映射和关联 CRUD
    • TypeORM 多对多的映射和关联 CRUD
    • 在 Nest 里集成 TypeORM
    • TypeORM保存任意层级的关系
    • 生产环境为什么用TypeORM的migration迁移功能
    • Nest 项目里如何使用 TypeORM 迁移
    • 如何动态读取不同环境的配置?
    • 快速入门 Redis
    • 在 Nest 里操作 Redis
    • 为什么不用 cache-manager 操作 Redis
    • 两种登录状态保存方式:JWT、Session
    • Nest 里实现 Session 和 JWT
    • MySQL + TypeORM + JWT 实现登录注册
    • 基于 ACL 实现权限控制
    • 基于 RBAC 实现权限控制
    • access_token和refresh_token实现无感登录
    • 单token无限续期实现登录无感刷新
    • 使用 passport 做身份认证
    • passport 实现 GitHub 三方账号登录
    • passport 实现 Google 三方账号登录
    • 为什么要使用 Docker Compose ?
    • Docker 容器通信的最简单方式:桥接网络
    • Docker 支持重启策略,是否还需要 PM2
    • 快速掌握 Nginx 的 2 大核心用法
    • 基于 Nginx 实现灰度系统
    • 基于 Redis 实现分布式 session
    • Redis + 高德地图,实现附近的充电宝
    • 用 Swagger 自动生成 api 文档
    • 如何灵活创建 DTO
    • class- validator 的内置装饰器,如何自定义装饰器
    • 序列化 Entity,你不需要 VO 对象
    • 手写序列化 Entity 的拦截器
    • 使用 compodoc 生成文档
    • Node 如何发邮件?
    • 实现基于邮箱验证码的登录
    • 基于 sharp 实现 gif 压缩工具
    • 大文件如何实现流式下载?
    • Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 实现扫二维码登录
    • Nest 的 REPL 模式
    • 实现 Excel 导入导出
    • 如何用代码动态生成 PPT
    • 如何拿到服务器 CPU、内存、磁盘状态
    • Nest 如何实现国际化?
    • 会议室预订系统:需求分析和原型图
    • 会议室预订系统:技术方案和数据库设计
    • 会议室预订系统:用户管理模块--用户注册
    • 会议室预订系统:用户管理模块--配置抽离、登录认证鉴权
    • 会议室预订系统:用户管理模块-- interceptor、修改信息接口
    • 会议室预订系统:用户管理模块--用户列表和分页查询
    • 会议室预订系统:用户管理模块-- swagger 接口文档
    • 会议室预订系统:用户管理模块-- 用户端登录注册页面
    • 会议室预订系统:用户管理模块-- 用户端信息修改页面
    • 会议室预订系统:用户管理模块-- 头像上传
    • 会议室预订系统:用户管理模块-- 管理端用户列表页面
    • 会议室预订系统:用户管理模块-- 管理端信息修改页面
    • 会议室预订系统:会议室管理模块-后端开发
    • 会议室预订系统:会议室管理模块-管理端前端开发
    • 会议室预订系统:会议室管理模块-用户端前端开发
    • 会议室预订系统:预定管理模块-后端开发
    • 会议室预订系统:预定管理模块-管理端前端开发
    • 会议室预订系统:预定管理模块-用户端前端开发
    • 会议室预订系统:统计管理模块-后端开发
    • 会议室预订系统:统计管理模块-前端开发
    • 会议室预订系统:后端项目部署到阿里云
    • 会议室预订系统:前端项目部署到阿里云
    • 会议室预定系统:用 migration 初始化表和数据
    • 会议室预定系统:文件上传 OSS
    • 会议室预定系统:Google 账号登录后端开发
    • 会议室预定系统:Google 账号登录前端开发
    • 会议室预定系统:后端代码优化
    • 会议室预定系统:集成日志框架 winston
    • 会议室预定系统:前端代码优化
    • 会议室预定系统:全部功能测试
    • 会议室预定系统:项目总结
    • Nest 如何创建微服务?
    • Nest 的 Monorepo 和 Library
    • 用 Etcd 实现微服务配置中心和注册中心
    • Nest 集成 Etcd 做注册中心、配置中心
    • 用 Nacos 实现微服务配置中心和注册中心
    • 基于 gRPC 实现跨语言的微服务通信
    • 快速入门 ORM 框架 Prisma
    • Prisma 的全部命令
    • Prisma 的全部 schema 语法
    • Primsa Client 单表 CRUD 的全部 api
    • Prisma Client 多表 CRUD 的全部 api
    • 在 Nest 里集成 Prisma
    • 为什么前端监控系统要用 RabbitMQ?
    • 基于 Redis 实现关注关系
    • 基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 考试系统:需求分析
    • 考试系统:技术方案和数据库设计
    • 考试系统:微服务、Lib 拆分
    • 考试系统;用户注册
    • 考试系统:用户登录、修改密码
    • 考试系统:考试微服务
    • 考试系统:登录、注册页面
    • 考试系统:修改密码、试卷列表页面
    • 考试系统:新增试卷、回收站
    • 考试系统:试卷编辑器
    • 考试系统:试卷回显、预览、保存
    • 考试系统:答卷微服务
    • 考试系统:答题页面
    • 考试系统:自动判卷
    • 考试系统:分析微服务、排行榜页面
    • 考试系统:整体测试
    • 考试系统:项目总结
    • 用 Node.js 手写 WebSocket 协议
    • Nest 开发 WebSocket 服务
    • 基于 Socket.io 的 room 实现群聊
    • 聊天室:需求分析和原型图
    • 聊天室:技术选型和数据库设计
    • 聊天室:用户注册
    • 聊天室:用户登录
    • 聊天室:修改密码、修改信息
    • 聊天室:好友列表、发送好友申请
    • 聊天室:创建聊天室、加入群聊
    • 聊天室:登录、注册页面开发
    • 聊天室:修改密码、信息页面开发
    • 聊天室:头像上传
    • 聊天室:好友∕群聊列表页面
    • 聊天室:添加好友弹窗、通知页面
    • 聊天室:聊天功能后端开发
    • 聊天室:聊天功能前端开发
    • 聊天室:一对一聊天
    • 聊天室:创建群聊、进入群聊
      • 总结
    • 聊天室:发送表情、图片、文件
    • 聊天室:收藏
    • 聊天室:全部功能测试
    • 聊天室:项目总结
    • MongoDB 快速入门
    • 使用 mongoose 操作 MongoDB 数据库
    • GraphQL 快速入门
    • Nest 开发 GraphQL 服务:实现 CRUD
    • GraphQL + Primsa + React 实现 TodoList
    • 如何调试 Nest 源码?
  • 其他

  • 服务端
  • Nest
神说要有光
2025-03-10
目录

聊天室:创建群聊、进入群聊

上节打通了一对一聊天的流程,这节继续打通群聊的流程。

其实现在展示的群聊列表有点问题,应该把一对一的聊天室过滤掉,而且把群聊人数显示出来。

我们改一下:

interface GroupSearchResult {
    id: number;
    name: string;
    type: boolean;
    userCount: number;
    userIds: Array<number>;
    createTime: Date;
}
1
2
3
4
5
6
7
8
setGroupResult(res.data.filter((item: GroupSearchResult) => {
    return item.type === true
}).map((item: GroupSearchResult) => {
    return {
        ...item,
        key: item.id
    }
}));
1
2
3
4
5
6
7
8

现在就只剩下群聊了:

然后我们优化下日期展示,加上人数展示,加一个详情按钮:

{
    title: '创建时间',
    dataIndex: 'createTime',
    render: (_, record) => {
        return new Date(record.createTime).toLocaleString()
    }
},
{
    title: '人数',
    dataIndex: 'userCount'
},
{
    title: '操作',
    render: (_, record) => (
        <div>
            <a href="#">聊天</a>&nbsp;
            <a href="#">详情</a>
        </div>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

点击详情按钮的时候,通过弹窗展示群聊的成员。

我们加一个 Modal 组件:

src/pages/Group/MembersModal.tsx

import { message, Modal, Table } from "antd";
import { ColumnsType } from "antd/es/table";
import { useEffect, useState } from "react";
import { groupMembers } from "../../interfaces";

export interface MembersModalProps {
    isOpen: boolean
    chatroomId: number
    handleClose: () => void
}

interface User {
    id: number;
    username: string;
    nickName: string;
    headPic: string;
    email: string;
}

export function MembersModal(props: MembersModalProps) {

    const [members, setMembers] = useState<Array<User>>();

    const queryMembers = async () => {
        try{
            const res = await groupMembers(props.chatroomId);

            if(res.status === 201 || res.status === 200) {
                setMembers(res.data.map((item: User) => {
                    return {
                        ...item,
                        key: item.id
                    }
                }));
            }
        } catch(e: any){
            message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
        }
    };

    useEffect(() => {
        queryMembers();
    }, [props.chatroomId]);

    const columns: ColumnsType<User> = [
        {
            title: 'ID',
            dataIndex: 'id'
        },
        {
            title: '用户名',
            dataIndex: 'username'
        },
        {
            title: '昵称',
            dataIndex: 'nickName'
        },
        {
            title: '头像',
            dataIndex: 'headPic',
            render: (_, record) => (
                <div>
                    <img src={record.headPic} width={50} height={50}/>
                </div>
            )
        },
        {
            title: '邮箱',
            dataIndex: 'email'
        }
    ]

    return <Modal 
        title="群聊成员"
        open={props.isOpen}
        onCancel={() => props.handleClose()}
        onOk={() => props.handleClose()}
        width={1000}
    >
        <Table columns={columns} dataSource={members} pagination={false}/>
    </Modal>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

它就是调用 members 接口查询成员数据,用 table 展示。

在 interface 加一下这个接口:

export async function groupMembers(chatroomId: number) {
    return axiosInstance.get(`/chatroom/members`, {
        params: {
            chatroomId
        }
    });
}
1
2
3
4
5
6
7

然后在 Group/index.tsx 调用下:

就是用一个 state 保存 Modal 打开状态,一个 state 保存当前的 chatroomId

import { Badge, Button, Form, Input, Popconfirm, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './index.css';
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
import { chatroomList } from "../../interfaces";
import { MembersModal } from "./MembersModal";

interface SearchGroup {
    name: string;
}

interface GroupSearchResult {
    id: number;
    name: string;
    type: boolean;
    userCount: number;
    userIds: Array<number>;
    createTime: Date;
}

export function Group() {
    const [groupResult, setGroupResult] = useState<Array<GroupSearchResult>>([]);

    const [isMembersModalOpen, setMembersModalOpen] = useState(false);
    const [chatroomId, setChatroomId] = useState<number>(-1);

    const columns: ColumnsType<GroupSearchResult> = [
        {
            title: '名称',
            dataIndex: 'name'
        },
        {
            title: '创建时间',
            dataIndex: 'createTime',
            render: (_, record) => {
                return new Date(record.createTime).toLocaleString()
            }
        },
        {
            title: '人数',
            dataIndex: 'userCount'
        },
        {
            title: '操作',
            render: (_, record) => (
                <div>
                    <a href="#">聊天</a>&nbsp;
                    <a href="#" onClick={() => {
                        setChatroomId(record.id);
                        setMembersModalOpen(true);
                    }}>详情</a>
                </div>
            )
        }
    ]

    const searchGroup = async (values: SearchGroup) => {
        try{
            const res = await chatroomList(values.name || '');

            if(res.status === 201 || res.status === 200) {
                setGroupResult(res.data.filter((item: GroupSearchResult) => {
                    return item.type === true
                }).map((item: GroupSearchResult) => {
                    return {
                        ...item,
                        key: item.id
                    }
                }));
            }
        } catch(e: any){
            message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
        }
    };

    const [form ]  = useForm();

    useEffect(() => {
        searchGroup({
            name: form.getFieldValue('name')
        });
    }, []);

    return <div id="group-container">
        <div className="group-form">
            <Form
                form={form}
                onFinish={searchGroup}
                name="search"
                layout='inline'
                colon={false}
            >
                <Form.Item label="名称" name="name">
                    <Input />
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" htmlType="submit">
                        搜索
                    </Button>
                </Form.Item>
            </Form>
        </div>
        <div className="group-table">
            <Table columns={columns} dataSource={groupResult} style={{width: '1000px'}}/>
        </div>
        <MembersModal isOpen={isMembersModalOpen} handleClose={() => {
            setMembersModalOpen(false)
        }} chatroomId={chatroomId}/>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

试下效果:

没啥问题。

其实好友列表那里也应该把 id 和 username 展示出来:

然后群聊点击聊天的时候可以直接知道 chatroomId,跳到聊天页面:

<a href="" onClick={() => {
    navigate('/chat', {
        state: {
            chatroomId: record.id
        }
    });
}}>聊天</a>
1
2
3
4
5
6
7

没啥问题。

但现在这个群就一个人,没啥好聊的。

我们加一下添加成员的功能。

同样,我们要加一个弹窗:

src/pages/Group/AddMemberModal.tsx

import { Button, Form, Input, InputNumber, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { addMember } from "../../interfaces";

interface AddMemberModalProps {
    chatroomId: number;
    isOpen: boolean;
    handleClose: Function
}

const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 }
}

export interface AddMember {
    username: string;
}

export function AddMemberModal(props: AddMemberModalProps) {

    const [form] = useForm<AddMember>();

    const handleOk = async function() {
        await form.validateFields();

        const values = form.getFieldsValue();

        try{
            const res = await addMember(values.username);

            if(res.status === 201 || res.status === 200) {
                message.success('成员添加成功');
                form.resetFields();
                props.handleClose();
            }
        } catch(e: any){
            message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
        }
    }

    return <Modal 
        title="添加成员"
        open={props.isOpen}
        onOk={handleOk}
        onCancel={() => props.handleClose()}
        okText={'添加'}
        cancelText={'取消'}    
    >
        <Form
            form={form}
            colon={false}
            {...layout}
        >
            <Form.Item
                label="用户名"
                name="username"
                rules={[
                    { required: true, message: '请输入用户名!' },
                ]}
            >
                <Input />
            </Form.Item>
        </Form>
    </Modal>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

这里调用 memberAdd 接口来添加成员。

我们之前的接口有点问题,没有支持根据 username 加人。

改一下:

接收 username 参数,根据 username 查询用户,如果用户不存在,返回错误,否则,把用户加入聊天室。

@Get('join/:id')
async join(@Param('id') id: number, @Query('joinUsername') joinUsername: string) {
    if(!id) {
      throw new BadRequestException('id 不能为空')
    }
    if(!joinUsername) {
      throw new BadRequestException('joinUsername 不能为空')
    }
    return this.chatroomService.join(id, joinUsername);
}
1
2
3
4
5
6
7
8
9
10
async join(id: number, username: string) {
    const chatroom = await this.prismaService.chatroom.findUnique({
        where: {
            id
        }
    });

    if(chatroom.type === false) {
        throw new BadRequestException('一对一聊天室不能加人');
    }

    const user = await this.prismaService.user.findUnique({
        where: {
            username
        }
    });

    if(!user) {
        throw new BadRequestException('用户不存在');
    }

    await this.prismaService.userChatroom.create({
        data: {
            userId: user.id,
            chatroomId: id
        }
    })

    return chatroom.id;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

然后 interfaces 里调用下:

export async function addMember(chatroomId: number, joinUsername: string) {
    return axiosInstance.get(`/chatroom/join/${chatroomId}`, {
        params: {
            joinUsername
        }
    });
}
1
2
3
4
5
6
7

改下参数:

在 Group/index.tsx 调用下:

加一个 state 来保存弹窗状态,点击添加成员的时候展示,关闭弹窗的时候重新搜索。

测试下:

可以看到,添加成功后成员数量变了,但点击详情还是一个成员。

这是因为 chatroomId 参数没变, MembersModal 并没有触发重新搜索。

我们加一个 queryKey 参数来控制重新查询:

在 Group/index.tsx 组件里传进来:

const [queryKey, setQueryKey] = useState<string>('');
1
setQueryKey(Math.random().toString().slice(2, 10))
1

当添加成员完毕,设置 queryKey 为一个新的随机值,从而触发重新请求:

现在群里有三个成员,我们登录另外两个账号聊聊天试试:

登录小红的账号:

确实,她有了这个群聊。

再登录东东的账号聊天试试:

没问题,可以三人在房间里聊天。

但现在还有个 bug:

点击聊天进入聊天室的时候,并不会查询聊天记录,只有切换聊天室才会。

当解析 state 中的 chatroomId 的时候,再查一下聊天记录就好了:

useEffect(() => {
    if(location.state?.chatroomId) {
        setChatroomId(location.state?.chatroomId);

        queryChatHistoryList(location.state?.chatroomId);
    }
}, [location.state?.chatroomId]);
1
2
3
4
5
6
7

而且也要判断下,chatroomId 不为空才查聊天记录。

现在从群聊、从好友点进来,就都会查询聊天记录了:

此外,我们还要添加下创建群聊的功能。

同样,要写一个 Modal:

src/pages/CreateGroupModal.tsx

import { Button, Form, Input, InputNumber, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { createGroup } from "../../interfaces";

interface CreateGroupModalProps {
    isOpen: boolean;
    handleClose: Function
}

const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 }
}

export interface GroupGroup {
    name: string;
}

export function CreateGroupModal(props: CreateGroupModalProps) {

    const [form] = useForm<GroupGroup>();

    const handleOk = async function() {
        await form.validateFields();

        const values = form.getFieldsValue();

        try{
            const res = await createGroup(values.name);

            if(res.status === 201 || res.status === 200) {
                message.success('群聊创建成功过');
                form.resetFields();
                props.handleClose();
            }
        } catch(e: any){
            message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
        }
    }

    return <Modal 
        title="创建群聊"
        open={props.isOpen}
        onOk={handleOk}
        onCancel={() => props.handleClose()}
        okText={'创建'}
        cancelText={'取消'}    
    >
        <Form
            form={form}
            colon={false}
            {...layout}
        >
            <Form.Item
                label="群聊名称"
                name="name"
                rules={[
                    { required: true, message: '请输入群聊名称!' },
                ]}
            >
                <Input />
            </Form.Item>
        </Form>
    </Modal>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

和之前的 Modal 一样,创建群聊需要填入群聊名称。

在 interfaces 添加这个接口:

export async function createGroup(name: string) {
    return axiosInstance.get(`/chatroom/create-group`, {
        params: {
            name
        }
    });
}
1
2
3
4
5
6
7

在 Group/index.tsx 里用一下:

加一个按钮,点击显示创建群聊弹窗,关闭弹窗的时候刷新列表。

import { Badge, Button, Form, Input, Popconfirm, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './index.css';
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
import { chatroomList } from "../../interfaces";
import { MembersModal } from "./MembersModal";
import { useNavigate } from "react-router-dom";
import { AddMemberModal } from "./AddMemberModal";
import { CreateGroupModal } from "./CreateGroupModal";

interface SearchGroup {
    name: string;
}

interface GroupSearchResult {
    id: number;
    name: string;
    type: boolean;
    userCount: number;
    userIds: Array<number>;
    createTime: Date;
}

export function Group() {
    const [groupResult, setGroupResult] = useState<Array<GroupSearchResult>>([]);

    const [isMembersModalOpen, setMembersModalOpen] = useState(false);
    const [isMemberAddModalOpen, setMemberAddModalOpen] = useState(false);
    const [isCreateGroupModalOpen, setCreateGroupModalOpen] = useState(false);

    const [chatroomId, setChatroomId] = useState<number>(-1);

    const navigate = useNavigate();

    const columns: ColumnsType<GroupSearchResult> = [
        {
            title: '名称',
            dataIndex: 'name'
        },
        {
            title: '创建时间',
            dataIndex: 'createTime',
            render: (_, record) => {
                return new Date(record.createTime).toLocaleString()
            }
        },
        {
            title: '人数',
            dataIndex: 'userCount'
        },
        {
            title: '操作',
            render: (_, record) => (
                <div>
                    <a href="javascript:void(0);" onClick={() => {
                        navigate('/chat', {
                            state: {
                                chatroomId: record.id
                            }
                        });
                    }}>聊天</a>&nbsp;
                    <a href="#" onClick={() => {
                        setChatroomId(record.id);
                        setMembersModalOpen(true);
                    }}>详情</a>&nbsp;
                    <a href="#" onClick={() => {
                        setChatroomId(record.id);
                        setMemberAddModalOpen(true);
                    }}>添加成员</a>
                </div>
            )
        }
    ]

    const searchGroup = async (values: SearchGroup) => {
        try{
            const res = await chatroomList(values.name || '');

            if(res.status === 201 || res.status === 200) {
                setGroupResult(res.data.filter((item: GroupSearchResult) => {
                    return item.type === true
                }).map((item: GroupSearchResult) => {
                    return {
                        ...item,
                        key: item.id
                    }
                }));
            }
        } catch(e: any){
            message.error(e.response?.data?.message || '系统繁忙,请稍后再试');
        }
    };

    const [form ]  = useForm();

    useEffect(() => {
        searchGroup({
            name: form.getFieldValue('name')
        });
    }, []);

    const [queryKey, setQueryKey] = useState<string>('');
    return <div id="group-container">
        <div className="group-form">
            <Form
                form={form}
                onFinish={searchGroup}
                name="search"
                layout='inline'
                colon={false}
            >
                <Form.Item label="名称" name="name">
                    <Input />
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" htmlType="submit">
                        搜索
                    </Button>
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" style={{background: 'green'}} onClick={() => setCreateGroupModalOpen(true)}>
                        创建群聊
                    </Button>
                </Form.Item>
            </Form>
        </div>
        <div className="group-table">
            <Table columns={columns} dataSource={groupResult} style={{width: '1000px'}}/>
        </div>
        <MembersModal isOpen={isMembersModalOpen} handleClose={() => {
            setMembersModalOpen(false)
        }} chatroomId={chatroomId} queryKey={queryKey}/>
        <AddMemberModal isOpen={isMemberAddModalOpen} handleClose={() => {
            setMemberAddModalOpen(false)

            setQueryKey(Math.random().toString().slice(2, 10))
            searchGroup({
                name: form.getFieldValue('name')
            });
        }} chatroomId={chatroomId}/>
        <CreateGroupModal isOpen={isCreateGroupModalOpen} handleClose={() => {
            setCreateGroupModalOpen(false)

            searchGroup({
                name: form.getFieldValue('name')
            });
        }}/>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

然后给这个闲聊群加几个成员:

进入聊天:

三个人都能正常聊天。

从创建聊天、加入聊天到在群里聊天,整个流程都没问题。

过程中发现一个小问题:

刚加载出聊天记录的时候,不会滚动到底部。

我们也加一下自动滚动:

useEffect(() => {
    setTimeout(() => {
        document.getElementById('bottom-bar')?.scrollIntoView({block: 'end'});
    }, 300);
}, [roomId])
1
2
3
4
5

这样就好了。

前端代码 (opens new window)

后端代码 (opens new window)

# 总结

这节我们打通了群聊的流程。

首先支持了查看群聊成员,通过弹窗展示。

然后支持了添加成员,填入对方的 username 即可。

之后实现了点击聊天进入群聊。

然后实现了创建群聊。

最后从创建群聊、添加成员、查看成员、开始聊天,到三个人在群里聊天,都没问题。

这样,群聊功能就完成了。

编辑 (opens new window)
上次更新: 2025/10/27 10:53:52
聊天室:一对一聊天
聊天室:发送表情、图片、文件

← 聊天室:一对一聊天 聊天室:发送表情、图片、文件→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
如何调试 Nest 项目
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式