二九(vue2-05)、父子通信v-model、sync、ref、¥nextTick、自定义指令、具名插槽、作用域插槽、综合案例 - 商品列表

1. 进阶语法

1.1 v-model 简化代码

App.vue
html 复制代码
<template>
  <!-- 11-src-下拉封装 -->
  <div class="app">
    <!-- <BaseSelect :cityId="selectId" @changeId="handleChangeId"></BaseSelect> -->

    <!-- v-model 简化代码 → 父组件 v-model 简化代码,实现 子组件 和 父组件数据 双向绑定 -->
    <!-- 1. 子组件中:props 通过 value 接收,事件触发 input -->
    <!-- 2. 父组件中:v-model 给组件直接绑数据 -->
    <BaseSelect :cityId="selectId" v-model="selectId"></BaseSelect>
  </div>
</template>

<script>
import BaseSelect from "./components/BaseSelect.vue";
export default {
  data() {
    return {
      selectId: "102",
    };
  },
  components: {
    BaseSelect,
  },
  methods: {
    /* handleChangeId(id) {
      this.selectId = id;
    }, */
  },
  /* watch: {
    selectId(newval) {
      console.log(newval);
    },
  }, */
};
</script>

<style>
</style>

BaseSelect.vue

html 复制代码
<template>
  <div>
    <!-- 
      表单类组件封装 & v-model 简化代码
      实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
      
      下拉菜单 → value 和 input 事件的语法糖
      model 不能用 → 双向绑定,代表要修改数据 → cityId来自于父组件,子组件不能直接修改
     -->
    <select :value="cityId" @change="changeId">
      <option value="101">北京</option>
      <option value="102">上海</option>
      <option value="103">武汉</option>
      <option value="104">广州</option>
      <option value="105">深圳</option>
    </select>
  </div>
</template>

<script>
export default {
  props: {
    cityId: String,
  },
  methods: {
    changeId(e) {
      // 通知父组件修改数据 → 当前下拉菜单的value值
      // this.$emit("changeId", e.target.value);

      this.$emit("input", e.target.value);
    },
  },
};
</script>

<style>
</style>

1.2 sync 修饰符

App.vue
html 复制代码
<template>
  <!-- 12-src-sync修饰符 -->
  <div class="app">
    <button @click="isShow = true">退出按钮</button>
    <!-- isShow.sync  => :isShow="isShow" @update:isShow="isShow=$event" -->
    <!-- <BaseDialog v-show="isShow"></BaseDialog> -->

    <!-- <BaseDialog :isShow="isShow" @closeDialog="handleClose"></BaseDialog> -->

    <!--
      .sync 修饰符 → 可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
      应用场景:封装弹框类的基础组件, visible属性 true显示 false隐藏
      .sync修饰符 就是 :属性名 和 @update:属性名 合写
    -->
    <BaseDialog :isShow.sync="isShow"></BaseDialog>
  </div>
</template>

<script>
import BaseDialog from "./components/BaseDialog.vue";
export default {
  data() {
    return {
      isShow: false,
    };
  },
  methods: {
    /* handleClose(newVal) {
      this.isShow = newVal;
    }, */
  },
  components: {
    BaseDialog,
  },
};
</script>

<style>
</style>

BaseDialog.vue

html 复制代码
<template>
  <div class="base-dialog-wrap" v-show="isShow">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button class="close" @click="close">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button>确认</button>
        <button>取消</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    isShow: Boolean,
  },
  methods: {
    close() {
      // this.$emit("closeDialog", false);

      this.$emit("update:isShow", false);
    },
  },
};
</script>

<style scoped>
.base-dialog-wrap {
  width: 300px;
  height: 200px;
  box-shadow: 2px 2px 2px 2px #ccc;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 0 10px;
}
.base-dialog .title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 2px solid #000;
}
.base-dialog .content {
  margin-top: 38px;
}
.base-dialog .title .close {
  width: 20px;
  height: 20px;
  cursor: pointer;
  line-height: 10px;
}
.footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 26px;
}
.footer button {
  width: 80px;
  height: 40px;
}
.footer button:nth-child(1) {
  margin-right: 10px;
  cursor: pointer;
}
</style>

1.3 ref 和 $refs

1.3.1 获取 dom

App.vue
html 复制代码
<template>
  <!-- 13-src-ref-DOM -->
  <div class="app">
    <div class="base-chart-box">父组件</div>
    <BaseChart></BaseChart>
  </div>
</template>

