
鸿蒙多屏协同办公系统设计与实现 原创
鸿蒙多屏协同办公系统设计与实现
一、系统架构设计
基于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分布式能力实现文件传输
跨设备剪贴板同步技术
拖拽交互的实现和优化
多设备协同工作流设计
分布式数据同步策略
系统特点:
直观的拖拽操作体验
低延迟的文件传输
实时的剪贴板同步
可扩展的协同编辑架构
完善的用户反馈机制
这个系统可以进一步扩展为功能更完善的协同办公平台,如:
添加实时文档协同编辑
支持更多文件类型的预览
实现版本控制和历史记录
添加任务分配和进度跟踪
支持多语言和多时区协作
