请添加图片描述

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

本文对应模块:待办事项应用中的应用设置与个性化配置功能,重点关注用户偏好设置、主题切换、通知配置、数据管理等核心设置功能的实现。

📌 概述

一个优秀的应用不仅要提供强大的功能,更要尊重用户的个性化需求。待办事项应用的设置页面是用户定制应用行为、外观和数据管理的中心枢纽。我们需要实现以下核心功能:

  • 用户偏好设置:语言选择、时间格式、周开始日期等;
  • 主题与外观:深色/浅色主题、自定义颜色、字体大小调整;
  • 通知与提醒:提醒时间、声音开关、桌面通知权限;
  • 数据管理:数据备份、导入导出、清空数据等高危操作;
  • 应用信息:版本号、更新检查、关于应用、帮助文档。

这些设置需要持久化到本地存储(LocalStorage/IndexedDB),并在应用启动时恢复用户的个性化配置。

🔗 设置系统架构流程

用户进入设置页面

加载用户偏好设置

显示设置表单

用户修改设置?

验证设置值

验证通过?

显示错误提示

保存到 LocalStorage

应用设置到 UI

显示保存成功提示

用户离开设置页面

保持当前配置

这个流程展示了从用户进入设置页面到配置被应用的完整链路:先加载已保存的设置,用户修改后进行验证,通过后保存到本地存储并实时应用到应用界面。

🔧 Web 层:SettingsManager 核心实现

// 应用设置管理器
class SettingsManager {
  static DEFAULT_SETTINGS = {
    // 基础设置
    language: 'zh-CN',
    timeFormat: '24h',
    weekStart: 'Monday',
    
    // 主题设置
    theme: 'light',
    primaryColor: '#4a90e2',
    fontSize: 'medium',
    
    // 通知设置
    enableNotifications: true,
    notificationSound: true,
    reminderTime: 9,
    
    // 数据设置
    autoBackup: false,
    backupFrequency: 'weekly',
    
    // 其他设置
    compactMode: false,
    showCompletedTasks: true
  };

  static async loadSettings() {
    try {
      const stored = localStorage.getItem('appSettings');
      if (stored) {
        return JSON.parse(stored);
      }
      return { ...this.DEFAULT_SETTINGS };
    } catch (error) {
      console.error('加载设置失败:', error);
      return { ...this.DEFAULT_SETTINGS };
    }
  }

  static async saveSettings(settings) {
    try {
      // 验证设置值
      this.validateSettings(settings);
      localStorage.setItem('appSettings', JSON.stringify(settings));
      return true;
    } catch (error) {
      console.error('保存设置失败:', error);
      throw error;
    }
  }

  static validateSettings(settings) {
    const validLanguages = ['zh-CN', 'en-US', 'ja-JP'];
    const validThemes = ['light', 'dark', 'auto'];
    const validFontSizes = ['small', 'medium', 'large'];

    if (settings.language && !validLanguages.includes(settings.language)) {
      throw new Error('无效的语言设置');
    }
    if (settings.theme && !validThemes.includes(settings.theme)) {
      throw new Error('无效的主题设置');
    }
    if (settings.fontSize && !validFontSizes.includes(settings.fontSize)) {
      throw new Error('无效的字体大小');
    }
    if (settings.reminderTime && (settings.reminderTime < 0 || settings.reminderTime > 23)) {
      throw new Error('提醒时间必须在 0-23 之间');
    }
  }

  static applyTheme(theme) {
    const root = document.documentElement;
    if (theme === 'dark') {
      root.style.setProperty('--bg-primary', '#1a1a1a');
      root.style.setProperty('--bg-secondary', '#2d2d2d');
      root.style.setProperty('--text-primary', '#ffffff');
      root.style.setProperty('--text-secondary', '#b0b0b0');
    } else {
      root.style.setProperty('--bg-primary', '#ffffff');
      root.style.setProperty('--bg-secondary', '#f5f5f5');
      root.style.setProperty('--text-primary', '#333333');
      root.style.setProperty('--text-secondary', '#666666');
    }
  }

  static applyFontSize(size) {
    const root = document.documentElement;
    const sizeMap = {
      small: '12px',
      medium: '14px',
      large: '16px'
    };
    root.style.setProperty('--font-size-base', sizeMap[size] || '14px');
  }

  static applyPrimaryColor(color) {
    const root = document.documentElement;
    root.style.setProperty('--color-primary', color);
  }
}

