介绍
Dnd (Drag and Drop) 拖放组件用于列表的拖放排序。
Dnd 组件用于提供上下文;DndItem 组件用于展示拖放样式以及作为列表项容器;DndHandle 需放置在 DndItem 中,控制着拖拽区域,短按可进入拖拽状态,样式和内容可自定义。
引入
js
import Dnd from 'sard-uniapp/components/dnd/dnd.vue'
import DndItem from 'sard-uniapp/components/dnd-item/dnd-item.vue'
import DndHandle from 'sard-uniapp/components/dnd-handle/dnd-handle.vue'代码演示
基础使用
通过 v-model:list 双向绑定列表,可在拖放时修改绑定数组的元素顺序;使用默认插槽的 list 参数渲染列表,需绑定 itemInfo 和 key 到 DndItem 组件;key 和数组元素的引用一一对应,无需自行处理循环组件时的唯一性问题;data 是原始数组的元素项。
下面演示了拖拽组件和列表组件的组合使用。因每个列表项被拖拽项包围分割,没了边框,因此需使用 Divider 组件添加边框。
vue
<template>
<sar-dnd v-model:list="listData">
<template #default="{ list }">
<sar-list card>
<sar-dnd-item
v-for="({ data, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
>
<sar-list-item :title="data.title">
<template #value>
<sar-dnd-handle>
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</template>
</sar-list-item>
<sar-divider
v-if="i !== listData.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-list-item-padding-x)"
/>
</sar-dnd-item>
</sar-list>
</template>
</sar-dnd>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const listData = ref(
Array(10)
.fill(0)
.map((_, i) => {
return {
title: `标题${i}`,
}
}),
)
</script>vue
<template>
<sar-dnd v-model:list="listData">
<template #default="{ list }">
<sar-list card>
<sar-dnd-item
v-for="({ data, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
>
<sar-list-item :title="data.title">
<template #value>
<sar-dnd-handle>
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</template>
</sar-list-item>
<sar-divider
v-if="i !== listData.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-list-item-padding-x)"
/>
</sar-dnd-item>
</sar-list>
</template>
</sar-dnd>
</template>
<script setup lang="js">
import { ref } from "vue";
const listData = ref(
Array(10).fill(0).map((_, i) => {
return {
title: `\u6807\u9898${i}`
};
})
);
</script>表单列表
下面演示了拖放组件组合表单组件实现的表单列表增删改以及拖放排序的效果。
vue
<template>
<sar-form
ref="formRef"
:model="formModel"
star-position="right"
direction="vertical"
>
<sar-form-item label="活动名称" name="activity" required>
<sar-input
v-model="formModel.activity"
inlaid
placeholder="请输入活动名称"
clearable
/>
</sar-form-item>
<sar-form-item
label="活动奖品"
name="awards"
:rules="{
type: 'array',
required: true,
}"
:class="{ 'pb-0': formModel.awards.length > 0 }"
>
<sar-dnd
v-if="formModel.awards.length > 0"
v-model:list="formModel.awards"
>
<template #default="{ list }">
<sar-dnd-item
v-for="({ key, itemInfo, data }, i) in list"
:key="key"
:item-info="itemInfo"
style="
margin: 0 calc(var(--sar-form-item-padding-x) * -1);
padding: 0 var(--sar-form-item-padding-x);
"
>
<view class="flex items-center gap-20 py-20">
<sar-button
type="text"
theme="danger"
icon="trash"
size="small"
@click="removeAward(i)"
/>
<sar-form-item
:name="['awards', i, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<sar-picker-input
v-model="data.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</sar-form-item>
<sar-form-item
:name="['awards', i, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<sar-stepper v-model="data.num" :min="1" placeholder="数量" />
</sar-form-item>
<sar-dnd-handle class="ml-20 stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
<sar-divider
v-if="i !== formModel.awards.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-form-item-padding-x)"
/>
</sar-dnd-item>
</template>
</sar-dnd>
<view v-else>无</view>
</sar-form-item>
<sar-form-item>
<sar-button type="outline" class="mb-20" @click="addAward">
新增奖品
</sar-button>
<sar-button class="mb-20" @click="submitForm(formRef)">提交</sar-button>
<sar-button type="mild" @click="resetForm(formRef)">重置</sar-button>
</sar-form-item>
</sar-form>
</template>
<script setup lang="ts">
import { type FormExpose, toast, uniqid } from 'sard-uniapp'
import { reactive, ref } from 'vue'
const awardList = ['台式机', '笔记本', '平板', '手机', '耳机']
const formRef = ref<FormExpose>()
interface Award {
name: string
num: number | null
key: string
}
const formModel = reactive<{
activity: string
awards: Award[]
}>({
activity: '',
awards: [],
})
const addAward = () => {
formModel.awards.push(
reactive({
name: '',
num: null,
key: uniqid(),
}),
)
}
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>
<sar-form
ref="formRef"
:model="formModel"
star-position="right"
direction="vertical"
>
<sar-form-item label="活动名称" name="activity" required>
<sar-input
v-model="formModel.activity"
inlaid
placeholder="请输入活动名称"
clearable
/>
</sar-form-item>
<sar-form-item
label="活动奖品"
name="awards"
:rules="{
type: 'array',
required: true,
}"
:class="{ 'pb-0': formModel.awards.length > 0 }"
>
<sar-dnd
v-if="formModel.awards.length > 0"
v-model:list="formModel.awards"
>
<template #default="{ list }">
<sar-dnd-item
v-for="({ key, itemInfo, data }, i) in list"
:key="key"
:item-info="itemInfo"
style="
margin: 0 calc(var(--sar-form-item-padding-x) * -1);
padding: 0 var(--sar-form-item-padding-x);
"
>
<view class="flex items-center gap-20 py-20">
<sar-button
type="text"
theme="danger"
icon="trash"
size="small"
@click="removeAward(i)"
/>
<sar-form-item
:name="['awards', i, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<sar-picker-input
v-model="data.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</sar-form-item>
<sar-form-item
:name="['awards', i, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<sar-stepper v-model="data.num" :min="1" placeholder="数量" />
</sar-form-item>
<sar-dnd-handle class="ml-20 stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
<sar-divider
v-if="i !== formModel.awards.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-form-item-padding-x)"
/>
</sar-dnd-item>
</template>
</sar-dnd>
<view v-else>无</view>
</sar-form-item>
<sar-form-item>
<sar-button type="outline" class="mb-20" @click="addAward">
新增奖品
</sar-button>
<sar-button class="mb-20" @click="submitForm(formRef)">提交</sar-button>
<sar-button type="mild" @click="resetForm(formRef)">重置</sar-button>
</sar-form-item>
</sar-form>
</template>
<script setup lang="js">
import { toast, uniqid } from "sard-uniapp";
import { reactive, ref } from "vue";
const awardList = ["\u53F0\u5F0F\u673A", "\u7B14\u8BB0\u672C", "\u5E73\u677F", "\u624B\u673A", "\u8033\u673A"];
const formRef = ref();
const formModel = reactive({
activity: "",
awards: []
});
const addAward = () => {
formModel.awards.push(
reactive({
name: "",
num: null,
key: uniqid()
})
);
};
const removeAward = (index) => {
formModel.awards.splice(index, 1);
};
const submitForm = (formEl) => {
formEl?.validate().then(() => {
console.log(formModel);
toast("\u63D0\u4EA4\u6210\u529F");
}).catch(() => {
toast("\u63D0\u4EA4\u5931\u8D25");
});
};
const resetForm = (formEl) => {
formEl?.reset();
};
</script>嵌套拖拽
拖拽里面也可以包含嵌套。
WARNING
小程序端暂不支持嵌套拖拽,如果你有解决方案,可以提交 PR。
vue
<template>
<sar-form-plain ref="formRef" :model="formModel" star-position="right">
<sar-dnd v-model:list="formModel.activities">
<template #default="{ list }">
<sar-dnd-item
v-for="({ data: activity, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
style="border-radius: var(--sar-card-border-radius)"
class="mb-20"
>
<sar-card>
<template #title>
<view class="flex items-center gap-20">
<sar-button
type="mild"
theme="danger"
icon="trash"
size="small"
@click="removeActivity(i)"
/>
<sar-form-item
label="活动名称"
:name="['activities', i, 'name']"
inlaid
required
>
<sar-input
v-model="activity.name"
inlaid
placeholder="请输入活动名称"
clearable
/>
</sar-form-item>
<sar-dnd-handle class="stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
</template>
<sar-form-item
inlaid
direction="vertical"
label="活动奖品"
:name="['activities', i, 'awards']"
:rules="{
type: 'array',
required: true,
}"
:class="{ 'pb-0': activity.awards.length > 0 }"
>
<sar-dnd
v-if="activity.awards.length > 0"
v-model:list="activity.awards"
>
<template #default="{ list }">
<sar-dnd-item
v-for="({ itemInfo, key, data: award }, j) in list"
:key="key"
:item-info="itemInfo"
style="
margin: 0 calc(var(--sar-form-item-padding-x) * -1);
padding: 0 var(--sar-form-item-padding-x);
"
>
<view class="flex items-center gap-20 py-20">
<sar-button
type="text"
theme="danger"
icon="trash"
size="small"
@click="removeAward(activity.awards, j)"
/>
<sar-form-item
:name="['activities', i, 'awards', j, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<sar-picker-input
v-model="award.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</sar-form-item>
<sar-form-item
:name="['activities', i, 'awards', j, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<sar-stepper
v-model="award.num"
:min="1"
size="small"
placeholder="数量"
/>
</sar-form-item>
<sar-dnd-handle class="stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
<sar-divider
v-if="i !== activity.awards.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-form-item-padding-x)"
/>
</sar-dnd-item>
</template>
</sar-dnd>
<view v-else>无</view>
</sar-form-item>
<sar-button
class="mt-20"
type="outline"
size="small"
@click="addAward(activity.awards)"
>
新增奖品
</sar-button>
</sar-card>
</sar-dnd-item>
</template>
</sar-dnd>
<sar-card>
<sar-button type="outline" class="mb-20" @click="addActivity">
新增活动
</sar-button>
<sar-button class="mb-20" @click="submitForm()">提交</sar-button>
<sar-button type="mild" @click="resetForm()">重置</sar-button>
</sar-card>
</sar-form-plain>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { toast, type FormExpose } from 'sard-uniapp'
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>
<sar-form-plain ref="formRef" :model="formModel" star-position="right">
<sar-dnd v-model:list="formModel.activities">
<template #default="{ list }">
<sar-dnd-item
v-for="({ data: activity, key, itemInfo }, i) in list"
:item-info="itemInfo"
:key="key"
style="border-radius: var(--sar-card-border-radius)"
class="mb-20"
>
<sar-card>
<template #title>
<view class="flex items-center gap-20">
<sar-button
type="mild"
theme="danger"
icon="trash"
size="small"
@click="removeActivity(i)"
/>
<sar-form-item
label="活动名称"
:name="['activities', i, 'name']"
inlaid
required
>
<sar-input
v-model="activity.name"
inlaid
placeholder="请输入活动名称"
clearable
/>
</sar-form-item>
<sar-dnd-handle class="stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
</template>
<sar-form-item
inlaid
direction="vertical"
label="活动奖品"
:name="['activities', i, 'awards']"
:rules="{
type: 'array',
required: true,
}"
:class="{ 'pb-0': activity.awards.length > 0 }"
>
<sar-dnd
v-if="activity.awards.length > 0"
v-model:list="activity.awards"
>
<template #default="{ list }">
<sar-dnd-item
v-for="({ itemInfo, key, data: award }, j) in list"
:key="key"
:item-info="itemInfo"
style="
margin: 0 calc(var(--sar-form-item-padding-x) * -1);
padding: 0 var(--sar-form-item-padding-x);
"
>
<view class="flex items-center gap-20 py-20">
<sar-button
type="text"
theme="danger"
icon="trash"
size="small"
@click="removeAward(activity.awards, j)"
/>
<sar-form-item
:name="['activities', i, 'awards', j, 'name']"
:rules="{
required: true,
message: '请选择奖品',
}"
inlaid
>
<sar-picker-input
v-model="award.name"
:columns="awardList"
clearable
placeholder="奖品"
/>
</sar-form-item>
<sar-form-item
:name="['activities', i, 'awards', j, 'num']"
:rules="{
required: true,
message: '请输入数量',
}"
inlaid
>
<sar-stepper
v-model="award.num"
:min="1"
size="small"
placeholder="数量"
/>
</sar-form-item>
<sar-dnd-handle class="stext-tertiary">
<sar-icon name="list" size="36rpx" />
</sar-dnd-handle>
</view>
<sar-divider
v-if="i !== activity.awards.length - 1"
class="absolute my-0 left-0 right-0 bottom-0"
style="margin: 0 var(--sar-form-item-padding-x)"
/>
</sar-dnd-item>
</template>
</sar-dnd>
<view v-else>无</view>
</sar-form-item>
<sar-button
class="mt-20"
type="outline"
size="small"
@click="addAward(activity.awards)"
>
新增奖品
</sar-button>
</sar-card>
</sar-dnd-item>
</template>
</sar-dnd>
<sar-card>
<sar-button type="outline" class="mb-20" @click="addActivity">
新增活动
</sar-button>
<sar-button class="mb-20" @click="submitForm()">提交</sar-button>
<sar-button type="mild" @click="resetForm()">重置</sar-button>
</sar-card>
</sar-form-plain>
</template>
<script setup lang="js">
import { reactive, ref } from "vue";
import { toast } from "sard-uniapp";
const awardList = ["\u53F0\u5F0F\u673A", "\u7B14\u8BB0\u672C", "\u5E73\u677F", "\u624B\u673A", "\u8033\u673A"];
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("\u63D0\u4EA4\u6210\u529F");
}).catch(() => {
toast("\u63D0\u4EA4\u5931\u8D25");
});
};
const resetForm = () => {
formRef.value?.reset();
};
</script>API
DndProps<T>
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
| 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
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
| item-info | 组件根元素样式 | DndItemInfo | - |
DndItemSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义默认内容 | - |
DndHandleProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| root-class | 组件根元素类名 | string | - |
| root-style | 组件根元素样式 | StyleValue | - |
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
}主题定制
CSS 变量
scss
page,
.sar-portal {
--sar-dnd-duration: var(--sar-duration);
--sar-dnd-dragging-shadow: var(--sar-shadow-dragging);
}