<script>
import BaseChart from "./components/BaseChart.vue";
export default {
  components: {
    BaseChart,
  },
};
</script>

<style>
.base-chart-box {
  width: 300px;
  height: 200px;
}
</style>
BaseChart.vue
html 复制代码
<template>
  <!-- 
    ref 和 $refs 
    作用:利用 ref 和 $refs 可以用于 获取 dom 元素, 或 组件实例
    特点:查找范围 → 当前组件内 (更精确稳定)
  -->
  <!-- 1. 目标标签 -- 添加 ref 属性 -->
  <div class="base-chart-box" ref="chartEl">子组件</div>
</template>

<script>
import * as echarts from "echarts";

export default {
  mounted() {
    // 基于准备好的dom,初始化echarts实例
    // document.querySelector 会查找项目中所有的元素
    // $refs只会在当前组件查找盒子
    // var myChart = echarts.init(document.querySelector('.base-chart-box'))

    // 2. 恰当时机, 通过 this.$refs.xxx, 获取目标标签
    var myChart = echarts.init(this.$refs.chartEl);
    // 绘制图表
    myChart.setOption({
      title: {
        text: "ECharts 入门示例",
      },
      tooltip: {},
      xAxis: {
        data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
      },
      yAxis: {},
      series: [
        {
          name: "销量",
          type: "bar",
          data: [5, 20, 36, 10, 10, 20],
        },
      ],
    });
  },
};
</script>

<style scoped>
.base-chart-box {
  width: 400px;
  height: 300px;
  border: 3px solid #000;
  border-radius: 6px;
}
</style>

1.3.2 获取组件

App.vue
html 复制代码
<template>
  <!-- 14-src-ref-组件 -->
  <div class="app">
    <!-- 通过这种方式,可以在父组件中直接操作子组件的方法和数据,这在需要跨组件通信时非常有用。 -->
    <!-- 1. 目标组件 -- 添加 ref 属性 -->
    <BaseForm ref="myForm"></BaseForm>
    <button @click="getValues">获取数据</button>
    <button @click="resetValues">重置数据</button>
  </div>
</template>

<script>
import BaseForm from "./components/BaseForm.vue";
export default {
  components: {
    BaseForm,
  },
  methods: {
    getValues() {
      // 2. 恰当时机, 通过 this.$refs.xxx, 获取目标组件,就可以调用组件对象里面的方法
      // this.$refs.myForm → BaseForm 组件的 Vue 实例对象
      console.log(this.$refs.myForm); // BaseForm vue对象
      console.log(this.$refs.myForm.getValues()); // {username: '12', password: '12'}
    },
    resetValues() {
      this.$refs.myForm.resetValues();
    },
  },
};
</script>

<style>
.app {
  width: 300px;
  height: 200px;
  border: 1px solid #000;
  border-radius: 10px;
  padding-top: 30px;
  padding-left: 30px;
}
button {
  margin-top: 50px;
  margin-left: 30px;
}
</style>
BaseForm.vue
html 复制代码
<template>
  <form action="">
    账号:<input type="text" v-model="username" /><br /><br />
    密码:<input type="text" v-model="password" />
  </form>
</template>

<script>
export default {
  data() {
    return {
      username: "",
      password: "",
    };
  },
  methods: {
    getValues() {
      return {
        username: this.username,
        password: this.password,
      };
    },
    resetValues() {
      this.username = "";
      this.password = "";
    },
  },
};
</script>

<style>
</style>

1.4 Vue异步更新、$nextTick

html 复制代码
<template>
  <!-- 15-src-$nextTick -->
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="ipt" />
      <button @click="hide">确认</button>
    </div>

    <div v-else>
      <span>{{ title }}</span>
      <button @click="show">编辑</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: "大标题",
      editValue: "",
      isShowEdit: false,
    };
  },
  methods: {
    // 需求:编辑标题, 编辑框自动聚焦
    show() {
      // 点击编辑,显示编辑框
      this.isShowEdit = true;

      // 让编辑框,立刻获取焦点
      // 1. 报错 Cannot read properties of undefined (reading 'focus')
      // ! Vue 是 异步更新 DOM (提升性能)
      // this.$refs.ipt.focus();

      // 2. 延迟慢 体验差
      /* setTimeout(()=>{
        this.$refs.ipt.focus();
      },1500) */

      // 3. Vue异步更新、$nextTick
      //    → 等 DOM 更新后, 才会触发执行此方法里的函数体
      // 语法: this.$nextTick(函数体)
      this.$nextTick(() => {
        this.$refs.ipt.focus();
      });
    },
    hide() {
      this.isShowEdit = false;
    },
  },
};
</script>

