应用设置与个性化配置 Cordova 与 OpenHarmony 混合开发实战
摘要: 本文介绍了待办事项应用中设置模块的核心功能实现,包括用户偏好配置、主题切换、通知管理等个性化选项。通过SettingsManager类处理设置项的加载、验证与持久化存储,采用CSS变量实现UI设置的即时生效。设置页面提供语言选择、时间格式、主题颜色等可定制化选项,所有配置通过LocalStorage保存并在应用启动时恢复。系统采用模块化设计,确保设置值的有效性和一致性,为用户提供灵活的应用
·
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应模块:待办事项应用中的应用设置与个性化配置功能,重点关注用户偏好设置、主题切换、通知配置、数据管理等核心设置功能的实现。
📌 概述
一个优秀的应用不仅要提供强大的功能,更要尊重用户的个性化需求。待办事项应用的设置页面是用户定制应用行为、外观和数据管理的中心枢纽。我们需要实现以下核心功能:
- 用户偏好设置:语言选择、时间格式、周开始日期等;
- 主题与外观:深色/浅色主题、自定义颜色、字体大小调整;
- 通知与提醒:提醒时间、声音开关、桌面通知权限;
- 数据管理:数据备份、导入导出、清空数据等高危操作;
- 应用信息:版本号、更新检查、关于应用、帮助文档。
这些设置需要持久化到本地存储(LocalStorage/IndexedDB),并在应用启动时恢复用户的个性化配置。
🔗 设置系统架构流程
这个流程展示了从用户进入设置页面到配置被应用的完整链路:先加载已保存的设置,用户修改后进行验证,通过后保存到本地存储并实时应用到应用界面。
🔧 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);
代码解释:
- 应用启动时从原生层获取系统主题和语言;
- 如果用户首次使用应用,自动采用系统设置作为初始值;
- 后续用户修改的设置会覆盖系统设置,实现个性化定制;
- 在获得用户同意的情况下请求通知权限。
📝 总结
应用设置与个性化配置是提升用户体验的关键。通过实现完整的设置系统,我们可以:
- 让用户根据自己的习惯定制应用外观和行为;
- 提供主题、语言、字体等多维度的个性化选项;
- 实现数据的安全备份和恢复机制;
- 与鸿蒙系统深度集成,自动适配系统主题和语言;
- 为用户提供清晰的帮助文档和应用信息。
一个设计良好的设置系统不仅能提升应用的专业度,更能让用户感受到被尊重和被理解。在架构设计阶段就考虑好设置的持久化、验证和应用机制,可以为后续功能扩展奠定坚实基础。
更多推荐

所有评论(0)