一、本期目标
给本系列文章画上一个相对圆满的句号
- 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;
}
});
-
效果图
三、用户管理 ---- 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>
<% }) %>
-
效果图
四、小黑屋
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、效果展示
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、用户中心概览
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
-
概览
- 想法 =>
众所周知,有不少网站的支持单独修改某条个人信息中,我觉得这种方式很好,所以我也就朝着这个方向走了
实现思路 =>
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、密码更改
这里我使用的是模态框方式
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>
<% })%>
-
未找到
九、扩展
1、 帖子后面显示留言数量
实现方法
在帖子标题后新增一个span,用来展示留言数msg_num
为了方便一点,在topic表新增字段msg_num,然后在留言的时候更新此字段
-
实现
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:具体代码就不贴了,有兴趣的看源码吧
十、结束语
1、概括
此系列文章共计5篇文章,总用时5个星期
2、感想
本来以为写这个很简单,结果完全不是那么回事,我大概估计了下平均每篇文章用时4.5小时,甚至还不止。
期间我有怀疑过写这个有没有意义,想过是否要放弃写这个,毕竟花的时间太长了,好处说是可以梳理自己的知识体系,只不过并没有什么特别大的效果,懂的依旧懂,印象的深刻程度也没怎么变。
但作为一个有始有终的人,是坚决不会做“太监”的。经过一些思考、搜索、询问之后,我觉得文章好处肯定是有的,只不过短期内不是很明显而已,就跟运动一样,坚持下去,自然会收获。
第一次写这种文章,感觉自己的文笔不咋地、条理不是很清楚、周密性可能也不是很好,还会遗漏不少东西、写点错别字什么的,敬请各位读者大大见谅吧!
如果诸位有什么疑惑或者建议什么的欢迎留言,或者联系我也可以,万分感谢!