HarmonyOS NEXT AI 智能生活助手:设置中心开发

HarmonyOS NEXT AI 智能生活助手:设置中心开发

图1:设置中心页面布局

前言

第 23 篇中,我们实现了主题切换。本文将实现完整的 设置中心,让用户可以统一管理 AI 配置、主题、数据等。

设置中心 是用户自定义 APP 行为的核心入口。良好的设置设计应分类清晰、操作直观、修改即时生效。


一、设置页面架构

设置分组 包含项 依赖模块
AI 设置 模型切换、API Key、参数配置 ProviderFactory
主题设置 浅色/深色/自动、字体大小 ThemeManager
数据管理 清除缓存、导出数据、清除所有 CacheManager
关于 版本号、开源许可、反馈 ---

二、SettingPage 实现

2.1 核心代码

typescript 复制代码
// pages/SettingPage.ets
@Entry
@Component
struct SettingPage {
  @State cacheSize: string = '计算中...';
  @State appVersion: string = 'v1.0.0';

  aboutToAppear() {
    this.calcCacheSize();
  }

  async calcCacheSize() {
    const stats = CacheManager.getInstance().getStats();
    this.cacheSize = `${stats.memorySize}/${stats.memoryMax} 条`;
  }

  build() {
    Column() {
      // 导航栏
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).onClick(() => RouterUtil.back());
        Text('⚙️ 设置').fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 12 });
      }
      .width('100%').height(56).padding({ left: 16, right: 16 });

      Scroll() {
        Column() {
          // AI 设置组
          this.sectionHeader('AI 设置');
          this.settingItem('🤖 模型切换', 'OpenAI GPT-4o-mini', () => {
            RouterUtil.navigateTo('pages/ModelSelectorPage');
          });
          this.settingItem('🔑 API Key', '已配置', () => {
            RouterUtil.navigateTo('pages/ModelConfigPage');
          });
          this.settingItem('⚡ 模型参数', '温度/最大Token', () => {
            RouterUtil.navigateTo('pages/ModelConfigPage');
          });

          // 主题设置组
          this.sectionHeader('主题设置');
          this.settingItem('🎨 主题模式', '浅色', () => {
            RouterUtil.navigateTo('pages/ThemePage');
          });
          this.settingItem('📏 字体大小', '标准', () => {
            // 字体大小选择
          });

          // 数据管理组
          this.sectionHeader('数据管理');
          this.settingItem('🗑️ 清除缓存', this.cacheSize, () => {
            CacheManager.getInstance().clear();
            this.cacheSize = '0 条';
            ToastUtil.show('缓存已清除');
          });
          this.settingItem('📤 导出数据', '', () => {
            this.exportData();
          });
          this.settingItem('⚠️ 清除所有数据', '', () => {
            this.confirmClearAll();
          });

          // 关于组
          this.sectionHeader('关于');
          this.settingItem('📌 版本号', this.appVersion, () => {});
          this.settingItem('📄 开源许可', 'Apache 2.0', () => {});
          this.settingItem('💬 反馈建议', '', () => {
            // 打开反馈页面
          });
        }
        .padding(16);
      }
      .layoutWeight(1);
    }
    .width('100%').height('100%').backgroundColor('#F5F6FA');
  }

  @Builder
  sectionHeader(title: string) {
    Text(title).fontSize(13).fontColor('#6C5CE7').fontWeight(FontWeight.Bold)
      .margin({ top: 20, bottom: 8 });
  }

  @Builder
  settingItem(label: string, value: string, onClick: () => void) {
    Row() {
      Text(label).fontSize(15);
      Blank();
      Text(value).fontSize(13).fontColor(Color.Gray).margin({ right: 4 });
      Image($r('app.media.ic_chevron_right')).width(16).height(16);
    }
    .width('100%').padding(16).backgroundColor(Color.White).borderRadius(12)
    .margin({ bottom: 6 }).onClick(() => onClick());
  }

  async exportData() {
    try {
      const data = await DataExporter.exportToJson();
      const clipboard = getContext(this).clipboard;
      await clipboard.set({ primary: data });
      ToastUtil.show('数据已复制到剪贴板');
    } catch {
      ToastUtil.show('导出失败');
    }
  }

  confirmClearAll() {
    AlertDialog.show({
      title: '确认清除',
      message: '此操作将清除所有对话记录、设置和缓存数据,不可恢复!',
      primaryButton: { value: '取消', action: () => {} },
      secondaryButton: {
        value: '确认清除',
        fontColor: '#E17055',
        action: () => {
          this.clearAllData();
        }
      }
    });
  }

  async clearAllData() {
    await DatabaseManager.getInstance().getStore()?.executeSql('DELETE FROM conversation');
    await DatabaseManager.getInstance().getStore()?.executeSql('DELETE FROM chat_message');
    CacheManager.getInstance().clear();
    ToastUtil.show('所有数据已清除');
  }
}