代码解释:

  • DEFAULT_SETTINGS 定义了所有可配置项的默认值,包括语言、主题、通知等;
  • loadSettings() 从 LocalStorage 读取用户保存的设置,如果不存在则返回默认值;
  • saveSettings() 在保存前进行验证,确保设置值在有效范围内,然后序列化为 JSON 存储;
  • validateSettings() 对关键设置项进行白名单验证,防止无效值被保存;
  • applyTheme/applyFontSize/applyPrimaryColor() 通过 CSS 变量实时应用设置到页面,实现即时生效。

🧩 设置页面 UI 实现

// 设置页面模块
class SettingsModule {
  static async render() {
    const settings = await SettingsManager.loadSettings();
    
    const container = document.createElement('div');
    container.className = 'settings-container';
    container.innerHTML = `
      <div class="settings-section">
        <h3 class="section-title">基础设置</h3>
        <div class="settings-item">
          <label>语言</label>
          <select id="language-select" class="ui-select">
            <option value="zh-CN" ${settings.language === 'zh-CN' ? 'selected' : ''}>中文</option>
            <option value="en-US" ${settings.language === 'en-US' ? 'selected' : ''}>English</option>
            <option value="ja-JP" ${settings.language === 'ja-JP' ? 'selected' : ''}>日本語</option>
          </select>
        </div>
        
        <div class="settings-item">
          <label>时间格式</label>
          <select id="timeformat-select" class="ui-select">
            <option value="24h" ${settings.timeFormat === '24h' ? 'selected' : ''}>24小时制</option>
            <option value="12h" ${settings.timeFormat === '12h' ? 'selected' : ''}>12小时制</option>
          </select>
        </div>

        <div class="settings-item">
          <label>周开始日期</label>
          <select id="weekstart-select" class="ui-select">
            <option value="Monday" ${settings.weekStart === 'Monday' ? 'selected' : ''}>星期一</option>
            <option value="Sunday" ${settings.weekStart === 'Sunday' ? 'selected' : ''}>星期日</option>
          </select>
        </div>
      </div>

      <div class="settings-section">
        <h3 class="section-title">主题与外观</h3>
        <div class="settings-item">
          <label>主题</label>
          <div class="theme-selector">
            <button class="theme-btn ${settings.theme === 'light' ? 'active' : ''}" 
                    data-theme="light">☀️ 浅色</button>
            <button class="theme-btn ${settings.theme === 'dark' ? 'active' : ''}" 
                    data-theme="dark">🌙 深色</button>
            <button class="theme-btn ${settings.theme === 'auto' ? 'active' : ''}" 
                    data-theme="auto">🔄 自动</button>
          </div>
        </div>

        <div class="settings-item">
          <label>主题色</label>
          <div class="color-picker-wrapper">
            <input type="color" id="primary-color" class="color-picker" 
                   value="${settings.primaryColor}">
            <span class="color-preview" style="background-color: ${settings.primaryColor}"></span>
          </div>
        </div>

        <div class="settings-item">
          <label>字体大小</label>
          <div class="font-size-selector">
            <button class="size-btn ${settings.fontSize === 'small' ? 'active' : ''}" 
                    data-size="small">小</button>
            <button class="size-btn ${settings.fontSize === 'medium' ? 'active' : ''}" 
                    data-size="medium">中</button>
            <button class="size-btn ${settings.fontSize === 'large' ? 'active' : ''}" 
                    data-size="large">大</button>
          </div>
        </div>

        <div class="settings-item">
          <label>
            <input type="checkbox" id="compact-mode" 
                   ${settings.compactMode ? 'checked' : ''}>
            紧凑模式
          </label>
        </div>
      </div>

      <div class="settings-section">
        <h3 class="section-title">通知与提醒</h3>
        <div class="settings-item">
          <label>
            <input type="checkbox" id="enable-notifications" 
                   ${settings.enableNotifications ? 'checked' : ''}>
            启用通知
          </label>
        </div>

        <div class="settings-item">
          <label>
            <input type="checkbox" id="notification-sound" 
                   ${settings.notificationSound ? 'checked' : ''}>
            通知声音
          </label>
        </div>

        <div class="settings-item">
          <label>默认提醒时间</label>
          <input type="number" id="reminder-time" class="ui-input" 
                 min="0" max="23" value="${settings.reminderTime}">
          <small>设置为 0-23 之间的小时数</small>
        </div>
      </div>

      <div class="settings-section">
        <h3 class="section-title">数据管理</h3>
        <div class="settings-item">
          <label>
            <input type="checkbox" id="auto-backup" 
                   ${settings.autoBackup ? 'checked' : ''}>
            自动备份
          </label>
        </div>

        <div class="settings-item">
          <label>备份频率</label>
          <select id="backup-frequency" class="ui-select" 
                  ${!settings.autoBackup ? 'disabled' : ''}>
            <option value="daily" ${settings.backupFrequency === 'daily' ? 'selected' : ''}>每天</option>
            <option value="weekly" ${settings.backupFrequency === 'weekly' ? 'selected' : ''}>每周</option>
            <option value="monthly" ${settings.backupFrequency === 'monthly' ? 'selected' : ''}>每月</option>
          </select>
        </div>

        <div class="settings-item">
          <button class="ui-button button-primary" id="export-data-btn">
            📥 导出数据
          </button>
        </div>

        <div class="settings-item">
          <button class="ui-button button-primary" id="import-data-btn">
            📤 导入数据
          </button>
        </div>

        <div class="settings-item">
          <button class="ui-button button-danger" id="clear-data-btn">
            🗑️ 清空所有数据
          </button>
        </div>
      </div>

      <div class="settings-section">
        <h3 class="section-title">其他设置</h3>
        <div class="settings-item">
          <label>
            <input type="checkbox" id="show-completed-tasks" 
                   ${settings.showCompletedTasks ? 'checked' : ''}>
            显示已完成的任务
          </label>
        </div>

        <div class="settings-item">
          <button class="ui-button button-secondary" id="help-btn">
            ❓ 帮助文档
          </button>
        </div>

        <div class="settings-item">
          <button class="ui-button button-secondary" id="about-btn">
            ℹ️ 关于应用
          </button>
        </div>
      </div>

      <div class="settings-actions">
        <button class="ui-button button-primary" id="save-settings-btn">
          💾 保存设置
        </button>
        <button class="ui-button button-secondary" id="reset-settings-btn">
          🔄 恢复默认
        </button>
      </div>
    `;

    return container;
  }

  static async init() {
    const settings = await SettingsManager.loadSettings();
    
    // 主题按钮事件
    document.querySelectorAll('.theme-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
        e.target.classList.add('active');
      });
    });

    // 字体大小按钮事件
    document.querySelectorAll('.size-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        document.querySelectorAll('.size-btn').forEach(b => b.classList.remove('active'));
        e.target.classList.add('active');
      });
    });

    // 自动备份复选框
    document.getElementById('auto-backup').addEventListener('change', (e) => {
      document.getElementById('backup-frequency').disabled = !e.target.checked;
    });

    // 保存设置
    document.getElementById('save-settings-btn').addEventListener('click', async () => {
      await this.saveSettings(settings);
    });

    // 恢复默认
    document.getElementById('reset-settings-btn').addEventListener('click', async () => {
      if (confirm('确定要恢复默认设置吗?')) {
        localStorage.removeItem('appSettings');
        location.reload();
      }
    });

    // 导出数据
    document.getElementById('export-data-btn').addEventListener('click', async () => {
      await this.exportData();
    });

    // 导入数据
    document.getElementById('import-data-btn').addEventListener('click', () => {
      this.importData();
    });

    // 清空数据
    document.getElementById('clear-data-btn').addEventListener('click', async () => {
      if (confirm('确定要清空所有数据吗?此操作不可撤销!')) {
        await this.clearAllData();
      }
    });

    // 帮助和关于
    document.getElementById('help-btn').addEventListener('click', () => {
      this.showHelp();
    });

    document.getElementById('about-btn').addEventListener('click', () => {
      this.showAbout();
    });
  }

  static async saveSettings(settings) {
    try {
      const newSettings = {
        language: document.getElementById('language-select').value,
        timeFormat: document.getElementById('timeformat-select').value,
        weekStart: document.getElementById('weekstart-select').value,
        theme: document.querySelector('.theme-btn.active').dataset.theme,
        primaryColor: document.getElementById('primary-color').value,
        fontSize: document.querySelector('.size-btn.active').dataset.size,
        enableNotifications: document.getElementById('enable-notifications').checked,
        notificationSound: document.getElementById('notification-sound').checked,
        reminderTime: parseInt(document.getElementById('reminder-time').value),
        autoBackup: document.getElementById('auto-backup').checked,
        backupFrequency: document.getElementById('backup-frequency').value,
        compactMode: document.getElementById('compact-mode').checked,
        showCompletedTasks: document.getElementById('show-completed-tasks').checked
      };

      await SettingsManager.saveSettings(newSettings);
      SettingsManager.applyTheme(newSettings.theme);
      SettingsManager.applyFontSize(newSettings.fontSize);
      SettingsManager.applyPrimaryColor(newSettings.primaryColor);

      UIComponents.showToast('✅ 设置已保存');
    } catch (error) {
      UIComponents.showToast('❌ 保存失败: ' + error.message);
    }
  }

  static async exportData() {
    try {
      const allTasks = await db.getAllTasks();
      const allCategories = await db.getAllCategories();
      const settings = await SettingsManager.loadSettings();

      const exportData = {
        version: '1.0',
        exportTime: new Date().toISOString(),
        tasks: allTasks,
        categories: allCategories,
        settings: settings
      };

      const dataStr = JSON.stringify(exportData, null, 2);
      const blob = new Blob([dataStr], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = `tasks-backup-${Date.now()}.json`;
      link.click();
      URL.revokeObjectURL(url);

      UIComponents.showToast('✅ 数据导出成功');
    } catch (error) {
      UIComponents.showToast('❌ 导出失败: ' + error.message);
    }
  }

  static importData() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';
    input.addEventListener('change', async (e) => {
      try {
        const file = e.target.files[0];
        const text = await file.text();
        const importData = JSON.parse(text);

        if (confirm('确定要导入数据吗?将覆盖现有数据。')) {
          // 清空现有数据
          await db.clearAllTasks();
          await db.clearAllCategories();

          // 导入新数据
          for (const task of importData.tasks) {
            await db.addTask(task);
          }
          for (const category of importData.categories) {
            await db.addCategory(category);
          }

          UIComponents.showToast('✅ 数据导入成功');
          location.reload();
        }
      } catch (error) {
        UIComponents.showToast('❌ 导入失败: ' + error.message);
      }
    });
    input.click();
  }

  static async clearAllData() {
    try {
      await db.clearAllTasks();
      await db.clearAllCategories();
      localStorage.clear();
      UIComponents.showToast('✅ 数据已清空');
      location.reload();
    } catch (error) {
      UIComponents.showToast('❌ 清空失败: ' + error.message);
    }
  }

  static showHelp() {
    const helpContent = document.createElement('div');
    helpContent.innerHTML = `
      <div class="help-content">
        <h4>常见问题</h4>
        <p><strong>Q: 如何备份我的数据?</strong></p>
        <p>A: 进入设置页面,点击"导出数据"按钮,系统会下载一个 JSON 文件。</p>
        
        <p><strong>Q: 如何恢复备份的数据?</strong></p>
        <p>A: 进入设置页面,点击"导入数据"按钮,选择之前导出的 JSON 文件。</p>
        
        <p><strong>Q: 如何更改主题?</strong></p>
        <p>A: 在设置页面的"主题与外观"部分选择浅色、深色或自动主题。</p>
        
        <p><strong>Q: 数据会被上传到云端吗?</strong></p>
        <p>A: 不会。所有数据都存储在您的本地设备上,我们不收集任何个人数据。</p>
      </div>
    `;

    const dialog = UIComponents.createDialog('帮助文档', helpContent, [
      UIComponents.createButton('关闭', () => dialog.hide())
    ]);
    dialog.show();
  }

  static showAbout() {
    const aboutContent = document.createElement('div');
    aboutContent.innerHTML = `
      <div class="about-content">
        <h3>待办事项应用</h3>
        <p><strong>版本:</strong> 1.0.0</p>
        <p><strong>开发者:</strong> HarmonyOS 开发者社区</p>
        <p><strong>技术栈:</strong> HarmonyOS + Cordova + Web</p>
        <p style="margin-top: 20px; font-size: 12px; color: #999;">
          © 2024 开源鸿蒙跨平台开发者社区。保留所有权利。
        </p>
      </div>
    `;

    const dialog = UIComponents.createDialog('关于应用', aboutContent, [
      UIComponents.createButton('关闭', () => dialog.hide())
    ]);
    dialog.show();
  }
}

