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

📌 概述
外观设置模块主要负责控制喝茶记录应用在视觉层面的呈现方式,包括主题模式(明亮/深色)、主色调、字体大小以及卡片密度等。这个模块表面上只是"换皮肤",但在 Cordova 与 OpenHarmony 混合架构下,它同时要协调 Web CSS 变量、LocalStorage/IndexedDB 配置以及 ArkTS 原生侧的窗口样式、状态栏图标样式等多种能力。合理的设计可以让用户在 PC、平板等设备上获得一致且舒适的体验,同时又兼顾性能和可维护性。
在整体架构上,外观设置模块遵循"配置中心 + CSS 变量 + 原生桥接"的思路:所有外观相关的用户偏好被集中保存在 appearanceSettings 结构中,通过 IndexedDB 与原生 Preferences 同步;Web 侧通过设置 :root 上的一组 CSS 自定义属性来驱动主题变化;原生 ArkTS 通过插件读取关键信息,例如当前是否为暗色模式,从而调整 ArkUI 窗口背景、系统栏图标颜色等。这种划分使得外观逻辑既高度集中,又与业务逻辑解耦。
🔗 完整流程
第一步:加载外观偏好并应用到 CSS
当应用启动或用户打开"外观设置"页面时,前端首先会从 IndexedDB 的 settings 表中读取已保存的外观配置(如 themeMode、primaryColor、fontScale 等)。如果没有记录,就使用默认值(例如跟随系统或固定浅色)。随后,应用会调用一个统一的 applyAppearance(settings) 方法,将这些配置转换为 CSS 变量写入到 document.documentElement.style 上。这一步会立即影响整站的配色和部分布局,例如卡片阴影、背景色和字体大小等,而不需要重新加载页面。
第二步:用户在外观设置页面中实时预览
用户进入"外观设置"页面后,可以通过切换主题模式(浅色/深色/跟随系统)、选择主色调(如茶色、绿色、蓝色)、调整字体大小(小/中/大)等操作立即看到效果。外观设置页面并不会立即写入数据库,而是先更新内存中的 appearanceSettings,并调用 applyAppearance() 做实时预览。当用户对当前效果满意并点击"保存外观"按钮时,才会把最终配置持久化到 IndexedDB 与 OpenHarmony Preferences 中。这种"先体验再保存"的交互方式避免了频繁磁盘写入,也让用户操作更有安全感。
第三步:原生侧同步与系统级外观协调
在用户保存外观设置时,除了 Web 层写入 settings,还会通过 cordova.exec 调用 ArkTS 侧的 AppearanceBridge.saveNativeAppearance()。原生插件会把 themeMode、primaryColor 等关键信息写入 Preferences,并在下次窗口创建或主题切换时应用:例如暗色模式下使用深色背景并将状态栏图标改为浅色。对于"跟随系统"模式,ArkTS 还会监听系统主题变化事件,在系统切换深浅色时主动回调 Web 端(可选实现),触发一次 applyAppearance({ themeMode: 'system' }),从而保持两端视觉一致。
🔧 Web 代码实现
外观设置页面 HTML 结构
html
<div id="appearance-settings-page" class="page">
<div class="page-header">
<h1>外观设置</h1>
</div>
<form id="appearance-form" class="form">
<div class="form-group">
<label for="theme-mode">主题模式</label>
<select id="theme-mode" name="themeMode">
<option value="light">浅色模式</option>
<option value="dark">深色模式</option>
<option value="system">跟随系统</option>
</select>
</div>
<div class="form-group">
<label for="primary-color">主色调</label>
<select id="primary-color" name="primaryColor">
<option value="#409EFF">经典蓝</option>
<option value="#67c23a">茶叶绿</option>
<option value="#e6a23c">暖茶橙</option>
<option value="#f56c6c">红茶红</option>
</select>
</div>
<div class="form-group">
<label for="font-scale">字体大小</label>
<select id="font-scale" name="fontScale">
<option value="0.9">小号</option>
<option value="1">标准</option>
<option value="1.1">偏大</option>
<option value="1.2">超大</option>
</select>
</div>
<div class="form-group">
<label for="card-density">卡片密度</label>
<select id="card-density" name="cardDensity">
<option value="comfortable">宽松</option>
<option value="cozy">适中</option>
<option value="compact">紧凑</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="resetAppearance()">恢复默认</button>
<button type="submit" class="btn btn-primary">保存外观</button>
</div>
</form>
</div>
这部分 HTML 定义了外观设置的表单结构,包含主题模式、主色调、字体缩放以及卡片密度四大类配置。采用下拉框而不是开关,可以方便后续扩展更多选项(例如新增主题色、字体比例)。表单提交会由 JavaScript 统一处理,而"恢复默认"按钮用于一键回到设计推荐配置。
外观设置逻辑与 CSS 变量应用
javascript
let appearanceSettings = {
themeMode: 'light',
primaryColor: '#409EFF',
fontScale: 1,
cardDensity: 'cozy'
};
async function initAppearanceSettingsPage() {
try {
const saved = await db.getAppearanceSettings();
if (saved) {
appearanceSettings = { ...appearanceSettings, ...saved };
}
fillAppearanceForm();
applyAppearance(appearanceSettings);
document
.getElementById('appearance-form')
.addEventListener('submit', handleAppearanceSubmit);
} catch (error) {
console.error('Failed to init appearance page:', error);
showToast('加载外观设置失败', 'error');
fillAppearanceForm();
}
}
function fillAppearanceForm() {
document.getElementById('theme-mode').value = appearanceSettings.themeMode;
document.getElementById('primary-color').value = appearanceSettings.primaryColor;
document.getElementById('font-scale').value = String(appearanceSettings.fontScale);
document.getElementById('card-density').value = appearanceSettings.cardDensity;
}
function applyAppearance(settings) {
const root = document.documentElement;
root.style.setProperty('--primary-color', settings.primaryColor);
root.style.setProperty('--font-scale', String(settings.fontScale));
if (settings.themeMode === 'dark') {
root.classList.add('theme-dark');
root.classList.remove('theme-light');
} else {
root.classList.add('theme-light');
root.classList.remove('theme-dark');
}
root.classList.remove('density-comfortable', 'density-cozy', 'density-compact');
root.classList.add(`density-${settings.cardDensity}`);
}
async function handleAppearanceSubmit(event) {
event.preventDefault();
const formData = new FormData(document.getElementById('appearance-form'));
const newSettings = {
themeMode: formData.get('themeMode') || 'light',
primaryColor: formData.get('primaryColor') || '#409EFF',
fontScale: parseFloat(formData.get('fontScale') || '1'),
cardDensity: formData.get('cardDensity') || 'cozy'
};
appearanceSettings = newSettings;
applyAppearance(newSettings);
try {
await db.saveAppearanceSettings(newSettings);
if (window.cordova) {
cordova.exec(
() => console.log('Native appearance saved'),
err => console.error('Save native appearance error:', err),
'AppearanceBridge',
'saveNativeAppearance',
[newSettings]
);
}
showToast('外观设置已保存', 'success');
} catch (error) {
console.error('Failed to save appearance:', error);
showToast('保存失败,请重试', 'error');
}
}
function resetAppearance() {
appearanceSettings = {
themeMode: 'light',
primaryColor: '#409EFF',
fontScale: 1,
cardDensity: 'cozy'
};
fillAppearanceForm();
applyAppearance(appearanceSettings);
showToast('已恢复默认外观(记得点击保存)', 'info');
}
这段 JavaScript 展示了外观设置从加载到应用再到保存的完整流程。applyAppearance() 将配置映射到 CSS 自定义属性与根元素的 class 上,CSS 文件中只需根据 --primary-color、--font-scale 和 .theme-dark/.density-compact 等选择器来调整颜色和间距即可。通过这种方式,不需要在多处手动切换 class,只要维护好一套 CSS 变量即可。
🔌 OpenHarmony 原生代码(ArkTS)
AppearanceBridge:桥接主题和系统栏样式
typescript
// entry/src/main/ets/plugins/AppearanceBridge.ets
import preferences from '@ohos.data.preferences';
import window from '@ohos.window';
const PREF_NAME = 'tea_app_appearance';
export class AppearanceBridge {
static async saveNativeAppearance(settings: Record<string, unknown>): Promise<void> {
const pref = await preferences.getPreferences(globalThis.context, PREF_NAME);
if (settings.themeMode !== undefined) {
await pref.put('themeMode', settings.themeMode as string);
}
if (settings.primaryColor !== undefined) {
await pref.put('primaryColor', settings.primaryColor as string);
}
await pref.flush();
await this.applyToWindow(settings);
}
static async applyToWindow(settings: Record<string, unknown>): Promise<void> {
const mode = (settings.themeMode as string) ?? 'light';
const win = await window.getLastWindow(globalThis.context);
if (mode === 'dark') {
await win.setWindowBackgroundColor('#121212');
await win.setSystemBarProperties({
statusBarContentColor: '#FFFFFF',
navigationBarContentColor: '#FFFFFF'
});
} else {
await win.setWindowBackgroundColor('#F5F7FA');
await win.setSystemBarProperties({
statusBarContentColor: '#000000',
navigationBarContentColor: '#000000'
});
}
}
}
AppearanceBridge 插件在原生侧接收来自 Web 的外观配置,并写入 Preferences 后立即对当前窗口进行调整:themeMode 为 dark 时使用深色背景并把系统栏内容颜色设为白色,反之则使用浅色背景与深色图标。通过这种方式,Web 的 CSS 主题与 ArkUI 窗口的外观可以保持统一,避免出现"页面是深色、系统栏是亮色"的割裂体验。
📝 总结
外观设置模块将简单的"切换主题"上升为一套完整的跨层外观管理方案:
- Web 层通过表单和 CSS 变量负责 UI 呈现和即时预览;
- IndexedDB 与 Preferences 负责长期持久化;
- ArkTS 原生插件负责窗口背景、系统栏样式等系统级外观;
- Cordova 作为桥梁让两侧在一次操作中同时生效。
在这个实践中,你可以清楚地看到 Cordova 与 OpenHarmony 在 UI 层的协同方式:绝大多数组件、布局和动画仍由 Web 完成,而需要与系统风格一致的部分(比如状态栏、导航栏、窗口背景)则交由 ArkTS 来处理。这样的分层既保留了 Web 开发的高效率,又充分利用了 HarmonyOS 的原生能力,是混合应用在 UI 外观方面的一种典型最佳实践。