0
点赞
收藏
分享

微信扫一扫

react+koa2+mongodb实现留言功能(可体验)

留言功能在社交中占据很重要的作用。这里实现的留言功能,参考微信朋友圈的方式:

用户发送一个​​TOPIC​​话题,读者可以在该话题下面进行评论,也可以对该话题下的留言进行评论。但是始终只会展示两层树的评论。

当然,也可以像掘金这样进行嵌套多层树的结构展示。臣妾觉得嵌套得太深~

实际完成的效果如下:

react+koa2+mongodb实现留言功能(可体验)_表单

体验站点请戳 ​​jimmyarea.com​​ 。

前端实现

使用技术

  • react
  • ant design
  • typescript

在上面的截图中,很明显,就是一个表单的设计,外加一个列表的展示。

表单的设计使用了​​ant design​​​框架自带的​​form​​组件:

<Form
{...layout}
form={form}
name="basic"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="主题"
name="subject"
rules={[
{ required: true, message: '请输入你的主题' },
{ whitespace: true, message: '输入不能为空' },
{ min: 6, message: '主题不能小于6个字符' },
{ max: 30, message: '主题不能大于30个字符' },
]}
>
<Input maxLength={30} placeholder="请输入你的主题(最少6字符,最多30字符)"
</Form.Item>

<Form.Item
label="内容"
name="content"
rules={[
{ required: true, message: '请输入你的内容' },
{ whitespace: true, message: '输入不能为空' },
{ min: 30, message: '内容不能小于30个字符' },
]}
>
<Input.TextArea
placeholder="请输入你的内容(最少30字符)"
autoSize={{
minRows: 6,
maxRows: 12,
}}
showCount
maxLength={300}
</Form.Item>
<Form.Item {...tailLayout}>
<Button
type="primary"
htmlType="submit"
style={{ width: '100%' }}
loading={loading}
disabled={loading}
<CloudUploadOutlined
&nbsp;Submit
</Button>
</Form.Item>
</Form>

这里限制了输入的主题名称的长度为6-30;内容是30-300字符

针对留言的展示,这里使用的是​​ant design​​​自带的​​List​​​和​​Comment​​组件:

