最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • react+koa2+mongodb实现留言功能(可体验)

    正文概述 掘金(Jimmy)   2021-07-09   409

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

    实际完成的效果如下:

    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>
    

    针对留言的展示,这里使用的是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>
      )}
    />
    

    列表是对用户发表的主题,留言以及子留言的展示。如果你纵览上面的代码片段,你会发现里面有一个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


    下载网 » react+koa2+mongodb实现留言功能(可体验)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元