mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-23 23:39:47 +08:00
feat(projects): theme store完成
This commit is contained in:
@ -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
8
build/define/index.ts
Normal 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
|
||||||
|
};
|
@ -1 +1,2 @@
|
|||||||
export * from './plugins';
|
export * from './plugins';
|
||||||
|
export * from './define';
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
10
src/components/common/DarkModeContainer/index.vue
Normal file
10
src/components/common/DarkModeContainer/index.vue
Normal 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>
|
@ -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 };
|
||||||
|
@ -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
30
src/enum/common/theme.ts
Normal 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' = '缩放消退'
|
||||||
|
}
|
@ -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 };
|
||||||
|
34
src/hooks/common/useModalVisible.ts
Normal file
34
src/hooks/common/useModalVisible.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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
137
src/interface/theme.ts
Normal 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;
|
||||||
|
}
|
69
src/layouts/BasicLayout/index.vue
Normal file
69
src/layouts/BasicLayout/index.vue
Normal 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>
|
8
src/layouts/BlankLayout/index.vue
Normal file
8
src/layouts/BlankLayout/index.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<global-content />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { GlobalContent } from '../common';
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
@ -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>
|
||||||
|
10
src/layouts/common/GlobalFooter/index.vue
Normal file
10
src/layouts/common/GlobalFooter/index.vue
Normal 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>
|
12
src/layouts/common/GlobalHeader/index.vue
Normal file
12
src/layouts/common/GlobalHeader/index.vue
Normal 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>
|
12
src/layouts/common/GlobalSider/index.vue
Normal file
12
src/layouts/common/GlobalSider/index.vue
Normal 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>
|
12
src/layouts/common/GlobalTab/index.vue
Normal file
12
src/layouts/common/GlobalTab/index.vue
Normal 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>
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
|
@ -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 };
|
||||||
|
3
src/package/SoybeanLayout/index.ts
Normal file
3
src/package/SoybeanLayout/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SoybeanLayout from './src/index.vue';
|
||||||
|
|
||||||
|
export default SoybeanLayout;
|
@ -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">
|
@ -1,3 +1,3 @@
|
|||||||
import SoybeanLayout from './SoybeanLayout/index.vue';
|
import SoybeanLayout from './SoybeanLayout';
|
||||||
|
|
||||||
export { SoybeanLayout };
|
export { SoybeanLayout };
|
||||||
|
@ -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
1
src/settings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './theme';
|
0
src/settings/theme.json
Normal file
0
src/settings/theme.json
Normal file
91
src/settings/theme.ts
Normal file
91
src/settings/theme.ts
Normal 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;
|
@ -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;
|
||||||
|
253
src/store/modules/theme/hooks.ts
Normal file
253
src/store/modules/theme/hooks.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
2
src/typings/common/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** 构建时间 */
|
||||||
|
declare const PROJECT_BUILD_TIME: string;
|
10
src/typings/common/route.d.ts
vendored
10
src/typings/common/route.d.ts
vendored
@ -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: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)
|
||||||
|
@ -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';
|
||||||
|
4
src/utils/common/object.ts
Normal file
4
src/utils/common/object.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** 设置对象数据 */
|
||||||
|
export function objectAssign<T extends { [key: string]: any }>(target: T, source: Partial<T>) {
|
||||||
|
Object.assign(target, source);
|
||||||
|
}
|
@ -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]
|
||||||
};
|
};
|
||||||
|
17
src/views/about/components/DevDependency.vue
Normal file
17
src/views/about/components/DevDependency.vue
Normal 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>
|
17
src/views/about/components/ProDependency.vue
Normal file
17
src/views/about/components/ProDependency.vue
Normal 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>
|
27
src/views/about/components/ProjectInfo.vue
Normal file
27
src/views/about/components/ProjectInfo.vue
Normal 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>
|
13
src/views/about/components/ProjectIntroduction.vue
Normal file
13
src/views/about/components/ProjectIntroduction.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="关于" :bordered="false" size="large" class="rounded-16px shadow-sm">
|
||||||
|
<p class="leading-24px">
|
||||||
|
Soybean Admin 是一个基于 Vue3、Vite、Naive UI、TypeScript
|
||||||
|
的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你。
|
||||||
|
</p>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NCard } from 'naive-ui';
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
6
src/views/about/components/index.ts
Normal file
6
src/views/about/components/index.ts
Normal 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 };
|
@ -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
43
src/views/about/model.ts
Normal 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))
|
||||||
|
};
|
@ -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: {
|
||||||
|
@ -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']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user