前端【VUE】03-vue【computed 计算属性】【watch 侦听器】
一、computed 计算属性
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <style> 9 table { 10 border: 1px solid #000; 11 text-align: center; 12 width: 240px; 13 } 14 th,td { 15 border: 1px solid #000; 16 } 17 h3 { 18 position: relative; 19 } 20 </style> 21 </head> 22 <body> 23 24 <div id="app"> 25 <h3>小黑的礼物清单</h3> 26 <table> 27 <tr> 28 <th>名字</th> 29 <th>数量</th> 30 </tr> 31 <tr v-for="(item, index) in list" :key="item.id"> 32 <td>{{ item.name }}</td> 33 <td>{{ item.num }}个</td> 34 </tr> 35 </table> 36 37 <!-- 目标:统计求和,求得礼物总数 --> 38 <p>礼物总数:{{ totalCount }} 个</p> 39 </div> 40 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 41 <script> 42 const app = new Vue({ 43 el: '#app', 44 data: { 45 // 现有的数据 46 list: [ 47 { id: 1, name: '篮球', num: 1 }, 48 { id: 2, name: '玩具', num: 2 }, 49 { id: 3, name: '铅笔', num: 5 }, 50 ] 51 }, 52 computed: { 53 totalCount () { 54 // 基于现有的数据,编写求值逻辑 55 // 计算属性函数内部,可以直接通过 this 访问到 app 实例 56 // console.log(this.list) 57 58 // 需求:对 this.list 数组里面的 num 进行求和 → reduce 59 let total = this.list.reduce((sum, item) => sum + item.num, 0) 60 return total 61 } 62 } 63 }) 64 </script> 65 </body> 66 </html>
computed 计算属性 vs methods 方法
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <style> 9 table { 10 border: 1px solid #000; 11 text-align: center; 12 width: 300px; 13 } 14 th,td { 15 border: 1px solid #000; 16 } 17 h3 { 18 position: relative; 19 } 20 span { 21 position: absolute; 22 left: 145px; 23 top: -4px; 24 width: 16px; 25 height: 16px; 26 color: white; 27 font-size: 12px; 28 text-align: center; 29 border-radius: 50%; 30 background-color: #e63f32; 31 } 32 </style> 33 </head> 34 <body> 35 36 <div id="app"> 37 <!-- methods中的函数调用多次,会执行多次函数 --> 38 <h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3> 39 <h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3> 40 <h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3> 41 <h3>小黑的礼物清单🛒<span>{{ totalCountFn() }}</span></h3> 42 <table> 43 <tr> 44 <th>名字</th> 45 <th>数量</th> 46 </tr> 47 <tr v-for="(item, index) in list" :key="item.id"> 48 <td>{{ item.name }}</td> 49 <td>{{ item.num }}个</td> 50 </tr> 51 </table> 52 <!-- 计算属性调用多次,只会执行一次函数,其余调用都会直接从内存读取结果 --> 53 <p>礼物总数:{{ totalCountFn() }} 个</p> 54 </div> 55 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 56 <script> 57 const app = new Vue({ 58 el: '#app', 59 data: { 60 // 现有的数据 61 list: [ 62 { id: 1, name: '篮球', num: 3 }, 63 { id: 2, name: '玩具', num: 2 }, 64 { id: 3, name: '铅笔', num: 5 }, 65 ] 66 }, 67 68 methods: { 69 totalCountFn () { 70 console.log('methods方法执行了') 71 let total = this.list.reduce((sum, item) => sum + item.num, 0) 72 return total 73 } 74 }, 75 computed: { 76 // 计算属性:有缓存的,一旦计算出来结果,就会立刻缓存 77 // 下一次读取 → 直接读缓存就行 → 性能特别高 78 // totalCount () { 79 // console.log('计算属性执行了') 80 // let total = this.list.reduce((sum, item) => sum + item.num, 0) 81 // return total 82 // } 83 } 84 }) 85 </script> 86 </body> 87 </html>
计算属性完整写法
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <style> 9 input { 10 width: 30px; 11 } 12 </style> 13 </head> 14 <body> 15 16 <div id="app"> 17 姓:<input type="text" v-model="firstName"> + 18 名:<input type="text" v-model="lastName"> = 19 <span>{{ fullName }}</span><br><br> 20 21 <button @click="changeName">改名卡</button> 22 </div> 23 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 24 <script> 25 const app = new Vue({ 26 el: '#app', 27 data: { 28 firstName: '刘', 29 lastName: '备', 30 }, 31 methods: { 32 changeName () { 33 this.fullName = '黄忠' 34 } 35 }, 36 computed: { 37 // *** 计算属性的简写(函数形式): → 获取,没有配置设置的逻辑 38 // fullName () { 39 // return this.firstName + this.lastName 40 // } 41 42 // *** 计算属性的完整写法(对象形式): → 获取函数 + 设置函数 43 fullName: { 44 // (1) 当fullName计算属性,被获取求值时,执行get(有缓存,优先读缓存) 45 // 会将返回值作为,求值的结果 46 get () { 47 return this.firstName + this.lastName 48 }, 49 // (2) 当fullName计算属性,被修改赋值时,执行set 50 // 修改的值,传递给set方法的形参 51 set (value) { 52 // console.log(value.slice(0, 1)) 53 // console.log(value.slice(1)) 54 this.firstName = value.slice(0, 1) 55 this.lastName = value.slice(1) 56 } 57 } 58 } 59 }) 60 </script> 61 </body> 62 </html>
成绩案例
样式 index.less文件内容
1 .score-case { 2 width: 1000px; 3 margin: 50px auto; 4 display: flex; 5 .table { 6 flex: 4; 7 table { 8 width: 100%; 9 border-spacing: 0; 10 border-top: 1px solid #ccc; 11 border-left: 1px solid #ccc; 12 th { 13 background: #f5f5f5; 14 } 15 tr:hover td { 16 background: #f5f5f5; 17 } 18 td, 19 th { 20 border-bottom: 1px solid #ccc; 21 border-right: 1px solid #ccc; 22 text-align: center; 23 padding: 10px; 24 &.red { 25 color: red; 26 } 27 } 28 } 29 .none { 30 height: 100px; 31 line-height: 100px; 32 color: #999; 33 } 34 } 35 .form { 36 flex: 1; 37 padding: 20px; 38 .form-item { 39 display: flex; 40 margin-bottom: 20px; 41 align-items: center; 42 } 43 .form-item .label { 44 width: 60px; 45 text-align: right; 46 font-size: 14px; 47 } 48 .form-item .input { 49 flex: 1; 50 } 51 .form-item input, 52 .form-item select { 53 appearance: none; 54 outline: none; 55 border: 1px solid #ccc; 56 width: 200px; 57 height: 40px; 58 box-sizing: border-box; 59 padding: 10px; 60 color: #666; 61 } 62 .form-item input::placeholder { 63 color: #666; 64 } 65 .form-item .cancel, 66 .form-item .submit { 67 appearance: none; 68 outline: none; 69 border: 1px solid #ccc; 70 border-radius: 4px; 71 padding: 4px 10px; 72 margin-right: 10px; 73 font-size: 12px; 74 background: #ccc; 75 } 76 .form-item .submit { 77 border-color: #069; 78 background: #069; 79 color: #fff; 80 } 81 } 82 }
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <link rel="stylesheet" href="./styles/index.css" /> 8 <title>Document</title> 9 </head> 10 <body> 11 <div id="app" class="score-case"> 12 <div class="table"> 13 <table> 14 <thead> 15 <tr> 16 <th>编号</th> 17 <th>科目</th> 18 <th>成绩</th> 19 <th>操作</th> 20 </tr> 21 </thead> 22 23 <tbody v-if="list.length > 0"> 24 <tr v-for="(item, index) in list" :key="item.id"> 25 <td>{{ index + 1 }}</td> 26 <td>{{ item.subject }}</td> 27 <!-- 需求:不及格的标红, < 60 分, 加上 red 类 --> 28 <td :class="{ red: item.score < 60 }">{{ item.score }}</td> 29 <td><a @click.prevent="del(item.id)" href="http://www.baidu.com">删除</a></td> 30 </tr> 31 </tbody> 32 33 <tbody v-else> 34 <tr> 35 <td colspan="5"> 36 <span class="none">暂无数据</span> 37 </td> 38 </tr> 39 </tbody> 40 41 <tfoot> 42 <tr> 43 <td colspan="5"> 44 <span>总分:{{ totalScore }}</span> 45 <span style="margin-left: 50px">平均分:{{ averageScore }}</span> 46 </td> 47 </tr> 48 </tfoot> 49 </table> 50 </div> 51 <div class="form"> 52 <div class="form-item"> 53 <div class="label">科目:</div> 54 <div class="input"> 55 <input 56 type="text" 57 placeholder="请输入科目" 58 v-model.trim="subject" 59 /> 60 </div> 61 </div> 62 <div class="form-item"> 63 <div class="label">分数:</div> 64 <div class="input"> 65 <input 66 type="text" 67 placeholder="请输入分数" 68 v-model.number="score" 69 /> 70 </div> 71 </div> 72 <div class="form-item"> 73 <div class="label"></div> 74 <div class="input"> 75 <button @click="add" class="submit" >添加</button> 76 </div> 77 </div> 78 </div> 79 </div> 80 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 81 82 <script> 83 const app = new Vue({ 84 el: '#app', 85 data: { 86 list: [ 87 { id: 1, subject: '语文', score: 62 }, 88 { id: 7, subject: '数学', score: 89 }, 89 { id: 12, subject: '英语', score: 70 }, 90 ], 91 subject: '', 92 score: '' 93 }, 94 computed: { 95 totalScore() { 96 return this.list.reduce((sum, item) => sum + item.score, 0) 97 }, 98 averageScore () { 99 if (this.list.length === 0) { 100 return 0 101 } 102 return (this.totalScore / this.list.length).toFixed(2) 103 } 104 }, 105 methods: { 106 del (id) { 107 // console.log(id) 108 this.list = this.list.filter(item => item.id !== id) 109 }, 110 add () { 111 if (!this.subject) { 112 alert('请输入科目') 113 return 114 } 115 if (typeof this.score !== 'number') { 116 alert('请输入正确的成绩') 117 return 118 } 119 this.list.unshift({ 120 id: +new Date(), 121 subject: this.subject, 122 score: this.score 123 }) 124 125 this.subject = '' 126 this.score = '' 127 } 128 } 129 }) 130 </script> 131 </body> 132 </html>
二、watch 侦听器
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>Document</title> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 font-size: 18px; 14 } 15 #app { 16 padding: 10px 20px; 17 } 18 .query { 19 margin: 10px 0; 20 } 21 .box { 22 display: flex; 23 } 24 textarea { 25 width: 300px; 26 height: 160px; 27 font-size: 18px; 28 border: 1px solid #dedede; 29 outline: none; 30 resize: none; 31 padding: 10px; 32 } 33 textarea:hover { 34 border: 1px solid #1589f5; 35 } 36 .transbox { 37 width: 300px; 38 height: 160px; 39 background-color: #f0f0f0; 40 padding: 10px; 41 border: none; 42 } 43 .tip-box { 44 width: 300px; 45 height: 25px; 46 line-height: 25px; 47 display: flex; 48 } 49 .tip-box span { 50 flex: 1; 51 text-align: center; 52 } 53 .query span { 54 font-size: 18px; 55 } 56 57 .input-wrap { 58 position: relative; 59 } 60 .input-wrap span { 61 position: absolute; 62 right: 15px; 63 bottom: 15px; 64 font-size: 12px; 65 } 66 .input-wrap i { 67 font-size: 20px; 68 font-style: normal; 69 } 70 </style> 71 </head> 72 <body> 73 <div id="app"> 74 <!-- 条件选择框 --> 75 <div class="query"> 76 <span>翻译成的语言:</span> 77 <select> 78 <option value="italy">意大利</option> 79 <option value="english">英语</option> 80 <option value="german">德语</option> 81 </select> 82 </div> 83 84 <!-- 翻译框 --> 85 <div class="box"> 86 <div class="input-wrap"> 87 <textarea v-model="obj.words"></textarea> 88 <span><i>⌨️</i>文档翻译</span> 89 </div> 90 <div class="output-wrap"> 91 <div class="transbox">mela</div> 92 </div> 93 </div> 94 </div> 95 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 96 <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> 97 <script> 98 // 接口地址:https://applet-base-api-t.itheima.net/api/translate 99 // 请求方式:get 100 // 请求参数: 101 // (1)words:需要被翻译的文本(必传) 102 // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 103 // ----------------------------------------------- 104 105 const app = new Vue({ 106 el: '#app', 107 data: { 108 // 基本类型属性 109 words: '', 110 // 对象类型属性 111 obj: { 112 words: '' 113 } 114 }, 115 // 具体讲解:(1) watch语法 (2) 具体业务实现 116 watch: { // 通过函数的方式定义监听 117 // 监听基本类型属性, 根据属性名定义监听函数, 当属性变化, 会调用对应的监听函数, 将修改前和修改后的值作为参数传递到函数中 118 words (newValue,oldValue) { 119 console.log('变化了', newValue) 120 }, 121 // 监听对象类型属性中的某个值, 采用 对象.属性名 的方式定义监听函数名, 但是需要用引号引起来 122 'obj.words' (newValue,oldValue) { 123 console.log('变化了', newValue) 124 }, 125 // 监听对象类型属性, 直接根据对象的属性名定义监听函数 126 obj (newValue,oldValue) { 127 console.log('变化了', newValue) 128 } 129 } 130 }) 131 </script> 132 </body> 133 </html>
调用翻译接口,实现功能
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>Document</title> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 font-size: 18px; 14 } 15 #app { 16 padding: 10px 20px; 17 } 18 .query { 19 margin: 10px 0; 20 } 21 .box { 22 display: flex; 23 } 24 textarea { 25 width: 300px; 26 height: 160px; 27 font-size: 18px; 28 border: 1px solid #dedede; 29 outline: none; 30 resize: none; 31 padding: 10px; 32 } 33 textarea:hover { 34 border: 1px solid #1589f5; 35 } 36 .transbox { 37 width: 300px; 38 height: 160px; 39 background-color: #f0f0f0; 40 padding: 10px; 41 border: none; 42 } 43 .tip-box { 44 width: 300px; 45 height: 25px; 46 line-height: 25px; 47 display: flex; 48 } 49 .tip-box span { 50 flex: 1; 51 text-align: center; 52 } 53 .query span { 54 font-size: 18px; 55 } 56 57 .input-wrap { 58 position: relative; 59 } 60 .input-wrap span { 61 position: absolute; 62 right: 15px; 63 bottom: 15px; 64 font-size: 12px; 65 } 66 .input-wrap i { 67 font-size: 20px; 68 font-style: normal; 69 } 70 </style> 71 </head> 72 <body> 73 <div id="app"> 74 <!-- 条件选择框 --> 75 <div class="query"> 76 <span>翻译成的语言:</span> 77 <select> 78 <option value="italy">意大利</option> 79 <option value="english">英语</option> 80 <option value="german">德语</option> 81 </select> 82 </div> 83 84 <!-- 翻译框 --> 85 <div class="box"> 86 <div class="input-wrap"> 87 <textarea v-model="obj.words"></textarea> 88 <span><i>⌨️</i>文档翻译</span> 89 </div> 90 <div class="output-wrap"> 91 <div class="transbox">{{ result }}</div> 92 </div> 93 </div> 94 </div> 95 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 96 <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> 97 <script> 98 // 接口地址:https://applet-base-api-t.itheima.net/api/translate 99 // 请求方式:get 100 // 请求参数: 101 // (1)words:需要被翻译的文本(必传) 102 // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 103 // ----------------------------------------------- 104 105 const app = new Vue({ 106 el: '#app', 107 data: { 108 // words: '' 109 obj: { 110 words: '' 111 }, 112 result: '', // 翻译结果 113 // timer: null // 延时器id 114 }, 115 // 具体讲解:(1) watch语法 (2) 具体业务实现 116 watch: { 117 'obj.words' (newValue) { 118 // 防抖: 延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行 119 clearTimeout(this.timer) 120 this.timer = setTimeout(async () => { 121 const res = await axios({ 122 url: 'https://applet-base-api-t.itheima.net/api/translate', 123 params: { 124 words: newValue 125 } 126 }) 127 this.result = res.data.data 128 console.log(res.data.data) 129 }, 300) 130 } 131 } 132 }) 133 </script> 134 </body> 135 </html>
侦听器完整写法
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>Document</title> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 font-size: 18px; 14 } 15 #app { 16 padding: 10px 20px; 17 } 18 .query { 19 margin: 10px 0; 20 } 21 .box { 22 display: flex; 23 } 24 textarea { 25 width: 300px; 26 height: 160px; 27 font-size: 18px; 28 border: 1px solid #dedede; 29 outline: none; 30 resize: none; 31 padding: 10px; 32 } 33 textarea:hover { 34 border: 1px solid #1589f5; 35 } 36 .transbox { 37 width: 300px; 38 height: 160px; 39 background-color: #f0f0f0; 40 padding: 10px; 41 border: none; 42 } 43 .tip-box { 44 width: 300px; 45 height: 25px; 46 line-height: 25px; 47 display: flex; 48 } 49 .tip-box span { 50 flex: 1; 51 text-align: center; 52 } 53 .query span { 54 font-size: 18px; 55 } 56 57 .input-wrap { 58 position: relative; 59 } 60 .input-wrap span { 61 position: absolute; 62 right: 15px; 63 bottom: 15px; 64 font-size: 12px; 65 } 66 .input-wrap i { 67 font-size: 20px; 68 font-style: normal; 69 } 70 </style> 71 </head> 72 <body> 73 <div id="app"> 74 <!-- 条件选择框 --> 75 <div class="query"> 76 <span>翻译成的语言:</span> 77 <select v-model="obj.lang"> 78 <option value="italy">意大利</option> 79 <option value="english">英语</option> 80 <option value="german">德语</option> 81 </select> 82 </div> 83 84 <!-- 翻译框 --> 85 <div class="box"> 86 <div class="input-wrap"> 87 <textarea v-model="obj.words"></textarea> 88 <span><i>⌨️</i>文档翻译</span> 89 </div> 90 <div class="output-wrap"> 91 <div class="transbox">{{ result }}</div> 92 </div> 93 </div> 94 </div> 95 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 96 <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> 97 <script> 98 // 需求:输入内容,修改语言,都实时翻译 99 // 接口地址:https://applet-base-api-t.itheima.net/api/translate 100 // 请求方式:get 101 // 请求参数: 102 // (1)words:需要被翻译的文本(必传) 103 // (2)lang: 需要被翻译成的语言(可选)默认值-意大利 104 // ----------------------------------------------- 105 106 const app = new Vue({ 107 el: '#app', 108 data: { 109 obj: { 110 words: '小黑', 111 lang: 'italy' 112 }, 113 result: '', // 翻译结果 114 }, 115 watch: { 116 // 通过对象的形式定义侦听 117 obj: { 118 deep: true, // 是否深度监视, 为true则会监听对象中嵌套关系的属性 119 immediate: true, // 是否立刻执行, 一进入页面handler就立刻执行一次 120 handler (newValue) { // 监听函数 121 clearTimeout(this.timer) 122 this.timer = setTimeout(async () => { 123 const res = await axios({ 124 url: 'https://applet-base-api-t.itheima.net/api/translate', 125 params: newValue 126 }) 127 this.result = res.data.data 128 console.log(res.data.data) 129 }, 300) 130 } 131 } 132 } 133 }) 134 </script> 135 </body> 136 </html>
购物车案例
./css/inputnumber.css
1 .my-input-number { 2 position: relative; 3 display: inline-block; 4 width: 140px; 5 line-height: 38px; 6 } 7 .my-input-number span { 8 -moz-user-select: none; 9 -webkit-user-select: none; 10 -ms-user-select: none; 11 } 12 .my-input-number .my-input { 13 display: block; 14 position: relative; 15 font-size: 14px; 16 width: 100%; 17 } 18 .my-input-number .my-input__inner { 19 -webkit-appearance: none; 20 background-color: #fff; 21 background-image: none; 22 border-radius: 4px; 23 border: 1px solid #dcdfe6; 24 box-sizing: border-box; 25 color: #606266; 26 display: inline-block; 27 font-size: inherit; 28 height: 40px; 29 line-height: 40px; 30 outline: none; 31 padding: 0 15px; 32 transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); 33 width: 100%; 34 padding-left: 50px; 35 padding-right: 50px; 36 text-align: center; 37 } 38 .my-input-number .my-input-number__decrease, 39 .my-input-number .my-input-number__increase { 40 position: absolute; 41 z-index: 1; 42 top: 1px; 43 width: 40px; 44 height: auto; 45 text-align: center; 46 background: #f5f7fa; 47 color: #606266; 48 cursor: pointer; 49 font-size: 13px; 50 } 51 .my-input-number .my-input-number__decrease { 52 left: 1px; 53 border-radius: 4px 0 0 4px; 54 border-right: 1px solid #dcdfe6; 55 } 56 .my-input-number .my-input-number__increase { 57 right: 1px; 58 border-radius: 0 4px 4px 0; 59 border-left: 1px solid #dcdfe6; 60 } 61 .my-input-number .my-input-number__decrease.is-disabled, 62 .my-input-number .my-input-number__increase.is-disabled { 63 color: #c0c4cc; 64 cursor: not-allowed; 65 }
./css/index.css
1 .app-container { 2 padding-bottom: 300px; 3 width: 800px; 4 margin: 0 auto; 5 } 6 @media screen and (max-width: 800px) { 7 .app-container { 8 width: 600px; 9 } 10 } 11 .app-container .banner-box { 12 border-radius: 20px; 13 overflow: hidden; 14 margin-bottom: 10px; 15 } 16 .app-container .banner-box img { 17 width: 100%; 18 } 19 .app-container .nav-box { 20 background: #ddedec; 21 height: 60px; 22 border-radius: 10px; 23 padding-left: 20px; 24 display: flex; 25 align-items: center; 26 } 27 .app-container .nav-box .my-nav { 28 display: inline-block; 29 background: #5fca71; 30 border-radius: 5px; 31 width: 90px; 32 height: 35px; 33 color: white; 34 text-align: center; 35 line-height: 35px; 36 margin-right: 10px; 37 } 38 39 .breadcrumb { 40 font-size: 16px; 41 color: gray; 42 } 43 .table { 44 width: 100%; 45 text-align: left; 46 border-radius: 2px 2px 0 0; 47 border-collapse: separate; 48 border-spacing: 0; 49 } 50 .th { 51 color: rgba(0, 0, 0, 0.85); 52 font-weight: 500; 53 text-align: left; 54 background: #fafafa; 55 border-bottom: 1px solid #f0f0f0; 56 transition: background 0.3s ease; 57 } 58 .th.num-th { 59 flex: 1.5; 60 } 61 .th { 62 text-align: center; 63 } 64 .th:nth-child(4), 65 .th:nth-child(5), 66 .th:nth-child(6), 67 .th:nth-child(7) { 68 text-align: center; 69 } 70 .th.th-pic { 71 flex: 1.3; 72 } 73 .th:nth-child(6) { 74 flex: 1.3; 75 } 76 77 .th, 78 .td { 79 position: relative; 80 padding: 16px 16px; 81 overflow-wrap: break-word; 82 flex: 1; 83 } 84 .pick-td { 85 font-size: 14px; 86 } 87 .main, 88 .empty { 89 border: 1px solid #f0f0f0; 90 margin-top: 10px; 91 } 92 .tr { 93 display: flex; 94 cursor: pointer; 95 border-bottom: 1px solid #ebeef5; 96 } 97 .tr.active { 98 background-color: #f5f7fa; 99 } 100 .td { 101 display: flex; 102 justify-content: center; 103 align-items: center; 104 } 105 106 .table img { 107 width: 100px; 108 height: 100px; 109 } 110 111 button { 112 outline: 0; 113 box-shadow: none; 114 color: #fff; 115 background: #d9363e; 116 border-color: #d9363e; 117 color: #fff; 118 background: #d9363e; 119 border-color: #d9363e; 120 line-height: 1.5715; 121 position: relative; 122 display: inline-block; 123 font-weight: 400; 124 white-space: nowrap; 125 text-align: center; 126 background-image: none; 127 border: 1px solid transparent; 128 box-shadow: 0 2px 0 rgb(0 0 0 / 2%); 129 cursor: pointer; 130 transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); 131 -webkit-user-select: none; 132 -moz-user-select: none; 133 -ms-user-select: none; 134 user-select: none; 135 touch-action: manipulation; 136 height: 32px; 137 padding: 4px 15px; 138 font-size: 14px; 139 border-radius: 2px; 140 } 141 button.pay { 142 background-color: #3f85ed; 143 margin-left: 20px; 144 } 145 146 .bottom { 147 height: 60px; 148 display: flex; 149 align-items: center; 150 justify-content: space-between; 151 padding-right: 20px; 152 border: 1px solid #f0f0f0; 153 border-top: none; 154 padding-left: 20px; 155 } 156 .right-box { 157 display: flex; 158 align-items: center; 159 } 160 .check-all { 161 cursor: pointer; 162 } 163 .price { 164 color: hotpink; 165 font-size: 30px; 166 font-weight: 700; 167 } 168 .price-box { 169 display: flex; 170 align-items: center; 171 } 172 .empty { 173 padding: 20px; 174 text-align: center; 175 font-size: 30px; 176 color: #909399; 177 } 178 .my-input-number { 179 display: flex; 180 } 181 .my-input-number button { 182 height: 40px; 183 color: #333; 184 border: 1px solid #dcdfe6; 185 background-color: #f5f7fa; 186 } 187 .my-input-number button:disabled { 188 cursor: not-allowed!important; 189 } 190 .my-input-number .my-input__inner { 191 height: 40px; 192 width: 50px; 193 padding: 0; 194 border: none; 195 border-top: 1px solid #dcdfe6; 196 border-bottom: 1px solid #dcdfe6; 197 }
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <link rel="stylesheet" href="./css/inputnumber.css" /> 8 <link rel="stylesheet" href="./css/index.css" /> 9 <title>购物车</title> 10 </head> 11 <body> 12 <div class="app-container" id="app"> 13 <!-- 顶部banner --> 14 <div class="banner-box"><img src="http://autumnfish.cn/static/fruit.jpg" alt="" /></div> 15 <!-- 面包屑 --> 16 <div class="breadcrumb"> 17 <span>🏠</span> 18 / 19 <span>购物车</span> 20 </div> 21 <!-- 购物车主体 --> 22 <div class="main" v-if="fruitList.length > 0"> 23 <div class="table"> 24 <!-- 头部 --> 25 <div class="thead"> 26 <div class="tr"> 27 <div class="th">选中</div> 28 <div class="th th-pic">图片</div> 29 <div class="th">单价</div> 30 <div class="th num-th">个数</div> 31 <div class="th">小计</div> 32 <div class="th">操作</div> 33 </div> 34 </div> 35 <!-- 身体 --> 36 <div class="tbody"> 37 <div v-for="(item, index) in fruitList" :key="item.id" class="tr" :class="{ active: item.isChecked }"> 38 <div class="td"><input type="checkbox" v-model="item.isChecked" /></div> 39 <div class="td"><img :src="item.icon" alt="" /></div> 40 <div class="td">{{ item.price }}</div> 41 <div class="td"> 42 <div class="my-input-number"> 43 <button :disabled="item.num <= 1" class="decrease" @click="sub(item.id)"> - </button> 44 <span class="my-input__inner">{{ item.num }}</span> 45 <button class="increase" @click="add(item.id)"> + </button> 46 </div> 47 </div> 48 <div class="td">{{ item.num * item.price }}</div> 49 <div class="td"><button @click="del(item.id)">删除</button></div> 50 </div> 51 </div> 52 </div> 53 <!-- 底部 --> 54 <div class="bottom"> 55 <!-- 全选 --> 56 <label class="check-all"> 57 <input type="checkbox" v-model="isAll"/> 58 全选 59 </label> 60 <div class="right-box"> 61 <!-- 所有商品总价 --> 62 <span class="price-box">总价 : ¥ <span class="price">{{ totalPrice }}</span></span> 63 <!-- 结算按钮 --> 64 <button class="pay">结算( {{ totalCount }} )</button> 65 </div> 66 </div> 67 </div> 68 <!-- 空车 --> 69 <div class="empty" v-else>🛒空空如也</div> 70 </div> 71 <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 72 <script> 73 const defaultArr = [ 74 { 75 id: 1, 76 icon: 'http://autumnfish.cn/static/火龙果.png', 77 isChecked: true, 78 num: 2, 79 price: 6, 80 }, 81 { 82 id: 2, 83 icon: 'http://autumnfish.cn/static/荔枝.png', 84 isChecked: false, 85 num: 7, 86 price: 20, 87 }, 88 { 89 id: 3, 90 icon: 'http://autumnfish.cn/static/榴莲.png', 91 isChecked: false, 92 num: 3, 93 price: 40, 94 }, 95 { 96 id: 4, 97 icon: 'http://autumnfish.cn/static/鸭梨.png', 98 isChecked: true, 99 num: 10, 100 price: 3, 101 }, 102 { 103 id: 5, 104 icon: 'http://autumnfish.cn/static/樱桃.png', 105 isChecked: false, 106 num: 20, 107 price: 34, 108 }, 109 ] 110 const app = new Vue({ 111 el: '#app', 112 data: { 113 // 水果列表 114 fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr, 115 }, 116 computed: { 117 // 默认计算属性:只能获取不能设置,要设置需要写完整写法 118 // isAll () { 119 // // 必须所有的小选框都选中,全选按钮才选中 → every 120 // return this.fruitList.every(item => item.isChecked) 121 // } 122 123 // 完整写法 = get + set 124 isAll: { 125 get () { 126 return this.fruitList.every(item => item.isChecked) 127 }, 128 set (value) { 129 // 基于拿到的布尔值,要让所有的小选框 同步状态 130 this.fruitList.forEach(item => item.isChecked = value) 131 } 132 }, 133 // 统计选中的总数 reduce 134 totalCount () { 135 return this.fruitList.reduce((sum, item) => { 136 if (item.isChecked) { 137 // 选中 → 需要累加 138 return sum + item.num 139 } else { 140 // 没选中 → 不需要累加 141 return sum 142 } 143 }, 0) 144 }, 145 // 总计选中的总价 num * price 146 totalPrice () { 147 return this.fruitList.reduce((sum, item) => { 148 if (item.isChecked) { 149 return sum + item.num * item.price 150 } else { 151 return sum 152 } 153 }, 0) 154 } 155 }, 156 methods: { 157 del (id) { 158 this.fruitList = this.fruitList.filter(item => item.id !== id) 159 }, 160 add (id) { 161 // 1. 根据 id 找到数组中的对应项 → find 162 const fruit = this.fruitList.find(item => item.id === id) 163 // 2. 操作 num 数量 164 fruit.num++ 165 }, 166 sub (id) { 167 // 1. 根据 id 找到数组中的对应项 → find 168 const fruit = this.fruitList.find(item => item.id === id) 169 // 2. 操作 num 数量 170 fruit.num-- 171 } 172 }, 173 watch: { 174 fruitList: { 175 deep: true, 176 handler (newValue) { 177 // 需要将变化后的 newValue 存入本地 (转JSON) 178 localStorage.setItem('list', JSON.stringify(newValue)) 179 } 180 } 181 } 182 }) 183 </script> 184 </body> 185 </html>