5.6.9 • Published 7 months ago

@cimom/vben-preferences v5.6.9

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

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

注意事项

  1. 偏好设置会自动保存到浏览器的 localStorage 中,在用户下次访问时会自动恢复。
  2. 更新偏好设置时,只需要提供要更改的属性,不需要提供完整的偏好设置对象。
  3. 如果需要在多个应用中共享相同的默认偏好设置,应该使用 defineOverridesPreferences 函数,而不是修改 @cimom/vben-core-preferences 中的默认设置。
  4. 偏好设置的变更是响应式的,可以使用 Vue 的 watchcomputed 来监听和响应变化。

常见问题

偏好设置没有正确保存

确保没有禁用浏览器的 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,
});
5.6.9

7 months ago

5.6.3

7 months ago

5.6.2

7 months ago

5.6.1

7 months ago

5.5.16

7 months ago

5.5.15

7 months ago

5.5.14

7 months ago

5.5.13

7 months ago

5.5.12

7 months ago

5.5.9

7 months ago