feat:拖拽排序

main
向文可 2 years ago
parent ca7751ce6a
commit 376f17fe29

@ -1,43 +1,43 @@
{
"name": "msb-shop-admin",
"author": {
"name": "向文可",
"email": "1041367524@qq.com"
},
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build:test": "vite build --mode test",
"build:preview": "vite build --mode preview",
"build:prod": "vite build --mode prod",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons": "^0.0.11",
"axios": "^0.26.1",
"dayjs": "^1.11.0",
"element-plus": "^2.1.2",
"lodash": "^4.17.21",
"qs": "^6.10.3",
"sortablejs": "^1.14.0",
"vue": "^3.2.25",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
},
"devDependencies": {
"@originjs/vite-plugin-global-style": "^1.0.2",
"@types/node": "^17.0.21",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.0",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"consola": "^2.15.3",
"less": "^4.1.2",
"unplugin-auto-import": "^0.6.4",
"unplugin-vue-components": "^0.18.0",
"vite": "^2.8.0",
"vite-plugin-remove-console": "^0.0.6",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1"
}
"name": "msb-shop-admin",
"author": {
"name": "向文可",
"email": "1041367524@qq.com"
},
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build:test": "vite build --mode test",
"build:preview": "vite build --mode preview",
"build:prod": "vite build --mode prod",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons": "^0.0.11",
"axios": "^0.26.1",
"dayjs": "^1.11.0",
"element-plus": "^2.1.2",
"lodash": "^4.17.21",
"qs": "^6.10.3",
"sortablejs": "^1.14.0",
"vue": "^3.2.25",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
},
"devDependencies": {
"@originjs/vite-plugin-global-style": "^1.0.2",
"@types/node": "^17.0.21",
"@vitejs/plugin-legacy": "^1.7.1",
"@vitejs/plugin-vue": "^2.2.0",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"consola": "^2.15.3",
"less": "^4.1.2",
"unplugin-auto-import": "^0.6.4",
"unplugin-vue-components": "^0.18.0",
"vite": "^2.8.0",
"vite-plugin-remove-console": "^0.0.6",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1"
}
}

