vue2-计算属性computed,侦听器watch

@keyup.enter:当回车时才触发事件,如果不写【.enter】按下任意键都会触发事件

不写【.enter】代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <input type="text" v-model="enter" @keyup="fn">
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                enter:""
            },
            methods: {
                fn() {
                    console.log(this.enter)
                }
            },
        })
    </script>
</body>
</html>

结果:按下任意键,都会将文本框内容输出到控制台

加上【.enter】代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <input type="text" v-model="enter" @keyup.enter="fn">
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                enter:""
            },
            methods: {
                fn() {
                    console.log(this.enter)
                }
            },
        })
    </script>
</body>
</html>

结果:当回车时,才输出文本框内容到控制台

v-model.trim:去掉表单数据前后空格

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
        <span>.trim</span> <input type="text" v-model.trim="trim1">
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                enter:"",
                trim1:""
            },
            methods: {
                fn() {
                    console.log(this.enter)
                }
            },
        })
    </script>
</body>
</html>

结果:

v-model.number:将表单数据转换类型为数字

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
        <span>.trim</span> <input type="text" v-model.trim="trim1"><br><br>
        <span>.number</span> <input type="text" v-model.number="number">
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                enter:"",
                trim1:"",
                number:""
            },
            methods: {
                fn() {
                    console.log(this.enter)
                }
            },
        })
    </script>
</body>
</html>

结果:

@click.stop:阻止冒泡

子盒子的事件触发时,默认父盒子的事件也会触发

要达到只触发子盒子的效果,加上【.stop】就可以了

@click.prevent:阻止默认行为

默认点击a标签的网址会跳转,如果不想跳转,加上【@click.prevent】就可以了

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .father{
            width: 200px;
            height: 200px;
            background: greenyellow;
            text-align: center;
        }
        .son{
            width: 100px;
            height: 100px;
            background: pink;
            text-align: center;
        }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <span>.enter</span> <input type="text" v-model="enter" @keyup.enter="fn"><br><br>
        <span>.trim</span> <input type="text" v-model.trim="trim1"><br><br>
        <span>.number</span> <input type="text" v-model.number="number"><br><br>
        <span>.stop 阻止冒泡</span><div @click="ffn" class="father">
            <div @click.stop="sfn" class="son">sBox</div>
            fBox
        </div>
        <span>.prevent</span>
        <a @click.prevent href="https://www.baidu.com">阻止默认行为</a>
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                enter:"",
                trim1:"",
                number:""
            },
            methods: {
                fn() {
                    console.log(this.enter)
                },
                ffn() {
                    alert("父方法执行力")
                },
                sfn() {
                    alert("子方法执行力")
                }
            },
        })
    </script>
</body>
</html>

结果:子方法执行后,父方法不会执行;链接点击不会有反应

v-bind对样式class的控制:通过对象或者数组来控制生效的class样式

对象形式::class="{pink: true, big: true}",用键值对决定生效的类名

数组形式::class="['pink', 'big']",写在数组里的类都会生效

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
            border: 3px solid;
            margin-top: 10px;
            font-size: 30px;
        }
        .pink {
            background: pink;
        }
        .big {
            width: 300px;
            height: 300px;
        }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <div id = "app">
        <!-- :class的作用:增强class -->
        <div class="box" :class="{pink: true, big: true}">我爱前端</div>
        <div class="box" :class="['pink', 'big']">我爱前端</div>
    </div>
    <script>
        const app = new Vue({
            el: "#app",
            data: {
                
            }
        })
    </script>
</body>
</html>

结果:

案例:被点击的导航栏显示高亮(利用:class)

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    ul {
      display: flex;
      border-bottom: 2px solid #e01222;
      padding: 0 10px;
    }
    li {
      width: 100px;
      height: 50px;
      line-height: 50px;
      list-style: none;
      text-align: center;
    }
    li a {
      display: block;
      text-decoration: none;
      font-weight: bold;
      color: #333333;
    }
    /* #id和.class中间有空格和无空格的区别
    首先中间有空格的情况:.class1 .class2
    是选择到.class1类下的.class2类子节点,即.class2类的节点要是.class1类子节点
    无空格情况:.class1.class2
    是选择到同时拥有.class1和.class2的节点
    */
    li a.active {
      background-color: #e01222;
      color: #fff;
    }

  </style>
