mirror of https://github.com/halo-dev/halo
parent
0826f51f67
commit
9fef47f291
|
@ -34,11 +34,12 @@
|
|||
"@formkit/vue": "1.0.0-beta.10",
|
||||
"@halo-dev/admin-api": "^1.1.0",
|
||||
"@halo-dev/admin-shared": "workspace:*",
|
||||
"@halo-dev/api-client": "^0.0.11",
|
||||
"@halo-dev/api-client": "^0.0.10",
|
||||
"@halo-dev/components": "workspace:*",
|
||||
"@halo-dev/richtext-editor": "0.0.0-alpha.1",
|
||||
"@vueuse/components": "^8.9.4",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/router": "^9.1.0",
|
||||
"axios": "^0.27.2",
|
||||
"filepond": "^4.30.4",
|
||||
"filepond-plugin-image-preview": "^4.6.11",
|
||||
|
@ -52,6 +53,7 @@
|
|||
"vue-filepond": "^7.0.3",
|
||||
"vue-grid-layout": "3.0.0-beta1",
|
||||
"vue-router": "^4.1.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"yaml": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@halo-dev/api-client": "^0.0.11",
|
||||
"@halo-dev/api-client": "^0.0.10",
|
||||
"@halo-dev/components": "workspace:*",
|
||||
"axios": "^0.27.2"
|
||||
},
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
V1alpha1RoleBindingApi,
|
||||
V1alpha1SettingApi,
|
||||
V1alpha1UserApi,
|
||||
V1alpha1MenuApi,
|
||||
V1alpha1MenuItemApi,
|
||||
ThemeHaloRunV1alpha1ThemeApi,
|
||||
ApiHaloRunV1alpha1ThemeApi,
|
||||
} from "@halo-dev/api-client";
|
||||
|
@ -61,10 +63,8 @@ function setupApiClient(axios: AxiosInstance) {
|
|||
plugin: new PluginHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
||||
user: new V1alpha1UserApi(undefined, apiUrl, axios),
|
||||
theme: new ThemeHaloRunV1alpha1ThemeApi(undefined, apiUrl, axios),
|
||||
|
||||
// TODO optional
|
||||
// link: new CoreHaloRunV1alpha1LinkApi(undefined, apiUrl, axios),
|
||||
// linkGroup: new CoreHaloRunV1alpha1LinkGroupApi(undefined, apiUrl, axios),
|
||||
menu: new V1alpha1MenuApi(undefined, apiUrl, axios),
|
||||
menuItem: new V1alpha1MenuItemApi(undefined, apiUrl, axios),
|
||||
},
|
||||
user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios),
|
||||
plugin: new ApiHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
||||
|
|
|
@ -14,7 +14,7 @@ importers:
|
|||
'@formkit/vue': 1.0.0-beta.10
|
||||
'@halo-dev/admin-api': ^1.1.0
|
||||
'@halo-dev/admin-shared': workspace:*
|
||||
'@halo-dev/api-client': ^0.0.11
|
||||
'@halo-dev/api-client': ^0.0.10
|
||||
'@halo-dev/components': workspace:*
|
||||
'@halo-dev/richtext-editor': 0.0.0-alpha.1
|
||||
'@rushstack/eslint-patch': ^1.1.4
|
||||
|
@ -35,6 +35,7 @@ importers:
|
|||
'@vue/tsconfig': ^0.1.3
|
||||
'@vueuse/components': ^8.9.4
|
||||
'@vueuse/core': ^8.9.4
|
||||
'@vueuse/router': ^9.1.0
|
||||
autoprefixer: ^10.4.8
|
||||
axios: ^0.27.2
|
||||
c8: ^7.12.0
|
||||
|
@ -74,6 +75,7 @@ importers:
|
|||
vue-grid-layout: 3.0.0-beta1
|
||||
vue-router: ^4.1.3
|
||||
vue-tsc: ^0.39.5
|
||||
vuedraggable: ^4.1.0
|
||||
yaml: ^2.1.1
|
||||
dependencies:
|
||||
'@formkit/addons': 1.0.0-beta.10_vue@3.2.37
|
||||
|
@ -85,11 +87,12 @@ importers:
|
|||
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
|
||||
'@halo-dev/admin-api': 1.1.0
|
||||
'@halo-dev/admin-shared': link:packages/shared
|
||||
'@halo-dev/api-client': 0.0.11
|
||||
'@halo-dev/api-client': 0.0.10
|
||||
'@halo-dev/components': link:packages/components
|
||||
'@halo-dev/richtext-editor': 0.0.0-alpha.1_vue@3.2.37
|
||||
'@vueuse/components': 8.9.4_vue@3.2.37
|
||||
'@vueuse/core': 8.9.4_vue@3.2.37
|
||||
'@vueuse/router': 9.1.0_45jhv7el6g2ztux7wgm2ofrf4e
|
||||
axios: 0.27.2
|
||||
filepond: 4.30.4
|
||||
filepond-plugin-image-preview: 4.6.11_filepond@4.30.4
|
||||
|
@ -103,6 +106,7 @@ importers:
|
|||
vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
|
||||
vue-grid-layout: 3.0.0-beta1
|
||||
vue-router: 4.1.3_vue@3.2.37
|
||||
vuedraggable: 4.1.0_vue@3.2.37
|
||||
yaml: 2.1.1
|
||||
devDependencies:
|
||||
'@changesets/cli': 2.24.2
|
||||
|
@ -176,12 +180,12 @@ importers:
|
|||
|
||||
packages/shared:
|
||||
specifiers:
|
||||
'@halo-dev/api-client': ^0.0.11
|
||||
'@halo-dev/api-client': ^0.0.10
|
||||
'@halo-dev/components': workspace:*
|
||||
axios: ^0.27.2
|
||||
vite-plugin-dts: ^1.4.1
|
||||
dependencies:
|
||||
'@halo-dev/api-client': 0.0.11
|
||||
'@halo-dev/api-client': 0.0.10
|
||||
'@halo-dev/components': link:../components
|
||||
axios: 0.27.2
|
||||
devDependencies:
|
||||
|
@ -2126,8 +2130,8 @@ packages:
|
|||
- debug
|
||||
dev: false
|
||||
|
||||
/@halo-dev/api-client/0.0.11:
|
||||
resolution: {integrity: sha512-67e7lrWPoHVKF4XItT6OWr7rX96CR9s/3SOVXYD3xCyAMIgLyFULfKVI7JGxisFMKvadTx7rm5RK4zzZ/CbXIw==}
|
||||
/@halo-dev/api-client/0.0.10:
|
||||
resolution: {integrity: sha512-DKQKkEAKMR/rbopI6jbjbzLiYUZeY6dOcgqGoDGG8MAcwkWOI6iWaZnuR5z+X8vd51XjiPhnekAphfcO6PaWEQ==}
|
||||
dev: false
|
||||
|
||||
/@halo-dev/logger/1.1.0:
|
||||
|
@ -3653,6 +3657,19 @@ packages:
|
|||
resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==}
|
||||
dev: false
|
||||
|
||||
/@vueuse/router/9.1.0_45jhv7el6g2ztux7wgm2ofrf4e:
|
||||
resolution: {integrity: sha512-ub2B8XfHCQXX4Migab9bBnFjJI+NmqeLbQox1UdhWhy9cZIpgKx40aW7eq/LLUMGVIKAdriDI6YsD0LZrZFeaA==}
|
||||
peerDependencies:
|
||||
vue-router: '>=4.0.0-rc.1'
|
||||
dependencies:
|
||||
'@vueuse/shared': 9.1.0_vue@3.2.37
|
||||
vue-demi: 0.12.1_vue@3.2.37
|
||||
vue-router: 4.1.3_vue@3.2.37
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared/8.9.4_vue@3.2.37:
|
||||
resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==}
|
||||
peerDependencies:
|
||||
|
@ -3668,6 +3685,15 @@ packages:
|
|||
vue-demi: 0.12.1_vue@3.2.37
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared/9.1.0_vue@3.2.37:
|
||||
resolution: {integrity: sha512-pB/3njQu4tfJJ78ajELNda0yMG6lKfpToQW7Soe09CprF1k3QuyoNi1tBNvo75wBDJWD+LOnr+c4B5HZ39jY/Q==}
|
||||
dependencies:
|
||||
vue-demi: 0.12.1_vue@3.2.37
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/abab/2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
dev: true
|
||||
|
@ -8195,6 +8221,10 @@ packages:
|
|||
yargs: 15.4.1
|
||||
dev: true
|
||||
|
||||
/sortablejs/1.14.0:
|
||||
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
|
||||
dev: false
|
||||
|
||||
/source-map-js/1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -9279,6 +9309,15 @@ packages:
|
|||
'@vue/server-renderer': 3.2.37_vue@3.2.37
|
||||
'@vue/shared': 3.2.37
|
||||
|
||||
/vuedraggable/4.1.0_vue@3.2.37:
|
||||
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.1
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
vue: 3.2.37
|
||||
dev: false
|
||||
|
||||
/w3c-hr-time/1.0.2:
|
||||
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
||||
dependencies:
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
IconDeleteBin,
|
||||
IconListSettings,
|
||||
VButton,
|
||||
VCard,
|
||||
VMenu,
|
||||
VMenuItem,
|
||||
VPageHeader,
|
||||
VSpace,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import { ref } from "vue";
|
||||
|
||||
const menus = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: "首页",
|
||||
url: "https://halo.run",
|
||||
priority: 0,
|
||||
target: "_self",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "官方文档",
|
||||
url: "https://docs.halo.run",
|
||||
priority: 1,
|
||||
target: "_blank",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "主题仓库",
|
||||
url: "/themes.html",
|
||||
priority: 2,
|
||||
target: "_self",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 36,
|
||||
name: "博客",
|
||||
url: "/blog.html",
|
||||
priority: 3,
|
||||
target: "_self",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 40,
|
||||
name: "论坛",
|
||||
url: "https://bbs.halo.run",
|
||||
priority: 4,
|
||||
target: "_blank",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 168,
|
||||
name: "在线体验",
|
||||
url: "https://docs.halo.run/#%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C",
|
||||
priority: 5,
|
||||
target: "_blank",
|
||||
icon: "",
|
||||
parentId: 0,
|
||||
team: "main",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<VPageHeader title="菜单">
|
||||
<template #icon>
|
||||
<IconListSettings class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VButton type="secondary">重建索引</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
<div class="m-0 md:m-4">
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<div class="w-96">
|
||||
<VCard title="分组">
|
||||
<VMenu class="!p-0">
|
||||
<VMenuItem id="default" active title="未分组"></VMenuItem>
|
||||
<VMenuItem id="community" title="社区"></VMenuItem>
|
||||
</VMenu>
|
||||
<template #footer>
|
||||
<VButton block type="secondary">新增</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<template #header>
|
||||
<div class="block w-full bg-gray-50 px-4 py-3">
|
||||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<span class="text-base font-medium">未分组</span>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:mt-0">
|
||||
<VSpace>
|
||||
<VButton size="xs" type="primary">保存</VButton>
|
||||
<VButton size="xs" type="default">新增</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ul
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li v-for="(menu, index) in menus" :key="index">
|
||||
<div
|
||||
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
||||
>
|
||||
<div class="relative flex flex-row items-center">
|
||||
<div class="mr-4 hidden cursor-move items-center sm:flex">
|
||||
<IconListSettings />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-row items-center">
|
||||
<span
|
||||
class="mr-2 truncate text-sm font-medium text-gray-900"
|
||||
>
|
||||
{{ menu.name }}
|
||||
</span>
|
||||
<VTag class="sm:hidden">asd</VTag>
|
||||
</div>
|
||||
<div class="mt-1 flex">
|
||||
<VSpace align="start" direction="column" spacing="xs">
|
||||
<a
|
||||
:href="menu.url"
|
||||
class="text-xs text-gray-500 hover:text-gray-900"
|
||||
target="_blank"
|
||||
>
|
||||
{{ menu.url }}
|
||||
</a>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div
|
||||
class="inline-flex flex-col flex-col-reverse items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
||||
>
|
||||
<span class="cursor-pointer text-sm hover:text-red-600">
|
||||
<IconDeleteBin />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,194 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
IconListSettings,
|
||||
useDialog,
|
||||
VButton,
|
||||
VCard,
|
||||
VPageHeader,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import MenuItemEditingModal from "./components/MenuItemEditingModal.vue";
|
||||
import MenuItemListItem from "./components/MenuItemListItem.vue";
|
||||
import MenuList from "./components/MenuList.vue";
|
||||
import { ref } from "vue";
|
||||
import { apiClient } from "@halo-dev/admin-shared";
|
||||
import type { Menu, MenuItem } from "@halo-dev/api-client";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import type { MenuTreeItem } from "./utils";
|
||||
import {
|
||||
buildMenuItemsTree,
|
||||
convertMenuTreeItemToMenuItem,
|
||||
convertTreeToMenuItems,
|
||||
getChildrenNames,
|
||||
resetMenuItemsTreePriority,
|
||||
} from "./utils";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
const menuItems = ref<MenuItem[]>([] as MenuItem[]);
|
||||
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
|
||||
const selectedMenu = ref<Menu | undefined>();
|
||||
const selectedMenuItem = ref<MenuItem | null>(null);
|
||||
const menuListRef = ref();
|
||||
const menuItemEditingModal = ref();
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const handleFetchMenuItems = async () => {
|
||||
try {
|
||||
if (!selectedMenu.value?.spec.menuItems) {
|
||||
return;
|
||||
}
|
||||
const menuItemNames = Array.from(selectedMenu.value.spec.menuItems)?.map(
|
||||
(item) => item
|
||||
);
|
||||
const { data } = await apiClient.extension.menuItem.listv1alpha1MenuItem(
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
[`name=(${menuItemNames.join(",")})`]
|
||||
);
|
||||
menuItems.value = data.items;
|
||||
// Build the menu tree
|
||||
menuTreeItems.value = buildMenuItemsTree(data.items);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch menu items", e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenEditingModal = (menuItem: MenuTreeItem) => {
|
||||
selectedMenuItem.value = convertMenuTreeItemToMenuItem(menuItem);
|
||||
menuItemEditingModal.value = true;
|
||||
};
|
||||
|
||||
const onMenuItemSaved = async (menuItem: MenuItem) => {
|
||||
const menuToUpdate = cloneDeep(selectedMenu.value);
|
||||
|
||||
if (menuToUpdate) {
|
||||
const menuItemsToUpdate = Array.from(
|
||||
cloneDeep(menuToUpdate.spec.menuItems) || new Set<string>()
|
||||
);
|
||||
menuItemsToUpdate.push(menuItem.metadata.name);
|
||||
|
||||
// @ts-ignore
|
||||
menuToUpdate.spec.menuItems = Array.from(new Set(menuItemsToUpdate));
|
||||
await apiClient.extension.menu.updatev1alpha1Menu(
|
||||
menuToUpdate.metadata.name,
|
||||
menuToUpdate
|
||||
);
|
||||
}
|
||||
|
||||
await menuListRef.value.handleFetchMenus();
|
||||
await handleFetchMenuItems();
|
||||
};
|
||||
|
||||
const handleUpdateInBatch = useDebounceFn(async () => {
|
||||
const menuTreeItemsToUpdate = resetMenuItemsTreePriority(menuTreeItems.value);
|
||||
const menuItemsToUpdate = convertTreeToMenuItems(menuTreeItemsToUpdate);
|
||||
try {
|
||||
const promises = menuItemsToUpdate.map((menuItem) =>
|
||||
apiClient.extension.menuItem.updatev1alpha1MenuItem(
|
||||
menuItem.metadata.name,
|
||||
menuItem
|
||||
)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
} catch (e) {
|
||||
console.log("Failed to update menu items", e);
|
||||
} finally {
|
||||
await menuListRef.value.handleFetchMenus();
|
||||
await handleFetchMenuItems();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const handleDelete = async (menuItem: MenuTreeItem) => {
|
||||
dialog.info({
|
||||
title: "是否确定删除该菜单?",
|
||||
description: "删除后将无法恢复",
|
||||
confirmType: "danger",
|
||||
onConfirm: async () => {
|
||||
await apiClient.extension.menuItem.deletev1alpha1MenuItem(
|
||||
menuItem.metadata.name
|
||||
);
|
||||
|
||||
const childrenNames = getChildrenNames(menuItem);
|
||||
|
||||
if (childrenNames.length) {
|
||||
setTimeout(() => {
|
||||
dialog.info({
|
||||
title: "检查到当前菜单下包含子菜单,是否删除?",
|
||||
description: "如果选择否,那么所有子菜单将转移到一级菜单",
|
||||
confirmType: "danger",
|
||||
onConfirm: async () => {
|
||||
const promises = childrenNames.map((name) =>
|
||||
apiClient.extension.menuItem.deletev1alpha1MenuItem(name)
|
||||
);
|
||||
await Promise.all(promises);
|
||||
},
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
await menuListRef.value.handleFetchMenus();
|
||||
await handleFetchMenuItems();
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<MenuItemEditingModal
|
||||
v-model:visible="menuItemEditingModal"
|
||||
:menu-item="selectedMenuItem"
|
||||
@close="selectedMenuItem = null"
|
||||
@saved="onMenuItemSaved"
|
||||
/>
|
||||
<VPageHeader title="菜单">
|
||||
<template #icon>
|
||||
<IconListSettings class="mr-2 self-center" />
|
||||
</template>
|
||||
</VPageHeader>
|
||||
<div class="m-0 md:m-4">
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<div class="w-96">
|
||||
<MenuList
|
||||
ref="menuListRef"
|
||||
v-model:selected-menu="selectedMenu"
|
||||
@select="handleFetchMenuItems"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<template #header>
|
||||
<div class="block w-full bg-gray-50 px-4 py-3">
|
||||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<span class="text-base font-medium">
|
||||
{{ selectedMenu?.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:mt-0">
|
||||
<VSpace>
|
||||
<VButton
|
||||
size="xs"
|
||||
type="default"
|
||||
@click="menuItemEditingModal = true"
|
||||
>
|
||||
新增
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<MenuItemListItem
|
||||
:menu-tree-items="menuTreeItems"
|
||||
@change="handleUpdateInBatch"
|
||||
@delete="handleDelete"
|
||||
@open-editing="handleOpenEditingModal"
|
||||
/>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts" setup>
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import type { Menu } from "@halo-dev/api-client";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type { PropType } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { apiClient } from "@halo-dev/admin-shared";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { useMagicKeys } from "@vueuse/core";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
menu: {
|
||||
type: Object as PropType<Menu | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "close"]);
|
||||
|
||||
const initialFormState: Menu = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
// @ts-ignore
|
||||
menuItems: [],
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "Menu",
|
||||
metadata: {
|
||||
name: uuid(),
|
||||
},
|
||||
};
|
||||
|
||||
const formState = ref<Menu>(cloneDeep(initialFormState));
|
||||
const saving = ref(false);
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
const handleCreateMenu = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
if (isUpdateMode.value) {
|
||||
await apiClient.extension.menu.updatev1alpha1Menu(
|
||||
formState.value.metadata.name,
|
||||
formState.value
|
||||
);
|
||||
} else {
|
||||
await apiClient.extension.menu.createv1alpha1Menu(formState.value);
|
||||
}
|
||||
onVisibleChange(false);
|
||||
} catch (e) {
|
||||
console.error("Failed to create menu", e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
if (!visible) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
watch(props, (newVal) => {
|
||||
const { Command_Enter } = useMagicKeys();
|
||||
let keyboardWatcher;
|
||||
if (newVal.visible) {
|
||||
keyboardWatcher = watch(Command_Enter, (v) => {
|
||||
if (v) {
|
||||
submitForm("menu-form");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
keyboardWatcher?.unwatch();
|
||||
}
|
||||
|
||||
if (newVal.visible && props.menu) {
|
||||
formState.value = cloneDeep(props.menu);
|
||||
return;
|
||||
}
|
||||
formState.value = cloneDeep(initialFormState);
|
||||
formState.value.metadata.name = uuid();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
title="编辑菜单"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<FormKit
|
||||
id="menu-form"
|
||||
:classes="{ form: 'w-full' }"
|
||||
type="form"
|
||||
@submit="handleCreateMenu"
|
||||
>
|
||||
<FormKit
|
||||
v-model="formState.spec.displayName"
|
||||
help="可根据此名称查询菜单项"
|
||||
label="菜单名称"
|
||||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton type="secondary" @click="$formkit.submit('menu-form')">
|
||||
提交 ⌘ + ↵
|
||||
</VButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
|
@ -0,0 +1,137 @@
|
|||
<script lang="ts" setup>
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import type { PropType } from "vue";
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import type { MenuItem } from "@halo-dev/api-client";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { apiClient } from "@halo-dev/admin-shared";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { useMagicKeys } from "@vueuse/core";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
menuItem: {
|
||||
type: Object as PropType<MenuItem | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "close", "saved"]);
|
||||
|
||||
const initialFormState: MenuItem = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
href: "",
|
||||
children: new Set([]),
|
||||
priority: 0,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: uuid(),
|
||||
},
|
||||
};
|
||||
|
||||
const formState = ref<MenuItem>(cloneDeep(initialFormState));
|
||||
const saving = ref(false);
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
const handleSaveMenuItem = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
// TODO 需要后端设置为 Array
|
||||
// @ts-ignore
|
||||
formState.value.spec.children = Array.from(formState.value.spec.children);
|
||||
|
||||
if (isUpdateMode.value) {
|
||||
const { data } =
|
||||
await apiClient.extension.menuItem.updatev1alpha1MenuItem(
|
||||
formState.value.metadata.name,
|
||||
formState.value
|
||||
);
|
||||
onVisibleChange(false);
|
||||
emit("saved", data);
|
||||
} else {
|
||||
const { data } =
|
||||
await apiClient.extension.menuItem.createv1alpha1MenuItem(
|
||||
formState.value
|
||||
);
|
||||
onVisibleChange(false);
|
||||
emit("saved", data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to create menu item", e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
if (!visible) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
watch(props, (newVal) => {
|
||||
if (newVal.visible && props.menuItem) {
|
||||
formState.value = cloneDeep(props.menuItem);
|
||||
return;
|
||||
}
|
||||
formState.value = cloneDeep(initialFormState);
|
||||
formState.value.metadata.name = uuid();
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
let keyboardWatcher;
|
||||
const { Command_Enter } = useMagicKeys();
|
||||
if (props.visible) {
|
||||
keyboardWatcher = watch(Command_Enter, (v) => {
|
||||
if (v) {
|
||||
submitForm("menuitem-form");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
keyboardWatcher?.unwatch();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
title="编辑菜单项"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<FormKit id="menuitem-form" type="form" @submit="handleSaveMenuItem">
|
||||
<FormKit
|
||||
v-model="formState.spec.displayName"
|
||||
label="名称"
|
||||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.href"
|
||||
label="链接地址"
|
||||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton type="secondary" @click="$formkit.submit('menuitem-form')">
|
||||
提交 ⌘ + ↵
|
||||
</VButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
|
@ -0,0 +1,127 @@
|
|||
<script lang="ts" setup>
|
||||
import { IconList, IconSettings, VButton, VSpace } from "@halo-dev/components";
|
||||
import Draggable from "vuedraggable";
|
||||
import type { PropType } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { MenuTreeItem } from "@/modules/interface/menus/utils";
|
||||
|
||||
defineProps({
|
||||
menuTreeItems: {
|
||||
type: Array as PropType<MenuTreeItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["change", "open-editing", "delete"]);
|
||||
|
||||
const isDragging = ref(false);
|
||||
|
||||
function onChange() {
|
||||
emit("change");
|
||||
}
|
||||
|
||||
function onOpenEditingModal(menuItem: MenuTreeItem) {
|
||||
emit("open-editing", menuItem);
|
||||
}
|
||||
|
||||
function onDelete(menuItem: MenuTreeItem) {
|
||||
emit("delete", menuItem);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<draggable
|
||||
:list="menuTreeItems"
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
ghost-class="opacity-50"
|
||||
group="menu-item"
|
||||
handle=".drag-element"
|
||||
item-key="metadata.name"
|
||||
tag="ul"
|
||||
@change="onChange"
|
||||
@end="isDragging = false"
|
||||
@start="isDragging = true"
|
||||
>
|
||||
<template #item="{ element: menuItem }">
|
||||
<li>
|
||||
<div
|
||||
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
||||
>
|
||||
<div
|
||||
class="drag-element absolute inset-y-0 left-0 flex hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||
>
|
||||
<IconList class="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div class="relative flex flex-row items-center">
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-row items-center">
|
||||
<span class="truncate text-sm font-medium text-gray-900">
|
||||
{{ menuItem.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex">
|
||||
<VSpace align="start" direction="column" spacing="xs">
|
||||
<a
|
||||
:href="menuItem.spec.href"
|
||||
class="text-xs text-gray-500 hover:text-gray-900"
|
||||
target="_blank"
|
||||
>
|
||||
{{ menuItem.spec.href }}
|
||||
</a>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
<FloatingTooltip
|
||||
v-if="menuItem.metadata.deletionTimestamp"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600">
|
||||
<span
|
||||
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
|
||||
></span>
|
||||
</div>
|
||||
<template #popper> 删除中</template>
|
||||
</FloatingTooltip>
|
||||
<div class="self-center">
|
||||
<FloatingDropdown>
|
||||
<IconSettings
|
||||
class="cursor-pointer transition-all hover:text-blue-600"
|
||||
/>
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
type="secondary"
|
||||
@click="onOpenEditingModal(menuItem)"
|
||||
>
|
||||
修改
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
type="danger"
|
||||
@click="onDelete(menuItem)"
|
||||
>
|
||||
删除
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MenuItemListItem
|
||||
:menu-tree-items="menuItem.spec.children"
|
||||
class="pl-10 transition-all duration-300"
|
||||
@change="onChange"
|
||||
@delete="onDelete"
|
||||
@open-editing="onOpenEditingModal"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
|
@ -0,0 +1,184 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
IconSettings,
|
||||
useDialog,
|
||||
VButton,
|
||||
VCard,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import MenuEditingModal from "./MenuEditingModal.vue";
|
||||
import type { PropType } from "vue";
|
||||
import { defineExpose, onMounted, ref } from "vue";
|
||||
import type { Menu } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@halo-dev/admin-shared";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
||||
const props = defineProps({
|
||||
selectedMenu: {
|
||||
type: Object as PropType<Menu | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select", "update:selectedMenu"]);
|
||||
|
||||
const menus = ref<Menu[]>([] as Menu[]);
|
||||
const selectedMenuToUpdate = ref<Menu | null>(null);
|
||||
const menuEditingModal = ref<boolean>(false);
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const handleFetchMenus = async () => {
|
||||
selectedMenuToUpdate.value = null;
|
||||
try {
|
||||
const { data } = await apiClient.extension.menu.listv1alpha1Menu();
|
||||
menus.value = data.items;
|
||||
|
||||
// update selected menu
|
||||
if (props.selectedMenu) {
|
||||
const updatedMenu = menus.value.find(
|
||||
(menu) => menu.metadata.name === props.selectedMenu?.metadata.name
|
||||
);
|
||||
if (updatedMenu) {
|
||||
emit("update:selectedMenu", updatedMenu);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch menus", e);
|
||||
}
|
||||
};
|
||||
|
||||
const menuQuery = useRouteQuery("menu");
|
||||
const handleSelect = (menu: Menu) => {
|
||||
emit("update:selectedMenu", menu);
|
||||
emit("select", menu);
|
||||
menuQuery.value = menu.metadata.name;
|
||||
};
|
||||
|
||||
const handleDeleteMenu = async (menu: Menu) => {
|
||||
dialog.warning({
|
||||
title: "确定要删除该菜单吗?",
|
||||
description: "将同时删除该菜单下的所有菜单项,该操作不可恢复。",
|
||||
confirmType: "danger",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.menu.deletev1alpha1Menu(menu.metadata.name);
|
||||
|
||||
const deleteItemsPromises = Array.from(menu.spec.menuItems || []).map(
|
||||
(item) => apiClient.extension.menuItem.deletev1alpha1MenuItem(item)
|
||||
);
|
||||
|
||||
await Promise.all(deleteItemsPromises);
|
||||
} catch (e) {
|
||||
console.error("Failed to delete menu", e);
|
||||
} finally {
|
||||
await handleFetchMenus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenEditingModal = (menu: Menu | null) => {
|
||||
selectedMenuToUpdate.value = menu;
|
||||
menuEditingModal.value = true;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await handleFetchMenus();
|
||||
|
||||
if (menuQuery.value) {
|
||||
const menu = menus.value.find((m) => m.metadata.name === menuQuery.value);
|
||||
if (menu) {
|
||||
handleSelect(menu);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (menus.value.length > 0) {
|
||||
handleSelect(menus.value[0]);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
handleFetchMenus,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<MenuEditingModal
|
||||
v-model:visible="menuEditingModal"
|
||||
:menu="selectedMenuToUpdate"
|
||||
@close="handleFetchMenus"
|
||||
/>
|
||||
<VCard :bodyClass="['!p-0']" title="菜单">
|
||||
<div class="divide-y divide-gray-100 bg-white">
|
||||
<div
|
||||
v-for="(menu, index) in menus"
|
||||
:key="index"
|
||||
:class="{
|
||||
'bg-gray-50': selectedMenu?.metadata.name === menu.metadata.name,
|
||||
}"
|
||||
class="relative flex items-center p-4"
|
||||
@click="handleSelect(menu)"
|
||||
>
|
||||
<div
|
||||
v-if="selectedMenu?.metadata.name === menu.metadata.name"
|
||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||
></div>
|
||||
<span class="flex flex-1 cursor-pointer flex-col gap-y-1">
|
||||
<span class="block text-sm font-medium">
|
||||
{{ menu.spec?.displayName }}
|
||||
</span>
|
||||
<span class="block text-xs text-gray-400">
|
||||
{{ Array.from(menu.spec.menuItems || new Set()).length }}
|
||||
个菜单项
|
||||
</span>
|
||||
</span>
|
||||
<FloatingTooltip
|
||||
v-if="menu.metadata.deletionTimestamp"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<div class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600">
|
||||
<span
|
||||
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
|
||||
></span>
|
||||
</div>
|
||||
<template #popper> 删除中</template>
|
||||
</FloatingTooltip>
|
||||
<div class="self-center">
|
||||
<FloatingDropdown>
|
||||
<IconSettings
|
||||
class="cursor-pointer transition-all hover:text-blue-600"
|
||||
/>
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
type="secondary"
|
||||
@click="handleOpenEditingModal(menu)"
|
||||
>
|
||||
修改
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(menu)"
|
||||
>
|
||||
删除
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VButton block type="secondary" @click="handleOpenEditingModal(null)">
|
||||
新增
|
||||
</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
import { BasicLayout, definePlugin } from "@halo-dev/admin-shared";
|
||||
import MenuList from "./MenuList.vue";
|
||||
import Menus from "./Menus.vue";
|
||||
import { IconListSettings } from "@halo-dev/components";
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -13,7 +13,7 @@ export default definePlugin({
|
|||
{
|
||||
path: "",
|
||||
name: "Menus",
|
||||
component: MenuList,
|
||||
component: Menus,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,378 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`buildMenuItemsTree > should match snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "411a3639-bf0d-4266-9cfb-14184259dab5",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "首页",
|
||||
"href": "https://ryanc.cc/",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:19:37.252228Z",
|
||||
"name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
"version": 12,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-07-28T06:50:32.777556Z",
|
||||
"name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"version": 4,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Halo",
|
||||
"href": "https://ryanc.cc/categories/halo",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Spring Boot",
|
||||
"href": "https://ryanc.cc/categories/spring-boot",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"displayName": "Java",
|
||||
"href": "https://ryanc.cc/categories/java",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
"displayName": "文章分类",
|
||||
"href": "https://ryanc.cc/categories",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`convertMenuTreeItemToMenuItem > should match snapshot 1`] = `
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:19:37.252228Z",
|
||||
"name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
"version": 12,
|
||||
},
|
||||
"spec": {
|
||||
"children": Set {
|
||||
"caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
},
|
||||
"displayName": "文章分类",
|
||||
"href": "https://ryanc.cc/categories",
|
||||
"priority": 1,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`convertMenuTreeItemToMenuItem > should match snapshot 2`] = `
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": Set {
|
||||
"96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
},
|
||||
"displayName": "Java",
|
||||
"href": "https://ryanc.cc/categories/java",
|
||||
"priority": 1,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`convertTreeToMenuItems > will match snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "411a3639-bf0d-4266-9cfb-14184259dab5",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "首页",
|
||||
"href": "https://ryanc.cc/",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:19:37.252228Z",
|
||||
"name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
"version": 12,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
"caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
],
|
||||
"displayName": "文章分类",
|
||||
"href": "https://ryanc.cc/categories",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-07-28T06:50:32.777556Z",
|
||||
"name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"version": 4,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Halo",
|
||||
"href": "https://ryanc.cc/categories/halo",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
"96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
],
|
||||
"displayName": "Java",
|
||||
"href": "https://ryanc.cc/categories/java",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Spring Boot",
|
||||
"href": "https://ryanc.cc/categories/spring-boot",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`resetMenuItemsTreePriority > should match snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "411a3639-bf0d-4266-9cfb-14184259dab5",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "首页",
|
||||
"href": "https://ryanc.cc/",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:19:37.252228Z",
|
||||
"name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
"version": 12,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-07-28T06:50:32.777556Z",
|
||||
"name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"version": 4,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Halo",
|
||||
"href": "https://ryanc.cc/categories/halo",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
"version": 1,
|
||||
},
|
||||
"spec": {
|
||||
"children": [],
|
||||
"displayName": "Spring Boot",
|
||||
"href": "https://ryanc.cc/categories/spring-boot",
|
||||
"priority": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"displayName": "Java",
|
||||
"href": "https://ryanc.cc/categories/java",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
"displayName": "文章分类",
|
||||
"href": "https://ryanc.cc/categories",
|
||||
"priority": 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`sortMenuItemsTree > will match snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:19:37.252228Z",
|
||||
"name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
"version": 12,
|
||||
},
|
||||
"spec": {
|
||||
"categoryRef": {
|
||||
"name": "",
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-08-05T04:22:03.377364Z",
|
||||
"name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"version": 0,
|
||||
},
|
||||
"spec": {
|
||||
"categoryRef": {
|
||||
"name": "",
|
||||
},
|
||||
"children": [],
|
||||
"displayName": "Java",
|
||||
"href": "https://ryanc.cc/categories/java",
|
||||
"pageRef": {
|
||||
"name": "",
|
||||
},
|
||||
"postRef": {
|
||||
"name": "",
|
||||
},
|
||||
"priority": 0,
|
||||
"tagRef": {
|
||||
"name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"apiVersion": "v1alpha1",
|
||||
"kind": "MenuItem",
|
||||
"metadata": {
|
||||
"creationTimestamp": "2022-07-28T06:50:32.777556Z",
|
||||
"name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"version": 4,
|
||||
},
|
||||
"spec": {
|
||||
"categoryRef": {
|
||||
"name": "",
|
||||
},
|
||||
"children": [],
|
||||
"displayName": "Halo",
|
||||
"href": "https://ryanc.cc/categories/halo",
|
||||
"pageRef": {
|
||||
"name": "",
|
||||
},
|
||||
"postRef": {
|
||||
"name": "",
|
||||
},
|
||||
"priority": 1,
|
||||
"tagRef": {
|
||||
"name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"displayName": "文章分类",
|
||||
"href": "https://ryanc.cc/categories",
|
||||
"pageRef": {
|
||||
"name": "",
|
||||
},
|
||||
"postRef": {
|
||||
"name": "",
|
||||
},
|
||||
"priority": 0,
|
||||
"tagRef": {
|
||||
"name": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,269 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { MenuTreeItem } from "../index";
|
||||
import {
|
||||
buildMenuItemsTree,
|
||||
convertMenuTreeItemToMenuItem,
|
||||
convertTreeToMenuItems,
|
||||
getChildrenNames,
|
||||
resetMenuItemsTreePriority,
|
||||
sortMenuItemsTree,
|
||||
} from "../index";
|
||||
import type { MenuItem } from "@halo-dev/api-client";
|
||||
|
||||
const rawMenuItems: MenuItem[] = [
|
||||
{
|
||||
spec: {
|
||||
displayName: "文章分类",
|
||||
href: "https://ryanc.cc/categories",
|
||||
// @ts-ignore
|
||||
children: [
|
||||
"caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
],
|
||||
priority: 1,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
version: 12,
|
||||
creationTimestamp: "2022-08-05T04:19:37.252228Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: {
|
||||
displayName: "Halo",
|
||||
href: "https://ryanc.cc/categories/halo",
|
||||
// @ts-ignore
|
||||
children: [],
|
||||
priority: 0,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
version: 4,
|
||||
creationTimestamp: "2022-07-28T06:50:32.777556Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: {
|
||||
displayName: "Java",
|
||||
href: "https://ryanc.cc/categories/java",
|
||||
// @ts-ignore
|
||||
children: ["96b636bb-3e4a-44d1-8ea7-f9da9e876f45"],
|
||||
priority: 1,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
version: 1,
|
||||
creationTimestamp: "2022-08-05T04:22:03.377364Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: {
|
||||
displayName: "Spring Boot",
|
||||
href: "https://ryanc.cc/categories/spring-boot",
|
||||
// @ts-ignore
|
||||
children: [],
|
||||
priority: 0,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: "96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
version: 1,
|
||||
creationTimestamp: "2022-08-05T04:22:03.377364Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: {
|
||||
displayName: "首页",
|
||||
href: "https://ryanc.cc/",
|
||||
// @ts-ignore
|
||||
children: [],
|
||||
priority: 0,
|
||||
},
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
name: "411a3639-bf0d-4266-9cfb-14184259dab5",
|
||||
version: 1,
|
||||
creationTimestamp: "2022-08-05T04:22:03.377364Z",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("buildMenuItemsTree", () => {
|
||||
it("should match snapshot", () => {
|
||||
expect(buildMenuItemsTree(rawMenuItems)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should be sorted correctly", () => {
|
||||
const menuItems = buildMenuItemsTree(rawMenuItems);
|
||||
expect(menuItems[0].spec.priority).toBe(0);
|
||||
expect(menuItems[1].spec.priority).toBe(1);
|
||||
|
||||
// children should be sorted
|
||||
expect(menuItems[1].spec.children[0].spec.priority).toBe(0);
|
||||
expect(menuItems[1].spec.children[1].spec.priority).toBe(1);
|
||||
expect(menuItems[1].spec.children[1].spec.children[0].spec.priority).toBe(
|
||||
0
|
||||
);
|
||||
|
||||
expect(menuItems[0].spec.displayName).toBe("首页");
|
||||
expect(menuItems[1].spec.displayName).toBe("文章分类");
|
||||
expect(menuItems[1].spec.children[0].spec.displayName).toBe("Halo");
|
||||
expect(menuItems[1].spec.children[1].spec.displayName).toBe("Java");
|
||||
expect(
|
||||
menuItems[1].spec.children[1].spec.children[0].spec.displayName
|
||||
).toBe("Spring Boot");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertTreeToMenuItems", () => {
|
||||
it("will match snapshot", function () {
|
||||
const menuTreeItems = buildMenuItemsTree(rawMenuItems);
|
||||
expect(convertTreeToMenuItems(menuTreeItems)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortMenuItemsTree", () => {
|
||||
it("will match snapshot", () => {
|
||||
const tree: MenuTreeItem[] = [
|
||||
{
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
creationTimestamp: "2022-08-05T04:19:37.252228Z",
|
||||
name: "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
version: 12,
|
||||
},
|
||||
spec: {
|
||||
categoryRef: {
|
||||
name: "",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
creationTimestamp: "2022-07-28T06:50:32.777556Z",
|
||||
name: "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
version: 4,
|
||||
},
|
||||
spec: {
|
||||
categoryRef: {
|
||||
name: "",
|
||||
},
|
||||
children: [],
|
||||
priority: 1,
|
||||
displayName: "Halo",
|
||||
href: "https://ryanc.cc/categories/halo",
|
||||
pageRef: {
|
||||
name: "",
|
||||
},
|
||||
postRef: {
|
||||
name: "",
|
||||
},
|
||||
tagRef: {
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "MenuItem",
|
||||
metadata: {
|
||||
creationTimestamp: "2022-08-05T04:22:03.377364Z",
|
||||
name: "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
version: 0,
|
||||
},
|
||||
spec: {
|
||||
categoryRef: {
|
||||
name: "",
|
||||
},
|
||||
children: [],
|
||||
priority: 0,
|
||||
displayName: "Java",
|
||||
href: "https://ryanc.cc/categories/java",
|
||||
pageRef: {
|
||||
name: "",
|
||||
},
|
||||
postRef: {
|
||||
name: "",
|
||||
},
|
||||
tagRef: {
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
priority: 0,
|
||||
displayName: "文章分类",
|
||||
href: "https://ryanc.cc/categories",
|
||||
pageRef: {
|
||||
name: "",
|
||||
},
|
||||
postRef: {
|
||||
name: "",
|
||||
},
|
||||
tagRef: {
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(sortMenuItemsTree(tree)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetMenuItemsTreePriority", () => {
|
||||
it("should match snapshot", function () {
|
||||
expect(
|
||||
resetMenuItemsTreePriority(buildMenuItemsTree(rawMenuItems))
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChildrenNames", () => {
|
||||
it("should return correct names", () => {
|
||||
const menuTreeItems = buildMenuItemsTree(rawMenuItems);
|
||||
expect(getChildrenNames(menuTreeItems[0])).toEqual([]);
|
||||
expect(getChildrenNames(menuTreeItems[1])).toEqual([
|
||||
"caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
"96b636bb-3e4a-44d1-8ea7-f9da9e876f45",
|
||||
]);
|
||||
|
||||
expect(getChildrenNames(menuTreeItems[1].spec.children[0])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertMenuTreeItemToMenuItem", () => {
|
||||
it("should match snapshot", () => {
|
||||
const menuTreeItems = buildMenuItemsTree(rawMenuItems);
|
||||
expect(convertMenuTreeItemToMenuItem(menuTreeItems[1])).toMatchSnapshot();
|
||||
expect(
|
||||
convertMenuTreeItemToMenuItem(menuTreeItems[1].spec.children[1])
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return correct MenuItem", () => {
|
||||
const menuTreeItems = buildMenuItemsTree(rawMenuItems);
|
||||
expect(
|
||||
convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.displayName
|
||||
).toBe("文章分类");
|
||||
expect(
|
||||
convertMenuTreeItemToMenuItem(menuTreeItems[1]).spec.children
|
||||
).toStrictEqual(
|
||||
new Set([
|
||||
"caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
"ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,267 @@
|
|||
import type { MenuItem, MenuItemSpec } from "@halo-dev/api-client";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
|
||||
export interface MenuTreeItemSpec extends Omit<MenuItemSpec, "children"> {
|
||||
children: MenuTreeItem[];
|
||||
}
|
||||
|
||||
export interface MenuTreeItem extends Omit<MenuItem, "spec"> {
|
||||
spec: MenuTreeItemSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a flat array of menu items into flattens a menu tree.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```json
|
||||
* [
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "文章分类",
|
||||
* "href": "https://ryanc.cc/categories",
|
||||
* "children": [
|
||||
* "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
* "ded1943d-9fdb-4563-83ee-2f04364872e0"
|
||||
* ]
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
* "version": 12,
|
||||
* "creationTimestamp": "2022-08-05T04:19:37.252228Z"
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "Halo",
|
||||
* "href": "https://ryanc.cc/categories/halo",
|
||||
* "children": []
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
* "version": 4,
|
||||
* "creationTimestamp": "2022-07-28T06:50:32.777556Z"
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "Java",
|
||||
* "href": "https://ryanc.cc/categories/java",
|
||||
* "children": []
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
* "version": 1,
|
||||
* "creationTimestamp": "2022-08-05T04:22:03.377364Z"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* will be transformed to:
|
||||
*
|
||||
* ```json
|
||||
* [
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "文章分类",
|
||||
* "href": "https://ryanc.cc/categories",
|
||||
* "children": [
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "Halo",
|
||||
* "href": "https://ryanc.cc/categories/halo",
|
||||
* "children": []
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "caeef383-3828-4039-9114-6f9ad3b4a37e",
|
||||
* "version": 4,
|
||||
* "creationTimestamp": "2022-07-28T06:50:32.777556Z"
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* "spec": {
|
||||
* "displayName": "Java",
|
||||
* "href": "https://ryanc.cc/categories/java",
|
||||
* "children": []
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "ded1943d-9fdb-4563-83ee-2f04364872e0",
|
||||
* "version": 1,
|
||||
* "creationTimestamp": "2022-08-05T04:22:03.377364Z"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* "apiVersion": "v1alpha1",
|
||||
* "kind": "MenuItem",
|
||||
* "metadata": {
|
||||
* "name": "40e4ba86-5c7e-43c2-b4c0-cee376d26564",
|
||||
* "version": 12,
|
||||
* "creationTimestamp": "2022-08-05T04:19:37.252228Z"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param menuItems
|
||||
*/
|
||||
export function buildMenuItemsTree(menuItems: MenuItem[]): MenuTreeItem[] {
|
||||
const menuItemsToUpdate = cloneDeep(menuItems);
|
||||
|
||||
const menuItemsMap = {};
|
||||
const parentMap = {};
|
||||
|
||||
menuItemsToUpdate.forEach((menuItem) => {
|
||||
menuItemsMap[menuItem.metadata.name] = menuItem;
|
||||
// @ts-ignore
|
||||
menuItem.spec.children.forEach((child) => {
|
||||
parentMap[child] = menuItem.metadata.name;
|
||||
});
|
||||
// @ts-ignore
|
||||
menuItem.spec.children = [];
|
||||
});
|
||||
|
||||
menuItemsToUpdate.forEach((menuItem) => {
|
||||
const parentName = parentMap[menuItem.metadata.name];
|
||||
if (parentName && menuItemsMap[parentName]) {
|
||||
menuItemsMap[parentName].spec.children.push(menuItem);
|
||||
}
|
||||
});
|
||||
|
||||
const menuTreeItems = menuItemsToUpdate.filter(
|
||||
(node) => parentMap[node.metadata.name] === undefined
|
||||
);
|
||||
|
||||
return sortMenuItemsTree(menuTreeItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a menu tree by priority.
|
||||
*
|
||||
* @param menuTreeItems
|
||||
*/
|
||||
export function sortMenuItemsTree(
|
||||
menuTreeItems: MenuTreeItem[] | MenuItem[]
|
||||
): MenuTreeItem[] {
|
||||
return menuTreeItems
|
||||
.sort((a, b) => {
|
||||
if (a.spec.priority < b.spec.priority) {
|
||||
return -1;
|
||||
}
|
||||
if (a.spec.priority > b.spec.priority) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((menuTreeItem) => {
|
||||
if (menuTreeItem.spec.children.length) {
|
||||
return {
|
||||
...menuTreeItem,
|
||||
spec: {
|
||||
...menuTreeItem.spec,
|
||||
children: sortMenuItemsTree(menuTreeItem.spec.children),
|
||||
},
|
||||
};
|
||||
}
|
||||
return menuTreeItem;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the menu tree item's priority.
|
||||
*
|
||||
* @param menuItems
|
||||
*/
|
||||
export function resetMenuItemsTreePriority(
|
||||
menuItems: MenuTreeItem[]
|
||||
): MenuTreeItem[] {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
menuItems[i].spec.priority = i;
|
||||
if (menuItems[i].spec.children) {
|
||||
resetMenuItemsTreePriority(menuItems[i].spec.children);
|
||||
}
|
||||
}
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a menu tree items into a flat array of menu.
|
||||
*
|
||||
* @param menuTreeItems
|
||||
*/
|
||||
export function convertTreeToMenuItems(menuTreeItems: MenuTreeItem[]) {
|
||||
const menuItems: MenuItem[] = [];
|
||||
const menuItemsMap = new Map<string, MenuItem>();
|
||||
const convertMenuItem = (node: MenuTreeItem | undefined) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const children = node.spec.children || [];
|
||||
menuItemsMap.set(node.metadata.name, {
|
||||
...node,
|
||||
spec: {
|
||||
...node.spec,
|
||||
// @ts-ignore
|
||||
children: children.map((child) => child.metadata.name),
|
||||
},
|
||||
});
|
||||
children.forEach((child) => {
|
||||
convertMenuItem(child);
|
||||
});
|
||||
};
|
||||
menuTreeItems.forEach((node) => {
|
||||
convertMenuItem(node);
|
||||
});
|
||||
menuItemsMap.forEach((node) => {
|
||||
menuItems.push(node);
|
||||
});
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
export function getChildrenNames(menuTreeItem: MenuTreeItem): string[] {
|
||||
const childrenNames: string[] = [];
|
||||
|
||||
function getChildrenNamesRecursive(menuTreeItem: MenuTreeItem) {
|
||||
if (menuTreeItem.spec.children) {
|
||||
menuTreeItem.spec.children.forEach((child) => {
|
||||
childrenNames.push(child.metadata.name);
|
||||
getChildrenNamesRecursive(child);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getChildrenNamesRecursive(menuTreeItem);
|
||||
|
||||
return childrenNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link MenuTreeItem} to {@link MenuItem} with flat children name array.
|
||||
*
|
||||
* @param menuTreeItem
|
||||
*/
|
||||
export function convertMenuTreeItemToMenuItem(
|
||||
menuTreeItem: MenuTreeItem
|
||||
): MenuItem {
|
||||
const childNames = menuTreeItem.spec.children.map(
|
||||
(child) => child.metadata.name
|
||||
);
|
||||
return {
|
||||
...menuTreeItem,
|
||||
spec: {
|
||||
...menuTreeItem.spec,
|
||||
children: new Set(childNames),
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue