外观设置模块 Cordova 与 OpenHarmony 混合开发实战

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

📌 概述

外观设置模块主要负责控制喝茶记录应用在视觉层面的呈现方式,包括主题模式(明亮/深色)、主色调、字体大小以及卡片密度等。这个模块表面上只是"换皮肤",但在 Cordova 与 OpenHarmony 混合架构下,它同时要协调 Web CSS 变量、LocalStorage/IndexedDB 配置以及 ArkTS 原生侧的窗口样式、状态栏图标样式等多种能力。合理的设计可以让用户在 PC、平板等设备上获得一致且舒适的体验,同时又兼顾性能和可维护性。

在整体架构上,外观设置模块遵循"配置中心 + CSS 变量 + 原生桥接"的思路:所有外观相关的用户偏好被集中保存在 appearanceSettings 结构中,通过 IndexedDB 与原生 Preferences 同步;Web 侧通过设置 :root 上的一组 CSS 自定义属性来驱动主题变化;原生 ArkTS 通过插件读取关键信息,例如当前是否为暗色模式,从而调整 ArkUI 窗口背景、系统栏图标颜色等。这种划分使得外观逻辑既高度集中,又与业务逻辑解耦。

🔗 完整流程

第一步:加载外观偏好并应用到 CSS

当应用启动或用户打开"外观设置"页面时,前端首先会从 IndexedDB 的 settings 表中读取已保存的外观配置(如 themeModeprimaryColorfontScale 等)。如果没有记录,就使用默认值(例如跟随系统或固定浅色)。随后,应用会调用一个统一的 applyAppearance(settings) 方法,将这些配置转换为 CSS 变量写入到 document.documentElement.style 上。这一步会立即影响整站的配色和部分布局,例如卡片阴影、背景色和字体大小等,而不需要重新加载页面。

第二步:用户在外观设置页面中实时预览

用户进入"外观设置"页面后,可以通过切换主题模式(浅色/深色/跟随系统)、选择主色调(如茶色、绿色、蓝色)、调整字体大小(小/中/大)等操作立即看到效果。外观设置页面并不会立即写入数据库,而是先更新内存中的 appearanceSettings,并调用 applyAppearance() 做实时预览。当用户对当前效果满意并点击"保存外观"按钮时,才会把最终配置持久化到 IndexedDB 与 OpenHarmony Preferences 中。这种"先体验再保存"的交互方式避免了频繁磁盘写入,也让用户操作更有安全感。

第三步:原生侧同步与系统级外观协调

在用户保存外观设置时,除了 Web 层写入 settings,还会通过 cordova.exec 调用 ArkTS 侧的 AppearanceBridge.saveNativeAppearance()。原生插件会把 themeModeprimaryColor 等关键信息写入 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 后立即对当前窗口进行调整:themeModedark 时使用深色背景并把系统栏内容颜色设为白色,反之则使用浅色背景与深色图标。通过这种方式,Web 的 CSS 主题与 ArkUI 窗口的外观可以保持统一,避免出现"页面是深色、系统栏是亮色"的割裂体验。

📝 总结

外观设置模块将简单的"切换主题"上升为一套完整的跨层外观管理方案:

  • Web 层通过表单和 CSS 变量负责 UI 呈现和即时预览;
  • IndexedDB 与 Preferences 负责长期持久化;
  • ArkTS 原生插件负责窗口背景、系统栏样式等系统级外观;
  • Cordova 作为桥梁让两侧在一次操作中同时生效。

在这个实践中,你可以清楚地看到 Cordova 与 OpenHarmony 在 UI 层的协同方式:绝大多数组件、布局和动画仍由 Web 完成,而需要与系统风格一致的部分(比如状态栏、导航栏、窗口背景)则交由 ArkTS 来处理。这样的分层既保留了 Web 开发的高效率,又充分利用了 HarmonyOS 的原生能力,是混合应用在 UI 外观方面的一种典型最佳实践。

相关推荐
培之2 小时前
联想拯救者windows11装ubuntu双系统要点
linux·运维·ubuntu
TAEHENGV2 小时前
目标列表模块 Cordova 与 OpenHarmony 混合开发实战
服务器·数据库
Aniugel2 小时前
前端服务端渲染 SSR
服务器·javascript
南山nash2 小时前
Linux 系统如何释放内存
linux·运维·服务器
Vic101012 小时前
Spring AOP 高级陷阱:为什么 @Before 修改参数是“伪修改“?
java·python·spring
阿钱真强道2 小时前
02-COAP ubuntu 下 coap 使用及测试
运维·服务器
Violet_YSWY2 小时前
domain文件夹
java
最贪吃的虎2 小时前
JVM扫盲:内存模型
java·运维·jvm·后端
weixin_439706252 小时前
如何使用JAVA进行MCP服务创建以及通过大模型进行调用
java·开发语言