</head>
<body>

  <div id="app">
    <ul>
      <li v-for="(item, index) in list" :key="item.id" @click="activeIndex = index">
        <a :class="{active: index === activeIndex}" href="#">{{ item.name }}</a>
      </li>
    </ul>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        activeIndex: 0, // 记录高亮
        list: [
          { id: 1, name: '京东秒杀' },
          { id: 2, name: '每日特价' },
          { id: 3, name: '品类秒杀' }
        ]
      }
    })
  </script>
</body>
</html>

效果:

v-bind对样式style的控制

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .progress {
      height: 25px;
      width: 400px;
      border-radius: 15px;
      background-color: #272425;
      border: 3px solid #272425;
      box-sizing: border-box;
      margin-bottom: 30px;
    }
    .inner {
      width: 50%;
      height: 20px;
      border-radius: 10px;
      text-align: right;
      position: relative;
      background-color: #409eff;
      background-size: 20px 20px;
      box-sizing: border-box;
      transition: all 1s;
    }
    .inner span {
      position: absolute;
      right: -20px;
      bottom: -25px;
    }
  </style>
</head>
<body>
  <div id="app">
    <!-- 外层盒子底色 (黑色) -->
    <div class="progress">
      <!-- 内层盒子 - 进度(蓝色) -->
      <div class="inner" :style="{width : percent + '%'}">
        <span>{{ percent }}%</span>
      </div>
    </div>
    <button @click="percent = 25">设置25%</button>
    <button @click="percent = 50">设置50%</button>
    <button @click="percent = 75">设置75%</button>
    <button @click="percent = 100">设置100%</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        percent: 30
      }
    })
  </script>
</body>
</html>

效果:点击不同按钮时,改变的百分比percent变量会变成进度条的width值,实现进度条变化

v-model应用于表单元素

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    textarea {
      display: block;
      width: 240px;
      height: 100px;
      margin: 10px 0;
    }
  </style>
</head>
<body>

  <div id="app">
    <h3>前端学习网</h3>

    姓名:
      <input type="text" v-model="username"> 
      <br><br>

    是否单身:
      <input type="checkbox" v-model="isSingle"> 
      <br><br>

    <!-- 
      前置理解:
        1. name:  给单选框加上 name 属性 可以分组 → 同一组互相会互斥
        2. value: 给单选框加上 value 属性,用于提交给后台的数据
      结合 Vue 使用 → v-model
    -->
    性别: 
      <input v-model="sex" type="radio" name="gender" value="1">男
      <input v-model="sex" type="radio" name="gender" value="2">女
      <br><br>

    <!-- 
      前置理解:
        1. option 需要设置 value 值,提交给后台
        2. select 的 value 值,关联了选中的 option 的 value 值
      结合 Vue 使用 → v-model
    -->
    所在城市:
      <select v-model="cityId">
        <option value="101">北京</option>
        <option value="102">上海</option>
        <option value="103">成都</option>
        <option value="104">南京</option>
      </select>
      <br><br>

    自我描述:
      <textarea v-model="desc"></textarea> 

    <button>立即注册</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        username: '',
        isSingle: false,
        sex: "2",
        cityId: '102',
        desc: ""
      }
    })
  </script>
</body>
</html>

效果:对所有表单元素都进行了双向数据绑定

计算属性computed:根据现有数据计算出来结果会同步变化

代码:统计礼物总数

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    table {
      border: 1px solid #000;
      text-align: center;
      width: 240px;
    }
    th,td {
      border: 1px solid #000;
    }
    h3 {
      position: relative;
    }
  </style>
