feat(projects): 菜单数据及组件接入

This commit is contained in:
Soybean
2021-09-15 07:35:38 +08:00
parent 57e00e6417
commit 3226a724be
25 changed files with 361 additions and 95 deletions

View File

@ -1,8 +1,10 @@
import type { Component } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import type { MenuOption } from 'naive-ui';
import { EnumRoutePath, EnumLoginModule } from '@/enum';
/** 路由描述 */
export interface RouteMeta {
interface RouteMeta {
/** 路由名称 */
title?: string;
/** 页面100%视高 */
@ -10,11 +12,16 @@ export interface RouteMeta {
/** 作为菜单 */
asMenu?: boolean;
/** 菜单和面包屑对应的图标 */
icon?: string;
icon?: Component;
}
export type CustomRoute = RouteRecordRaw & { meta: RouteMeta };
export type RoutePathKey = keyof typeof EnumRoutePath;
export type GlobalMenuOption = MenuOption & {
routeName: string;
routePath: string;
};
export type LoginModuleType = keyof typeof EnumLoginModule;

View File

@ -1,3 +1,3 @@
export { UserInfo } from './business';
export { ThemeSettings, NavMode, AnimateType } from './theme';
export { CustomRoute, RoutePathKey, LoginModuleType } from './common';
export { CustomRoute, RoutePathKey, GlobalMenuOption, LoginModuleType } from './common';

View File

@ -0,0 +1,45 @@
<template>
<n-dropdown :options="options" @select="handleDropdown">
<header-item class="px-12px">
<n-avatar :src="avatar" :round="true" />
<span class="pl-8px text-16px font-medium">Soybean</span>
</header-item>
</n-dropdown>
</template>
<script lang="ts" setup>
import { NDropdown, NAvatar } from 'naive-ui';
import { UserAvatar, Logout } from '@vicons/carbon';
import { dynamicIconRender } from '@/utils';
import HeaderItem from './HeaderItem.vue';
import avatar from '@/assets/img/common/logo-fill.png';
import { useAuthStore } from '@/store';
type DropdownKey = 'user-center' | 'logout';
const { resetAuthState } = useAuthStore();
const options = [
{
label: '用户中心',
key: 'user-center',
icon: dynamicIconRender(UserAvatar)
},
{
type: 'divider',
key: 'divider'
},
{
label: '退出登录',
key: 'logout',
icon: dynamicIconRender(Logout)
}
];
function handleDropdown(key: DropdownKey) {
if (key === 'logout') {
resetAuthState();
}
}
</script>
<style scoped></style>

View File

@ -1,3 +1,4 @@
import UserAvatar from './UserAvatar.vue';
import HeaderItem from './HeaderItem.vue';
export { HeaderItem };
export { UserAvatar, HeaderItem };

View File

@ -10,6 +10,7 @@
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-16px" />
<icon-gridicons-fullscreen v-else class="text-16px" />
</header-item>
<user-avatar />
<header-item class="w-40px h-full" @click="openSettingDrawer">
<icon-mdi-light-cog class="text-16px" />
</header-item>
@ -23,7 +24,7 @@ import { computed } from 'vue';
import { NLayoutHeader } from 'naive-ui';
import { useFullscreen } from '@vueuse/core';
import { useAppStore, useThemeStore } from '@/store';
import { HeaderItem } from './components';
import { UserAvatar, HeaderItem } from './components';
import { GlobalLogo } from '../common';
defineProps({

View File

@ -1,18 +1,21 @@
<template>
<router-link to="/" class="nowrap-hidden flex-center h-64px">
<a :href="homePath" class="nowrap-hidden flex-center h-64px cursor-pointer">
<img src="@/assets/img/common/logo.png" alt="" class="w-32px h-32px" />
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold">{{ title }}</h2>
</router-link>
</a>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useAppStore, useThemeStore } from '@/store';
import { useAppTitle } from '@/hooks';
import { EnumRoutePath } from '@/enum';
const app = useAppStore();
const theme = useThemeStore();
const showTitle = computed(() => !app.menu.collapsed && theme.navStyle.mode !== 'vertical-mix');
const title = useAppTitle();
const showTitle = computed(() => !app.menu.collapsed && theme.navStyle.mode !== 'vertical-mix');
const homePath = EnumRoutePath['dashboard-analysis'];
</script>
<style scoped></style>

View File

@ -1,11 +1,35 @@
<template>
<div>
<h3 class="text-center text-18px text-error">菜单</h3>
<div class="flex-center h-48px">
<router-link to="/login" class="text-primary text-18px">登录页</router-link>
</div>
</div>
<n-menu
:value="activeKey"
:collapsed="app.menu.collapsed"
:collapsed-width="theme.menuStyle.collapsedWidth"
:collapsed-icon-size="22"
:options="menus"
@update:value="handleUpdateMenu"
/>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { NMenu } from 'naive-ui';
import { useThemeStore, useAppStore } from '@/store';
import { menus } from '@/router';
import { GlobalMenuOption } from '@/interface';
const theme = useThemeStore();
const app = useAppStore();
const router = useRouter();
const route = useRoute();
const activeKey = computed(() => getActiveKey());
function getActiveKey() {
return route.name as string;
}
function handleUpdateMenu(key: string, menuItem: GlobalMenuOption) {
router.push(menuItem.routePath);
}
</script>
<style scoped></style>

View File

@ -3,7 +3,7 @@ import App from './App.vue';
import AppProvider from './AppProvider.vue';
import { setupStore } from './store';
import { setupRouter } from './router';
import { setupSmoothScroll, setupWindicssDarkMode } from './plugins';
import { setupWindicssDarkMode } from './plugins';
import 'virtual:windi.css';
import './styles/css/global.css';
@ -27,5 +27,4 @@ async function setupApp() {
setupWindicssDarkMode();
}
setupSmoothScroll();
setupApp();

View File

@ -1,4 +1,3 @@
import setupSmoothScroll from './smooth-scroll';
import setupWindicssDarkMode from './dark-mode';
export { setupSmoothScroll, setupWindicssDarkMode };
export { setupWindicssDarkMode };

View File

@ -1,6 +0,0 @@
import smoothscroll from 'smoothscroll-polyfill';
/** 平滑滚动插件(兼容主流浏览器) */
export default function setupSmoothScroll(): void {
smoothscroll.polyfill();
}

View File

@ -21,3 +21,4 @@ export async function setupRouter(app: App) {
}
export { RouteNameMap };
export { menus } from './menus';

View File

@ -1,5 +1,49 @@
import { CustomRoute } from '@/interface';
import type { Component } from 'vue';
import { customRoutes } from './routes';
import { CustomRoute, GlobalMenuOption } from '@/interface';
import { dynamicIconRender } from '@/utils';
export function transformRouteToMenu(routes: CustomRoute[]) {
return routes;
const globalMenu: GlobalMenuOption[] = [];
routes.forEach(route => {
if (asMenu(route)) {
const { name, path, meta } = route;
const routeName = name as string;
let menuChildren: GlobalMenuOption[] | undefined;
if (route.children) {
menuChildren = transformRouteToMenu(route.children as CustomRoute[]);
}
const menuItem: GlobalMenuOption = addPartialProps(
{
key: routeName,
label: meta?.title ?? routeName,
routeName,
routePath: path
},
meta?.icon,
menuChildren
);
globalMenu.push(menuItem);
}
});
return globalMenu;
}
/** 判断路由是否作为菜单 */
function asMenu(route: CustomRoute) {
return Boolean(route.meta?.asMenu);
}
/** 给菜单添加可选属性 */
function addPartialProps(menuItem: GlobalMenuOption, icon?: Component, children?: GlobalMenuOption[]) {
const item = { ...menuItem };
if (icon) {
Object.assign(item, { icon: dynamicIconRender(icon) });
}
if (children) {
Object.assign(item, { children });
}
return item;
}
export const menus = transformRouteToMenu(customRoutes);

View File

@ -1,4 +1,6 @@
import type { RouteRecordRaw } from 'vue-router';
import { Dashboard } from '@vicons/carbon';
import { ExceptionOutlined } from '@vicons/antd';
import { BasicLayout, BlankLayout } from '@/layouts';
import { EnumRoutePath, EnumRouteTitle } from '@/enum';
import type { CustomRoute, RoutePathKey, LoginModuleType } from '@/interface';
@ -100,7 +102,7 @@ export const customRoutes: CustomRoute[] = [
meta: {
title: EnumRouteTitle.dashboard,
asMenu: true,
icon: 'mdi:view-dashboard'
icon: Dashboard
},
children: [
{
@ -130,7 +132,7 @@ export const customRoutes: CustomRoute[] = [
meta: {
title: EnumRouteTitle.exception,
asMenu: true,
icon: 'ant-design:exception-outlined'
icon: ExceptionOutlined
},
children: [
{
@ -157,7 +159,7 @@ export const customRoutes: CustomRoute[] = [
component: () => import('@/views/system/exception/500.vue'),
meta: {
title: EnumRouteTitle['exception-500'],
asMenu: true
asMenu: false
}
}
]

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { store } from '@/store';
import { removeToken } from '@/utils';
import type { UserInfo } from '@/interface';
interface AuthState {
@ -30,7 +31,9 @@ const authStore = defineStore({
actions: {
/** 重置auth状态 */
resetAuthState() {
removeToken();
this.$reset();
window.location.reload();
}
}
});

View File

@ -1,2 +1,2 @@
export { getToken, setToken, getUserInfo, getLoginModuleRegExp } from './user';
export { getToken, setToken, removeToken, getUserInfo, getLoginModuleRegExp } from './user';
export { getLoginRedirectUrl, toLoginRedirectUrl, toHomeByLocation } from './location';

View File

@ -1,6 +1,6 @@
import { EnumStorageKey } from '@/enum';
import type { LoginModuleType } from '@/interface';
import { setLocal, getLocal } from '../storage';
import { setLocal, getLocal, removeLocal } from '../storage';
/** 设置token */
export function getToken() {
@ -12,6 +12,10 @@ export function setToken(token: string) {
setLocal(EnumStorageKey.token, token);
}
export function removeToken() {
removeLocal(EnumStorageKey.token);
}
export function getUserInfo() {}
/** 获取登录模块的正则字符串 */

12
src/utils/common/icon.ts Normal file
View File

@ -0,0 +1,12 @@
import { h } from 'vue';
import type { Component } from 'vue';
import { NIcon } from 'naive-ui';
/** 动态渲染vicon */
export function dynamicIconRender(icon: Component) {
return () => {
return h(NIcon, null, {
default: () => h(icon)
});
};
}

View File

@ -13,3 +13,5 @@ export {
} from './typeof';
export { brightenColor, darkenColor } from './color';
export { dynamicIconRender } from './icon';

View File

@ -1,6 +1,7 @@
export {
setToken,
getToken,
removeToken,
getUserInfo,
getLoginModuleRegExp,
getLoginRedirectUrl,
@ -21,7 +22,8 @@ export {
isSet,
isMap,
brightenColor,
darkenColor
darkenColor,
dynamicIconRender
} from './common';
export { setLocal, getLocal, setSession, getSession } from './storage';

View File

@ -1,2 +1,2 @@
export { setLocal, getLocal } from './local';
export { setLocal, getLocal, removeLocal } from './local';
export { setSession, getSession } from './session';

View File

@ -1,12 +1,16 @@
export function setLocal(key: string, value: unknown) {
const json = JSON.stringify(value);
localStorage.setItem(key, json);
window.localStorage.setItem(key, json);
}
export function getLocal<T>(key: string) {
const json = localStorage.getItem(key);
const json = window.localStorage.getItem(key);
if (json) {
return JSON.parse(json) as T;
}
return json;
}
export function removeLocal(key: string) {
window.localStorage.removeItem(key);
}

View File

@ -2,20 +2,10 @@
<div>
<h2>工作台</h2>
<router-link :to="EnumRoutePath['dashboard-analysis']">analysis</router-link>
<n-button @click="removeCurrent">去除</n-button>
</div>
</template>
<script lang="ts" setup>
import { NButton } from 'naive-ui';
import { useRouter } from 'vue-router';
import { EnumRoutePath } from '@/enum';
import { RouteNameMap } from '@/router';
const router = useRouter();
function removeCurrent() {
router.removeRoute(RouteNameMap.get('dashboard-workbench')!);
}
</script>
<style scoped></style>

View File

@ -1,6 +1,5 @@
<template>
<div class="relative flex-center w-full h-full bg-[#DBE0F9]">
<login-bg />
<div class="w-400px p-40px bg-white rounded-20px z-10">
<header class="flex-y-center justify-between">
<div class="w-70px h-70px rounded-35px overflow-hidden">
@ -15,6 +14,7 @@
</div>
</main>
</div>
<login-bg />
</div>
</template>