留言功能在社交中占据很重要的作用。这里实现的留言功能,参考微信朋友圈的方式:
实际完成的效果如下:
体验站点请戳 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 />
Submit
</Button>
</Form.Item>
</Form>
针对留言的展示,这里使用的是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>
)}
/>
列表是对用户发表的主题,留言以及子留言的展示。如果你纵览上面的代码片段,你会发现里面有一个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: '获取留言失败!'
}
至此,可以愉快地进行留言~
后话
-
更多内容可前往 jimmy github
-
留言的关键代码可前往 jimmy 留言功能
-
留言的体验地址可前往 jimmyarea.com
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!