</head>
<body>

  <div id="app">
    <h3>礼物清单</h3>
    <table>
      <tr>
        <th>名字</th>
        <th>数量</th>
      </tr>
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ item.name}}</td>
        <td>{{ item.num }}个</td>
      </tr>
    </table>

    <!-- 目标:统计求和,求得礼物总数 -->
    <p>礼物总数:{{ totalCount}} 个</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        // 现有的数据
        list: [
          { id: 1, name: '篮球', num: 1 },
          { id: 2, name: '玩具', num: 2 },
          { id: 3, name: '铅笔', num: 5 },
        ]
      },
      computed: {
        totalCount () {
          /* 使用reduce函数求和,sum叫做阶段性结果,item为list里的每一项
          由于传入了初始值0,所以开始时sum的值为0,item的值为数组第一项,
          相加之后返回值为1作为下一轮回调的sum值,然后再继续与下一个数组项相加,
          以此类推,直至完成所有数组项的和并返回。 */
          let total = this.list.reduce((sum, item) => sum + item.num, 0)
          return total
        }
      }
    })
  </script>
</body>
</html>

结果:

computed和methods的区别

computed用于计算出结果,使用时不加括号,比如{{ totalCount }},totalCount是属性名

methods用于处理业务逻辑,比如修改向数组中添加元素,使用方法:

①@click="add"

②{{ totalCountFn() }},totalCountFn是方法名

computed完整写法:获取(get)+设置(set)

代码:当修改计算属性fullName的值时,要通过set方法更改

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app">
    姓:<input type="text" v-model="firstName"><br>
    名:<input type="text" v-model="lastName"><br>
    <p>姓名:{{ fullName }}</p>
    <button @click="changeName">修改姓名</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        firstName: "张",
        lastName: "小飞"
      },
      methods: {
        changeName() {
          this.fullName = "吕小布"
        }
      },
      computed: {
        fullName: {
          get() {
            return this.firstName + this.lastName
          },
          set(value) {
            // slice方法:截取字符串
            this.firstName = value.slice(0, 1)
            this.lastName = value.slice(1)
          }
        }
      },
    })
  </script>
</body>
</html>

结果:点击改名卡按钮时,fullName的值变成“吕小布”

综合案例:成绩表

代码:

html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./styles/index.css" />
    <title>Document</title>
  </head>
  <body>
    <div id="app" class="score-case">
      <div class="table">
        <table>
          <thead>
            <tr>
              <th>编号</th>
              <th>科目</th>
              <th>成绩</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody v-if="list.length > 0">
            <tr v-for="(item, index) in list" :key="item.id">
              <td>{{ index + 1 }}</td>
              <td>{{ item.subject }}</td>
              <td :class="{red: item.score < 60}">{{ item.score }}</td>
              <td><a @click.prevent="del(item.id)" href="#">删除</a></td>
            </tr>
          </tbody>
          <tbody v-else>
            <tr>
              <td colspan="5">
                <span class="none">暂无数据</span>
              </td>
            </tr>
          </tbody>

          <tfoot>
            <tr>
              <td colspan="5">
                <span>总分:{{ totalScore }}</span>
                <span style="margin-left: 50px">平均分:{{ averageScore }}</span>
              </td>
            </tr>
          </tfoot>
        </table>
      </div>
      <div class="form">
        <div class="form-item">
          <div class="label">科目:</div>
          <div class="input">
            <input
              type="text"
              placeholder="请输入科目"
              v-model.trim="subject"
            />
          </div>
        </div>
        <div class="form-item">
          <div class="label">分数:</div>
          <div class="input">
            <input
              type="text"
              placeholder="请输入分数"
              v-model.number="score"
            />
          </div>
        </div>
        <div class="form-item">
          <div class="label"></div>
          <div class="input">
            <button class="submit" @click="add">添加</button>
          </div>
        </div>
      </div>
    </div>
    <script src="../vue.js"></script>

    <script>
      const app = new Vue({
        el: '#app',
        data: {
          list: [
            { id: 1, subject: '语文', score: 20 },
            { id: 7, subject: '数学', score: 99 },
            { id: 12, subject: '英语', score: 70 },
          ],
          subject: '',
          score: ''
        },
        methods: {
          del (id) {
            this.list = this.list.filter((item) => item.id !== id)
          },
          add () {
            if (!this.subject){
              alert("请输入科目")
              return
            }
            if (typeof this.score !== "number"){
              alert("请输入合法的分数")
              return
            }
            this.list.unshift({ id: +new Date(), subject: this.subject, score: this.score })

            this.subject = '',
            this.score = ''
          }
        },
        computed: {
          totalScore() {
            let total = this.list.reduce((sum, item) => sum + item.score, 0)
            return total
          },
          averageScore() {
            if (this.list.length === 0){
              return 0
            }
            return (this.totalScore / this.list.length).toFixed(2)
          }
        }
      })
    </script>
  </body>
