216 lines
4.6 KiB
Vue
216 lines
4.6 KiB
Vue
<script setup>
|
|
import {
|
|
h,
|
|
useSlots,
|
|
computed,
|
|
shallowRef,
|
|
onMounted,
|
|
onUnmounted,
|
|
nextTick,
|
|
markRaw,
|
|
watch,
|
|
} from "vue";
|
|
import Sortable from "sortablejs";
|
|
import { debounce } from "lodash-es";
|
|
import { NDataTable } from "naive-ui";
|
|
|
|
// Props 定义
|
|
const props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
default: () => [],
|
|
required: true,
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default: () => [],
|
|
required: true,
|
|
},
|
|
align: {
|
|
type: String,
|
|
default: "center",
|
|
validator: (value) => ["left", "center", "right"].includes(value),
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 拖拽功能相关逻辑
|
|
*/
|
|
const useDraggable = (props, emit) => {
|
|
const dragConfig = {
|
|
handleId: "drag-handle",
|
|
handleStyle: { cursor: "move", padding: "0 4px" },
|
|
};
|
|
|
|
const dragColumn = computed(() =>
|
|
props.columns?.find((col) => col.type === "drag")
|
|
);
|
|
|
|
const sortable = shallowRef();
|
|
const nDataTableRef = shallowRef();
|
|
|
|
const initSortable = () => {
|
|
if (!dragColumn.value) return;
|
|
|
|
const tbody = nDataTableRef.value?.$el?.querySelector?.(
|
|
".n-data-table-tbody"
|
|
);
|
|
if (!tbody) return;
|
|
|
|
sortable.value = markRaw(
|
|
new Sortable(tbody, {
|
|
animation: 150,
|
|
handle: dragColumn.value.handle ? `.${dragConfig.handleId}` : undefined,
|
|
onEnd: ({ oldIndex, newIndex }) => {
|
|
if (oldIndex === newIndex) return;
|
|
|
|
const newData = [...props.data];
|
|
const [removed] = newData.splice(oldIndex, 1);
|
|
newData.splice(newIndex, 0, removed);
|
|
|
|
emit("update:data", newData);
|
|
dragColumn.value?.onDragEnd?.({
|
|
oldIndex,
|
|
newIndex,
|
|
data: newData,
|
|
row: removed,
|
|
});
|
|
},
|
|
})
|
|
);
|
|
};
|
|
|
|
const debouncedInitSortable = debounce(initSortable, 200);
|
|
|
|
onMounted(() => {
|
|
nextTick(initSortable);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
sortable.value?.destroy();
|
|
});
|
|
|
|
watch(
|
|
() => props.data,
|
|
() => nextTick(debouncedInitSortable)
|
|
);
|
|
|
|
return {
|
|
dragConfig,
|
|
dragColumn,
|
|
nDataTableRef,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 列渲染相关逻辑
|
|
*/
|
|
const useColumns = (props, slots, dragConfig) => {
|
|
// 创建标题
|
|
const createTitle = (column, slotKey) => {
|
|
const titleSlotKey = `${slotKey}_title`;
|
|
|
|
if (column.titleRender) {
|
|
return column.titleRender;
|
|
}
|
|
|
|
if (slots[titleSlotKey]) {
|
|
return () => slots[titleSlotKey]({ column });
|
|
}
|
|
|
|
return column.title;
|
|
};
|
|
|
|
// 创建展开渲染器
|
|
const createExpandRenderer = () => {
|
|
if (!slots["templateExpand"]) return null;
|
|
return (row, index) => h(slots["templateExpand"], { row, index });
|
|
};
|
|
|
|
// 创建拖拽列渲染器
|
|
const createDragColumnRenderer = (column, slotKey) => {
|
|
return (row, index) =>
|
|
h(
|
|
"div",
|
|
{
|
|
class: [dragConfig.handleId, "drag-handle-wrapper"],
|
|
style: dragConfig.handleStyle,
|
|
onClick: column.handle ? (e) => e.stopPropagation() : undefined,
|
|
},
|
|
slots[slotKey] ? slots[slotKey]({ row, index, column }) : "⋮⋮"
|
|
);
|
|
};
|
|
|
|
// 创建普通列渲染器
|
|
const createDefaultColumnRenderer = (column, slotKey) => {
|
|
if (slots[slotKey]) {
|
|
return (row, index) => slots[slotKey]({ row, index, column });
|
|
}
|
|
if (column.render) {
|
|
return column.render;
|
|
}
|
|
return (row) => row[column.key];
|
|
};
|
|
|
|
// 创建列配置
|
|
const createColumnRender = (column) => {
|
|
const slotKey = column.key;
|
|
|
|
const baseColumn = {
|
|
...column,
|
|
align: props.align,
|
|
title: createTitle(column, slotKey),
|
|
renderExpand: createExpandRenderer(),
|
|
};
|
|
|
|
if (column.type === "drag") {
|
|
return {
|
|
...baseColumn,
|
|
render: createDragColumnRenderer(column, slotKey),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...baseColumn,
|
|
render: createDefaultColumnRenderer(column, slotKey),
|
|
};
|
|
};
|
|
|
|
return computed(() => props.columns?.map(createColumnRender) || []);
|
|
};
|
|
|
|
// 事件定义
|
|
const emit = defineEmits(["update:data"]);
|
|
const slots = useSlots();
|
|
|
|
// 组合功能
|
|
const { dragConfig, dragColumn, nDataTableRef } = useDraggable(props, emit);
|
|
const computedColumns = useColumns(props, slots, dragConfig);
|
|
</script>
|
|
|
|
<template>
|
|
<n-data-table
|
|
ref="nDataTableRef"
|
|
:class="[dragColumn?.handle ? 'handle-only-drag' : 'full-row-drag']"
|
|
remote
|
|
v-bind="{ ...$attrs, ...$props, columns: computedColumns }"
|
|
/>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.drag-handle-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
}
|
|
|
|
.handle-only-drag tbody tr {
|
|
cursor: default;
|
|
}
|
|
|
|
.full-row-drag tbody tr {
|
|
cursor: move;
|
|
}
|
|
</style>
|