在这里插入图片描述

每日一句正能量

人生的每一步都算数,你只需埋头播种,坐等秋后丰收。
付出不会消失,只是延迟显现。

导读

分布式能力是 HarmonyOS 最核心的差异化特性,也是学生开发者在毕业设计中最想展示的技术亮点。但"跨设备协同"不能只停留在概念层面——用户需要的是在手机上速记、在平板上深度编辑、在手表上查看提醒的无缝体验。本文将完整解析一个"多端协同笔记应用"的项目实现,涵盖手机端的富文本编辑、平板端的大屏深度处理、手表端的轻量提醒,以及三者之间的实时数据同步、任务接续和跨设备拖拽。所有代码基于 HarmonyOS 7.0 API 26 编写,可直接在 DevEco Studio 4.2 中运行。


一、项目架构设计:三端协同的总体思路

1.1 功能分工

设备 核心功能 交互形态 数据角色
手机 速记、拍照插入、语音转文字 单手握持、快速操作 主控端 + 数据发起
平板 长文编辑、手写批注、多窗口对照 大屏沉浸、精细操作 协同端 + 深度处理
手表 查看标题列表、语音速记、到期提醒 抬腕即用、极简交互 轻量端 + 提醒触达

1.2 技术架构

图1:三端协同笔记应用系统架构图

图片内容说明(中文):中心为"分布式数据层(UDDL)“,内部包含"NoteSyncObject(笔记同步对象)“和"ConflictResolver(冲突解决器)”。中心向外连接三个设备节点:手机(包含NoteEditorAbility、CameraCapture、VoiceInput)、平板(包含NoteEditorAbility、HandwritingCanvas、SplitWindow)、手表(包含NoteListAbility、VoiceMemo、ReminderService)。三个设备之间用双向箭头标注"实时同步”,箭头旁标注"sessionId: note_sync_group"。底部为"云端备份(可选)",用虚线连接中心数据层。

手表端

平板端

手机端

分布式数据层

实时同步

实时同步

实时同步

UDDL 统一数据层

NoteSyncObject
笔记同步对象

ConflictResolver
LWW + 字段级合并

NoteEditorAbility
富文本编辑

CameraCapture
拍照插入

VoiceInput
语音转文字

NoteEditorAbility
长文编辑

HandwritingCanvas
手写批注

SplitWindow
多窗口对照

NoteListAbility
标题列表

VoiceMemo
语音速记

ReminderService
到期提醒

云端备份
可选

1.3 数据模型

// model/NoteModel.ets
export class NoteModel {
  id: string = '';
  title: string = '';
  content: string = '';        // 富文本 HTML / MarkDown
  handwritingData: ArrayBuffer | null = null;  // 手写笔迹数据
  voiceMemoUri: string = '';   // 语音备忘路径
  tags: string[] = [];
  createTime: number = 0;
  updateTime: number = 0;
  reminderTime: number = 0;    // 提醒时间
  syncVersion: number = 0;     // 用于冲突解决
}

export class NoteChangeLog {
  noteId: string = '';
  field: string = '';          // 变更字段:title/content/handwriting/...
  oldValue: string = '';
  newValue: string = '';
  timestamp: number = 0;
  deviceId: string = '';
}

二、分布式数据同步:三端一致的基石

2.1 同步服务封装

// service/NoteSyncService.ets
import { distributed } from '@ohos.data.distributed';
import { NoteModel } from '../model/NoteModel';

export class NoteSyncService {
  private syncObject: distributed.DistributedObject | null = null;
  private sessionId: string = 'note_sync_group';
  private listeners: Array<(notes: NoteModel[]) => void> = [];

  async init(): Promise<void> {
    // 7.0:创建分布式数据对象,启用实时同步
    this.syncObject = distributed.createObject(this.sessionId, {
      notes: [] as NoteModel[],
      lastSyncTime: 0
    }, {
      qos: distributed.QoS.HIGH_RELIABILITY,
      syncMode: distributed.SyncMode.REALTIME,
      conflictResolution: distributed.ConflictResolution.LAST_WRITE_WINS
    });

    // 监听数据变更
    this.syncObject.on('change', (sessionId: string, fields: Array<string>) => {
      if (fields.includes('notes')) {
        const notes = this.syncObject!.notes as NoteModel[];
        this.notifyListeners(notes);
      }
    });
  }