</html>

index.css 

.score-case {
  width: 1000px;
  margin: 50px auto;
  display: flex;
}
.score-case .table {
  flex: 4;
}
.score-case .table table {
  width: 100%;
  border-spacing: 0;
  border-top: 1px solid #ccc;
  border-left: 1px solid #ccc;
}
.score-case .table table th {
  background: #f5f5f5;
}
.score-case .table table tr:hover td {
  background: #f5f5f5;
}
.score-case .table table td,
.score-case .table table th {
  border-bottom: 1px solid #ccc;
  border-right: 1px solid #ccc;
  text-align: center;
  padding: 10px;
}
.score-case .table table td.red,
.score-case .table table th.red {
  color: red;
}
.score-case .table .none {
  height: 100px;
  line-height: 100px;
  color: #999;
}
.score-case .form {
  flex: 1;
  padding: 20px;
}
.score-case .form .form-item {
  display: flex;
  margin-bottom: 20px;
  align-items: center;
}
.score-case .form .form-item .label {
  width: 60px;
  text-align: right;
  font-size: 14px;
}
.score-case .form .form-item .input {
  flex: 1;
}
.score-case .form .form-item input,
.score-case .form .form-item select {
  appearance: none;
  outline: none;
  border: 1px solid #ccc;
  width: 200px;
  height: 40px;
  box-sizing: border-box;
  padding: 10px;
  color: #666;
}
.score-case .form .form-item input::placeholder {
  color: #666;
}
.score-case .form .form-item .cancel,
.score-case .form .form-item .submit {
  appearance: none;
  outline: none;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 10px;
  margin-right: 10px;
  font-size: 12px;
  background: #ccc;
}
.score-case .form .form-item .submit {
  border-color: #069;
  background: #069;
  color: #fff;
}

index.less

.score-case {
  width: 1000px;
  margin: 50px auto;
  display: flex;
  .table {
    flex: 4;
    table {
      width: 100%;
      border-spacing: 0;
      border-top: 1px solid #ccc;
      border-left: 1px solid #ccc;
      th {
        background: #f5f5f5;
      }
      tr:hover td {
        background: #f5f5f5;
      }
      td,
      th {
        border-bottom: 1px solid #ccc;
        border-right: 1px solid #ccc;
        text-align: center;
        padding: 10px;
        &.red {
          color: red;
        }
      }
    }
    .none {
      height: 100px;
      line-height: 100px;
      color: #999;
    }
  }
  .form {
    flex: 1;
    padding: 20px;
    .form-item {
      display: flex;
      margin-bottom: 20px;
      align-items: center;
    }
    .form-item .label {
      width: 60px;
      text-align: right;
      font-size: 14px;
    }
    .form-item .input {
      flex: 1;
    }
    .form-item input,
    .form-item select {
      appearance: none;
      outline: none;
      border: 1px solid #ccc;
      width: 200px;
      height: 40px;
      box-sizing: border-box;
      padding: 10px;
      color: #666;
    }
    .form-item input::placeholder {
      color: #666;
    }
    .form-item .cancel,
    .form-item .submit {
      appearance: none;
      outline: none;
      border: 1px solid #ccc;
      border-radius: 4px;
      padding: 4px 10px;
      margin-right: 10px;
      font-size: 12px;
      background: #ccc;
    }
    .form-item .submit {
      border-color: #069;
      background: #069;
      color: #fff;
    }
  }
}

