45. 我的问答列表-前端页面
在index.html页面,先找到整个列表区域的父级,为其添加id
,以便于创建Vue对象:
<!-- 约第185行 -->
<div class="container-fluid" id="questionsApp">
找到每个问题的显示区域,为这个区域的根级添加v-for
以循环显示:
<!-- 约第193行 -->
<div v-for="question in questions" class="media bg-white m-2 p-3">
关于问题的状态:
代码语言:javascript复制<span class="badge badge-pill" v-bind:class="[question.statusClass]" v-text="question.statusText">已解决</span>
问题的标题:
代码语言:javascript复制<a class="text-dark" href="question/detail.html" v-text="question.title">eclipse 如何导入项目?</a>
问题的内容:
代码语言:javascript复制<div class="font-weight-light text-truncate text-wrap text-justify mb-2" style="height: 70px;" v-html="question.content">
<p>
eclipse 如何导入项目?
</p>
</div>
问题的标签列表:
代码语言:javascript复制<a v-for="tag in question.tags" class="text-info badge badge-pill bg-light" href="tag/tag_question.html">
<small v-text="tag.name">Java基础</small>
</a>
右下角的更多信息:
代码语言:javascript复制<small class="list-inline-item" v-text="question.userNickName">风继续吹</small>
<small class="list-inline-item"><span v-text="question.hits">12</span>浏览</small>
<small class="list-inline-item" v-text="question.createdTimeText">13分钟前</small>
显示图片:
代码语言:javascript复制<img v-bind:src="question.tagImage" class="ml-3 border img-fluid rounded" alt="" width="208" height="116">
添加了标签之后,在static下的**/js/question/文件夹下创建my.js**文件,先创建出Vue对象,并配置一些模拟的数据:
代码语言:javascript复制let questionsApp = new Vue({
el: '#questionsApp',
data: {
questions: [
{
statusText: '已解决',
statusClass: 'badge-success',
title: '这是第1个问题',
content: '很<b>严肃</b>的提出了第1个问题',
tags: [
{ id: 8, name: 'Java基础' },
{ id: 12, name: 'Spring' },
{ id: 15, name: 'SpringBoot' }
],
userNickName: '天下第一',
hits: '826',
createdTimeText: '8小时之前',
tagImage: '/img/tags/8.jpg'
},
{
statusText: '未回复',
statusClass: 'badge-warning',
title: '这是第2个问题',
content: '我也不告诉你是什么问题……',
tags: [
{ id: 10, name: 'MySQL' },
{ id: 17, name: 'SpringSecurity' }
],
userNickName: '天下第一',
hits: '537',
createdTimeText: '15小时之前',
tagImage: '/img/tags/10.jpg'
}
]
}
});
然后,在index.html中调用以上文件:
代码语言:javascript复制<script src="/js/question/my.js"></script>
完成后,先利用以上模拟的数据进行测试,也就是直接打开浏览器,观察运行效果与预期是否相符!
测试完成后,在my.js中,向服务器端发送请求获取真实的数据,并用于显示页面:
代码语言:javascript复制let questionsApp = new Vue({
el: '#questionsApp',
data: {
questions: []
},
methods: {
loadMyQuestions: function () {
$.ajax({
url: '/api/v1/questions/my',
success: function (json) {
// json.data.list
let data = json.data;
let questions = data.list;
let statusTexts = ['未回复', '未解决', '已解决'];
let statusClasses = ['badge-warning', 'badge-info', 'badge-success'];
for (let i = 0; i < questions.length; i ) {
questions[i].statusText = statusTexts[questions[i].status];
questions[i].statusClass = statusClasses[questions[i].status];
questions[i].tagImage = '/img/tags/' questions[i].tags[0].id '.jpg';
questions[i].createdTimeText = "未知时间";
}
questionsApp.questions = questions;
}
});
}
},
created: function () {
this.loadMyQuestions();
}
});
关于将时间显示为“刚刚” / “xx分钟前”等格式的代码:
代码语言:javascript复制let now = new Date().getTime();
let pastTime = (now - new Date(questions[i].createdTime).getTime()) / 1000;
let createdTimeText = "未知时间";
if (pastTime < 60) { // 不足1分钟
createdTimeText = "刚刚";
} else if (pastTime < 60 * 60) { // 不足1小时
createdTimeText = parseInt(pastTime / 60) "分钟前";
} else if (pastTime < 60 * 60 * 24) {
createdTimeText = parseInt(pastTime / 60 / 60) "小时前";
} else {
createdTimeText = parseInt(pastTime / 60 / 60 / 24) "天前";
}
questions[i].createdTimeText = createdTimeText;
最后,关于显示分页,首先要使得以前加载数据的函数是支持页码参数的:
代码语言:javascript复制loadMyQuestions: function (page) {
if (!page || page < 1) {
page = 1;
}
$.ajax({
url: '/api/v1/questions/my',
data: 'page=' page,
// 省略后续代码
});
}
关于分页的页面部分的代码:
代码语言:javascript复制<nav aria-label="Page navigation example">
<div class="pagination">
<a class="page-item page-link" href="#"
v-on:click.prevent="loadMyQuestions(pageInfo.prePage)">上一页</a>
<a class="page-item page-link " href="#"
v-for="i in pageInfo.navigatepageNums"
v-on:click.prevent="loadMyQuestions(i)"
v-bind:class="{ 'bg-primary text-light': i == pageInfo.pageNum }">
<span v-text="i">1</span>
</a>
<a class="page-item page-link" href="#"
v-on:click.prevent="loadMyQuestions(pageInfo.nextPage)">下一页</a>
</div>
</nav>
在my.js中,在属性中声明pageInfo
:
data: {
questions: [],
pageInfo: null
}
当获取数据后,添加:
代码语言:javascript复制questionsApp.pageInfo = data;
至此,页面的显示已完成,关于my.js的完整代码:
代码语言:javascript复制let questionsApp = new Vue({
el: '#questionsApp',
data: {
questions: [
{
statusText: '已解决',
statusClass: 'badge-success',
title: '这是第1个问题',
content: '很<b>严肃</b>的提出了第1个问题',
tags: [
{ id: 8, name: 'Java基础' },
{ id: 12, name: 'Spring' },
{ id: 15, name: 'SpringBoot' }
],
userNickName: '天下第一',
hits: '826',
createdTimeText: '8小时之前',
tagImage: '/img/tags/8.jpg'
},
{
statusText: '未回复',
statusClass: 'badge-warning',
title: '这是第2个问题',
content: '我也不告诉你是什么问题……',
tags: [
{ id: 10, name: 'MySQL' },
{ id: 17, name: 'SpringSecurity' }
],
userNickName: '天下第一',
hits: '537',
createdTimeText: '15小时之前',
tagImage: '/img/tags/10.jpg'
}
],
pageInfo: null
},
methods: {
loadMyQuestions: function (page) {
if (!page || page < 1) {
page = 1;
}
$.ajax({
url: '/api/v1/questions/my',
data: 'page=' page,
success: function (json) {
// json.data.list
let data = json.data;
let questions = data.list;
let statusTexts = ['未回复', '未解决', '已解决'];
let statusClasses = ['badge-warning', 'badge-info', 'badge-success'];
for (let i = 0; i < questions.length; i ) {
questions[i].statusText = statusTexts[questions[i].status];
questions[i].statusClass = statusClasses[questions[i].status];
questions[i].tagImage = '/img/tags/' questions[i].tags[0].id '.jpg';
let now = new Date().getTime();
let pastTime = (now - new Date(questions[i].createdTime).getTime()) / 1000;
let createdTimeText = "未知时间";
if (pastTime < 60) { // 不足1分钟
createdTimeText = "刚刚";
} else if (pastTime < 60 * 60) { // 不足1小时
createdTimeText = parseInt(pastTime / 60) "分钟前";
} else if (pastTime < 60 * 60 * 24) {
createdTimeText = parseInt(pastTime / 60 / 60) "小时前";
} else {
createdTimeText = parseInt(pastTime / 60 / 60 / 24) "天前";
}
questions[i].createdTimeText = createdTimeText;
}
questionsApp.questions = questions;
questionsApp.pageInfo = data;
}
});
}
},
created: function () {
this.loadMyQuestions();
}
});
46. 关于Summernote的图片处理
使用Summernote富文本编辑器时,当需要处理图片时,会自动将图片转换为Base64编码,当提交问题时,图片的Base64编码会作为“问题正文”的一部分提交到服务器端,最终,会被存储到数据库中!使用这种做法,会急剧增加数据库所占用的存储空间,对数据库的检索性能也会产生影响,不利于数据库的管理和维护,同时,由于图片已经转换为Base64编码作为正文的一部分数据,也不利于管理图片!
Summernote允许在配置Summernote富文本编辑器时自定义回调函数,该函数会在用户填写正文时选择图片会自动调用,则开发人员可以配置这个回调函数,当用户选择图片后,将图片以文件的形式直接上传到服务器端,当上传成功后,再将图片的路径返回到客户端,插入到Summernote中即可!
最后,在Summernote组织的“问题正文”中,关于图片可能就只是一段例如<img src="http://localhost:8080/1.jpg />
这样的代码,就能够减少数据库的存储数据量,同时,对于文件的管理也会非常直观。
47. 基于SpringMVC的文件上传
【本知识点的案例为:fileupload】
关于文件上传,在HTTP协议中规定:
- 必须使用POST方式提交请求;
- 在HTML表单中必须配置
enctype="multipart/form-data"
;
另外,在HTML表单中必须使用<input type="file" />
控件。
所以,可以将页面设计为:
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传图片</title>
</head>
<body>
<h1>上传图片</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<p>请选择您要上传的文件:<input type="file" name="image"></p>
<p><input type="submit" value="上传"></p>
</form>
</body>
</html>
SpringMVC框架处理文件上传是基于commons-fileupload
的,如果使用SpringMVC框架,需要自行添加这个依赖,如果使用SpringBoot框架则不需要,已经内置添加了。
在SpringMVC框架中,在控制器端会使用MultipartFile
接口类型的参数来接收客户端提交的上传数据,在处理请求的方法中,直接声明这个接口类型的参数即可,参数名应该与客户端提交请求时的名称保持一致!在处理请求的过程中,调用MutlipartFile
接口对象的void transferTo(File dest)
方法就可以将图片保持到参数dest
对应的文件位置。
可以在服务器端创建控制器处理上传请求:
代码语言:javascript复制@RestController
public class FileUploadController {
@RequestMapping("/upload")
public String upload(MultipartFile image) throws IOException {
image.transferTo(new File("d:/1.jpg"));
return "OK";
}
}
注意:SpringBoot默认限制了上传文件的大小为1M / 10M(根据SpringBoot版本不同存在差异)。
关于文件名的处理:
- 文件名必须保证唯一,不要出现“覆盖上传”的现象(即使你认为原有的文件没有用了,也不要覆盖);
- 扩展名应该与原始扩展名(文件在客户端设备中的名称)保持一致,注意:如果某个文件全名中只有第1位是小数点,并没有更多的小数点,是表示该文件在Linux / MacOS中是隐藏文件,小数点右侧的并不是扩展名!
示例代码:
代码语言:javascript复制@RequestMapping("/upload")
public String upload(MultipartFile image) throws IOException {
String parent = "d:/";
// 处理文件名
// 关于文件名的策略:时间 随机数
// 无论当前上传功能是用于哪个用途,文件名必须唯一
String filename = UUID.randomUUID().toString();
// 处理扩展名
// 获取原始文件名
String originalFilename = image.getOriginalFilename();
System.out.println("originalFilename=" originalFilename);
// 暂定扩展名空字符串
String suffix = "";
// 如果原始文件名中存在有效的扩展名,则截取
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex > 0) {
suffix = originalFilename.substring(beginIndex);
}
// 得到完整的文件名
String child = filename suffix;
// 保存上传的文件
image.transferTo(new File(parent, child));
return "OK";
}
关于保存文件的路径,首先,所有的上传都是为了下载的,所以,必须保证上传的文件夹是可以被访问到的文件夹,例如将文件上传到Tomcat的部署文件夹中,对于使用SpringBoot开发项目来说,也可以理解为“需要将文件上传到static文件夹或webapp文件夹下”!
可选的解决方案有:
代码语言:javascript复制String parent = request.getServletContext().getRealPath("20200725");
System.out.println("parent=" parent);
File parentFile = new File(parent);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
以上做法是将文件直接上传到项目的webapp文件夹中,这样做不便于管理文件,因为项目文件和上传的文件都在同个文件夹之下!
SpringMVC / SpringBoot可以自定义“资源目录”,当某个文件夹被设置为“资源目录”时,该目录下的内容是可以直接通过HTTP协议进行访问的!相当于static或webapp文件夹。
在SpringBoot项目的application.properties文件中进行配置:
代码语言:javascript复制spring.resources.static-locations=file:d:/upload
则d:/upload
就变成了“资源目录”,如果在这个文件夹中添加文件,是可以直接通过HTTP协议访问的!
然后,在application.properties中添加自定义配置,并将自定义配置值用于配置“资源目录”,并且,由于自定义了资源目录,原本static就不再是资源目录了,需要显式的指定:
代码语言:javascript复制project.upload-location=d:/upload
spring.resources.static-locations[0]=classpath:/static
spring.resources.static-locations[1]=file:${project.upload-location}
在控制器中,可以直接读取到以上配置:
代码语言:javascript复制@Value("${project.upload-location}")
private String parent;
后续,使用以上parent
作为上传的文件夹即可。
在处理上传时,关于MultipartFile
的常用API有:
-
boolean isEmpty()
:判断上传的文件是否为空,如果在表单中没有选择文件,或选择的文件是0字节的,即为空; -
long getSize()
:获取文件大小,以字节为单位; -
String contentType
:获取文档的MIME类型; -
String getOriginalFilename()
:获取上传的文件的原始文件名; -
void transferTo(File dest)
:保存上传的文件。