鸿蒙PC实战:打造一款“原生级”分布式智能笔记应用


在这里插入图片描述


一、项目背景与需求分析

在Windows/macOS上,笔记应用通常功能强大但生态割裂;在移动端,笔记应用便捷但缺乏生产力。HarmonyOS PC的出现,让我们有机会打造一款**“生于云端,长在PC,活在多端”**的超级笔记应用。

1.1 核心功能定义

  • 多窗口并行编辑:利用PC大屏优势,支持同时打开多个笔记窗口,支持拖拽分屏。
  • AI智能语义搜索:集成端侧AI模型,支持自然语言搜索(如“上周关于鸿蒙开发的会议记录”),而非简单的关键词匹配。
  • 跨端剪贴板与拖拽:手机上复制图片/文字,PC端直接粘贴;手机相册图片直接拖拽进PC笔记。
  • 离线优先架构:数据本地加密存储(使用RDB + 向量索引),网络恢复后自动同步。

1.2 技术栈选型

  • 开发语言:ArkTS (API 12+)
  • UI框架:ArkUI (声明式)
  • 状态管理@Observed, @ObjectLink, AppStorage
  • 数据存储:RelationalStore (RDB), VectorKit (端侧向量检索)
  • 多窗口能力:WindowExtensionAbility
  • AI能力:NLU (自然语言理解) Kit, Embedding Model

二、架构设计:MVVM + 仓库模式

为了保证代码的可维护性和可测试性,我们采用经典的 MVVM (Model-View-ViewModel) 架构,并引入 Repository (仓库) 层来屏蔽数据源细节。

┌─────────────────────────────────────────┐
│              View Layer (UI)            │
│  (Pages, Components, Custom Views)      │
└──────────────────┬──────────────────────┘
                   │ (Observes)
┌──────────────────▼──────────────────────┐
│           ViewModel Layer               │
│  (State Management, Business Logic)     │
└──────────────────┬──────────────────────┘
                   │ (Calls)
┌──────────────────▼──────────────────────┐
│          Repository Layer               │
│  (Data Aggregation, Sync Logic)         │
└─────────┬──────────────────┬────────────┘
          │                  │
┌─────────▼───────┐  ┌───────▼────────────┐
│  Local Source   │  │   Remote Source    │
│ (RDB, VectorDB) │  │ (Cloud Sync Service)│
└─────────────────┘  └────────────────────┘

三、核心功能实战编码

3.1 基础环境搭建与多窗口入口

首先,我们需要在module.json5中配置多窗口能力。

// entry/src/main/module.json5
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "multiton", // 关键:设置为多实例,支持多窗口
        "backgroundModes": ["dataTransfer"],
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      },
      {
        "name": "WindowExtensionAbility", // 用于处理特定的窗口扩展逻辑
        "srcEntry": "./ets/windowextension/WindowExtensionAbility.ets",
        "type": "windowExtension"
      }
    ]
  }
}

启动新窗口代码 (ViewModel层):

// viewModel/NoteWindowManager.ets
import window from '@ohos.window';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';

export class NoteWindowManager {
  private context: Context;

  constructor(context: Context) {
    this.context = context;
  }

  async openNewNoteWindow(noteId?: string) {
    try {
      const wantInfo = {
        deviceId: '',
        bundleName: this.context.bundleName,
        abilityName: 'EntryAbility',
        parameters: {
          'noteId': noteId || 'new_note', // 传递参数区分是新开还是编辑
          'timestamp': Date.now()
        }
      };

      // 启动一个新的实例
      await this.context.startAbility(wantInfo);
      console.info('New note window opened successfully');
    } catch (error) {
      console.error('Failed to open new window', error);
    }
  }
}

3.2 高性能列表与自定义渲染 (PC端优化)

PC端笔记列表可能包含数千条数据。直接使用List可能会导致滚动卡顿。我们需要使用LazyForEach并进行组件复用优化。

// components/NoteListItem.ets
@Component
export struct NoteListItem {
  @ObjectLink note: NoteItem; // 使用ObjectLink实现细粒度更新
  private onDelete: (id: string) => void;