@ -0,0 +1,293 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import { ElTable } from 'element-plus/es/components/table/index';
import 'element-plus/es/components/table/style/css';
import Sortable from 'sortablejs';
const props = defineProps({
/**
* 拖拽表格唯一标识用于区分多个拖拽区域互相拖拽时数据来源
*/
code: {
type: String,
default: '',
},
/**
* el-table原生属性表格数据代理element原生属性用于修改数据为排序后的数据
*/
data: {
type: Array,
required: true,
},
/**
* el-table原生属性唯一标识属性字段名默认id
*/
rowKey: {
type: String,
default: 'id',
},
/**
* 排序属性字段名默认sort
*/
sortKey: {
type: String,
default: 'sort',
},
/**
* 控制是否修改表格数据排序默认修改如果不修改则只改变表格渲染顺序但是真实数据顺序不变
*/
modifyData: {
type: Boolean,
default: true,
},
/**
* sortablejs原生属性分组详细用法见官方文档
* http://www.sortablejs.com/options.html#:~:text=group%EF%BC%9Astring%20or%20object
*/
group: {
type: [String, Object],
default: '',
},
/**
* sortablejs原生属性是否列表单元
*/
sort: {
type: Boolean,
default: true,
},
/**
* sortablejs原生属性是否此sortable对象是否可用
*/
disabled: {
type: Boolean,
default: false,
},
/**
* sortablejs原生属性动画持续时间
*/
animation: {
type: Number,
default: 150,
},
/**
* sortablejs原生属性可拖拽元素
*/
draggable: {
type: String,
default: 'el-table__row',
},
/**
* sortablejs原生属性元素可拖拽部分选择器默认为空表示元素任意部分都可拖拽
*/
handle: {
type: String,
default: '',
},
/**
* sortablejs原生属性幽灵元素
*/
ghostClass: {
type: String,
default: 'ghost-row',
},
/**
* sortablejs原生属性选中的元素
*/
chosenClass: {
type: String,
default: 'chosen-row',
},
/**
* sortablejs原生属性拖拽的元素
*/
dragClass: {
type: String,
default: 'drag-row',
},
/**
* sortablejs原生属性不允许拖拽元素选择器
*/
filter: {
type: String,
default: '.ignore-drag-sort',
},
});
const attrs = useAttrs();
const slots = useSlots();
const emits = defineEmits(['row-sort', 'row-add', 'row-remove', 'row-clone', 'expand-change']);
//
const uid = 'sortableTable' + getCurrentInstance().uid;
const { proxy } = getCurrentInstance();
// ref
const sortting = ref(false);
const refsTable = ref(null);
const expandRowKeys = ref([]);
const handleExpandChange = (row, expandRows) => {
expandRowKeys.value = expandRows.map((item) => item[props.rowKey]);
emits('expand-change', row, expandRows);
};
// sortablejs
let sortable = ref(null);
// sortablejs
const handleInit = () => {
//
const el = document.querySelector(`[sort-id='${uid}'] :not(.el-table) tbody`);
if (el) {
sortable.value = new Sortable(el, {
group: props.group,
sort: props.sort,
disabled: props.disabled,
animation: props.animation,
draggable: '.' + props.draggable,
handle: props.handle,
ghostClass: props.ghostClass,
chosenClass: props.chosenClass,
dragClass: props.dragClass,
filter: props.filter,
onUpdate(e) {
if (typeof e.newIndex === 'number') {
if (props.modifyData) {
//
const row = unref(props.data).splice(e.oldIndex, 1)[0];
unref(props.data).splice(e.newIndex, 0, row);
//
unref(props.data).forEach((item, index) => {
item[props.sortKey] = index + 1;
});
proxy.$nextTick(() => {
//
proxy.$nextTick(() => {
proxy.$forceUpdate();
//
emits('row-sort', e.newIndex, e.oldIndex, e);
});
});
} else {
//
emits('row-sort', e.newIndex, e.oldIndex, e);
}
}
},
setData(dataTransfer) {
dataTransfer.setData('code', props.code);
},
onAdd(e) {
// sort-id
let code = e.originalEvent.dataTransfer.getData('code');
//
emits(
'row-add',
e.newIndex,
e.oldIndex,
code,
(row) => {
if (props.modifyData && row) {
sortting.value = true;
unref(props.data).splice(e.newIndex, 0, row);
// v-if
proxy.$nextTick(() => {
sortting.value = false;
proxy.$nextTick(() => {
handleInit();
});
});
}
},
e
);
},
onRemove(e) {
if (typeof e.newIndex === 'number') {
if (props.modifyData && e.pullMode !== 'clone') {
unref(props.data).splice(e.oldIndex, 1);
proxy.$forceUpdate();
}
//
emits('row-remove', e.newIndex, e.oldIndex, e);
}
},
onClone(e) {
if (typeof e.newIndex === 'number') {
//
emits('row-clone', e.newIndex, e.oldIndex, e);
}
},
});
} else {
console.error('可拖拽表格ID不存在');
}
};
// sortablejs
onMounted(handleInit);
//
const handleProxy = (fnName, args) => {
return unref(refsTable)[fnName]?.apply(unref(refsTable), args);
};
const clearSelection = function () {
return handleProxy('clearSelection', arguments);
};
const toggleRowSelection = function () {
return handleProxy('toggleRowSelection', arguments);
};
const toggleAllSelection = function () {
return handleProxy('toggleAllSelection', arguments);
};
const toggleRowExpansion = function () {
return handleProxy('toggleRowExpansion', arguments);
};
const setCurrentRow = function () {
return handleProxy('setCurrentRow', arguments);
};
const clearSort = function () {
return handleProxy('clearSort', arguments);
};
const clearFilter = function () {
return handleProxy('clearFilter', arguments);
};
const doLayout = function () {
return handleProxy('doLayout', arguments);
};
const sort = function () {
return handleProxy('sort', arguments);
};
//
defineExpose({
refsTable,
clearSelection,
toggleRowSelection,
toggleAllSelection,
toggleRowExpansion,
setCurrentRow,
clearSort,
clearFilter,
doLayout,
sort,
});
const render = () => (
<ElTable
ref={refsTable}
{...props}
{...attrs}
class={{ 'sortable-table': true, [props.group]: true }}
expand-row-keys={unref(expandRowKeys)}
row-key={props.rowKey}
sort-id={unref(uid)}
onExxpandChange={() => handleExpandChange}
v-slots={unref(sortting) ? {} : slots}
/>
);
</script>
<style lang="less" scoped>
:deep(.ghost-row) {
background: #ddd;
}
:deep(.chosen-row) {
background: #eee;
}
:deep(.drag-row) {
background: #ccc;
}
</style>