动态渲染表格数据:

添加成绩:

计算总分、平均分:

效果:

watch侦听器:当侦听的数据发生变化时,执行相应操作

代码:翻译器

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        font-size: 18px;
      }
      #app {
        padding: 10px 20px;
      }
      .query {
        margin: 10px 0;
      }
      .box {
        display: flex;
      }
      textarea {
        width: 300px;
        height: 160px;
        font-size: 18px;
        border: 1px solid #dedede;
        outline: none;
        resize: none;
        padding: 10px;
      }
      textarea:hover {
        border: 1px solid #1589f5;
      }
      .transbox {
        width: 300px;
        height: 160px;
        background-color: #f0f0f0;
        padding: 10px;
        border: none;
      }
      .tip-box {
        width: 300px;
        height: 25px;
        line-height: 25px;
        display: flex;
      }
      .tip-box span {
        flex: 1;
        text-align: center;
      }
      .query span {
        font-size: 18px;
      }

      .input-wrap {
        position: relative;
      }
      .input-wrap span {
        position: absolute;
        right: 15px;
        bottom: 15px;
        font-size: 12px;
      }
      .input-wrap i {
        font-size: 20px;
        font-style: normal;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 条件选择框 -->
      <div class="query">
        <span>翻译成的语言:</span>
        <select v-model="obj.lang">
          <option value="italy">意大利</option>
          <option value="english">英语</option>
          <option value="german">德语</option>
        </select>
      </div>

      <!-- 翻译框 -->
      <div class="box">
        <div class="input-wrap">
          <!-- 双向数据绑定到obj对象的words属性 -->
          <textarea v-model="obj.words"></textarea>
          <span><i>⌨️</i>文档翻译</span>
        </div>
        <div class="output-wrap">
          <!-- 将翻译结果result显示到右框 -->
          <div class="transbox">{{ result }}</div>
        </div>
      </div>
    </div>
    <script src="../vue.js"></script>
    <script src="../axios.js"></script>
    <script>
      // 接口地址:https://applet-base-api-t.itheima.net/api/translate
      // 请求方式:get
      // 请求参数:
      // (1)words:需要被翻译的文本(必传)
      // (2)lang: 需要被翻译成的语言(可选)默认值-意大利
      // -----------------------------------------------
      
      const app = new Vue({
        el: '#app',
        data: {
          //words: '',
          obj: {
            words: 'popo',
            lang: 'italy'
          },
          result: '',
          //timer: null //延时器id
        },
        // 具体讲解:(1) watch语法 (2) 具体业务实现
        watch: {
          /* words (newValue, oldValue) {
            console.log(newValue, oldValue)
          } */
         /* 侦听器用法:当obj.words变量发生变化时,会触发与之同名的侦听事件,
         也就是每输入一个字母,都会触发下面的函数 */
        //  默认传入了两个参数,newValue是变化后的值, oldValue是变化前的值,可以只写newValue
          obj: {
            deep: true, // 深度监听,words和lang都可以被监听到
            immediate: true, // 立刻执行,页面刷新时,立即触发,将words的默认值翻译出来
            handler(newValue) {
              //console.log(newValue)
              // 但是,如果连续输入,每输入一个字母,obj.words函数就会被执行一次,生成一个延时器
              // 无法达到输入最后一个字母后,不再有任何输入时,才延时300ms发送请求
              // (还是输入了n个字母,就发送了n次请求)
              // 解决办法:防抖
              // clearTimeout函数:清除延时器
              // 当输入最后一个字母后,触发一次obj.words函数,倒数第二个字母的延时器被清除,不发送请求
              // 最后一个字母的延时器在300ms后会发送请求,因为不再输入,所以obj.words函数不会再触发
              clearTimeout(this.timer)
              // 设置延时器setTimeout:输入一个字母后,延迟300ms才发送请求
              this.timer =  setTimeout(async () => {
                const res = await axios({
                  url: "https://applet-base-api-t.itheima.net/api/translate",
                  params: newValue
                })

                this.result = res.data.data
                console.log(res.data.data)
              }, 300)
            }
          }
          // 'obj.words' (newValue) {
            
          // }
        }
      })
    </script>
  </body>
</html>

words是要翻译的内容,result是翻译的结果 

当words输入完以后,发送翻译请求,更新result的值 

如果选择的语言发生变化 ,也会触发监听事件

效果:

综合案例:水果购物车

渲染数据:

1.水果信息与空购物车互斥(v-if、v-else

效果:

有水果信息,没有空购物车

有空购物车,没有水果

2.展示水果信息(v-bind控制class样式、v-for循环遍历数组、v-model与复选框的checked双向数据绑定、v-bind绑定标签img的src属性、插值表达式展示单价、个数和小计

删除功能:

修改个数:

:disabled的作用:当水果的数量到1时,“-”的按钮不可用

find函数:找到与传入id对应的水果,才能修改它的个数 

全选反选:

效果:

统计选中的总价和总数量:

持久化到本地:

持久化以后,再次刷新网页,会保留前面操作的结果

清空本地存储:

最终效果: 

html代码: 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./css/inputnumber.css" />
    <link rel="stylesheet" href="./css/index.css" />
    <title>购物车</title>
  </head>
  <body>
    <div class="app-container" id="app">
      <!-- 顶部banner -->
      <div class="banner-box"><img src="./img/fruit.jpg" alt="" /></div>
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <span>🏠</span>
        /
        <span>购物车</span>
      </div>
      <!-- 购物车主体 -->
       <!-- 水果信息与空购物车互斥 -->
      <div class="main" v-if="fruitList.length > 0">
        <div class="table">
          <!-- 头部 -->
          <div class="thead">
            <div class="tr">
              <div class="th">选中</div>
              <div class="th th-pic">图片</div>
              <div class="th">单价</div>
              <div class="th num-th">个数</div>
              <div class="th">小计</div>
              <div class="th">操作</div>
            </div>
          </div>
          <!-- 身体 -->
          <div class="tbody">
            <div class="tr" :class="{ active:item.isChecked }" v-for="(item, index) in fruitList" :key="item.id">
              <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
              <div class="td"><img :src="item.icon" alt="" /></div>
              <div class="td">{{ item.price }}</div>
              <div class="td">
                <div class="my-input-number">
                  <!-- :disabled的作用:当水果的数量到1时,“-”的按钮不可用 -->
                  <button class="decrease" @click="decrease(item.id)" :disabled="item.num <= 1"> - </button>
                  <span class="my-input__inner">{{ item.num }}</span>
                  <button class="increase" @click="increase(item.id)"> + </button>
                </div>
              </div>
              <div class="td">{{ item.price * item.num }}</div>
              <div class="td"><button @click="del(item.id)">删除</button></div>
            </div>
          </div>
        </div>
        <!-- 底部 -->
        <div class="bottom">
          <!-- 全选 -->
          <label class="check-all">
            <!-- isAll是计算属性 -->
            <input v-model="isAll" type="checkbox" />
            全选
          </label>
          <div class="right-box">
            <!-- 所有商品总价 -->
             <!-- sumPrice是计算属性 -->
            <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{ sumPrice }}</span></span>
            <!-- 结算按钮 -->
             <!-- sumNum是计算属性 -->
            <button class="pay">结算( {{ sumNum }} )</button>
          </div>
        </div>
      </div>
      <!-- 空车 -->
       <!-- 水果信息与空购物车互斥 -->
      <div class="empty" v-else>🛒空空如也</div>
    </div>
    <script src="D:\vue_code\vue2-study\vue.js"></script>
    <script>
      //默认数组
      const defaultArr = [
            {
              id: 1,
              icon: './img/火龙果.png',
              isChecked: true,
              num: 2,
              price: 6,
            },
            {
              id: 2,
              icon: './img/荔枝.png',
              isChecked: false,
              num: 7,
              price: 20,
            },
            {
              id: 3,
              icon: './img/榴莲.png',
              isChecked: false,
              num: 3,
              price: 40,
            },
            {
              id: 4,
              icon: './img/鸭梨.png',
              isChecked: true,
              num: 10,
              price: 3,
            },
            {
              id: 5,
              icon: './img/樱桃.png',
              isChecked: false,
              num: 20,
              price: 34,
            },
          ]
      const app = new Vue({
        el: '#app',
        data: {
          // 解析JSON串,从本地读取水果列表,当删除所有水果时,列表为空,null无法读取,会导致网页整个丢失
          //可以清空本地存储,让fruitList恢复为默认数组defaultArr
          fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr,
        },
        computed: {
          // isAll(){
          //   return this.fruitList.every(item => item.isChecked)
          // }
          // 当计算属性要作修改时,写完整写法:get + set
          isAll: {
            // 获取isAll的值
            get () {
              // every函数:数组中的元素都满足是选中状态时,返回true,
              // 也就是水果都被选中时,全选按钮亮起
              return this.fruitList.every(item => item.isChecked)
            },
            // 设置isAll的值为value
            set (value) {
              // forEach函数:遍历数组,修改数组中的选择状态都为true,
              // 也就是,当勾选全选按钮时,遍历数组,让每个水果都被选中
              this.fruitList.forEach(item => item.isChecked = value)
            }
          },
          sumPrice () {
            // reduce函数:求和
            let sum = this.fruitList.reduce((sum, item) => {
              // 如果水果被选中,把这个水果的总价加到sum上
              if (item.isChecked){
                return sum + item.num * item.price
              } else { //否则,只返回sum
                return sum
              }
            }, 0)
            return sum
          },
          sumNum () {
            let sum = this.fruitList.reduce((sum, item) => {
              if (item.isChecked){
                return sum + item.num
              } else {
                return sum
              }
            }, 0)
            return sum
          },
        },
        methods: {
          del (id) {
            this.fruitList = this.fruitList.filter((item) => item.id !== id)
          },
          decrease (id) {
            // find函数:找到与传入id对应的水果,才能修改它的个数
            const fruit = this.fruitList.find(item => item.id === id)
            fruit.num--
          },
          increase (id) {
            const fruit = this.fruitList.find(item => item.id === id)
            fruit.num++
          }
        },
        watch: {
          fruitList: {
            deep: true, //深度监听,水果数组的任何值发生变化,都能监听到
            handler(newValue) { //newValue是变化后的fruitList
              console.log(newValue)
              //localStorage:本地存储,把fruitList转成JSON格式,当作值,list是键,存储到浏览器本地
              localStorage.setItem('list', JSON.stringify(newValue))
            }
          }
        }
      })
    </script>
  </body>
</html>

index.css

.app-container {
  padding-bottom: 300px;
  width: 800px;
  margin: 0 auto;
}
@media screen and (max-width: 800px) {
  .app-container {
    width: 600px;
  }
}
.app-container .banner-box {
  border-radius: 20px;
  overflow: hidden;
  margin-bottom: 10px;
}
.app-container .banner-box img {
  width: 100%;
}
.app-container .nav-box {
  background: #ddedec;
  height: 60px;
  border-radius: 10px;
  padding-left: 20px;
  display: flex;
  align-items: center;
}
.app-container .nav-box .my-nav {
  display: inline-block;
  background: #5fca71;
  border-radius: 5px;
  width: 90px;
  height: 35px;
  color: white;
  text-align: center;
  line-height: 35px;
  margin-right: 10px;
}

.breadcrumb {
  font-size: 16px;
  color: gray;
}
.table {
  width: 100%;
  text-align: left;
  border-radius: 2px 2px 0 0;
  border-collapse: separate;
  border-spacing: 0;
}
.th {
  color: rgba(0, 0, 0, 0.85);
  font-weight: 500;
  text-align: left;
  background: #fafafa;
  border-bottom: 1px solid #f0f0f0;
  transition: background 0.3s ease;
}
.th.num-th {
  flex: 1.5;
}
.th {
  text-align: center;
}
.th:nth-child(4),
.th:nth-child(5),
.th:nth-child(6),
.th:nth-child(7) {
  text-align: center;
}
.th.th-pic {
  flex: 1.3;
}
.th:nth-child(6) {
  flex: 1.3;
}

.th,
.td {
  position: relative;
  padding: 16px 16px;
  overflow-wrap: break-word;
  flex: 1;
}
.pick-td {
  font-size: 14px;
}
.main,
.empty {
  border: 1px solid #f0f0f0;
  margin-top: 10px;
}
.tr {
  display: flex;
  cursor: pointer;
  border-bottom: 1px solid #ebeef5;
}
.tr.active {
  background-color: #f5f7fa;
}
.td {
  display: flex;
  justify-content: center;
  align-items: center;
}

.table img {
  width: 100px;
  height: 100px;
}

button {
  outline: 0;
  box-shadow: none;
  color: #fff;
  background: #d9363e;
  border-color: #d9363e;
  color: #fff;
  background: #d9363e;
  border-color: #d9363e;
  line-height: 1.5715;
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  background-image: none;
  border: 1px solid transparent;
  box-shadow: 0 2px 0 rgb(0 0 0 / 2%);
  cursor: pointer;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  touch-action: manipulation;
  height: 32px;
  padding: 4px 15px;
  font-size: 14px;
  border-radius: 2px;
}
button.pay {
  background-color: #3f85ed;
  margin-left: 20px;
}

.bottom {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: 20px;
  border: 1px solid #f0f0f0;
  border-top: none;
  padding-left: 20px;
}
.right-box {
  display: flex;
  align-items: center;
}
.check-all {
  cursor: pointer;
}
.price {
  color: hotpink;
  font-size: 30px;
  font-weight: 700;
}
.price-box {
  display: flex;
  align-items: center;
}
.empty {
  padding: 20px;
  text-align: center;
  font-size: 30px;
  color: #909399;
}
.my-input-number {
  display: flex;
}
.my-input-number button {
  height: 40px;
  color: #333;
  border: 1px solid #dcdfe6;
  background-color: #f5f7fa;
}
.my-input-number button:disabled {
  cursor: not-allowed!important;
}
.my-input-number .my-input__inner {
  height: 40px;
  width: 50px;
  padding: 0;
  border: none;
  border-top: 1px solid #dcdfe6;
  border-bottom: 1px solid #dcdfe6;
}

inputnumber.css 

.my-input-number {
  position: relative;
  display: inline-block;
  width: 140px;
  line-height: 38px;
}
.my-input-number span {
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}
.my-input-number .my-input {
  display: block;
  position: relative;
  font-size: 14px;
  width: 100%;
}
.my-input-number .my-input__inner {
  -webkit-appearance: none;
  background-color: #fff;
  background-image: none;
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  color: #606266;
  display: inline-block;
  font-size: inherit;
  height: 40px;
  line-height: 40px;
  outline: none;
  padding: 0 15px;
  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  width: 100%;
  padding-left: 50px;
  padding-right: 50px;
  text-align: center;
}
.my-input-number .my-input-number__decrease,
.my-input-number .my-input-number__increase {
  position: absolute;
  z-index: 1;
  top: 1px;
  width: 40px;
  height: auto;
  text-align: center;
  background: #f5f7fa;
  color: #606266;
  cursor: pointer;
  font-size: 13px;
}
.my-input-number .my-input-number__decrease {
  left: 1px;
  border-radius: 4px 0 0 4px;
  border-right: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__increase {
  right: 1px;
  border-radius: 0 4px 4px 0;
  border-left: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__decrease.is-disabled,
.my-input-number .my-input-number__increase.is-disabled {
  color: #c0c4cc;
  cursor: not-allowed;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值