介绍
Dnd (Drag and Drop) 拖放组件用于列表的拖放排序。
Dnd 组件用于提供上下文;DndItem 组件用于展示拖放样式以及作为列表项容器;DndHandle 需放置在 DndItem 中,控制着拖拽区域,短按可进入拖拽状态,样式和内容可自定义。
Dnd 和 DndItem 都不会添加额外元素。
代码演示
基础使用
通过 v-model:list 双向绑定列表,可在拖放时修改绑定数组的元素顺序;使用默认插槽的 list 参数渲染列表,需绑定 itemInfo 和 key 到 DndItem 组件;key 和数组元素的引用一一对应,无需自行处理循环组件时的唯一性问题;data 是原始数组的元素项。
下面演示了拖拽组件和列表组件的组合使用。
vue
<template>
<s-list card>
<s-dnd v-model:list="listData" #default="{ list }">
<s-dnd-item v-for="{ data, key, itemInfo } in list" :item-info="itemInfo" :key="key">
<s-list-item :title="data.title">
<template #value>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</template>
</s-list-item>
</s-dnd-item>
</s-dnd>
</s-list>
</template>
<script setup lang="ts">
import { List } from '@sard/icons'
import { ref } from 'vue'
const listData = ref(
Array(10)
.fill(0)
.map((_, i) => {
return {
title: `标题${i}`,
}
}),
)
</script>vue
<template>
<s-list card>
<s-dnd v-model:list="listData" #default="{ list }">
<s-dnd-item v-for="{ data, key, itemInfo } in list" :item-info="itemInfo" :key="key">
<s-list-item :title="data.title">
<template #value>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</template>
</s-list-item>
</s-dnd-item>
</s-dnd>
</s-list>
</template>
<script setup lang="js">
import { List } from '@sard/icons'
import { ref } from 'vue'
const listData = ref(
Array(10)
.fill(0)
.map((_, i) => {
return { title: `标题${i}` }
}),
)
</script>表单列表
下面演示了拖放组件组合表单组件实现的表单列表增删改以及拖放排序的效果。
vue
<template>
<s-form ref="formRef" :model="formModel" star-position="end" direction="vertical">
<s-form-item label="活动名称" name="activity" required>
<s-input v-model="formModel.activity" inlaid placeholder="请输入活动名称" clearable />
</s-form-item>
<s-form-item
label="活动奖品"
name="awards"
:rules="{
type: 'array',
required: true,
}"
>
<s-dnd v-if="formModel.awards.length > 0" v-model:list="formModel.awards" #default="{ list }">
<s-dnd-item v-for="({ key, itemInfo, data }, i) in list" :key="key" :item-info="itemInfo">
<div class="flex items-center gap-3 py-1">
<s-button variant="text" color="danger" size="small" @click="removeAward(i)" #icon>
<Trash />
</s-button>
<s-form-item
:name="['awards', i, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
class="flex-1"
inlaid
>
<s-picker-input
v-model="data.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</s-form-item>
<s-form-item
:name="['awards', i, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<s-stepper v-model="data.num" :min="1" size="small" placeholder="数量" />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</s-dnd-item>
</s-dnd>
<div v-else>无</div>
</s-form-item>
<s-form-item>
<s-button variant="outlined" class="mb-2" @click="addAward">新增奖品</s-button>
<s-button class="mb-2" @click="submitForm(formRef)">提交</s-button>
<s-button variant="outlined" color="secondary" @click="resetForm(formRef)">重置</s-button>
</s-form-item>
</s-form>
</template>
<script setup lang="ts">
import { List, Trash } from '@sard/icons'
import { type FormExpose, toast } from 'sard'
import { reactive, ref } from 'vue'
const awardList = ['台式机', '笔记本', '平板', '手机', '耳机']
const formRef = ref<FormExpose>()
interface Award {
name: string
num: number | null
}
const formModel = reactive<{
activity: string
awards: Award[]
}>({
activity: '',
awards: [],
})
const addAward = () => {
formModel.awards.push(
reactive({
name: '',
num: null,
}),
)
}
const removeAward = (index: number) => {
formModel.awards.splice(index, 1)
}
const submitForm = (formEl?: FormExpose) => {
formEl
?.validate()
.then(() => {
console.log(formModel)
toast('提交成功')
})
.catch(() => {
toast('提交失败')
})
}
const resetForm = (formEl?: FormExpose) => {
formEl?.reset()
}
</script>vue
<template>
<s-form ref="formRef" :model="formModel" star-position="end" direction="vertical">
<s-form-item label="活动名称" name="activity" required>
<s-input v-model="formModel.activity" inlaid placeholder="请输入活动名称" clearable />
</s-form-item>
<s-form-item
label="活动奖品"
name="awards"
:rules="{
type: 'array',
required: true,
}"
>
<s-dnd v-if="formModel.awards.length > 0" v-model:list="formModel.awards" #default="{ list }">
<s-dnd-item v-for="({ key, itemInfo, data }, i) in list" :key="key" :item-info="itemInfo">
<div class="flex items-center gap-3 py-1">
<s-button variant="text" color="danger" size="small" @click="removeAward(i)" #icon>
<Trash />
</s-button>
<s-form-item
:name="['awards', i, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
class="flex-1"
inlaid
>
<s-picker-input
v-model="data.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</s-form-item>
<s-form-item
:name="['awards', i, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<s-stepper v-model="data.num" :min="1" size="small" placeholder="数量" />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</s-dnd-item>
</s-dnd>
<div v-else>无</div>
</s-form-item>
<s-form-item>
<s-button variant="outlined" class="mb-2" @click="addAward">新增奖品</s-button>
<s-button class="mb-2" @click="submitForm(formRef)">提交</s-button>
<s-button variant="outlined" color="secondary" @click="resetForm(formRef)">重置</s-button>
</s-form-item>
</s-form>
</template>
<script setup lang="js">
import { List, Trash } from '@sard/icons'
import { toast } from 'sard'
import { reactive, ref } from 'vue'
const awardList = ['台式机', '笔记本', '平板', '手机', '耳机']
const formRef = ref()
const formModel = reactive({
activity: '',
awards: [],
})
const addAward = () => {
formModel.awards.push(
reactive({
name: '',
num: null,
}),
)
}
const removeAward = (index) => {
formModel.awards.splice(index, 1)
}
const submitForm = (formEl) => {
formEl
?.validate()
.then(() => {
console.log(formModel)
toast('提交成功')
})
.catch(() => {
toast('提交失败')
})
}
const resetForm = (formEl) => {
formEl?.reset()
}
</script>嵌套拖拽
拖拽里面也可以嵌套拖拽。
vue
<template>
<s-form-plain ref="formRef" :model="formModel" star-position="end">
<s-dnd v-model:list="formModel.activities" #default="{ list }">
<s-dnd-item
v-for="({ data: activity, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
class="mb-3"
>
<s-card>
<template #title>
<div class="flex items-center gap-5">
<s-button
variant="filled"
color="danger"
size="small"
@click="removeActivity(i)"
#icon
>
<Trash />
</s-button>
<s-form-item label="活动名称" :name="['activities', i, 'name']" inlaid required>
<s-input v-model="activity.name" inlaid placeholder="请输入活动名称" clearable />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</template>
<s-form-item
inlaid
direction="vertical"
label="活动奖品"
:name="['activities', i, 'awards']"
:rules="{
type: 'array',
required: true,
}"
>
<s-dnd
v-if="activity.awards.length > 0"
v-model:list="activity.awards"
#default="{ list }"
>
<s-dnd-item
v-for="({ itemInfo, key, data: award }, j) in list"
:key="key"
:item-info="itemInfo"
>
<div class="flex items-center gap-3 py-1">
<s-button
variant="text"
color="danger"
size="small"
@click="removeAward(activity.awards, j)"
#icon
>
<Trash />
</s-button>
<s-form-item
:name="['activities', i, 'awards', j, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<s-picker-input
v-model="award.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</s-form-item>
<s-form-item
:name="['activities', i, 'awards', j, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<s-stepper v-model="award.num" :min="1" size="small" placeholder="数量" />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</s-dnd-item>
</s-dnd>
<div v-else>无</div>
</s-form-item>
<s-button
class="mt-3"
block
variant="outlined"
size="small"
@click="addAward(activity.awards)"
>
新增奖品
</s-button>
</s-card>
</s-dnd-item>
</s-dnd>
<s-list card>
<s-list-item>
<s-empty v-if="formModel.activities.length === 0" size="small" />
</s-list-item>
<s-list-item>
<s-button block variant="outlined" class="mb-2" @click="addActivity">新增活动</s-button>
<s-button block class="mb-2" @click="submitForm()">提交</s-button>
<s-button block variant="outlined" color="secondary" @click="resetForm()">重置</s-button>
</s-list-item>
</s-list>
</s-form-plain>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { toast, type FormExpose } from 'sard'
import { List, Trash } from '@sard/icons'
interface Award {
name: string
num: number | null
}
interface Activity {
name: string
awards: Award[]
}
const awardList = ['台式机', '笔记本', '平板', '手机', '耳机']
const formRef = ref<FormExpose>()
const formModel = reactive<{
activities: Activity[]
}>({
activities: [],
})
const addActivity = () => {
formModel.activities.push(
reactive({
name: '',
awards: [],
}),
)
}
const removeActivity = (index: number) => {
formModel.activities.splice(index, 1)
}
const addAward = (awards: Award[]) => {
awards.push(
reactive({
name: '',
num: null,
}),
)
}
const removeAward = (awards: Award[], index: number) => {
awards.splice(index, 1)
}
const submitForm = () => {
formRef.value
?.validate()
.then(() => {
console.log(formModel)
toast('提交成功')
})
.catch(() => {
toast('提交失败')
})
}
const resetForm = () => {
formRef.value?.reset()
}
</script>vue
<template>
<s-form-plain ref="formRef" :model="formModel" star-position="end">
<s-dnd v-model:list="formModel.activities" #default="{ list }">
<s-dnd-item
v-for="({ data: activity, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
class="mb-3"
>
<s-card>
<template #title>
<div class="flex items-center gap-5">
<s-button
variant="filled"
color="danger"
size="small"
@click="removeActivity(i)"
#icon
>
<Trash />
</s-button>
<s-form-item label="活动名称" :name="['activities', i, 'name']" inlaid required>
<s-input v-model="activity.name" inlaid placeholder="请输入活动名称" clearable />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</template>
<s-form-item
inlaid
direction="vertical"
label="活动奖品"
:name="['activities', i, 'awards']"
:rules="{
type: 'array',
required: true,
}"
>
<s-dnd
v-if="activity.awards.length > 0"
v-model:list="activity.awards"
#default="{ list }"
>
<s-dnd-item
v-for="({ itemInfo, key, data: award }, j) in list"
:key="key"
:item-info="itemInfo"
>
<div class="flex items-center gap-3 py-1">
<s-button
variant="text"
color="danger"
size="small"
@click="removeAward(activity.awards, j)"
#icon
>
<Trash />
</s-button>
<s-form-item
:name="['activities', i, 'awards', j, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<s-picker-input
v-model="award.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</s-form-item>
<s-form-item
:name="['activities', i, 'awards', j, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<s-stepper v-model="award.num" :min="1" size="small" placeholder="数量" />
</s-form-item>
<s-dnd-handle>
<s-text color="tertiary" size="large">
<List />
</s-text>
</s-dnd-handle>
</div>
</s-dnd-item>
</s-dnd>
<div v-else>无</div>
</s-form-item>
<s-button
class="mt-3"
block
variant="outlined"
size="small"
@click="addAward(activity.awards)"
>
新增奖品
</s-button>
</s-card>
</s-dnd-item>
</s-dnd>
<s-list card>
<s-list-item>
<s-empty v-if="formModel.activities.length === 0" size="small" />
</s-list-item>
<s-list-item>
<s-button block variant="outlined" class="mb-2" @click="addActivity">新增活动</s-button>
<s-button block class="mb-2" @click="submitForm()">提交</s-button>
<s-button block variant="outlined" color="secondary" @click="resetForm()">重置</s-button>
</s-list-item>
</s-list>
</s-form-plain>
</template>
<script setup lang="js">
import { reactive, ref } from 'vue'
import { toast } from 'sard'
import { List, Trash } from '@sard/icons'
const awardList = ['台式机', '笔记本', '平板', '手机', '耳机']
const formRef = ref()
const formModel = reactive({ activities: [] })
const addActivity = () => {
formModel.activities.push(
reactive({
name: '',
awards: [],
}),
)
}
const removeActivity = (index) => {
formModel.activities.splice(index, 1)
}
const addAward = (awards) => {
awards.push(
reactive({
name: '',
num: null,
}),
)
}
const removeAward = (awards, index) => {
awards.splice(index, 1)
}
const submitForm = () => {
formRef.value
?.validate()
.then(() => {
console.log(formModel)
toast('提交成功')
})
.catch(() => {
toast('提交失败')
})
}
const resetForm = () => {
formRef.value?.reset()
}
</script>API
DndProps<T>
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| list | 列表数据 | T[] | - |
DndSlots<T>
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | {list: DndListItem<T>[] } |
DndEmits<T>
| 事件 | 描述 | 类型 |
|---|---|---|
| item-drag-start | 拖拽开始时触发 | (event: { itemIndex: number }) => void |
| item-drag-move | 拖拽项在拖拽范围内移动时触发 | (event: { itemIndex: number; insertIndex: number }) => void |
| item-drop | 拖拽被释放时触发 | (event: { itemIndex: number; insertIndex: number }) => void |
| update:list | 拖拽被释放时触发 | (list: T[]) => void |
DndItemProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| item-info | 拖拽项数据对象,必填 | DndItemInfo | - |
DndItemSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | - |
DndHandleSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | - |
DndListItem<T>
ts
export interface DndListItem<T> {
data: UnwrapRef<T>
itemInfo: DndItemInfo
key: string
}DndItemInfo
ts
export interface DndItemInfo {
offset: number
dragging: boolean
}