feat(projects): theme store完成

This commit is contained in:
Soybean
2022-01-08 20:49:21 +08:00
parent 10e4d81bd6
commit bf020a8258
56 changed files with 1205 additions and 164 deletions

View File

@ -56,6 +56,11 @@ module.exports = {
group: 'internal', group: 'internal',
position: 'before' position: 'before'
}, },
{
pattern: '@/settings',
group: 'internal',
position: 'before'
},
{ {
pattern: '@/enum', pattern: '@/enum',
group: 'internal', group: 'internal',

8
build/define/index.ts Normal file
View File

@ -0,0 +1,8 @@
import dayjs from 'dayjs';
/** 项目构建时间 */
const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'));
export const define = {
PROJECT_BUILD_TIME
};

View File

@ -1 +1,2 @@
export * from './plugins'; export * from './plugins';
export * from './define';

View File

@ -4,7 +4,7 @@ const routes: AuthRoute.Route[] = [
{ {
name: 'dashboard', name: 'dashboard',
path: '/dashboard', path: '/dashboard',
component: 'layout', component: 'basic',
children: [ children: [
{ {
name: 'dashboard_analysis', name: 'dashboard_analysis',
@ -36,7 +36,7 @@ const routes: AuthRoute.Route[] = [
component: 'self', component: 'self',
meta: { meta: {
title: '关于', title: '关于',
singleLayout: 'layout', singleLayout: 'basic',
permissions: ['super', 'admin', 'test'], permissions: ['super', 'admin', 'test'],
icon: 'fluent:book-information-24-regular' icon: 'fluent:book-information-24-regular'
} }
@ -44,7 +44,7 @@ const routes: AuthRoute.Route[] = [
{ {
name: 'multi-menu', name: 'multi-menu',
path: '/multi-menu', path: '/multi-menu',
component: 'layout', component: 'basic',
children: [ children: [
{ {
name: 'multi-menu_first', name: 'multi-menu_first',

View File

@ -1,6 +1,6 @@
{ {
"name": "soybean-admin-thin", "name": "soybean-admin-thin",
"version": "0.0.1", "version": "0.1.0",
"scripts": { "scripts": {
"dev": "cross-env VITE_HTTP_ENV=test vite", "dev": "cross-env VITE_HTTP_ENV=test vite",
"dev:prod": "cross-env VITE_HTTP_ENV=prod vite", "dev:prod": "cross-env VITE_HTTP_ENV=prod vite",

View File

@ -0,0 +1,10 @@
<template>
<div
class="bg-white text-[#333639] dark:(bg-[#18181c] text-white text-opacity-82) transition-all duration-300 ease-in-out"
>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,5 +1,6 @@
import NaiveProvider from './NaiveProvider/index.vue'; import NaiveProvider from './NaiveProvider/index.vue';
import SystemLogo from './SystemLogo/index.vue'; import SystemLogo from './SystemLogo/index.vue';
import DarkModeSwitch from './DarkModeSwitch/index.vue'; import DarkModeSwitch from './DarkModeSwitch/index.vue';
import DarkModeContainer from './DarkModeContainer/index.vue';
export { NaiveProvider, SystemLogo, DarkModeSwitch }; export { NaiveProvider, SystemLogo, DarkModeSwitch, DarkModeContainer };

View File

@ -2,3 +2,4 @@ export * from './typeof';
export * from './storage'; export * from './storage';
export * from './service'; export * from './service';
export * from './system'; export * from './system';
export * from './theme';

30
src/enum/common/theme.ts Normal file
View File

@ -0,0 +1,30 @@
/** 布局模式 */
export enum EnumThemeLayoutMode {
'vertical' = '左侧菜单模式',
'horizontal' = '顶部菜单模式',
'vertical-mix' = '左侧菜单混合模式',
'horizontal-mix' = '顶部菜单混合模式'
}
/** 多页签风格 */
export enum EnumThemeTabMode {
'chrome' = '谷歌风格',
'button' = '按钮风格'
}
/** 水平模式的菜单位置 */
export enum EnumThemeHorizontalMenuPosition {
'flex-start' = '居左',
'center' = '居中',
'flex-end' = '居右'
}
/** 过渡动画类型 */
export enum EnumThemeAnimateMode {
'zoom-fade' = '渐变',
'zoom-out' = '闪现',
'fade-slide' = '滑动',
'fade' = '消退',
'fade-bottom' = '底部消退',
'fade-scale' = '缩放消退'
}

View File

@ -3,5 +3,6 @@ import useBoolean from './useBoolean';
import useLoading from './useLoading'; import useLoading from './useLoading';
import useLoadingEmpty from './useLoadingEmpty'; import useLoadingEmpty from './useLoadingEmpty';
import useReload from './useReload'; import useReload from './useReload';
import useModalVisible from './useModalVisible';
export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload }; export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload, useModalVisible };

View File

@ -0,0 +1,34 @@
import { watch, onUnmounted } from 'vue';
import useBoolean from './useBoolean';
/**
* 使用弹窗
* @param hide - 关闭html滚动条
*/
export default function useModalVisible(hideScroll = true) {
const { bool: visible, setTrue: openModal, setFalse: closeModal, toggle: toggleModal } = useBoolean();
const stopHandle = watch(visible, async newValue => {
if (hideScroll) {
const className = 'overflow-hidden';
if (newValue) {
document.body.classList.add(className);
} else {
setTimeout(() => {
document.body.classList.remove(className);
}, 300);
}
}
});
onUnmounted(() => {
stopHandle();
});
return {
visible,
openModal,
closeModal,
toggleModal
};
}

View File

@ -1,4 +1,22 @@
import { EnumLoginModule } from '@/enum'; import {
EnumThemeLayoutMode,
EnumThemeTabMode,
EnumThemeHorizontalMenuPosition,
EnumThemeAnimateMode,
EnumLoginModule
} from '@/enum';
/** 布局模式 */
export type ThemeLayoutMode = keyof typeof EnumThemeLayoutMode;
/** 多页签风格 */
export type ThemeTabMode = keyof typeof EnumThemeTabMode;
/** 水平模式的菜单位置 */
export type ThemeHorizontalMenuPosition = keyof typeof EnumThemeHorizontalMenuPosition;
/** 过渡动画 */
export type ThemeAnimateMode = keyof typeof EnumThemeAnimateMode;
/** 登录模块 */ /** 登录模块 */
export type LoginModuleKey = keyof typeof EnumLoginModule; export type LoginModuleKey = keyof typeof EnumLoginModule;

View File

@ -1,2 +1,3 @@
export * from './enum'; export * from './enum';
export * from './theme';
export * from './system'; export * from './system';

137
src/interface/theme.ts Normal file
View File

@ -0,0 +1,137 @@
import { EnumThemeTabMode, EnumThemeHorizontalMenuPosition, EnumThemeAnimateMode } from '@/enum';
import type { ThemeLayoutMode, ThemeTabMode, ThemeHorizontalMenuPosition, ThemeAnimateMode } from './enum';
/** 主题相关类型 */
export interface ThemeSetting {
/** 暗黑模式 */
darkMode: boolean;
/** 布局样式 */
layout: ThemeLayout;
/** 主题颜色 */
themeColor: string;
/** 主题颜色列表 */
themeColorList: string[];
/** 其他颜色 */
otherColor: ThemeOtherColor;
/** 固定头部和多页签 */
fixedHeaderAndTab: boolean;
/** 显示重载按钮 */
showReload: boolean;
/** 头部样式 */
header: ThemeHeader;
/** 标多页签样式 */
tab: ThemeTab;
/** 侧边栏样式 */
sider: ThemeSider;
/** 菜单样式 */
menu: ThemeMenu;
/** 底部样式 */
footer: ThemeFooter;
/** 页面样式 */
page: ThemePage;
}
/** 布局样式 */
interface ThemeLayout {
/** 最小宽度 */
minWidth: number;
/** 布局模式 */
mode: ThemeLayoutMode;
}
/** 其他主题颜色 */
interface ThemeOtherColor {
/** 信息 */
info: string;
/** 成功 */
success: string;
/** 警告 */
warning: string;
/** 错误 */
error: string;
}
/** 头部样式 */
interface ThemeHeader {
/** 头部高度 */
height: number;
/** 面包屑样式 */
crumb: ThemeCrumb;
}
/** 面包屑样式 */
interface ThemeCrumb {
/** 面包屑可见 */
visible: boolean;
/** 显示图标 */
showIcon: boolean;
}
/** 标多页签样式 */
export interface ThemeTab {
/** 多页签可见 */
visible: boolean;
/** 多页签高度 */
height: number;
/** 多页签风格 */
mode: ThemeTabMode;
/** 多页签风格列表 */
modeList: ThemeTabModeList[];
/** 开启多页签缓存 */
isCache: boolean;
}
/** 多页签风格列表 */
interface ThemeTabModeList {
value: ThemeTabMode;
label: EnumThemeTabMode;
}
/** 侧边栏样式 */
interface ThemeSider {
/** 侧边栏宽度 */
width: number;
/** 侧边栏折叠时的宽度 */
collapsedWidth: number;
/** vertical-mix模式下侧边栏宽度 */
mixWidth: number;
/** vertical-mix模式下侧边栏折叠时的宽度 */
mixCollapsedWidth: number;
/** vertical-mix模式下侧边栏的子菜单的宽度 */
mixChildMenuWidth: number;
}
/** 菜单样式 */
interface ThemeMenu {
/** 水平模式的菜单的位置 */
horizontalPosition: ThemeHorizontalMenuPosition;
/** 水平模式的菜单的位置列表 */
horizontalPositionList: ThemeHorizontalMenuPositionList[];
}
/** 水平模式的菜单的位置列表 */
interface ThemeHorizontalMenuPositionList {
value: ThemeHorizontalMenuPosition;
label: EnumThemeHorizontalMenuPosition;
}
/** 底部样式 */
interface ThemeFooter {
/** 是否固定底部 */
fixed: boolean;
/** 底部高度 */
height: number;
}
/** 页面样式 */
interface ThemePage {
/** 页面是否开启动画 */
animate: boolean;
/** 动画类型 */
animateMode: ThemeAnimateMode;
/** 动画类型列表 */
animateModeList: ThemeAnimateModeList[];
}
/** 动画类型列表 */
interface ThemeAnimateModeList {
value: ThemeAnimateMode;
label: EnumThemeAnimateMode;
}

View File

@ -0,0 +1,69 @@
<template>
<soybean-layout
:mode="mode"
:fixed-header-and-tab="theme.fixedHeaderAndTab"
:header-height="theme.header.height"
:tab-visible="theme.tab.visible"
:tab-height="theme.tab.height"
:sider-visible="siderVisible"
:sider-width="siderWidth"
:sider-collapsed-width="siderCollapsedWidth"
:sider-collapse="false"
:fixed-footer="theme.footer.fixed"
>
<template #header>
<global-header />
</template>
<template #tab>
<global-tab />
</template>
<template #sider>
<global-sider />
</template>
<global-content />
<template #footer>
<global-footer />
</template>
</soybean-layout>
<setting-drawer />
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useAppStore, useThemeStore } from '@/store';
import { SoybeanLayout } from '@/package';
import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common';
const app = useAppStore();
const theme = useThemeStore();
const siderVisible = computed(() => theme.layout.mode !== 'horizontal');
type LayoutMode = 'vertical' | 'horizontal';
const mode = computed(() => {
const vertical: LayoutMode = 'vertical';
const horizontal: LayoutMode = 'horizontal';
return theme.layout.mode.includes(vertical) ? vertical : horizontal;
});
const siderWidth = computed(() => {
const { width, mixWidth, mixChildMenuWidth } = theme.sider;
const isVerticalMix = theme.layout.mode === 'vertical-mix';
let w = isVerticalMix ? mixWidth : width;
if (isVerticalMix && app.mixSiderFixed) {
w += mixChildMenuWidth;
}
return w;
});
const siderCollapsedWidth = computed(() => {
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = theme.sider;
const isVerticalMix = theme.layout.mode === 'vertical-mix';
let w = isVerticalMix ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix && app.mixSiderFixed) {
w += mixChildMenuWidth;
}
return w;
});
</script>
<style scoped></style>

View File

@ -0,0 +1,8 @@
<template>
<global-content />
</template>
<script setup lang="ts">
import { GlobalContent } from '../common';
</script>
<style scoped></style>

View File

@ -1,81 +1,69 @@
<template> <template>
<soybean-layout :mode="mode" :fixed-header-and-tab="fixed" :fixed-footer="fixedFooter" :sider-collapse="collapse"> <soybean-layout
:mode="mode"
:fixed-header-and-tab="theme.fixedHeaderAndTab"
:header-height="theme.header.height"
:tab-visible="theme.tab.visible"
:tab-height="theme.tab.height"
:sider-visible="siderVisible"
:sider-width="siderWidth"
:sider-collapsed-width="siderCollapsedWidth"
:sider-collapse="false"
:fixed-footer="theme.footer.fixed"
>
<template #header> <template #header>
<div class="flex justify-end h-full bg-red-600"> <global-header />
<h3 class="text-white">Header</h3>
</div>
</template> </template>
<template #tab> <template #tab>
<div class="h-full bg-green-600"></div> <global-tab />
</template> </template>
<template #sider> <template #sider>
<div class="w-full h-full bg-gray-200"> <global-sider />
<n-space :vertical="true" align="center" class="pt-24px">
<n-button type="primary" @click="toggle">折叠</n-button>
<div>
<span class="pr-12px">固定头部和标签</span>
<n-switch v-model:value="fixed" />
</div>
<div>
<span class="pr-12px">固定底部</span>
<n-switch v-model:value="fixedFooter" />
</div>
<div>
<span class="pr-12px">vertical布局</span>
<n-radio-group v-model:value="mode">
<n-radio v-for="item in radios" :key="item.value" :value="item.value">
{{ item.label }}
</n-radio>
</n-radio-group>
</div>
</n-space>
</div>
</template> </template>
<global-content /> <global-content />
<template #footer> <template #footer>
<div class="h-full bg-blue-400"> <global-footer />
<h3>footer</h3>
</div>
</template> </template>
</soybean-layout> </soybean-layout>
<setting-drawer /> <setting-drawer />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { computed } from 'vue';
import { NSpace, NButton, NSwitch, NRadioGroup, NRadio } from 'naive-ui'; import { useAppStore, useThemeStore } from '@/store';
import { useElementSize } from '@vueuse/core';
import { useBoolean } from '@/hooks';
import { SoybeanLayout } from '@/package'; import { SoybeanLayout } from '@/package';
import { SettingDrawer, GlobalContent } from '../common'; import { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter } from '../common';
const app = useAppStore();
const theme = useThemeStore();
const siderVisible = computed(() => theme.layout.mode !== 'horizontal');
type LayoutMode = 'vertical' | 'horizontal'; type LayoutMode = 'vertical' | 'horizontal';
const mode = computed(() => {
const vertical: LayoutMode = 'vertical';
const horizontal: LayoutMode = 'horizontal';
return theme.layout.mode.includes(vertical) ? vertical : horizontal;
});
interface ModeRadio { const siderWidth = computed(() => {
value: LayoutMode; const { width, mixWidth, mixChildMenuWidth } = theme.sider;
label: string; const isVerticalMix = theme.layout.mode === 'vertical-mix';
} let w = isVerticalMix ? mixWidth : width;
if (isVerticalMix && app.mixSiderFixed) {
const { width } = useElementSize(document.documentElement); w += mixChildMenuWidth;
const { bool: collapse, toggle } = useBoolean();
const minWidthOfLayout = 1200;
const fixed = ref(true);
const fixedFooter = ref(true);
const mode = ref<LayoutMode>('vertical');
const radios: ModeRadio[] = [
{ value: 'vertical', label: 'vertical' },
{ value: 'horizontal', label: 'horizontal' }
];
watch(width, newValue => {
if (newValue < minWidthOfLayout) {
document.documentElement.style.overflowX = 'auto';
} else {
document.documentElement.style.overflowX = 'hidden';
} }
return w;
});
const siderCollapsedWidth = computed(() => {
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = theme.sider;
const isVerticalMix = theme.layout.mode === 'vertical-mix';
let w = isVerticalMix ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix && app.mixSiderFixed) {
w += mixChildMenuWidth;
}
return w;
}); });
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -0,0 +1,10 @@
<template>
<dark-mode-container class="flex-center h-full">
<span>Copyright ©2021 Soybean Admin</span>
</dark-mode-container>
</template>
<script setup lang="ts">
import { DarkModeContainer } from '@/components';
</script>
<style scoped></style>

View File

@ -0,0 +1,12 @@
<template>
<dark-mode-container class="global-header flex-y-center h-full"></dark-mode-container>
</template>
<script setup lang="ts">
import { DarkModeContainer } from '@/components';
</script>
<style scoped>
.global-header {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<dark-mode-container class="global-sider flex-y-center h-full"></dark-mode-container>
</template>
<script setup lang="ts">
import { DarkModeContainer } from '@/components';
</script>
<style scoped>
.global-sider {
box-shadow: 2px 0 8px 0 rgb(29 35 41 / 5%);
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<dark-mode-container class="global-tab flex-y-center h-full"></dark-mode-container>
</template>
<script setup lang="ts">
import { DarkModeContainer } from '@/components';
</script>
<style scoped>
.global-tab {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
}
</style>

View File

@ -1,10 +1,12 @@
<template> <template>
<n-button <n-button
class="fixed top-240px right-14px z-10000" type="primary"
:class="{ '!right-330px': app.settingDrawerVisible }" :class="[{ '!right-330px': app.settingDrawerVisible }, app.settingDrawerVisible ? 'ease-out' : 'ease-in']"
class="fixed top-240px right-14px z-10000 w-42px h-42px !p-0 transition-all duration-300"
@click="toggleSettingdrawerVisible" @click="toggleSettingdrawerVisible"
> >
点击 <icon-ant-design:close-outlined v-if="app.settingDrawerVisible" class="text-24px" />
<icon-ant-design:setting-outlined v-else class="text-24px" />
</n-button> </n-button>
</template> </template>

View File

@ -1,4 +1,8 @@
import SettingDrawer from './SettingDrawer/index.vue'; import SettingDrawer from './SettingDrawer/index.vue';
import GlobalHeader from './GlobalHeader/index.vue';
import GlobalTab from './GlobalTab/index.vue';
import GlobalSider from './GlobalSider/index.vue';
import GlobalContent from './GlobalContent/index.vue'; import GlobalContent from './GlobalContent/index.vue';
import GlobalFooter from './GlobalFooter/index.vue';
export { SettingDrawer, GlobalContent }; export { SettingDrawer, GlobalHeader, GlobalTab, GlobalSider, GlobalContent, GlobalFooter };

View File

@ -1,3 +1,5 @@
import Layout from './Layout/index.vue'; import Layout from './Layout/index.vue';
import BasicLayout from './BasicLayout/index.vue';
import BlankLayout from './BlankLayout/index.vue';
export { Layout }; export { Layout, BasicLayout, BlankLayout };

View File

@ -0,0 +1,3 @@
import SoybeanLayout from './src/index.vue';
export default SoybeanLayout;

View File

@ -1,7 +1,7 @@
<template> <template>
<header class="soybean-layout__footer" :style="style"> <footer class="soybean-layout__footer" :style="style">
<slot></slot> <slot></slot>
</header> </footer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -1,3 +1,3 @@
import SoybeanLayout from './SoybeanLayout/index.vue'; import SoybeanLayout from './SoybeanLayout';
export { SoybeanLayout }; export { SoybeanLayout };

View File

@ -1,7 +1,7 @@
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'; import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
import { routeName } from '@/router'; import { routeName } from '@/router';
import { useAuthStore, useRouteStore } from '@/store'; import { useAuthStore, useRouteStore } from '@/store';
import { exeStrategyActions } from '@/utils'; import { exeStrategyActions, getToken } from '@/utils';
/** 处理路由页面的权限 */ /** 处理路由页面的权限 */
export async function handlePagePermission( export async function handlePagePermission(
@ -14,6 +14,7 @@ export async function handlePagePermission(
const route = useRouteStore(); const route = useRouteStore();
const { initDynamicRoute } = useRouteStore(); const { initDynamicRoute } = useRouteStore();
const isLogin = Boolean(getToken());
const permissions = to.meta.permissions || []; const permissions = to.meta.permissions || [];
const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length); const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length);
const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole); const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole);
@ -38,7 +39,7 @@ export async function handlePagePermission(
const actions: Common.StrategyAction[] = [ const actions: Common.StrategyAction[] = [
// 已登录状态跳转登录页,跳转至首页 // 已登录状态跳转登录页,跳转至首页
[ [
auth.isLogin && to.name === routeName('login'), isLogin && to.name === routeName('login'),
() => { () => {
next({ name: routeName('root') }); next({ name: routeName('root') });
} }
@ -52,7 +53,7 @@ export async function handlePagePermission(
], ],
// 未登录状态进入需要登录权限的页面 // 未登录状态进入需要登录权限的页面
[ [
!auth.isLogin && needLogin, !isLogin && needLogin,
() => { () => {
const redirect = to.fullPath; const redirect = to.fullPath;
next({ name: routeName('login'), query: { redirect } }); next({ name: routeName('login'), query: { redirect } });
@ -60,14 +61,14 @@ export async function handlePagePermission(
], ],
// 登录状态进入需要登录权限的页面,有权限直接通行 // 登录状态进入需要登录权限的页面,有权限直接通行
[ [
auth.isLogin && needLogin && hasPermission, isLogin && needLogin && hasPermission,
() => { () => {
next(); next();
} }
], ],
[ [
// 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面 // 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面
auth.isLogin && needLogin && !hasPermission, isLogin && needLogin && !hasPermission,
() => { () => {
next({ name: routeName('no-permission') }); next({ name: routeName('no-permission') });
} }

1
src/settings/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './theme';

0
src/settings/theme.json Normal file
View File

91
src/settings/theme.ts Normal file
View File

@ -0,0 +1,91 @@
import { EnumThemeTabMode, EnumThemeHorizontalMenuPosition, EnumThemeAnimateMode } from '@/enum';
import type { ThemeSetting } from '@/interface';
const themeColorList = [
'#409EFF',
'#2d8cf0',
'#0960bd',
'#009688',
'#536dfe',
'#ff5c93',
'#ee4f12',
'#0096c7',
'#9c27b0',
'#ff9800',
'#FF3D68',
'#00C1D4',
'#71EFA3',
'#171010',
'#78DEC7',
'#1768AC',
'#FB9300',
'#FC5404'
];
const defaultThemeSetting: ThemeSetting = {
darkMode: false,
layout: {
minWidth: 900,
mode: 'vertical'
},
themeColor: themeColorList[0],
themeColorList,
otherColor: {
info: '#2080f0',
success: '#67C23A',
warning: '#E6A23C',
error: '#F56C6C'
},
fixedHeaderAndTab: true,
showReload: true,
header: {
height: 56,
crumb: {
visible: true,
showIcon: true
}
},
tab: {
visible: true,
height: 44,
mode: 'chrome',
modeList: [
{ value: 'chrome', label: EnumThemeTabMode.chrome },
{ value: 'button', label: EnumThemeTabMode.button }
],
isCache: true
},
sider: {
width: 200,
collapsedWidth: 64,
mixWidth: 80,
mixCollapsedWidth: 48,
mixChildMenuWidth: 200
},
menu: {
horizontalPosition: 'flex-start',
horizontalPositionList: [
{ value: 'flex-start', label: EnumThemeHorizontalMenuPosition['flex-start'] },
{ value: 'center', label: EnumThemeHorizontalMenuPosition.center },
{ value: 'flex-end', label: EnumThemeHorizontalMenuPosition['flex-end'] }
]
},
footer: {
fixed: true,
height: 48
},
page: {
animate: true,
animateMode: 'fade-slide',
animateModeList: [
{ value: 'fade-slide', label: EnumThemeAnimateMode['fade-slide'] },
{ value: 'fade', label: EnumThemeAnimateMode.fade },
{ value: 'fade-bottom', label: EnumThemeAnimateMode['fade-bottom'] },
{ value: 'fade-scale', label: EnumThemeAnimateMode['fade-scale'] },
{ value: 'zoom-fade', label: EnumThemeAnimateMode['zoom-fade'] },
{ value: 'zoom-out', label: EnumThemeAnimateMode['zoom-out'] }
]
}
};
export const themeSetting = defaultThemeSetting;

View File

@ -1,6 +1,6 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useReload, useBoolean } from '@/hooks'; import { useReload, useModalVisible, useBoolean } from '@/hooks';
interface AppStore { interface AppStore {
/** 重载页面的标志 */ /** 重载页面的标志 */
@ -18,6 +18,14 @@ interface AppStore {
closeSettingDrawer(): void; closeSettingDrawer(): void;
/** 切换抽屉可见状态 */ /** 切换抽屉可见状态 */
toggleSettingdrawerVisible(): void; toggleSettingdrawerVisible(): void;
/** 侧边栏折叠状态 */
siderCollapse: Ref<boolean>;
/** 设置侧边栏折叠状态 */
setSiderCollapse(collapse: boolean): void;
/** vertical-mix模式下 侧边栏的固定状态 */
mixSiderFixed: Ref<boolean>;
/** 设置 vertical-mix模式下 侧边栏的固定状态 */
setMixSiderIsFixed(isFixed: boolean): void;
} }
export const useAppStore = defineStore('app-store', () => { export const useAppStore = defineStore('app-store', () => {
@ -26,11 +34,17 @@ export const useAppStore = defineStore('app-store', () => {
// 设置抽屉 // 设置抽屉
const { const {
bool: settingDrawerVisible, visible: settingDrawerVisible,
setTrue: openSettingDrawer, openModal: openSettingDrawer,
setFalse: closeSettingDrawer, closeModal: closeSettingDrawer,
toggle: toggleSettingdrawerVisible toggleModal: toggleSettingdrawerVisible
} = useBoolean(); } = useModalVisible();
// 侧边栏的折叠状态
const { bool: siderCollapse, setBool: setSiderCollapse } = useBoolean();
// vertical-mix模式下 侧边栏的固定状态
const { bool: mixSiderFixed, setBool: setMixSiderIsFixed } = useBoolean();
const appStore: AppStore = { const appStore: AppStore = {
reloadFlag, reloadFlag,
@ -38,7 +52,11 @@ export const useAppStore = defineStore('app-store', () => {
settingDrawerVisible, settingDrawerVisible,
openSettingDrawer, openSettingDrawer,
closeSettingDrawer, closeSettingDrawer,
toggleSettingdrawerVisible toggleSettingdrawerVisible,
siderCollapse,
setSiderCollapse,
mixSiderFixed,
setMixSiderIsFixed
}; };
return appStore; return appStore;

View File

@ -0,0 +1,253 @@
import { watch, onUnmounted } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import { useOsTheme } from 'naive-ui';
import { useElementSize } from '@vueuse/core';
import { objectAssign } from '@/utils';
import type { ThemeSetting, ThemeLayoutMode, ThemeTabMode, ThemeAnimateMode } from '@/interface';
import { handleWindicssDarkMode } from './helpers';
export interface LayoutFunc {
/** 设置布局最小宽度 */
setLayoutMinWidth(minWidth: number): void;
/** 设置布局模式 */
setLayoutMode(mode: ThemeLayoutMode): void;
}
export function useLayoutFunc(layout: ThemeSetting['layout']): LayoutFunc {
function setLayout(data: Partial<ThemeSetting['layout']>) {
objectAssign(layout, data);
}
function setLayoutMinWidth(minWidth: number) {
setLayout({ minWidth });
}
function setLayoutMode(mode: ThemeLayoutMode) {
setLayout({ mode });
}
return {
setLayoutMinWidth,
setLayoutMode
};
}
export interface HeaderFunc {
/** 设置头部高度 */
setHeaderHeight(height: number): void;
/** 设置头部面包屑可见 */
setHeaderCrumbVisible(visible: boolean): void;
/** 设置头部面包屑图标可见 */
setHeaderCrumbIconVisible(visible: boolean): void;
}
export function useHeaderFunc(header: ThemeSetting['header']): HeaderFunc {
function setHeader(data: Partial<ThemeSetting['header']>) {
objectAssign(header, data);
}
function setHeaderHeight(height: number) {
setHeader({ height });
}
function setHeaderCrumbVisible(visible: boolean) {
setHeader({ crumb: { ...header.crumb, visible } });
}
function setHeaderCrumbIconVisible(visible: boolean) {
setHeader({ crumb: { ...header.crumb, showIcon: visible } });
}
return {
setHeaderHeight,
setHeaderCrumbVisible,
setHeaderCrumbIconVisible
};
}
export interface TabFunc {
/** 设置多页签可见 */
setTabVisible(visible: boolean): void;
/** 设置多页签高度 */
setTabHeight(height: number): void;
/** 设置多页签风格 */
setTabMode(mode: ThemeTabMode): void;
/** 设置多页签缓存 */
setTabIsCache(isCache: boolean): void;
}
export function useTabFunc(tab: ThemeSetting['tab']): TabFunc {
function setTab(data: Partial<ThemeSetting['tab']>) {
objectAssign(tab, data);
}
function setTabVisible(visible: boolean) {
setTab({ visible });
}
function setTabHeight(height: number) {
setTab({ height });
}
function setTabMode(mode: ThemeTabMode) {
setTab({ mode });
}
function setTabIsCache(isCache: boolean) {
setTab({ isCache });
}
return {
setTabVisible,
setTabHeight,
setTabMode,
setTabIsCache
};
}
export interface SiderFunc {
/** 侧边栏宽度 */
setSiderWidth(width: number): void;
/** 侧边栏折叠时的宽度 */
setSiderCollapsedWidth(width: number): void;
/** vertical-mix模式下侧边栏宽度 */
setMixSiderWidth(width: number): void;
/** vertical-mix模式下侧边栏折叠时的宽度 */
setMixSiderCollapsedWidth(width: number): void;
/** vertical-mix模式下侧边栏展示子菜单的宽度 */
setMixSiderChildMenuWidth(width: number): void;
}
export function useSiderFunc(sider: ThemeSetting['sider']): SiderFunc {
function setSider(data: Partial<ThemeSetting['sider']>) {
objectAssign(sider, data);
}
function setSiderWidth(width: number) {
setSider({ width });
}
function setSiderCollapsedWidth(width: number) {
setSider({ collapsedWidth: width });
}
function setMixSiderWidth(width: number) {
setSider({ mixWidth: width });
}
function setMixSiderCollapsedWidth(width: number) {
setSider({ mixCollapsedWidth: width });
}
function setMixSiderChildMenuWidth(width: number) {
setSider({ mixChildMenuWidth: width });
}
return {
setSiderWidth,
setSiderCollapsedWidth,
setMixSiderWidth,
setMixSiderCollapsedWidth,
setMixSiderChildMenuWidth
};
}
export interface FooterFunc {
/** 设置底部是否固定 */
setFooterIsFixed(isFixed: boolean): void;
/** 设置底部高度 */
setFooterHeight(height: number): void;
}
export function useFooterFunc(footer: ThemeSetting['footer']): FooterFunc {
function setFooter(data: Partial<ThemeSetting['footer']>) {
objectAssign(footer, data);
}
function setFooterIsFixed(isFixed: boolean) {
setFooter({ fixed: isFixed });
}
function setFooterHeight(height: number) {
setFooter({ height });
}
return {
setFooterIsFixed,
setFooterHeight
};
}
export interface PageFunc {
/** 设置切换页面时是否过渡动画 */
setPageIsAnimate(animate: boolean): void;
/** 设置页面过渡动画类型 */
setPageAnimateMode(mode: ThemeAnimateMode): void;
}
export function usePageFunc(page: ThemeSetting['page']): PageFunc {
function setPage(data: Partial<ThemeSetting['page']>) {
objectAssign(page, data);
}
function setPageIsAnimate(animate: boolean) {
setPage({ animate });
}
function setPageAnimateMode(mode: ThemeAnimateMode) {
setPage({ animateMode: mode });
}
return {
setPageIsAnimate,
setPageAnimateMode
};
}
/**
* 操作系统主题模式变化的回调函数
* @param isDark - 暗黑模式
*/
type OsThemeCallback = (isDark: boolean) => void;
/** 监听操作系统主题模式 */
export function osThemeWatcher(callback: OsThemeCallback) {
/** 操作系统暗黑主题 */
const osTheme = useOsTheme();
const stopHandle = watch(
osTheme,
newValue => {
const isDark = newValue === 'dark';
callback(isDark);
},
{ immediate: true }
);
onUnmounted(() => {
stopHandle();
});
}
/** 应用windicss的暗黑模式 */
export function setupWindicssDarkMode(darkMode: Ref<boolean>) {
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
const stopHandle = watch(
() => darkMode.value,
newValue => {
if (newValue) {
addDarkClass();
} else {
removeDarkClass();
}
},
{ immediate: true }
);
onUnmounted(() => {
stopHandle();
});
}
/**
* 禁用横向滚动
* @description 页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止
*/
export function setupHiddenScroll(minWidthOfLayout: ComputedRef<number>) {
const { width } = useElementSize(document.documentElement);
const stopHandle = watch(width, newValue => {
if (newValue < minWidthOfLayout.value) {
document.documentElement.style.overflowX = 'auto';
} else {
document.documentElement.style.overflowX = 'hidden';
}
});
onUnmounted(() => {
stopHandle();
});
}

View File

@ -1,36 +1,67 @@
import { ref, computed, watch } from 'vue'; import { ref, reactive, computed } from 'vue';
import type { Ref, ComputedRef } from 'vue'; import type { Ref, ComputedRef } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useThemeVars, darkTheme, useOsTheme } from 'naive-ui'; import { darkTheme } from 'naive-ui';
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui'; import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
import { themeSetting } from '@/settings';
import { useBoolean } from '@/hooks'; import { useBoolean } from '@/hooks';
import { getColorPalette } from '@/utils'; import { getColorPalette } from '@/utils';
import { getNaiveThemeOverrides, addThemeCssVarsToHtml, handleWindicssDarkMode } from './helpers'; import type { ThemeSetting, ThemeHorizontalMenuPosition } from '@/interface';
import { getNaiveThemeOverrides, addThemeCssVarsToHtml } from './helpers';
interface OtherColor { import {
/** 信息 */ useLayoutFunc,
info: string; useHeaderFunc,
/** 成功 */ useTabFunc,
success: string; useSiderFunc,
/** 警告 */ useFooterFunc,
warning: string; usePageFunc,
/** 错误 */ osThemeWatcher,
error: string; setupWindicssDarkMode,
} setupHiddenScroll
} from './hooks';
import type { LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc } from './hooks';
type BuiltInGlobalTheme = Omit<Required<GlobalTheme>, 'InternalSelectMenu' | 'InternalSelection'>; type BuiltInGlobalTheme = Omit<Required<GlobalTheme>, 'InternalSelectMenu' | 'InternalSelection'>;
interface ThemeStore { interface ThemeStore extends LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc {
/** 暗黑模式 */ /** 暗黑模式 */
darkMode: Ref<boolean>; darkMode: Ref<boolean>;
/** 设置暗黑模式 */ /** 设置暗黑模式 */
setDarkMode(dark: boolean): void; setDarkMode(dark: boolean): void;
/** 切换/关闭 暗黑模式 */ /** 切换/关闭 暗黑模式 */
toggleDarkMode(dark: boolean): void; toggleDarkMode(dark: boolean): void;
/** 布局样式 */
layout: ThemeSetting['layout'];
/** 主题颜色 */ /** 主题颜色 */
themeColor: Ref<string>; themeColor: Ref<string>;
/** 设置系统主题颜色 */
setThemeColor(color: string): void;
/** 主题颜色列表 */
themeColorList: string[];
/** 其他颜色 */ /** 其他颜色 */
otherColor: ComputedRef<OtherColor>; otherColor: ComputedRef<ThemeSetting['otherColor']>;
/** 固定头部和多页签 */
fixedHeaderAndTab: Ref<boolean>;
/** 设置固定头部和多页签 */
setIsFixedHeaderAndTab(isFixed: boolean): void;
/** 重载按钮可见 */
reloadVisible: Ref<boolean>;
/** 设置 显示/隐藏 重载按钮 */
setReloadVisible(visible: boolean): void;
/** 头部 */
header: ThemeSetting['header'];
/** 多页签 */
tab: ThemeSetting['tab'];
/** 侧边栏 */
sider: ThemeSetting['sider'];
/** 菜单 */
menu: ThemeSetting['menu'];
/** 设置水平模式的菜单的位置 */
setHorizontalMenuPosition(posiiton: ThemeHorizontalMenuPosition): void;
/** 底部 */
footer: ThemeSetting['footer'];
/** 页面 */
page: ThemeSetting['page'];
/** naiveUI的主题配置 */ /** naiveUI的主题配置 */
naiveThemeOverrides: ComputedRef<GlobalThemeOverrides>; naiveThemeOverrides: ComputedRef<GlobalThemeOverrides>;
/** naive-ui暗黑主题 */ /** naive-ui暗黑主题 */
@ -38,72 +69,148 @@ interface ThemeStore {
} }
export const useThemeStore = defineStore('theme-store', () => { export const useThemeStore = defineStore('theme-store', () => {
const themeVars = useThemeVars(); // 暗黑模式
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean(); const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
const themeColor = ref('#1890ff'); // 布局
const otherColor = computed<OtherColor>(() => ({ const layout = reactive<ThemeSetting['layout']>({
info: getColorPalette(themeColor.value, 7), ...themeSetting.layout
success: '#52c41a', });
warning: '#faad14', const { setLayoutMinWidth, setLayoutMode } = useLayoutFunc(layout);
error: '#f5222d'
// 主题色
const themeColor = ref(themeSetting.themeColor);
/** 设置系统主题颜色 */
function setThemeColor(color: string) {
themeColor.value = color;
}
const { themeColorList } = themeSetting;
const otherColor = computed<ThemeSetting['otherColor']>(() => ({
...themeSetting.otherColor,
info: getColorPalette(themeColor.value, 7)
})); }));
// 固定头部和多页签
const { bool: fixedHeaderAndTab, setBool: setIsFixedHeaderAndTab } = useBoolean(themeSetting.fixedHeaderAndTab);
// 重载按钮
const { bool: reloadVisible, setBool: setReloadVisible } = useBoolean(themeSetting.showReload);
// 头部
const header = reactive<ThemeSetting['header']>({
height: themeSetting.header.height,
crumb: { ...themeSetting.header.crumb }
});
const { setHeaderHeight, setHeaderCrumbVisible, setHeaderCrumbIconVisible } = useHeaderFunc(header);
// 多页签
const tab = reactive<ThemeSetting['tab']>({
...themeSetting.tab
});
const { setTabVisible, setTabHeight, setTabMode, setTabIsCache } = useTabFunc(tab);
// 侧边栏
const sider = reactive<ThemeSetting['sider']>({
...themeSetting.sider
});
const {
setSiderWidth,
setSiderCollapsedWidth,
setMixSiderWidth,
setMixSiderCollapsedWidth,
setMixSiderChildMenuWidth
} = useSiderFunc(sider);
// 菜单
const menu = reactive<ThemeSetting['menu']>({
...themeSetting.menu
});
function setHorizontalMenuPosition(posiiton: ThemeHorizontalMenuPosition) {
menu.horizontalPosition = posiiton;
}
// 底部
const footer = reactive<ThemeSetting['footer']>({
...themeSetting.footer
});
const { setFooterIsFixed, setFooterHeight } = useFooterFunc(footer);
// 页面
const page = reactive<ThemeSetting['page']>({
...themeSetting.page
});
const { setPageIsAnimate, setPageAnimateMode } = usePageFunc(page);
// naive主题
const naiveThemeOverrides = computed<GlobalThemeOverrides>(() => const naiveThemeOverrides = computed<GlobalThemeOverrides>(() =>
getNaiveThemeOverrides({ primary: themeColor.value, ...otherColor.value }) getNaiveThemeOverrides({ primary: themeColor.value, ...otherColor.value })
); );
/** naive-ui暗黑主题 */
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined)); const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
/** 操作系统暗黑主题 */
const osTheme = useOsTheme();
/** 初始化css vars, 并添加至html */ /** 初始化css vars, 并添加至html */
function initThemeCssVars() { function initThemeCssVars() {
const updatedThemeVars = { ...themeVars.value, ...naiveThemeOverrides.value.common }; const updatedThemeVars = { ...naiveThemeOverrides.value.common };
addThemeCssVarsToHtml(updatedThemeVars); addThemeCssVarsToHtml(updatedThemeVars);
} }
function init() { /** 系统主题适应操作系统 */
initThemeCssVars(); function handleAdaptOsTheme() {
} osThemeWatcher(isDark => {
init();
// 监听操作系统主题模式
watch(
osTheme,
newValue => {
const isDark = newValue === 'dark';
if (isDark) { if (isDark) {
setDarkMode(true); setDarkMode(true);
} else { } else {
setDarkMode(false); setDarkMode(false);
} }
}, });
{ immediate: true } }
);
// 监听主题的暗黑模式 function init() {
watch( initThemeCssVars();
() => darkMode.value, handleAdaptOsTheme();
newValue => { setupWindicssDarkMode(darkMode);
if (newValue) { setupHiddenScroll(computed(() => layout.minWidth));
addDarkClass(); }
} else {
removeDarkClass(); init();
}
},
{ immediate: true }
);
const themeStore: ThemeStore = { const themeStore: ThemeStore = {
darkMode, darkMode,
setDarkMode, setDarkMode,
toggleDarkMode, toggleDarkMode,
layout,
setLayoutMinWidth,
setLayoutMode,
themeColor, themeColor,
setThemeColor,
themeColorList,
otherColor, otherColor,
fixedHeaderAndTab,
setIsFixedHeaderAndTab,
reloadVisible,
setReloadVisible,
header,
setHeaderHeight,
setHeaderCrumbVisible,
setHeaderCrumbIconVisible,
tab,
setTabVisible,
setTabHeight,
setTabMode,
setTabIsCache,
sider,
setSiderWidth,
setSiderCollapsedWidth,
setMixSiderWidth,
setMixSiderCollapsedWidth,
setMixSiderChildMenuWidth,
menu,
setHorizontalMenuPosition,
footer,
setFooterIsFixed,
setFooterHeight,
page,
setPageIsAnimate,
setPageAnimateMode,
naiveThemeOverrides, naiveThemeOverrides,
naiveTheme naiveTheme
}; };

View File

@ -1,3 +1,13 @@
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* fade-slide */ /* fade-slide */
.fade-slide-leave-active, .fade-slide-leave-active,
.fade-slide-enter-active { .fade-slide-enter-active {
@ -11,3 +21,56 @@
opacity: 0; opacity: 0;
transform: translateX(30px); transform: translateX(30px);
} }
/* fade-bottom */
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s, transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
/* fade-scale */
.fade-scale-leave-active,
.fade-scale-enter-active {
transition: all 0.28s;
}
.fade-scale-enter-from {
opacity: 0;
transform: scale(1.2);
}
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
/* zoom-fade */
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: transform 0.2s, opacity 0.3s ease-out;
}
.zoom-fade-enter-from {
opacity: 0;
transform: scale(0.92);
}
.zoom-fade-leave-to {
opacity: 0;
transform: scale(1.06);
}
/* zoom-out */
.zoom-out-enter-active,
.zoom-out-leave-active {
transition: opacity 0.1s ease-in-out, transform 0.15s ease-out;
}
.zoom-out-enter-from,
.zoom-out-leave-to {
opacity: 0;
transform: scale(0);
}

2
src/typings/common/global.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/** 构建时间 */
declare const PROJECT_BUILD_TIME: string;

View File

@ -30,12 +30,12 @@ declare namespace AuthRoute {
/** /**
* 路由的组件 * 路由的组件
* - layout - 基础布局,具有公共部分的布局 * - basic - 基础布局,具有公共部分的布局
* - blank - 空白布局 * - blank - 空白布局
* - multi - 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局) * - multi - 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局)
* - self - 作为子路由,使用自身的布局(作为最后一级路由,没有子路由) * - self - 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)
*/ */
type RouteComponent = 'layout' | 'blank' | 'multi' | 'self'; type RouteComponent = 'basic' | 'blank' | 'multi' | 'self';
/** 路由描述 */ /** 路由描述 */
type RouteMeta = { type RouteMeta = {
@ -44,15 +44,13 @@ declare namespace AuthRoute {
/** 路由的动态路径 */ /** 路由的动态路径 */
dynamicPath?: PathToDynamicPath<'/login'>; dynamicPath?: PathToDynamicPath<'/login'>;
/** 作为单独路由的父级路由布局组件 */ /** 作为单独路由的父级路由布局组件 */
singleLayout?: Extract<RouteComponent, 'layout' | 'blank'>; singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>;
/** 需要登录权限 */ /** 需要登录权限 */
requiresAuth?: boolean; requiresAuth?: boolean;
/** 哪些类型的用户有权限才能访问的路由 */ /** 哪些类型的用户有权限才能访问的路由 */
permissions?: Auth.RoleType[]; permissions?: Auth.RoleType[];
/** 缓存页面 */ /** 缓存页面 */
keepAlive?: boolean; keepAlive?: boolean;
/** 是否是空白布局 */
blankLayout?: boolean;
/** 菜单和面包屑对应的图标 */ /** 菜单和面包屑对应的图标 */
icon?: string; icon?: string;
/** 是否在菜单中隐藏 */ /** 是否在菜单中隐藏 */
@ -73,7 +71,7 @@ declare namespace AuthRoute {
redirect?: RoutePath; redirect?: RoutePath;
/** /**
* 路由组件 * 路由组件
* - layout: 基础布局,具有公共部分的布局 * - basic: 基础布局,具有公共部分的布局
* - blank: 空白布局 * - blank: 空白布局
* - multi: 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局) * - multi: 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局)
* - self: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由) * - self: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)

View File

@ -2,5 +2,6 @@ export * from './typeof';
export * from './console'; export * from './console';
export * from './color'; export * from './color';
export * from './number'; export * from './number';
export * from './object';
export * from './icon'; export * from './icon';
export * from './design-pattern'; export * from './design-pattern';

View File

@ -0,0 +1,4 @@
/** 设置对象数据 */
export function objectAssign<T extends { [key: string]: any }>(target: T, source: Partial<T>) {
Object.assign(target, source);
}

View File

@ -1,5 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/layouts'; import { BasicLayout, BlankLayout } from '@/layouts';
import { consoleError } from '../common'; import { consoleError } from '../common';
import { getViewComponent } from './component'; import { getViewComponent } from './component';
@ -30,14 +30,11 @@ function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
if (hasComponent(item)) { if (hasComponent(item)) {
const action: ComponentAction = { const action: ComponentAction = {
layout() { basic() {
itemRoute.component = Layout; itemRoute.component = BasicLayout;
}, },
blank() { blank() {
itemRoute.component = Layout; itemRoute.component = BlankLayout;
if (itemRoute.meta) {
itemRoute.meta.blankLayout = true;
}
}, },
multi() { multi() {
// 多级路由一定有子路由 // 多级路由一定有子路由
@ -77,13 +74,11 @@ function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
} else { } else {
const parentPath = `${itemRoute.path}-parent` as AuthRoute.SingleRouteParentPath; const parentPath = `${itemRoute.path}-parent` as AuthRoute.SingleRouteParentPath;
if (item.meta.singleLayout === 'blank') { const layout = item.meta.singleLayout === 'basic' ? BasicLayout : BlankLayout;
itemRoute.meta!.blankLayout = true;
}
const parentRoute: RouteRecordRaw = { const parentRoute: RouteRecordRaw = {
path: parentPath, path: parentPath,
component: Layout, component: layout,
redirect: item.path, redirect: item.path,
children: [itemRoute] children: [itemRoute]
}; };

View File

@ -0,0 +1,17 @@
<template>
<n-card title="开发环境依赖" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small">
<n-descriptions-item v-for="item in devDependencies" :key="item.name" :label="item.name">
{{ item.version }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { pkgJson } from '../model';
const { devDependencies } = pkgJson;
</script>
<style scoped></style>

View File

@ -0,0 +1,17 @@
<template>
<n-card title="生产环境依赖" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small">
<n-descriptions-item v-for="item in dependencies" :key="item.name" :label="item.name">
{{ item.version }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { pkgJson } from '../model';
const { dependencies } = pkgJson;
</script>
<style scoped></style>

View File

@ -0,0 +1,27 @@
<template>
<n-card title="项目信息" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small" :column="2">
<n-descriptions-item label="版本">
<n-tag type="primary">{{ version }}</n-tag>
</n-descriptions-item>
<n-descriptions-item label="最后编译时间">
<n-tag type="primary">{{ lastestBuildTime }}</n-tag>
</n-descriptions-item>
<n-descriptions-item label="Github地址">
<a class="text-primary" href="https://github.com/honghuangdc/soybean-admin" target="_blank">Github地址</a>
</n-descriptions-item>
<n-descriptions-item label="预览地址">
<a class="text-primary" href="https://soybean.pro" target="_blank">预览地址</a>
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem, NTag } from 'naive-ui';
import { pkgJson } from '../model';
const { version } = pkgJson;
const lastestBuildTime = PROJECT_BUILD_TIME;
</script>
<style scoped></style>

View File

@ -0,0 +1,13 @@
<template>
<n-card title="关于" :bordered="false" size="large" class="rounded-16px shadow-sm">
<p class="leading-24px">
Soybean Admin 是一个基于 Vue3ViteNaive UITypeScript
的中后台解决方案它使用了最新的前端技术栈并提炼了典型的业务模型页面包括二次封装组件动态菜单权限校验粒子化权限控制等功能它可以帮助你快速搭建企业级中后台项目相信不管是从新技术使用还是其他方面都能帮助到你
</p>
</n-card>
</template>
<script setup lang="ts">
import { NCard } from 'naive-ui';
</script>
<style scoped></style>

View File

@ -0,0 +1,6 @@
import ProjectIntroduction from './ProjectIntroduction.vue';
import ProjectInfo from './ProjectInfo.vue';
import ProDependency from './ProDependency.vue';
import DevDependency from './DevDependency.vue';
export { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency };

View File

@ -1,9 +1,14 @@
<template> <template>
<div> <n-space :vertical="true" :size="16">
<h3>about</h3> <project-introduction />
<router-link to="/">analysis</router-link> <project-info />
</div> <pro-dependency />
<dev-dependency />
</n-space>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import { NSpace } from 'naive-ui';
import { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency } from './components';
</script>
<style scoped></style> <style scoped></style>

43
src/views/about/model.ts Normal file
View File

@ -0,0 +1,43 @@
import pkg from '~/package.json';
/** npm依赖包版本信息 */
export interface PkgVersionInfo {
name: string;
version: string;
}
interface Package {
name: string;
version: string;
dependencies: {
[key: string]: string;
};
devDependencies: {
[key: string]: string;
};
[key: string]: any;
}
interface PkgJson {
name: string;
version: string;
dependencies: PkgVersionInfo[];
devDependencies: PkgVersionInfo[];
}
const pkgWithType = pkg as Package;
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
const [name, version] = tuple;
return {
name,
version
};
}
export const pkgJson: PkgJson = {
name: pkgWithType.name,
version: pkgWithType.version,
dependencies: Object.entries(pkgWithType.dependencies).map(item => transformVersionData(item)),
devDependencies: Object.entries(pkgWithType.devDependencies).map(item => transformVersionData(item))
};

View File

@ -1,6 +1,6 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv } from 'vite';
import { setupVitePlugins } from './build'; import { setupVitePlugins, define } from './build';
export default defineConfig(configEnv => { export default defineConfig(configEnv => {
const viteEnv = loadEnv(configEnv.mode, `.env.${configEnv.mode}`); const viteEnv = loadEnv(configEnv.mode, `.env.${configEnv.mode}`);
@ -13,6 +13,7 @@ export default defineConfig(configEnv => {
'~': fileURLToPath(new URL('./', import.meta.url)) '~': fileURLToPath(new URL('./', import.meta.url))
} }
}, },
define,
plugins: setupVitePlugins(configEnv), plugins: setupVitePlugins(configEnv),
css: { css: {
preprocessorOptions: { preprocessorOptions: {

View File

@ -41,6 +41,7 @@ export default defineConfig({
'nowrap-hidden': 'whitespace-nowrap overflow-hidden', 'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
'ellipsis-text': 'nowrap-hidden overflow-ellipsis', 'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
'transition-base': 'transition-all duration-300 ease-in-out', 'transition-base': 'transition-all duration-300 ease-in-out',
// 'dark-transition': "",
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse' 'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
}, },
theme: { theme: {
@ -67,6 +68,14 @@ export default defineConfig({
'error-pressed': 'var(--error-color-pressed)', 'error-pressed': 'var(--error-color-pressed)',
'error-active': 'var(--error-color-active)' 'error-active': 'var(--error-color-active)'
}, },
backgroundColor: {
dark: '#18181c',
'dark-base': '#101014'
},
textColor: {
'black-base': '#333639',
'white-base': 'rgba(255, 255, 255, 0.82)'
},
transitionProperty: ['padding-left'] transitionProperty: ['padding-left']
} }
}, },