本文主要是介绍从无到有搭建一个电商项目(九):品牌管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
功能一:实现品牌查询功能
品牌查询这个功能,我们从0开始,实现下从前端到后端的完整开发
前端页面设计
新建一个MyBrand.vue(注意先停掉服务器),从0开始搭建
MyBrand.vue内容初始化:
<template><span>hello</span>
</template><script>export default {name: "myBrand"}
</script><!-- scoped:当前样式只作用于当前组件的节点 -->
<style scoped></style>
改变index.js中品牌的路由,将路由地址指向MyBrand.vue
route("/item/brand",'/item/MyBrand',"MyBrand")
打开服务器,查看页面:只显示一个hello,说明初始化完毕
1)查询表格
查看原型页面,猜测其主体就是一个table。我们去Vuetify查看有关table的文档:
仔细阅读,发现v-data-table中有以下核心属性:
- dark:是否使用黑暗色彩主题,默认是false
- expand:表格的行是否可以展开,默认是false
- headers:定义表头的数组,数组的每个元素就是一个表头信息对象,结构:
{text: string, // 表头的显示文本value: string, // 表头对应的每行数据的keyalign: 'left' | 'center' | 'right', // 位置sortable: boolean, // 是否可排序class: string[] | string,// 样式width: string,// 宽度
}
- items:表格的数据的数组,数组的每个元素是一行数据的对象,对象的key要与表头的value一致
- loading:是否显示加载数据的进度条,默认是false
- no-data-text:当没有查询到数据时显示的提示信息,string类型,无默认值
- pagination.sync:包含分页和排序信息的对象,将其与vue实例中的属性关联,表格的分页或排序按钮被触发时,会自动将最新的分页和排序信息更新。对象结构:
{page: 1, // 当前页rowsPerPage: 5, // 每页大小sortBy: '', // 排序字段descending:false, // 是否降序
}
- total-items:分页的总条数信息,number类型,无默认值
- select-all :是否显示每一行的复选框,Boolean类型,无默认值
- value:当表格可选的时候,返回选中的行
我们继续翻阅文档,看看有没有现成的案例:我们希望能在服务端完成对整体品牌数据的排序和分页,而下面这个案例恰好合适
查看源码,然后直接复制到MyBrand.vue中
<template><div><v-data-table:headers="headers":items="desserts":pagination.sync="pagination":total-items="totalDesserts":loading="loading"class="elevation-1"><template slot="items" slot-scope="props"><td>{{ props.item.name }}</td><td class="text-xs-right">{{ props.item.calories }}</td><td class="text-xs-right">{{ props.item.fat }}</td><td class="text-xs-right">{{ props.item.carbs }}</td><td class="text-xs-right">{{ props.item.protein }}</td><td class="text-xs-right">{{ props.item.iron }}</td></template></v-data-table></div>
</template>
2)表格分析
先看模板中table上的一些属性:
<v-data-table:headers="headers":items="desserts":pagination.sync="pagination":total-items="totalDesserts":loading="loading"class="elevation-1">
</v-data-table>
- headers:表头信息,是一个数组
- items:要在表格中展示的数据,数组结构,每一个元素是一行。在这里应该是品牌集合
- pagination.sync:分页信息,包含了当前页,每页大小,排序字段,排序方式等。加上.sync代表服务端排序,当用户点击分页条时,该对象的值会跟着变化。监控这个值,并在这个值变化时去服务端查询,即可实现页面数据动态加载了。
- total-items:总条数,在这里是品牌的总记录数
- loading:boolean类型,true:代表数据正在加载,会有进度条。false:数据加载完毕。
在v-data-tables中,我们还看到另一段代码:
<template slot="items" slot-scope="props"><td>{{ props.item.name }}</td><td class="text-xs-right">{{ props.item.calories }}</td><td class="text-xs-right">{{ props.item.fat }}</td><td class="text-xs-right">{{ props.item.carbs }}</td><td class="text-xs-right">{{ props.item.protein }}</td><td class="text-xs-right">{{ props.item.iron }}</td>
</template>
这段就是在渲染每一行的数据。Vue会自动遍历上面传递的items属性,并把得到的对象传递给这段template中的props.item属性。我们从中得到数据,渲染在页面即可。
我们需要做的事情,主要有两件:
- 给items和totalItems赋值
- 当pagination变化时,重新获取数据,再次给items和totalItems赋值
3)实现
表格中具体有哪些列要参照品牌表:id,name,image,letter字段
① 修改模板:
<template><div><v-data-table:headers="headers":items="brands":pagination.sync="pagination":total-items="totalBrands":loading="loading"class="elevation-1"><template slot="items" slot-scope="props"><td class="text-xs-center">{{ props.item.id }}</td><td class="text-xs-center">{{ props.item.name }}</td><td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td><td class="text-xs-center">{{ props.item.letter }}</td></template></v-data-table></div>
</template>
- items:指向一个brands变量,等下在js代码中定义
- total-items:指向了totalBrands变量,等下在js代码中定义
- template模板中,渲染了四个字段:
- id:
- name
- image,注意,我们不是以文本渲染,而是赋值到一个img标签的src属性中,并且做了非空判断
- letter
② js中编写数据模型
data () {return {totalBrands: 0, // 总条数brands: [], // 当前页品牌数据loading: true, // 是否在加载中pagination: {}, // 分页信息headers: [ // 头信息{text: 'id', align: 'center', value: 'id'},{text: '名称', align: 'center', value: 'name', sortable: false},{text: 'LOGO', align: 'center', value: 'image', sortable: false},{text: '首字母', align: 'center', value: 'letter'},]}}
③ 数据初始化
对brands和totalBrands完成赋值,编写一个函数来完成赋值,提高复用性:
methods: {getDataFromServer(){ // 从服务端加载数据的函数// 伪造演示数据const brands = [{"id": 2032,"name": "OPPO","image": "http://img10.360buyimg.com/popshop/jfs/t2119/133/2264148064/4303/b8ab3755/56b2f385N8e4eb051.jpg","letter": "O","categories": null},{"id": 2033,"name": "飞利浦(PHILIPS)","image": "http://img12.360buyimg.com/popshop/jfs/t18361/122/1318410299/1870/36fe70c9/5ac43a4dNa44a0ce0.jpg","letter": "F","categories": null},{"id": 2034,"name": "华为(HUAWEI)","image": "http://img10.360buyimg.com/popshop/jfs/t5662/36/8888655583/7806/1c629c01/598033b4Nd6055897.jpg","letter": "H","categories": null},{"id": 2036,"name": "酷派(Coolpad)","image": "http://img10.360buyimg.com/popshop/jfs/t2521/347/883897149/3732/91c917ec/5670cf96Ncffa2ae6.jpg","letter": "K","categories": null},{"id": 2037,"name": "魅族(MEIZU)","image": "http://img13.360buyimg.com/popshop/jfs/t3511/131/31887105/4943/48f83fa9/57fdf4b8N6e95624d.jpg","letter": "M","categories": null}];// 延迟一段时间,模拟数据请求时间setTimeout(()=>{this.brands = brands; // 赋值给品牌数组this.totalBrands = brands.length; // 赋值数据总条数this.loading = false; // 数据加载完成}, 1000);}}
使用钩子函数,在Vue实例初始化完毕后调用这个方法,这里使用mounted(渲染后)函数:
// 渲染后执行mounted(){this.getDataFromServer() // 调用数据初始化函数}
④ 页面完整代码
<template><div><v-data-table:headers="headers":items="brands":pagination.sync="pagination":total-items="totalBrands":loading="loading"class="elevation-1"><template slot="items" slot-scope="props"><td class="text-xs-center">{{ props.item.id }}</td><td class="text-xs-center">{{ props.item.name }}</td><td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td><td class="text-xs-center">{{ props.item.letter }}</td></template></v-data-table></div>
</template><script>export default {name: "myBrand",data () {return {totalBrands: 0, // 总条数brands: [], // 当前页品牌数据loading: true, // 是否在加载中pagination: {}, // 分页信息headers: [ // 头信息{text: 'id', align: 'center', value: 'id'},{text: '名称', align: 'center', value: 'name', sortable: false},{text: 'LOGO', align: 'center', value: 'image', sortable: false},{text: '首字母', align: 'center', value: 'letter'},]}},methods: {getDataFromServer(){ // 从服务端加载数据的函数// 伪造演示数据const brands = [{"id": 2032,"name": "OPPO","image": "http://img10.360buyimg.com/popshop/jfs/t2119/133/2264148064/4303/b8ab3755/56b2f385N8e4eb051.jpg","letter": "O","categories": null},{"id": 2033,"name": "飞利浦(PHILIPS)","image": "http://img12.360buyimg.com/popshop/jfs/t18361/122/1318410299/1870/36fe70c9/5ac43a4dNa44a0ce0.jpg","letter": "F","categories": null},{"id": 2034,"name": "华为(HUAWEI)","image": "http://img10.360buyimg.com/popshop/jfs/t5662/36/8888655583/7806/1c629c01/598033b4Nd6055897.jpg","letter": "H","categories": null},{"id": 2036,"name": "酷派(Coolpad)","image": "http://img10.360buyimg.com/popshop/jfs/t2521/347/883897149/3732/91c917ec/5670cf96Ncffa2ae6.jpg","letter": "K","categories": null},{"id": 2037,"name": "魅族(MEIZU)","image": "http://img13.360buyimg.com/popshop/jfs/t3511/131/31887105/4943/48f83fa9/57fdf4b8N6e95624d.jpg","letter": "M","categories": null}];// 延迟一段时间,模拟数据请求时间setTimeout(()=>{this.brands = brands; // 赋值给品牌数组this.totalBrands = brands.length; // 赋值数据总条数this.loading = false; // 数据加载完成}, 1000);}},// 渲染后执行mounted(){this.getDataFromServer() // 调用数据初始化函数}}
</script><!-- scoped:当前样式只作用于当前组件的节点 -->
<style scoped></style>
4)页面优化
① 编辑和删除按钮
将来要对品牌进行增删改,需要给每一行数据添加修改删除
的按钮,在Vuetify官方文档中找一个带有操作按钮的表格,作为参考:
headers
数组中添加一列:
{text: '操作', align: 'center', value: 'id', sortable: false }
然后在模板中添加按钮:
<td class="text-xs-center"><v-icon small class="mr-2" @click="editItem(props.item)">edit</v-icon><v-icon small @click="deleteItem(props.item)">delete</v-icon></td>
② 新增按钮
官方文档中找到按钮的用法:
新增跟某个品牌无关,是独立的,因此我们可以放到表格的外面。
③ 卡片(card)
为了不让按钮显得过于孤立,我们可以将按新增按钮和表格放到一张卡片(card)中。
官网查看卡片的用法:
卡片v-card包含四个基本组件:
- v-card-media:一般放图片或视频
- v-card-title:卡片的标题,一般位于卡片顶部
- v-card-text:卡片的文本(主体内容),一般位于卡片正中
- v-card-action:卡片的按钮,一般位于卡片底部
我们可以把新增的按钮放到v-card-title位置,把table放到下面,这样就成一个上下关系。
<template><v-card><v-card-title flat color="white"><v-btn color="primary">新增</v-btn></v-card-title><v-data-table:headers="headers":items="brands":pagination.sync="pagination":total-items="totalBrands":loading="loading"class="elevation-1"><template slot="items" slot-scope="props"><td class="text-xs-center">{{ props.item.id }}</td><td class="text-xs-center">{{ props.item.name }}</td><td class="text-xs-center"><img v-if="props.item.image" :src="props.item.image" width="130" height="40"/></td><td class="text-xs-center">{{ props.item.letter }}</td><td class="text-xs-center"><v-icon small class="mr-2" @click="editItem(props.item)">edit</v-icon><v-icon small @click="deleteItem(props.item)">delete</v-icon></td></template></v-data-table></v-card>
</template>
④ 添加搜索框
我们还可以在卡片头部添加一个搜索框,其实就是一个文本输入框。
查看官网中,文本框的用法:
- name:字段名,表单中会用到
- label/placeholder:提示文字
- value:值。可以用v-model代替,实现双向绑定
修改模板,添加输入框:
<v-card-title><v-btn color="primary">新增品牌</v-btn><!--搜索框,与search属性关联--><v-text-field label="输入关键字搜索" v-model="search"/>
</v-card-title>
注意:要在数据模型中,添加search字段:
data() {return {totalBrands: 0, // 总条数brands: [], // 当前页品牌数据search: "", // 查询关键字loading: true, // 是否在加载中pagination: {}, // 分页信息headers: [ // 头信息{text: 'id', align: 'center', value: 'id'},{text: '名称', align: 'center', value: 'name', sortable: false},{text: 'LOGO', align: 'center', value: 'image', sortable: false},{text: '首字母', align: 'center', value: 'letter'},{text: '操作', align: 'center', value: 'id', sortable: false}]}
}
发现输入框超级长!!!使用Vuetify提供的一个空间隔离工具:
<v-card-title><v-btn color="primary">新增品牌</v-btn><!--空间隔离组件--><v-spacer /><!--搜索框,与search属性关联--><v-text-field label="输入关键字搜索" v-model="search"/></v-card-title>
⑤ 添加搜索图标
查看textfiled的文档,发现:
通过append-icon属性可以为 输入框添加后置图标
<v-text-field label="输入关键字搜索" v-model="search" append-icon="search"/>
⑥ 把文本框变紧凑
搜索框看起来高度比较高,页面不够紧凑。这其实是因为默认在文本框下面预留有错误提示空间。通过下面的属性可以取消提示:
<v-text-field label="输入关键字搜索" v-model="search" append-icon="search" hide-details/>
页面最终效果图:
编写后台查询接口
1)数据库表
品牌表:
CREATE TABLE `tb_brand` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',`name` varchar(50) NOT NULL COMMENT '品牌名称',`image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',`letter` char(1) DEFAULT '' COMMENT '品牌的首字母',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';
品牌和商品分类是多对多关系,因此我们有一张中间表来维护两者之间的关系:
CREATE TABLE `tb_category_brand` (`category_id` bigint(20) NOT NULL COMMENT '商品类目id',`brand_id` bigint(20) NOT NULL COMMENT '品牌id',PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';
观察发现,这张表中没有设置外键约束,这与数据库的设计范式不符,为什么这么做?
- 外键会严重影响数据库读写的效率
- 数据删除时会比较麻烦
电商行业,性能非常重要。我们宁可在代码中通过逻辑来维护表关系,也不设置外键。
2)创建实体类
venom-item-interface包中创建Brand实体类
@Table(name = "tb_brand")
public class Brand {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;// 品牌名称private String image;// 品牌图片private Character letter;// getter setter 略
}
3)编写mapper
通用mapper来简化开发:
public interface BrandMapper extends Mapper<Brand> {
}
4)编写表现层Controller
思考四个问题,这次没有前端代码,需要我们自己来设定:
- 请求方式:查询,肯定是Get
- 请求路径:分页查询,/brand/page
- 请求参数:根据编写的页面,有分页功能,有排序功能,有搜索过滤功能,因此至少要有5个参数:
- page:当前页,int
- rows:每页大小,int
- sortBy:排序字段,String
- desc:是否为降序,boolean
- key:搜索关键词,String
- 响应结果:分页结果一般至少需要两个数据
- total:总条数
- items:当前页数据
- totalPage:有些还需要总页数
我们封装一个类,表示分页结果:PageResult以后可能在其它项目中也有需求,因此我们将其抽取到venom-common模块中,提高复用性
public class PageResult<T> {private Long total;// 总条数private Long totalPage;// 总页数private List<T> items;// 当前页数据public PageResult() {}public PageResult(Long total, List<T> items) {this.total = total;this.items = items;}public PageResult(Long total, Long totalPage, List<T> items) {this.total = total;this.totalPage = totalPage;this.items = items;}// getter setter 略
}
在venom-item-service工程的pom.xml中引入leyou-common的依赖:
<dependency><groupId>com.venom.common</groupId><artifactId>venom-common</artifactId><version>1.0.0-SNAPSHOT</version></dependency>
controller
@RestController
@RequestMapping("brand")
public class BrandController {@Autowiredprivate BrandService brandService;@RequestMapping("page")public ResponseEntity<PageResult<Brand>> queryBrandByPage(@RequestParam(value = "page",defaultValue = "1")Integer page,@RequestParam(value = "rows",defaultValue = "5")Integer rows,@RequestParam(value = "sortBy",required = false)String sortBy,@RequestParam(value = "desc",defaultValue = "false")Boolean desc,@RequestParam(value = "key",required = false)String key){PageResult<Brand> brandPageResult = this.brandService.queryBrandByPage(page,rows,sortBy,desc,key);if(brandPageResult == null || brandPageResult.getItems().size() == 0){return ResponseEntity.notFound().build();}return ResponseEntity.ok(brandPageResult);}
}
5)编写业务逻辑层Service
@Service
public class BrandService {@Autowiredprivate BrandMapper brandMapper;public PageResult<Brand> queryBrandByPageAndSort(Integer page, Integer rows, String sortBy, Boolean desc, String key) {// 开始分页PageHelper.startPage(page, rows);// 过滤Example example = new Example(Brand.class);if (StringUtils.isNotBlank(key)) {example.createCriteria().andLike("name", "%" + key + "%").orEqualTo("letter", key);}if (StringUtils.isNotBlank(sortBy)) {// 排序 注意留空格String orderByClause = sortBy + (desc ? " DESC" : " ASC");example.setOrderByClause(orderByClause);}// 查询Page<Brand> pageInfo = (Page<Brand>) brandMapper.selectByExample(example);// 返回结果return new PageResult<>(pageInfo.getTotal(), pageInfo);}
}
通过Postman测试:http://api.venom.com/api/item/brand/page
到此,后台接口编写完成!
异步查询工具axios
异步查询数据,通过ajax查询,首先想起的肯定是jQuery,但jQuery与MVVM的思想不吻合,而且ajax只是jQuery的一小部分。因此不可能为了发起ajax请求而去引用这么大的一个库。
1)axios入门
Vue官方推荐的ajax请求框架:axios
axios的Get请求语法:
axios.get("/item/category/list?pid=0") // 请求路径和请求参数拼接.then(function(resp){// 成功回调函数}).catch(function(){// 失败回调函数})
// 参数较多时,可以通过params来传递参数
axios.get("/item/category/list", {params:{pid:0}}).then(function(resp){})// 成功时的回调.catch(function(error){})// 失败时的回调
axios的POST请求语法:
axios.post("/user",{name:"Jack",age:21}).then(function(resp){}).catch(function(error){})
注意:POST请求传参,不需要像GET请求那样定义一个对象,在对象的params参数中传参。post()方法的第二个参数对象,就是将来要传递的参数;PUT和DELETE请求与POST请求类似
2)axios的全局配置
项目中,已经引入axios,并且进行了简单的封装,在src下的http.js中,http.js中对axios进行了一些默认配置:
import Vue from 'vue'
import axios from 'axios'
import config from './config'
// config中定义的基础路径是:http://api.venom.com/api
axios.defaults.baseURL = config.api; // 设置axios的基础请求路径
axios.defaults.timeout = 2000; // 设置axios的请求时间Vue.prototype.$http = axios;// 将axios赋值给Vue原型的$http属性,这样所有vue实例都可使用该对象
- http.js中导入了config的配置
- http.js对axios进行了全局配置:baseURL=config.api,即http://api.venom.com/api。因此以后所有用axios发起的请求,都会以这个地址作为前缀。
- 通过Vue.property.$ http = axios,将axios赋值给了 Vue原型中的$ http。这样以后所有的Vue实例都可以访问到$http,也就是访问到了axios。
3)小测一把
我们在组件MyBrand.vue的getDataFromServer方法,通过$http发起get请求,测试查询品牌的接口,看是否能获取到数据:
查看控制台结果:可以看到,在请求成功的返回结果response中,有一个data属性,里面就是真正的响应数据
- total:总条数,目前是165
- items:当前页数据
- totalPage:总页数,我们没有返回
异步加载品牌数据
虽然已经通过ajax请求获取了品牌数据,但是上面测试的请求没有携带任何参数,这样显然不对。我们后端接口需要5个参数:
- page:当前页,int
- rows:每页大小,int
- sortBy:排序字段,String
- desc:是否为降序,boolean
- key:搜索关键词,String
页面中分页信息应该是在pagination对象中,我们通过浏览器工具,查看pagination中有哪些属性:
- descending:是否是降序,对应请求参数的desc
- page:当前页,对应参数的page
- rowsPerpage:每页大小,对应参数中的rows
- sortBy:排序字段,对应参数的sortBy
缺少一个搜索关键词,这个应该是通过v-model与输入框绑定的属性:search。这样,所有参数就都有了。最后把查询的结果赋值给brands和totalBrands属性,Vuetify会帮我们渲染页面
完善请求参数:
methods: {getDataFromServer() { // 从服务端加载数据的函数this.loading = true; // 加载数据// 通过axios获取数据this.$http.get("/item/brand/page", {params: {page: this.pagination.page, // 当前页rows: this.pagination.rowsPerPage, // 每页条数sortBy: this.pagination.sortBy, // 排序字段desc: this.pagination.descending, // 是否降序key: this.search // 查询字段}}).then(resp => { // 获取响应结果对象this.totalBrands = resp.data.total; // 总条数this.brands = resp.data.items; // 品牌数据this.loading = false; // 加载完成});}}
完成分页和过滤
1)分页
前面实现了页面加载时的第一次查询,但是点击分页或搜索不会发起新的请求,怎么解决?
虽然点击分页,不会发起请求,但通过浏览器工具查看,会发现pagination对象的属性一直在变化:
我们可以利用Vue的监视功能:watch,当pagination发生改变时,会调用我们的回调函数,我们在回调函数中进行数据的查询:这样就实现了分页
watch: {pagination: { // 监视pagination属性变化deep: true, // deep为true,会监视pagination的属性及属性中的对象属性变化handler() { // 变化后的回调函数,再次调用getDataFromServerthis.getDataFromServer();}}}
2)过滤
实现过滤跟分页一样。过滤字段对应的是search属性,我们只要监视这个属性即可:
功能二:实现品牌的新增功能
1)页面实现
① 编写弹窗
分析:当点击新增按钮,应该出现一个弹窗,然后在弹窗中出现一个表格,我们就可以填写品牌信息
查看Vuetify官网,弹窗是如何实现:
通过文档看到对话框的一些属性:
- value:控制窗口的可见性,true可见,false,不可见
- max-width:控制对话框最大宽度
- with:设置对话框的宽度
- scrollable :是否可滚动,要配合v-card来使用,默认是false
- persistent :点击弹窗以外的地方不会关闭弹窗,默认是false
我们在data中定义一个dialog属性,来控制对话框的显示状态:
dialog: false // 通过它控制对话框的显示
在页面增加一个对话框v-dialog
<!--弹出的对话框--><v-dialog persistent v-model="dialog" width="500"><v-card><!--对话框标题--><v-toolbar dense dark color="primary"><v-toolbar-title>新增品牌</v-toolbar-title></v-toolbar><!--对话框内容--><v-card-text class="px-5">这是一个表单</v-card-text></v-card></v-dialog>
组件说明:
dialog指定了3个属性:
- width:限制宽度
- v-model:value值双向绑定到dialog变量,用来控制窗口显示
- persisitent:控制窗口不会被意外关闭
因为可滚动需要配合v-card使用,因此我们在对话框中加入了一个v-card
在v-card的头部添加了一个 v-toolbar,作为窗口的头部,并且写了标题为:新增品牌
- dense:紧凑显示
- dark:黑暗主题
- color:颜色,primary就是整个网站的主色调,蓝色
在v-card的内容部分,暂时空置,等会写表单
- class=“px-5":vuetify的内置样式,含义是padding的x轴设置为5,这样表单内容会缩进一些,而不是顶着边框
基本语法:{property}{direction}-{size}
- property:属性,有两种padding和margin
- p:对应padding
- m:对应margin
- direction:只padding和margin的作用方向,
- t - 对应margin-top或者padding-top属性
- b - 对应margin-bottom or padding-bottom
- l - 对应margin-left or padding-left
- r - 对应margin-right or padding-right
- x - 同时对应*-left和*-right属性
- y - 同时对应*-top和*-bottom属性
- size:控制空间大小,基于 s p a c e r 进 行 倍 增 , spacer进行倍增, spacer进行倍增,spacer默认是16px
- 0:将margin或padding的大小设置为0
- 1:将margin或者padding属性设置为$spacer * .25
- 2:将margin或者padding属性设置为$spacer * .5
- 3:将margin或者padding属性设置为$spacer
- 4:将margin或者padding属性设置为$spacer * 1.5
- 5:将margin或者padding属性设置为$spacer * 3
② 实现弹窗的可见和关闭
窗口可见:点击新增品牌按钮时,将窗口显示,因此要给新增按钮绑定事件
<v-btn color="primary" @click="addBrand()">新增</v-btn>
定义一个addBrand方法:
addBrand(){// 控制弹窗可见:this.dialog = true;
}
窗口关闭:我们设置了persistent属性,窗口无法被关闭了。除非把dialog属性设置为false,因此我们需要给窗口添加一个关闭按钮:
<!--对话框的标题-->
<v-toolbar dense dark color="primary"><v-toolbar-title>新增品牌</v-toolbar-title><v-spacer/><!--关闭窗口的按钮--><v-btn icon @click="close"><v-icon>close</v-icon></v-btn>
</v-toolbar>
我们还给按钮绑定了点击事件,回调函数为close:
close(){// 关闭窗口this.dialog = false;
}
③ 表单页
我们编写一个组件,组件内写表单代码。然后在对话框引用组件。优点:
- 表单代码独立组件,可拔插,方便后期的维护。
- 代码分离,可读性更好。
新建一个MyBrandForm.vue组件:
<template><div>my brand form</div>
</template><script>export default {name: "MyBrandForm"}
</script><style scoped></style>
将MyBrandForm引入到MyBrand中,这里使用局部组件的语法
在js中先导入自定义组件:
// 导入自定义的表单组件import MyBrandForm from './MyBrandForm'
通过components属性来指定局部组件:
components:{MyBrandForm
}
页面中引用:
<v-card-text class="px-5">这是一个表单<my-brand-form></my-brand-form>
</v-card-text>
创建表单
查看文档,找到关于表单的部分:
v-form,表单组件,内部可以有许多输入项。v-form有下面的属性:
- value:true,代表表单验证通过;false,代表表单验证失败。
v-form提供了两个方法:
- reset:重置表单数据
- validate:校验整个表单数据,前提是你写好了校验规则。返回Boolean表示校验成功或失败
在data中定义一个valid属性,跟表单的value进行双向绑定,观察表单是否通过校验,同时把等会要跟表单关联的品牌brand对象声明出来:
export default {name: "my-brand-form",data() {return {valid:false, // 表单校验结果标记brand:{name:'', // 品牌名称letter:'', // 品牌首字母image:'',// 品牌logocategories:[], // 品牌所属的商品分类数组}}}}
页面先写一个表单:
<v-form v-model="valid"></v-form>
文本框
品牌总共需要这些字段:名称,首字母,商品分类,LOGO
表单项主要包括文本框、密码框、多选框、单选框、文本域、下拉选框、文件上传等。
品牌需要的表单项:
- 文本框:品牌名称、品牌首字母都属于文本框
- 文件上传:品牌需要图片,这个是文件上传框
- 下拉选框:商品分类提前已经定义好,这里需要通过下拉选框展示,提供给用户选择。
查看文档,v-text-field
有以下关键属性:
- append-icon:文本框后追加图标,需要填写图标名称。无默认值
- clearable:是否添加一个清空图标,点击会清空文本框。默认是false
- color:颜色
- counter:是否添加一个文本计数器,在角落显示文本长度,指定true或允许的最大长度。无默认值
- dark:是否应用黑暗色调,默认是false
- disable:是否禁用,默认是false
- flat:是否移除默认的动画效果,默认是false
- full-width:指定宽度为全屏,默认是false
- hide-details:是否隐藏错误提示,默认是false
- hint:输入框的提示文本
- label:输入框的标签
- multi-line:是否转为文本域,默认是false。文本框和文本域可以自由切换
- placeholder:输入框占位符文本,focus后消失
- required:是否为必填项,如果是,会在label后加*,不具备校验功能。默认是false
- rows:文本域的行数,multi-line为true时才有效
- rules:指定校验规则及错误提示信息,数组结构。默认[]
- single-line:是否单行文本显示,默认是false
- suffix:显示后缀
级联下拉选框
分析:商品分类应该是下拉选框,它包含三级。在展示的时候,应该是先由用户选中1级,才显示2级;选择了2级,才显示3级。形成一个多级分类的三级联动效果。Vuetify中并没有提供(它提供的是基本的下拉框),这里已经自定义一个无限级联动的下拉选框,我们直接使用:
<v-cascaderurl="/item/category/list"multiple requiredv-model="brand.categories"label="请选择商品分类"/>
- url:加载商品分类选项的接口路径
- multiple:是否多选,这里设置为true,因为一个品牌可能有多个分类
- requried:是否是必须的,这里为true,会在提示上加*,提醒用户
- v-model:关联我们brand对象的categories属性
- label:文字说明
文件上传项
Vuetify中,也没有文件上传的组件。已经自定义好了一个文件上传的组件,我们直接用:
<v-layout row><v-flex xs3><label style="color: rgba(0,0,0,.54); font-size: 16px">品牌LOG:</label></v-flex><v-flex><v-uploadv-model="brand.image"url="/upload":multiple="false":pic-width="250":pic-height="90"/></v-flex></v-layout>
注意:
- 文件上传组件本身没有提供文字提示。因此我们需要自己添加一段文字说明
- 我们要实现文字和图片组件左右放置,因此这里使用了v-layout布局组件:
- layout添加了row属性,代表这是一行,如果是column,代表是多行
- layout下面有v-flex组件,是这一行的单元,我们有2个单元
<v-flex xs3>
:显示文字说明,xs3是响应式布局,代表占12格中的3格- 剩下的部分就是图片上传组件了
- v-upload:图片上传组件,包含以下属性:
- v-model:将上传的结果绑定到brand的image属性
- url:上传的路径,我们先随便写一个。
- multiple:是否运行多图片上传,这里是false。因为品牌LOGO只有一个
- pic-width和pic-height:可以控制图片上传后展示的宽高
按钮
表单的最下面添加两个按钮:提交和重置
<v-layout row my-3><v-spacer/><v-btn @click="reset">重置</v-btn><v-btn @click="submit" color="primary">提交</v-btn></v-layout>
- 通过layout来进行布局,my-3增大上下边距
- v-spacer占用一定空间,将按钮都排挤到页面右侧
- 两个按钮分别绑定了submit和reset事件
④ 重置表单
因为v-form组件已经提供了reset方法,用来清空表单数据。只要我们拿到表单组件对象,就可以调用方法了,我们可以通过$refs
内置对象来获取表单组件
首先,在表单上定义ref属性:
这篇关于从无到有搭建一个电商项目(九):品牌管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!