Web前端-Vue2+Vue3基础入门到实战项目-Day5(自定义指令, 插槽, 案例商品列表, 路由入门)

自定义指令

基本使用

  • 自定义指令: 自己定义的指令, 可以封装一些dom操作, 扩展额外功能
  • 全局注册
    // 1. 全局注册指令
    Vue.directive('focus', {
      // inserted 会在 指令所在的元素, 被插入到页面中时触发
      inserted (el) {
          // el 就是指令所绑定的元素
          // console.log(el)
          el.focus()
      }
    })
    
  • 局部注册
    <script>
    export default {
    // mounted(){
    //   this.$refs.inp.focus()
    // }
    
    // 2. 局部注册指令
    directives: {
        // 指令名: 指令的配置项
        focus: {
        inserted (el) {
            el.focus()
        }
        }
    }
    }
    script>
    

指令的值

  • v-指令名=“指令值”, 通过等号可以绑定指令的值
  • 通过binding.value可以拿到指令的值
  • 通过update钩子, 可以监听指令值的变化, 进行dom更新操作
<template>
  <div>
    <h1 v-color="color1">指令的值1测试h1>
    <h1 v-color="color2">指令的值2测试h1>
  div>
template>

<script>
export default {
  data () {
    return {
      color1: 'red',
      color2: 'green'
    }
  },
  directives: {
    color: {
      // 1. inserted 提供的是元素被添加到页面中时的逻辑
      inserted (el, binding) {
        // binding.value 就是指令的值
        el.style.color = binding.value
      },
      // 2. update 指令的值修改的时候触发, 提供值变化后, dom更新的逻辑
      update(el, binding){
        // console.log(binding.value)
        el.style.color = binding.value
      }
    }
  }
}
script>

<style>

style>

封装v-loading指令

  • 核心思路
    1. 准备类名loading, 通过伪元素提供遮罩层
    2. 添加或移除类名, 实现loading蒙层的添加移除
    3. 利用指令语法, 封装v-loading通用指令
      inserted钩子中, binding.value判断指令的值, 设置默认状态
      update钩子中, binding.value判断指令的值, 更新类名状态
<template>
  <div class="main">
    <div class="box" v-loading="isLoading">
      <ul>
        <li v-for="item in list" :key="item.id" class="news">
          <div class="left">
            <div class="title">{{ item.title }}div>
            <div class="info">
              <span>{{ item.source }}span>
              <span>{{ item.time }}span>
            div>
          div>

          <div class="right">
            <img :src="item.img" alt="">
          div>
        li>
      ul>
    div>
  div>
template>

<script>
// 安装axios =>  yarn add axios
import axios from 'axios'

// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
  data () {
    return {
      list: [],
      isLoading: true
    }
  },
  directives: {
    loading(el, binding){
      binding.value ? el.classList.add('loading') : el.classList.remove('loading')
    },
    update(el, binding){
      binding.value ? el.classList.add('loading') : el.classList.remove('loading')
    }
  },
  async created () {
    // 1. 发送请求获取数据
    const res = await axios.get('http://hmajax.itheima.net/api/news')
    
    setTimeout(() => {
      // 2. 更新到 list 中
      this.list = res.data.data
      this.isLoading = false
    }, 2000)
  }
}
script>

<style>
/* 伪类 - 蒙层效果 */
.loading:before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url('./loading.gif') no-repeat center;
}

/* .box2 {
  width: 400px;
  height: 400px;
  border: 2px solid #000;
  position: relative;
} */

.box {
  width: 800px;
  min-height: 500px;
  border: 3px solid orange;
  border-radius: 5px;
  position: relative;
}
.news {
  display: flex;
  height: 120px;
  width: 600px;
  margin: 0 auto;
  padding: 20px 0;
  cursor: pointer;
}
.news .left {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-right: 10px;
}
.news .left .title {
  font-size: 20px;
}
.news .left .info {
  color: #999999;
}
.news .left .info span {
  margin-right: 20px;
}
.news .right {
  width: 160px;
  height: 120px;
}
.news .right img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
style>

插槽