  addNote(note: NoteModel): void {
    if (!this.syncObject) return;
    const notes = this.syncObject.notes as NoteModel[];
    note.syncVersion = Date.now();
    notes.push(note);
    this.syncObject.batchUpdate({ notes });
  }

  updateNote(id: string, updates: Partial<NoteModel>): void {
    if (!this.syncObject) return;
    const notes = this.syncObject.notes as NoteModel[];
    const idx = notes.findIndex(n => n.id === id);
    if (idx >= 0) {
      notes[idx] = { ...notes[idx], ...updates, syncVersion: Date.now() };
      this.syncObject.batchUpdate({ notes });
    }
  }

  deleteNote(id: string): void {
    if (!this.syncObject) return;
    const notes = (this.syncObject.notes as NoteModel[]).filter(n => n.id !== id);
    this.syncObject.batchUpdate({ notes });
  }

  onNotesChange(callback: (notes: NoteModel[]) => void): void {
    this.listeners.push(callback);
  }

  private notifyListeners(notes: NoteModel[]): void {
    this.listeners.forEach(cb => cb(notes));
  }
}

2.2 冲突解决策略

三端同时编辑同一笔记时,采用**字段级 LWW(Last Write Wins)**策略:

// service/ConflictResolver.ets
export class NoteConflictResolver {
  static resolve(local: NoteModel, remote: NoteModel): NoteModel {
    const merged = { ...local };

    // 逐字段比较时间戳,取较新的值
    if (remote.updateTime > local.updateTime) {
      merged.title = remote.title;
      merged.content = remote.content;
      merged.tags = remote.tags;
    }

    // 手写数据和语音备忘独立判断(不同字段可能由不同设备更新)
    if (remote.handwritingData && (!local.handwritingData || remote.syncVersion > local.syncVersion)) {
      merged.handwritingData = remote.handwritingData;
    }

    if (remote.voiceMemoUri && (!local.voiceMemoUri || remote.syncVersion > local.syncVersion)) {
      merged.voiceMemoUri = remote.voiceMemoUri;
    }

    merged.updateTime = Math.max(local.updateTime, remote.updateTime);
    merged.syncVersion = Date.now();
    return merged;
  }
}

三、任务接续:从手机速记到平板编辑

3.1 手机端发起接续

// phone/PhoneNoteAbility.ets
import { UIAbility, Want } from '@ohos.app.ability.UIAbility';
import { distributedMissionManager } from '@ohos.distributedMissionManager';

export default class PhoneNoteAbility extends UIAbility {
  private currentNoteId: string = '';

  // 用户点击"在平板上继续编辑"
  async continueToTablet(): Promise<void> {
    const continuationDevices = await distributedMissionManager.getContinuationDevices();
    if (continuationDevices.length === 0) {
      // 提示用户先组网
      return;
    }

    // 保存当前编辑状态到 want 参数
    const want: Want = {
      bundleName: 'com.example.crossnote',
      abilityName: 'PadNoteAbility',
      parameters: {
        noteId: this.currentNoteId,
        scrollPosition: AppStorage.get('scrollPosition') || 0,
        cursorPosition: AppStorage.get('cursorPosition') || 0
      }
    };

    await distributedMissionManager.continueMission({
      srcDeviceId: '',
      dstDeviceId: continuationDevices[0].deviceId,
      missionId: this.context.missionId,
      want: want
    });
  }

  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
    // 手机端保存状态
    wantParam['noteId'] = this.currentNoteId;
    wantParam['lastEditTime'] = Date.now();
    return AbilityConstant.OnContinueResult.AGREE;
  }
}

3.2 平板端恢复接续

// pad/PadNoteAbility.ets
import { UIAbility, Want } from '@ohos.app.ability.UIAbility';

export default class PadNoteAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      // 从手机接续过来,恢复编辑状态
      const noteId = want.parameters?.['noteId'] as string;
      const scrollPosition = want.parameters?.['scrollPosition'] as number || 0;
      const cursorPosition = want.parameters?.['cursorPosition'] as number || 0;

      AppStorage.setOrCreate('continuedNoteId', noteId);
      AppStorage.setOrCreate('continuedScrollPosition', scrollPosition);
      AppStorage.setOrCreate('continuedCursorPosition', cursorPosition);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pad/pages/PadEditor', (err) => {
      if (err.code) {
        console.error('加载平板编辑页失败:', err);
      }
    });
  }
}

