5.6.9 • Published 7 months ago
@cimom/vben-preferences v5.6.9
@cimom/vben-preferences
用户偏好设置管理包,提供了一套完整的用户偏好设置管理机制,用于存储和管理用户的主题、语言、布局等偏好设置。该包继承了 @cimom/vben-core-preferences 的所有能力,并允许在应用级别覆盖默认偏好设置。
安装
# 进入目标应用目录,例如 apps/xxxx-app
# cd apps/xxxx-app
pnpm add @cimom/vben-preferences基本使用
获取和更新偏好设置
import { usePreferences } from '@cimom/vben-preferences';
// 在组件中使用
const { preferences, updatePreferences, resetPreferences } = usePreferences();
// 获取当前偏好设置
console.log('当前主题:', preferences.theme);
console.log('当前语言:', preferences.locale);
console.log('菜单模式:', preferences.menuMode);
// 更新偏好设置
function changeTheme(theme: 'light' | 'dark') {
updatePreferences({
theme,
});
}
// 更新多个偏好设置
function updateSettings(settings) {
updatePreferences({
theme: settings.theme,
locale: settings.locale,
menuMode: settings.menuMode,
});
}
// 重置为默认偏好设置
function resetToDefaults() {
resetPreferences();
}在模板中使用
<template>
<div>
<h2>用户偏好设置</h2>
<div class="setting-item">
<span>主题模式:</span>
<select v-model="currentTheme" @change="handleThemeChange">
<option value="light">浅色主题</option>
<option value="dark">深色主题</option>
<option value="auto">跟随系统</option>
</select>
</div>
<div class="setting-item">
<span>语言:</span>
<select v-model="currentLocale" @change="handleLocaleChange">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<div class="setting-item">
<span>菜单模式:</span>
<select v-model="currentMenuMode" @change="handleMenuModeChange">
<option value="vertical">垂直菜单</option>
<option value="horizontal">水平菜单</option>
<option value="inline">内嵌菜单</option>
</select>
</div>
<button @click="resetToDefaults">重置为默认设置</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { usePreferences } from '@cimom/vben-preferences';
const { preferences, updatePreferences, resetPreferences } = usePreferences();
// 计算属性,用于双向绑定
const currentTheme = computed({
get: () => preferences.theme,
set: (value) => updatePreferences({ theme: value }),
});
const currentLocale = computed({
get: () => preferences.locale,
set: (value) => updatePreferences({ locale: value }),
});
const currentMenuMode = computed({
get: () => preferences.menuMode,
set: (value) => updatePreferences({ menuMode: value }),
});
// 处理变更
function handleThemeChange() {
// 可以在这里添加额外的逻辑
console.log('主题已更改为:', currentTheme.value);
}
function handleLocaleChange() {
console.log('语言已更改为:', currentLocale.value);
}
function handleMenuModeChange() {
console.log('菜单模式已更改为:', currentMenuMode.value);
}
// 重置为默认设置
function resetToDefaults() {
resetPreferences();
}
</script>可用偏好设置
以下是可用的偏好设置项及其类型:
interface Preferences {
/**
* 主题模式
* - light: 浅色主题
* - dark: 深色主题
* - auto: 跟随系统
*/
theme: 'light' | 'dark' | 'auto';
/**
* 语言设置
*/
locale: string;
/**
* 菜单模式
* - vertical: 垂直菜单
* - horizontal: 水平菜单
* - inline: 内嵌菜单
*/
menuMode: 'vertical' | 'horizontal' | 'inline';
/**
* 菜单主题
* - light: 浅色主题
* - dark: 深色主题
*/
menuTheme: 'light' | 'dark';
/**
* 是否固定头部
*/
fixedHeader: boolean;
/**
* 是否固定侧边栏
*/
fixedSidebar: boolean;
/**
* 是否显示标签页
*/
showTabs: boolean;
/**
* 是否显示面包屑
*/
showBreadcrumb: boolean;
/**
* 是否显示页脚
*/
showFooter: boolean;
/**
* 内容区域宽度模式
* - fixed: 固定宽度
* - full: 流式宽度
*/
contentWidth: 'fixed' | 'full';
/**
* 是否折叠菜单
*/
collapsed: boolean;
/**
* 主色调
*/
primaryColor: string;
/**
* 是否开启色弱模式
*/
colorWeak: boolean;
/**
* 是否开启灰色模式
*/
grayMode: boolean;
/**
* 自定义扩展设置
*/
[key: string]: any;
}高级用法
覆盖默认偏好设置
如果你想为所有应用提供统一的默认偏好设置,可以使用 defineOverridesPreferences 函数:
// 在应用入口文件中
import { defineOverridesPreferences } from '@cimom/vben-preferences';
// 定义覆盖默认偏好设置
defineOverridesPreferences({
theme: 'light',
locale: 'zh-CN',
menuMode: 'vertical',
menuTheme: 'light',
fixedHeader: true,
fixedSidebar: true,
showTabs: true,
showBreadcrumb: true,
showFooter: false,
contentWidth: 'fixed',
collapsed: false,
primaryColor: '#1890ff',
colorWeak: false,
grayMode: false,
});监听偏好设置变化
import { watch } from 'vue';
import { usePreferences } from '@cimom/vben-preferences';
const { preferences } = usePreferences();
// 监听整个偏好设置对象的变化
watch(
() => preferences,
(newPreferences) => {
console.log('偏好设置已更改:', newPreferences);
},
{ deep: true },
);
// 监听特定偏好设置的变化
watch(
() => preferences.theme,
(newTheme) => {
console.log('主题已更改为:', newTheme);
// 更新 HTML 标签的类名以应用主题
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
},
);创建偏好设置管理组件
<!-- PreferencesPanel.vue -->
<template>
<div class="preferences-panel">
<h2>系统设置</h2>
<div class="section">
<h3>外观</h3>
<div class="setting-item">
<span>主题模式:</span>
<div class="theme-options">
<div
v-for="theme in themeOptions"
:key="theme.value"
class="theme-option"
:class="{ active: preferences.theme === theme.value }"
@click="updatePreferences({ theme: theme.value })"
>
<div class="theme-preview" :class="theme.value"></div>
<span>{{ theme.label }}</span>
</div>
</div>
</div>
<div class="setting-item">
<span>主色调:</span>
<div class="color-picker">
<div
v-for="color in colorOptions"
:key="color.value"
class="color-option"
:style="{ backgroundColor: color.value }"
:class="{ active: preferences.primaryColor === color.value }"
@click="updatePreferences({ primaryColor: color.value })"
></div>
</div>
</div>
<div class="setting-item">
<span>特殊模式:</span>
<div class="checkbox-group">
<label>
<input
type="checkbox"
:checked="preferences.colorWeak"
@change="updatePreferences({ colorWeak: !preferences.colorWeak })"
/>
色弱模式
</label>
<label>
<input
type="checkbox"
:checked="preferences.grayMode"
@change="updatePreferences({ grayMode: !preferences.grayMode })"
/>
灰色模式
</label>
</div>
</div>
</div>
<div class="section">
<h3>布局</h3>
<div class="setting-item">
<span>菜单模式:</span>
<div class="radio-group">
<label v-for="mode in menuModeOptions" :key="mode.value">
<input
type="radio"
:value="mode.value"
:checked="preferences.menuMode === mode.value"
@change="updatePreferences({ menuMode: mode.value })"
/>
{{ mode.label }}
</label>
</div>
</div>
<div class="setting-item">
<span>菜单主题:</span>
<div class="radio-group">
<label v-for="theme in menuThemeOptions" :key="theme.value">
<input
type="radio"
:value="theme.value"
:checked="preferences.menuTheme === theme.value"
@change="updatePreferences({ menuTheme: theme.value })"
/>
{{ theme.label }}
</label>
</div>
</div>
<div class="setting-item">
<span>内容区域:</span>
<div class="radio-group">
<label v-for="width in contentWidthOptions" :key="width.value">
<input
type="radio"
:value="width.value"
:checked="preferences.contentWidth === width.value"
@change="updatePreferences({ contentWidth: width.value })"
/>
{{ width.label }}
</label>
</div>
</div>
<div class="setting-item">
<span>固定选项:</span>
<div class="checkbox-group">
<label>
<input
type="checkbox"
:checked="preferences.fixedHeader"
@change="
updatePreferences({ fixedHeader: !preferences.fixedHeader })
"
/>
固定头部
</label>
<label>
<input
type="checkbox"
:checked="preferences.fixedSidebar"
@change="
updatePreferences({ fixedSidebar: !preferences.fixedSidebar })
"
/>
固定侧边栏
</label>
</div>
</div>
<div class="setting-item">
<span>显示选项:</span>
<div class="checkbox-group">
<label>
<input
type="checkbox"
:checked="preferences.showTabs"
@change="updatePreferences({ showTabs: !preferences.showTabs })"
/>
显示标签页
</label>
<label>
<input
type="checkbox"
:checked="preferences.showBreadcrumb"
@change="
updatePreferences({
showBreadcrumb: !preferences.showBreadcrumb,
})
"
/>
显示面包屑
</label>
<label>
<input
type="checkbox"
:checked="preferences.showFooter"
@change="
updatePreferences({ showFooter: !preferences.showFooter })
"
/>
显示页脚
</label>
</div>
</div>
</div>
<div class="section">
<h3>语言</h3>
<div class="setting-item">
<span>系统语言:</span>
<select
:value="preferences.locale"
@change="updatePreferences({ locale: $event.target.value })"
>
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
</div>
<div class="actions">
<button @click="resetPreferences">恢复默认设置</button>
</div>
</div>
</template>
<script setup lang="ts">
import { usePreferences } from '@cimom/vben-preferences';
const { preferences, updatePreferences, resetPreferences } = usePreferences();
// 选项数据
const themeOptions = [
{ label: '浅色', value: 'light' },
{ label: '深色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const colorOptions = [
{ value: '#1890ff' }, // 蓝色
{ value: '#f5222d' }, // 红色
{ value: '#fa8c16' }, // 橙色
{ value: '#faad14' }, // 黄色
{ value: '#52c41a' }, // 绿色
{ value: '#13c2c2' }, // 青色
{ value: '#722ed1' }, // 紫色
];
const menuModeOptions = [
{ label: '垂直', value: 'vertical' },
{ label: '水平', value: 'horizontal' },
{ label: '内嵌', value: 'inline' },
];
const menuThemeOptions = [
{ label: '浅色', value: 'light' },
{ label: '深色', value: 'dark' },
];
const contentWidthOptions = [
{ label: '固定宽度', value: 'fixed' },
{ label: '流式宽度', value: 'full' },
];
</script>
<style scoped>
.preferences-panel {
padding: 16px;
max-width: 800px;
margin: 0 auto;
}
.section {
margin-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
}
.setting-item {
margin-bottom: 16px;
display: flex;
align-items: flex-start;
}
.setting-item > span {
width: 100px;
text-align: right;
margin-right: 16px;
flex-shrink: 0;
}
.theme-options {
display: flex;
gap: 16px;
}
.theme-option {
text-align: center;
cursor: pointer;
}
.theme-option.active {
border: 2px solid #1890ff;
border-radius: 4px;
}
.theme-preview {
width: 48px;
height: 48px;
border-radius: 4px;
margin-bottom: 8px;
}
.theme-preview.light {
background-color: #fff;
border: 1px solid #f0f0f0;
}
.theme-preview.dark {
background-color: #141414;
}
.theme-preview.auto {
background: linear-gradient(to right, #fff 50%, #141414 50%);
border: 1px solid #f0f0f0;
}
.color-picker {
display: flex;
gap: 8px;
}
.color-option {
width: 20px;
height: 20px;
border-radius: 4px;
cursor: pointer;
}
.color-option.active {
box-shadow:
0 0 0 2px #fff,
0 0 0 4px #1890ff;
}
.checkbox-group,
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.actions {
margin-top: 24px;
text-align: center;
}
button {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #40a9ff;
}
select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #d9d9d9;
}
</style>与其他包的集成
与 @cimom/vben-effects-hooks 集成
import { watch } from 'vue';
import { usePreferences } from '@cimom/vben-preferences';
import { useDesignTokens } from '@cimom/vben-effects-hooks';
// 创建主题钩子
export function useThemeManager() {
const { preferences, updatePreferences } = usePreferences();
const { primaryColor } = useDesignTokens();
// 监听主题变化
watch(
() => preferences.theme,
(theme) => {
// 更新 HTML 类名
const htmlEl = document.documentElement;
if (theme === 'dark') {
htmlEl.classList.add('dark');
htmlEl.classList.remove('light');
} else if (theme === 'light') {
htmlEl.classList.add('light');
htmlEl.classList.remove('dark');
} else if (theme === 'auto') {
// 检测系统主题
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches;
if (prefersDark) {
htmlEl.classList.add('dark');
htmlEl.classList.remove('light');
} else {
htmlEl.classList.add('light');
htmlEl.classList.remove('dark');
}
// 监听系统主题变化
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (preferences.theme === 'auto') {
if (e.matches) {
htmlEl.classList.add('dark');
htmlEl.classList.remove('light');
} else {
htmlEl.classList.add('light');
htmlEl.classList.remove('dark');
}
}
});
}
},
{ immediate: true },
);
// 监听色弱模式变化
watch(
() => preferences.colorWeak,
(colorWeak) => {
const htmlEl = document.documentElement;
if (colorWeak) {
htmlEl.classList.add('color-weak');
} else {
htmlEl.classList.remove('color-weak');
}
},
{ immediate: true },
);
// 监听灰色模式变化
watch(
() => preferences.grayMode,
(grayMode) => {
const htmlEl = document.documentElement;
if (grayMode) {
htmlEl.classList.add('gray-mode');
} else {
htmlEl.classList.remove('gray-mode');
}
},
{ immediate: true },
);
return {
preferences,
updatePreferences,
primaryColor,
};
}与 @cimom/vben-locales 集成
import { watch } from 'vue';
import { usePreferences } from '@cimom/vben-preferences';
import { useI18n } from '@cimom/vben-locales';
// 创建语言管理钩子
export function useLanguageManager() {
const { preferences, updatePreferences } = usePreferences();
const { locale, setLocale, t } = useI18n();
// 监听语言变化
watch(
() => preferences.locale,
(newLocale) => {
// 更新 i18n 语言设置
if (newLocale !== locale.value) {
setLocale(newLocale);
}
},
{ immediate: true },
);
// 更新语言并同时更新偏好设置
function changeLanguage(lang: string) {
updatePreferences({ locale: lang });
}
return {
locale,
changeLanguage,
t,
};
}注意事项
- 偏好设置会自动保存到浏览器的 localStorage 中,在用户下次访问时会自动恢复。
- 更新偏好设置时,只需要提供要更改的属性,不需要提供完整的偏好设置对象。
- 如果需要在多个应用中共享相同的默认偏好设置,应该使用
defineOverridesPreferences函数,而不是修改@cimom/vben-core-preferences中的默认设置。 - 偏好设置的变更是响应式的,可以使用 Vue 的
watch或computed来监听和响应变化。
常见问题
偏好设置没有正确保存
确保没有禁用浏览器的 localStorage 功能,可以通过以下方式检查:
function checkLocalStorage() {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (e) {
return false;
}
}
if (!checkLocalStorage()) {
console.warn('localStorage 不可用,偏好设置将无法保存');
}主题切换不生效
确保在应用中正确应用了主题类名:
import { watch } from 'vue';
import { usePreferences } from '@cimom/vben-preferences';
const { preferences } = usePreferences();
// 在应用初始化时设置主题
watch(
() => preferences.theme,
(theme) => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
}
},
{ immediate: true },
);自定义扩展偏好设置
如果需要添加自定义的偏好设置项,可以通过类型扩展:
import { Preferences, usePreferences } from '@cimom/vben-preferences';
// 扩展偏好设置类型
declare module '@cimom/vben-preferences' {
interface Preferences {
// 添加自定义偏好设置
customSetting1: string;
customSetting2: boolean;
}
}
// 使用扩展后的偏好设置
const { preferences, updatePreferences } = usePreferences();
// 更新自定义偏好设置
updatePreferences({
customSetting1: 'value',
customSetting2: true,
});