Appearance
BizDragSort 拖拽排序列表
通用拖拽排序容器(原生 HTML5 DnD,零第三方依赖):v-model 数组拖拽换位自动回写,支持把手拖拽、放置指示线、纵向 / 横向两种布局。行内视觉完全交给 item 作用域插槽,排序能力可与任意内容组合。
基础用法
⋮⋮待办事项12 个内容
⋮⋮快捷入口8 个内容
⋮⋮数据看板6 个内容
⋮⋮公告栏3 个内容
⋮⋮常用文档5 个内容
按住 ⋮⋮ 把手拖动调整栏目顺序(行内其他区域不触发拖拽,方便放按钮 / 链接)
handle-class 约束仅把手区域可发起拖拽——行内放按钮 / 链接 / 输入框互不干扰;item-key 稳定 key 避免拖动时组件状态错位。<script setup lang="ts">
/** 基础用法:首页栏目排序——把手拖拽,v-model 自动回写新序列 */
import { ref } from 'vue'
import { BizDragSort } from 'vue3-ep-biz-lib'
interface Column {
id: number
title: string
count: number
}
const columns = ref<Column[]>([
{ id: 1, title: '待办事项', count: 12 },
{ id: 2, title: '快捷入口', count: 8 },
{ id: 3, title: '数据看板', count: 6 },
{ id: 4, title: '公告栏', count: 3 },
{ id: 5, title: '常用文档', count: 5 },
])
</script>
<template>
<div>
<BizDragSort v-model="columns" item-key="id" handle-class="drag-handle">
<template #item="{ item }">
<div class="row">
<span class="drag-handle">⋮⋮</span>
<span class="title">{{ item.title }}</span>
<span class="count">{{ item.count }} 个内容</span>
</div>
</template>
</BizDragSort>
<div class="tip">按住 ⋮⋮ 把手拖动调整栏目顺序(行内其他区域不触发拖拽,方便放按钮 / 链接)</div>
</div>
</template>
<style scoped>
.row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
background: var(--el-fill-color-blank);
}
.drag-handle {
color: var(--el-text-color-secondary);
cursor: grab;
letter-spacing: -2px;
}
.title {
flex: 1;
font-size: 14px;
}
.count {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.tip {
margin-top: 8px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
横向排序 · 禁用 · 事件
1. 录入
2. 审核
3. 风控
4. 放款
禁用拖拽
流程步骤排序:拖动胶囊调整环节先后direction="horizontal" 渲染横排(步骤条 / 标签排序);sorted 事件携带 { list, from, to },可直接提交后端保存顺序。<script setup lang="ts">
/** 进阶:横向排序条 + sorted 事件(from/to 下标)+ 禁用态 */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizDragSort } from 'vue3-ep-biz-lib'
const steps = ref(['录入', '审核', '风控', '放款'])
const disabled = ref(false)
const onSorted = (payload: { from: number; to: number }): void => {
ElMessage.success(`步骤「${steps.value[payload.to]}」从第 ${payload.from + 1} 位移到第 ${payload.to + 1} 位`)
}
</script>
<template>
<div>
<BizDragSort
v-model="steps"
direction="horizontal"
:disabled="disabled"
@sorted="onSorted"
>
<template #item="{ item, index }">
<div class="chip">{{ index + 1 }}. {{ item }}</div>
</template>
</BizDragSort>
<div class="ops">
<ElSwitch v-model="disabled" active-text="禁用拖拽" />
<span class="tip">流程步骤排序:拖动胶囊调整环节先后</span>
</div>
</div>
</template>
<style scoped>
.chip {
padding: 6px 14px;
border: 1px solid var(--el-border-color);
border-radius: 14px;
font-size: 13px;
background: var(--el-fill-color-blank);
user-select: none;
}
.ops {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.tip {
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
菜单管理排序
⋮⋮工作台/dashboard
⋮⋮用户管理/users
⋮⋮订单中心/orders
⋮⋮商品管理/goods
⋮⋮系统设置/settings
拖拽与上移/下移按钮双通道(键盘可达);sorted 事件即时保存后端。
<script setup lang="ts">
/** 菜单管理排序:拖拽 + 上移/下移按钮双通道(无鼠标用户可达) */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizDragSort } from 'vue3-ep-biz-lib'
const menus = ref([
{ id: 1, title: '工作台', path: '/dashboard' },
{ id: 2, title: '用户管理', path: '/users' },
{ id: 3, title: '订单中心', path: '/orders' },
{ id: 4, title: '商品管理', path: '/goods' },
{ id: 5, title: '系统设置', path: '/settings' },
])
const move = (from: number, to: number): void => {
if (to < 0 || to >= menus.value.length) return
const next = [...menus.value]
const [item] = next.splice(from, 1)
if (item === undefined) return
next.splice(to, 0, item)
menus.value = next
}
const onSorted = (): void => {
ElMessage.success('新顺序已生效(sorted 事件可即时保存到后端)')
}
</script>
<template>
<BizDragSort v-model="menus" item-key="id" handle-class="handle" @sorted="onSorted">
<template #item="{ item, index }">
<div class="row">
<span class="handle">⋮⋮</span>
<span class="title">{{ item.title }}</span>
<span class="path">{{ item.path }}</span>
<span class="ops">
<ElButton size="small" text :disabled="index === 0" @click="move(index, index - 1)">上移</ElButton>
<ElButton size="small" text :disabled="index === menus.length - 1" @click="move(index, index + 1)">下移</ElButton>
</span>
</div>
</template>
</BizDragSort>
</template>
<style scoped>
.row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
}
.handle {
color: var(--el-text-color-secondary);
cursor: grab;
letter-spacing: -2px;
}
.title {
width: 90px;
font-size: 14px;
}
.path {
flex: 1;
font-size: 13px;
color: var(--el-text-color-secondary);
font-family: monospace;
}
</style>
显示代码
问卷题目排序
⋮⋮Q1
必答
⋮⋮Q2
必答
⋮⋮Q3
必答
卡片内含输入控件:handle-class 把手约束避免输入时误拖。
<script setup lang="ts">
/** 问卷题目排序:卡片内含输入控件,把手约束避免误拖 */
import { ref } from 'vue'
import { BizDragSort } from 'vue3-ep-biz-lib'
const questions = ref([
{ id: 1, text: '您的岗位是?', required: true },
{ id: 2, text: '您使用频率最高的功能?', required: false },
{ id: 3, text: '您对响应速度的评分?', required: true },
])
</script>
<template>
<BizDragSort v-model="questions" item-key="id" handle-class="q-handle">
<template #item="{ item, index }">
<div class="q">
<span class="q-handle">⋮⋮</span>
<span class="no">Q{{ index + 1 }}</span>
<ElInput v-model="item.text" size="small" style="flex: 1" placeholder="题目标题" />
<ElSwitch v-model="item.required" size="small" active-text="必答" />
</div>
</template>
</BizDragSort>
</template>
<style scoped>
.q {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
}
.q-handle {
color: var(--el-text-color-secondary);
cursor: grab;
letter-spacing: -2px;
}
.no {
font-size: 13px;
font-weight: 600;
color: var(--el-color-primary);
}
</style>
显示代码
轮播图排序
1. 618 大促
2. 新品首发
3. 会员日
4. 直播预告
5. 限时秒杀
首页轮播顺序:618 大促 → 新品首发 → 会员日 → 直播预告 → 限时秒杀
direction=horizontal 横向胶囊,实时预览首页轮播顺序。
<script setup lang="ts">
/** 轮播图排序:横向胶囊条 + 实时预览序列 */
import { computed, ref } from 'vue'
import { BizDragSort } from 'vue3-ep-biz-lib'
const banners = ref(['618 大促', '新品首发', '会员日', '直播预告', '限时秒杀'])
const preview = computed(() => banners.value.join(' → '))
</script>
<template>
<div>
<BizDragSort v-model="banners" direction="horizontal">
<template #item="{ item, index }">
<div class="banner">{{ index + 1 }}. {{ item }}</div>
</template>
</BizDragSort>
<div class="tip">首页轮播顺序:{{ preview }}</div>
</div>
</template>
<style scoped>
.banner {
padding: 8px 16px;
border: 1px solid var(--el-border-color);
border-radius: 16px;
font-size: 13px;
background: var(--el-fill-color-blank);
user-select: none;
}
.tip {
margin-top: 12px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
候选人列表(拖拽 + CRUD)
张张三88 分
李李四92 分
王王五85 分
赵赵六90 分
整行可拖 + 删除 / 追加联动,v-model 单一数据源驱动全部操作。
<script setup lang="ts">
/** 候选人面试排序:拖拽 + 删除 + 追加联动(列表 CRUD 一体) */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizDragSort } from 'vue3-ep-biz-lib'
let seed = 4
const candidates = ref([
{ id: 1, name: '张三', score: 88 },
{ id: 2, name: '李四', score: 92 },
{ id: 3, name: '王五', score: 85 },
{ id: 4, name: '赵六', score: 90 },
])
const add = (): void => {
seed += 1
candidates.value = [...candidates.value, { id: seed, name: `候选人${seed}`, score: 80 + (seed % 15) }]
}
const remove = (id: number): void => {
if (candidates.value.length <= 1) {
ElMessage.warning('至少保留一位候选人')
return
}
candidates.value = candidates.value.filter((c) => c.id !== id)
}
</script>
<template>
<div>
<BizDragSort v-model="candidates" item-key="id">
<template #item="{ item }">
<div class="c-row">
<ElAvatar :size="28">{{ item.name.slice(0, 1) }}</ElAvatar>
<span class="name">{{ item.name }}</span>
<ElTag size="small" :type="item.score >= 90 ? 'success' : 'info'">{{ item.score }} 分</ElTag>
<ElButton size="small" text type="danger" style="margin-left: auto" @click="remove(item.id)">移除</ElButton>
</div>
</template>
</BizDragSort>
<ElButton size="small" style="margin-top: 10px" @click="add">追加候选人</ElButton>
</div>
</template>
<style scoped>
.c-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
cursor: grab;
}
.name {
font-size: 14px;
}
</style>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| model-value / v-model | T[] | [] | 排序列表(泛型,拖拽完成后回写新序列) |
| item-key | string | ((item, index) => string | number) | 下标 | 行 key 取值(推荐传唯一字段) |
| handle-class | string | — | 拖拽把手 class:仅命中元素内可发起拖拽,缺省整行可拖 |
| disabled | boolean | false | 禁用拖拽(只读浏览态) |
| direction | 'vertical' | 'horizontal' | 'vertical' | 布局方向 |
| empty-text | string | '暂无内容' | 空列表占位文案,空串不渲染 |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:model-value | (list: T[]) | 排序结果回写 |
| sorted | (payload: { list, from, to }) | 一次拖拽完成(from/to 为起止下标) |
| drag-start | (index: number) | 行开始被拖拽 |
Slots
| 插槽名 | 说明 |
|---|---|
| item | 行内容(作用域:item、index),视觉完全自定义 |
Expose
| 属性/方法 | 说明 |
|---|---|
| — | 无 |