3.3 接续状态恢复 UI

// pad/pages/PadEditor.ets
@Entry
@Component
struct PadEditorPage {
  @State note: NoteModel = new NoteModel();
  @State scrollPosition: number = 0;
  private syncService: NoteSyncService = new NoteSyncService();

  aboutToAppear() {
    this.syncService.init();
    this.syncService.onNotesChange((notes) => {
      const continuedId = AppStorage.get('continuedNoteId') as string;
      const found = notes.find(n => n.id === continuedId);
      if (found) {
        this.note = found;
        this.scrollPosition = AppStorage.get('continuedScrollPosition') as number || 0;
      }
    });
  }

  build() {
    Column() {
      Text('从手机接续编辑')
        .fontSize(14)
        .fontColor('#666')
        .visibility(this.note.id ? Visibility.Visible : Visibility.None)

      TextInput({ text: $$this.note.title })
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      RichEditor({ text: $$this.note.content })
        .layoutWeight(1)

      // 手写批注区域(平板特有)
      HandwritingCanvas({
        data: this.note.handwritingData,
        onChange: (data) => {
          this.syncService.updateNote(this.note.id, { handwritingData: data });
        }
      })
    }
    .width('100%')
    .height('100%')
  }
}

四、跨设备拖拽:从手机拖图片到平板

4.1 拖拽发起(手机端)

// components/DraggableImage.ets
@Component
struct DraggableImage {
  @Prop imageUri: string;
  @Prop noteId: string;

  build() {
    Image(this.imageUri)
      .width(100)
      .height(100)
      .borderRadius(8)
      .draggable(true)
      .onDragStart((event: DragEvent) => {
        // 7.0:设置跨设备拖拽数据
        event.setData({
          uniformDataItems: [
            {
              uniformDataType: 'image',
              uri: this.imageUri,
              noteId: this.noteId
            }
          ],
          dragProperties: {
            crossDevice: true,           // 允许跨设备拖拽
            supportedDevices: ['tablet'] // 目标设备类型
          }
        });
      })
  }
}

4.2 拖拽接收(平板端)

// pad/components/DropZone.ets
@Component
struct DropZone {
  @State droppedImages: string[] = [];
  private syncService: NoteSyncService = new NoteSyncService();

  build() {
    Column() {
      Text('拖拽图片到此处')
        .fontSize(16)
        .fontColor('#999')

      ForEach(this.droppedImages, (uri: string) => {
        Image(uri).width(120).height(120)
      })
    }
    .width('100%')
    .height(200)
    .border({ width: 2, color: '#DDD', style: BorderStyle.Dashed })
    .backgroundColor('#F9F9F9')
    .dropArea({ ... })  // 声明可接收拖拽
    .onDrop((event: DragEvent, extraParams: string) => {
      const dragData = event.getData();
      const imageItem = dragData.uniformDataItems.find(
        item => item.uniformDataType === 'image'
      );

      if (imageItem) {
        // 接收跨设备图片
        this.droppedImages.push(imageItem.uri);

        // 同步到笔记中
        const noteId = imageItem.noteId;
        this.syncService.updateNote(noteId, {
          content: `![插入图片](${imageItem.uri})`
        });
      }
    })
  }
}

五、手表端:轻量提醒与语音速记

5.1 手表端 Ability

// watch/WatchNoteAbility.ets
import { UIAbility } from '@ohos.app.ability.UIAbility';

export default class WatchNoteAbility extends UIAbility {
  onCreate(): void {
    // 手表端仅显示标题列表和语音速记入口
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('watch/pages/WatchList');
  }
}

5.2 手表端列表与语音速记

// watch/pages/WatchList.ets
@Entry
@Component
struct WatchListPage {
  @State notes: NoteModel[] = [];
  @State isRecording: boolean = false;
  private syncService: NoteSyncService = new NoteSyncService();

  aboutToAppear() {
    this.syncService.init();
    this.syncService.onNotesChange((allNotes) => {
      // 手表端仅显示有提醒的笔记和最近 5 条
      this.notes = allNotes
        .filter(n => n.reminderTime > Date.now() || n.updateTime > Date.now() - 86400000)
        .slice(0, 5);
    });
  }

