从无到有搭建一个电商项目(九):品牌管理

2024-02-24 03:50

本文主要是介绍从无到有搭建一个电商项目(九):品牌管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

功能一:实现品牌查询功能

品牌查询这个功能,我们从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进行倍增, spacerspacer默认是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属性:

这篇关于从无到有搭建一个电商项目(九):品牌管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/740878

相关文章

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

配置springboot项目动静分离打包分离lib方式

《配置springboot项目动静分离打包分离lib方式》本文介绍了如何将SpringBoot工程中的静态资源和配置文件分离出来,以减少jar包大小,方便修改配置文件,通过在jar包同级目录创建co... 目录前言1、分离配置文件原理2、pom文件配置3、使用package命令打包4、总结前言默认情况下,

本地搭建DeepSeek-R1、WebUI的完整过程及访问

《本地搭建DeepSeek-R1、WebUI的完整过程及访问》:本文主要介绍本地搭建DeepSeek-R1、WebUI的完整过程及访问的相关资料,DeepSeek-R1是一个开源的人工智能平台,主... 目录背景       搭建准备基础概念搭建过程访问对话测试总结背景       最近几年,人工智能技术

python实现简易SSL的项目实践

《python实现简易SSL的项目实践》本文主要介绍了python实现简易SSL的项目实践,包括CA.py、server.py和client.py三个模块,文中通过示例代码介绍的非常详细,对大家的学习... 目录运行环境运行前准备程序实现与流程说明运行截图代码CA.pyclient.pyserver.py参

Linux内存泄露的原因排查和解决方案(内存管理方法)

《Linux内存泄露的原因排查和解决方案(内存管理方法)》文章主要介绍了运维团队在Linux处理LB服务内存暴涨、内存报警问题的过程,从发现问题、排查原因到制定解决方案,并从中学习了Linux内存管理... 目录一、问题二、排查过程三、解决方案四、内存管理方法1)linux内存寻址2)Linux分页机制3)

5分钟获取deepseek api并搭建简易问答应用

《5分钟获取deepseekapi并搭建简易问答应用》本文主要介绍了5分钟获取deepseekapi并搭建简易问答应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需... 目录1、获取api2、获取base_url和chat_model3、配置模型参数方法一:终端中临时将加

IDEA运行spring项目时,控制台未出现的解决方案

《IDEA运行spring项目时,控制台未出现的解决方案》文章总结了在使用IDEA运行代码时,控制台未出现的问题和解决方案,问题可能是由于点击图标或重启IDEA后控制台仍未显示,解决方案提供了解决方法... 目录问题分析解决方案总结问题js使用IDEA,点击运行按钮,运行结束,但控制台未出现http://

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时