三、功能详解

3.1 数据导出格式

json 复制代码
{
  "version": "1.0",
  "exportTime": "2025-01-15T10:30:00.000Z",
  "conversations": [
    {
      "id": "conv_001",
      "title": "AI 开发讨论",
      "messages": [
        { "role": "user", "content": "如何实现流式输出?" },
        { "role": "assistant", "content": "使用 AsyncGenerator..." }
      ]
    }
  ],
  "settings": {
    "provider": "OpenAI",
    "model": "gpt-4o-mini",
    "theme": "light"
  }
}
设置项 值类型 存储方式
API Key 加密字符串 Preferences
主题模式 string Preferences
模型参数 number\[\] Preferences
聊天记录 JSON PersistenceV2
缓存数据 Map 内存 + Preferences

四、设置项详解

4.1 AI 设置

typescript 复制代码
export class AISettingsManager {
  private preferences: Preferences | null = null;

  async init(context: Context): Promise<void> {
    this.preferences = await getPreferences(context, 'ai_settings');
  }

  // 获取当前模型
  async getCurrentModel(): Promise<string> {
    return this.preferences?.get('model', 'gpt-4o-mini') as string || 'gpt-4o-mini';
  }

  // 设置 API Key
  async setApiKey(key: string): Promise<void> {
    if (!this.preferences) return;
    const encrypted = SimpleCipher.encrypt(key);
    await this.preferences.put('api_key', encrypted);
    await this.preferences.flush();
  }

  // 获取 API Key(解密)
  async getApiKey(): Promise<string> {
    if (!this.preferences) return '';
    const encrypted = await this.preferences.get('api_key', '') as string;
    return encrypted ? SimpleCipher.decrypt(encrypted) : '';
  }

  // 清除 API Key
  async clearApiKey(): Promise<void> {
    if (!this.preferences) return;
    await this.preferences.delete('api_key');
    await this.preferences.flush();
  }
}
设置项 类型 默认值 说明
模型选择 string gpt-4o-mini AI 对话模型
API Key 加密 string 第三方 API 密钥
Temperature number 0.7 生成随机性
Max Tokens number 4096 最大生成长度
系统 Prompt string 默认 自定义系统指令

4.2 数据管理

typescript 复制代码
export class DataManager {
  private databaseManager = DatabaseManager.getInstance();
  private cacheManager = CacheManager.getInstance();

  // 导出所有数据为 JSON
  async exportAllData(): Promise<string> {
    const store = this.databaseManager.getStore();
    const predicates = new relationalStore.RdbPredicates('conversation');
    const resultSet = await store.query(predicates, ['*']);

    const conversations: any[] = [];
    while (resultSet.goToNextRow()) {
      conversations.push({
        id: resultSet.getString(0),
        title: resultSet.getString(1),
        messageCount: resultSet.getLong(9)
      });
    }
    resultSet.close();

    return JSON.stringify({
      version: '1.0',
      exportTime: new Date().toISOString(),
      conversations,
      cacheStats: this.cacheManager.getStats()
    }, null, 2);
  }

