mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-25 16:16:20 +08:00
chore: init project
This commit is contained in:
7
packages/preference/build.config.ts
Normal file
7
packages/preference/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
42
packages/preference/package.json
Normal file
42
packages/preference/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@vben/preference",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/preference"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"vue": "^3.4.27"
|
||||
}
|
||||
}
|
60
packages/preference/src/cache.ts
Normal file
60
packages/preference/src/cache.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Preference } from '@vben-core/typings';
|
||||
|
||||
class PreferenceCache {
|
||||
cachePrefix: string;
|
||||
constructor(cachePrefix: string = 'vben-admin') {
|
||||
this.cachePrefix = cachePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 localStorage 中获取偏好设置
|
||||
* @returns 返回从 localStorage 中获取的偏好设置,如果没有获取到,则返回默认偏好设置。
|
||||
*/
|
||||
get(defaultValue: Preference): Preference {
|
||||
let cache = defaultValue;
|
||||
try {
|
||||
cache = JSON.parse(localStorage.getItem(this.getCacheKey()) || '');
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏好设置的缓存键
|
||||
*/
|
||||
getCacheKey(name: string = 'preference') {
|
||||
const env = import.meta.env.DEV ? 'dev' : 'prod';
|
||||
return `__${this.cachePrefix}-${name}-${env}__`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 localStorage 中移除偏好设置
|
||||
*/
|
||||
remove() {
|
||||
localStorage.removeItem(this.getCacheKey());
|
||||
localStorage.removeItem(this.getCacheKey('locale'));
|
||||
localStorage.removeItem(this.getCacheKey('theme'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前偏好设置持久化到 localStorage
|
||||
*/
|
||||
set(preference: Preference) {
|
||||
localStorage.setItem(this.getCacheKey(), JSON.stringify(preference));
|
||||
// 额外存储一份主题、语言
|
||||
localStorage.setItem(this.getCacheKey('locale'), preference.locale);
|
||||
localStorage.setItem(this.getCacheKey('theme'), preference.theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置偏好设置的缓存前缀
|
||||
* @param prefix - 前缀
|
||||
*/
|
||||
setCachePrefix(prefix: string) {
|
||||
this.cachePrefix = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
export type PreferenceCacheType = PreferenceCache;
|
||||
export { PreferenceCache };
|
69
packages/preference/src/config.ts
Normal file
69
packages/preference/src/config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Preference, StaticPreference } from '@vben-core/typings';
|
||||
|
||||
const defaultPreference: Preference = {
|
||||
appName: 'Vben Admin Pro',
|
||||
authPageLayout: 'panel-right',
|
||||
breadcrumbHideOnlyOne: false,
|
||||
breadcrumbHome: false,
|
||||
breadcrumbIcon: true,
|
||||
breadcrumbStyle: 'normal',
|
||||
breadcrumbVisible: true,
|
||||
colorGrayMode: false,
|
||||
colorPrimary: 'hsl(211 91% 39%)',
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
copyright: 'Copyright © 2024 Vben Admin PRO',
|
||||
defaultAvatar:
|
||||
'https://cdn.jsdelivr.net/gh/vbenjs/vben-cdn-static@0.1.2/vben-admin/pro-avatar.webp',
|
||||
footerFixed: true,
|
||||
footerVisible: true,
|
||||
headerMode: 'fixed',
|
||||
headerVisible: true,
|
||||
isMobile: false,
|
||||
keepAlive: true,
|
||||
layout: 'side-nav',
|
||||
locale: 'zh-CN',
|
||||
logo: 'https://cdn.jsdelivr.net/gh/vbenjs/vben-cdn-static@0.1.2/vben-admin/admin-logo.png',
|
||||
logoVisible: true,
|
||||
navigationStyle: 'rounded',
|
||||
pageProgress: true,
|
||||
pageTransition: 'fade-slide',
|
||||
pageTransitionEnable: true,
|
||||
semiDarkMenu: true,
|
||||
sideCollapse: false,
|
||||
sideCollapseShowTitle: false,
|
||||
sideExpandOnHover: true,
|
||||
sideExtraCollapse: true,
|
||||
sideVisible: true,
|
||||
sideWidth: 240,
|
||||
tabsIcon: true,
|
||||
tabsVisible: true,
|
||||
theme: 'dark',
|
||||
};
|
||||
|
||||
/**
|
||||
* 静态偏好设置,这些配置不会被用户修改
|
||||
*/
|
||||
const staticPreference: StaticPreference = {
|
||||
colorPrimaryPresets: [
|
||||
'hsl(211 91% 39%)',
|
||||
'hsl(212 100% 45%)',
|
||||
'hsl(181 84% 32%)',
|
||||
'hsl(230 99% 66%)',
|
||||
'hsl(245 82% 67%)',
|
||||
'hsl(340 100% 68%)',
|
||||
],
|
||||
supportLanguages: [
|
||||
{
|
||||
key: 'zh-CN',
|
||||
text: '简体中文',
|
||||
},
|
||||
{
|
||||
key: 'en-US',
|
||||
text: 'English',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export { defaultPreference, staticPreference };
|
19
packages/preference/src/index.ts
Normal file
19
packages/preference/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Preference } from '@vben-core/typings';
|
||||
|
||||
import { readonly } from 'vue';
|
||||
|
||||
import {
|
||||
currentPreference,
|
||||
resetPreference,
|
||||
updatePreference,
|
||||
} from './preference';
|
||||
|
||||
export { staticPreference } from './config';
|
||||
|
||||
// 只读偏好设置
|
||||
const preference: Readonly<Preference> = readonly(currentPreference);
|
||||
|
||||
export * from './setup';
|
||||
|
||||
export { preference, resetPreference, updatePreference };
|
||||
export * from './use-preference';
|
198
packages/preference/src/preference.ts
Normal file
198
packages/preference/src/preference.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type {
|
||||
DeepPartial,
|
||||
Preference,
|
||||
PreferenceKeys,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
import { convertToHslCssVar, merge } from '@vben-core/toolkit';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { breakpointsTailwind, useBreakpoints, useCssVar } from '@vueuse/core';
|
||||
import { markRaw, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { defaultPreference } from './config';
|
||||
|
||||
import type { PreferenceCacheType } from './cache';
|
||||
|
||||
/**
|
||||
* 当前偏好设置
|
||||
*/
|
||||
const currentPreference: Preference = reactive(defaultPreference);
|
||||
/**
|
||||
* 当前偏好设置原始值
|
||||
*/
|
||||
const initialPreference: Ref<Preference> = ref(defaultPreference);
|
||||
|
||||
let preferenceCache: PreferenceCacheType;
|
||||
|
||||
/**
|
||||
* 是否监听过系统设置变化
|
||||
*/
|
||||
let isRegisterListen = false;
|
||||
|
||||
function updatePreference(key: keyof Preference, value: boolean | string): void;
|
||||
function updatePreference(
|
||||
preference: DeepPartial<Preference>,
|
||||
value?: undefined,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 更新偏好设置
|
||||
* @param preference - 一个部分偏好设置对象,它将被合并到当前偏好设置中。
|
||||
*/
|
||||
function updatePreference(preference: any, value: any) {
|
||||
if (typeof preference === 'string') {
|
||||
updatePreference({ [preference]: value }, value);
|
||||
} else {
|
||||
const updateKeys = Object.keys(preference) as PreferenceKeys[];
|
||||
|
||||
const mergePreference = merge(preference, markRaw(currentPreference));
|
||||
|
||||
Object.assign(currentPreference, mergePreference);
|
||||
|
||||
// 当修改到颜色变量时,更新 css 变量
|
||||
if (updateKeys.includes('colorPrimary')) {
|
||||
updateCssVar(currentPreference);
|
||||
}
|
||||
|
||||
// 更新主题
|
||||
if (updateKeys.includes('theme')) {
|
||||
updateTheme(currentPreference);
|
||||
}
|
||||
|
||||
// 更新页面颜色模式(灰色、色弱)
|
||||
if (
|
||||
updateKeys.includes('colorGrayMode') ||
|
||||
updateKeys.includes('colorWeakMode')
|
||||
) {
|
||||
updateColorMode(currentPreference);
|
||||
}
|
||||
|
||||
preferenceCache.set(currentPreference);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 CSS 变量
|
||||
* @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
|
||||
*/
|
||||
function updateCssVar(preference: Preference) {
|
||||
for (const [key, value] of Object.entries(preference)) {
|
||||
if (['colorPrimary'].includes(key)) {
|
||||
const cssVarKey = key.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
|
||||
const cssVarValue = useCssVar(`--${cssVarKey}`);
|
||||
cssVarValue.value = convertToHslCssVar(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
* @param preference - 当前偏好设置对象,它的主题值将被用来设置文档的主题。
|
||||
*/
|
||||
function updateTheme(preference: Preference) {
|
||||
// 当修改到颜色变量时,更新 css 变量
|
||||
const root = document.documentElement;
|
||||
if (root) {
|
||||
const dark = isDarkTheme(preference.theme);
|
||||
root.classList.toggle('dark', dark);
|
||||
}
|
||||
|
||||
// 只需要监听一次即可
|
||||
listenOnce(preference);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面颜色模式(灰色、色弱)
|
||||
* @param preference
|
||||
*/
|
||||
function updateColorMode(preference: Preference) {
|
||||
const { colorGrayMode, colorWeakMode } = preference;
|
||||
const body = document.body;
|
||||
const COLOR_WEAK = 'invert';
|
||||
const COLOR_GRAY = 'grayscale';
|
||||
colorWeakMode
|
||||
? body.classList.add(COLOR_WEAK)
|
||||
: body.classList.remove(COLOR_WEAK);
|
||||
colorGrayMode
|
||||
? body.classList.add(COLOR_GRAY)
|
||||
: body.classList.remove(COLOR_GRAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 监听系统主题偏好设置变化
|
||||
* 2. 监听断点,判断是否移动端
|
||||
* @param preference - 当前偏好设置对象,当系统主题偏好变化时,它的主题值会被更新。
|
||||
*/
|
||||
function listenOnce(preference: Preference) {
|
||||
if (isRegisterListen) {
|
||||
return;
|
||||
}
|
||||
isRegisterListen = true;
|
||||
// 监听系统主题偏好设置变化
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', ({ matches: isDark }) => {
|
||||
preference.theme = isDark ? 'dark' : 'light';
|
||||
updateTheme(preference);
|
||||
});
|
||||
|
||||
// 监听断点,判断是否移动端
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
watch(
|
||||
() => isMobile.value,
|
||||
(val) => {
|
||||
currentPreference.isMobile = val;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置偏好设置
|
||||
* 偏好设置将被重置为初始值,并从 localStorage 中移除。
|
||||
*/
|
||||
function resetPreference() {
|
||||
Object.assign(currentPreference, initialPreference.value);
|
||||
updatePreference(currentPreference);
|
||||
preferenceCache.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置当前app默认的偏好配置
|
||||
* @param overrides
|
||||
*/
|
||||
function overridesPreference(
|
||||
overrides: DeepPartial<Preference>,
|
||||
cache: PreferenceCacheType,
|
||||
) {
|
||||
preferenceCache = cache;
|
||||
/**
|
||||
* 重置状态时用到的原始值
|
||||
*/
|
||||
initialPreference.value = merge(overrides, defaultPreference);
|
||||
const mergedPreference = merge(
|
||||
overrides,
|
||||
preferenceCache.get(defaultPreference),
|
||||
);
|
||||
updatePreference(mergedPreference);
|
||||
}
|
||||
|
||||
function isDarkTheme(theme: string) {
|
||||
let dark = theme === 'dark';
|
||||
if (theme === 'auto') {
|
||||
dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return dark;
|
||||
}
|
||||
|
||||
export {
|
||||
currentPreference,
|
||||
initialPreference,
|
||||
isDarkTheme,
|
||||
overridesPreference,
|
||||
resetPreference,
|
||||
updatePreference,
|
||||
};
|
28
packages/preference/src/setup.ts
Normal file
28
packages/preference/src/setup.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { DeepPartial, Preference } from '@vben-core/typings';
|
||||
|
||||
import { PreferenceCache } from './cache';
|
||||
import { overridesPreference } from './preference';
|
||||
|
||||
interface SetupPreferenceOptions {
|
||||
/**
|
||||
* @zh_CN 应用名,由于 @vben/preference 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
|
||||
* 应用名将被用于持久化的前缀
|
||||
*/
|
||||
cachePrefix?: string;
|
||||
/**
|
||||
* @zh_CN app自行覆盖偏好设置
|
||||
*/
|
||||
overrides?: DeepPartial<Preference>;
|
||||
}
|
||||
|
||||
async function setupPreference(options: SetupPreferenceOptions = {}) {
|
||||
const { cachePrefix = 'vben-admin-pro', overrides = {} } = options;
|
||||
|
||||
const cache = new PreferenceCache(cachePrefix);
|
||||
|
||||
overridesPreference(overrides, cache);
|
||||
}
|
||||
|
||||
export { setupPreference };
|
||||
|
||||
export type { SetupPreferenceOptions };
|
119
packages/preference/src/use-preference.ts
Normal file
119
packages/preference/src/use-preference.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { diff } from '@vben-core/toolkit';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
initialPreference,
|
||||
isDarkTheme,
|
||||
currentPreference as preference,
|
||||
} from './preference';
|
||||
|
||||
function usePreference() {
|
||||
/**
|
||||
* @zh_CN 计算偏好设置的变化
|
||||
*/
|
||||
const diffPreference = computed(() => {
|
||||
return diff(initialPreference.value, preference);
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 判断是否为暗黑模式
|
||||
* @param preference - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
|
||||
* @returns 如果主题为暗黑模式,返回 true,否则返回 false。
|
||||
*/
|
||||
const isDark = computed(() => {
|
||||
const theme = preference.theme;
|
||||
return isDarkTheme(theme);
|
||||
});
|
||||
|
||||
const theme = computed(() => {
|
||||
return isDark.value ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 布局方式
|
||||
*/
|
||||
const layout = computed(() =>
|
||||
preference.isMobile ? 'side-nav' : preference.layout,
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域
|
||||
*/
|
||||
const isFullContent = computed(() => preference.layout === 'full-content');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否侧边导航模式
|
||||
*/
|
||||
const isSideNav = computed(() => preference.layout === 'side-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否侧边混合模式
|
||||
*/
|
||||
const isSideMixedNav = computed(() => preference.layout === 'side-mixed-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为头部导航模式
|
||||
*/
|
||||
const isHeaderNav = computed(() => preference.layout === 'header-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为混合导航模式
|
||||
*/
|
||||
const isMixedNav = computed(() => preference.layout === 'mixed-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否包含侧边导航模式
|
||||
*/
|
||||
const isSideMode = computed(() => {
|
||||
return isMixedNav.value || isSideMixedNav.value || isSideNav.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 是否开启keep-alive
|
||||
* 在tabs可见以及开启keep-alive的情况下才开启
|
||||
*/
|
||||
const keepAlive = computed(
|
||||
() => preference.keepAlive && preference.tabsVisible,
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为左侧
|
||||
*/
|
||||
const authPanelLeft = computed(() => {
|
||||
return preference.authPageLayout === 'panel-left';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为左侧
|
||||
*/
|
||||
const authPanelRight = computed(() => {
|
||||
return preference.authPageLayout === 'panel-right';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为中间
|
||||
*/
|
||||
const authPanelCenter = computed(() => {
|
||||
return preference.authPageLayout === 'panel-center';
|
||||
});
|
||||
|
||||
return {
|
||||
authPanelCenter,
|
||||
authPanelLeft,
|
||||
authPanelRight,
|
||||
diffPreference,
|
||||
isDark,
|
||||
isFullContent,
|
||||
isHeaderNav,
|
||||
isMixedNav,
|
||||
isSideMixedNav,
|
||||
isSideMode,
|
||||
isSideNav,
|
||||
keepAlive,
|
||||
layout,
|
||||
theme,
|
||||
};
|
||||
}
|
||||
|
||||
export { usePreference };
|
5
packages/preference/tsconfig.json
Normal file
5
packages/preference/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"]
|
||||
}
|
Reference in New Issue
Block a user