OK,经常前面的学习,我们基本掌握了 EasyUI 的使用。接下来的这个功能算是挺简单的。电影动态,就是新出了什么电影的动态信息,像我们的QQ空间动态、朋友圈动态一样:让别人了解发生了什么新鲜事。
OK,我们开始用程序的思维思考问题。先来创建这一节需要的数据库表:movie_dynamic
CREATE TABLE `movie_dynamic` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键,自动增长',
`movie_id` int(11) DEFAULT NULL COMMENT '参考 movie_info 表的主键',
`dynamic` varchar(100) DEFAULT NULL COMMENT '动态信息',
`movie_url` varchar(255) DEFAULT NULL COMMENT '电影参考链接',
`create_time` date DEFAULT NULL COMMENT '发布日期',
PRIMARY KEY (`id`),
KEY `movie_id` (`movie_id`),
CONSTRAINT `movie_dynamic_ibfk_1` FOREIGN KEY (`movie_id`) REFERENCES `movie_info` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='电影动态信息表';
这是设置了一个外键,参考 movie_info 表的主键。
使用 Mybatis 逆向工程,生成电影动态信息的实体和 mapper 文件,并剪切到对应的目录下。这里不再赘述。
package com.movie.entity;
import java.io.Serializable;
import java.util.Date;
public class MovieDynamicEntity implements Serializable {
private static final long serialVersionUID = -1816292474079277549L;
private Integer id;
private Integer movieId;//电影主键 id
private String dynamic;//电影动态信息
private String movieUrl;//电影参考链接
private Date createTime;//发布日期
为缩短篇幅,这里省略 get、set 方法,请自行增加。
}
MovieDynamicEntityMapper.xml 完整代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.movie.database.dao.MovieDynamicDao">
<resultMap id="BaseResultMap" type="com.movie.entity.MovieDynamicEntity">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="movie_id" jdbcType="INTEGER" property="movieId" />
<result column="dynamic" jdbcType="VARCHAR" property="dynamic" />
<result column="movie_url" jdbcType="VARCHAR" property="movieUrl" />
<result column="create_time" jdbcType="DATE" property="createTime" />
</resultMap>
<!-- 根据主键 id 删除 -->
<delete id="delete" parameterType="java.lang.Integer">
delete from movie_dynamic
where id = #{id,jdbcType=INTEGER}
</delete>
<!-- 新增,发布时间直接使用 mysql 的 now() 函数 -->
<insert id="insert" parameterType="com.movie.entity.MovieDynamicEntity">
insert into movie_dynamic
(movie_id, dynamic,movie_url,create_time)
values
(#{movieId,jdbcType=INTEGER},
#{dynamic,jdbcType=VARCHAR},
#{movieUrl,jdbcType=VARCHAR},
now())
</insert>
</mapper>
在 main.html 文件中,我们用到“电影动态管理”需要对应的文件名是:movieDynamic.html
我们在 html 文件夹上创建 movieDynamic.html
movieDynamic.html 的完整代码如下(需要好好斟酌一下,或者自学 EasyUI 的基本用法):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>电影动态信息管理页面</title>
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/default/easyui.css"></link>
<link rel="stylesheet" type="text/css" href="/static/jquery-easyui-1.7.0/themes/icon.css"></link>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/jquery.easyui.min.js"></script>
<script type="text/javascript" src="/static/jquery-easyui-1.7.0/locale/easyui-lang-zh_CN.js"></script>
<script type="text/javascript">
//进入页面时,自动调用 API 接口加载数据
$(document).ready(function(){
// 把电影名称调用 API 接口,加载到 movieComboList 下拉框中
$('#movieComboList').combobox({
mode:'remote' ,//remote 是远程模式,当用户输入字符到 input 文本框中,控件将发送 'q' 参数到远程服务器。
url:'/admin/movie/comboList',//调动的接口地址是电影管理的模块
valueField:'id',//数据字段:对应电影实体的id
textField:'movieName',//显示的字段:对应电影实体的名称
delay:200 //页面加载完毕后,延迟 200 毫秒加载
});
});
//模糊搜索电影动态信息
function searchInfo(){
//调用 datagrid 的 load 方法做查询
$("#movieDataGrid").datagrid('load',{
queryParams: {
dynamic: $("#searchId").val() //后台取值时,根据 dynamic 作为 key 来取值
}
});
}
//删除信息
function deleteInfo(){
//获取数据网格的选中框
var selectedRows = $("#movieDataGrid").datagrid("getSelections");
if(selectedRows.length == 0){
$.messager.alert("系统提示","请选择要删除的数据!");
return;
}
//将勾选的电影动态信息存入数组
var movieIds = [];
for(var i=0;i < selectedRows.length;i++){
movieIds.push(selectedRows[i].id);
}
var deleteIds = movieIds.join(",");
$.messager.confirm("系统提示","您确定要删除这<font color=red>"+selectedRows.length+"</font>条数据吗?",function(r){
if(r){
//使用 post 方法,调用 API 接口,删除 id 集合
$.post("/admin/movieDynamic/delete",{ids:deleteIds},function(result){
//获取接口返回的数据,API 接口需要返回 flag 作为 key 的键值对数据,即 Map
if(result.flag){
$.messager.alert("系统提示","数据已成功删除!");
$("#movieDataGrid").datagrid("reload");//重新加载网格数据
}else{
$.messager.alert("系统提示","数据删除失败!");
}
},"json");//指定 post 方法传递的是 json 格式的参数
}
});
}
var url;
//打开“添加电影动态信息”的窗口,把 url 赋值
function openInfoAddDialog(){
$("#dlg").dialog("open").dialog("setTitle","添加电影动态信息");
url="/admin/movieDynamic/save";
}
//点击“保存”按钮时调用的方法,注意这个 url 是打开窗口时赋值的 url
function saveInfo(){
$("#fm").form("submit",{
url:url,
onSubmit:function(){
return $(this).form("validate");//form 表单的基本校验
},
success:function(result){
var result = eval('('+result+')');
//API 后台返回的数据,flag 作为key
if(result.flag){
$.messager.alert("系统提示","保存成功!");
resetValue();//清空数据
$("#dlg").dialog("close");//关掉窗口
$("#movieDataGrid").datagrid("reload");//重新加载网格数据
}else{
$.messager.alert("系统提示","保存失败!");
return;
}
}
});
}
//关掉“添加电影动态信息”窗口,调用窗口“重置值”的方法
function closeInfoDialog(){
$("#dlg").dialog("close");
resetValue();
}
//窗口重置值的方法,将各个输入框的数据清空
function resetValue(){
$("#movieComboList").combobox("setValue","");//清空电影名称下拉框数据
$("#dynamicId").val("");//动态信息输入框
$("#movieUrlId").val("");//电影网站 URL 输入框
}
//格式化发布日期
function formatCreateTime(val,row) {
if (val != null) {
var date = new Date(val);
return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
}
}
</script>
</head>
<body style="margin: 1px">
<table id="movieDataGrid" title="电影动态信息管理" class="easyui-datagrid"
fitColumns="true" pagination="true" rownumbers="true"
url="/admin/movieDynamic/list" fit="true" toolbar="#tb">
<thead>
<tr>
<th field="cb" checkbox="true" align="center"></th>
<th field="movieName" width="100" align="center">电影名称</th>
<th field="dynamic" width="300" align="center">动态信息</th>
<th field="movieUrl" width="150" align="center">详细网址</th>
<th field="createTime" width="80" align="center" formatter="formatCreateTime">发布日期</th>
</tr>
</thead>
</table>
<div id="tb">
<div>
<a href="javascript:openInfoAddDialog()" class="easyui-linkbutton" iconCls="icon-add" plain="true">添加</a>
<a href="javascript:deleteInfo()" class="easyui-linkbutton" iconCls="icon-remove" plain="true">删除</a>
</div>
<div>
<!-- event.keyCode==13 是监听回车键 -->
电影动态信息: <input type="text" id="searchId" size="20" onkeydown="if(event.keyCode == 13) searchInfo()"/>
<a href="javascript:searchInfo()" class="easyui-linkbutton" iconCls="icon-search" plain="true">搜索</a>
</div>
</div>
<!-- 添加信息的窗口,默认关闭。 -->
<div id="dlg" class="easyui-dialog" style="width:500px;height:230px;padding: 10px 20px" closed="true" buttons="#dlg-buttons">
<form id="fm" method="post">
<table cellspacing="8px">
<tr>
<td>电影名称:</td>
<!-- 注意:form 表单的 name 对应电影动态信息实体的属性 -->
<td><input type="text" id="movieComboList" name="movieId" size="24" maxlength="80" class="easyui-combobox" data-options="required:true,panelHeight:'auto'"/></td>
</tr>
<tr>
<td>动态信息:</td>
<td><input type="text" id="dynamicId" name="dynamic" class="easyui-validatebox" required="true" style="width: 250px"/></td>
</tr>
<tr>
<td>详细网址:</td>
<td><input type="text" id="movieUrlId" name="movieUrl" class="easyui-validatebox" validType="url" required="true" style="width: 300px"/></td>
</tr>
</table>
</form>
</div>
<div id="dlg-buttons">
<a href="javascript:saveInfo()" class="easyui-linkbutton" iconCls="icon-ok">保存</a>
<a href="javascript:closeInfoDialog()" class="easyui-linkbutton" iconCls="icon-cancel">关闭</a>
</div>
</body>
</html>
先把服务起来,看下我们的页面是个什么效果。
点击“添加”按钮:
OK,有增加、删除的功能按钮,有搜索框,有数据网格,还有分页信息等等,这些都需要我们开发对应 API 接口。
我们先创建一个接口,即进入管理页面后,电影名称从数据库中读取到下拉列表中。
OK,刚从菜单进入页面时,就去调用 API 接口,拉取电影名称存入到下拉框中,这时候输入框内容是空的,所以获取到的数据应该是没有。同时,这是使用 remote 是远程模式,当用户输入字符到 input 文本框中,控件将发送 'q' 参数到远程服务器,也会调用 API 接口。这个路径请求的是 admin/movie/comboList ,在我们项目中分在 movieInfo 模块。因此我们需要在 MovieInfoController 类里增加这个 API 接口。
注意:
返回的对象必须是 result,因为前端已经根据 result.id 来获取返回的电影名称了。
MovieInfoController 完整代码如下:
package com.movie.controller.admin;
import com.movie.entity.MovieInfoEntity;
import com.movie.manage.MovieInfoManage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-17 上午 11:27
*/
@RestController
@RequestMapping(value = "/admin/movie")
public class MovieInfoController {
@Autowired
private MovieInfoManage movieInfoManage;
@ResponseBody
@RequestMapping("/save")
public Map<String,Object> save(MovieInfoEntity entity, @RequestParam("imageFile") MultipartFile file){
Map<String,Object> map = new HashMap<>();
boolean flag = movieInfoManage.save(entity,file);
map.put("flag",flag);
return map;
}
//富文本编辑器上传文件
@ResponseBody
@RequestMapping("/ckeditorUpload")
public String ckeditorUpload(@RequestParam("upload")MultipartFile file,String CKEditorFuncNum){
String result = movieInfoManage.ckeditorUpload(file,CKEditorFuncNum);
return result;
}
//模糊查询用户输入的电影名称,返回到下拉框中
@ResponseBody
@RequestMapping("/comboList")
public List<MovieInfoEntity> comboList(String q){
List<MovieInfoEntity> result = movieInfoManage.comboList(q);
return result;
}
}
接下来,需要在 MovieInfoManage 接口里增加 comboList 方法,完整代码如下:
package com.movie.manage;
import com.movie.entity.MovieInfoEntity;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-17 上午 11:32
*/
public interface MovieInfoManage {
//保存电影信息
boolean save(MovieInfoEntity entity, MultipartFile file);
//富文本编辑器上传文件
String ckeditorUpload(MultipartFile file,String CKEditorFuncNum);
//模糊查询用户输入的电影名称,返回到下拉框中
List<MovieInfoEntity> comboList(String q);
}
MovieInfoManageImpl 实现类完整代码;
package com.movie.manage.impl;
import com.movie.database.service.MovieInfoService;
import com.movie.entity.MovieInfoEntity;
import com.movie.manage.MovieInfoManage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-17 上午 11:36
*/
@Service
public class MovieInfoManageImpl implements MovieInfoManage {
@Value("${image.path}")
private String imageFilePath;
@Autowired
private MovieInfoService movieInfoService;
//新增电影信息
@Override
public boolean save(MovieInfoEntity entity, MultipartFile file) {
boolean flag = false;
try {
if (!StringUtils.isEmpty(file)) {
String originalFilename = file.getOriginalFilename();//获取文件名
entity.setImageName(originalFilename);//设置到实体中
String suffixName = originalFilename.substring(originalFilename.lastIndexOf("."));//获取文件后缀
String newFileName = System.currentTimeMillis() + suffixName;//新的文件名,避免文件名重复
FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(imageFilePath + newFileName));
}
//将富文本的 html 代码转换特殊字符,防止对网站进行xss跨站攻击
//方案 1:转成十进制
String movieContent = HtmlUtils.htmlEscapeDecimal(entity.getMovieContent());
//方案 2:转成十六进制
//String movieContent = HtmlUtils.htmlEscapeHex(entity.getMovieContent());
entity.setMovieContent(movieContent);
//解码只需一个方法即可,解码后返回给前端,跟传递进来的时候是一样的。
//String movieContent = HtmlUtils.htmlUnescape(entity.getMovieContent());
int count = movieInfoService.insert(entity);
if (count > 0) {
flag = true;
}
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
//富文本编辑器上传文件
@Override
public String ckeditorUpload(MultipartFile file,String CKEditorFuncNum){
String result = "";
try{
String originalFilename = file.getOriginalFilename();//获取文件名
String suffixName = originalFilename.substring(originalFilename.lastIndexOf("."));//获取文件后缀
String newFileName = System.currentTimeMillis() + suffixName;//新的文件名,避免文件名重复
FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(imageFilePath + newFileName));
StringBuilder sb = new StringBuilder();
sb.append("<script type=\"text/javascript\">");
//注意:/movieImages/ 返回虚拟路径给前端,对应配置类 WebConfig 的路径。
sb.append("window.parent.CKEDITOR.tools.callFunction("+ CKEditorFuncNum + ",'" + "/movieImages/" + newFileName + "','')");
sb.append("</script>");
result = sb.toString();
}catch (Exception e){
e.printStackTrace();
}
return result;
}
//模糊查询用户输入的电影名称,返回到下拉框中
@Override
public List<MovieInfoEntity> comboList(String q){
List<MovieInfoEntity> list = new ArrayList<>();
try{
if(!StringUtils.isEmpty(q)){
list = movieInfoService.findByParam(q);//调用接口作电影名称的模糊查询
}
}catch (Exception e){
e.printStackTrace();
}
return list;
}
}
MovieInfoService 类需要增加 findByParam 方法,完整代码:
package com.movie.database.service;
import com.movie.database.dao.MovieInfoDao;
import com.movie.entity.MovieInfoEntity;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-17 下午 12:27
*/
@Service
public class MovieInfoService {
@Autowired(required = false)
private MovieInfoDao movieInfoDao;
//新增电影信息。
//@Transactional(readOnly = false) //readOnly 的事务默认为 false,有读写的操作,可以不设置
public int insert(MovieInfoEntity entity){
return movieInfoDao.insert(entity);
}
//调用接口作电影名称的模糊查询
@Transactional(readOnly = true)//事务只读
public List<MovieInfoEntity> findByParam(@Param("q") String q){
return movieInfoDao.findByParam(q);
}
}
MovieInfoDao 接口需要增加 findByParam 方法,完整代码:
package com.movie.database.dao;
import com.movie.entity.MovieInfoEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-17 下午 12:33
*/
@Mapper
public interface MovieInfoDao {
//新增电影信息
int insert(MovieInfoEntity entity);
//调用接口作电影名称的模糊查询
List<MovieInfoEntity> findByParam(@Param("q") String q);
}
最后,MovieInfoEntityMapper.xml 文件需要增加查询的方法,我们限定只查询前 10 条记录(多少由你定)返回给接口。
<!-- 根据电影名称模糊查询,限制前 10 个即可 -->
<select id="findByParam" parameterType="java.lang.String" resultMap="BaseResultMap">
select id, movie_name
from movie_info
where movie_name like CONCAT('%',#{q,jdbcType=VARCHAR}, '%')
limit 0,10
</select>
这里注意 like (模糊查询)语句的用法,以及 concat 字符串拼接函数的用法。
OK,我们启动服务,测试功能。
点击左边的“电影动态管理”菜单,延迟 200 毫秒已经调用我们的 API 接口了。这是后输入框是空的,接口直接返回空数据。
如果这时候打开浏览器的调试模式(F12),会看到一行报错信息:
这是 EasyUI 的 DataGrid 调用的接口,因为我们还没写对应的 API 接口,所以报 404 错误,可以先忽略。
我们点击“添加”按钮,在电影名称的下拉框输入电影的某个字,也会远程调用了 API ,返回结果到下拉选项中。可以在数据库里增加更多的电影信息,来自行体验。
接下来,我们来完成这个电影动态信息的添加 API 接口。我们需要注意与 EasyUI 定义的参数对应:请求的 URL 路径以及返回的对象。
需要注意返回的 map 的键是 flag。
我们在 Controller 层创建类:MovieDynamicController,用来处理电影动态信息的请求,注意:不同的类、接口,请放在对应的包下,现在开始养成编码的好习惯,早日成为合格架构师!
完整代码如下:
package com.movie.controller.admin;
import com.movie.entity.MovieDynamicEntity;
import com.movie.manage.MovieDynamicManage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-22 下午 11:46
*/
@RestController
@RequestMapping(value = "/admin/movieDynamic")
public class MovieDynamicController {
@Autowired
private MovieDynamicManage movieDynamicManage;
//保存电影动态信息
@ResponseBody
@RequestMapping("/save")
public Map<String,Object> save(MovieDynamicEntity entity){
Map<String,Object> map = new HashMap<>();
boolean flag = movieDynamicManage.save(entity);
map.put("flag",flag);
return map;
}
}
然后,编写 MovieDynamicManage 接口,完整代码:
package com.movie.manage;
import com.movie.entity.MovieDynamicEntity;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-22 上午 11:32
*/
public interface MovieDynamicManage {
//保存电影动态信息
boolean save(MovieDynamicEntity entity);
}
然后,编写 MovieDynamicManage 接口的实现类 MovieDynamicManageImpl,完整代码:
package com.movie.manage.impl;
import com.movie.database.service.MovieDynamicService;
import com.movie.entity.MovieDynamicEntity;
import com.movie.manage.MovieDynamicManage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-22 上午 11:36
*/
@Service
public class MovieDynamicManageImpl implements MovieDynamicManage {
@Autowired
private MovieDynamicService movieDynamicService;
//新增电影信息
@Override
public boolean save(MovieDynamicEntity entity) {
boolean flag = false;
try {
int count = movieDynamicService.insert(entity);
if (count > 0) {
flag = true;
}
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
}
然后,编写 MovieDynamicService 类,完整代码:
package com.movie.database.service;
import com.movie.database.dao.MovieDynamicDao;
import com.movie.entity.MovieDynamicEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-22 下午 12:27
*/
@Service
public class MovieDynamicService {
@Autowired(required = false)
private MovieDynamicDao movieDynamicDao;
//新增电影动态信息。
//@Transactional(readOnly = false) //readOnly 的事务默认为 false,有读写的操作,可以不设置
public int insert(MovieDynamicEntity entity){
return movieDynamicDao.insert(entity);
}
}
然后,编写 MovieDynamicDao 接口,完整代码:
package com.movie.database.dao;
import com.movie.entity.MovieDynamicEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-11-22 下午 12:33
*/
@Mapper
public interface MovieDynamicDao {
//新增电影动态信息
int insert(MovieDynamicEntity entity);
}
OK,我们起服务,做一下测试。
①电影名称可以使用我们的搜索功能。
②动态信息随你喜欢填入:《少年的你》主演: 周冬雨/易烊千玺/尹昉/黄觉
③详细网址(EasyUI 帮我们做 URL 检查了)可以去优酷复制一段网址粘贴过来:https://v.youku.com/v_show/id_XNDQxNTU1MzM4MA==.html?spm=a2h0k.11417342.soresults.dposter&s=36df45e4a1a84b2baa39
点击保存,我们在后台打个断点,看下数据,自动封装成动态信息的实体给我们:
我们去数据库看下数据,正如我们预期的一致:
OK,下一篇博客继续讲解 EasyUI DataGrid 数据网格控件。