鸿蒙多屏协同办公系统设计与实现 原创

进修的泡芙
发布于 2025-6-18 22:20
浏览
0收藏

鸿蒙多屏协同办公系统设计与实现

一、系统架构设计

基于HarmonyOS的分布式能力和统一数据管理框架,我们设计了一个多屏协同办公系统,支持跨设备拖拽文件、文字和图片,实现无缝的办公协作体验。

!https://example.com/multi-screen-collab-arch.png

系统包含三个核心模块:
分布式文件管理模块 - 使用@ohos.file.distributedFile实现文件传输

数据同步模块 - 通过@ohos.distributedData实现剪贴板和数据同步

UI协同模块 - 提供拖拽交互和可视化反馈

二、核心代码实现
分布式文件服务(ArkTS)

// FileTransferService.ets
import distributedFile from ‘@ohos.file.distributedFile’;
import distributedData from ‘@ohos.distributedData’;

const FILE_TRANSFER_CHANNEL = ‘file_transfer’;

class FileTransferService {
private static instance: FileTransferService = null;
private dataManager: distributedData.DataManager;
private listeners: FileTransferListener[] = [];

private constructor() {
this.initDataManager();
public static getInstance(): FileTransferService {

if (!FileTransferService.instance) {
  FileTransferService.instance = new FileTransferService();

return FileTransferService.instance;

private initDataManager() {

this.dataManager = distributedData.createDataManager({
  bundleName: 'com.example.multiscreen',
  area: distributedData.Area.GLOBAL
});

this.dataManager.registerDataListener(FILE_TRANSFER_CHANNEL, (data) => {
  this.handleSyncData(data);
});

public async sendFile(fileUri: string, targetDeviceId: string): Promise<boolean> {

try {
  // 发送文件传输请求
  this.dataManager.sendData(targetDeviceId, FILE_TRANSFER_CHANNEL, {
    type: 'fileRequest',
    fileName: this.getFileNameFromUri(fileUri),
    fileSize: await this.getFileSize(fileUri),
    requestId: Date.now().toString(),
    sourceDeviceId: this.getDeviceId()
  });
  
  // 实际传输文件
  await distributedFile.sendFile(targetDeviceId, fileUri);
  return true;

catch (err) {

  console.error('文件传输失败:', JSON.stringify(err));
  return false;

}

public async receiveFile(fileRequest: FileRequest): Promise<string> {
const destPath = this.getReceiveFilePath(fileRequest.fileName);
await distributedFile.recvFile(fileRequest.sourceDeviceId, destPath);
return destPath;
public addListener(listener: FileTransferListener): void {

if (!this.listeners.includes(listener)) {
  this.listeners.push(listener);

}

public removeListener(listener: FileTransferListener): void {
this.listeners = this.listeners.filter(l => l !== listener);
private handleSyncData(data: any): void {

if (!data) return;

switch (data.type) {
  case 'fileRequest':
    this.listeners.forEach(listener => {
      listener.onFileRequest(data as FileRequest);
    });
    break;
  case 'fileProgress':
    this.listeners.forEach(listener => {
      listener.onFileProgress(data.fileId, data.progress);
    });
    break;
  case 'fileComplete':
    this.listeners.forEach(listener => {
      listener.onFileComplete(data.fileId, data.filePath);
    });
    break;

}

private getFileNameFromUri(uri: string): string {
return uri.split(‘/’).pop() || ‘file’;
private async getFileSize(uri: string): Promise<number> {

// 实际实现需要获取文件大小
return 0;

private getReceiveFilePath(fileName: string): string {

return /data/storage/el2/base/files/received/{Date.now()}_{fileName};

private getDeviceId(): string {

// 实际实现需要获取设备ID
return 'local_device';

}

interface FileRequest {
type: string;
fileName: string;
fileSize: number;
requestId: string;
sourceDeviceId: string;
interface FileTransferListener {

onFileRequest(request: FileRequest): void;
onFileProgress(fileId: string, progress: number): void;
onFileComplete(fileId: string, filePath: string): void;
export const fileTransferService = FileTransferService.getInstance();

跨设备剪贴板服务(ArkTS)

// ClipboardService.ets
import pasteboard from ‘@ohos.pasteboard’;
import distributedData from ‘@ohos.distributedData’;

const CLIPBOARD_SYNC_CHANNEL = ‘clipboard_sync’;

class ClipboardService {
private static instance: ClipboardService = null;
private dataManager: distributedData.DataManager;
private listeners: ClipboardListener[] = [];

private constructor() {
this.initDataManager();
public static getInstance(): ClipboardService {

if (!ClipboardService.instance) {
  ClipboardService.instance = new ClipboardService();

return ClipboardService.instance;

private initDataManager() {

this.dataManager = distributedData.createDataManager({
  bundleName: 'com.example.multiscreen',
  area: distributedData.Area.GLOBAL
});

this.dataManager.registerDataListener(CLIPBOARD_SYNC_CHANNEL, (data) => {
  this.handleSyncData(data);
});

public async syncClipboard(targetDeviceId: string): Promise<void> {

const systemPasteboard = pasteboard.getSystemPasteboard();
const data = await systemPasteboard.getData();

if (data) {
  this.dataManager.sendData(targetDeviceId, CLIPBOARD_SYNC_CHANNEL, {
    type: 'clipboardData',
    dataType: data.type,
    data: this.convertPasteData(data),
    timestamp: Date.now(),
    sourceDeviceId: this.getDeviceId()
  });

}

private convertPasteData(data: pasteboard.PasteData): any {
switch (data.type) {
case pasteboard.PasteDataType.TEXT:
return data.text;
case pasteboard.PasteDataType.URI:
return data.uri;
case pasteboard.PasteDataType.PIXELMAP:
return data.pixelMap;
default:
return null;
}

private async applyClipboardData(data: ClipboardData): Promise<void> {
const systemPasteboard = pasteboard.getSystemPasteboard();
let pasteData: pasteboard.PasteData | null = null;

switch (data.dataType) {
  case pasteboard.PasteDataType.TEXT:
    pasteData = pasteboard.createPlainTextData(data.data);
    break;
  case pasteboard.PasteDataType.URI:
    pasteData = pasteboard.createUriData(data.data);
    break;
  case pasteboard.PasteDataType.PIXELMAP:
    pasteData = pasteboard.createPixelMapData(data.data);
    break;

if (pasteData) {

  await systemPasteboard.setData(pasteData);

}

public addListener(listener: ClipboardListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}

public removeListener(listener: ClipboardListener): void {
this.listeners = this.listeners.filter(l => l !== listener);
private handleSyncData(data: any): void {

if (data?.type = 'clipboardData' && data.sourceDeviceId ! this.getDeviceId()) {
  this.listeners.forEach(listener => {
    listener.onClipboardData(data as ClipboardData);
  });
  
  this.applyClipboardData(data);

}

private getDeviceId(): string {
// 实际实现需要获取设备ID
return ‘local_device’;
}

interface ClipboardData {
type: string;
dataType: pasteboard.PasteDataType;
data: any;
timestamp: number;
sourceDeviceId: string;
interface ClipboardListener {

onClipboardData(data: ClipboardData): void;
export const clipboardService = ClipboardService.getInstance();

协同办公主界面(ArkUI)

// MultiScreenCollaboration.ets
import { fileTransferService, clipboardService } from ‘./FileTransferService’;

@Entry
@Component
struct MultiScreenCollaboration {
@State connectedDevices: deviceManager.DeviceInfo[] = [];
@State activeTransfer: FileTransfer | null = null;
@State dragContent: DragContent | null = null;

private deviceListener: DeviceListener = {
onDeviceConnected: (device) => {
this.connectedDevices = […this.connectedDevices, device];
},
onDeviceDisconnected: (deviceId) => {
this.connectedDevices = this.connectedDevices.filter(d => d.deviceId !== deviceId);
};

aboutToAppear() {
this.initDeviceManager();
fileTransferService.addListener(this.fileTransferListener);
clipboardService.addListener(this.clipboardListener);
aboutToDisappear() {

fileTransferService.removeListener(this.fileTransferListener);
clipboardService.removeListener(this.clipboardListener);

build() {

Stack() {
  // 主工作区
  Column() {
    // 文件浏览器区域
    this.buildFileBrowser()
    
    // 文本编辑区域
    this.buildTextEditor()

// 设备连接面板

  if (this.connectedDevices.length > 0) {
    this.buildDevicePanel()

// 传输进度指示器

  if (this.activeTransfer) {
    this.buildTransferIndicator()

}

.width('100%')
.height('100%')

@Builder

buildFileBrowser() {
Column() {
Text(‘文件浏览器’)
.fontSize(18)
.margin({ bottom: 10 })

  Grid() {
    ForEach(this.getSampleFiles(), (file) => {
      GridItem() {
        Column() {
          Image($r('app.media.ic_file'))
            .width(40)
            .height(40)
          
          Text(file.name)
            .fontSize(12)
            .margin({ top: 4 })

.padding(10)

        .gesture(
          GestureGroup(GestureMode.Exclusive,
            LongPressGesture()
              .onAction(() => {
                this.dragContent = {
                  type: 'file',
                  uri: file.uri
                };
              })
          )
        )

})

.columnsTemplate(‘1fr 1fr 1fr 1fr’)

  .rowsTemplate('1fr')
  .columnsGap(10)
  .rowsGap(10)

.padding(20)

.width('100%')
.height('50%')
.border({ width: 1, color: '#EEEEEE' })
.onDrop((event: DragEvent) => {
  if (this.dragContent?.type === 'file') {
    this.handleFileDrop(event);

})

@Builder

buildTextEditor() {
Column() {
Text(‘文本编辑器’)
.fontSize(18)
.margin({ bottom: 10 })

  TextInput({ placeholder: '输入文本...' })
    .width('90%')
    .height(120)
    .border({ width: 1, color: '#EEEEEE' })
    .onSelectChange((start: number, end: number) => {
      this.dragContent = {
        type: 'text',
        text: this.getSelectedText(start, end)
      };
    })

.padding(20)

.width('100%')
.height('50%')
.border({ width: 1, color: '#EEEEEE' })
.onDrop((event: DragEvent) => {
  if (this.dragContent?.type === 'text') {
    this.handleTextDrop(event);

})

@Builder

buildDevicePanel() {
Column() {
Text(‘已连接设备’)
.fontSize(16)
.margin({ bottom: 10 })

  ForEach(this.connectedDevices, (device) => {
    Row() {
      Image($r('app.media.ic_device'))
        .width(24)
        .height(24)
        .margin({ right: 10 })
      
      Text(device.deviceName)
        .fontSize(14)
        .layoutWeight(1)

.padding(10)

    .width('80%')
    .border({ width: 1, color: '#EEEEEE' })
    .onDrop((event: DragEvent) => {
      if (this.dragContent) {
        this.sendToDevice(device.deviceId, this.dragContent);

})

  })

.position({ x: ‘80%’, y: 20 })

.width('20%')
.padding(10)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#CCCCCC' })

@Builder

buildTransferIndicator() {
Column() {
Text(正在传输: ${this.activeTransfer?.fileName || ‘’})
.fontSize(14)

  Progress({
    value: this.activeTransfer?.progress || 0,
    total: 100
  })
    .width('80%')
    .margin({ top: 10 })

.position({ x: ‘50%’, y: ‘50%’ })

.width('60%')
.padding(20)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#CCCCCC' })
.borderRadius(8)
.translate({ x: '-50%', y: '-50%' })

private fileTransferListener: FileTransferListener = {

onFileRequest: (request) => {
  // 显示文件接收确认对话框
  promptAction.showDialog({
    title: '文件传输请求',
    message: 是否接收文件 {request.fileName} ({this.formatFileSize(request.fileSize)})?,
    buttons: [

text: ‘接收’, color: ‘#0A59F7’ },

text: ‘拒绝’, color: ‘#FF0000’ }

}).then((result) => {

    if (result.index === 0) {
      fileTransferService.receiveFile(request);

});

},
onFileProgress: (fileId, progress) => {
  if (this.activeTransfer?.fileId === fileId) {
    this.activeTransfer.progress = progress;

},

onFileComplete: (fileId, filePath) => {
  if (this.activeTransfer?.fileId === fileId) {
    promptAction.showToast({ message: '文件接收完成', duration: 2000 });
    this.activeTransfer = null;

}

};

private clipboardListener: ClipboardListener = {
onClipboardData: (data) => {
promptAction.showToast({
message: 已从${data.sourceDeviceId}同步剪贴板,
duration: 2000
});
};

private handleFileDrop(event: DragEvent): void {
// 实现文件放置逻辑
console.log(‘文件放置到:’, event.position);
private handleTextDrop(event: DragEvent): void {

// 实现文本放置逻辑
console.log('文本放置到:', event.position);

private sendToDevice(deviceId: string, content: DragContent): void {

switch (content.type) {
  case 'file':
    this.activeTransfer = {
      fileId: Date.now().toString(),
      fileName: this.getFileNameFromUri(content.uri),
      progress: 0
    };
    fileTransferService.sendFile(content.uri, deviceId);
    break;
  case 'text':
    clipboardService.syncClipboard(deviceId);
    break;

}

private getSampleFiles(): FileItem[] {
return [
name: ‘文档1.docx’, uri: ‘internal://files/doc1.docx’ },

name: ‘表格.xlsx’, uri: ‘internal://files/sheet.xlsx’ },

name: ‘图片.png’, uri: ‘internal://files/image.png’ },

name: ‘报告.pdf’, uri: ‘internal://files/report.pdf’ }

];

private getFileNameFromUri(uri: string): string {

return uri.split('/').pop() || 'file';

private formatFileSize(bytes: number): string {

if (bytes < 1024) return ${bytes} B;
if (bytes < 1024 * 1024) return ${(bytes / 1024).toFixed(1)} KB;
return ${(bytes / (1024 * 1024)).toFixed(1)} MB;

private getSelectedText(start: number, end: number): string {

// 实际实现需要获取选中文本
return '';

private initDeviceManager(): void {

// 实际实现需要初始化设备管理

}

interface FileItem {
name: string;
uri: string;
interface DragContent {

type: ‘file’ | ‘text’;
uri?: string;
text?: string;
interface FileTransfer {

fileId: string;
fileName: string;
progress: number;
interface DeviceListener {

onDeviceConnected(device: deviceManager.DeviceInfo): void;
onDeviceDisconnected(deviceId: string): void;
export const multiScreenService = new MultiScreenCollaboration();

三、项目配置
权限配置

// module.json5
“module”: {

"requestPermissions": [

“name”: “ohos.permission.READ_MEDIA”,

    "reason": "读取文件内容"
  },

“name”: “ohos.permission.WRITE_MEDIA”,

    "reason": "保存接收的文件"
  },

“name”: “ohos.permission.DISTRIBUTED_DATASYNC”,

    "reason": "跨设备数据同步"
  },

“name”: “ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE”,

    "reason": "发现和管理分布式设备"
  },

“name”: “ohos.permission.PASTEBOARD_WRITE”,

    "reason": "写入剪贴板"
  },

“name”: “ohos.permission.PASTEBOARD_READ”,

    "reason": "读取剪贴板"

],

"abilities": [

“name”: “MainAbility”,

    "type": "page",
    "visible": true

],

"distributedNotification": {
  "scenarios": [

“name”: “multiscreen_collab”,

      "value": "file_transfer"

]

}

资源文件

<!-- resources/base/element/string.json -->
“string”: [

“name”: “app_name”,

  "value": "多屏协同办公"
},

“name”: “file_browser”,

  "value": "文件浏览器"
},

“name”: “text_editor”,

  "value": "文本编辑器"
},

“name”: “connected_devices”,

  "value": "已连接设备"
},

“name”: “file_transfer_request”,

  "value": "文件传输请求"
},

“name”: “receive_button”,

  "value": "接收"
},

“name”: “reject_button”,

  "value": "拒绝"
},

“name”: “transfer_complete”,

  "value": "文件接收完成"
},

“name”: “clipboard_synced”,

  "value": "已从设备同步剪贴板"

]

四、功能扩展
拖拽视觉反馈增强

// 在MultiScreenCollaboration中添加拖拽反馈
@State dragOverTarget: string | null = null;

@Builder
buildFileBrowser() {
Column() {
// …其他代码
.opacity(this.dragOverTarget === ‘fileBrowser’ ? 0.7 : 1)

.backgroundColor(this.dragOverTarget === ‘fileBrowser’ ? ‘#E3F2FD’ : ‘#FFFFFF’)
.onDragEnter(() => {
this.dragOverTarget = ‘fileBrowser’;
})
.onDragLeave(() => {
this.dragOverTarget = null;
})
@Builder

buildTextEditor() {
Column() {
// …其他代码
.opacity(this.dragOverTarget === ‘textEditor’ ? 0.7 : 1)

.backgroundColor(this.dragOverTarget === ‘textEditor’ ? ‘#E3F2FD’ : ‘#FFFFFF’)
.onDragEnter(() => {
this.dragOverTarget = ‘textEditor’;
})
.onDragLeave(() => {
this.dragOverTarget = null;
})

文件预览功能

// 添加文件预览组件
@Component
struct FilePreview {
@Prop fileUri: string = ‘’;

build() {
Column() {
if (this.fileUri.endsWith(‘.png’) || this.fileUri.endsWith(‘.jpg’)) {
Image(this.fileUri)
.width(‘100%’)
.height(200)
.objectFit(ImageFit.Contain)
else if (this.fileUri.endsWith(‘.txt’)) {

    Text(this.getFileContent())
      .width('100%')
      .height(200)
      .fontSize(14)

else {

    Text('不支持预览此文件类型')
      .fontSize(14)

}

.padding(10)

private getFileContent(): string {

// 实际实现需要读取文件内容
return '';

}

协同编辑功能

// 添加协同文本编辑服务
class CollaborativeEditor {
private static instance: CollaborativeEditor = null;
private dataManager: distributedData.DataManager;

private constructor() {
this.initDataManager();
public static getInstance(): CollaborativeEditor {

if (!CollaborativeEditor.instance) {
  CollaborativeEditor.instance = new CollaborativeEditor();

return CollaborativeEditor.instance;

private initDataManager() {

this.dataManager = distributedData.createDataManager({
  bundleName: 'com.example.multiscreen',
  area: distributedData.Area.GLOBAL
});

this.dataManager.registerDataListener('collab_edit', (data) => {
  this.handleEditData(data);
});

public sendEdit(edit: TextEdit): void {

this.dataManager.sendData(edit.targetDeviceId, 'collab_edit', edit);

private handleEditData(data: any): void {

if (data?.type === 'textEdit') {
  this.applyEdit(data as TextEdit);

}

private applyEdit(edit: TextEdit): void {
// 实际实现需要应用文本编辑
}

interface TextEdit {
type: string;
position: number;
text: string;
operation: ‘insert’ | ‘delete’;
targetDeviceId: string;
timestamp: number;

五、总结

通过这个多屏协同办公系统的实现,我们学习了:
使用HarmonyOS分布式能力实现文件传输

跨设备剪贴板同步技术

拖拽交互的实现和优化

多设备协同工作流设计

分布式数据同步策略

系统特点:
直观的拖拽操作体验

低延迟的文件传输

实时的剪贴板同步

可扩展的协同编辑架构

完善的用户反馈机制

这个系统可以进一步扩展为功能更完善的协同办公平台,如:
添加实时文档协同编辑

支持更多文件类型的预览

实现版本控制和历史记录

添加任务分配和进度跟踪

支持多语言和多时区协作

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