  build() {
    Row() {
      Column() {
        Text(this.note.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(this.note.preview)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(2)
          .margin({ top: 4 })
        
        Text(this.formatTime(this.note.updateTime))
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      // PC端特有的悬停删除按钮
      Button() {
        Image($r('app.media.icon_delete'))
          .width(20)
          .height(20)
      }
      .width(40)
      .height(40)
      .backgroundColor(Color.Transparent)
      .hoverEffect(true) // 开启悬停特效
      .onClick(() => this.onDelete(this.note.id))
      .visibility(this.note.isHovered ? Visibility.Visible : Visibility.Hidden) // 结合状态控制
    }
    .padding(16)
    .backgroundColor(this.note.isSelected ? '#E6F7FF' : Color.Transparent)
    .borderRadius(8)
    .onHover((event) => {
      // 更新悬停状态,触发局部刷新
      this.note.isHovered = event.hover; 
    })
    .onClick(() => {
      // 选中逻辑
      this.note.isSelected = true;
    })
  }

  private formatTime(timestamp: number): string {
    // 时间格式化逻辑...
    return new Date(timestamp).toLocaleDateString();
  }
}

// 主页面使用 LazyForEach
@Entry
@Component
struct NoteListPage {
  @State notes: NoteItem[] = [];

  build() {
    Column() {
      List() {
        LazyForEach(this.notes, (item: NoteItem) => {
          ListItem() {
            NoteListItem({ note: item, onDelete: (id) => this.handleDelete(id) })
          }
        }, (item: NoteItem) => item.id) // 唯一键值
      }
      .layoutWeight(1)
      .divider({ strokeWidth: 1, color: '#F0F0F0', startMargin: 16, endMargin: 16 })
    }
  }
}

3.3 端侧AI:实现语义搜索 (VectorKit实战)

这是本应用的亮点。我们不依赖云端,利用PC强大的NPU,在本地进行向量检索。

步骤1:初始化向量数据库

// utils/VectorDBHelper.ets
import vectorKit from '@ohos.vectorKit';

export class VectorDBHelper {
  private static instance: VectorDBHelper;
  private db: vectorKit.VectorDB | null = null;

  private constructor() {}

  static getInstance(): VectorDBHelper {
    if (!VectorDBHelper.instance) {
      VectorDBHelper.instance = new VectorDBHelper();
    }
    return VectorDBHelper.instance;
  }

  async init() {
    if (this.db) return;
    const config = {
      dbPath: '/data/local/tmp/flownote_vector_db',
      dimension: 768, // 使用通用的Embedding模型维度
      distanceMetric: vectorKit.DistanceMetric.COSINE
    };
    this.db = await vectorKit.createVectorDB(config);
    console.info('Vector DB initialized');
  }

  async addNoteEmbedding(id: string, text: string) {
    if (!this.db) return;
    // 调用端侧NLU模型生成向量 (伪代码,实际需调用NLU Kit)
    const embedding = await this.generateEmbedding(text); 
    const vectorData = {
      id: id,
      vector: embedding,
      metadata: { content: text }
    };
    await this.db.insert([vectorData]);
  }

  async searchNotes(query: string, topK: number = 5): Promise<NoteItem[]> {
    if (!this.db) return [];
    const queryEmbedding = await this.generateEmbedding(query);
    const results = await this.db.search(queryEmbedding, topK);
    
    // 将搜索结果映射回NoteItem对象
    return results.map(res => ({
      id: res.id,
      title: res.metadata.content.substring(0, 20) + '...',
      preview: res.metadata.content,
      score: res.score
    }));
  }

  private async generateEmbedding(text: string): Promise<number[]> {
    // 实际项目中需集成具体的Embedding模型推理引擎
    // 这里模拟返回一个768维向量
    return new Array(768).fill(0).map(() => Math.random());
  }
}

步骤2:在UI中集成搜索

// 在ViewModel中调用
async function search(keyword: string) {
  const results = await VectorDBHelper.getInstance().searchNotes(keyword);
  this.searchResults = results;
}

3.4 分布式协同:手机拍照,PC插入

利用鸿蒙的DataShareDrag & Drop能力。

PC端接收拖拽数据:

// 在编辑器组件中
RichEditor() {
  // 富文本编辑器内容
}
.onDrop((event) => {
  // 获取拖拽数据
  const data = event.getDataByMime('image/png');
  if (data) {
    // 将图片插入到光标位置
    this.insertImage(data);
    event.setDropResult(DropResult.SUCCESS);
  }
})

手机端发送(无需额外代码,系统级支持):
用户在手机相册长按图片,拖拽(或“一抓一放”)到PC屏幕上的FlowNote窗口,系统自动完成跨设备数据传输,PC端onDrop回调被触发。


四、性能优化与用户体验打磨

4.1 内存管理与大图加载

PC端虽然内存大,但笔记应用可能包含大量高清截图。

  • 策略:使用ImageCache组件,并限制缓存大小。
  • 缩略图机制:列表页只加载缩略图,点击详情后再加载原图。
  • 懒加载:利用LazyForEach确保只有可视区域内的图片被解码。

4.2 键盘快捷键适配

PC用户重度依赖键盘。我们需要全局监听按键事件。

// 全局快捷键注册 (在Ability或主页面)
import inputMethod from '@ohos.inputMethod';

// 简单示例:监听 Ctrl + N 新建笔记
.onKeyDown((event) => {
  if (event.key === Key.N && event.ctrlKey) {
    this.viewModel.createNewNote();
    event.stop(); // 阻止默认行为
  }
  if (event.key === Key.F && event.ctrlKey) {
    this.focusSearchBar();
    event.stop();
  }
})

4.3 深色模式与自适应布局

利用ArkUI的媒体查询,完美适配不同分辨率的PC屏幕和深色模式。

@media (prefers-color-scheme: dark) {
  .container {
    background-color: #1A1A1A;
    text-color: #FFFFFF;
  }
}

@media (min-width: 1200px) {
  /* 大屏布局:三栏式 (列表-预览-编辑) */
  .layout-mode {
    flex-direction: row;
  }
}

@media (max-width: 1199px) {
  /* 小屏/平板模式:单栏或双栏切换 */
  .layout-mode {
    flex-direction: column;
  }
}

五、打包发布与真机调试

5.1 签名与打包

  1. 在DevEco Studio中配置自动签名(Debug版)或手动签名(Release版)。
  2. 选择 Build > Build Hap(s)/APP(s) > Build APP(s)
  3. 生成的.app文件位于build/default/outputs/default目录下。

5.2 上架注意事项

  • 隐私合规:必须在首次启动时明确告知用户AI模型会本地处理数据,不上传云端(除非用户开启同步)。
  • 权限申请:仅在需要时申请OHOS_PERMISSION_READ_MEDIA等权限,并在module.json5中详细说明用途。
  • 多端测试:务必在折叠屏、普通笔记本、台式机等多种形态设备上测试布局适应性。

六、总结

通过“FlowNote”这个实战项目,我们验证了HarmonyOS PC在多窗口管理端侧AI能力分布式交互以及高性能渲染方面的巨大潜力。

  • 架构层面:MVVM + Repository模式保证了代码的清晰度。
  • 技术层面LazyForEachVectorKitWindowExtension等API的灵活运用,解决了性能与功能难题。
  • 体验层面:键盘快捷键、拖拽交互、深色模式等细节,让应用具备了“原生PC应用”的质感。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