案例-商品列表

1.3k 词

案例-商品列表#

效果:
024-案例05商品列表_cut_1700619888264

需求说明#

  1. my-tag标签组件封装
    1. 双击显示输入框,输入框获取焦点
    2. 失去焦点,隐藏输入框
    3. 回显标签信息
    4. 内容修改,回车修改标签信息
  2. my-table表格组件封装
    1. 动态传递表格数据渲染
    2. 表头支持用户自定义
    3. 主体支持用户自定义

实现#

my-tag组件封装#

  1. 双击显示输入框,输入框获取焦点
    标题及输入框用v-ifv-else通过编辑状态的修改,来控制二者的显示隐藏
    标题盒子绑定双击事件@dblclick,修改编辑状态为true
    输入框获得焦点:
    1. $nextTick+$refs获取到 dom,进行focus获取焦点
    2. 通过全局注册自定义指令v-focus的方式实现
1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
// 全局注册指令focus
Vue.directive('focus', {
inserted(el) {
console.log(el);
el.focus();
},
});
new Vue({
render: h => h(App),
}).$mount('#app');
  1. 失去焦点,隐藏输入框
    输入框失焦绑定blur事件修改编辑状态为false即可
  2. 回显标签信息
    回显的标签信息是父组件传递过来的
    v-model实现功能(简化代码)v-model=>:value+@input
    组件内部通过props接收,:value设置给输入框
  3. 内容修改,回车修改标签信息
    @keyup.enter触发事件$emit('input',e.target.value)

my-table 组件封装#

  1. 动态传递表格数据渲染
    表格数据不能写死,数据来自外部
    外部使用表格组件时,用v-bind绑定要传递的数据,组件内以props接收使用
  2. 表头支持用户自定义
  3. 主体支持用户自定义
    为了支持表头及主体的自定义,使用具名插槽,将表格结构移至组件外部
    主体要用到的数据使用作用域插槽,以添加属性的方式传值

完整代码:
App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<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><img :src="item.picture" /></td>
<td>{{ item.name }}</td>
<td>
<!-- 标签组件 -->
<MyTag v-model="item.tag"></MyTag>
</td>
</template>
</MyTable>
</div>
</template>

<script>
// my-tag标签组件封装
// 1.创建组件-初始化
// 2.实现功能
// 1)双击显示,自动聚焦
// 2)失去焦点,隐藏输入框
// 3)回显标签信息
// 4)内容修改完成,回车,修改标签信息
import MyTag from './components/MyTag.vue';

// my-table组件封装
// 1.数据不能写死,动态传递表格渲染的数据 props
// 2.结构不能写死-多处结构自定义--具名插槽
// 1)表头支持自定义
// 2)主体支持自定义
import MyTable from './components/MyTable.vue';
export default {
name: 'TableCase',
components: {
MyTag,
MyTable,
},
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>

main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
// 全局注册指令focus
Vue.directive('focus', {
inserted(el) {
console.log(el);
el.focus();
},
});
new Vue({
render: h => h(App),
}).$mount('#app');

标签组件 MyTag.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template lang="">
<div class="my-tag">
<!-- 2)失焦隐藏输入框,绑定blur事件,修改编辑状态即可 -->
<input v-if="isEdit" v-focus ref="inp" @blur="isEdit = false" :value="value" @keyup.enter="handleEnter" class="input" type="text" placeholder="输入标签" />
<!-- 1)双击显示输入框,自动聚焦 -->
<div v-else @dblclick="handleClick" class="text">{{ value }}</div>
</div>
</template>
<script>
export default {
props: {
value: String,
},
data() {
return {
isEdit: false,
};
},
methods: {
handleClick() {
// 1)双击显示输入框,修改isEdit为true即可
this.isEdit = true;
// Vue是异步dom更新,所以编辑状态修改后dom还未更新完就聚焦,不可能成功
// this.$refs.inp.focus();
// 方法1,$nextTick配合$refs
// 等dom更新完了,再获取焦点,用$nextTick
// this.$nextTick(() => {
// this.$refs.inp.focus();
// });
// 方法2,将focus封装为自定义指令
// 方法1较麻烦,每个输入框都要单独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>

表格组件 MyTable.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template lang="">
<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,
},
},
};
</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>