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',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/settings',
|
||||
group: 'internal',
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '@/enum',
|
||||
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 './define';
|
||||
|
@ -4,7 +4,7 @@ const routes: AuthRoute.Route[] = [
|
||||
{
|
||||
name: 'dashboard',
|
||||
path: '/dashboard',
|
||||
component: 'layout',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'dashboard_analysis',
|
||||
@ -36,7 +36,7 @@ const routes: AuthRoute.Route[] = [
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '关于',
|
||||
singleLayout: 'layout',
|
||||
singleLayout: 'basic',
|
||||
permissions: ['super', 'admin', 'test'],
|
||||
icon: 'fluent:book-information-24-regular'
|
||||
}
|
||||
@ -44,7 +44,7 @@ const routes: AuthRoute.Route[] = [
|
||||
{
|
||||
name: 'multi-menu',
|
||||
path: '/multi-menu',
|
||||
component: 'layout',
|
||||
component: 'basic',
|
||||
children: [
|
||||
{
|
||||
name: 'multi-menu_first',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soybean-admin-thin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_HTTP_ENV=test 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 SystemLogo from './SystemLogo/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 './service';
|
||||
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 useLoadingEmpty from './useLoadingEmpty';
|
||||
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;
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './enum';
|
||||
export * from './theme';
|
||||
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>
|
||||
<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>
|
||||
<div class="flex justify-end h-full bg-red-600">
|
||||
<h3 class="text-white">Header</h3>
|
||||
</div>
|
||||
<global-header />
|
||||
</template>
|
||||
<template #tab>
|
||||
<div class="h-full bg-green-600"></div>
|
||||
<global-tab />
|
||||
</template>
|
||||
<template #sider>
|
||||
<div class="w-full h-full bg-gray-200">
|
||||
<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>
|
||||
<global-sider />
|
||||
</template>
|
||||
<global-content />
|
||||
<template #footer>
|
||||
<div class="h-full bg-blue-400">
|
||||
<h3>footer</h3>
|
||||
</div>
|
||||
<global-footer />
|
||||
</template>
|
||||
</soybean-layout>
|
||||
<setting-drawer />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { NSpace, NButton, NSwitch, NRadioGroup, NRadio } from 'naive-ui';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
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';
|
||||
const mode = computed(() => {
|
||||
const vertical: LayoutMode = 'vertical';
|
||||
const horizontal: LayoutMode = 'horizontal';
|
||||
return theme.layout.mode.includes(vertical) ? vertical : horizontal;
|
||||
});
|
||||
|
||||
interface ModeRadio {
|
||||
value: LayoutMode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const { width } = useElementSize(document.documentElement);
|
||||
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';
|
||||
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>
|
||||
|
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>
|
||||
<n-button
|
||||
class="fixed top-240px right-14px z-10000"
|
||||
:class="{ '!right-330px': app.settingDrawerVisible }"
|
||||
type="primary"
|
||||
: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"
|
||||
>
|
||||
点击
|
||||
<icon-ant-design:close-outlined v-if="app.settingDrawerVisible" class="text-24px" />
|
||||
<icon-ant-design:setting-outlined v-else class="text-24px" />
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
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 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 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>
|
||||
<header class="soybean-layout__footer" :style="style">
|
||||
<footer class="soybean-layout__footer" :style="style">
|
||||
<slot></slot>
|
||||
</header>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
@ -1,3 +1,3 @@
|
||||
import SoybeanLayout from './SoybeanLayout/index.vue';
|
||||
import SoybeanLayout from './SoybeanLayout';
|
||||
|
||||
export { SoybeanLayout };
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
import { useAuthStore, useRouteStore } from '@/store';
|
||||
import { exeStrategyActions } from '@/utils';
|
||||
import { exeStrategyActions, getToken } from '@/utils';
|
||||
|
||||
/** 处理路由页面的权限 */
|
||||
export async function handlePagePermission(
|
||||
@ -14,6 +14,7 @@ export async function handlePagePermission(
|
||||
const route = useRouteStore();
|
||||
const { initDynamicRoute } = useRouteStore();
|
||||
|
||||
const isLogin = Boolean(getToken());
|
||||
const permissions = to.meta.permissions || [];
|
||||
const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length);
|
||||
const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole);
|
||||
@ -38,7 +39,7 @@ export async function handlePagePermission(
|
||||
const actions: Common.StrategyAction[] = [
|
||||
// 已登录状态跳转登录页,跳转至首页
|
||||
[
|
||||
auth.isLogin && to.name === routeName('login'),
|
||||
isLogin && to.name === routeName('login'),
|
||||
() => {
|
||||
next({ name: routeName('root') });
|
||||
}
|
||||
@ -52,7 +53,7 @@ export async function handlePagePermission(
|
||||
],
|
||||
// 未登录状态进入需要登录权限的页面
|
||||
[
|
||||
!auth.isLogin && needLogin,
|
||||
!isLogin && needLogin,
|
||||
() => {
|
||||
const redirect = to.fullPath;
|
||||
next({ name: routeName('login'), query: { redirect } });
|
||||
@ -60,14 +61,14 @@ export async function handlePagePermission(
|
||||
],
|
||||
// 登录状态进入需要登录权限的页面,有权限直接通行
|
||||
[
|
||||
auth.isLogin && needLogin && hasPermission,
|
||||
isLogin && needLogin && hasPermission,
|
||||
() => {
|
||||
next();
|
||||
}
|
||||
],
|
||||
[
|
||||
// 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面
|
||||
auth.isLogin && needLogin && !hasPermission,
|
||||
isLogin && needLogin && !hasPermission,
|
||||
() => {
|
||||
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 { defineStore } from 'pinia';
|
||||
import { useReload, useBoolean } from '@/hooks';
|
||||
import { useReload, useModalVisible, useBoolean } from '@/hooks';
|
||||
|
||||
interface AppStore {
|
||||
/** 重载页面的标志 */
|
||||
@ -18,6 +18,14 @@ interface AppStore {
|
||||
closeSettingDrawer(): 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', () => {
|
||||
@ -26,11 +34,17 @@ export const useAppStore = defineStore('app-store', () => {
|
||||
|
||||
// 设置抽屉
|
||||
const {
|
||||
bool: settingDrawerVisible,
|
||||
setTrue: openSettingDrawer,
|
||||
setFalse: closeSettingDrawer,
|
||||
toggle: toggleSettingdrawerVisible
|
||||
} = useBoolean();
|
||||
visible: settingDrawerVisible,
|
||||
openModal: openSettingDrawer,
|
||||
closeModal: closeSettingDrawer,
|
||||
toggleModal: toggleSettingdrawerVisible
|
||||
} = useModalVisible();
|
||||
|
||||
// 侧边栏的折叠状态
|
||||
const { bool: siderCollapse, setBool: setSiderCollapse } = useBoolean();
|
||||
|
||||
// vertical-mix模式下 侧边栏的固定状态
|
||||
const { bool: mixSiderFixed, setBool: setMixSiderIsFixed } = useBoolean();
|
||||
|
||||
const appStore: AppStore = {
|
||||
reloadFlag,
|
||||
@ -38,7 +52,11 @@ export const useAppStore = defineStore('app-store', () => {
|
||||
settingDrawerVisible,
|
||||
openSettingDrawer,
|
||||
closeSettingDrawer,
|
||||
toggleSettingdrawerVisible
|
||||
toggleSettingdrawerVisible,
|
||||
siderCollapse,
|
||||
setSiderCollapse,
|
||||
mixSiderFixed,
|
||||
setMixSiderIsFixed
|
||||
};
|
||||
|
||||
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 { defineStore } from 'pinia';
|
||||
import { useThemeVars, darkTheme, useOsTheme } from 'naive-ui';
|
||||
import { darkTheme } from 'naive-ui';
|
||||
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
|
||||
import { themeSetting } from '@/settings';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { getColorPalette } from '@/utils';
|
||||
import { getNaiveThemeOverrides, addThemeCssVarsToHtml, handleWindicssDarkMode } from './helpers';
|
||||
|
||||
interface OtherColor {
|
||||
/** 信息 */
|
||||
info: string;
|
||||
/** 成功 */
|
||||
success: string;
|
||||
/** 警告 */
|
||||
warning: string;
|
||||
/** 错误 */
|
||||
error: string;
|
||||
}
|
||||
import type { ThemeSetting, ThemeHorizontalMenuPosition } from '@/interface';
|
||||
import { getNaiveThemeOverrides, addThemeCssVarsToHtml } from './helpers';
|
||||
import {
|
||||
useLayoutFunc,
|
||||
useHeaderFunc,
|
||||
useTabFunc,
|
||||
useSiderFunc,
|
||||
useFooterFunc,
|
||||
usePageFunc,
|
||||
osThemeWatcher,
|
||||
setupWindicssDarkMode,
|
||||
setupHiddenScroll
|
||||
} from './hooks';
|
||||
import type { LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc } from './hooks';
|
||||
|
||||
type BuiltInGlobalTheme = Omit<Required<GlobalTheme>, 'InternalSelectMenu' | 'InternalSelection'>;
|
||||
|
||||
interface ThemeStore {
|
||||
interface ThemeStore extends LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterFunc, PageFunc {
|
||||
/** 暗黑模式 */
|
||||
darkMode: Ref<boolean>;
|
||||
/** 设置暗黑模式 */
|
||||
setDarkMode(dark: boolean): void;
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDarkMode(dark: boolean): void;
|
||||
/** 布局样式 */
|
||||
layout: ThemeSetting['layout'];
|
||||
/** 主题颜色 */
|
||||
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的主题配置 */
|
||||
naiveThemeOverrides: ComputedRef<GlobalThemeOverrides>;
|
||||
/** naive-ui暗黑主题 */
|
||||
@ -38,72 +69,148 @@ interface ThemeStore {
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('theme-store', () => {
|
||||
const themeVars = useThemeVars();
|
||||
// 暗黑模式
|
||||
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
|
||||
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
||||
|
||||
const themeColor = ref('#1890ff');
|
||||
const otherColor = computed<OtherColor>(() => ({
|
||||
info: getColorPalette(themeColor.value, 7),
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
error: '#f5222d'
|
||||
// 布局
|
||||
const layout = reactive<ThemeSetting['layout']>({
|
||||
...themeSetting.layout
|
||||
});
|
||||
const { setLayoutMinWidth, setLayoutMode } = useLayoutFunc(layout);
|
||||
|
||||
// 主题色
|
||||
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>(() =>
|
||||
getNaiveThemeOverrides({ primary: themeColor.value, ...otherColor.value })
|
||||
);
|
||||
|
||||
/** naive-ui暗黑主题 */
|
||||
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
|
||||
|
||||
/** 操作系统暗黑主题 */
|
||||
const osTheme = useOsTheme();
|
||||
|
||||
/** 初始化css vars, 并添加至html */
|
||||
function initThemeCssVars() {
|
||||
const updatedThemeVars = { ...themeVars.value, ...naiveThemeOverrides.value.common };
|
||||
const updatedThemeVars = { ...naiveThemeOverrides.value.common };
|
||||
addThemeCssVarsToHtml(updatedThemeVars);
|
||||
}
|
||||
|
||||
function init() {
|
||||
initThemeCssVars();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
// 监听操作系统主题模式
|
||||
watch(
|
||||
osTheme,
|
||||
newValue => {
|
||||
const isDark = newValue === 'dark';
|
||||
/** 系统主题适应操作系统 */
|
||||
function handleAdaptOsTheme() {
|
||||
osThemeWatcher(isDark => {
|
||||
if (isDark) {
|
||||
setDarkMode(true);
|
||||
} else {
|
||||
setDarkMode(false);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// 监听主题的暗黑模式
|
||||
watch(
|
||||
() => darkMode.value,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
addDarkClass();
|
||||
} else {
|
||||
removeDarkClass();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function init() {
|
||||
initThemeCssVars();
|
||||
handleAdaptOsTheme();
|
||||
setupWindicssDarkMode(darkMode);
|
||||
setupHiddenScroll(computed(() => layout.minWidth));
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
const themeStore: ThemeStore = {
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
toggleDarkMode,
|
||||
layout,
|
||||
setLayoutMinWidth,
|
||||
setLayoutMode,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
themeColorList,
|
||||
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,
|
||||
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-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
@ -11,3 +21,56 @@
|
||||
opacity: 0;
|
||||
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 - 空白布局
|
||||
* - multi - 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局)
|
||||
* - self - 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)
|
||||
*/
|
||||
type RouteComponent = 'layout' | 'blank' | 'multi' | 'self';
|
||||
type RouteComponent = 'basic' | 'blank' | 'multi' | 'self';
|
||||
|
||||
/** 路由描述 */
|
||||
type RouteMeta = {
|
||||
@ -44,15 +44,13 @@ declare namespace AuthRoute {
|
||||
/** 路由的动态路径 */
|
||||
dynamicPath?: PathToDynamicPath<'/login'>;
|
||||
/** 作为单独路由的父级路由布局组件 */
|
||||
singleLayout?: Extract<RouteComponent, 'layout' | 'blank'>;
|
||||
singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>;
|
||||
/** 需要登录权限 */
|
||||
requiresAuth?: boolean;
|
||||
/** 哪些类型的用户有权限才能访问的路由 */
|
||||
permissions?: Auth.RoleType[];
|
||||
/** 缓存页面 */
|
||||
keepAlive?: boolean;
|
||||
/** 是否是空白布局 */
|
||||
blankLayout?: boolean;
|
||||
/** 菜单和面包屑对应的图标 */
|
||||
icon?: string;
|
||||
/** 是否在菜单中隐藏 */
|
||||
@ -73,7 +71,7 @@ declare namespace AuthRoute {
|
||||
redirect?: RoutePath;
|
||||
/**
|
||||
* 路由组件
|
||||
* - layout: 基础布局,具有公共部分的布局
|
||||
* - basic: 基础布局,具有公共部分的布局
|
||||
* - blank: 空白布局
|
||||
* - multi: 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局)
|
||||
* - self: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)
|
||||
|
@ -2,5 +2,6 @@ export * from './typeof';
|
||||
export * from './console';
|
||||
export * from './color';
|
||||
export * from './number';
|
||||
export * from './object';
|
||||
export * from './icon';
|
||||
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 { Layout } from '@/layouts';
|
||||
import { BasicLayout, BlankLayout } from '@/layouts';
|
||||
import { consoleError } from '../common';
|
||||
import { getViewComponent } from './component';
|
||||
|
||||
@ -30,14 +30,11 @@ function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
|
||||
|
||||
if (hasComponent(item)) {
|
||||
const action: ComponentAction = {
|
||||
layout() {
|
||||
itemRoute.component = Layout;
|
||||
basic() {
|
||||
itemRoute.component = BasicLayout;
|
||||
},
|
||||
blank() {
|
||||
itemRoute.component = Layout;
|
||||
if (itemRoute.meta) {
|
||||
itemRoute.meta.blankLayout = true;
|
||||
}
|
||||
itemRoute.component = BlankLayout;
|
||||
},
|
||||
multi() {
|
||||
// 多级路由一定有子路由
|
||||
@ -77,13 +74,11 @@ function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
|
||||
} else {
|
||||
const parentPath = `${itemRoute.path}-parent` as AuthRoute.SingleRouteParentPath;
|
||||
|
||||
if (item.meta.singleLayout === 'blank') {
|
||||
itemRoute.meta!.blankLayout = true;
|
||||
}
|
||||
const layout = item.meta.singleLayout === 'basic' ? BasicLayout : BlankLayout;
|
||||
|
||||
const parentRoute: RouteRecordRaw = {
|
||||
path: parentPath,
|
||||
component: Layout,
|
||||
component: layout,
|
||||
redirect: item.path,
|
||||
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>
|
||||
<div>
|
||||
<h3>about</h3>
|
||||
<router-link to="/">analysis</router-link>
|
||||
</div>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<project-introduction />
|
||||
<project-info />
|
||||
<pro-dependency />
|
||||
<dev-dependency />
|
||||
</n-space>
|
||||
</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>
|
||||
|
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 { defineConfig, loadEnv } from 'vite';
|
||||
import { setupVitePlugins } from './build';
|
||||
import { setupVitePlugins, define } from './build';
|
||||
|
||||
export default defineConfig(configEnv => {
|
||||
const viteEnv = loadEnv(configEnv.mode, `.env.${configEnv.mode}`);
|
||||
@ -13,6 +13,7 @@ export default defineConfig(configEnv => {
|
||||
'~': fileURLToPath(new URL('./', import.meta.url))
|
||||
}
|
||||
},
|
||||
define,
|
||||
plugins: setupVitePlugins(configEnv),
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
@ -41,6 +41,7 @@ export default defineConfig({
|
||||
'nowrap-hidden': 'whitespace-nowrap overflow-hidden',
|
||||
'ellipsis-text': 'nowrap-hidden overflow-ellipsis',
|
||||
'transition-base': 'transition-all duration-300 ease-in-out',
|
||||
// 'dark-transition': "",
|
||||
'init-loading-spin': 'w-16px h-16px bg-primary rounded-8px animate-pulse'
|
||||
},
|
||||
theme: {
|
||||
@ -67,6 +68,14 @@ export default defineConfig({
|
||||
'error-pressed': 'var(--error-color-pressed)',
|
||||
'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']
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user