默认插槽

  • 使用步骤
    1. 先在组件内用slot占位
    2. 使用组件时, 传入具体标签内容插入
  • 插槽中的内容会作为默认值
  • MyDialog.vue
    <template>
      <div class="dialog">
        <div class="dialog-header">
          <h3>友情提示h3>
          <span class="close">✖️span>
        div>
    
        <div class="dialog-content">
          
          <slot>slot>
        div>
        <div class="dialog-footer">
          <button>取消button>
          <button>确认button>
        div>
      div>
    template>
    
    <script>
    export default {
      data () {
        return {
    
        }
      }
    }
    script>
    
    <style scoped>
    * {
      margin: 0;
      padding: 0;
    }
    .dialog {
      width: 470px;
      height: 230px;
      padding: 0 25px;
      background-color: #ffffff;
      margin: 40px auto;
      border-radius: 5px;
    }
    .dialog-header {
      height: 70px;
      line-height: 70px;
      font-size: 20px;
      border-bottom: 1px solid #ccc;
      position: relative;
    }
    .dialog-header .close {
      position: absolute;
      right: 0px;
      top: 0px;
      cursor: pointer;
    }
    .dialog-content {
      height: 80px;
      font-size: 18px;
      padding: 15px 0;
    }
    .dialog-footer {
      display: flex;
      justify-content: flex-end;
    }
    .dialog-footer button {
      width: 65px;
      height: 35px;
      background-color: #ffffff;
      border: 1px solid #e1e3e9;
      cursor: pointer;
      outline: none;
      margin-left: 10px;
      border-radius: 3px;
    }
    .dialog-footer button:last-child {
      background-color: #007acc;
      color: #fff;
    }
    style>
    
  • App.vue
    <template>
      <div>
        
        <MyDialog>你确认要删除吗MyDialog>
    
        <MyDialog>你确认要退出吗MyDialog>
      div>
    template>
    
    <script>
    import MyDialog from "./components/MyDialog.vue"
    export default {
      data() {
        return {}
      },
      components: {
        MyDialog,
      },
    }
    script>
    
    <style>
    body {
      background-color: #b3b3b3;
    }
    style>
    

具名插槽

  • slot占位, 给name属性起名字来区分
  • template配合v-slot:插槽名分发内容
  • v-slot:插槽名 可以简化为 #插槽名

App.vue

<template>
  <div>
    <MyDialog>
      
      <template v-slot:head>
        <div>我是大标题div>
      template>
      <template v-slot:content>
        <div>我是内容div>
      template>
      <template #footer>
        <button>确认button>
        <button>取消button>
      template>
    MyDialog>
  div>
template>

<script>
import MyDialog from './components/MyDialog.vue'
export default {
  data () {
    return {

    }
  },
  components: {
    MyDialog
  }
}
script>

<style>
body {
  background-color: #b3b3b3;
}
style>

MyDialog.vue

<template>
  <div class="dialog">
    <div class="dialog-header">
      
      <slot name="head">slot>
    div>

    <div class="dialog-content">
      <slot name="content">slot>
    div>
    <div class="dialog-footer">
      <slot name="footer">slot>
    div>
  div>
template>

<script>
export default {
  data() {
    return {}
  },
}
script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
style>

作用域插槽

  • 作用: 可以给插槽上绑定数据, 供将来使用组件时使用
  • 使用步骤:
    1. 给slot标签, 以添加属性的方式传值
    2. 所有属性都会被收集到一个对象中
    3. template中, 通过#插槽名="obj"接收
  • App.vue
    <template>
      <div>
        <MyTable :data="list">
          
          <template #default="obj">
            <button @click="del(obj.row.id)">
              删除
            button>
          template>
        MyTable>
        <MyTable :data="list2">
          <template #default="{ row }">
            <button @click="show(row)">
              查看
            button>
          template>
        MyTable>
      div>
    template>
    
    <script>
    import MyTable from './components/MyTable.vue'
    export default {
      data () {
        return {
          list: [
            { id: 1, name: '张小花', age: 18 },
            { id: 2, name: '孙大明', age: 19 },
            { id: 3, name: '刘德忠', age: 17 },
          ],
          list2: [
            { id: 1, name: '赵小云', age: 18 },
            { id: 2, name: '刘蓓蓓', age: 19 },
            { id: 3, name: '姜肖泰', age: 17 },
          ]
        }
      },
      components: {
        MyTable
      },
      methods: {
        del(id){
          this.list = this.list.filter(item => item.id !== id)
        },
        show(row){
          // console.log(row)
          alert(`姓名: ${row.name}\n年纪: ${row.age}`)
        }
      }
    }
    script>
    
  • MyTable.vue
    <template>
      <table class="my-table">
        <thead>
          <tr>
            <th>序号th>
            <th>姓名th>
            <th>年纪th>
            <th>操作th>
          tr>
        thead>
        <tbody>
          <tr v-for="(item, index) in data" :key="item.id">
            <td> {{index+1}} td>
            <td> {{item.name}} td>
            <td> {{item.age}} td>
            <td>
              
              <slot :row="item" msg="test">slot>
              
              
            td>
          tr>
        tbody>
      table>
    template>
    
    <script>
    export default {
      props: {
        data: Array,
      },
    }
    script>
    
    <style scoped>
    .my-table {
      width: 450px;
      text-align: center;
      border: 1px solid #ccc;
      font-size: 24px;
      margin: 30px auto;
    }
    .my-table thead {
      background-color: #1f74ff;
      color: #fff;
    }
    .my-table thead th {
      font-weight: normal;
    }
    .my-table thead tr {
      line-height: 40px;
    }
    .my-table th,
    .my-table td {
      border-bottom: 1px solid #ccc;
      border-right: 1px solid #ccc;
    }
    .my-table td:last-child {
      border-right: none;
    }
    .my-table tr:last-child td {
      border-bottom: none;
    }
    .my-table button {
      width: 65px;
      height: 35px;
      font-size: 18px;
      border: 1px solid #ccc;
      outline: none;
      border-radius: 3px;
      cursor: pointer;
      background-color: #ffffff;
      margin-left: 5px;
    }
    style>
    