<style>
span {
  font-size: 30px;
  margin-right: 30px;
}
</style>

2. 自定义指令

2.1 全局&局部注册

main.js
javascript 复制代码
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

/* 
  指令介绍
  内置指令:v-html、v-if、v-bind、v-on... 这都是Vue给咱们内置的一些指令,可以直接使用
  自定义指令:同时Vue也支持让开发者,自己注册一些指令。这些指令被称为自定义指令
  每个指令都有自己各自独立的功能
*/

// 自定义指令
// 自定义指令:自己定义的指令,可以封装一些 dom 操作,扩展额外功能
// 1. 全局注册 - 语法
/* Vue.directive("指令名", {
  inserted(el) {
    // 对el标签扩展额外功能
  },
}); */
/* Vue.directive("focus", {
  // inserted 指令的生命周期钩子 → 当指令被加到标签上面自动执行的代码
  // el 使用指令的那个DOM元素
  // 自带一个形参 → 添加当前指令的标签
  inserted(el) {
    el.focus();
  },
}); */

new Vue({
  render: (h) => h(App),
}).$mount("#app");
App.vue
html 复制代码
<template>
  <!-- 01-src-自定义指令-focus -->
  <div id="app">
    <h1>自定义指令</h1>
    <!-- 
      使用指令
      注意:在使用指令的时候,一定要先注册,再使用,否则会报错
      使用指令语法:v-指令名 如:<input type="text"  v-focus/>  
      注册指令时不用加v-前缀,但使用时一定要加v-前缀
     -->
    <input type="text" v-focus />
  </div>
</template>

<script>
export default {
  // 2. 局部注册 -- 语法 指令名/'指令名'
  /* directives: {
    指令名: {
      inserted(el) {
        // 可以对 el 标签,扩展额外功能
        el.focus();
      },
    },
  }, */
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      },
    },
  },
};
</script>

<style>
</style>

2.2 指令的值

html 复制代码
<template>
  <!-- 02-src-自定义指令-值 -->
  <!-- 
    1. 通过指令的值相关语法,可以应对更复杂指令封装场景
    2. 指令值的语法:
    ① v-指令名 = "指令值",通过 等号 可以绑定指令的值
    ② 通过 binding.value 可以拿到指令的值
    ③ 通过 update 钩子,可以监听指令值的变化,进行dom更新操作
  -->
  <div id="app">
    <!-- 语法:在绑定指令时,可以通过"等号"的形式为指令 绑定 具体的参数值 -->
    <h1 v-color="color1">
      自定义指令
      <button @click="color1 = 'green'">修改颜色</button>
    </h1>
    <h1 v-color="color2">自定义指令</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      color1: "red",
      color2: "pink",
    };
  },
  // 需求:实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
  directives: {
    color: {
      // binding 对象会包含所有与该指令相关的信息
      inserted(el, binding) {
        // console.log(el);
        // console.log(binding);

        // 通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数
        // console.log(binding.value);

        el.style.color = binding.value;
      },
      update(el, binding) {
        el.style.color = binding.value;
      },
    },
  },
};
</script>

<style>
</style>

2.3 v-loading 指令封装

main.js
javascript 复制代码
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

// // 1. 全局注册指令
// Vue.directive('focus', {
//   // inserted 会在 指令所在的元素,被插入到页面中时触发
//   inserted (el) {
//     // el 就是指令所绑定的元素
//     // console.log(el);
//     el.focus()
//   }
// })

new Vue({
  render: (h) => h(App),
}).$mount("#app");
App.vue
html 复制代码
<template>
  <!-- 03-src-封装loading指令 -->
  <!-- 
    场景:实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好
    需求:封装一个 v-loading 指令,实现加载中的效果
  -->
  <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>
</template>

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

// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
  data() {
    return {
      list: [],
      isLoading: true,
    };
  },
  watch: {
    list: {
      deep: true,
      handler(newList) {
        // newList !== [] ? (this.isLoading = false) : (this.isLoading = true);
        this.isLoading = newList.length === 0;
      },
    },
  },
  async created() {
    // 1. 发送请求获取数据
    const res = await axios.get("http://hmajax.itheima.net/api/news");

    setTimeout(() => {
      // 2. 更新到 list 中,用于页面渲染 v-for
      this.list = res.data.data;
      // this.isLoading = false;
    }, 1000);
  },
  directives: {
    loading: {
      inserted(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");
      },
    },
  },
};
</script>