代码解释:

  • render() 方法生成完整的设置页面 HTML,包括基础设置、主题、通知、数据管理等多个分区;
  • init() 方法绑定所有交互事件,包括主题切换、字体大小调整、数据导入导出等;
  • saveSettings() 收集表单数据,验证后保存到 LocalStorage,并实时应用到 UI;
  • exportData() 导出所有任务、分类和设置为 JSON 文件,用户可以下载备份;
  • importData() 允许用户选择 JSON 文件进行导入,覆盖现有数据;
  • clearAllData() 清空所有本地数据,需要用户二次确认以防误操作。

🌉 原生层的设置支持

在鸿蒙原生侧,可以通过以下方式增强设置功能:

// SettingsPlugin.ets - 原生设置插件
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct SettingsPlugin {
  // 获取系统主题偏好
  static getSystemTheme(): string {
    // 调用鸿蒙系统 API 获取当前主题
    // 返回 'light' 或 'dark'
    return 'light';
  }

  // 获取系统语言
  static getSystemLanguage(): string {
    // 调用鸿蒙系统 API 获取系统语言
    // 返回语言代码,如 'zh-CN'
    return 'zh-CN';
  }

  // 注册原生设置插件
  static registerPlugin(controller: webview.WebviewController) {
    const settingsPlugin = {
      getSystemTheme: () => this.getSystemTheme(),
      getSystemLanguage: () => this.getSystemLanguage(),
      requestNotificationPermission: () => this.requestNotificationPermission(),
      saveAppSettings: (settings: string) => this.saveAppSettings(settings)
    };

    controller.registerJavaScriptProxy(settingsPlugin, 'settingsNative', ['getSystemTheme', 'getSystemLanguage']);
  }

  // 请求通知权限
  static async requestNotificationPermission(): Promise<boolean> {
    // 调用鸿蒙权限 API 请求通知权限
    return true;
  }

  // 保存应用设置到原生层
  static saveAppSettings(settings: string): void {
    // 可以在原生层持久化某些关键设置
    // 例如保存到应用私有目录
  }
}

