开发问题详情页面
首先来了解一下问题详情页面的整体结构
这个页面的整体开发流程如下
- 显示页面
- 异步查询本问题的详细信息显示在当前问题区域
- 异步完成讲师回答问题添加到数据的功能
- 异步查询当前问题的所有回答
- 异步添加指定回答的评论
- 异步查询所有回答的评论
- 异步实现评论的修改和删除
显示问题详情页
开发步骤
步骤1:
复制static/question/detail.html文件到templates/question/detail.html
步骤2:
在HomeController中编写显示这个页面的方法
代码如下
//显示问题详情页面
@GetMapping("/question/detail.html")
public ModelAndView detail(){
return new ModelAndView("question/detail");
}
步骤3:
现在由于问题列表中任何一个问题的标题连接的都是/question/detail.html
所有都可以正确显示出问题详情页
但是我们现在没有办法知道用户请求的问题的具体id
我们可以采用将问题的id保存在url中的方法来实现页面显示之后使用异步查询问题详情
这个id的保存实现在index.html页面
因为index_teacher.html也是复用index.html页面内容的
index.html标题连接的a标签代码修改为
<a class="text-dark" href="question/detail.html"
v-bind:href="'/question/detail.html?'+question.id"
v-text="question.title">
eclipse 如何导入项目?
</a>
显示问题详情
步骤1:
按id查询Question的方法是有MybatisPlus直接提供的,所以Mapper不用写
直接从业务逻辑层开始
IQuestionService接口添加方法
//按id查询问题详情的方法
Question getQuestionById(Integer id);
步骤2:
QuestionServiceImpl类实现如下
@Override
public Question getQuestionById(Integer id) {
//先按id查询出Question
Question question=questionMapper.selectById(id);
//再按Question的tag_names列的标签转换为List<Tag>
List<Tag> tags=tagNamesToTags(question.getTagNames());
//将转换完成的List<Tag>保存到这个Question的tags属性中
question.setTags(tags);
return question;
}
步骤3:
编写控制层方法调用
QuestionController代码如下
//显示问题详细的Controller方法
//为了遵守RESTful的风格这个位置的路径比较特殊
//例如:/v1/questions/12
//上面的路径SpringMvc会自动将12赋值给{id}
//@PathVariable标记的同名属性的值也会是12
@GetMapping("/{id}")
public R<Question> question(
@PathVariable Integer id){
//判断必须要有id
if(id==null){
return R.invalidRequest("ID不能为空");
}
Question question=questionService.getQuestionById(id);
return R.ok(question);
}
步骤4:
编写detail.html代码
主要在当前问题显示详情的为位置
<!-- 素材187行 --->
<div id="questionApp" class="container-fluid bg-light">
<div id="questionApp" class="container-fluid bg-light">
<div class="row">
<div class="col-2 px-0 ">
<div class="container-fluid ">
<div class="row mt-4 mx-0 px-0" >
<a class="btn btn-outline-danger btn-md rounded-lg fa fa-close " style="font-size: x-small">删除</a>
</div>
<div class="row mt-4 mx-0 px-0" >
<a class="btn btn-outline-primary btn-md rounded-lg fa fa-edit " style="font-size: x-small"
href="../question/edit.html">编辑</a>
</div>
<div class="row mt-4 mx-0 px-0">
<a class="btn btn-outline-info btn-md rounded-lg fa fa-pencil " style="font-size: x-small"
href="#writeAnswer">回答</a>
</div>
<div class="row mt-4 mx-0 px-0" id="collectApp">
<a class="btn btn-outline-secondary btn-md rounded-lg fa fa-star "
style="font-size: x-small">收藏</a>
<a class="btn btn-outline-secondary btn-md rounded-lg fa fa-star "
style="font-size: x-small;display: none" >已收藏</a>
</div>
</div>
</div>
<div class="col-10 px-0">
<div class="container-fluid ">
<div class="row px-0 mb-3">
<div class="col-9 px-0">
<a class="badge badge-pill badge-info mx-1"
href="../tag/tag_question.html"
v-for="tag in question.tags"
v-text="tag.name">Java基础</a>
</div>
<div class="col-3 px-0">
<div class="row px-0">
<div class="col border-right text-right">
<p class="font-weight-light mb-0">收藏</p>
<p class="font-weight-bold mt-1">1</p>
</div>
<div class="col">
<p class="font-weight-light mb-0">浏览</p>
<p class="font-weight-bold mt-1"
v-text="question.pageViews">100</p>
</div>
</div>
</div>
</div>
<p class=" px-0 text-center font-weight-bold" style="font-size: x-large"
v-text="question.title">
Java中方法重载和重写的区别
</p>
<div class="px-0 container-fluid question-content"
v-html="question.content">
请问的方法中重写和重载的区别都是什么,如何使用
</div>
<p class="text-right px-0 mt-5">
<span class="font-weight-light badge badge-primary"
v-text="question.userNickName">张三</span>
<span class="font-weight-light badge badge-info"
v-text="question.duration">3天前</span>
</p>
</div>
</div>
</div>
</div>
末尾引用
</body>
<script src="../js/utils.js"></script>
<script src="../js/question_detail.js"></script>
</html>
步骤5:
创建question_detail.js文件
代码如下
let questionApp = new Vue({
el:"#questionApp",
data:{
question:{}
},
methods:{
loadQuestion:function(){
//获取浏览器地址栏中当前url中?之后的内容
let questionId=location.search;
console.log("questionId:"+questionId);
//判断是不是获得了?之后的内容
if(!questionId){
//如果没有?则终止
alert("必须指定问题id");
return;
}
//如果存在?之后的内容,则去掉? ?354
questionId=questionId.substring(1);
//发送异步请求
$.ajax({
url:"/v1/questions/"+questionId,//v1/questions/15
method:"get",
success:function(r){
console.log(r);
if(r.code == OK){
questionApp.question=r.data;
questionApp.updateDuration();
}else{
alert(r.message);
}
}
})
},
updateDuration:function(){
//获得问题中的创建时间属性(毫秒数)
let createtime=new Date(this.question.createtime).getTime();
//获得当前时间的毫秒数
let now=new Date().getTime();
//计算时间差(秒)
let durtaion=(now-createtime)/1000;
if(durtaion<60){
// 显示刚刚
//duration这个名字可以随便起,只要保证和页面上取的一样就行
this.question.duration="刚刚";
}else if(durtaion<60*60){
// 显示XX分钟
this.question.duration=
(durtaion/60).toFixed(0)+"分钟前";
}else if (durtaion<60*60*24){
//显示XX小时
this.question.duration=
(durtaion/60/60).toFixed(0)+"小时前";
}else{
//显示XX天
this.question.duration=
(durtaion/60/60/24).toFixed(0)+"天前";
}
}
},
created:function(){
this.loadQuestion();
}
});
完成讲师回复问题功能
开发页面和控制层
步骤1:
编写值对象类
在vo包中创建AnswerVo类
代码如下
@Data
@Accessors(chain = true)
public class AnswerVo implements Serializable {
@NotNull(message = "问题编号不能为空")
private Integer questionId;
@NotBlank(message = "回复内容不能为空")
private String content;
}
步骤2:
编写控制层代码
AnswerController中代码如下
@RestController
@RequestMapping("/v1/answers")
@Slf4j
public class AnswerController {
@Resource
private IAnswerService answerService;
//新增回复的控制方法
@PostMapping("")
@PreAuthorize("hasRole('ROLE_TEACHER')")
public R postAnswer(
@Validated AnswerVo answerVo,
BindingResult result){
log.debug("收到回复信息{}",answerVo);
if(result.hasErrors()){
String message=result.getFieldError().getDefaultMessage();
log.warn(message);
return R.unproecsableEntity(message);
}
//这里调用业务逻辑层方法
return R.created("回复问题成功!");
}
}
步骤3:
编写detail.html页面的代码
主要针对富文本编辑器提交的表单范围
<div class="container-fluid mt-4" id="postAnswerApp">
<h5 class="text-info mb-2"><i class="fa fa-edit"></i>写答案</h5>
<form action="#" method="post"
v-on:submit.prevent="postAnswer"
class="needs-validation" novalidate>
<div class="form-group">
<textarea id="summernote" name="content" required ></textarea>
<div class="invalid-feedback"
v-bind:class="{'d-block':hasError}">
<h5 v-text="message">回答内容不能为空!</h5>
</div>
</div>
<div class="form-group">
<p class="text-right">
<button type="submit" class="btn btn-primary">提交回答</button>
</p>
</div>
</form>
</div>
步骤4:
为了完成讲师回复时选择的图片上载,和讲师回复的提交操作
在question_detail.js文件中添加如下代码
$(function(){
$('#summernote').summernote({
height: 300,
lang: 'zh-CN',
placeholder: '请输入问题的详细描述...',
callbacks:{
//在执行指定操作后自动调用下面的方法
//onImageUpload方法就会在用户选中图片之后立即运行
onImageUpload:function(files) {
//参数是一个file数组取出第一个,因为我们只会选中一个
let file =files[0];
//构建表单
let form=new FormData();
form.append("imageFile",file);
$.ajax({
url:"/upload/file",
method:"post",
data:form,//发送的是我们构建的表单中的数据
//下面有两个特殊参数,需要在文件上传时设置
cache:false,
contentType:false,
processData:false,
success:function(r){
if(r.code==OK){
console.log(r);
//将刚刚上传成功的图片显示在summernote富文本编辑器中
var img=new Image();//实例化了一个img标签
img.src=r.message;//将img标签的src属性赋值为刚上传的图片
//summernote方法中提供了插入标签的功能
//支持使用"insertNode"表示要向富文本编辑器中添加标签内容
$("#summernote").summernote(
"insertNode",img
)
}else{
alert(r.message);
}
}
});
}
}
});
})
let postAnswerApp=new Vue({
el:"#postAnswerApp",
data:{
message:"",
hasError:false
},
methods:{
postAnswer:function(){
postAnswerApp.hasError=false;
let questionId=location.search;
if(!questionId){
this.message="没有问题ID";
this.hasError=true;
return;
}
//去掉?
questionId=questionId.substring(1);
let content=$("#summernote").val();
if(!content){
this.message="请填写回复内容";
this.hasError=true;
return;
}
let data={
questionId:questionId,
content:content
}
$.ajax({
url:"/v1/answers",
method:"post",
data:data,
success:function(r){
if(r.code==CREATED){
postAnswerApp.message=r.message;
postAnswerApp.hasError=true;
}else{
postAnswerApp.message=r.message;
postAnswerApp.hasError=true;
}
}
})
}
}
})
开发业务逻辑和数据访问层
步骤1:
IAnswerService接口中添加方法代码如下
public interface IAnswerService extends IService<Answer> {
//提交讲师回复问题的答案信息
Answer saveAnswer(AnswerVo answerVo,String username);
}
步骤2:
对这个接口方法进行实现
AnswerServiceImpl类中编写代码如下
@Service
@Slf4j
public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer> implements IAnswerService {
@Resource
private AnswerMapper answerMapper;
@Resource
private UserMapper userMapper;
@Override
@Transactional
public Answer saveAnswer(AnswerVo answerVo, String username) {
//收集信息,先获得当前回答问题的讲师的用户信息,结合answerVo
User user=userMapper.findUserByUsername(username);
Answer answer=new Answer()
.setUserId(user.getId())
.setUserNickName(user.getNickname())
.setContent(answerVo.getContent())
.setQuestId(answerVo.getQuestionId())
.setLikeCount(0)
.setAcceptStatus(0)
.setCreatetime(LocalDateTime.now());
int rows=answerMapper.insert(answer);
if(rows!=1){
throw new ServiceException("数据库忙!");
}
return answer;
}
}
步骤3:
重构AnswerController类中处理新增回复方法
//新增回复的控制方法
@PostMapping("")
@PreAuthorize("hasRole('ROLE_TEACHER')")
public R postAnswer(
@Validated AnswerVo answerVo,
BindingResult result,
@AuthenticationPrincipal User user){
log.debug("收到回复信息{}",answerVo);
if(result.hasErrors()){
String message=result.getFieldError().getDefaultMessage();
log.warn(message);
return R.unproecsableEntity(message);
}
//这里调用业务逻辑层方法
answerService.saveAnswer(answerVo,user.getUsername());
return R.created("回复问题成功!");
}
完成显示回答列表功能
如何实现显示回答问题列表功能
根据当前显示的问题的id,查询所有回答这个问题的答案
根据这个需求设计业务逻辑层开发
步骤1:
IAnswerService添加接口方法
//根据问题id查询这个问题的所有回答的方法
List<Answer> getAnswersByQuestionId(Integer questionId);
步骤2:
AnswerServiceImpl类中对这个方法进行实现
@Override
public List<Answer> getAnswersByQuestionId(Integer questionId) {
if(questionId==null){
throw ServiceException.invalidRequest("问题id不能为空");
}
QueryWrapper<Answer> query=new QueryWrapper<>();
query.eq("quest_id",questionId);
query.orderByDesc("createtime");
List<Answer> answers=answerMapper.selectList(query);
return answers;
}
步骤3:
完成控制器的调用
AnswerController中编写代码如下
//根据问题id获得这个问题的所有回答的方法
// 例如:/v1/answers/question/12
@GetMapping("/question/{id}")
public R<List<Answer>> questionAnswers(
@PathVariable Integer id){
if(id==null){
return R.invalidRequest("问题ID不能为空!");
}
List<Answer> answers=answerService
.getAnswersByQuestionId(id);
return R.ok(answers);
}
步骤4:
改写detail.html页面,使用Vue绑定回答相关信息
<div class="row mt-5 ml-2" id="answersApp">
<div class="col-12">
<div class="well-sm"><h3>
<span v-text="answers.length">3</span>条回答
</h3></div>
<div class="card card-default my-5"
v-for="answer in answers">
<!-- Default panel contents -->
<div class="card-header">
<div class="row">
<div class="col-1">
<img style="width: 50px;height: 50px;border-radius: 50%;"
src="../img/user.jpg">
</div>
<div class="col-8 ">
<div class="row">
<span class="ml-3"
v-text="answer.userNickName">张三</span>
</div>
<div class="row">
<span class="ml-3"
v-text="answer.duration">2天前</span>
</div>
</div>
<div class="3">
</div>
</div>
</div>
<div class="card-body ">
<span class="question-content text-monospace"
v-html="answer.content">
方法的重载是overloading,方法名相同,参数的类型或个数不同,对权限没有要求
方法的重写是overrding 方法名称和参数列表,参数类型,返回值类型全部相同,但是所实现的内容可以不同,一般发生在继承中
</span>
<!-- 以下代码略 -->
</div>
步骤5:
编写js文件
question_detail.js文件中再添加vue方法
let answersApp=new Vue({
el:"#answersApp",
data:{
message:"",
hasError:false,
answers:[]
},
methods:{
loadAnswers:function(){
let questionId=location.search;
if(!questionId){
this.message="必须有问题ID";
this.hasError=true;
return;
}
questionId=questionId.substring(1);
$.ajax({
url:"/v1/answers/question/"+questionId,
method:"get",
success:function(r){
if(r.code==OK){
answersApp.answers=r.data;
}else{
answersApp.message=r.message;
answersApp.hasError=true;
}
}
})
}
},
created:function(){
this.loadAnswers();
}
})