Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
reng99 opened this issue Jul 9, 2021 · 0 comments
Open

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

reng99 opened this issue Jul 9, 2021 · 0 comments
Labels
blog a single blog

Comments

@reng99
Copy link
Owner

reng99 commented Jul 9, 2021

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

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

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

实际完成的效果如下:

global_leave_message.png

体验站点请戳 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自带的ListComment组件:

<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>
                用户&nbsp;{item.userId?.username}&nbsp;&nbsp;发表于&nbsp;
                {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 />
                    &nbsp; Delete
                  </a>
                ) : null}
                <a
                  style={{ fontSize: '12px', marginRight: '12px' }}
                  onClick={() => replyMsg(item)}
                >
                  <MessageOutlined />
                  &nbsp; 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 />
                            &nbsp; Delete
                          </a>
                        ) : null}
                      </>,
                      <a
                        style={{ fontSize: '12px', marginRight: '12px' }}
                        onClick={() => replyMsg(innerItem)}
                      >
                        <MessageOutlined />
                        &nbsp; 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: '获取留言失败!'
}

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

后话

@reng99 reng99 added the blog a single blog label Jul 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blog a single blog
Projects
None yet
Development

No branches or pull requests

1 participant