前端【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">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<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>

 

posted @ 2024-04-07 15:23  为你编程  阅读(20)  评论(0编辑  收藏  举报