  build() {
    Column() {
      List() {
        ForEach(this.notes, (note: NoteModel) => {
          ListItem() {
            Column() {
              Text(note.title || '无标题')
                .fontSize(16)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })

              if (note.reminderTime > Date.now()) {
                Text(this.formatTime(note.reminderTime))
                  .fontSize(12)
                  .fontColor('#FF6B6B')
              }
            }
            .width('100%')
            .padding(8)
          }
        })
      }

      // 语音速记按钮
      Button(this.isRecording ? '录音中...' : '语音速记')
        .width(120)
        .height(120)
        .type(ButtonType.Circle)
        .backgroundColor(this.isRecording ? '#FF6B6B' : '#4ECDC4')
        .onClick(() => this.toggleRecording())
    }
    .width('100%')
    .height('100%')
    .padding(8)
  }

  async toggleRecording(): Promise<void> {
    if (this.isRecording) {
      // 停止录音,保存
      const audioUri = await this.stopRecording();
      const newNote = new NoteModel();
      newNote.id = `voice_${Date.now()}`;
      newNote.voiceMemoUri = audioUri;
      newNote.title = '语音速记';
      newNote.createTime = Date.now();
      this.syncService.addNote(newNote);
      this.isRecording = false;
    } else {
      // 开始录音
      await this.startRecording();
      this.isRecording = true;
    }
  }

  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
  }
}

六、数据同步时序:一次完整的协同流程

图2:三端协同笔记应用数据同步时序图

图片内容说明(中文):时序图,四个泳道从上到下为"手机端"、“平板端”、“手表端”、“分布式数据层”。流程:①手机端创建笔记→写入UDDL→②UDDL推送变更→平板端/手表端同步收到。③平板端编辑标题→写入UDDL→④手机端/手表端收到更新。⑤手表端语音速记→写入UDDL→⑥手机端/平板端收到新笔记。⑦手机端跨设备拖拽图片到平板→平板端接收并更新UDDL→⑧所有端同步显示图片。各操作旁标注时间戳和syncVersion。

分布式数据层 手表端 平板端 手机端 分布式数据层 手表端 平板端 手机端 收到新笔记 Note-A 标题更新 收到语音笔记 内容同步更新 创建笔记 Note-A syncVersion=1001 推送变更: notes[] 推送变更: notes[] 编辑标题 syncVersion=1002 推送变更 推送变更 语音速记 创建语音笔记 Note-B syncVersion=1003 推送变更 推送变更 跨设备拖拽图片 更新笔记内容(插入图片) syncVersion=1004 推送变更 推送变更

七、测试验证要点

测试场景 操作步骤 预期结果
实时同步 手机创建笔记 平板和手表 1 秒内显示新笔记
字段级冲突 手机改标题,平板改内容(同时) 两端都保留:新标题 + 新内容
任务接续 手机编辑到一半,点击"平板继续" 平板打开同一笔记,光标位置一致
跨设备拖拽 手机相册图片拖入平板编辑区 平板显示图片,笔记内容同步更新
离线恢复 手表断网后语音速记,恢复网络后 语音笔记自动同步到手机和平板
手表提醒 设置提醒时间,到达时间点 手表震动提醒,显示笔记标题

八、结语

跨设备协同不是"把同一个应用装到三个设备上",而是"让三个设备共同完成一个任务的不同片段"。手机适合速记和采集,平板适合深度编辑,手表适合提醒和轻量查看——各自发挥所长,数据实时贯通。

HarmonyOS 7.0 的分布式数据对象、任务接续和跨设备拖拽,为这种"场景化协同"提供了底层能力支撑。开发者不再需要关心设备之间如何组网、如何传输、如何解决冲突,只需关注"用户在每个设备上需要做什么"。

对于高校学生开发者,三端协同项目是一个极具展示价值的毕业设计选题。它既体现了对鸿蒙分布式能力的深入理解,又展现了完整的系统架构设计能力。当你在答辩现场演示"手机拍照→拖拽到平板→平板手写批注→手表提醒复习"的完整链路时,评委看到的不仅是一个 App,更是一个生态。


转载自:https://blog.csdn.net/u014727709/article/details/162933246
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

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

更多推荐