FORMA

属性排序需求

需求背景如下:

  1. 需要实现属性的拖拽排序;
  2. 支持同级子项排序,但禁止跨容器拖拽。
    例如:A 容器内的项不能拖到 B 容器,只能在 A 容器内部调整顺序(B 容器同理)。

示意图:

属性排序需求.png

说明:规格配置 不能拖拽到 行业属性 容器,行业属性 也不能拖拽到 规格配置 容器。

实现思路

将拖拽排序封装为独立组件 AttrDragItem,由父组件传入数据并接收更新结果。组件同时支持:

  • 扁平列表排序;
  • 树状结构(带子集)排序。
vue
<!-- 父组件 -->

<script setup lang="ts">
import { ref } from "vue";
import attrDragItem from "./attrDragItem.vue";

const test = ref(
  Array.from({ length: 7 }, (_, i) => ({
    id: i + 1,
    name: `C00${i + 1}${
      ["白色", "黑色", "红色", "绿色", "蓝色", "黄色", "紫色"][i]
    }`,
    elements: [],
  }))
);
const test2 = ref([
  {
    id: 1,
    name: "电机型号",
    elements: [
      {
        id: 1,
        name: "M2/V2",
      },
      {
        id: 2,
        name: "米家",
      },
    ],
  },
]);
</script>

<template>
  <h3>颜色配置</h3>
  <attrDragItem
    :data="test"
    :is-tree="false"
    @update:data="(newData) => (test = newData)"
  />

  <h3>规格配置</h3>
  <attrDragItem
    :data="test2"
    :is-tree="true"
    @update:data="(newData) => (test2 = newData)"
  />
  <!-- .... 其他属性配置 -->
</template>

attrDragItem 组件核心参数如下:

  • data:排序数据(数组);
  • is-tree:是否为树状结构,默认 false
  • @update:data:数据更新事件,返回最新排序结果。
vue
<!-- 子组件 attrDragItem.vue -->

<script setup lang="ts">
defineOptions({
  name: "AttrDragItem",
});
import VabDraggable from "vuedraggable";

const props = defineProps<{
  // 数据
  data: TAny[];
  // 是否是树结构
  isTree: boolean;
}>();

const emit = defineEmits<{
  "update:data": [value: any[]];
}>();

const dataList = computed({
  get: () => props.data,
  set: (value) => emit("update:data", value),
});

const dragOptions = computed(() => {
  return {
    animation: 600,
    disabled: false,
    ghostClass: "ghost",
  };
});

// 处理子集元素更新(树结构)
const handleElementsUpdate = (item: TAny, newElements: TAny[]) => {
  const index = dataList.value.findIndex((i) => i.id === item.id);
  if (index !== -1) {
    const newData = [...dataList.value];
    newData[index] = { ...item, elements: newElements };
    emit("update:data", newData);
  }
};
</script>

<template>
  <vab-draggable
    v-model="dataList"
    v-bind="dragOptions"
    item-key="id"
    style="display: flex; flex-wrap: wrap; justify-content: flex-start"
    :style="{ flexDirection: props.isTree ? 'column' : 'row' }"
  >
    <template #item="{ element: item }">
      <div>
        <div :class="['drag-item-container', props.isTree ? 'mb' : '']">
          <vab-icon v-if="props.isTree" icon="draggable" />
          <div :class="[props.isTree ? 'tree-item' : 'drag-item']">
            {{ item.name }}
          </div>
        </div>
        <template v-if="props.isTree">
          <!-- 递归调用子组件 -->
          <attrDragItem
            :data="item.elements"
            :is-tree="false"
            @update:data="
              (newElements) => handleElementsUpdate(item, newElements)
            "
          />
        </template>
      </div>
    </template>
  </vab-draggable>
</template>

<style lang="scss" scoped>
.drag-item-container {
  display: flex;
  align-items: center;
  color: #333;

  &.mb {
    margin-bottom: 12px;
  }
  .drag-item {
    padding: 6px 12px;
    background-color: rgba(238, 238, 238, 1);
    border-radius: 50px;
    margin-bottom: 8px;
    margin-right: 8px;
    cursor: move;
  }
}
</style>

实现要点

  1. 禁止跨容器拖拽
    每个容器使用独立数据源与组件实例;如需更严格限制,可为不同容器设置不同 group
  2. 树结构递归渲染
    isTree = true 时递归渲染子节点,并通过 handleElementsUpdate 回传子集变更。
  3. 排序结果持久化
    推荐在拖拽结束事件(如 @end)触发后端保存接口,避免拖拽过程中频繁请求。