这是在学习 Vue 的模板语法和常用特性后自己敲的一个小练习。目的是避免眼高手低,做到掌握 Vue 的基本使用。其中具体涉及的模板语法和常用特性详细介绍在 xxx 记录(相关笔记还未总结~~)。
静态页面如下图所示:
功能简要描述:在表单中输入编号和图书名称在编号唯一(且还未存在)情况下可以添加书籍,添加成功后在表格中展示所有的图书信息。点击删除时可以删除响应的书籍。点击修改时将相应的图书信息展示到表单中,用户可以书籍名称进行修改(编号无法修改),点击提交即可完成修改。
书写页面结构是第一个步骤,根据上图的静态页面书写的页面结构示例如下:
<body>
<div class="container">
<div class="header">
<h2>图书管理h2>
<div class="form">
<label for="bookId"> 编号:label>
<input type="text" id="bookId">
<label for="bookName"> 名称:label>
<input type="text" id="bookName">
<button>提交button>
div>
div>
<table>
<thead>
<tr>
<th>编号th>
<th>名称th>
<th>时间th>
<th>操作th>
tr>
thead>
<tbody>
<tr>
<td>1td>
<td>三国演义td>
<td>2021-04-28td>
<td><a href="#">修改a> | <a href="#">删除a>td>
tr>
tbody>
table>
div>
body>
样式的话能看就行,纯粹只是为了练习 Vue 基础,没有必要那么严格,所以下面的示例代码可以略过(不同的人可以不一样的实现)。
<style>
.container {
width: 530px;
height: 700px;
margin: auto;
/* background-color: yellow; */
text-align: center;
}
.form {
padding: 5px 0 10px;
border-bottom: 1px solid black;
background-color: rgb(243, 220, 171);
}
table {
width: 100%;
border-collapse: collapse;
line-height: 35px;
}
thead {
background-color: rgb(243, 220, 171);
}
tbody {
border-left: 1px dashed rgb(243, 220, 171);
border-top: 1px dashed rgb(243, 220, 171);
}
td {
height: 35px;
border-right: 1px dashed rgb(243, 220, 171);
border-bottom: 1px dashed rgb(243, 220, 171);
}
a {
text-decoration: none;
color: #333;
}
a:hover {
color: red;
}
/* button {
height: 22px;
line-height: 22px;
} */
button:hover {
cursor: pointer;
}
input {
outline-style: none;
}
style>
交互功能包含图书添加功能,图书编辑功能、图书删除功能和图书的总数统计功能。其中图书的添加和编辑功能均在表单内操作图书信息。其中编辑功能需要在表单中展示图书信息,并且图书 id 信息无法修改(其实就是只能修改图书名称)。
1、实现图书例表的展示功能,利用 Vue 生命周期中执行的钩子函数 mounted() 模拟从后端获取数据并展示至页面。
2、在展示数据时对于时间数据需要使用过滤器 dataFormat 将原始数据转换为特定的格式(在次需要注意过滤器的使用语法)。
3、提交按钮添加点击事件,但是由于后面编辑功能同时用到该按钮所以需要设置一个 flag 标记执行的是编辑功能(true)还是添加功能(false)。
4、为验证表单信息是否完整和图书名称是否存在可以设置两个监听 id 和 name 的侦听器(判断图书 id 是否已存在放在事件处理函数内的添加逻辑内处理,因为图书编辑功能 id 无法编辑,即 id 没有必要在侦听器内判断是否存在。)。
5、当填写的数据信息完整时,提交按钮不再禁用。可以进行提交,提交后验证图书名称或者 id 是否已存在。如果不存在则进行添加操作,反之进行用户提示和终止操作。
6、最后添加一个自定义指令用于表单自动获取焦点。至此整个添加功能已经全部实现。
1、为删除按钮添加点击事件,因为是 a 元素所以需要使用事件修饰符阻止默认的跳转行为,绑定 modify 事件处理函数。
2、将用于区分提交按钮的添加和修改功能的 flag 标记值改为 true(修改功能,id 表单不可编辑),并将相应的图书 id 和 name 数据填充至表单。
3、在提交按钮的点击事件处理程序中实现图书编辑逻辑,但是这里需要深刻理解数据响应式,特别是数组和对象引用类型数据的响应式特点。
4、最后还需要注意的是,书籍名称可以不修改直接提交。但是修改后的名称不能和其他的书籍名称重复(重名)。最后恢复表单状态,即完成整个编辑功能。
1、删除功能相对比较简单,为删除按钮绑定点击事件并向事件处理函数传递该书籍 id 。
2、可以通过书籍 id 在 books(保存书籍数据的集合)使用 findIndex() 首先查找出该对象在数组中的索引,然后使用 splice() 方法删除数组中对应索引的元素。
3、还可以使用 filter() 方法筛选出新数组,然后将新数组赋值给 books 。
在此使用的是 Vue 中计算属性的方式实现。设置一个计算属性 total 用于统计书籍总数。这里主要是巩固计算属性的相关知识,需要理解其和方法的区别。
<style>
[v-cloak] {
display: none;
}
style>
<body>
<div class="container" id="app">
<div class="header">
<h2>图书管理h2>
<div class="form">
<label for="bookId"> 编号:label>
<input type="text" id="bookId" v-model.trim="id" v-focus :disabled="flag">
<label for="bookName"> 名称:label>
<input type="text" id="bookName" v-model.trim="name">
<button @click="submit" :disabled='!isFull'>提交button>
div>
<div class="total">
<span>图书总数: span>
<span>{
{total}}span>
<button @click="booksSort(-1)">降序button>
<button @click="booksSort(1)">升序button>
div>
div>
<table>
<thead>
<tr>
<th>编号th>
<th>名称th>
<th>时间th>
<th>操作th>
tr>
thead>
<tbody>
<tr :key="item.id" v-for="item in books" v-cloak>
<td>{
{item.id}}td>
<td>{
{item.name}}td>
<td>
<a href="#" @click.prevent="modify(item)">修改a>
<span>|span>
<a href="#" @click.prevent="del(item.id)">删除a>
td>
tr>
tbody>
table>
div>
body>
<script src="js/vue.js">script>
<script>
var vm = new Vue({
el: '#app',
data: {
id: '',
name: '',
flag: false,
isExist: false,
isFull: false,
books: []
},
methods: {
submit: function() {
if (this.flag) {
var index = this.books.findIndex((item) => {
return this.id == item.id;
});
// console.log(index);
// 防止修改名称为其他书籍名称
if (this.isExist && this.books[index].name != this.name) {
return alert('书籍重名,无法修改');
}
// 要深刻理解使用数组索引的方式更新数组数据不是响应式的问题
this.books[index].name = this.name;
this.id = '';
this.name = '';
this.flag = false;
} else {
// 图书添加功能
if (this.isExist) {
return alert('图书已存在不能重复添加');
}
// 这里需要使用箭头函数否则里面的this指向的就不再是vm
var flag = this.books.some((item) => {
// console.log(this);
return item.id == this.id;
})
if (flag) {
return alert('该图书 id 已被占用请重新编辑');
}
var book = {
};
book.id = this.id;
book.name = this.name;
book.date = +new Date();
this.books.push(book);
this.id = '';
this.name = ''
}
},
modify: function(item) {
this.flag = true;
this.id = item.id;
this.name = item.name
},
del: function(id) {
// 删除图书
// 根据id从数组中查找元素的索引
// var index = this.books.findIndex(function(item){
// return item.id == id;
// });
// 根据索引删除数组元素
// this.books.splice(index, 1);
// -------------------------
// 方法二:通过filter方法进行删除
this.books = this.books.filter(function(item) {
return item.id != id;
});
},
booksSort: function(flag) {
this.books = this.books.sort((a, b) => {
if (flag == -1) return b.id - a.id;
if (flag == 1) return a.id - b.id;
})
}
},
// 监听name和id的侦听器
watch: {
name: function(val) {
// 数据完整时将isFull设置为true,默认为false
this.isFull = this.id && this.name;
// 如果书籍名称已经存在则将isExist设置为true
this.isExist = this.books.some(function(item) {
return item.name == val;
});
},
id: function(val) {
this.isFull = this.id && this.name;
}
},
mounted: function() {
this.books = [{
id: '1',
name: '三国演义',
date: 1619776363317
}]
},
directives: {
focus: {
// 指令定义
inserted: function(el) {
el.focus();
}
}
},
filters: {
// 用于格式化时间数据的过滤器
dateFormat: function(date, format) {
if (typeof date === "string") {
var mts = date.match(/(\/Date\((\d+)\)\/)/);
if (mts && mts.length >= 3) {
date = parseInt(mts[2]);
}
}
date = new Date(date);
if (!date || date.toUTCString() == "Invalid Date") {
return "";
}
var map = {
"M": date.getMonth() + 1, //月份
"d": date.getDate(), //日
"h": date.getHours(), //小时
"m": date.getMinutes(), //分
"s": date.getSeconds(), //秒
"q": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
format = format.toString().replace(/([yMdhmsqS])+/g, function(all, t) {
var v = map[t];
if (v !== undefined) {
if (all.length > 1) {
v = '0' + v;
v = v.substr(v.length - 2);
}
return v;
} else if (t === 'y') {
return (date.getFullYear() + '').substr(4 - all.length);
}
return all;
});
return format;
}
},
computed: {
total: function() {
return this.books.length ? this.books.length : '0';
}
}
})
script>
在实现上述功能后,发现了一个不足的地方。书籍的展示顺序完全与添加时的顺序有关,为什么不能按照书籍 id 的大小来显示呢?常用的开发场景是有一个按钮可以控制排序方式,可以给按钮添加事件的形式添加排序功能,但是在添加新的书籍后新增的书籍并未按照该规则进行排序展示。因此我将新增的书籍使用 unshift 方法将其展示在最前面,这样可能比较符合用户需求。
按照上述的思路将各部分功能实现,当然对于排序功能的实现代码我就不去贴代码了。现在展示各部分功能实际的效果。
1、添加功能效果演示:书籍信息不全时提交按钮禁用,书籍 id 或名称已存在还试图添加书籍时终止操作并提示,所有信息符合时添加成功,将添加的书籍信息在第一项展示。注意此时的图书总数和升降序按钮也可以正常使用。
2、编辑功能:点击修改按钮自动在表单展示书籍信息,id 表单项禁用,当输入的书籍名称为已有名称且不是其原有名称时终止修改操作并提示。但是注意,如果是原名称则可以提交修改操作(这里没有演示,所以特别说明了一下),因为客户可能误点了修改而不想做出修改。
3、删除功能:点击删除删除对应的书籍,不妥之处就是直接删除没有给用户确定操作的交互效果,会出现误删。因此可以添加一个 confirm() 进行询问。
所有的需求都已经实现,某些需求是为了能够覆盖所学知识而增加的或者刻意使用所学知识去实现的。整个练习是本人在看完教程后,为避免眼高手低而自己去从零开始实现的,并对其进行了小小的改进。感觉收获还是有的。下一站 Vue 组件化开发小练习,冲~~~~~