vue-插槽作用域实用场景
- 1.插槽
- 1.1 自定义列表渲染
- 1.2 数据表格组件
- 1.3 树形组件
- 1.4 表单验证组件
- 1.5 无限滚动组件
1.插槽
插槽感觉知道有这个东西,但是挺少用过的,每次看到基本都会再去看一遍用法和概念。但是在项目里,自己还是没有用到过。总结下一些可能用到的场景,下次直接这样写了。
1.1 自定义列表渲染
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { reactive } from 'vue';
const myItems = reactive(
[
{
name: 'A',
age: 18
},
{
name: 'B',
age: 19
},
{
name: 'C',
age: 20
}
]
)
</script>
<template>
<HelloWorld :items="myItems">
<template v-slot:default="slotProps">
<span>{{ slotProps.item.name }}</span>
<button @click="doSomething(slotProps.item)">操作</button>
</template>
</HelloWorld>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item"></slot>
</li>
</ul>
</template>
效果:
拓展性很强。
1.2 数据表格组件
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { reactive } from 'vue';
const tableData = reactive(
[
{
name: 'A',
age: 18
},
{
name: 'B',
age: 19
},
{
name: 'C',
age: 20
}
]
)
const columns = reactive(
[
{
label: '姓名',
key: 'name'
},
{
label: '年龄',
key: 'age'
},
{
label: '性别',
key: 'sex'
},
{ key: 'actions', label: '操作' }
]
)
</script>
<template>
<HelloWorld :columns="columns" :data="tableData">
<template v-slot:actions="{ row }">
<button @click="edit(row)">编辑</button>
<button @click="del(row)">删除</button>
</template>
</HelloWorld>
</template>
<template>
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key">
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="column in columns" :key="column.key">
<slot :name="column.key" :row="row">
{{ row[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
columns: Array,
data: Array,
});
</script>
效果:
1.3 树形组件
<template>
<div class="tree-node">
<slot :node="node" :toggle="toggle" :expandTree="expandTree" name="trangle">
<span v-if="node.children.length > 0" @click="toggle">{{ expanded ? '▼' : '▶' }}</span>
</slot>
<slot :node="node" :toggle="toggle" :expandTree="expandTree">
<div>
{{ node.label }}
</div>
</slot>
<div v-if="expanded" class="tree-node-children">
<HelloWorld v-for="child in node.children" :key="child.id" :node="child">
<template v-slot="childSlotProps">
<slot v-bind="childSlotProps"></slot>
</template>
</HelloWorld>
</div>
</div>
</template>
<script setup>
import HelloWorld from './HelloWorld.vue'
import { defineProps, ref } from 'vue';
const props = defineProps({
node: {
type: Object,
required: true
}
})
const expanded = ref(false)
const toggle = () => {
console.log(999)
expanded.value = !expanded.value
}
const expandTree = () => {
expanded.value = true
}
</script>
<style>
.tree-node {
position: relative;
cursor: pointer;
}
.tree-node-children {
position: relative;
padding-left: 20px;
/* 这个值决定了每一层的缩进量 */
}
.tree-node>span {
margin-left: 0;
}
</style>
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { reactive } from 'vue';
const rootNode = reactive({
id: 1,
label: '根节点',
children: [
{
id: 2, label: '子节点1', children: [
{ id: 3, label: '子节点1-1', children: [] }
]
},
{ id: 4, label: '子节点2', children: [] }
]
})
const addChild = (node, expandTree) => {
const newId = Date.now()
expandTree()
node.children.push({
id: newId,
label: `新子节点${newId}`,
children: []
})
}
</script>
<template>
<HelloWorld :node="rootNode">
<template v-slot="{ node, toggle, expandTree }">
<span @click="toggle">{{ node.label }}</span>
<button @click="addChild(node, expandTree)">添加子节点</button>
</template>
</HelloWorld>
</template>
效果:
1.4 表单验证组件
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue'
const props = defineProps({
rules: {
type: Array,
default: () => []
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const value = ref(props.modelValue)
const error = ref('')
const validate = () => {
for (const rule of props.rules) {
if (!rule.validate(value.value)) {
error.value = rule.message
return false
}
}
error.value = ''
return true
}
watch(() => props.modelValue, (newValue) => {
value.value = newValue
})
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
</script>
<template>
<div class="a">
<slot :value="value" :error="error" :validate="validate"></slot>
<span v-if="error">{{ error }}</span>
</div>
</template>
<style lang="scss" scoped>
.a{
position: relative;
span{
color: red;
position: absolute;
left: 0;
bottom: -24px;
font-size: 14px;
}
}</style>
//使用
<script setup>
import FormField from './components/HelloWorld.vue'
import { ref } from 'vue';
const email = ref('')
const emailRules = [
{
validate: (value) => /.+@.+\..+/.test(value),
message: '请输入有效的电子邮件地址'
}
]
</script>
<template>
<FormField :rules="emailRules" v-model="email">
<template v-slot="{ value, error, validate }">
邮箱
<input :value="value" @input="$event => { email = $event.target.value; validate(); }"
:class="{ 'is-invalid': error }" />
</template>
</FormField>
</template>
<style >
input{
outline: none;
}
.is-invalid:focus {
border-color: red;
}
</style>
效果:
1.5 无限滚动组件
<script setup>
import { ref, onMounted, defineProps } from 'vue'
const props = defineProps({
fetchItems: {
type: Function,
required: true
}
})
const visibleItems = ref([])
const loading = ref(false)
const handleScroll = async (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.target
if (scrollTop + clientHeight >= scrollHeight - 20 && !loading.value) {
loading.value = true
const newItems = await props.fetchItems()
visibleItems.value = [...visibleItems.value, ...newItems]
loading.value = false
}
}
onMounted(async () => {
visibleItems.value = await props.fetchItems()
})
</script>
<template>
<div @scroll="handleScroll" style="height: 200px; overflow-y: auto;">
<slot :items="visibleItems"></slot>
<div v-if="loading">加载中...</div>
</div>
</template>
//使用
<script setup>
import InfiniteScroll from './components/HelloWorld.vue'
import { ref } from 'vue';
let page = 0
const fetchMoreItems = async () => {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
page++
return Array.from({ length: 10 }, (_, i) => ({
id: page * 10 + i,
content: `Item ${page * 10 + i}`
}))
}
</script>
<template>
<InfiniteScroll :fetch-items="fetchMoreItems">
<template #default="{ items }">
<div v-for="item in items" :key="item.id">
{{ item.content }}
</div>
</template>
</InfiniteScroll>
</template>