使用koa+mysql写一个简易论坛(完)

一、本期目标

给本系列文章画上一个相对圆满的句号
  • Admin ---- 帖子管理的无刷新处理
  • Admin ---- 用户管理
  • Admin ---- 小黑屋(相当于回收站
  • User ---- 简单的搜索功能
  • User ---- 留言功能
  • User ---- 个人信息设置
  • User ---- 更换头像
  • 扩展

二、帖子管理(无刷新) ---- Admin

这个其实很简单,说到底也就是Fetch的使用而已

1、思路
  • 点击对应按钮,使用fetch发送数据到后端
  • 后端通过该数据进行相关操作,判断要返回什么到前端
  • 前端再根据后端返回的数据来进行页面处理
2、实现
  • 对页面标签进行处理:将<a>换成<button>,删除href属性,只保留type和class,然后新增data-id属性,其值设置为topic的id,我们后面要根据data-id的值来进行相关操作 =>
<button type="button" data-id="<%= topic.id %>" class="text-primary btn btn-danger text-light mr-3"> Delete </button>
  • 前端JS代码及解析
// 根据标签选中所有的button
let buttonArray = document.getElementsByTagName("button");
    // 遍历buttonArray,方便给button添加点击事件
    for(let i = 0; i < buttonArray.length; i++) { 
        // 添加点击事件
        buttonArray[i].onclick = function () {
            // 获取经过加工(删除字符串前后的空格)后的button的innerHTML,
            // 例如:Delete,通过这个判断进行何种操作
            const buttonValue = this.innerHTML.substring(1,this.innerHTML.length-1);
            // jquery 获取button的data-id的值,通过这个来判断操作的是那一个帖子
            let buttonDataId = $(this).attr("data-id");
            // fetch发送数据的url
            const url = "/admin/manageTopics/all";
            // fetch 开始
            fetch(url, {
            // 数据发送方式,post更加安全
                method: 'POST',
            // 请求头
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
            // 要发送的数据,JSON.stringify()可以将数据转换成Json格式
                body: JSON.stringify({
                    buttonValue: buttonValue,
                    buttonDataId: buttonDataId
                })
            })
                    .then(function(response) {
                        // 返回后端返回的数据
                        return response.json();
                    })
                    // 处理数据
                    .then(myJson => {
                 
                        if(myJson.msg === "Delete success") {
                            // 删除此button的父级的父级,也就是tr
                            $(this.parentNode.parentNode).remove();

                        } else if (myJson.msg === "Set top success") {
                            // 将button.innerHTML 设置为" Cancel top ",
                            // 别忘了前后加空格,要不然再次操作就会出错,
                            // 因为前面的截取字符串会导致结果不一样
                            $(this).html(" Cancel top ");
                            // 将按钮class属性里的btn-success替换成btn-warning
                            $(this).toggleClass("btn-success btn-warning");

                        } else if (myJson.msg === "Cancel top success") {
                            // 将button.innerHTML 设置为" Set top ",同理前后加空格
                            $(this).html(" Set top ");
                            // 将按钮class属性里的btn-warning替换成btn-success
                            $(this).toggleClass("btn-warning btn-success");

                        } else if (myJson.msg === "Set star success") {
                            // 将button.innerHTML 设置为" Cancel star ",同理前后加空格
                            $ (this).html(" Cancel star ");
                            // 将按钮class属性里的btn-success替换成btn-warning
                            $(this).toggleClass("btn-success btn-warning");

                        } else if (myJson.msg === "Cancel star success") {
                            // 将button.innerHTML 设置为" Set star ",同理前后加空格
                            $(this).html(" Set star ");
                            // 将按钮class属性里的btn-warning替换成btn-success
                            $(this).toggleClass("btn-warning btn-success");
                        }
                    });
        }
    }
  • 后端代码及解析
router.post("/admin/manageTopics/all", async (ctx) => {
    // 获取前端发送过来的数据
    const postBody = ctx.request.body;
    // 获取buttonValue,通过这个来判断用户进行的是哪种操作
    const buttonValue = postBody.buttonValue;
    // 获取buttonDataId,这个其实就是topic的id
    const buttonDataId = postBody.buttonDataId;
    // 使用switch判断buttonValue
    switch(buttonValue) {
        // buttonValue === "Delete"
        case "Delete":
            // 操作数据库,将id为buttonDataId的topic的useful设置为0
            // 即伪删除该topic,该topic会在后面的小黑屋中显示
            const deleteTopicByIdPromise = editTopic.deleteTopicById(buttonDataId);
            await deleteTopicByIdPromise;
            // 返回数据到前端
            ctx.body = {msg: "Delete success"};
            break;
        // buttonValue === "Set top"
        case "Set top":
            // 操作数据库,将id为buttonDataId的topic的top设置为1,即置顶帖子
            const setTopTopicPromise = editTopic.setTopTopic(buttonDataId);
            await setTopTopicPromise;
            // 返回数据到前端
            ctx.body = {msg: "Set top success"};
            break;
        // buttonValue === "Cancel top"
        case "Cancel top":
            // 操作数据库,将id为buttonDataId的topic的top设置为0,即取消置顶
            const reduceTopTopicPromise = editTopic.reduceTopTopic(buttonDataId);
            await reduceTopTopicPromise;
            // 返回数据到前端
            ctx.body = {msg: "Cancel top success"};
            break;
        // buttonValue === "Set star"
        case "Set star":
            // 操作数据库,将id为buttonDataId的topic的star设置为1,即设置精华帖子
            const setStarTopicPromise = editTopic.setStarTopic(buttonDataId);
            await setStarTopicPromise;
            // 返回数据到前端
            ctx.body = {msg: "Set star success"};
            break;
        // buttonValue === "Cancel star"
        case "Cancel star":
            // 操作数据库,将id为buttonDataId的topic的star设置为0,即设取消精华帖子
            const reduceStarTopicPromise = editTopic.reduceStarTopic(buttonDataId);
            await reduceStarTopicPromise;
            // 返回数据到前端
            ctx.body = {msg: "Cancel star success"};
            break;
    }

});
  • 效果图


    image.png

三、用户管理 ---- Admin

跟之前的topic展示差不多,不过没有必要进行无刷新处理,直接上代码及解析

  • 后端实现
// 管理用户
router.get("/admin/manageUsers/all", async (ctx) => {
    // 操作数据库,选取所有useful字段为1的用户
    const listAlluserPromise = editUser.listAlluser();
    const allUser = await listAlluserPromise;
    // 渲染页面
    await ctx.render("/admin/users", {
        allUser: allUser,
        layout: 'layouts/layout_admin',
    });
});
  • 前端展示
<% allUser.forEach(user => { %>
    <tr>
        <th scope="row"> <%= user.id %> </th>
        <td> <%= user.username %> </td>
        <td> <%= user.email %> </td>
        <td>
            <a class="text-light btn btn-danger mr-3" href="/admin/manageUsers/delete/<%= user.id %>">
                Delete
            </a>
        </td>
    </tr>
<% }) %>
  • 效果图


    image.png

四、小黑屋

1、为什么要有小黑屋

想一想:删除一个子论坛,那么该子论坛下的帖子你怎么处理

  • 在一个标准的论坛系统中,直接删除子论坛、用户、帖子是很不可取的,因为数据之间有很多的交叉点
  • 所以就有了伪删除这个概念 ---- 即并非真正的从数据中删除数据,而是将其设为“不可用”的状态
  • 我的做法 : 在子论坛、帖子、用户的信息中都新增一个useful字段,默认为一,删除它就是将useful设置为0,那么在读取数据的时候就得加个限定条件,即:useful = 1.
  • 展示子论坛、帖子、用户都只展示usuful = 1的数据
  • useful = 0的数据则放入“回收站”,及标题中的“小黑屋”
2、后端
// 小黑屋 ---- 管理伪删除的内容
router.get("/admin/blackHouse", async (ctx) => {
    const listDelBoardPromise = editBoard.listDelBoard();
    const listDelBoard = await listDelBoardPromise;

    const listDelUserPromise = editUser.listDelUser();
    const listDelUser = await listDelUserPromise;

    const listDelTopicPromise = editTopic.listDelTopic();
    const listDelTopic = await listDelTopicPromise;

    await ctx.render("/admin/black_house", {
        layout: 'layouts/layout_admin',
        listDelUser: listDelUser,
        listDelBoard: listDelBoard,
        listDelTopic: listDelTopic
    });
});
2、前端数据处理
  • 子论坛
<% listDelBoard.forEach(board => { %>
    <tr>
        <th scope="row"> <%= board.id %> </th>
        <td> <%= board.board_name %> </td>
        <td>
            <a class="btn btn-info" href="/admin/blackHouse/out/board/<%= board.id %>"> Let out </a>
        </td>
    </tr>
<% }) %>
  • 用户
<% listDelUser.forEach( user => { %>
    <tr>
        <th scope="row"> <%= user.id %> </th>
        <td> <%= user.username %> </td>
        <td> <%= user.email %> </td>
        <td>
            <a class="btn btn-info" href="/admin/blackHouse/out/user/<%= user.id %>"> Let out </a>
        </td>
    </tr>
<% }) %>
  • 帖子
<% listDelTopic.forEach( topic => { %>
    <tr>
        <th scope="row"> <%= topic.id %> </th>
        <td> <%= topic.title %> </td>
        <td> <%= topic.board_name %> </td>
        <td>
            <% if(topic.top === 1) {%>
                True
            <%} else {%>
                False
            <% } %>
        </td>
        <td>
            <% if(topic.star === 1) {%>
                True
            <%} else {%>
                False
            <% } %>
        </td>
        <td>
            <a class="btn btn-info mr-2" href="/admin/blackHouse/out/topic/<%= topic.id %>"> Let out </a>
            <a class="btn btn-danger" href="/admin/blackHouse/delete/topic/<%= topic.id %>"> 彻底删除 </a>
        </td>
    </tr>
<% }) %>
3、效果展示
image.png
4、还原处理

小黑屋作为一个回收站,当然得有还原的功能

之前我们就在每一条信息的后面预留了一个按钮(Let out),用来还原数据,所以我们现在来完成这些按钮的功能

  • 子论坛
// 小黑屋 ---- 恢复子论坛
router.get("/admin/blackHouse/out/board/:id", async (ctx) => {
    const id = ctx.params.id;
    const outBoardPromise = editBoard.outBoard(id);
    await outBoardPromise;
    await ctx.redirect("/admin/manageBoards");
});
  • 用户
// 小黑屋 ---- 恢复用户
router.get("/admin/blackHouse/out/user/:id", async (ctx) => {
    const id = ctx.params.id;
    const outUserPromise = editUser.outUser(id);
    await outUserPromise;
    await ctx.redirect("/admin/manageUsers/all");
});
  • 帖子
    这个有两个,一个是恢复,一个是彻底删除
//  小黑屋 ---- 彻底删除帖子
router.get("/admin/blackHouse/delete/topic/:id", async (ctx) => {
    const id = ctx.params.id;
    const deleteCompeteTopicPromise = editTopic.deleteCompeteTopic(id);
    await deleteCompeteTopicPromise;
    ctx.redirect("/admin/blackHouse");
});
//  小黑屋 ---- 恢复帖子
router.get("/admin/blackHouse/out/topic/:id", async (ctx) => {
    const id = ctx.params.id;
    const outTopicPromise = editTopic.outTopic(id);
    await outTopicPromise;
    ctx.redirect("/admin/manageTopics/all");
});

五、用户基本信息设置

1、用户中心概览
image.png
2、左侧
  • 我在头像的左上角加了一个编辑按钮,点击该按钮会跳转到头像设置页面
  • 其他的就没什么特别的了
3、Home

如上图,显示的是用户的一些常见数据,如注册时间等等

  • 我的处理方式是 =>:

在user表中新增一些字段用来存储这些数据(如:注册时间 ---- register_time),当然了,这方法并不好,肯定会有更好的

// 获取当前时间
const date = new Date();
const registerTime = date.toLocaleString();
  • 用户注册时间 ---- register_time =>
    用户注册成功时,使用系统提供的Date对象获取当前时间,并存入数据库

  • 上次登录时间 ---- last_login_time =>
    1、用户注册时系统默认登录,这时候并没有上次登录时间,显示默认值,所以新增此字段的时候可以设置一个默认值;
    2、普通登录:登录成功时,用户的“上次登录时间”就是上一次登录的“本次登录时间”

  • 本次登录时间 ---- login_time
    1、用户注册时系统默认登录:这时候的登录时间就是注册时间
    2、普通登录:登录时间就是登录成功时的时间

  • 上次发帖时间 ---- last_post_time
    1、用户未发表过帖子:显示默认值,新增字段时设置一个默认值,如“您还没有发表任何帖子”
    2、已发表过帖子:获取发帖成功时的时间,并存储到数据库中

  • 上次留言时间 ---- last_msg_time
    1、用户未曾留言:显示默认值,新增字段时设置一个默认值,如“您还没有留言”
    2、已有留言:获取留言成功时的时间,并存储到数据库中

  • 上次发表内容 ---- last_post
    1、用户未发表过帖子:显示默认值,新增字段时设置一个默认值,如“您还没有发表任何帖子”
    2、已发表过帖子:获取发帖成功时的帖子标题,并存储到数据库中

  • 上次留言内容 ---- last_msg
    1、用户未曾留言:显示默认值,新增字段时设置一个默认值,如“您还没有留言”
    2、已有留言:获取留言成功时的内容(内容过长时可显示一部分),并存储到数据库中

⚠️ 具体代码这里我就不贴出来了,有兴趣的话可查看源码
4、Profile
  • 概览


    image.png
  • 想法 =>

众所周知,有不少网站的支持单独修改某条个人信息中,我觉得这种方式很好,所以我也就朝着这个方向走了

  • 实现思路 =>
    1、每条信息给一个input标签,将用户的信息展示在placeholder中
    2、给input设置disable ,使其不可编辑
    3、每条信息后加一个🖋,用户点击之后删除input的disabled属性,使其变为可编辑状态,并且隐藏🖋,显示确定按钮
    4、用户输入内容后点击确定按钮后,使用fetch将数据传到后段,后端处理数据,然后返回数据
    5、前端接收到后端返回的数据后:隐藏确定按钮,显示🖋,给input新增disabled属性,并将input的value付给它的placeholder
    PS:前面的昵称:之类的就没什么好说的了,使用span标签包裹即可

  • 代码及解析

// 获取元素  PS:这个应该不需要再说的那么具体了吧
let editPen = document.getElementsByTagName("svg");
let inputArray = document.getElementsByTagName("input");
let btnArray = document.getElementsByTagName("button");
let spanArray = document.getElementsByTagName("span");
let sureBtn = document.getElementsByClassName("sure");
for(let i = 1; i < editPen.length; i++) {
      editPen[i].onclick = function() {
      // 隐藏 🖋
      editPen[i].style.display = "none";
      // 删除input的 disabled 属性
      inputArray[i].removeAttribute("disabled");
      // 修改border,可要可不要
      inputArray[i].style.border = "1px solid black";
      inputArray[i].style.padding = "2px";
      // 使确定按钮跟input在同一行
      sureBtn[i - 1].style.display = "inline-block";
      }
      btnArray[i+1].onclick = function() {
          const url = '/userHome';
          const inputValue = inputArray[i].value;
          // 裁剪span标签的文本,去除前后的空格和后面的":"
          const spanValue = spanArray[i+8].innerHTML.substring(1, spanArray[i+8].innerHTML.length-1);
              fetch(url, {
                  method: 'POST',
                  headers: {
                      'Accept': 'application/json',
                      'Content-Type': 'application/json',
                      'X-Requested-With': 'XMLHttpRequest'
                  },
                  body: JSON.stringify({
                      spanValue: spanValue,
                      inputValue: inputValue,
                  })
              })
              .then(function(response) {
                  return response.json();
              })
              .then(function(myJson) {
                  // 设置input的placeholder 为 myJson.inputValue, 这里的myJson.inputValue === inputValue
                  inputArray[i].placeholder = myJson.inputValue;
                  inputArray[i].setAttribute("disabled", "disabled");
                  sureBtn[i-1].style.display = "none";
                  inputArray[i].style.border= "none";
                  editPen[i].style.display = "inline-block";
              });
    }
}
5、密码更改

这里我使用的是模态框方式

image.png

1、逻辑

  • 用户原密码与新密码相想等 => 提示用户,修改密码失败
  • 用户原密码与新密码相等,但新密码1和2不相等,提示用户,修改失败
  • 用户原密码与新密码相等,切新密码1和2相等,修改成功

2、代码分析

router.post("/settings/advanced", async (ctx) => {
    // 获取用户输入的新、旧密码
    const oldPassword = ctx.request.body.oldPassword;
    const newPassword1 = ctx.request.body.newPassword1;
    const newPassword2 = ctx.request.body.newPassword2;
    // 获取用户id
    const id = ctx.session.user.id;
    // 获取用户信息
    const getUserPromise = editUser.getUserById(id);
    const user = await getUserPromise;
    //得到该用户的初始密码
    const userPassword = user[0].password;
    const data = [newPassword2, id]
    if(oldPassword !== userPassword) {
        console.log('你输入的旧密码不正确,请重新输入');
        ctx.redirect("/settings/advanced");
    } else {
        if(newPassword1 !== newPassword2) {
            console.log('你2次输入的新密码不一致,请重新输入')
            ctx.redirect("/settings/advanced");
        } else {
            // 重设密码
            const resetPasswordPromise = editUser.resetPassword(data);
            await resetPasswordPromise;
            console.log('恭喜你,修改密码成功,请牢记你的新密码!');
            ctx.redirect("/userHome");
        }
    }
});
6、Contact
  • 这个我并没有写,有兴趣的可自行添加

六、更换头像

其实这个也是可以写在用户信息设置里面的,但是因为这个比较难,所以单独写

  • 头像设置入口

1、用户中心 =>左侧 => 头像的左上角
2、用户中心 => Profile => 头像的右上角

  • 头像裁剪及上传图片路径到后端
// 页面加载完成后执行
    // 图片的裁剪需要用到一个外部文件,是别人已经写好了的 => /public/js/cropbox.js
    $(window).load(function() {
        var options =
            {
                thumbBox: '.thumbBox',
                spinner: '.spinner',
                // 设置默认的图片
                imgSrc: '/public/images/wenyiXJJ_1.png'
            }
        var cropper = $('.imageBox').cropbox(options);
        $('#image').on('change', function(){
            var reader = new FileReader();
            reader.onload = function(e) {
                options.imgSrc = e.target.result;
                cropper = $('.imageBox').cropbox(options);
            }
            reader.readAsDataURL(this.files[0]);
        })
        // 缩小图片
        $('#btnZoomIn').on('click', function(){
            cropper.zoomIn();
        })
        // 放大图片
        $('#btnZoomOut').on('click', function(){
            cropper.zoomOut();
        })
        $('#btnCrop').on('click', function(){
            // 获取img的地址
            var img = cropper.getDataURL();
            $('.cropped').html('');
            $('.cropped').append('<img id="img1" src="'+img+'" align="absmiddle" style="width:64px;margin-top:4px;border-radius:64px;box-shadow:0px 0px 12px #7E7E7E;" ><p>64px*64px</p>');
            $('.cropped').append('<img id="img2" src="'+img+'" align="absmiddle" style="width:128px;margin-top:4px;border-radius:128px;box-shadow:0px 0px 12px #7E7E7E;"><p>128px*128px</p>');
            $('.cropped').append('<img id="img3" src="'+img+'" align="absmiddle" style="width:180px;margin-top:4px;border-radius:180px;box-shadow:0px 0px 12px #7E7E7E;"><p>180px*180px</p>');
            var img1Src = document.getElementById('img3').src;
            // 获取input, 隐式上传数据到后端
            var upload_base = document.getElementById('upload_base');
            // 将img的src赋给type为hidden的input,以便将其上传到后端
            upload_base.value = img1Src;
        });
    });
  • 后端处理

router.post("/settings/profile/changeImage", upload.single('image'), async (ctx) => {
    // 获取前端上传过来的base64数据
    const value = ctx.req.body.upload_base;
    // 使用正则表达式截取有用的信息
    const base64Data = value.replace(/^data:image\/\w+;base64,/, "");
    // 使用Buffer.from()函数处理数据
    const dataBuffer = Buffer.from(base64Data, 'base64');
    // 获取用户的id
    const userId = ctx.session.user.id;
    // 定义用户新头像的存储路径及名称
    const newUserPicturePath = `public/uploads/${userId}.png`;
    // 写文件,保存头像文件
    fs.writeFile (newUserPicturePath, dataBuffer, function(err) {
        if (err) {
            console.log(err);
        }else{
            console.log("保存成功!");
            if(ctx.req.file) {
                // 删除base64的头像文件,没有这一部分的话,每一次换头像都会多一个你选取的图片文件
                const filename = ctx.req.file.filename;
                const savedFilePath = `public/uploads/${filename}`;
                fs.unlinkSync(savedFilePath);
            }
        }
    });
    // 获取用户id
    const id = ctx.session.user.id;
    const data=[newUserPicturePath, id];
    // 更新用户头像地址
    const resetPicturePromise = editUser.resetPicture(data);
    await resetPicturePromise; //新的头像路径保存完成,但是要更新session才能使头像立即生效
    // 获取用户信息
    const getUserInformationPromise = editUser.getUserById(id);
    const userArray = await getUserInformationPromise;
    const user = userArray[0];
    // 更新session
    ctx.session.user = user;

    // 在用户更新头像完成后,我们要将数据库中该用户发表的所有话题的topic_image_path
    // 换成该用户当前头像的路径,即 --> newUserPicturePath
    // 根据数据表topic中的每条topic的post_man字段我们可以得到发表该话题的用户,因为用户名唯一
    // 其实在这里用户名就是当前用户的username属性,因为session、更新,所以我们也用新的,
    // 即ctx.session.user.username. 其实它 === user.username
    const userName = ctx.session.user.username;
    const data2 = [newUserPicturePath, userName]
    const updateTopicImagePathByPostManPromise = editTopic.updateTopicPic(data2);
    await updateTopicImagePathByPostManPromise;
    // 同上,用户更换头像,该用户留言前的图片也应该换
    const data3 = [newUserPicturePath, userName];
    const updateMessagePicPromise = editMessage.updateMessagePic(data3);
    await updateMessagePicPromise;
    ctx.redirect('/userHome');
});

七、User ---- 留言功能

一个论坛,当然得有留言评论功能
这里同样使用fetch

  • 思路

1、用户点击按钮
2、获取文本域中用户输入的文本text
3、获取button的data-id的值id,用id作为判断帖子的依据,id为帖子的id
4、使用fetch将text、id上传到后端
5、后端根据这些数据来进行相应的处理:将留言内容存储到数据库中,并指定其属于哪个帖子
6、后端返回数据到前端,前端再进行相应处理:将留言展示到页面,并为新的留言添加样式,删除之前留言的特殊样式

  • 实现代码及解析
// 获取留言的数量,即id="messages"的ul中li的个数
    const length = document.getElementById("messages").getElementsByTagName("li").length;
    $("#btn").click(function(event){
        // 获取id , 此id === topic的id
        let id = $("#btn").attr("data-id");
        // 获取用户输入的留言内容
        let userMessage = $("#userMessage").val();
        const url = '/showTopics/all/' + id;
        fetch(url, {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify({
                id: id,
                message: userMessage,
            })
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            // 如果有留言
            if(length !== 0) {
                // 设置背景颜色为空
                document.getElementsByTagName("li")[6].style.background = "";
                // 移除字体样式
                document.getElementsByTagName("li")[6].classList.remove("text-danger", "font-weight-bold");
            }
            // 新定义一个标签li,用来存储新的留言
            const newMessageLi = `
                <li class="list-group-item text-danger font-weight-bold" style="background:greenyellow">
                    <a class="user_avatar pull-left" href="/userHome" style="width:28px;height:28px">
                        <img src="../../${myJson[3]}" title="${myJson[2]}" style="width:28px;height:28px">
                    </a>
                    ${myJson[1]}
                </li>
            `;
            // 将留言加入到页面中
            $("#messages").prepend(newMessageLi);
        });

        // 使留言的数量 ++
        document.getElementById("num_msg").innerHTML ++;
        // 清空文本域
        document.getElementById("userMessage").value = '';
    });

八、User ---- 简单的搜索功能

  • 思路

1、获取用户在输入框中输入的字符串string
2、获取数据库中所有useful=1的帖子
3、定义一个数组来保存的结果resultArray
4、遍历topic的title,使用indexOf找出title中包含string的帖子topic,将其存入resultArray中
5、然后将结果string与result传输到前端,前端再进行处理

  • 后端

router.post("/allTopic/results", async (ctx) => {
    // 需求: 拿到用户在搜索框中输入的字符串,将其与Topic表中所有topic的title进行对比,
    // 有相等的就展示出来,没有则提示用户说:未找到符合条件的内容,搜索的显示页面可以是另外一个页面

    // 拿到用户在搜索框中输入的字符串
    const userInputString = ctx.request.body.user_input_string;

    const listAllTopicFromBBSPromise = editTopic.listAllTopic();
    const allTopic = await listAllTopicFromBBSPromise;

    // 定义一个结果数组,用来存储找到的结果
    let resultArray = [];
    // 将符合条件的添加到results里面
    for(let i = 0; i < allTopic.length; i++) {
        if(allTopic[i].title.indexOf(userInputString) !== -1) {
            resultArray.push(allTopic[i]);
        }
    }
    const user = ctx.session.user;
    await ctx.render("/topics/show_results", {
        user: user,
        resultArray: resultArray,
        userInputString: userInputString
    });

});
  • 前端
<% if (resultArray.length === 0) { %>
     <li class="list-group-item list-group-item-action cell form-inline" style="font-size: 24px">
         <h4> 抱歉,找不到和您查询的“ <span class="text-danger"> <%= userInputString %> </span> ”相符的内容或信息。</h4>
         <h5> 建议: </h5>
         <p> ∙ 请检查输入字词有无错误。 </p>
         <p> ∙ 请尝试其他查询词。 </p>
         <p> ∙ 请改用较常见的字词。 </p>
     </li>
<% } %>

<% resultArray.forEach(topic => { %>
      <li class="list-group-item list-group-item-action cell form-inline" style="font-size: 24px">
          <a class="user_avatar pull-left" href="/userHome" style="width:28px;height:28px">
              <img src="../<%= topic.topic_image_path %>" title="<%= topic.post_man %>" style="width:28px;height:28px">
          </a>
          <span class="text-center bg-primary mr-1"> <%= topic.board_name%> </span>
          <a class="topic-link text-lg-center" href="/showTopics/all/<%= topic.id %>">话题:<%= topic.title %></a>
      </li>
<% })%>
  • 未找到
image.png

九、扩展

1、 帖子后面显示留言数量
  • 实现方法

  • 在帖子标题后新增一个span,用来展示留言数msg_num

  • 为了方便一点,在topic表新增字段msg_num,然后在留言的时候更新此字段

  • 实现


    image.png
2、用户中心 ---- 上次发帖内容可点击跳转到该帖子页面
  • 实现方式
    将topic的标题用a标签包裹即可,然后设置好href
3、用户中心 ---- 上次留言内容可点击跳转到对应的帖子页面,并将用户的上次留言设置为特殊样式
  • 新增留言的时候为其设置好样式,并取消上个留言的特殊样式,代码前面贴出来了,源码里也有
  • 在用户中心将留言内容包裹在a标签里,设置好href,使链接到对应的topic页面
  • 其实还可以加个页面定位功能,例如点击留言内容,跳转到该页面,然后使留言位于浏览器的中间,不过我没做
4、帖子的所属子论坛显示
  • 在表topic中新增字段board_name,存储对应的子论坛名字,在展示页面的时候将其使用span包裹并渲染出来即可,样式自定 =>
<span class="put_top"><%= topic.board_name %></span>

⚠️ 因为置顶与精华并非子论坛,所以要特殊处理 =>

<span class="put_top">精华</span>
<span class="put_top">置顶</span>
5、帖子的发表人头像
  • 跟所属子论坛差不多,定义一个新的字段,存储头像的路径,然后再渲染出来即可
  • PS: 其实这一类问题并不应该这样处理,而是应该使用mysql的关联表查询,大家可以试试,因为现在这种处理方式有点low
6、帖子展示优先级

这个难道是不难,但是在展示页面的时候会显得有点麻烦,因为要用模版语法

1、优先级 置顶 > 精华 > 普通
2、当某个帖子既是精华贴又被置顶,则显示置顶
PS:具体代码就不贴了,有兴趣的看源码吧


image.png

十、结束语

1、概括

此系列文章共计5篇文章,总用时5个星期

2、感想

本来以为写这个很简单,结果完全不是那么回事,我大概估计了下平均每篇文章用时4.5小时,甚至还不止。
期间我有怀疑过写这个有没有意义,想过是否要放弃写这个,毕竟花的时间太长了,好处说是可以梳理自己的知识体系,只不过并没有什么特别大的效果,懂的依旧懂,印象的深刻程度也没怎么变。
但作为一个有始有终的人,是坚决不会做“太监”的。经过一些思考、搜索、询问之后,我觉得文章好处肯定是有的,只不过短期内不是很明显而已,就跟运动一样,坚持下去,自然会收获。
第一次写这种文章,感觉自己的文笔不咋地、条理不是很清楚、周密性可能也不是很好,还会遗漏不少东西、写点错别字什么的,敬请各位读者大大见谅吧!
如果诸位有什么疑惑或者建议什么的欢迎留言,或者联系我也可以,万分感谢!

附:

1、分期项目地址

https://github.com/ShyGodB/Forum-Code-Synchronize-

2、完整项目地址

https://github.com/ShyGodB/BBS-by-Koa-Mysql

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容