案例 - 商品列表

my-tag 标签组件的封装
1. 创建组件 - 初始化
2. 实现功能
  2.1 双击显示, 并且自动聚焦
    v-if v-else @dblclick
    自动聚焦
    1. $nextTick => $refs 获取到dom, 进行focus获取焦点
    2. 封装v-focus指令
  2.2 失去焦点, 隐藏输入框
    @blur 操作 isEdit
  2.3 回显标签内容
    回显的标签信息是父组件传递过来的
    v-model实现功能(简化代码) v-model => :value 和 @input
    组件内部通过props接收, :value设置给输入框
  2.4 内容修改, 回车=> 修改标签信息
    @keyup.enter, 触发事件 $emit('input', e.target.value)
------------------------
my-table 表格组件的封装
1. 数据不能写死, 动态传递表格渲染的数据 props
2. 结构不能写死 - 多处结构自定义 [具名插槽]
  2.1 表头支持自定义
  2.2 主体支持自定义
  • App.vue
    <template>
      <div class="table-case">
        <MyTable :data="goods">
          <template #head>
            <th>编号th>
            <th>名称th>
            <th>图片th>
            <th width="100px">标签th>
          template>
          <template #body="{item, index}">
            <td> {{index+1}} td>
            <td> {{item.name}} td>
            <td>
              <img :src="item.picture" />
            td>
            <td>
              
              <MyTag v-model="item.tag">MyTag>
            td>
          template>
        MyTable>
      div>
    template>
    
    <script>
    import MyTag from './components/MyTag.vue'
    import MyTable from './components/MyTable.vue'
    export default {
      name: 'TableCase',
      components: {
        MyTag,
        MyTable
      },
      methods: {
    
      },
      data() {
        return {
          goods: [
            {
              id: 101,
              picture:
                'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
              name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
              tag: '茶具',
            },
            {
              id: 102,
              picture:
                'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
              name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
              tag: '男鞋',
            },
            {
              id: 103,
              picture:
                'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
              name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
              tag: '儿童服饰',
            },
            {
              id: 104,
              picture:
                'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
              name: '基础百搭,儿童套头针织毛衣1-9岁',
              tag: '儿童服饰',
            },
          ],
        }
      },
    }
    script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    }
    style>
    
  • MyTable.vue
    <template>
      <table class="my-table">
        <thead>
          <tr>
            <slot name="head">slot>
          tr>
        thead>
        <tbody>
          <tr v-for="(item ,index) in data" :key="item.id">
            <slot name="body" :item="item" :index="index">slot>
          tr>
        tbody>
      table>
    template>
    
    <script>
    export default {
      props: {
        data: {
          type:Array,
          required: true
        },
      },
      components: {
      }
    }
    script>
    
    <style  lang="less" scoped>
    .my-table {
      width: 100%;
      border-spacing: 0;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
      th {
        background: #f5f5f5;
        border-bottom: 2px solid #069;
      }
      td {
        border-bottom: 1px dashed #ccc;
      }
      td,
      th {
        text-align: center;
        padding: 10px;
        transition: all 0.5s;
        &.red {
          color: red;
        }
      }
      .none {
        height: 100px;
        line-height: 100px;
        color: #999;
      }
    }
    style>
    
  • MyTag.vue
    <template>
      <div class="my-tag">
        <input 
          v-if="isEdit"
          v-focus
          @blur="isEdit = false"
          ref="inp"
          class="input" 
          type="text" 
          :value="value"
          @keyup.enter="handleEnter"
          placeholder="输入标签">
        <div 
          v-else
          @dblclick="handleClick"
          class="text">
          {{value}}
        div>
      div>
    template>
    
    <script>
    export default {
      data () {
        return {
          isEdit: false
        }
      },
      props: {
        value: String
      },
      methods: {
        handleClick(){
          // 双击后, 切换到显示状态
          this.isEdit = true
          // // 由于Vue是异步dom更新, 所以要等dom更新完, 再获取焦点
          // this.$nextTick(() => {
          //   // 获取焦点
          //   this.$refs.inp.focus()
          // })
        },
        handleEnter(e){
          if(e.target.value.trim() === ''){
            return alert('标签内容不能为空')
          }
          // 子传父, 将回车时, [输入框的内容] 提交给父组件更新
          // 由于父组件是v-model, 触发事件, 需要触发input事件
          this.$emit('input', e.target.value)
          this.isEdit = false
        }
      }
    }
    script>
    
    <style lang="less" scoped>
    .my-tag {
      cursor: pointer;
      .input {
        appearance: none;
        outline: none;
        border: 1px solid #ccc;
        width: 100px;
        height: 40px;
        box-sizing: border-box;
        padding: 10px;
        color: #666;
        &::placeholder {
          color: #666;
        }
      }
    }
    style>
    

