|
@@ -1,13 +1,14 @@
|
|
|
import editForm from "../form.vue";
|
|
import editForm from "../form.vue";
|
|
|
import { handleTree } from "@/utils/tree";
|
|
import { handleTree } from "@/utils/tree";
|
|
|
import { message } from "@/utils/message";
|
|
import { message } from "@/utils/message";
|
|
|
-import { getMenuList, createMenu, updateMenu, deleteMenuApi } from "@/api/system";
|
|
|
|
|
|
|
+import { getMenuList, createMenu, updateMenu, deleteMenuApi, updateMenuSort } from "@/api/system";
|
|
|
import { transformI18n } from "@/plugins/i18n";
|
|
import { transformI18n } from "@/plugins/i18n";
|
|
|
import { addDialog } from "@/components/ReDialog";
|
|
import { addDialog } from "@/components/ReDialog";
|
|
|
import { reactive, ref, onMounted, h } from "vue";
|
|
import { reactive, ref, onMounted, h } from "vue";
|
|
|
import type { FormItemProps } from "../utils/types";
|
|
import type { FormItemProps } from "../utils/types";
|
|
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
|
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
|
|
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
|
|
|
|
|
+import Sortable from "sortablejs";
|
|
|
|
|
|
|
|
export function useMenu() {
|
|
export function useMenu() {
|
|
|
const form = reactive({
|
|
const form = reactive({
|
|
@@ -17,6 +18,36 @@ export function useMenu() {
|
|
|
const formRef = ref();
|
|
const formRef = ref();
|
|
|
const dataList = ref([]);
|
|
const dataList = ref([]);
|
|
|
const loading = ref(true);
|
|
const loading = ref(true);
|
|
|
|
|
+ const flatDataMap = ref(new Map<string | number, any>());
|
|
|
|
|
+ const flatOrderedList = ref<any[]>([]);
|
|
|
|
|
+ let sortableInstance: Sortable | null = null;
|
|
|
|
|
+ let draggedDataCache: any = null;
|
|
|
|
|
+
|
|
|
|
|
+ /** 深度优先遍历树,返回与 DOM 行顺序一致的扁平列表 */
|
|
|
|
|
+ function buildFlatOrdered(tree: any[]): any[] {
|
|
|
|
|
+ const result: any[] = [];
|
|
|
|
|
+ function walk(nodes: any[]) {
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ result.push(node);
|
|
|
|
|
+ if (node.children?.length > 0) {
|
|
|
|
|
+ walk(node.children);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ walk(tree);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 给表格每行打上 data-id 属性,供拖拽时识别菜单 */
|
|
|
|
|
+ function tagRowIds(tableEl: HTMLElement) {
|
|
|
|
|
+ const rows = tableEl.querySelectorAll(".el-table__body-wrapper .el-table__row");
|
|
|
|
|
+ const list = flatOrderedList.value;
|
|
|
|
|
+ for (let i = 0; i < rows.length; i++) {
|
|
|
|
|
+ if (i < list.length) {
|
|
|
|
|
+ rows[i].setAttribute("data-id", String(list[i].id));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const getMenuType = (type, text = false) => {
|
|
const getMenuType = (type, text = false) => {
|
|
|
switch (type) {
|
|
switch (type) {
|
|
@@ -38,6 +69,16 @@ export function useMenu() {
|
|
|
align: "left",
|
|
align: "left",
|
|
|
cellRenderer: ({ row }) => (
|
|
cellRenderer: ({ row }) => (
|
|
|
<>
|
|
<>
|
|
|
|
|
+ <span class="drag-handle inline-flex align-middle mr-1" style="cursor: grab; color: #909399; user-select: none; vertical-align: middle;">
|
|
|
|
|
+ <svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
|
|
|
|
|
+ <circle cx="4" cy="3" r="1.2"/>
|
|
|
|
|
+ <circle cx="8" cy="3" r="1.2"/>
|
|
|
|
|
+ <circle cx="4" cy="8" r="1.2"/>
|
|
|
|
|
+ <circle cx="8" cy="8" r="1.2"/>
|
|
|
|
|
+ <circle cx="4" cy="13" r="1.2"/>
|
|
|
|
|
+ <circle cx="8" cy="13" r="1.2"/>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </span>
|
|
|
<span class="inline-block mr-1">
|
|
<span class="inline-block mr-1">
|
|
|
{h(useRenderIcon(row.icon), {
|
|
{h(useRenderIcon(row.icon), {
|
|
|
style: { paddingTop: "1px" }
|
|
style: { paddingTop: "1px" }
|
|
@@ -107,14 +148,24 @@ export function useMenu() {
|
|
|
async function onSearch() {
|
|
async function onSearch() {
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
|
|
const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
|
|
|
- let newData = data;
|
|
|
|
|
|
|
+ // 构建 id -> 菜单项 的扁平映射表,供拖拽排序时查询
|
|
|
|
|
+ const map = new Map<string | number, any>();
|
|
|
|
|
+ for (const item of data) {
|
|
|
|
|
+ map.set(item.id, item);
|
|
|
|
|
+ }
|
|
|
|
|
+ flatDataMap.value = map;
|
|
|
|
|
+ // 构建树并缓存深度优先扁平列表(与 DOM 行顺序一致)
|
|
|
|
|
+ const fullTree = handleTree(data);
|
|
|
|
|
+ flatOrderedList.value = buildFlatOrdered(fullTree);
|
|
|
if (!isAllEmpty(form.title)) {
|
|
if (!isAllEmpty(form.title)) {
|
|
|
- // 前端搜索菜单名称
|
|
|
|
|
- newData = newData.filter(item =>
|
|
|
|
|
|
|
+ // 前端搜索菜单名称:过滤扁平数据后重新构建树
|
|
|
|
|
+ const filtered = data.filter(item =>
|
|
|
transformI18n(item.title).includes(form.title)
|
|
transformI18n(item.title).includes(form.title)
|
|
|
);
|
|
);
|
|
|
|
|
+ dataList.value = handleTree(filtered);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ dataList.value = fullTree;
|
|
|
}
|
|
}
|
|
|
- dataList.value = handleTree(newData); // 处理成树结构
|
|
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
}, 500);
|
|
}, 500);
|
|
@@ -200,6 +251,74 @@ export function useMenu() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function initDragSort(tableEl: HTMLElement) {
|
|
|
|
|
+ destroyDragSort();
|
|
|
|
|
+ const tbody = tableEl.querySelector(".el-table__body-wrapper tbody") as HTMLElement;
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 给行打上 data-id 属性
|
|
|
|
|
+ tagRowIds(tableEl);
|
|
|
|
|
+
|
|
|
|
|
+ sortableInstance = Sortable.create(tbody, {
|
|
|
|
|
+ handle: ".drag-handle",
|
|
|
|
|
+ draggable: ".el-table__row",
|
|
|
|
|
+ animation: 200,
|
|
|
|
|
+ ghostClass: "sortable-ghost",
|
|
|
|
|
+ onStart() {
|
|
|
|
|
+ draggedDataCache = null;
|
|
|
|
|
+ },
|
|
|
|
|
+ onMove(evt) {
|
|
|
|
|
+ const draggedId = evt.dragged.getAttribute("data-id");
|
|
|
|
|
+ const relatedId = evt.related.getAttribute("data-id");
|
|
|
|
|
+ const draggedData = flatDataMap.value.get(draggedId);
|
|
|
|
|
+ const relatedData = flatDataMap.value.get(relatedId);
|
|
|
|
|
+ if (!draggedData || !relatedData) return false;
|
|
|
|
|
+ if (!draggedDataCache) draggedDataCache = draggedData;
|
|
|
|
|
+ // 只允许同级拖拽
|
|
|
|
|
+ return draggedData.parentId === relatedData.parentId;
|
|
|
|
|
+ },
|
|
|
|
|
+ async onEnd() {
|
|
|
|
|
+ const draggedData = draggedDataCache;
|
|
|
|
|
+ if (!draggedData) return;
|
|
|
|
|
+
|
|
|
|
|
+ const parentId = draggedData.parentId;
|
|
|
|
|
+ // 从 DOM 新顺序中收集同级兄弟的 id(data-id 在 DOM 移动后跟着走了)
|
|
|
|
|
+ const allRows = tbody.querySelectorAll(".el-table__row");
|
|
|
|
|
+ const siblingIds: any[] = [];
|
|
|
|
|
+ for (const row of allRows) {
|
|
|
|
|
+ const id = row.getAttribute("data-id");
|
|
|
|
|
+ const data = flatDataMap.value.get(id);
|
|
|
|
|
+ if (data && data.parentId === parentId && !siblingIds.includes(id)) {
|
|
|
|
|
+ siblingIds.push(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (siblingIds.length <= 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const sortData = siblingIds.map((id, index) => ({
|
|
|
|
|
+ id,
|
|
|
|
|
+ rank: index + 1
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await updateMenuSort(sortData);
|
|
|
|
|
+ message("排序更新成功", { type: "success" });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ message("排序更新失败", { type: "error" });
|
|
|
|
|
+ }
|
|
|
|
|
+ draggedDataCache = null;
|
|
|
|
|
+ onSearch();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function destroyDragSort() {
|
|
|
|
|
+ if (sortableInstance) {
|
|
|
|
|
+ sortableInstance.destroy();
|
|
|
|
|
+ sortableInstance = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
onSearch();
|
|
onSearch();
|
|
|
});
|
|
});
|
|
@@ -217,6 +336,10 @@ export function useMenu() {
|
|
|
openDialog,
|
|
openDialog,
|
|
|
/** 删除菜单 */
|
|
/** 删除菜单 */
|
|
|
handleDelete,
|
|
handleDelete,
|
|
|
- handleSelectionChange
|
|
|
|
|
|
|
+ handleSelectionChange,
|
|
|
|
|
+ /** 初始化拖拽排序 */
|
|
|
|
|
+ initDragSort,
|
|
|
|
|
+ /** 销毁拖拽排序 */
|
|
|
|
|
+ destroyDragSort
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|