<style>
/* 通过css添加一个伪元素 → 假的标签 */
/* ::before:这是一个伪元素选择器,用于在目标元素的内容之前插入额外的内容。
  这个伪元素并不存在于文档树中,但它会显示在目标元素的前面。
  常用于为正在加载的资源(如图片、视频或音频)添加自定义的加载样式,以提供更丰富的用户体验和视觉反馈。 */
/* 当将.loading和::before组合使用时,就表示在具有loading类的每个元素的内容之前插入额外的内容。 */
.loading::before {
  /* 必填属性 */
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url("./loading.gif") no-repeat center;
}

.box {
  width: 800px;
  min-height: 500px;
  border: 3px solid orange;
  border-radius: 5px;
  position: relative;
  margin: 0 auto;
}
.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>

3. 插槽

3.1 默认插槽

3.2 后备内容

3.3 具名插槽

App.vue
html 复制代码
<template>
  <!-- 04-src-封装对话框组件-默认和具名插槽 -->
  <div>
    <!-- 一、默认插槽 -->
    <!-- <MyDialog>文本内容1</MyDialog> -->
    <!-- <MyDialog><h3>标题3</h3></MyDialog> -->

    <!-- 二、后备内容 -->
    <!-- <MyDialog></MyDialog> -->

    <!-- 三、具名插槽 -->
    <MyDialog>
      <template v-slot:head><h4>温馨提示</h4></template>
      <template #content>确定要删除吗?</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

html 复制代码
<template>
  <div class="dialog">
    <div class="dialog-header">
      <!-- <h3>友情提示</h3> -->
      <slot name="head"></slot>
      <span class="close">✖️</span>
    </div>

    <div class="dialog-content">
      <!-- 我是文本内容 -->
      <!-- 
      一、插槽 - 默认插槽 → 让组件内部的一些 结构 支持 自定义
        插槽基本语法:
        1. 组件内需要定制的结构部分,改用<slot></slot>占位
        2. 使用组件时, <MyDialog></MyDialog>标签内部, 传入结构替换slot
        3. 给插槽传入内容时,可以传入纯文本、html标签、组件
      -->
      <!-- <slot></slot> -->

      <!-- 
      二、插槽 - 后备内容(默认值)
        插槽后备内容:封装组件时,可以为预留的 `<slot>` 插槽提供后备内容(默认内容)。
        在 <slot> 标签内,放置内容, 作为默认显示内容 → 外部使用组件时,不传东西,则slot会显示后备内容
      -->
      <!-- <slot>默认内容</slot> -->

      <!-- 
      三、插槽 - 具名插槽 → 一个组件内有多处结构,需要外部传入标签,进行定制
        具名插槽语法:
        1. 多个slot使用name属性区分名字
        2. template配合v-slot:名字来分发对应标签
        3. v-slot:插槽名 可以简化成 #插槽名
      -->
      <slot name="content"></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;
  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>

3.4 作用域插槽

App.vue
html 复制代码
<template>
  <!--
    05-src-封装表格组件-作用域插槽
    场景:封装表格组件
    1. 父传子,动态渲染表格内容
    2. 利用默认插槽,定制操作列
    3. 删除或查看都需要用到 当前项的 id,属于组件内部的数据, 通过 作用域插槽 传值绑定,进而使用
  -->
  <div>
    <MyTable :data="list">
      <template #default="obj">
        <button @click="del(obj.id)">删除</button>
      </template>
    </MyTable>

    <MyTable :data="list2">
      <template #default="{ id, msg }">
        <button @click="fn(id, msg)">查看</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);
    },
    fn(id, msg) {
      console.log(id, msg);
    },
  },
};
</script>

MyTable.vue

html 复制代码
<template>
  <table class="my-table">
    <thead>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年纪</th>
        <th>操作</th>
      </tr>
    </thead>
    <!-- 
      四、插槽 - 作用域插槽
      作用域插槽: 定义 slot 插槽的同时, 是可以传值的。给 插槽 上可以 绑定数据,将来 使用组件时可以用。
      基本使用步骤:
      1. 给 slot 标签, 以 添加属性的方式传值
      2. 所有添加的属性, 都会被收集到一个对象中
      3. 在template中, 通过 ` #插槽名= "obj" ` 接收,默认插槽名为 default
     -->
    <tbody>
      <!-- <tr>
        <td>1</td>
        <td>小张</td>
        <td>8</td>
        <td>
          <button>删除</button>
        </td>
      </tr> -->
      <tr v-for="(item, index) in data" :key="item.id">
        <td>{{ index + 1 }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.age }}</td>
        <td>
          <!-- <button>删除</button> -->
          <slot :id="item.id" msg="普通数据"></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>

