上节打通了一对一聊天的流程,这节继续打通群聊的流程。
其实现在展示的群聊列表有点问题,应该把一对一的聊天室过滤掉,而且把群聊人数显示出来。
我们改一下:
interface GroupSearchResult {
id: number;
name: string;
type: boolean;
userCount: number;
userIds: Array<number>;
createTime: Date;
}
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
}
}));
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>
<a href="#">详情</a>
</div>
)
}
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>
}
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
}
});
}
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>
<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>
}
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>
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>
}
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);
}
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;
}
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
}
});
}
2
3
4
5
6
7
改下参数:
在 Group/index.tsx 调用下:
加一个 state 来保存弹窗状态,点击添加成员的时候展示,关闭弹窗的时候重新搜索。
测试下:
可以看到,添加成功后成员数量变了,但点击详情还是一个成员。
这是因为 chatroomId 参数没变, MembersModal 并没有触发重新搜索。
我们加一个 queryKey 参数来控制重新查询:
在 Group/index.tsx 组件里传进来:
const [queryKey, setQueryKey] = useState<string>('');
setQueryKey(Math.random().toString().slice(2, 10))
当添加成员完毕,设置 queryKey 为一个新的随机值,从而触发重新请求:
现在群里有三个成员,我们登录另外两个账号聊聊天试试:
登录小红的账号:
确实,她有了这个群聊。
再登录东东的账号聊天试试:
没问题,可以三人在房间里聊天。
但现在还有个 bug:
点击聊天进入聊天室的时候,并不会查询聊天记录,只有切换聊天室才会。
当解析 state 中的 chatroomId 的时候,再查一下聊天记录就好了:
useEffect(() => {
if(location.state?.chatroomId) {
setChatroomId(location.state?.chatroomId);
queryChatHistoryList(location.state?.chatroomId);
}
}, [location.state?.chatroomId]);
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>
}
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
}
});
}
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>
<a href="#" onClick={() => {
setChatroomId(record.id);
setMembersModalOpen(true);
}}>详情</a>
<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>
}
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])
2
3
4
5
这样就好了。
# 总结
这节我们打通了群聊的流程。
首先支持了查看群聊成员,通过弹窗展示。
然后支持了添加成员,填入对方的 username 即可。
之后实现了点击聊天进入群聊。
然后实现了创建群聊。
最后从创建群聊、添加成员、查看成员、开始聊天,到三个人在群里聊天,都没问题。
这样,群聊功能就完成了。