  // 清除所有数据
  async clearAll(): Promise<void> {
    const store = this.databaseManager.getStore();
    await store.executeSql('DELETE FROM chat_message');
    await store.executeSql('DELETE FROM conversation');
    this.cacheManager.clear();
  }

  // 获取存储用量
  async getStorageUsage(): Promise<StorageInfo> {
    const store = this.databaseManager.getStore();
    const convCount = await store.query(
      new relationalStore.RdbPredicates('conversation'), ['count(*) as cnt']
    );
    const msgCount = await store.query(
      new relationalStore.RdbPredicates('chat_message'), ['count(*) as cnt']
    );
    let convs = 0, msgs = 0;
    if (convCount.goToFirstRow()) convs = convCount.getLong(0);
    if (msgCount.goToFirstRow()) msgs = msgCount.getLong(0);
    convCount.close();
    msgCount.close();

    return {
      conversationCount: convs,
      messageCount: msgs,
      cacheSize: this.cacheManager.getStats().memorySize,
      lastClearTime: Date.now()
    };
  }
}

interface StorageInfo {
  conversationCount: number;
  messageCount: number;
  cacheSize: number;
  lastClearTime: number;
}

4.3 设置页面路由

typescript 复制代码
// 设置页面的路由配置
export const SETTING_ROUTES = {
  model: 'pages/ModelConfigPage',
  theme: 'pages/ThemePage',
  apiKey: 'pages/ModelConfigPage',
  about: 'pages/AboutPage',
  feedback: 'pages/FeedbackPage'
} as const;

// 设置项与路由映射
export function getSettingRoute(key: string): string {
  return SETTING_ROUTES[key as keyof typeof SETTING_ROUTES] || '';
}

五、安全区适配

设置页面作为全屏页面,同样需要处理顶部状态栏和底部导航栏的安全区。

5.1 获取安全区高度

typescript 复制代码
// 在 EntryAbility.onWindowStageCreate 中统一计算
import { display } from '@kit.ArkUI';

const displayInfo = display.getDefaultDisplaySync();
const densityPixels = displayInfo.densityPixels;
const statusBarHeightPx = avoidArea.topRect.height;
const navBarHeightPx = avoidArea.bottomRect.height;

// px 转 vp
const statusBarHeight = densityPixels > 0 ? statusBarHeightPx / densityPixels : statusBarHeightPx;
const navBarHeight = densityPixels > 0 ? navBarHeightPx / densityPixels : navBarHeightPx;

AppStorage.setOrCreate('statusBarHeight', statusBarHeight);
AppStorage.setOrCreate('navBarHeight', navBarHeight);

5.2 SettingPage 安全区适配

typescript 复制代码
@Entry
@Component
struct SettingPage {
  @StorageLink('statusBarHeight') statusBarHeight: number = 0;
  @StorageLink('navBarHeight') navBarHeight: number = 0;

  build() {
    Column() {
      // 顶部安全区占位
      Row().width('100%').height(this.statusBarHeight);

      // 导航栏
      Row() {
        Image($r('app.media.ic_back')).width(24).height(24).onClick(() => RouterUtil.back());
        Text('设置').fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 12 });
      }
      .width('100%').height(56).padding({ left: 16, right: 16 });

      Scroll() {
        Column() {
          // 设置内容...
        }
        .padding(16);
      }
      .layoutWeight(1);

      // 底部安全区占位
      Row().width('100%').height(this.navBarHeight);
    }
    .width('100%').height('100%').backgroundColor('#F5F6FA');
  }
}
安全区位置 获取方式 单位转换 用途
顶部 topRect.height px / density = vp 状态栏占位
底部 bottomRect.height px / density = vp 导航栏占位

注意getWindowAvoidArea() 返回的高度是 px (物理像素),而 ArkUI 的 padding/height 使用 vp (虚拟像素)。必须通过 display.getDefaultDisplaySync().densityPixels 获取屏幕密度后进行除法转换,否则 padding 会被放大 2~3 倍,导致顶部空白过大。


六、图标资源规范

根据项目实际开发经验,HarmonyOS NEXT 设备上 emoji 会渲染为蓝色或紫色方块。因此所有图标必须使用 SVG 矢量图。