<List
loading={loadingMsg}
itemLayout="horizontal"
pagination={{
size: 'small',
total: count,
showTotal: () => `共 ${count},
pageSize,
current: activePage,
onChange: changePage,
}}
dataSource={list}
renderItem={(item: any, index: any) => (
<List.Item actions={[]} key={index}>
<List.Item.Meta
avatar={
<Avatar style={{ backgroundColor: '#1890ff' }}>
{item.userId?.username?.slice(0, 1)?.toUpperCase()}
</Avatar>
}
title={<b>{item.subject}</b>}
description={
<>
{item.content}
{/* 子留言 */}
<div
style={{
fontSize: '12px',
marginTop: '8px',
marginBottom: '16px',
alignItems: 'center',
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
}}
>
<span>
用户 {item.userId?.username}  发表于 
{moment(item.meta?.createAt).format('YYYY-MM-DD HH:mm:ss')}
</span>
<span>
{item.canDel ? (
<a
style={{ color: 'red', fontSize: '12px', marginRight: '12px' }}
onClick={() removeMsg(item)}
>
<DeleteOutlined
  Delete
</a>
) : null}
<a
style={{ fontSize: '12px', marginRight: '12px' }}
onClick={() replyMsg(item)}
>
<MessageOutlined
  Reply
</a>
</span>
</div>
{/* 回复的内容 */}
{item.children && item.children.length ? (
<>
{item.children.map((innerItem: any, innerIndex: any) => (
<Comment
key={innerIndex}
author={<span>{innerItem.subject}</span>}
avatar={
<Avatar style={{ backgroundColor: '#1890ff' }}>
{innerItem.userId?.username?.slice(0, 1)?.toUpperCase()}
</Avatar>
}
content={<p>{innerItem.content}</p>}
datetime={
<Tooltip
title={moment(innerItem.meta?.createAt).format(
'YYYY-MM-DD HH:mm:ss',
)}
>
<span>{moment(innerItem.meta?.createAt).fromNow()}</span>
</Tooltip>
}
actions={[
<>
{innerItem.canDel ? (
<a
style={{
color: 'red',
fontSize: '12px',
marginRight: '12px',
}}
onClick={() removeMsg(innerItem)}
>
<DeleteOutlined
  Delete
</a>
) : null}
</>,
<a
style={{ fontSize: '12px', marginRight: '12px' }}
onClick={() replyMsg(innerItem)}
>
<MessageOutlined
  Reply
</a>,
]}
/>
))}
</>
) : null}

{/* 回复的表单 */}
{replyObj._id === item._id || replyObj.pid === item._id ? (
<div style={{ marginTop: '12px' }} ref={replyArea}>
<Form
form={replyForm}
name="reply"
onFinish={onFinishReply}
onFinishFailed={onFinishFailed}
<Form.Item
name="reply"
rules={[
{ required: true, message: '请输入你的内容' },
{ whitespace: true, message: '输入不能为空' },
{ min: 2, message: '内容不能小于2个字符' },
]}
>
<Input.TextArea
placeholder={replyPlaceholder}
autoSize={{
minRows: 6,
maxRows: 12,
}}
showCount
maxLength={300}
</Form.Item>

<Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
style={{ marginRight: '12px' }}
onClick={() cancelReply()}
>
Dismiss
</Button>
<Button
type="primary"
htmlType="submit"
loading={innerLoading}
disabled={innerLoading}
Submit
</Button>
</div>
</Form.Item>
</Form>
</div>
) : null}
</>
}
/>
</List.Item>
)}
/>

当然,如果是多级地树结构嵌套,你完全可以只是使用​​Comment​​组件进行递归调用

列表是对用户发表的主题,留言以及子留言的展示。如果你纵览上面的代码片段,你会发现里面有一个​​Form​​表单。

是的,其​​Form​​​表单就是给留言使用的,其结构仅仅是剔除了主题留言中的​​subject​​字段输入框,但是实际传参我还是会使用到。

完整的前端代码可前往​​jimmyarea 留言(前端)​​查看。

后端

使用的技术:

  • mongodb 数据库,这里我使用到了其ODM​​mongoose​
  • koa2 一个​​Node​​框架
  • pm2 进程守卫
  • apidoc 用来生成接口文档(如果你留意​​体验站点​​,右上角有一个"文档"的链接,链接的内容就是生成的文档内容)

这里的搭建就不进行介绍了,可以参考​​koa2官网​​配合百度解决~

其实,本质上还是增删改查的操作。

首先,我们对自己要存储的数据结构​​schema​​进行相关的定义:

const mongoose = require('mongoose')
const Schema = mongoose.Schema

// 定义留言字段
let MessageSchema = new Schema({
// 关联字段 -- 用户的id
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
type: Number, // 1是留言,2是回复
subject: String, // 留言主题
content: String, // 留言内容
pid: { // 父id
type: String,
default: '-1'
},
replyTargetId: { // 回复目标记录id, 和父pid有所不同
type: String,
default: '-1'
},
meta: {
createAt: {
type: Date,
default: Date.now()
},
updateAt: {
type: Date,
default: Date.now()
}
}
})

mongoose.model('Message', MessageSchema)

这里有个注意的点​​userId​​字段,这里我直接关联了注册的用户。

完成了字段的设定之后,下面就可以进行增删改查了。

详细的​​crud​​​代码可以到​​jimmyarea 留言(后端)​​ 查看。

本篇的重点是,对评论的话题和留言,如何转换成两层的树型结构呢?

这就是涉及到了​​pid​​​这个字段,也就是​​父节点的id​​​: 话题的​​pid​​​为​​-1​​​,话题下留言的​​pid​​为话题的记录值。如下代码:

let count = await Message.count({pid: '-1'})
let data = await Message.find({pid: '-1'})
.skip((current-1) * pageSize)
.limit(pageSize)
.sort({ 'meta.createAt': -1})
.populate({
path: 'userId',
select: 'username _id' // select: 'username -_id' -_id 是排除_id
})
.lean(true) // 添加lean变成js的json字符串

const pids = Array.isArray(data) ? data.map(i i._id) : [];
let resReply = []
if(pids.length) {
resReply = await Message.find({pid: {$in: pids}})
.sort({ 'meta.createAt': 1})
.populate({
path: 'userId',
select: 'username _id' // select: 'username -_id' -_id 是排除_id
})
}

const list = data.map(item {
const children = JSON.parse(JSON.stringify(resReply.filter(i i.pid === item._id.toString()))) // 引用问题
const tranformChildren = children.map(innerItem ({
...innerItem,
canDel: innerItem.userId && innerItem.userId._id.toString() === (user._id&&user._id.toString()) ? 1 : 0
}))
return {
...item,
children: tranformChildren,
canDel: item.userId && item.userId._id.toString() === (user._id&&user._id.toString()) ? 1 : 0
}
})

if(list) {
ctx.body = {
results: list,
current: 1,
count
}
return
}
ctx.body = {
code: 10002,
msg: '获取留言失败!'

至此,可以愉快地进行留言~


举报

相关推荐

0 条评论