代码解释:

  • getSystemTheme() 获取系统当前的主题偏好,应用可以自动适配系统主题;
  • getSystemLanguage() 获取系统语言设置,用于应用的默认语言选择;
  • registerPlugin() 将原生功能暴露给 Web 层,通过 JavaScript Proxy 调用;
  • requestNotificationPermission() 请求系统通知权限,必要时弹出权限对话框。

🔌 Web-Native 设置同步机制

// 应用启动时同步系统设置
async function initializeAppSettings() {
  try {
    // 从原生层获取系统主题和语言
    const systemTheme = await window.settingsNative.getSystemTheme();
    const systemLanguage = await window.settingsNative.getSystemLanguage();

    // 加载用户保存的设置
    let settings = await SettingsManager.loadSettings();

    // 如果用户未设置主题,使用系统主题
    if (!localStorage.getItem('appSettings')) {
      settings.theme = systemTheme;
      settings.language = systemLanguage;
      await SettingsManager.saveSettings(settings);
    }

    // 应用设置到 UI
    SettingsManager.applyTheme(settings.theme);
    SettingsManager.applyFontSize(settings.fontSize);
    SettingsManager.applyPrimaryColor(settings.primaryColor);

    // 请求通知权限
    if (settings.enableNotifications) {
      await window.settingsNative.requestNotificationPermission();
    }

    console.log('✅ 应用设置初始化完成');
  } catch (error) {
    console.error('❌ 初始化设置失败:', error);
  }
}

// 在应用启动时调用
document.addEventListener('deviceready', initializeAppSettings);

代码解释:

  • 应用启动时从原生层获取系统主题和语言;
  • 如果用户首次使用应用,自动采用系统设置作为初始值;
  • 后续用户修改的设置会覆盖系统设置,实现个性化定制;
  • 在获得用户同意的情况下请求通知权限。

📝 总结

应用设置与个性化配置是提升用户体验的关键。通过实现完整的设置系统,我们可以:

  • 让用户根据自己的习惯定制应用外观和行为;
  • 提供主题、语言、字体等多维度的个性化选项;
  • 实现数据的安全备份和恢复机制;
  • 与鸿蒙系统深度集成,自动适配系统主题和语言;
  • 为用户提供清晰的帮助文档和应用信息。

一个设计良好的设置系统不仅能提升应用的专业度,更能让用户感受到被尊重和被理解。在架构设计阶段就考虑好设置的持久化、验证和应用机制,可以为后续功能扩展奠定坚实基础。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