vue3.0实现数组分组效果

  • 效果图

aca184ddf5a747d0ac36d62cd6912f61

e7cd1c7b5f6c4d969b961ed309575cb6

  • vue3.0+element-plus

  • 拖拽效果 vue-draggable-plus

  • 父组件使用

      <column-dialog :options="fields"
                      v-model="content"></column-dialog>
    
  • 子组件 ColumnDialog/index.vue

      <template>
        <div >
          <el-input placeholder="请选择"
                    readonly
                    @click="onVisible"
                    v-model="valueName" />
          <el-dialog title="编辑显示列"
                    width="600"
                    v-model="visible">
            <div class="column_content">
              <div class="column_header">
                <div class="flex justify-between">
                  <el-checkbox :indeterminate="indeterminate"
                              @change="onCheckAllChange"
                              v-model="checkAll">
                    全选
                  </el-checkbox>
                  <el-button size="default"
                            @click="handleAddGroup">
                    <el-icon><plus /></el-icon>
                    新增分组
                  </el-button>
                </div>
              </div>
              <vue-draggable ghostClass="custom_drag_class"
                            handle=".draggable_column"
                            @end="onEndDraggable"
                            @start="isDraggable = true"
                            v-model="currentContent">
                <div v-for="(item,index) in currentContent"
                    :key="item.dataIndex">
                  <!-- 分组类型 -->
                  <div v-if="item.children"
                      class="group-container" >
                    <div class="draggable_column ">
                      <div class="column_row flex justify-between column_child"
                          :class="{hideShadow: isDraggable}">
                        <div>
                          <img alt=""
                              class="drag_icon"
                              src="@/assets/drag.svg"/>
                          <span class="column_title">{{ item.title }}</span>
                        </div>
                        <div class="field-actions">
                          <el-button icon="Edit"
                                    size="small"
                                    title="重命名"
                                    @click="editField(index)"/>
                          <el-button icon="Delete"
                                    size="small"
                                    title="解散分组"
                                    @click="deleteField(index)"/>
                        </div>
    
                      </div>
                    </div>
                    <!-- 分组子字段拖拽容器-->
                    <div class="field-children">
                      <vue-draggable class="child-list"
                                    ghostClass="dragging"
                                    handle=".child-drag-handle"
                                    v-model="item.children">
                        <div v-for="ele in item.children"
                            :key="ele.dataIndex">
                          <div class="child-drag-handle">
                            <div class="column_row column_child">
                              <img alt=""
                                  class="drag_icon"
                                  src="@/assets/drag.svg"/>
                              <el-checkbox @change="e => onChangeItem(e,ele)"
                                          v-model="ele.selectFlag">
                                <span class="column_title">{{ ele.title }}</span>
                              </el-checkbox>
                            </div>
                          </div>
                        </div>
                      </vue-draggable>
                    </div>
    
                  </div>
                  <!-- 普通字段类型 -->
                  <div v-else
                      class="draggable_column">
                    <div class="column_row"
                        :class="{hideShadow: isDraggable}">
                      <img alt=""
                          class="drag_icon"
                          src="@/assets/drag.svg"/>
                      <el-checkbox @change="e => onChangeItem(e,item)"
                                  v-model="item.selectFlag">
                        <span class="column_title">{{ item.title }}</span>
                      </el-checkbox>
                    </div>
                  </div>
                </div>
              </vue-draggable>
            </div>
            <template #footer>
              <el-button @click="closeFather">取消</el-button>
              <el-button type="primary"
                        @click="onSubmit">保存</el-button>
            </template>
          </el-dialog>
          <el-dialog :close-on-click-modal="true"
                    title="新增分组"
                    width="600px"
                    @close="closeGroupModal"
                    v-model="groupModalVisible">
            <div class="modal-body">
              <el-form-item class="form-group"
                            label="分组名称">
                <el-input placeholder="请输入分组名称"
                          v-model.trim="groupName"/>
              </el-form-item>
              <el-form-item class="form-group"
                            label="选择字段">
                <div class="fields-selection">
                  <div v-for="(field, index) in nonGroupedFields"
                      :key="`select-field-${index}-${field.dataIndex}`"
                      class="field-option"
                      :class="{ selected: selectedFieldIndexes.includes(index) }">
                    <el-checkbox class="field-checkbox"
                                :value="index"
                                v-model="selectedFieldIndexes"/>
                    <label>{{ field.title }}</label>
                  </div>
                </div>
              </el-form-item>
            </div>
    
            <template #footer>
              <el-button @click="closeGroupModal">取消</el-button>
              <el-button type="primary"
                        @click="createGroup">确认</el-button>
            </template>
          </el-dialog>
        </div>
      </template>
    
      <script setup lang="ts">
        import { VueDraggable } from "vue-draggable-plus";
    
        import _ from "lodash";
    
        const props = defineProps({
          title: { type: String, default: "请选择" },
          options: { type: Array as () => any[], default: () => [] },
        });
        // 定义emits
        const emit = defineEmits(["change"]);
    
        const model: any = defineModel();
    
        const indeterminate = ref(false);
        const checkAll = ref(false);
        const visible = ref(false);
        const List = ref([]);
        const checkData = ref([]); // 选中勾选的数据
        const isDraggable = ref(false);
    
        const currentContent = ref([]);
    
        const groupName = ref(""); // 分组名称
        const groupModalVisible = ref(false);
        const selectedFieldIndexes: any = ref([]); // 勾选的索引
    
        // 计算属性:非分组字段(用于模态框选择)
        const nonGroupedFields = computed(() => currentContent.value.filter(field => !field.children));
    
        watch(
          () => model.value,
          () => {
            const newList: any = _.cloneDeep(model.value);
            currentContent.value = newList;
            checkData.value = newList?.filter(el => el.selectFlag).map(el => el.dataIndex);
            isCheckAll();
          },
          { immediate: true }
        );
        // 监听options变化
        watch(
          () => props.options,
          newVal => {
            List.value = _.cloneDeep(newVal);
            isCheckAll();
          },
          { immediate: true }
        );
    
        function onVisible() {
          visible.value = true;
        }
    
        // 全选处理
        function onCheckAllChange(checked) {
          checkAll.value = checked;
          indeterminate.value = false;
          checkData.value = checked ? List.value.map(el => el.dataIndex) : [];
          currentContent.value.forEach(item => {
            item.selectFlag = checked;
            if (item?.children && item?.children.length > 0) {
              item.children.forEach(ele => {
                ele.selectFlag = checked;
              });
            }
          });
        }
    
        // 单选处理
        function onChangeItem(checked, item) {
          item.selectFlag = checked;
          const groupId = item.dataIndex;
          const checkIndex = _.findIndex(checkData.value, e => e === groupId);
          if (checked) {
            checkData.value.push(groupId);
          } else {
            checkData.value.splice(checkIndex, 1);
          }
          isCheckAll();
        }
        function isCheckAll() {
          indeterminate.value = Boolean(checkData.value?.length) && checkData.value?.length < List.value?.length;
          checkAll.value = checkData.value?.length === List.value?.length;
        }
    
        // 提交处理
        function onSubmit() {
          model.value = currentContent.value;
          emit("change");
          closeFather();
        }
    
        // 拖拽结束处理
        function onEndDraggable() {
          isDraggable.value = false;
        }
    
        // 关闭弹窗
        function closeFather() {
          visible.value = false;
        }
    
        // 计算显示的名称
        const valueName = computed(() => {
          if (!model.value || model.value.length === 0) {
            return "";
          }
          const list = [];
          model.value?.forEach(el => {
            if (el.selectFlag) {
              list.push(el.title);
            }
            if (el?.children) {
              el.children.forEach(ele => {
                if (ele.selectFlag) {
                  list.push(ele.title);
                }
              });
            }
          });
          return list.map(el => el).join(", ");
        });
    
        // 新增分组
        function handleAddGroup() {
          groupName.value = "";
          selectedFieldIndexes.value = [];
          groupModalVisible.value = true;
        }
    
        function closeGroupModal() {
          groupModalVisible.value = false;
          groupName.value = "";
          selectedFieldIndexes.value = [];
        }
    
        function editField(index) {
          const field = currentContent.value[index];
          ElMessageBox.prompt("请输入新的名称", "编辑名称", {
            confirmButtonText: "确认",
            cancelButtonText: "取消",
            inputValue: field.title,
            inputPattern: /\S/,
            inputErrorMessage: "请输入新的分组名称",
            beforeClose: (action, instance, done) => {
              if (action === "confirm") {
                const inputValue = instance.inputValue?.trim();
                const newList = currentContent.value.filter((el, tmpIndex) => tmpIndex !== index);
                console.log(newList);
                const tmp = newList.some(el => el.title === inputValue);
                if (tmp) {
                  ElMessage.warning("分组名称不能重复");
                } else {
                  done();
                }
              } else {
                done();
              }
            },
          })
            .then(({ value }) => {
              const trimmedValue = value.trim();
              if (trimmedValue) {
                currentContent.value[index].title = trimmedValue;
                ElMessage.success("名称修改成功");
              }
            })
            .catch(() => {});
        }
    
        function deleteField(index) {
          const field = currentContent.value[index];
          ElMessageBox.confirm("确定要删除这个分组吗?分组内的字段将转为独立字段", "删除确认", {
            type: "warning",
          })
            .then(() => {
              const newList = [];
              if (field.children && field.children.length > 0) {
                field.children.forEach(child => {
                  newList.push({
                    ...child,
                  });
                });
              }
              currentContent.value.splice(index, 1, ...newList);
              ElMessage.success("删除成功");
            })
            .catch(() => {});
        }
    
        function createGroup() {
          const trimmedName = groupName.value;
          if (!trimmedName) {
            ElMessage.warning("请输入分组名称");
            return;
          }
          const tmp = currentContent.value.some(el => el.title === trimmedName);
          if (tmp) {
            ElMessage.warning("分组名称不能重复");
            return;
          }
          if (selectedFieldIndexes.value.length === 0) {
            ElMessage.warning("请至少选择一个字段");
            return;
          }
    
          // 新增分组逻辑
          const selectedFields = selectedFieldIndexes.value.map(index => nonGroupedFields.value[index]);
          const newGroup = { title: trimmedName, dataIndex: trimmedName, children: selectedFields };
          const selectedDataIndexes = selectedFields.map(f => f.dataIndex);
          // 更新字段数据
          currentContent.value = currentContent.value.filter(item => {
            if (item.children) {
              return !item.children.some(child => selectedDataIndexes.includes(child.dataIndex));
            }
            return !selectedDataIndexes.includes(item.dataIndex);
          });
          currentContent.value.unshift(newGroup);
          // 同步给父组件
          closeGroupModal();
          ElMessage.success("分组创建成功");
        }
      </script>
    
      <style lang="scss" scoped>
        .column_content {
          padding: 12px;
          border: solid 1px #eeeeee;
          background-color: #f6f6f6;
          max-height: 60vh;
          overflow-y: auto;
          .column_header {
            padding: 10px 5px 10px 10px;
          }
          .group-container {
            margin-bottom: 10px;
          }
    
          .column_row {
            padding: 10px;
            cursor: url("@/assets/cursor.svg"), auto;
            border-bottom: solid 1px #eeeeee;
            font-size: 14px;
            background-color: #fff;
            margin-bottom: 10px;
            &.column_child {
              margin-bottom: 0;
            }
    
            &:hover {
              box-shadow: 0px 0px 20px 5px rgba(116, 116, 116, 0.11);
              background-color: transparent;
            }
    
            &:active {
              box-shadow: none;
            }
    
            .drag_icon {
              margin-right: 5px;
              width: 14px;
              height: 14px;
              display: inline-block;
              vertical-align: text-top;
            }
    
            .column_title {
              margin: 0px 10px;
              color: rgba(23, 26, 29, 0.6);
              text-align: center;
            }
          }
    
          .column_row.hideShadow {
            box-shadow: none;
          }
    
          .custom_drag_class {
            box-shadow: 0px 0px 20px 5px rgba(116, 116, 116, 0.11);
            border: 0.5px solid #0089f9;
            color: #0089f9;
          }
    
          .custom_drag_class .column_title {
            color: #0089f9;
          }
        }
    
        .modal-body {
          padding: 20px;
          max-height: 60vh;
          overflow-y: auto;
          .form-group {
            margin-bottom: 20px;
    
            .fields-selection {
              width: 100%;
              border: 1px solid #e9ecef;
              border-radius: 6px;
              max-height: 300px;
              overflow-y: auto;
              .field-option {
                padding: 12px;
                border-bottom: 1px solid #f1f3f4;
                display: flex;
                align-items: center;
                cursor: pointer;
                transition: background 0.3s ease;
                .field-checkbox {
                  margin-right: 10px;
                }
              }
    
              .field-option:last-child {
                border-bottom: none;
              }
    
              .field-option:hover {
                background: #f8f9fa;
              }
    
              .field-option.selected {
                background: #e3f2fd;
                color: #1976d2;
              }
            }
          }
        }
      </style>
    
posted @ 2025-11-11 10:30  不完美的完美  阅读(7)  评论(0)    收藏  举报