vue3.0实现数组分组效果
- 效果图


-
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>

浙公网安备 33010602011771号