mirror of https://github.com/halo-dev/halo
parent
0826f51f67
commit
9fef47f291
|
@ -34,11 +34,12 @@
|
||||||
"@formkit/vue": "1.0.0-beta.10",
|
"@formkit/vue": "1.0.0-beta.10",
|
||||||
"@halo-dev/admin-api": "^1.1.0",
|
"@halo-dev/admin-api": "^1.1.0",
|
||||||
"@halo-dev/admin-shared": "workspace:*",
|
"@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/components": "workspace:*",
|
||||||
"@halo-dev/richtext-editor": "0.0.0-alpha.1",
|
"@halo-dev/richtext-editor": "0.0.0-alpha.1",
|
||||||
"@vueuse/components": "^8.9.4",
|
"@vueuse/components": "^8.9.4",
|
||||||
"@vueuse/core": "^8.9.4",
|
"@vueuse/core": "^8.9.4",
|
||||||
|
"@vueuse/router": "^9.1.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"filepond": "^4.30.4",
|
"filepond": "^4.30.4",
|
||||||
"filepond-plugin-image-preview": "^4.6.11",
|
"filepond-plugin-image-preview": "^4.6.11",
|
||||||
|
@ -52,6 +53,7 @@
|
||||||
"vue-filepond": "^7.0.3",
|
"vue-filepond": "^7.0.3",
|
||||||
"vue-grid-layout": "3.0.0-beta1",
|
"vue-grid-layout": "3.0.0-beta1",
|
||||||
"vue-router": "^4.1.3",
|
"vue-router": "^4.1.3",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
"yaml": "^2.1.1"
|
"yaml": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
|
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@halo-dev/api-client": "^0.0.11",
|
"@halo-dev/api-client": "^0.0.10",
|
||||||
"@halo-dev/components": "workspace:*",
|
"@halo-dev/components": "workspace:*",
|
||||||
"axios": "^0.27.2"
|
"axios": "^0.27.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
V1alpha1RoleBindingApi,
|
V1alpha1RoleBindingApi,
|
||||||
V1alpha1SettingApi,
|
V1alpha1SettingApi,
|
||||||
V1alpha1UserApi,
|
V1alpha1UserApi,
|
||||||
|
V1alpha1MenuApi,
|
||||||
|
V1alpha1MenuItemApi,
|
||||||
ThemeHaloRunV1alpha1ThemeApi,
|
ThemeHaloRunV1alpha1ThemeApi,
|
||||||
ApiHaloRunV1alpha1ThemeApi,
|
ApiHaloRunV1alpha1ThemeApi,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
|
@ -61,10 +63,8 @@ function setupApiClient(axios: AxiosInstance) {
|
||||||
plugin: new PluginHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
plugin: new PluginHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
||||||
user: new V1alpha1UserApi(undefined, apiUrl, axios),
|
user: new V1alpha1UserApi(undefined, apiUrl, axios),
|
||||||
theme: new ThemeHaloRunV1alpha1ThemeApi(undefined, apiUrl, axios),
|
theme: new ThemeHaloRunV1alpha1ThemeApi(undefined, apiUrl, axios),
|
||||||
|
menu: new V1alpha1MenuApi(undefined, apiUrl, axios),
|
||||||
// TODO optional
|
menuItem: new V1alpha1MenuItemApi(undefined, apiUrl, axios),
|
||||||
// link: new CoreHaloRunV1alpha1LinkApi(undefined, apiUrl, axios),
|
|
||||||
// linkGroup: new CoreHaloRunV1alpha1LinkGroupApi(undefined, apiUrl, axios),
|
|
||||||
},
|
},
|
||||||
user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios),
|
user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios),
|
||||||
plugin: new ApiHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
plugin: new ApiHaloRunV1alpha1PluginApi(undefined, apiUrl, axios),
|
||||||
|
|
|
@ -14,7 +14,7 @@ importers:
|
||||||
'@formkit/vue': 1.0.0-beta.10
|
'@formkit/vue': 1.0.0-beta.10
|
||||||
'@halo-dev/admin-api': ^1.1.0
|
'@halo-dev/admin-api': ^1.1.0
|
||||||
'@halo-dev/admin-shared': workspace:*
|
'@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/components': workspace:*
|
||||||
'@halo-dev/richtext-editor': 0.0.0-alpha.1
|
'@halo-dev/richtext-editor': 0.0.0-alpha.1
|
||||||
'@rushstack/eslint-patch': ^1.1.4
|
'@rushstack/eslint-patch': ^1.1.4
|
||||||
|
@ -35,6 +35,7 @@ importers:
|
||||||
'@vue/tsconfig': ^0.1.3
|
'@vue/tsconfig': ^0.1.3
|
||||||
'@vueuse/components': ^8.9.4
|
'@vueuse/components': ^8.9.4
|
||||||
'@vueuse/core': ^8.9.4
|
'@vueuse/core': ^8.9.4
|
||||||
|
'@vueuse/router': ^9.1.0
|
||||||
autoprefixer: ^10.4.8
|
autoprefixer: ^10.4.8
|
||||||
axios: ^0.27.2
|
axios: ^0.27.2
|
||||||
c8: ^7.12.0
|
c8: ^7.12.0
|
||||||
|
@ -74,6 +75,7 @@ importers:
|
||||||
vue-grid-layout: 3.0.0-beta1
|
vue-grid-layout: 3.0.0-beta1
|
||||||
vue-router: ^4.1.3
|
vue-router: ^4.1.3
|
||||||
vue-tsc: ^0.39.5
|
vue-tsc: ^0.39.5
|
||||||
|
vuedraggable: ^4.1.0
|
||||||
yaml: ^2.1.1
|
yaml: ^2.1.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formkit/addons': 1.0.0-beta.10_vue@3.2.37
|
'@formkit/addons': 1.0.0-beta.10_vue@3.2.37
|
||||||
|
@ -85,11 +87,12 @@ importers:
|
||||||
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
|
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
|
||||||
'@halo-dev/admin-api': 1.1.0
|
'@halo-dev/admin-api': 1.1.0
|
||||||
'@halo-dev/admin-shared': link:packages/shared
|
'@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/components': link:packages/components
|
||||||
'@halo-dev/richtext-editor': 0.0.0-alpha.1_vue@3.2.37
|
'@halo-dev/richtext-editor': 0.0.0-alpha.1_vue@3.2.37
|
||||||
'@vueuse/components': 8.9.4_vue@3.2.37
|
'@vueuse/components': 8.9.4_vue@3.2.37
|
||||||
'@vueuse/core': 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
|
axios: 0.27.2
|
||||||
filepond: 4.30.4
|
filepond: 4.30.4
|
||||||
filepond-plugin-image-preview: 4.6.11_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-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
|
||||||
vue-grid-layout: 3.0.0-beta1
|
vue-grid-layout: 3.0.0-beta1
|
||||||
vue-router: 4.1.3_vue@3.2.37
|
vue-router: 4.1.3_vue@3.2.37
|
||||||
|
vuedraggable: 4.1.0_vue@3.2.37
|
||||||
yaml: 2.1.1
|
yaml: 2.1.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@changesets/cli': 2.24.2
|
'@changesets/cli': 2.24.2
|
||||||
|
@ -176,12 +180,12 @@ importers:
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@halo-dev/api-client': ^0.0.11
|
'@halo-dev/api-client': ^0.0.10
|
||||||
'@halo-dev/components': workspace:*
|
'@halo-dev/components': workspace:*
|
||||||
axios: ^0.27.2
|
axios: ^0.27.2
|
||||||
vite-plugin-dts: ^1.4.1
|
vite-plugin-dts: ^1.4.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@halo-dev/api-client': 0.0.11
|
'@halo-dev/api-client': 0.0.10
|
||||||
'@halo-dev/components': link:../components
|
'@halo-dev/components': link:../components
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -2126,8 +2130,8 @@ packages:
|
||||||
- debug
|
- debug
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/api-client/0.0.11:
|
/@halo-dev/api-client/0.0.10:
|
||||||
resolution: {integrity: sha512-67e7lrWPoHVKF4XItT6OWr7rX96CR9s/3SOVXYD3xCyAMIgLyFULfKVI7JGxisFMKvadTx7rm5RK4zzZ/CbXIw==}
|
resolution: {integrity: sha512-DKQKkEAKMR/rbopI6jbjbzLiYUZeY6dOcgqGoDGG8MAcwkWOI6iWaZnuR5z+X8vd51XjiPhnekAphfcO6PaWEQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/logger/1.1.0:
|
/@halo-dev/logger/1.1.0:
|
||||||
|
@ -3653,6 +3657,19 @@ packages:
|
||||||
resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==}
|
resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==}
|
||||||
dev: false
|
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:
|
/@vueuse/shared/8.9.4_vue@3.2.37:
|
||||||
resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==}
|
resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -3668,6 +3685,15 @@ packages:
|
||||||
vue-demi: 0.12.1_vue@3.2.37
|
vue-demi: 0.12.1_vue@3.2.37
|
||||||
dev: false
|
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:
|
/abab/2.0.6:
|
||||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -8195,6 +8221,10 @@ packages:
|
||||||
yargs: 15.4.1
|
yargs: 15.4.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sortablejs/1.14.0:
|
||||||
|
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/source-map-js/1.0.2:
|
/source-map-js/1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -9279,6 +9309,15 @@ packages:
|
||||||
'@vue/server-renderer': 3.2.37_vue@3.2.37
|
'@vue/server-renderer': 3.2.37_vue@3.2.37
|
||||||
'@vue/shared': 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:
|
/w3c-hr-time/1.0.2:
|
||||||
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
||||||
dependencies:
|
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 { BasicLayout, definePlugin } from "@halo-dev/admin-shared";
|
||||||
import MenuList from "./MenuList.vue";
|
import Menus from "./Menus.vue";
|
||||||
import { IconListSettings } from "@halo-dev/components";
|
import { IconListSettings } from "@halo-dev/components";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
@ -13,7 +13,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
name: "Menus",
|
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