路由入门

单页面应用程序

Web前端-Vue2+Vue3基础入门到实战项目-Day5(自定义指令, 插槽, 案例商品列表, 路由入门)_第1张图片

  • 单页面应用程序
    • 所有功能在一个html页面上实现
    • 优点: 按需更新性能高, 开发效率高, 用户体验好
    • 缺点: 学习成本, 首屏加载慢, 不利于SEO
    • 应用场景: 系统类/内部/文档类/移动端 网站

路由基本使用

路由的使用步骤 5 + 2
5个基础步骤
1. 下载 v3.6.5
2. 引入
3. 安装注册 Vue.use(Vue插件)
4. 创建路由对象
5. 注入到new Vue中,建立关联

2个核心步骤
1. 建组件(views目录),配规则
2. 准备导航链接,配置路由出口(匹配的组件展示的位置) 
  • App.vue
    <template>
      <div>
        <div class="footer_wrap">
          <a href="#/find">发现音乐a>
          <a href="#/my">我的音乐a>
          <a href="#/friend">朋友a>
        div>
        <div class="top">
          
          <router-view>router-view>
        div>
      div>
    template>
    
    <script>
    export default {};
    script>
    
    <style>
    body {
      margin: 0;
      padding: 0;
    }
    .footer_wrap {
      position: relative;
      left: 0;
      top: 0;
      display: flex;
      width: 100%;
      text-align: center;
      background-color: #333;
      color: #ccc;
    }
    .footer_wrap a {
      flex: 1;
      text-decoration: none;
      padding: 20px 0;
      line-height: 20px;
      background-color: #333;
      color: #ccc;
      border: 1px solid black;
    }
    .footer_wrap a:hover {
      background-color: #555;
    }
    style>
    
  • main.js
    import Vue from 'vue'
    import App from './App.vue'
    
    import Find from './views/Find'
    import My from './views/My'
    import Friend from './views/Friend'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter) // VueRouter插件初始化
    
    const router = new VueRouter({
      // routes 路由规则们
      // route  一条路由规则 { path: 路径, component: 组件 }
      routes: [
        { path: '/find', component: Find },
        { path: '/my', component: My },
        { path: '/friend', component: Friend },
      ]
    })
    
    Vue.config.productionTip = false
    
    new Vue({
      render: h => h(App),
      router
    }).$mount('#app')
    
    

来源

黑马程序员. Vue2+Vue3基础入门到实战项目

你可能感兴趣的:(Web前端,前端,javascript,vue.js,自定义指令,插槽,路由,案例-商品列表)