chore: init project

This commit is contained in:
vben
2024-05-19 21:20:42 +08:00
commit 399334ac57
630 changed files with 45623 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View 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"
}
}

View 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 };

View 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 };

View 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';

View 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,
};

View 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 };

View 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 };

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"]
}