@ -14,14 +14,16 @@
background-color: @color-white;
border-radius: @layout-border-radius;
box-shadow: @layout-shadow;
:deep(.el-scrollbar__view) {
width: 100%;
height: 100%;
> * {
// display: inline-block; // BFC
> :deep(.el-scrollbar__wrap) {
> .el-scrollbar__view {
width: 100%;
height: 100%;
padding: @layout-space-large;
> * {
// display: inline-block; // BFC
width: 100%;
height: 100%;
padding: @layout-space-large;
}
}
}
}

@ -0,0 +1,22 @@
export default [
{
path: '/demo',
name: 'Demo',
component: () => import('@/layouts/default.vue'),
meta: {
title: '组件示例',
icon: 'home-fill',
},
children: [
{
path: 'sortable',
name: 'SortableTableDemo',
component: () => import('@/views/demo/sortableTableDemo.vue'),
meta: {
title: '拖拽排序',
icon: 'home-fill',
},
},
],
},
];

@ -20,6 +20,10 @@ export const globalRoutes = [
},
];
// 示例模块
import demoModule from './demo';
export const demeRoutes = import.meta.env.DEV ? demoModule : [];
// 动态模块
const dynamicRoutes = [];
const modules = import.meta.globEager('./modules/*.js');

@ -1,6 +1,6 @@
import { getUserInfo, getPermission, sendSmsCode, login } from '@/api/auth';
import { ElMessage } from '@/plugins/element-plus';
import router, { reset as resetRoutes } from '@/router';
import router, { reset as resetRoutes, demeRoutes } from '@/router';
import config from '@/configs';
const viewComponents = import.meta.glob('../../views/**/*.vue');
const state = () => ({
@ -56,7 +56,9 @@ const mutations = {
return route;
});
};
state.permission = config.useLocalRouter ? data : convert(data);
data = config.useLocalRouter ? data : convert(data);
data.push(...demeRoutes);
state.permission = data;
resetRoutes(state.permission);
},
};