6.1 SVG 图标替换示例

原 emoji 替换方式 SVG 资源名
设置齿轮 纯文字 "设置" ---
机器人 纯文字 "模型切换" ---
钥匙 纯文字 "API Key" ---
闪电 纯文字 "模型参数" ---
调色盘 纯文字 "主题模式" ---
尺子 纯文字 "字体大小" ---
垃圾桶 纯文字 "清除缓存" ---
导出 纯文字 "导出数据" ---
警告 纯文字 "清除所有数据" ---
图钉 纯文字 "版本号" ---
文档 纯文字 "开源许可" ---
对话 纯文字 "反馈建议" ---
typescript 复制代码
// 正确的设置项:使用纯文字,不依赖 emoji
this.settingItem('模型切换', 'OpenAI GPT-4o-mini', () => {
  RouterUtil.navigateTo('pages/ModelSelectorPage');
});

七、设置页面最佳实践

在实现设置中心时,以下实践经验可以帮助你避免常见问题:

  1. 配置即时保存:用户修改设置后应立即持久化,不要等用户手动点击保存
  2. 敏感信息加密:API Key 等敏感信息必须使用加密存储,禁止明文保存
  3. 危险操作二次确认:清除数据等不可逆操作必须弹出确认对话框
  4. 状态同步:设置变更后应通知所有订阅者,确保 UI 状态一致
  5. 安全区适配:全屏页面必须处理顶部状态栏和底部导航栏的避让
实践项 正确做法 错误做法
API Key 存储 SimpleCipher 加密后存 Preferences 明文存储在内存或文件
缓存清除 清除后刷新 UI 显示 仅清除数据不更新界面
数据导出 异步执行,显示 Loading 同步阻塞主线程
主题切换 通过 AppStorage 全局通知 仅修改当前页面颜色
安全区 px 转 vp 后设置 padding 直接使用 px 值

提示:设置页面的用户体验直接影响用户对 APP 专业度的判断。细节处理越到位,用户信任度越高。


八、Git 提交

bash 复制代码
git add .
git commit -m "feat(setting): 设置中心开发

- AI 设置(模型/API Key/参数)
- 主题设置(模式/字体)
- 数据管理(缓存/导出/清除)
- 存储用量统计
- API Key 加密存储
- 安全区适配
- 关于信息

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>"
git tag v0.2.3

总结

本文实现了 设置中心。核心要点:

  1. 四大设置组:AI / 主题 / 数据 / 关于,分类清晰
  2. 一键操作:清缓存、导数据、清除所有
  3. 安全确认:危险操作二次确认
  4. 即时生效:修改配置立即保存
  5. 安全区适配:顶部和底部正确处理,避免超出屏幕
  6. SVG 图标:所有界面元素使用矢量图,避免 emoji 渲染异常

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
崖边看雾1 小时前
机器学习——支持向量机
人工智能·机器学习·支持向量机
l0001091 小时前
康养地产适老化智能改造:安全防护体系构建路径与方案选型
人工智能·物联网·安全
LaughingZhu1 小时前
Product Hunt 每日热榜 | 2026-08-05
人工智能·深度学习·神经网络·搜索引擎·百度
梦想三三1 小时前
Python从零实现AI Agent电商客服(Qwen/Ollama+SQLite源码解析)
人工智能·python·sqlite
Canace1 小时前
Harness Engineering 到底在做什么
前端·人工智能·ai编程
zzm6281 小时前
KSD测试:线性时间的分布异同检验方法
人工智能·机器学习·论文笔记·nips·ksd·分布检验
笨鸟先飞,勤能补拙1 小时前
密码学深度指南 — SecOps 工程师实战手册
人工智能·vscode·python·安全·github·密码学·visual studio
美团技术团队1 小时前
Agent评测漫谈 —— 由浅入深讲解Agent评测
人工智能
前端 贾公子1 小时前
Tavily Search:一个专为 AI Agent 打造的专属搜索引擎 API
人工智能