4. 综合案例 - 商品列表

App.vue

html 复制代码
<template>
  <!-- 06-src-商品列表 -->
  <!-- 
    需求说明:
    1. my-tag 标签组件封装
    (1) 双击显示输入框,输入框获取焦点
    (2) 失去焦点,隐藏输入框
    (3) 回显标签信息
    (4) 内容修改,回车 → 修改标签信息
    2. my-table 表格组件封装
    (1) 动态传递表格数据渲染
    (2) 表头支持用户自定义
    (3) 主体支持用户自定义
  -->
  <div class="table-case">
    <MyTable :data="goods">
      <template #head>
        <th>编号</th>
        <th>图片</th>
        <th>名称</th>
        <th width="100px">标签</th>
      </template>
      <!-- 拿到插槽传入的数据 解构 -->
      <template #con="{ item, index }">
        <td>{{ index + 1 }}</td>
        <td>
          <img :src="item.picture" />
        </td>
        <td>{{ item.name }}</td>
        <td>
          <MyTag v-model="item.tag"></MyTag>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from "./components/MyTable.vue";
import MyTag from "./components/MyTag.vue";
export default {
  name: "TableCase",
  data() {
    return {
      goods: JSON.parse(localStorage.getItem("newGoods")) || [
        {
          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: "儿童服饰",
        },
      ],
    };
  },
  components: {
    MyTable,
    MyTag,
  },
  watch: {
    goods: {
      deep: true,
      handler(newGoods) {
        localStorage.setItem("newGoods", JSON.stringify(newGoods));
      },
    },
  },
};
</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

html 复制代码
<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="con" :item="item" :index="index"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script>
// import MyTag from "./MyTag.vue";
export default {
  components: {
    // MyTag,
  },
  props: {
    data: Array,
  },
};
</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

html 复制代码
<template>
  <div class="my-tag">
    <input
      class="input"
      type="text"
      placeholder="输入标签"
      v-if="isEdit"
      v-focus
      @blur="isEdit = false"
      @keyup.enter="changeTag"
      :value="value"
    />
    <!-- :value 是html标签自带的属性  "value" 是props数据 -->
    <!-- dbl - double, 双击 - dblclick -->
    <div class="text" v-else @dblclick="isEdit = true">{{ value }}</div>
  </div>
</template>

<script>
// 分类标签的功能:
// input和文字互斥 → 布尔数据 v-if
// 双击文字div 显示input标签 → 自动获得焦点
// input 要数据回显 - 父子通信
// v-model = value属性和input事件的缩写
// 用户输入后,回车 → 把新的内容渲染到页面 → 数据应该同步
export default {
  props: {
    value: String,
  },
  data() {
    return {
      isEdit: false,
    };
  },
  methods: {
    changeTag(e) {
      // 通知父组件保存用户输入的数据 $emit(事件名, 用户输入的数据)
      this.$emit("input", e.target.value);
      // 显示div,隐藏输入框
      this.isEdit = false;
    },
  },
};
</script>

<style  lang="less" scoped>
// lang="less" 表示的是less语法

.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>
相关推荐
落魄实习生2 小时前
AI应用-本地模型实现AI生成PPT(简易版)
python·ai·vue·ppt
java_heartLake10 小时前
Vue3之状态管理Vuex
vue·vuex·前端状态管理
小马超会养兔子11 小时前
如何写一个数字老虎机滚轮
开发语言·前端·javascript·vue
小阳生煎13 小时前
多个Echart遍历生成 / 词图云
vue
小马超会养兔子2 天前
如何写一个转盘
开发语言·前端·vue
bpmf_fff2 天前
二八(vue2-04)、scoped、data函数、父子通信、props校验、非父子通信(EventBus、provide&inject)、v-model进阶
vue
好开心332 天前
04、Vue与Ajax
前端·ajax·前端框架·vue·js
工业互联网专业2 天前
Python毕业设计选题:基于Python的社区爱心养老管理系统设计与实现_django
python·django·vue·毕业设计·源码·课程设计
平行线也会相交3 天前
云图库平台(一)后端项目初始化
spring boot·vue·云图库平台