@ -4,6 +4,10 @@ body,
width: 100vw;
height: 100vh;
}
ul,
ol {
list-style: none;
}
// *:not([class^='el-']):not([class^='el-'] *) {
*:not([class^='el-']) {
margin: 0;

@ -0,0 +1,218 @@
<template>
<div class="flaw-category-container">
<ul>
<li>设置group属性实现多个拖拽表格互相拖拽</li>
<li>group需要配合code属性判断数据是从哪里拖过来的</li>
<li>设置row-class-name给某些行加上filter属性指定的class ( 默认ignore-drag-sort ) 实现禁止拖拽</li>
<li>row-sort | row-remove | row-clone 事件入参: 新下标旧下标事件对象</li>
<li>row-add事件入参: 新下标旧下标来源表格code来源数据provider函数事件对象</li>
<li>单表格拖拽触发 row-add</li>
<li>多表格拖拽来源表格触发 row-remove 或者 row-clone ,目的表单触发 row-add</li>
</ul>
<p>{{ list.map((item) => item.id) }}</p>
<p>{{ list.find((item) => item.id === '1').childList.map((item) => item.id) }}</p>
<SortableTable
v-loading="listLoading"
border
code="parent"
:data="list"
group="test"
:row-class-name="handleRowClassName"
@row-add="handleRowAdd"
@row-clone="handleRowClone"
@row-remove="handleRowRemove"
@row-sort="handleRowSort"
>
<el-table-column label="展开" type="expand" width="60">
<template #default="{ row }">
<SortableTable
v-loading="listLoading"
border
code="child"
:data="row.childList || []"
:group="{ name: 'test', pull: 'clone' }"
:row-class-name="handleRowClassName"
@row-add="handleChildrenRowAdd"
@row-clone="handleChildrenRowClone"
@row-remove="handleChildrenRowRemove"
@row-sort="handleChildrenRowSort"
>
<el-table-column align="center" label="排序" prop="sort" width="60" />
<el-table-column align="center" label="ID" prop="id" show-overflow-tooltip />
<el-table-column align="center" label="字段1" prop="key1" show-overflow-tooltip />
<el-table-column align="center" label="字段2" prop="key2" show-overflow-tooltip />
<el-table-column align="center" label="字段3" prop="key3" show-overflow-tooltip />
<el-table-column align="center" label="字段4" prop="key4" show-overflow-tooltip />
<el-table-column align="center" label="字段5" prop="key5" show-overflow-tooltip />
<template #empty>
<el-empty class="vab-data-empty" description="暂无数据" />
</template>
</SortableTable>
</template>
</el-table-column>
<el-table-column align="center" label="排序" prop="sort" width="60" />
<el-table-column align="center" label="ID" prop="id" show-overflow-tooltip />
<el-table-column align="center" label="字段1" prop="key1" show-overflow-tooltip />
<el-table-column align="center" label="字段2" prop="key2" show-overflow-tooltip />
<el-table-column align="center" label="字段3" prop="key3" show-overflow-tooltip />
<el-table-column align="center" label="字段4" prop="key4" show-overflow-tooltip />
<el-table-column align="center" label="字段5" prop="key5" show-overflow-tooltip />
<template #empty>
<el-empty class="vab-data-empty" description="暂无数据" />
</template>
</SortableTable>
</div>
</template>
<script>
import { ElMessage } from '@/plugins/element-plus';
export default defineComponent({
name: 'SortableTableDemo',
setup() {
/* 通用 */
// proxyvue2this
const { proxy } = getCurrentInstance();
const commonState = reactive({
//
layout: 'total, sizes, prev, pager, next, jumper',
});
/* 查询 */
const queryState = reactive({
//
listLoading: false,
//
list: [
{
id: '1',
sort: 1,
key1: '1-1',
key2: '1-2',
key3: '1-3',
key4: '1-4',
key5: '1-5',
childList: [
{ id: '1-1', sort: 1, key1: '1-1', key2: '1-2', key3: '1-3', key4: '1-4', key5: '1-5' },
{ id: '1-2', sort: 2, key1: '2-1', key2: '2-2', key3: '2-3', key4: '2-4', key5: '2-5' },
{ id: '1-3', sort: 3, key1: '3-1', key2: '3-2', key3: '3-3', key4: '3-4', key5: '3-5' },
{ id: '1-4', sort: 4, key1: '4-1', key2: '4-2', key3: '4-3', key4: '4-4', key5: '4-5' },
{ id: '1-5', sort: 5, key1: '5-1', key2: '5-2', key3: '5-3', key4: '5-4', key5: '5-5' },
],
},
{
id: '2',
sort: 2,
key1: '2-1',
key2: '2-2',
key3: '2-3',
key4: '2-4',
key5: '2-5',
childList: [],
},
{
id: '3',
sort: 3,
key1: '3-1',
key2: '3-2',
key3: '3-3',
key4: '3-4',
key5: '3-5',
childList: [],
},
{
id: '4',
sort: 4,
key1: '4-1',
key2: '4-2',
key3: '4-3',
key4: '4-4',
key5: '4-5',
childList: [],
},
{
id: '5',
sort: 5,
key1: '5-1',
key2: '5-2',
key3: '5-3',
key4: '5-4',
key5: '5-5',
childList: [],
},
],
//
total: 0,
//
queryForm: {
pageIndex: 1,
length: 10,
name: null,
},
});
/* 操作 */
//
const handleRowClassName = ({ rowIndex }) => (rowIndex === 2 ? 'ignore-drag-sort' : '');
// sortablejs
const handleRowSort = (newIndex, oldIndex, e) => {
console.info('sort', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
//
const handleRowAdd = (newIndex, oldIndex, tableCode, done, e) => {
console.info('add', e);
if (tableCode === 'child') {
done(queryState.list.find((item) => item.id === '1').childList[oldIndex]);
}
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
//
const handleRowRemove = (newIndex, oldIndex, e) => {
console.info('remove', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
//
const handleRowClone = (newIndex, oldIndex, e) => {
console.info('clone', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
/* 子表格操作 */
const handleChildrenRowSort = (newIndex, oldIndex, e) => {
console.info('child sort', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
const handleChildrenRowAdd = (newIndex, oldIndex, tableCode, done, e) => {
console.info('child add', e);
if (tableCode === 'parent') {
done(queryState.list[oldIndex]);
}
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
const handleChildrenRowRemove = (newIndex, oldIndex, e) => {
console.info('child remove', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
const handleChildrenRowClone = (newIndex, oldIndex, e) => {
console.info('child clone', e);
ElMessage.success(`排序从 ${oldIndex} 变成了 ${newIndex} `);
};
return {
...toRefs(commonState),
...toRefs(queryState),
handleRowClassName,
handleRowSort,
handleRowAdd,
handleRowRemove,
handleRowClone,
handleChildrenRowSort,
handleChildrenRowAdd,
handleChildrenRowRemove,
handleChildrenRowClone,
};
},
});
</script>

@ -1,20 +1,5 @@
<template>
<div class="container">
<el-dialog center :model-value="true">
<el-select :opts="opts"></el-select>
<el-cascader :options="opts" @change="handleAdd"></el-cascader>
<el-checkbox-group :opts="opts"></el-checkbox-group>
<el-radio-group :opts="opts" v-model="form.msg"></el-radio-group>
<el-dropdown :opts="opts2"></el-dropdown>
<el-input v-model="form.msg"></el-input>
<el-image
:preview-src-list="null"
src="http://ksimage.mashibing.com/504cb56ac7f44d2bbe65ade478e1f3d4.jpg"
alt="测试"
></el-image>
{{ imgList }}
<el-upload-image v-model="imgList" />
</el-dialog>
<h1>{{ $route.name }}</h1>
<h1>
<el-icon name="app-store" size="30" />
@ -35,9 +20,6 @@
<script setup>
const store = useStore();
const token = computed(() => store.state.local.token);
const userInfo = computed(() => store.state.auth.userInfo);
const permission = computed(() => store.state.auth.permission);
const count = computed(() => store.state.demo.count);
const doubleCount = computed(() => store.getters['demo/doubleCount']);
const handleAdd = () => {
@ -47,25 +29,6 @@
store.dispatch('demo/clear');
};
const form = reactive({ msg: '123' });
const opts = reactive([
{ label: '男', value: 1 },
{ label: '女', value: 0 },
]);
const opts2 = reactive([
{
label: '编辑',
onClick() {
alert('编辑');
},
},
{
label: '删除',
onClick() {
alert('删除');
},
},
]);
const imgList = ref('http://ksimage.mashibing.com/504cb56ac7f44d2bbe65ade478e1f3d4.jpg');
</script>
<style lang="less" scoped>

Loading…
Cancel
Save