mirror of https://github.com/halo-dev/halo-admin
feat: add menu component (#534)
parent
4e3f11829b
commit
bd446449c0
|
@ -15,6 +15,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"vue/multi-word-component-names": 0,
|
"vue/multi-word-component-names": 0,
|
||||||
|
"@typescript-eslint/ban-ts-comment": 0,
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|
20
package.json
20
package.json
|
@ -17,37 +17,39 @@
|
||||||
"story:build": "histoire build"
|
"story:build": "histoire build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^2.0.12",
|
"pinia": "^2.0.13",
|
||||||
"vue": "^3.2.31",
|
"vue": "^3.2.31",
|
||||||
"vue-router": "^4.0.14"
|
"vue-router": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/ri": "^1.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.1.1",
|
"@rushstack/eslint-patch": "^1.1.1",
|
||||||
"@types/jsdom": "^16.2.14",
|
"@types/jsdom": "^16.2.14",
|
||||||
"@types/node": "^16.11.26",
|
"@types/node": "^16.11.26",
|
||||||
"@vitejs/plugin-vue": "^2.2.4",
|
"@vitejs/plugin-vue": "^2.3.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.8",
|
"@vitejs/plugin-vue-jsx": "^1.3.9",
|
||||||
"@vue/eslint-config-prettier": "^7.0.0",
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
"@vue/test-utils": "^2.0.0-rc.18",
|
"@vue/test-utils": "^2.0.0-rc.18",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/tsconfig": "^0.1.3",
|
||||||
"autoprefixer": "^10.4.4",
|
"autoprefixer": "^10.4.4",
|
||||||
"c8": "^7.11.0",
|
"c8": "^7.11.0",
|
||||||
"cypress": "^9.5.2",
|
"cypress": "^9.5.3",
|
||||||
"eslint": "^8.11.0",
|
"eslint": "^8.12.0",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
"eslint-plugin-vue": "^8.5.0",
|
"eslint-plugin-vue": "^8.5.0",
|
||||||
"histoire": "^0.2.0",
|
"histoire": "^0.2.1",
|
||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"postcss": "^8.4.12",
|
"postcss": "^8.4.12",
|
||||||
"prettier": "^2.6.0",
|
"prettier": "^2.6.1",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.10",
|
||||||
"start-server-and-test": "^1.14.0",
|
"start-server-and-test": "^1.14.0",
|
||||||
"tailwindcss": "^3.0.23",
|
"tailwindcss": "^3.0.23",
|
||||||
"tailwindcss-themeable": "^1.3.0",
|
"tailwindcss-themeable": "^1.3.0",
|
||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"vite": "^2.8.6",
|
"unplugin-icons": "^0.14.1",
|
||||||
|
"vite": "^2.9.1",
|
||||||
"vitest": "^0.5.9",
|
"vitest": "^0.5.9",
|
||||||
"vue-tsc": "^0.31.4"
|
"vue-tsc": "^0.31.4"
|
||||||
}
|
}
|
||||||
|
|
545
pnpm-lock.yaml
545
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
69
src/App.vue
69
src/App.vue
|
@ -1,7 +1,74 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
|
import { VMenu, VMenuItem, VMenuLabel } from "@/components/base/menu";
|
||||||
|
import { IconDashboard } from "@/core/icons";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<div class="flex">
|
||||||
|
<div class="navbar h-screen w-72 p-4" style="background: #fff">
|
||||||
|
<VMenu :open-ids="['dashboard']">
|
||||||
|
<VMenuLabel>首页</VMenuLabel>
|
||||||
|
<VMenuItem id="dashboard" title="仪表盘">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
</template>
|
</template>
|
||||||
|
<VMenuItem title="子菜单">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="子菜单">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
<VMenuItem title="子菜单">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="子菜单">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="仪表盘">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="仪表盘">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
<VMenuItem title="子菜单1">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="子菜单1">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
<VMenuItem title="子菜单1">
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
body {
|
||||||
|
background: #eff4f9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<Story title="Menu">
|
||||||
|
<Variant title="Playground">
|
||||||
|
<template #default>
|
||||||
|
<VMenu>
|
||||||
|
<VMenuItem>
|
||||||
|
<template #icon>
|
||||||
|
<Component :is="IconDashboard" />
|
||||||
|
</template>
|
||||||
|
仪表盘
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
</template>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VMenu, VMenuItem } from "./index";
|
||||||
|
import { IconDashboard } from "@/core/icons";
|
||||||
|
</script>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { provide } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
openIds: {
|
||||||
|
type: Object as PropType<string[]>,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
provide<string[] | undefined>("openIds", props.openIds);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="menu-container w-full h-full">
|
||||||
|
<ul>
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,121 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { IconArrowRight } from "@/core/icons";
|
||||||
|
import { computed, inject, ref, useSlots } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["select"]);
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
const openIds = inject<string[] | undefined>("openIds");
|
||||||
|
|
||||||
|
if (openIds?.includes(props.id)) {
|
||||||
|
open.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSubmenus = computed(() => {
|
||||||
|
return slots.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (hasSubmenus.value) {
|
||||||
|
open.value = !open.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("select", props.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ 'has-submenus': hasSubmenus }"
|
||||||
|
@click.stop="handleClick"
|
||||||
|
>
|
||||||
|
<div class="menu-item-title">
|
||||||
|
<span v-if="$slots.icon" class="menu-icon self-center mr-3">
|
||||||
|
<slot name="icon" />
|
||||||
|
</span>
|
||||||
|
<span class="menu-title self-center flex-1">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="$slots.default"
|
||||||
|
:class="{ open }"
|
||||||
|
class="menu-icon-collapse self-center transition-all"
|
||||||
|
>
|
||||||
|
<IconArrowRight />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition name="submenus-show">
|
||||||
|
<ul v-show="$slots.default && open" class="sub-menu-items transition-all">
|
||||||
|
<slot />
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.menu-item {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-title {
|
||||||
|
@apply transition-all;
|
||||||
|
@apply text-base;
|
||||||
|
@apply flex;
|
||||||
|
@apply select-none;
|
||||||
|
@apply relative;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
background: #f4f5f7;
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active::after {
|
||||||
|
@apply absolute;
|
||||||
|
top: calc(50% - 13px);
|
||||||
|
left: -8px;
|
||||||
|
width: 3px;
|
||||||
|
height: 26px;
|
||||||
|
content: "";
|
||||||
|
background: #242e41;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon-collapse.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenus-show-enter-active {
|
||||||
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenus-show-leave-active {
|
||||||
|
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenus-show-enter-from,
|
||||||
|
.submenus-show-enter-to {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<li class="menu-label flex flex-col">
|
||||||
|
<slot />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
.menu-label {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 13px;
|
||||||
|
color: #847e7e;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { mount } from "@vue/test-utils";
|
||||||
|
import { VMenu, VMenuItem } from "../index";
|
||||||
|
|
||||||
|
describe("Menu", () => {
|
||||||
|
it("should render", () => {
|
||||||
|
expect(VMenu).toBeDefined();
|
||||||
|
expect(VMenuItem).toBeDefined();
|
||||||
|
expect(mount(VMenu).html()).toMatchSnapshot();
|
||||||
|
expect(mount(VMenuItem).html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with sub menus", async () => {
|
||||||
|
const wrapper = await mount({
|
||||||
|
setup() {
|
||||||
|
return () => (
|
||||||
|
<VMenu openIds={["3"]}>
|
||||||
|
<VMenuItem id="1" title="Menu Item 1" />
|
||||||
|
<VMenuItem id="2" title="Menu Item 2" />
|
||||||
|
<VMenuItem id="3" title="Menu Item 3">
|
||||||
|
<VMenuItem key="4" title="Menu Item 4" />
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
|
||||||
|
// toggling sub menu
|
||||||
|
expect(
|
||||||
|
wrapper.find(".has-submenus .sub-menu-items").attributes().style
|
||||||
|
).toBeUndefined(); // visible
|
||||||
|
|
||||||
|
await wrapper.find(".has-submenus").trigger("click");
|
||||||
|
expect(
|
||||||
|
wrapper.find(".has-submenus .sub-menu-items").attributes().style
|
||||||
|
).toBe("display: none;");
|
||||||
|
|
||||||
|
await wrapper.find(".has-submenus").trigger("click");
|
||||||
|
expect(
|
||||||
|
wrapper.find(".has-submenus .sub-menu-items").attributes().style
|
||||||
|
).toBeUndefined(); // visible
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with openIds prop", function () {
|
||||||
|
const wrapper = mount({
|
||||||
|
setup() {
|
||||||
|
return () => (
|
||||||
|
<VMenu openIds={["3"]}>
|
||||||
|
<VMenuItem id="1" title="Menu Item 1" />
|
||||||
|
<VMenuItem id="2" title="Menu Item 2" />
|
||||||
|
<VMenuItem id="3" title="Menu Item 3">
|
||||||
|
<VMenuItem key="4" title="Menu Item 4" />
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.find(".sub-menu-items .menu-title").text()).contain(
|
||||||
|
"Menu Item 4"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with select emit", async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
setup() {
|
||||||
|
return () => (
|
||||||
|
<VMenu openIds={["3"]}>
|
||||||
|
<VMenuItem id="1" title="Menu Item 1" />
|
||||||
|
<VMenuItem id="2" title="Menu Item 2" />
|
||||||
|
<VMenuItem id="3" title="Menu Item 3">
|
||||||
|
<VMenuItem id="4" title="Menu Item 4" />
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.findAllComponents(VMenuItem).forEach((item) => {
|
||||||
|
// has not sub menu
|
||||||
|
if (item.props().id === "1") {
|
||||||
|
item.trigger("click");
|
||||||
|
expect(item.emitted().select[0]).toEqual(["1"]);
|
||||||
|
}
|
||||||
|
// has sub menu
|
||||||
|
if (item.props().id === "3") {
|
||||||
|
item.trigger("click");
|
||||||
|
expect(item.emitted().select).toBeUndefined();
|
||||||
|
|
||||||
|
expect(item.vm.open).toBe(false);
|
||||||
|
|
||||||
|
item.trigger("click");
|
||||||
|
expect(item.vm.open).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Vitest Snapshot v1
|
||||||
|
|
||||||
|
exports[`Menu > should render 1`] = `
|
||||||
|
"<div class=\\"menu-container w-full h-full\\">
|
||||||
|
<ul></ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Menu > should render 2`] = `
|
||||||
|
"<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\"></span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Menu > should work with openIds prop 1`] = `
|
||||||
|
"<div class=\\"menu-container w-full h-full\\">
|
||||||
|
<ul>
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 1</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 2</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
<li class=\\"menu-item has-submenus\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 3</span><span class=\\"open menu-icon-collapse self-center transition-all\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"m13.172 12l-4.95-4.95l1.414-1.414L16 12l-6.364 6.364l-1.414-1.414z\\"></path></svg></span>
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\">
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 4</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Menu > should work with sub menus 1`] = `
|
||||||
|
"<div class=\\"menu-container w-full h-full\\">
|
||||||
|
<ul>
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 1</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 2</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
<li class=\\"menu-item has-submenus\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 3</span><span class=\\"open menu-icon-collapse self-center transition-all\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"m13.172 12l-4.95-4.95l1.414-1.414L16 12l-6.364 6.364l-1.414-1.414z\\"></path></svg></span>
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\">
|
||||||
|
<li class=\\"menu-item\\">
|
||||||
|
<div class=\\"menu-item-title\\">
|
||||||
|
<!--v-if--><span class=\\"menu-title self-center flex-1\\">Menu Item 4</span>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<transition-stub>
|
||||||
|
<ul class=\\"sub-menu-items transition-all\\" style=\\"display: none;\\"></ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</transition-stub>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>"
|
||||||
|
`;
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as VMenu } from "./Menu.vue";
|
||||||
|
export { default as VMenuItem } from "./MenuItem.vue";
|
||||||
|
export { default as VMenuLabel } from "./MenuLabel.vue";
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import IconDashboard from "~icons/ri/dashboard-3-line";
|
||||||
|
// @ts-ignore
|
||||||
|
import IconArrowRight from "~icons/ri/arrow-right-s-line";
|
||||||
|
|
||||||
|
export { IconDashboard, IconArrowRight };
|
|
@ -1,20 +1,10 @@
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import HomeView from "../views/HomeView.vue";
|
import routesConfig from "@/router/routes.config";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: routesConfig,
|
||||||
{
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
path: "/",
|
|
||||||
name: "home",
|
|
||||||
component: HomeView,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/about",
|
|
||||||
name: "about",
|
|
||||||
component: () => import("../views/AboutView.vue"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { IconDashboard } from "@/core/icons";
|
||||||
|
|
||||||
|
declare interface MenuGroupType {
|
||||||
|
name?: string;
|
||||||
|
items: MenuItemType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface MenuItemType {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
icon?: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
children?: MenuItemType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const menus: MenuGroupType[] = [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "仪表盘",
|
||||||
|
path: "/dashboard",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "内容",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "文章",
|
||||||
|
path: "/posts",
|
||||||
|
icon: IconDashboard,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "分类",
|
||||||
|
path: "/categories",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "标签",
|
||||||
|
path: "/tags",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "页面",
|
||||||
|
path: "/sheets",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "评论",
|
||||||
|
path: "/comment",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "附件",
|
||||||
|
path: "/attachment",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "外观",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "主题",
|
||||||
|
path: "/themes",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "菜单",
|
||||||
|
path: "/menus",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "可视化",
|
||||||
|
path: "/visual",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "系统",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "插件",
|
||||||
|
path: "/plugins",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "用户",
|
||||||
|
path: "/users",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "设置",
|
||||||
|
path: "/settings",
|
||||||
|
icon: IconDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type { MenuItemType, MenuGroupType };
|
||||||
|
|
||||||
|
export default menus;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
|
export const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
component: () => import("../views/HomeView.vue"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/about",
|
||||||
|
name: "about",
|
||||||
|
component: () => import("../views/AboutView.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
|
@ -3,10 +3,11 @@ import { fileURLToPath, URL } from "url";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||||
|
import icons from "unplugin-icons/vite";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), vueJsx()],
|
plugins: [vue(), vueJsx(), icons()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
|
Loading…
Reference in New Issue