最近Vue使用体验感十分不错,深刻体会到这款前端框架的强大,在此也向尤雨溪前辈致敬。
其实Vue这个东西在数据渲染方面就像Java的EL表达式,如果用JQuery获得数据我们需要拼接节点的方式将数据渲染到页面上,如<td>data</td>。但是Vue不会对节点直接操作,而是使用{{data}}这种方式直接渲染到页面,如果有做过微信小程序开发的会知道小程序也是这种渲染方式。
当然,标题说的是文件上传,但是下面的案例也有涉及到其他知识点(Mybatis、Vue的其他知识点)。如果只想看文件上传的,就直接去增加或者修改功能模块看。案例有增删改查,下面的展示也是分功能展示。
0.先看一下整体效果以及项目结构
环境:
操作系统:win7
JDK版本:jdk8
开发工具:idea
1.数据库表、实体类说明
//没有实现此接口的类将不能使它们的任意状态被序列化或反序列化,简单说就是不让数据乱掉
public class Item implements Serializable {
private Integer id;//商品ID
private String name;//商品名称
private float price;//商品价格
private String detail;//商品备注
private String pic;//图片路径,图片不直接放在数据库,存放在本机路
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createtime;//创建时间,备注不是时间格式,是为了接收时间参数
//数据库就一张表,字段如上,在此不展示
get/set...
}
//查询类,方便我们查询
public class QueryVo implements Serializable {
private String name;//商品名
private Float minPrice;//商品最低价格
private Float maxPrice;//商品最低价格
//get/set..
}
2.配置
.c3p0.properties
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/itemdb?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root
SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.wy.item.entity"/>
</typeAliases>
</configuration>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.wy.item"/>
<context:property-placeholder location="classpath:c3p0.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:SqlMapConfig.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.wy.item.mapper"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.wy.item.contoller"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<!-- 配置文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760000"/>
<property name="maxInMemorySize" value="4096"/>
<property name="defaultEncoding" value="UTF-8"></property>
</bean>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="WEB-INF/content/"
p:suffix=".jsp"/>
</beans>
3.数据访问层
public interface ItemMapper {
List<Item> getItemList(QueryVo queryVo);
void updateItem(Item item);
void addItem(Item item);
void deleteItem(Integer id);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wy.item.mapper.ItemMapper">
<select id="getItemList" parameterType="queryVo" resultType="item">
select
id,
name,
price,
detail,
pic,
createtime
from items
where 1=1
<if test="name != null and name != ''">
and name = #{name}
</if>
<if test="minPrice != null and minPrice != '' and maxPrice != null and maxPrice != ''">
and price between #{minPrice} and #{maxPrice}
</if>
<!-- 如果没有任何条件则全查,这样减少代码冗余-->
</select>
<update id="updateItem" parameterType="item">
update items
set name = #{name},
price = #{price},
detail = #{detail},
pic = #{pic},
createtime = #{createtime}
where id = #{id}
</update>
<insert id="addItem" parameterType="item">
insert into items(
name,
price,
detail,
pic,
createtime
)
values(
#{name},
#{price},
#{detail},
#{pic},
#{createtime}
)
</insert>
<delete id="deleteItem" parameterType="Integer">
delete from items
where id = #{id}
</delete>
</mapper>
4.业务层略,只是调用数据访问层方法
5.Contoller,下面结合页面分别介绍增删改查(代码会展示部分,最后面会给出Contoller和页面的完整代码)
a.查询
@RestController//本类方法都会默认使用@ResponseBody注解
public class ItemContoller {
@Autowired
private ItemService itemService;
@RequestMapping("/getItemList")
public List<Item> getItemList(@RequestBody QueryVo queryVo) {//前端传来Json格式数据
System.out.println("ItemContoller.getItemList");
System.out.println(queryVo);
List<Item> itemList = itemService.getItemList(queryVo);
return itemList;//以Json格式响应给客户端
}
}
created() {
var _this = this;//注1
axios.post('getItemList', {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
console.log(response.data)
_this.itemList = response.data
})
},
可以先去最后面看看完整代码,这样再看部分的就会清楚很多。
created()是Vue的钩子函数,也就是生命周期函数。这个函数是Vue自动调用,不需要我们手动调用,调用时机则在Vue创建完成时。所以把获取所有数据的异步请求写在该函数中,目的就是一进页面通过异步请求加载所有数据。
注1:设置一个临时变量,把当前的Vue对象存起来。因为在下面的then(function (response))出现了闭包,如果还用this,则是属于axios
最后,页面渲染超简单
<p><b>编号:</b>{{item.id}}</p>
<p><b>名称:</b>{{item.name}}</p>
<p><b>价格:</b>{{item.price}}</p>
<p><b>备注:</b>{{item.detail}}</p>
b.删除
@RequestMapping("/deleteItem")
public String deleteItem(@RequestBody Map<String, Integer> id) {
System.out.println(id);
itemService.deleteItem(id.get("id"));
return "ok";
}
参数用Map接收是有原因的,前端以Json格式发送数据,在使用Jackson的情况下。直接使用Integer接收参数会抛出反序列化异常。
deleteItem: function (index, id) {
console.log(index)
console.log(id)
var _this = this
var dataId = {
"id": id
}
console.log(dataId.id)
axios.post('deleteItem', dataId)
.then(function (response) {
if (response.data == 'ok') {
alert('删除成功!')
_this.itemList.splice(index, 1)
}
})
},
c.添加
public class PicUtil {
public static String upPic(MultipartFile file) throws IOException {
if (file.getSize() > 0) {
String orName = file.getOriginalFilename();
String extName = orName.substring(orName.lastIndexOf("."));
String newName = new Date().getTime() + extName;
File file2 = new File("E:/imgs/" + newName);
file.transferTo(file2);
return newName;
} else {
System.out.println("没有选择文件");
}
return null;
}
}
上传文件这一套处理是要反复用的,所以把他封装成一个工具类。
@RequestMapping("/addItem")
public String addItem(MultipartFile file, Item item) throws IOException {
System.out.println(item);
item.setCreatetime(new Date());
String fileName = PicUtil.upPic(file);
System.out.println(fileName);
item.setPic(fileName);
itemService.addItem(item);
return "ok";
}
这里参数接收不能再用@RequestBody,前端传过来的不再是Json格式的数据。
后端收到一个请求,DispatcherServlet的checkMultipart()方法会调用MultipartResolver 的 isMultipart() 方法判断请求中是否包含文件。如果存在文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller。
addItem: function () {
var _this = this
var formData = new FormData($('#addForm')[0])
axios.post('addItem', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function (response) {
if (response.data == 'ok') {
alert('添加成功!')
_this.queryItem()
}
})
}
d.修改
@RequestMapping("/updateItem")
public String updateItem(MultipartFile file, Item item) throws IOException {
String fileName = PicUtil.upPic(file);
if (fileName != null && !fileName.trim().isEmpty()) {
item.setPic(fileName);
}
itemService.updateItem(item);
return "ok";
}
判断其实就是判断有没有修改图片,如果有修改则把新的图片路径set进实体类最后存入数据库,没有则使用原来路径
在前端表单中是这样实现的
<label class="col-sm-2 control-label">图片</label>
<input type="hidden" name="pic" :value="backData.pic"/>
<div class="col-sm-10">
<img style="width: 180px;height: 180px" :src="'/imgs/'+backData.pic">
<input type="file" class="form-control" name="file"/>
</div>
设置隐藏域,把原来的路径传回去
updateItem: function () {
var _this = this
var formData = new FormData($("#updateForm")[0])
console.log(formData)
axios.post('updateItem', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function (response) {
if (response.data == 'ok') {
alert('更新成功!')
_this.queryItem()
}
})
},
再说一下数据修改功能数据回显到模态框的问题
用Vue做十分简单
当我们点击修改按钮时先调用一个方法
backShow: function (index) {
this.backData = this.itemList[index]
console.log(this.backData)
},
通过下标从原来的数据数组中取出要修改的商品数据然后渲染到修改模态框
5.总结
到这里功能也就全部展示完了,下面贴一下页面和Contoller全部代码
@RestController
public class ItemContoller {
@Autowired
private ItemService itemService;
@RequestMapping("/getItemList")
public List<Item> getItemList(@RequestBody QueryVo queryVo) {
System.out.println("ItemContoller.getItemList");
System.out.println(queryVo);
List<Item> itemList = itemService.getItemList(queryVo);
return itemList;
}
@RequestMapping("/deleteItem")
public String deleteItem(@RequestBody Map<String, Integer> id) {
System.out.println(id);
itemService.deleteItem(id.get("id"));
return "ok";
}
@RequestMapping("/addItem")
public String addItem(MultipartFile file, Item item) throws IOException {
System.out.println(item);
item.setCreatetime(new Date());
String fileName = PicUtil.upPic(file);
System.out.println(fileName);
item.setPic(fileName);
itemService.addItem(item);
return "ok";
}
@RequestMapping("/updateItem")
public String updateItem(MultipartFile file, Item item) throws IOException {
String fileName = PicUtil.upPic(file);
if (fileName != null && !fileName.trim().isEmpty()) {
item.setPic(fileName);
}
itemService.updateItem(item);
return "ok";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>商品列表</title>
<script src="static/jquery-2.1.4.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/jquery.serializejson.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/vue.js" type="text/javascript" charset="utf-8"></script>
<script src="static/axios.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="static/bootstrap.css"/>
<style type="text/css">
body {
font-size: 16px;
}
</style>
</head>
<body>
<div id="app">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h3>商品</h3>
</div>
<div class="panel panel-body">
<div class="container-fluid">
<form id="queryForm" class="form-inline">
<div class="form-group">
<label class="control-label">商品名称:</label>
<input type="text" class="form-control" name="name"/>
</div>
<div class="form-group">
<label class="control-label">价格区间:</label>
<input type="text" class="form-control" name="minPrice" placeholder="最低价格"/>
</div>
——
<div class="form-group">
<input type="text" class="form-control" name="maxPrice" placeholder="最高价格"/>
</div>
<div class="form-group">
<input @click="queryItem" type="button" class="btn btn-primary" value="查询"/>
</div>
<div class="form-group">
<button type="button" class="btn btn-success"data-toggle="modal" data-target="#myModal2">添加商品
</button>
</div>
</form>
<div class="row">
<div class="col-md-4 text-left" v-for="(item, index) in itemList">
<img class="center-block" style="width: 300px;height: 300px"
v-if="item.pic != null && item.pic != ''" :src="'/imgs/'+item.pic">
<div class="center-block" style="width: 300px;height: 300px">
<p><b>编号:</b>{{item.id}}</p>
<p><b>名称:</b>{{item.name}}</p>
<p><b>价格:</b>{{item.price}}</p>
<p><b>备注:</b>{{item.detail}}</p>
<p>
<button @click="backShow(index)" type="button" class="btn btn-info" data-toggle="modal"
data-target="#myModal">修改
</button>
<button @click="deleteItem(index, item.id)" class="btn btn-danger">删除</button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">修改商品</h4>
</div>
<div class="modal-body">
<form id="updateForm" class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">编号</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="id" v-model="backData.id"
readonly="readonly"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" v-model="backData.name"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">价格</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="price" v-model="backData.price"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">备注</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="detail" v-model="backData.detail"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">图片</label>
<input type="hidden" name="pic" :value="backData.pic"/>
<div class="col-sm-10">
<div>
<img style="width: 180px;height: 180px" :src="'/imgs/'+backData.pic">
</div>
<input type="file" class="form-control" name="file"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">创建时间</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="createtime"
:value="backData.createtime | formatTime('YMDHMS')" readonly="readonly"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button @click="updateItem" type="button" class="btn btn-primary" data-dismiss="modal">
提交
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="myModal2" tabindex="-1" role="dialog" aria-labelledby="myModalLabel2">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">×</span></button>
<h4 class="modal-title">添加商品</h4>
</div>
<div class="modal-body">
<form id="addForm" class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">价格</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="price"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">备注</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="detail"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">图片</label>
<div class="col-sm-10">
<input type="file" class="form-control" name="file"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button @click="addItem" type="button" class="btn btn-primary" data-dismiss="modal">
提交
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript">
function addZero(val) {
if (val < 10) {
return "0" + val;
} else {
return val;
}
};
Vue.filter("formatTime", function (value, type) {
var dataTime = "";
var data = new Date();
data.setTime(value);
var year = data.getFullYear();
var month = addZero(data.getMonth() + 1);
var day = addZero(data.getDate());
var hour = addZero(data.getHours());
var minute = addZero(data.getMinutes());
var second = addZero(data.getSeconds());
if (type == "YMD") {
dataTime = year + "-" + month + "-" + day;
} else if (type == "YMDHMS") {
dataTime = year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
} else if (type == "HMS") {
dataTime = hour + ":" + minute + ":" + second;
} else if (type == "YM") {
dataTime = year + "-" + month;
}
return dataTime; //将格式化后的字符串输出到前端显示
});
var app = new Vue({
el: "#app",
data: {
itemList: [],
backData: {}
},
created() {
var _this = this;
axios.post('getItemList', {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
console.log(response.data)
_this.itemList = response.data
})
},
methods: {
queryItem: function () {
var _this = this
var dataForm = $('#queryForm').serializeJSON()
console.log(dataForm)
axios.post('getItemList', dataForm)
.then(function (response) {
console.log(22222222222222222222222222222222)
console.log(response.data)
_this.itemList = response.data
})
},
deleteItem: function (index, id) {
console.log(index)
console.log(id)
var _this = this
var dataId = {
"id": id
}
console.log(dataId.id)
axios.post('deleteItem', dataId)
.then(function (response) {
if (response.data == 'ok') {
alert('删除成功!')
_this.itemList.splice(index, 1)
}
})
},
backShow: function (index) {
this.backData = this.itemList[index]
console.log(this.backData)
},
updateItem: function () {
var _this = this
var formData = new FormData($("#updateForm")[0])
console.log(formData)
axios.post('updateItem', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function (response) {
if (response.data == 'ok') {
alert('更新成功!')
_this.queryItem()
}
})
},
addItem: function () {
var _this = this
var formData = new FormData($('#addForm')[0])
axios.post('addItem', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(function (response) {
if (response.data == 'ok') {
alert('添加成功!')
_this.queryItem()
}
})
}
}
})
</script>
</html>
项目还有很多地方需要完善,比如图片上传没有做验证,其使用Vue做图片验证比较简单,可以限制图片大小还有格式。