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

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

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

搭建Kafka+zookeeper集群调度

前言 硬件环境 172.18.0.5        kafkazk1        Kafka+zookeeper                Kafka Broker集群 172.18.0.6        kafkazk2        Kafka+zookeeper                Kafka Broker集群 172.18.0.7        kafkazk3

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

pico2 开发环境搭建-基于ubuntu

pico2 开发环境搭建-基于ubuntu 安装编译工具链下载sdk 和example编译example 安装编译工具链 sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib 注意cmake的版本,需要在3.17 以上 下载sdk 和ex