diff --git a/.qoder/quests/music-player-error-handling.md b/.qoder/quests/music-player-error-handling.md new file mode 100644 index 0000000..e69de29 diff --git a/.qoder/quests/system-business-decoupling-design.md b/.qoder/quests/system-business-decoupling-design.md new file mode 100644 index 0000000..6199ca0 --- /dev/null +++ b/.qoder/quests/system-business-decoupling-design.md @@ -0,0 +1,623 @@ +# 系统与业务解耦架构设计 + +## 概述 + +本设计旨在构建一个高性能的类Windows桌面前端系统,实现系统框架与业务应用的完全解耦。系统提供统一的桌面环境、窗体管理和资源访问控制,而业务应用通过标准化的SDK接口进行开发,确保双方独立演进且相互不影响。 + +### 核心设计原则 + +- **完全隔离**:系统与应用在运行时完全隔离,应用无法直接访问系统资源 +- **标准化接口**:通过统一的SDK提供标准化的系统服务接口 +- **性能优先**:采用微前端沙箱、虚拟化渲染等技术确保高性能 +- **框架无关**:支持任意前端框架开发的第三方应用 +- **安全可控**:严格的权限控制和安全沙箱机制 + +## 整体架构 + +### 系统分层架构 + +```mermaid +graph TB + subgraph "用户界面层" + Desktop[桌面容器] + WindowManager[窗体管理器] + TaskBar[任务栏] + SystemUI[系统UI组件] + end + + subgraph "系统服务层" + WindowService[窗体服务] + ResourceService[资源服务] + EventService[事件服务] + SecurityService[安全服务] + StorageService[存储服务] + end + + subgraph "应用运行时层" + AppContainer[应用容器] + SandboxEngine[沙箱引擎] + SDKBridge[SDK桥接层] + AppLifecycle[应用生命周期] + end + + subgraph "第三方应用" + App1[业务应用A] + App2[业务应用B] + App3[业务应用C] + end + + Desktop --> WindowManager + WindowManager --> WindowService + AppContainer --> SandboxEngine + SDKBridge --> SystemService[系统服务层] + App1 --> AppContainer + App2 --> AppContainer + App3 --> AppContainer +``` + +### 核心组件职责 + +| 组件 | 职责 | 接口类型 | +| ---------- | -------------------------- | -------- | +| 桌面容器 | 管理应用图标、布局和交互 | 系统内部 | +| 窗体管理器 | 窗体创建、管理、切换和销毁 | 系统内部 | +| 应用容器 | 第三方应用运行环境隔离 | 对外SDK | +| 沙箱引擎 | 安全隔离和权限控制 | 系统内部 | +| SDK桥接层 | 系统服务的标准化接口 | 对外SDK | + +## 系统核心服务 + +### 窗体管理服务 + +窗体管理服务负责所有窗体的生命周期管理,提供统一的窗体操作接口。 + +#### 窗体生命周期 + +```mermaid +stateDiagram-v2 + [*] --> Creating: 创建窗体 + Creating --> Loading: 加载应用 + Loading --> Active: 激活显示 + Active --> Minimized: 最小化 + Minimized --> Active: 恢复显示 + Active --> Maximized: 最大化 + Maximized --> Active: 还原 + Active --> Closing: 关闭请求 + Closing --> Destroyed: 销毁完成 + Destroyed --> [*] + + Loading --> Error: 加载失败 + Error --> Destroyed: 错误处理 +``` + +#### 窗体服务接口规范 + +| 服务方法 | 参数 | 返回值 | 描述 | +| -------------- | ----------------------- | -------------- | ------------ | +| createWindow | appId, config | WindowInstance | 创建新窗体 | +| destroyWindow | windowId | boolean | 销毁指定窗体 | +| minimizeWindow | windowId | boolean | 最小化窗体 | +| maximizeWindow | windowId | boolean | 最大化窗体 | +| restoreWindow | windowId | boolean | 还原窗体 | +| setWindowTitle | windowId, title | boolean | 设置窗体标题 | +| setWindowSize | windowId, width, height | boolean | 设置窗体尺寸 | + +### 资源管理服务 + +资源管理服务提供统一的系统资源访问控制,确保应用只能通过授权访问系统资源。 + +#### 资源访问控制矩阵 + +| 资源类型 | 默认权限 | 申请方式 | 审批流程 | +| ------------ | ------------ | -------- | -------- | +| 本地存储 | 应用独立空间 | 配置声明 | 自动授权 | +| 网络请求 | 白名单域名 | 动态申请 | 用户确认 | +| 文件系统 | 禁止访问 | 动态申请 | 用户确认 | +| 系统通知 | 禁止发送 | 动态申请 | 用户确认 | +| 剪贴板 | 禁止访问 | 动态申请 | 用户确认 | +| 摄像头麦克风 | 禁止访问 | 动态申请 | 用户确认 | + +### 事件通信服务 + +提供系统级别的事件通信机制,支持应用间消息传递和系统事件监听。 + +#### 事件分类与权限 + +```mermaid +graph LR + subgraph "系统事件" + SE1[窗体状态变化] + SE2[主题切换] + SE3[网络状态变化] + SE4[系统资源变化] + end + + subgraph "应用事件" + AE1[应用间消息] + AE2[应用状态同步] + AE3[数据共享请求] + end + + subgraph "用户事件" + UE1[用户交互] + UE2[快捷键触发] + UE3[拖拽操作] + end + + SE1 --> 只读监听 + SE2 --> 只读监听 + AE1 --> 权限验证 + AE2 --> 权限验证 + UE1 --> 应用内处理 +``` + +## 应用沙箱机制 + +### 沙箱隔离策略 + +应用运行在完全隔离的沙箱环境中,通过多重隔离机制确保安全性。 + +#### 沙箱隔离层次 + +```mermaid +graph TD + subgraph "物理隔离层" + IFrame[IFrame容器] + ShadowDOM[Shadow DOM] + CSP[内容安全策略] + end + + subgraph "逻辑隔离层" + Namespace[命名空间隔离] + Memory[内存隔离] + Storage[存储隔离] + end + + subgraph "网络隔离层" + CORS[跨域限制] + Proxy[代理拦截] + Whitelist[白名单过滤] + end + + subgraph "API隔离层" + SDKProxy[SDK代理] + Permission[权限验证] + Audit[审计日志] + end + + IFrame --> Namespace + ShadowDOM --> Memory + CSP --> Storage + Namespace --> SDKProxy + Memory --> Permission + Storage --> Audit +``` + +### 沙箱性能优化 + +#### 虚拟化渲染机制 + +```mermaid +flowchart TD + Start[应用启动] --> CheckCache{检查缓存} + CheckCache -->|存在| LoadCache[加载缓存实例] + CheckCache -->|不存在| CreateSandbox[创建沙箱] + + CreateSandbox --> PreloadResources[预加载资源] + PreloadResources --> InitSDK[初始化SDK] + InitSDK --> MountApp[挂载应用] + + LoadCache --> ValidateCache{验证缓存有效性} + ValidateCache -->|有效| RestoreApp[恢复应用状态] + ValidateCache -->|无效| CreateSandbox + + MountApp --> AppReady[应用就绪] + RestoreApp --> AppReady + + AppReady --> Monitor[性能监控] + Monitor --> OptimizeMemory[内存优化] + OptimizeMemory --> CheckIdle{检查空闲状态} + CheckIdle -->|空闲| SuspendApp[挂起应用] + CheckIdle -->|活跃| Monitor + + SuspendApp --> CheckActivate{检查激活请求} + CheckActivate -->|激活| RestoreApp + CheckActivate -->|继续空闲| SuspendApp +``` + +## SDK接口设计 + +### SDK架构组成 + +SDK作为应用与系统间的唯一通信桥梁,提供完整的系统服务接口。 + +#### SDK模块划分 + +| 模块名称 | 功能描述 | 主要接口 | +| ----------- | -------- | --------------------------------------- | +| WindowSDK | 窗体操作 | setTitle, resize, minimize, maximize | +| StorageSDK | 数据存储 | get, set, remove, clear | +| EventSDK | 事件通信 | emit, on, off, broadcast | +| UISDK | UI组件 | showDialog, showNotification, showToast | +| NetworkSDK | 网络请求 | request, upload, download | +| ResourceSDK | 资源访问 | getFile, saveFile, getClipboard | + +### SDK使用示例 + +#### 窗体操作SDK + +```typescript +// 应用中使用窗体SDK的示例 +interface WindowSDK { + // 设置窗体标题 + setTitle(title: string): Promise + + // 调整窗体尺寸 + resize(width: number, height: number): Promise + + // 最小化窗体 + minimize(): Promise + + // 最大化窗体 + maximize(): Promise + + // 监听窗体状态变化 + onStateChange(callback: (state: WindowState) => void): void +} +``` + +#### 存储服务SDK + +```typescript +// 应用存储SDK接口定义 +interface StorageSDK { + // 存储数据 + set(key: string, value: any): Promise + + // 获取数据 + get(key: string): Promise + + // 删除数据 + remove(key: string): Promise + + // 清空存储 + clear(): Promise + + // 监听存储变化 + onChanged(callback: (key: string, newValue: any, oldValue: any) => void): void +} +``` + +### SDK权限控制机制 + +#### 权限申请流程 + +```mermaid +sequenceDiagram + participant App as 第三方应用 + participant SDK as SDK桥接层 + participant Security as 安全服务 + participant User as 用户界面 + participant System as 系统服务 + + App->>SDK: 调用需权限的API + SDK->>Security: 检查权限状态 + + alt 已授权 + Security->>SDK: 返回已授权 + SDK->>System: 调用系统服务 + System->>SDK: 返回执行结果 + SDK->>App: 返回结果 + else 未授权 + Security->>User: 显示权限申请弹窗 + User->>Security: 用户确认/拒绝 + alt 用户同意 + Security->>SDK: 授权通过 + SDK->>System: 调用系统服务 + System->>SDK: 返回执行结果 + SDK->>App: 返回结果 + else 用户拒绝 + Security->>SDK: 权限被拒绝 + SDK->>App: 返回权限错误 + end + end +``` + +## 应用生命周期管理 + +### 应用状态管理 + +应用在系统中具有完整的生命周期管理,确保资源的合理分配和回收。 + +#### 应用状态转换图 + +```mermaid +stateDiagram-v2 + [*] --> Installing: 安装应用 + Installing --> Installed: 安装完成 + Installing --> InstallFailed: 安装失败 + InstallFailed --> [*] + + Installed --> Starting: 启动应用 + Starting --> Running: 运行中 + Starting --> StartFailed: 启动失败 + StartFailed --> Installed + + Running --> Suspended: 挂起 + Suspended --> Running: 恢复 + Running --> Stopping: 停止 + Stopping --> Installed: 已停止 + + Installed --> Uninstalling: 卸载应用 + Uninstalling --> [*]: 卸载完成 +``` + +### 应用资源管理 + +#### 内存管理策略 + +| 应用状态 | 内存策略 | 清理时机 | 恢复方式 | +| ---------- | -------------- | -------- | ---------- | +| Running | 完整内存保持 | 不清理 | 无需恢复 | +| Suspended | 压缩非关键数据 | 5分钟后 | 延迟加载 | +| Background | 最小化内存占用 | 立即清理 | 重新初始化 | +| Stopped | 完全释放内存 | 立即清理 | 完整重启 | + +#### 性能监控指标 + +```mermaid +graph LR + subgraph "性能指标" + CPU[CPU使用率] + Memory[内存占用] + Network[网络请求] + Render[渲染性能] + end + + subgraph "监控阈值" + CPULimit[CPU < 30%] + MemoryLimit[内存 < 100MB] + NetworkLimit[请求 < 10/s] + RenderLimit[FPS > 30] + end + + subgraph "优化策略" + Throttle[请求节流] + Cache[智能缓存] + Lazy[懒加载] + Virtual[虚拟滚动] + end + + CPU --> CPULimit + Memory --> MemoryLimit + Network --> NetworkLimit + Render --> RenderLimit + + CPULimit --> Throttle + MemoryLimit --> Cache + NetworkLimit --> Throttle + RenderLimit --> Virtual +``` + +## 数据流架构 + +### 系统数据流 + +系统采用单向数据流架构,确保数据的一致性和可追踪性。 + +#### 数据流向图 + +```mermaid +flowchart TD + subgraph "系统层" + SystemStore[系统状态存储] + SystemEvents[系统事件总线] + SystemServices[系统服务] + end + + subgraph "应用层" + AppState[应用状态] + AppEvents[应用事件] + AppSDK[应用SDK] + end + + subgraph "界面层" + SystemUI[系统界面] + AppUI[应用界面] + end + + SystemStore --> SystemUI + SystemEvents --> SystemUI + SystemServices --> AppSDK + AppSDK --> AppState + AppState --> AppUI + AppEvents --> SystemEvents + + SystemUI --> SystemEvents + AppUI --> AppEvents +``` + +### 跨应用数据共享 + +#### 数据共享权限模型 + +```mermaid +graph TB + subgraph "数据提供者" + ProviderApp[应用A] + DataExport[导出数据接口] + Permission[权限配置] + end + + subgraph "系统中介" + DataBroker[数据代理服务] + PermissionCheck[权限验证] + DataTransform[数据转换] + end + + subgraph "数据消费者" + ConsumerApp[应用B] + DataImport[导入数据接口] + DataValidation[数据验证] + end + + ProviderApp --> DataExport + DataExport --> DataBroker + Permission --> PermissionCheck + DataBroker --> PermissionCheck + PermissionCheck --> DataTransform + DataTransform --> DataImport + DataImport --> ConsumerApp + DataImport --> DataValidation +``` + +## 部署与运维 + +### 应用分发机制 + +系统支持多种应用分发方式,确保第三方应用的便捷接入。 + +#### 应用打包规范 + +| 文件类型 | 必需性 | 描述 | 示例 | +| ------------- | ------ | ------------ | -------------------------- | +| manifest.json | 必需 | 应用清单文件 | 包含应用基本信息、权限需求 | +| index.html | 必需 | 应用入口文件 | 应用主页面 | +| assets/ | 可选 | 静态资源目录 | CSS、图片、字体等 | +| sdk/ | 可选 | SDK依赖文件 | 如需离线使用SDK | + +#### 应用安装流程 + +```mermaid +flowchart TD + Upload[上传应用包] --> Validate[验证应用包] + Validate --> Extract[解压应用文件] + Extract --> ScanSecurity[安全扫描] + ScanSecurity --> CheckPermission[权限检查] + CheckPermission --> RegisterApp[注册应用] + RegisterApp --> CreateSandbox[创建沙箱环境] + CreateSandbox --> InstallComplete[安装完成] + + Validate -->|验证失败| InstallFailed[安装失败] + ScanSecurity -->|安全风险| InstallFailed + CheckPermission -->|权限过度| UserConfirm[用户确认] + UserConfirm -->|同意| RegisterApp + UserConfirm -->|拒绝| InstallFailed +``` + +### 系统监控与日志 + +#### 监控体系架构 + +```mermaid +graph TB + subgraph "数据采集层" + AppMetrics[应用性能指标] + SystemMetrics[系统性能指标] + UserActions[用户行为数据] + ErrorLogs[错误日志] + end + + subgraph "数据处理层" + DataAggregator[数据聚合器] + AlertEngine[告警引擎] + Analytics[分析引擎] + end + + subgraph "展示层" + Dashboard[监控面板] + Reports[性能报告] + Alerts[告警通知] + end + + AppMetrics --> DataAggregator + SystemMetrics --> DataAggregator + UserActions --> Analytics + ErrorLogs --> AlertEngine + + DataAggregator --> Dashboard + Analytics --> Reports + AlertEngine --> Alerts +``` + +## 安全机制 + +### 多层安全防护 + +系统采用多层安全防护机制,确保系统和应用的安全运行。 + +#### 安全防护体系 + +| 防护层级 | 防护措施 | 防护目标 | +| -------- | ------------------ | -------- | +| 网络层 | HTTPS强制、CSP策略 | 传输安全 | +| 应用层 | 沙箱隔离、权限控制 | 运行安全 | +| 数据层 | 加密存储、访问控制 | 数据安全 | +| 用户层 | 身份验证、操作审计 | 使用安全 | + +### 权限管理系统 + +#### 权限分级模型 + +```mermaid +graph TD + subgraph "系统权限" + SysAdmin[系统管理员] + SysOperator[系统操作员] + SysUser[系统用户] + end + + subgraph "应用权限" + AppOwner[应用所有者] + AppUser[应用用户] + AppGuest[应用访客] + end + + subgraph "资源权限" + ReadOnly[只读权限] + ReadWrite[读写权限] + FullControl[完全控制] + end + + SysAdmin --> FullControl + SysOperator --> ReadWrite + SysUser --> ReadOnly + + AppOwner --> FullControl + AppUser --> ReadWrite + AppGuest --> ReadOnly +``` + +## 性能优化策略 + +### 渲染性能优化 + +#### 虚拟化渲染机制 + +```mermaid +flowchart TD + ViewportDetection[视口检测] --> VisibilityCheck{可见性检查} + VisibilityCheck -->|可见| RenderApp[渲染应用] + VisibilityCheck -->|不可见| SuspendRender[暂停渲染] + + RenderApp --> FrameOptimization[帧率优化] + FrameOptimization --> RequestAnimationFrame[RAF调度] + RequestAnimationFrame --> RenderComplete[渲染完成] + + SuspendRender --> MemoryCleanup[内存清理] + MemoryCleanup --> LowPowerMode[低功耗模式] + + RenderComplete --> ViewportDetection + LowPowerMode --> ViewportDetection +``` + +### 内存管理优化 + +#### 智能内存回收策略 + +| 回收时机 | 回收对象 | 回收策略 | 恢复方式 | +| ---------- | ------------- | ----------- | ------------ | +| 应用切换时 | 非活跃应用DOM | 延迟5秒回收 | 重新渲染 | +| 内存告警时 | 缓存数据 | LRU算法清理 | 重新请求 | +| 长时间空闲 | 应用实例 | 序列化存储 | 反序列化恢复 | +| 系统重启时 | 所有临时数据 | 立即清理 | 重新初始化 | diff --git a/.qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md b/.qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md new file mode 100644 index 0000000..b66c385 --- /dev/null +++ b/.qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md @@ -0,0 +1,204 @@ +# AppIcon组件 + + +**Referenced Files in This Document ** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue) +- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts) +- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts) +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts) + + +## 目录 +1. [简介](#简介) +2. [核心数据结构](#核心数据结构) +3. [拖拽交互机制](#拖拽交互机制) +4. [网格定位原理](#网格定位原理) +5. [事件过滤逻辑](#事件过滤逻辑) +6. [容器响应式布局](#容器响应式布局) +7. [图标重排策略](#图标重排策略) +8. [持久化存储](#持久化存储) +9. [扩展与最佳实践](#扩展与最佳实践) + +## 简介 +`AppIcon` 组件是桌面应用系统中的可交互图标单元,实现了基于HTML5 Drag API的拖拽功能。该组件通过精确的坐标计算和网格系统集成,允许用户将图标重新定位到桌面容器的任意有效网格位置。本文档深入解析其技术实现细节,涵盖从拖拽事件处理、坐标换算、网格定位到状态持久化的完整流程。 + +**Section sources** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52) + +## 核心数据结构 + +### IDesktopAppIcon 接口 +定义了桌面图标的元数据结构,包含名称、图标资源、启动路径及在网格布局中的位置坐标。 + +```typescript +export interface IDesktopAppIcon { + name: string; // 图标显示名称 + icon: string; // 图标资源路径或标识符 + path: string; // 关联的应用程序启动路径 + x: number; // 在grid布局中的列索引(从1开始) + y: number; // 在grid布局中的行索引(从1开始) +} +``` + +该设计意图明确:`x` 和 `y` 字段直接映射到CSS Grid的`grid-column`和`grid-row`属性,实现了数据驱动的UI布局。 + +**Section sources** +- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14) + +## 拖拽交互机制 + +`AppIcon` 组件利用原生HTML5 Drag API实现拖拽功能: + +1. **启用拖拽**:通过设置 `draggable="true"` 属性激活元素的拖拽能力。 +2. **事件监听**: + - `@dragstart`:拖拽开始时触发,当前实现为空函数,保留未来扩展空间。 + - `@dragend`:拖拽结束时触发,执行核心的坐标计算与位置更新逻辑。 + +此机制无需依赖第三方库,轻量且兼容性好。 + +**Section sources** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L2-L8) + +## 网格定位原理 + +### 动态样式绑定 +组件通过内联样式动态设置其在CSS Grid中的位置: +```vue +:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1}; grid-row: ${iconInfo.y}/${iconInfo.y + 1};`" +``` +此表达式将 `iconInfo.x` 和 `iconInfo.y` 的值转换为Grid的列/行跨度声明,确保图标精准占据一个网格单元。 + +### 坐标换算过程 +`onDragEnd` 事件处理器的核心任务是将鼠标绝对坐标转换为相对网格坐标: + +1. **获取容器边界**:使用 `getBoundingClientRect()` 获取父容器的绝对位置和尺寸。 +2. **计算相对坐标**:通过 `e.clientX - rect.left` 和 `e.clientY - rect.top` 得到鼠标相对于容器左上角的偏移量。 +3. **确定目标网格**:利用 `cellRealWidth` 和 `cellRealHeight` 进行除法运算并向上取整(`Math.ceil`),得到目标网格的行列索引。 + +```mermaid +flowchart TD +A[拖拽结束] --> B{获取鼠标
clientX/clientY} +B --> C{获取容器
getBoundingClientRect} +C --> D[计算鼠标相对
容器坐标] +D --> E[用cellRealWidth/Height
计算网格索引] +E --> F[更新iconInfo.x/y] +F --> G[触发UI重渲染] +``` + +**Diagram sources ** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L10-L38) + +**Section sources** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L10-L38) + +## 事件过滤逻辑 + +为了防止图标被错误地放置在其他图标之上,`onDragEnd` 方法实现了关键的事件过滤: + +1. 使用 `document.elementFromPoint(e.clientX, e.clientY)` 检测鼠标终点位置的DOM元素。 +2. 如果该元素是另一个 `.icon-container`,则立即返回,不执行任何位置更新。 + +此逻辑确保了“空位投放”原则,提升了用户体验,避免了图标的视觉重叠。 + +**Section sources** +- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L13-L17) + +## 容器响应式布局 + +### IGridTemplateParams 接口 +该接口定义了网格系统的动态参数,是实现响应式布局的关键。 + +```typescript +export interface IGridTemplateParams { + cellExpectWidth: number; // 单元格预设宽度 + cellExpectHeight: number; // 单元格预设高度 + cellRealWidth: number; // 单元格实际宽度 + cellRealHeight: number; // 单元格实际高度 + gapX: number; // 列间距 + gapY: number; // 行间距 + colCount: number; // 总列数 + rowCount: number; // 总行数 +} +``` + +### 实际尺寸计算 +`useDesktopContainerInit` 函数通过 `ResizeObserver` 监听容器尺寸变化,并动态计算 `cellRealWidth` 和 `cellRealHeight`: + +```javascript +const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1)); +gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2)); +``` + +此计算考虑了所有列间距的总和,确保了网格单元的实际尺寸能完美填充容器,无边距误差。 + +**Section sources** +- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L3-L20) +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L30-L38) + +## 图标重排策略 + +当网格的行列数发生变化时,`rearrangeIcons` 函数负责智能地重新分配图标位置: + +1. **优先保留原位**:遍历所有图标,若其原位置在新网格范围内且未被占用,则保留在原位。 +2. **寻找空位**:对于超出范围或位置冲突的图标,从 `(1,1)` 开始扫描,为其寻找第一个可用的空网格。 +3. **隐藏溢出图标**:如果所有网格均被占用,则将无法安置的图标放入 `hideAppIcons` 数组。 + +此策略保证了用户自定义布局的最大程度保留,同时优雅地处理了空间不足的情况。 + +```mermaid +sequenceDiagram +participant DC as DesktopContainer +participant UDCI as useDesktopContainerInit +participant RI as rearrangeIcons +DC->>UDCI : 监听gridTemplate变化 +UDCI->>RI : 调用rearrangeIcons() +loop 遍历每个图标 +RI->>RI : 检查是否在新网格内且位置空闲 +alt 是 +RI->>RI : 保留在原位 +else 否 +RI->>RI : 加入临时队列 +end +end +loop 处理临时队列 +RI->>RI : 扫描网格寻找空位 +alt 找到空位 +RI->>RI : 分配新位置 +else 无空位 +RI->>RI : 加入hideAppIcons +end +end +RI-->>UDCI : 返回appIcons和hideAppIcons +UDCI->>DC : 更新appIconsRef +``` + +**Diagram sources ** +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L77-L80) +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156) + +**Section sources** +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L77-L80) +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156) + +## 持久化存储 + +用户的图标布局偏好通过 `localStorage` 实现持久化: + +1. **写入**:使用 `watch` 监听 `appIconsRef.value` 的变化,一旦有更新,立即将整个数组序列化为JSON字符串并存入 `localStorage` 键名为 `'desktopAppIconInfo'` 的条目中。 +2. **读取**:在初始化时,尝试从 `localStorage` 中读取 `'desktopAppIconInfo'`,若存在则使用存储的位置信息覆盖默认布局。 + +这确保了用户关闭页面后再次打开时,仍能看到上次调整好的桌面布局。 + +**Section sources** +- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L88-L90) + +## 扩展与最佳实践 + +### 自定义图标样式 +可通过修改 ` \ No newline at end of file diff --git a/src/apps/components/BuiltInApp.vue b/src/apps/components/BuiltInApp.vue new file mode 100644 index 0000000..c09056c --- /dev/null +++ b/src/apps/components/BuiltInApp.vue @@ -0,0 +1,125 @@ + + + + + \ No newline at end of file diff --git a/src/apps/index.ts b/src/apps/index.ts new file mode 100644 index 0000000..41bec2b --- /dev/null +++ b/src/apps/index.ts @@ -0,0 +1,90 @@ +import { appRegistry } from './AppRegistry' +import { markRaw } from 'vue' +import Calculator from './calculator/Calculator.vue' +import Notepad from './notepad/Notepad.vue' +import Todo from './todo/Todo.vue' + +/** + * 注册所有内置应用 + */ +export function registerBuiltInApps() { + // 注册计算器应用 + appRegistry.registerApp({ + manifest: { + id: 'calculator', + name: '计算器', + version: '1.0.0', + description: '简单而功能强大的计算器,支持基本数学运算', + author: 'System', + icon: '🧮', + permissions: ['storage'], + window: { + width: 400, + height: 600, + minWidth: 320, + minHeight: 480, + resizable: true, + minimizable: true, + maximizable: false + }, + category: 'utilities', + keywords: ['计算器', '数学', '运算', 'calculator', 'math'] + }, + component: markRaw(Calculator), + isBuiltIn: true + }) + + // 注册记事本应用 + appRegistry.registerApp({ + manifest: { + id: 'notepad', + name: '记事本', + version: '1.0.0', + description: '功能丰富的文本编辑器,支持文件管理和多种编辑选项', + author: 'System', + icon: '📝', + permissions: ['storage', 'notification'], + window: { + width: 800, + height: 600, + minWidth: 400, + minHeight: 300, + resizable: true + }, + category: 'productivity', + keywords: ['记事本', '文本编辑', '笔记', 'notepad', 'text', 'editor'] + }, + component: markRaw(Notepad), + isBuiltIn: true + }) + + // 注册待办事项应用 + appRegistry.registerApp({ + manifest: { + id: 'todo', + name: '待办事项', + version: '1.0.0', + description: '高效的任务管理工具,帮助您组织和跟踪日常任务', + author: 'System', + icon: '✅', + permissions: ['storage', 'notification'], + window: { + width: 600, + height: 700, + minWidth: 400, + minHeight: 500, + resizable: true + }, + category: 'productivity', + keywords: ['待办事项', '任务管理', 'todo', 'task', 'productivity'] + }, + component: markRaw(Todo), + isBuiltIn: true + }) + + console.log('内置应用注册完成') +} + +// 导出应用注册中心 +export { appRegistry } from './AppRegistry' +export type { InternalAppManifest, AppRegistration } from './types/AppManifest' \ No newline at end of file diff --git a/src/apps/notepad/Notepad.vue b/src/apps/notepad/Notepad.vue new file mode 100644 index 0000000..84070e2 --- /dev/null +++ b/src/apps/notepad/Notepad.vue @@ -0,0 +1,527 @@ + + + + + \ No newline at end of file diff --git a/src/apps/todo/Todo.vue b/src/apps/todo/Todo.vue new file mode 100644 index 0000000..8846c07 --- /dev/null +++ b/src/apps/todo/Todo.vue @@ -0,0 +1,658 @@ + + + + + \ No newline at end of file diff --git a/src/apps/types/AppManifest.ts b/src/apps/types/AppManifest.ts new file mode 100644 index 0000000..480fe6a --- /dev/null +++ b/src/apps/types/AppManifest.ts @@ -0,0 +1,35 @@ +/** + * 内置应用清单接口 + */ +export interface InternalAppManifest { + id: string + name: string + version: string + description: string + author: string + icon: string + permissions: string[] + window: { + width: number + height: number + minWidth?: number + minHeight?: number + maxWidth?: number + maxHeight?: number + resizable?: boolean + minimizable?: boolean + maximizable?: boolean + closable?: boolean + } + category?: string + keywords?: string[] +} + +/** + * 应用注册信息 + */ +export interface AppRegistration { + manifest: InternalAppManifest + component: any // Vue组件 + isBuiltIn: boolean +} \ No newline at end of file diff --git a/src/core/XSystem.ts b/src/core/XSystem.ts deleted file mode 100644 index f681c42..0000000 --- a/src/core/XSystem.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts' -import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts' -import { NotificationService } from '@/core/service/services/NotificationService.ts' -import { SettingsService } from '@/core/service/services/SettingsService.ts' -import { WindowFormService } from '@/core/service/services/WindowFormService.ts' -import { UserService } from '@/core/service/services/UserService.ts' -import { processManager } from '@/core/process/ProcessManager.ts' - -export default class XSystem { - private static _instance: XSystem = new XSystem() - - private _desktopRootDom: HTMLElement; - - constructor() { - console.log('XSystem') - new NotificationService() - new SettingsService() - new WindowFormService() - new UserService() - } - - public static get instance() { - return this._instance - } - public get desktopRootDom() { - return this._desktopRootDom - } - - public async initialization(dom: HTMLDivElement) { - this._desktopRootDom = dom - await processManager.runProcess('basic-system', BasicSystemProcess) - await processManager.runProcess('desktop', DesktopProcess, dom) - } -} diff --git a/src/core/apps/department/app.json b/src/core/apps/department/app.json deleted file mode 100644 index b8f6d65..0000000 --- a/src/core/apps/department/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "department", - "title": "部门", - "description": "部门", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "部门", - "icon": "iconfont icon-setting", - "width": 200, - "height": 100 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/department/main.vue b/src/core/apps/department/main.vue deleted file mode 100644 index 85a7057..0000000 --- a/src/core/apps/department/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/fileManage/app.json b/src/core/apps/fileManage/app.json deleted file mode 100644 index 87d6b4f..0000000 --- a/src/core/apps/fileManage/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "fileManage", - "title": "文件管理", - "description": "文件管理", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "文件管理", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/fileManage/main.vue b/src/core/apps/fileManage/main.vue deleted file mode 100644 index b5a9436..0000000 --- a/src/core/apps/fileManage/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/music/app.json b/src/core/apps/music/app.json deleted file mode 100644 index 5093f01..0000000 --- a/src/core/apps/music/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "music", - "title": "音乐", - "description": "音乐", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "音乐", - "icon": "iconfont icon-setting", - "width": 200, - "height": 100 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/music/main.vue b/src/core/apps/music/main.vue deleted file mode 100644 index c3eceec..0000000 --- a/src/core/apps/music/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/personalCenter/app.json b/src/core/apps/personalCenter/app.json deleted file mode 100644 index 9c2fdc3..0000000 --- a/src/core/apps/personalCenter/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "personalCenter", - "title": "个人中心", - "description": "个人中心", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "个人中心", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/personalCenter/main.vue b/src/core/apps/personalCenter/main.vue deleted file mode 100644 index 9fa9894..0000000 --- a/src/core/apps/personalCenter/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/photograph/app.json b/src/core/apps/photograph/app.json deleted file mode 100644 index 9c8d9ca..0000000 --- a/src/core/apps/photograph/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "photograph", - "title": "照片", - "description": "照片", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "照片", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/photograph/main.vue b/src/core/apps/photograph/main.vue deleted file mode 100644 index 6856d83..0000000 --- a/src/core/apps/photograph/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/recycleBin/app.json b/src/core/apps/recycleBin/app.json deleted file mode 100644 index 06da9f8..0000000 --- a/src/core/apps/recycleBin/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "recycleBin", - "title": "回收站", - "description": "回收站", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "回收站", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/recycleBin/main.vue b/src/core/apps/recycleBin/main.vue deleted file mode 100644 index b1d7a5a..0000000 --- a/src/core/apps/recycleBin/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/setting/app.json b/src/core/apps/setting/app.json deleted file mode 100644 index d776a7c..0000000 --- a/src/core/apps/setting/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "setting", - "title": "设置", - "description": "设置", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "设置", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/setting/main.vue b/src/core/apps/setting/main.vue deleted file mode 100644 index 2bba39b..0000000 --- a/src/core/apps/setting/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/tv/app.json b/src/core/apps/tv/app.json deleted file mode 100644 index 0a24f7c..0000000 --- a/src/core/apps/tv/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "tv", - "title": "电视", - "description": "电视", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "电视", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/tv/main.vue b/src/core/apps/tv/main.vue deleted file mode 100644 index 07b6d59..0000000 --- a/src/core/apps/tv/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/apps/video/app.json b/src/core/apps/video/app.json deleted file mode 100644 index 22e1777..0000000 --- a/src/core/apps/video/app.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "video", - "title": "电影", - "description": "电影", - "icon": "iconfont icon-setting", - "startName": "main", - "singleton": true, - "isJustProcess": false, - "windowFormConfigs": [ - { - "name": "main", - "title": "电影", - "icon": "iconfont icon-setting", - "width": 800, - "height": 600 - } - ] -} \ No newline at end of file diff --git a/src/core/apps/video/main.vue b/src/core/apps/video/main.vue deleted file mode 100644 index 54f1d78..0000000 --- a/src/core/apps/video/main.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/src/core/common/hooks/useObservableVue.ts b/src/core/common/hooks/useObservableVue.ts deleted file mode 100644 index 9ca6458..0000000 --- a/src/core/common/hooks/useObservableVue.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { reactive, onBeforeUnmount, type Reactive } from 'vue' -import type { IObservable } from '@/core/state/IObservable.ts' - -/** - * Vue Hook: useObservable - * 支持深层解构赋值,直接修改触发 ObservableImpl 通知 + Vue 响应式更新 - * @example - * interface AppState { - * count: number - * user: { name: string; age: number } - * items: number[] - * } - * - * // 创建 ObservableImpl - * const obs = new ObservableImpl({ - * count: 0, - * user: { name: 'Alice', age: 20 }, - * items: [] - * }) - * - * export default defineComponent({ - * setup() { - * // 深层解构 Hook - * const { count, user, items } = useObservable(obs) - * - * const increment = () => { - * count += 1 // 触发 ObservableImpl 通知 + Vue 更新 - * } - * - * const changeAge = () => { - * user.age = 30 // 深层对象也能触发通知 - * } - * - * const addItem = () => { - * items.push(42) // 数组方法拦截,触发通知 - * } - * - * return { count, user, items, increment, changeAge, addItem } - * } - * }) - */ -export function useObservable(observable: IObservable): Reactive { - // 创建 Vue 响应式对象 - const state = reactive({} as T) - - /** - * 将 ObservableImpl Proxy 映射到 Vue 响应式对象 - * 递归支持深层对象 - */ - function mapKeys(obj: any, proxy: any) { - (Object.keys(proxy) as (keyof typeof proxy)[]).forEach(key => { - const value = proxy[key] - if (typeof value === 'object' && value !== null) { - // 递归创建子对象 Proxy - obj[key] = reactive({} as typeof value) - mapKeys(obj[key], value) - } else { - // 基本类型通过 getter/setter 同步 - Object.defineProperty(obj, key, { - enumerable: true, - configurable: true, - get() { - return proxy[key] - }, - set(val) { - proxy[key] = val - }, - }) - } - }) - } - - // 获取 ObservableImpl 的 Proxy - const refsProxy = observable.toRefsProxy() - mapKeys(state, refsProxy) - - // 订阅 ObservableImpl,保持响应式同步 - const unsubscribe = observable.subscribe(() => { - // 空实现即可,getter/setter 已同步 - }) - - onBeforeUnmount(() => { - unsubscribe() - }) - - return state -} diff --git a/src/core/common/naive-ui/components.ts b/src/core/common/naive-ui/components.ts deleted file mode 100644 index 3623052..0000000 --- a/src/core/common/naive-ui/components.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - create, - NButton, - NCard, - NConfigProvider, -} from 'naive-ui' - -export const naiveUi = create({ - components: [NButton, NCard, NConfigProvider] -}) diff --git a/src/core/common/naive-ui/discrete-api.ts b/src/core/common/naive-ui/discrete-api.ts deleted file mode 100644 index 6365dd7..0000000 --- a/src/core/common/naive-ui/discrete-api.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createDiscreteApi } from 'naive-ui' -import { configProviderProps } from './theme.ts' - -const { message, notification, dialog, loadingBar, modal } = createDiscreteApi( - ['message', 'dialog', 'notification', 'loadingBar', 'modal'], - { - configProviderProps: configProviderProps, - notificationProviderProps: { - placement: 'bottom-right', - max: 3 - } - } -) - -export const { messageApi, notificationApi, dialogApi, loadingBarApi, modalApi } = { - messageApi: message, - notificationApi: notification, - dialogApi: dialog, - loadingBarApi: loadingBar, - modalApi: modal -} diff --git a/src/core/common/naive-ui/theme.ts b/src/core/common/naive-ui/theme.ts deleted file mode 100644 index ab270f6..0000000 --- a/src/core/common/naive-ui/theme.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type ConfigProviderProps, darkTheme, dateZhCN, type GlobalTheme, lightTheme, zhCN } from 'naive-ui' - -const lTheme: GlobalTheme = { - ...lightTheme, - common: { - ...lightTheme.common, - primaryColor: '#0070f3' - } -} - -export const configProviderProps: ConfigProviderProps = { - theme: lTheme, - dateLocale: dateZhCN, - locale: zhCN, -} diff --git a/src/core/common/types/IDestroyable.ts b/src/core/common/types/IDestroyable.ts deleted file mode 100644 index f069e4d..0000000 --- a/src/core/common/types/IDestroyable.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * 可销毁接口 - * 销毁实例,清理副作用,让内存可以被回收 - */ -export interface IDestroyable { - /** 销毁实例,清理副作用,让内存可以被回收 */ - destroy(): void -} diff --git a/src/core/common/types/IVersion.ts b/src/core/common/types/IVersion.ts deleted file mode 100644 index dfc88d3..0000000 --- a/src/core/common/types/IVersion.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 版本信息 - */ -export interface IVersion { - /** - * 公司名称 - */ - company: string - - /** - * 版本号 - */ - major: number - - /** - * 子版本号 - */ - minor: number - - /** - * 修订号 - */ - build: number - - /** - * 私有版本号 - */ - private: number -} diff --git a/src/core/desktop/DesktopProcess.ts b/src/core/desktop/DesktopProcess.ts deleted file mode 100644 index 2a173c7..0000000 --- a/src/core/desktop/DesktopProcess.ts +++ /dev/null @@ -1,82 +0,0 @@ -import ProcessImpl from '@/core/process/impl/ProcessImpl.ts' -import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts' -import { createApp, h } from 'vue' -import DesktopComponent from '@/core/desktop/ui/DesktopComponent.vue' -import { naiveUi } from '@/core/common/naive-ui/components.ts' -import { debounce } from 'lodash' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' -import { processManager } from '@/core/process/ProcessManager.ts' -import './ui/DesktopElement.ts' -import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts' - -interface IDesktopDataState { - /** 显示器宽度 */ - monitorWidth: number; - /** 显示器高度 */ - monitorHeight: number; -} - -export class DesktopProcess extends ProcessImpl { - /** 桌面根dom,类似显示器 */ - private readonly _monitorDom: HTMLElement - private _isMounted: boolean = false - private _data = new ObservableImpl({ - monitorWidth: 0, - monitorHeight: 0, - }) - - public get monitorDom() { - return this._monitorDom - } - public get isMounted() { - return this._isMounted - } - public get basicSystemProcess() { - return processManager.findProcessByName('basic-system') - } - - get data() { - return this._data - } - - constructor(info: IProcessInfo, dom: HTMLDivElement) { - super(info) - console.log('DesktopProcess') - dom.style.position = 'relative' - dom.style.overflow = 'hidden' - dom.style.width = `${window.innerWidth}px` - dom.style.height = `${window.innerHeight}px` - - this._monitorDom = dom - this._data.state.monitorWidth = window.innerWidth - this._data.state.monitorHeight = window.innerHeight - window.addEventListener('resize', this.onResize) - - this.createDesktopUI() - } - - private onResize = debounce(() => { - this._monitorDom.style.width = `${window.innerWidth}px` - this._monitorDom.style.height = `${window.innerHeight}px` - this._data.state.monitorWidth = window.innerWidth - this._data.state.monitorHeight = window.innerHeight - }, 300) - - private createDesktopUI() { - if (this._isMounted) return - const app = createApp(DesktopComponent, { process: this }) - app.use(naiveUi) - app.mount(this._monitorDom) - this._isMounted = true - } - - private initDesktop(dom: HTMLDivElement) { - const d = document.createElement('desktop-element') - dom.appendChild(d) - } - - override destroy() { - super.destroy() - window.removeEventListener('resize', this.onResize) - } -} \ No newline at end of file diff --git a/src/core/desktop/DesktopProcessInfo.ts b/src/core/desktop/DesktopProcessInfo.ts deleted file mode 100644 index 4c4d731..0000000 --- a/src/core/desktop/DesktopProcessInfo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' - -export const DesktopProcessInfo = new ProcessInfoImpl({ - name: 'desktop', - title: '桌面', - version: { - company: 'XZG', - major: 1, - minor: 0, - build: 0, - private: 0 - }, - singleton: true, - isJustProcess: true -}) diff --git a/src/core/desktop/types/IDesktopAppIcon.ts b/src/core/desktop/types/IDesktopAppIcon.ts deleted file mode 100644 index 5550409..0000000 --- a/src/core/desktop/types/IDesktopAppIcon.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * 桌面应用图标信息 - */ -export interface IDesktopAppIcon { - /** 图标name */ - name: string; - /** 图标 */ - icon: string; - /** 图标路径 */ - path: string; - /** 图标在grid布局中的列 */ - x: number; - /** 图标在grid布局中的行 */ - y: number; -} diff --git a/src/core/desktop/types/IGridTemplateParams.ts b/src/core/desktop/types/IGridTemplateParams.ts deleted file mode 100644 index ccda108..0000000 --- a/src/core/desktop/types/IGridTemplateParams.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * 桌面网格模板参数 - */ -export interface IGridTemplateParams { - /** 单元格预设宽度 */ - readonly cellExpectWidth: number - /** 单元格预设高度 */ - readonly cellExpectHeight: number - /** 单元格实际宽度 */ - cellRealWidth: number - /** 单元格实际高度 */ - cellRealHeight: number - /** 列间距 */ - gapX: number - /** 行间距 */ - gapY: number - /** 总列数 */ - colCount: number - /** 总行数 */ - rowCount: number -} \ No newline at end of file diff --git a/src/core/desktop/ui/DesktopComponent.vue b/src/core/desktop/ui/DesktopComponent.vue deleted file mode 100644 index 76f32b9..0000000 --- a/src/core/desktop/ui/DesktopComponent.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - diff --git a/src/core/desktop/ui/DesktopElement.ts b/src/core/desktop/ui/DesktopElement.ts deleted file mode 100644 index b5a7c23..0000000 --- a/src/core/desktop/ui/DesktopElement.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { css, html, LitElement, unsafeCSS } from 'lit' -import { customElement } from 'lit/decorators.js' -import desktopStyle from './css/desktop.scss?inline' - -@customElement('desktop-element') -export class DesktopElement extends LitElement { - static override styles = css` - ${unsafeCSS(desktopStyle)} - ` - - private onContextMenu = (e: MouseEvent) => { - e.preventDefault() - e.stopPropagation() - console.log('contextmenu') - } - - override render() { - return html` -
-
-
- -
-
-
-
测试
-
-
- ` - } -} \ No newline at end of file diff --git a/src/core/desktop/ui/components/AppIcon.vue b/src/core/desktop/ui/components/AppIcon.vue deleted file mode 100644 index 6d0c0f0..0000000 --- a/src/core/desktop/ui/components/AppIcon.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/src/core/desktop/ui/components/DesktopAppIconElement.ts b/src/core/desktop/ui/components/DesktopAppIconElement.ts deleted file mode 100644 index 6c5ecf4..0000000 --- a/src/core/desktop/ui/components/DesktopAppIconElement.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { css, html, LitElement } from 'lit' - -export class DesktopAppIconElement extends LitElement { - static override styles = css` - :host { - width: 100%; - height: 100%; - @apply flex flex-col items-center justify-center bg-gray-200; - } - ` - - override render() { - return html`
- -
` - } -} \ No newline at end of file diff --git a/src/core/desktop/ui/css/desktop.scss b/src/core/desktop/ui/css/desktop.scss deleted file mode 100644 index 213ee41..0000000 --- a/src/core/desktop/ui/css/desktop.scss +++ /dev/null @@ -1,31 +0,0 @@ -*, -*::before, -*::after { - box-sizing: border-box; /* 使用更直观的盒模型 */ - margin: 0; - padding: 0; -} - -$taskBarHeight: 40px; - -.desktop-root { - @apply w-full h-full flex flex-col; - - .desktop-container { - @apply w-full h-full flex-1 p-2 pos-relative; - background-image: url("../imgs/desktop-bg-2.jpeg"); - background-repeat: no-repeat; - background-size: cover; - height: calc(100% - #{$taskBarHeight}); - } - - .desktop-icons-container { - @apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative; - } - - .task-bar { - @apply w-full bg-gray-200 flex justify-center items-center; - height: $taskBarHeight; - flex-shrink: 0; - } -} \ No newline at end of file diff --git a/src/core/desktop/ui/hooks/useDesktopInit.ts b/src/core/desktop/ui/hooks/useDesktopInit.ts deleted file mode 100644 index 3cca5bc..0000000 --- a/src/core/desktop/ui/hooks/useDesktopInit.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts' -import { - computed, - nextTick, - onMounted, - onUnmounted, - reactive, - ref, - toRaw, - toRefs, - toValue, - useTemplateRef, - watch, - watchEffect, -} from 'vue' -import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts' -import { eventManager } from '@/core/events/EventManager.ts' -import { processManager } from '@/core/process/ProcessManager.ts' - -export function useDesktopInit(containerStr: string) { - let container:HTMLElement - // 初始值 - const gridTemplate = reactive({ - cellExpectWidth: 90, - cellExpectHeight: 110, - cellRealWidth: 90, - cellRealHeight: 110, - gapX: 4, - gapY: 4, - colCount: 1, - rowCount: 1 - }) - - const ro = new ResizeObserver(entries => { - const entry= entries[0] - const containerRect = entry.contentRect - gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX)); - gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY)); - - const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1)) - const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1)) - gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2)) - gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2)) - }) - - const gridStyle = computed(() => ({ - gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`, - gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`, - gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px` - })) - - onMounted(() => { - container = document.querySelector(containerStr)! - ro.observe(container) - }) - onUnmounted(() => { - ro.unobserve(container) - ro.disconnect() - }) - - // 有桌面图标的app - const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess) - const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]') - const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => { - const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name) - - // 左上角坐标原点,从上到下从左到右 索引从1开始 - const x = index % gridTemplate.rowCount + 1 - const y = Math.floor(index / gridTemplate.rowCount) + 1 - - return { - name: processInfo.name, - icon: processInfo.icon, - path: processInfo.startName, - x: oldAppIcon ? oldAppIcon.x : x, - y: oldAppIcon ? oldAppIcon.y : y - } - }) - - const appIconsRef = ref(appIcons) - const exceedApp = ref([]) - - watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => { - // if (oCols == 1 && oRows == 1) return - if (oCols === nCols && oRows === nRows) return - const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows) - appIconsRef.value = appIcons - exceedApp.value = hideAppIcons - }) - - eventManager.addEventListener('onDesktopAppIconPos', (iconInfo) => { - localStorage.setItem('desktopAppIconInfo', JSON.stringify(toValue(appIconsRef.value))) - }) - - return { - gridTemplate, - appIconsRef, - gridStyle - } -} - -/** - * 重新安排图标位置 - * @param appIconInfos 图标信息 - * @param maxCol 列数 - * @param maxRow 行数 - */ -function rearrangeIcons( - appIconInfos: IDesktopAppIcon[], - maxCol: number, - maxRow: number -): IRearrangeInfo { - const occupied = new Set(); - - function key(x: number, y: number) { - return `${x},${y}`; - } - - const appIcons: IDesktopAppIcon[] = [] - const hideAppIcons: IDesktopAppIcon[] = [] - const temp: IDesktopAppIcon[] = [] - - for (const appIcon of appIconInfos) { - const { x, y } = appIcon; - - if (x <= maxCol && y <= maxRow) { - if (!occupied.has(key(x, y))) { - occupied.add(key(x, y)) - appIcons.push({ ...appIcon, x, y }) - } - } else { - temp.push(appIcon) - } - } - - const max = maxCol * maxRow - for (const appIcon of temp) { - if (appIcons.length < max) { - // 最后格子也被占 → 从 (1,1) 开始找空位 - let placed = false; - for (let c = 1; c <= maxCol; c++) { - for (let r = 1; r <= maxRow; r++) { - if (!occupied.has(key(c, r))) { - occupied.add(key(c, r)); - appIcons.push({ ...appIcon, x: c, y: r }); - placed = true; - break; - } - } - if (placed) break; - } - } else { - // 放不下了 - hideAppIcons.push(appIcon) - } - } - - return { - appIcons, - hideAppIcons - }; -} - -interface IRearrangeInfo { - /** 正常的桌面图标信息 */ - appIcons: IDesktopAppIcon[]; - /** 隐藏的桌面图标信息(超出屏幕显示的) */ - hideAppIcons: IDesktopAppIcon[]; -} diff --git a/src/core/desktop/ui/imgs/desktop-bg-1.jpeg b/src/core/desktop/ui/imgs/desktop-bg-1.jpeg deleted file mode 100644 index 719e22c..0000000 Binary files a/src/core/desktop/ui/imgs/desktop-bg-1.jpeg and /dev/null differ diff --git a/src/core/desktop/ui/imgs/desktop-bg-2.jpeg b/src/core/desktop/ui/imgs/desktop-bg-2.jpeg deleted file mode 100644 index 4e0e791..0000000 Binary files a/src/core/desktop/ui/imgs/desktop-bg-2.jpeg and /dev/null differ diff --git a/src/core/desktop/utils/useIconDrag.ts b/src/core/desktop/utils/useIconDrag.ts deleted file mode 100644 index 4976abe..0000000 --- a/src/core/desktop/utils/useIconDrag.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useDraggable } from '@vueuse/core' -import { ref } from 'vue' - -export function useIconDrag(el: HTMLElement, container: HTMLElement) { - let offsetX = 0 - let offsetY = 0 - let containerRect = container.getBoundingClientRect() - - el.addEventListener('mousedown', (e) => { - el.classList.add('dragging') - - let rect = el.getBoundingClientRect() - console.log(rect) - offsetX = e.clientX - rect.left - offsetY = e.clientY - rect.top - - // 临时脱离 grid,用绝对定位移动 - el.style.position = "absolute"; - el.style.left = rect.left - containerRect.left + "px"; - el.style.top = rect.top - containerRect.top + "px"; - el.style.gridRow = "auto"; - el.style.gridColumn = "auto"; - - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); - }) - - function onMouseMove(e: MouseEvent) { - if (!el) return; - el.style.left = e.clientX - containerRect.left - offsetX + "px"; - el.style.top = e.clientY - containerRect.top - offsetY + "px"; - } - - function onMouseUp(e: MouseEvent) { - if (!el) return; - - const cellWidth = 90 + 16; // 图标宽度 + gap - const cellHeight = 110 + 16; - - // 计算所在行列 - let col = Math.round((e.clientX - containerRect.left) / cellWidth) + 1; - let row = Math.round((e.clientY - containerRect.top) / cellHeight) + 1; - - // 限制在 grid 内 - const maxCols = Math.floor(containerRect.width / cellWidth); - const maxRows = Math.floor(containerRect.height / cellHeight); - col = Math.max(1, Math.min(maxCols, col)); - row = Math.max(1, Math.min(maxRows, row)); - - console.log(col, row) - - // 放回 grid - el.style.position = "relative"; - el.style.left = ""; - el.style.top = ""; - el.style.gridRow = `${row}`; - el.style.gridColumn = `${col}`; - - el.classList.remove("dragging"); - - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); - } -} \ No newline at end of file diff --git a/src/core/events/EventManager.ts b/src/core/events/EventManager.ts deleted file mode 100644 index 6b63a22..0000000 --- a/src/core/events/EventManager.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' -import type { IEventMap } from '@/core/events/IEventBuilder.ts' -import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts' - -export const eventManager = new EventBuilderImpl() - -/** - * 系统进程的事件 - * @description - *

onAuthChange - 认证状态改变

- *

onThemeChange - 主题改变

- */ -export interface IBasicSystemEvent extends IEventMap { - /** 认证状态改变 */ - onAuthChange: () => {}, - /** 主题改变 */ - onThemeChange: (theme: string) => void -} - -/** - * 桌面进程的事件 - * @description - *

onDesktopRootDomResize - 桌面根dom尺寸改变

- *

onDesktopProcessInitialize - 桌面进程初始化完成

- */ -export interface IDesktopEvent extends IEventMap { - /** 桌面根dom尺寸改变 */ - onDesktopRootDomResize: (width: number, height: number) => void - /** 桌面进程初始化完成 */ - onDesktopProcessInitialize: () => void - /** 桌面应用图标位置改变 */ - onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void -} - -export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {} diff --git a/src/core/events/IEventBuilder.ts b/src/core/events/IEventBuilder.ts deleted file mode 100644 index f29cbe0..0000000 --- a/src/core/events/IEventBuilder.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IDestroyable } from '@/core/common/types/IDestroyable.ts' - -/** - * 事件定义 - * @interface IEventMap 事件定义 键是事件名称,值是事件处理函数 - */ -export interface IEventMap { - [key: string]: (...args: any[]) => void -} - -/** - * 事件管理器接口定义 - */ -export interface IEventBuilder extends IDestroyable { - /** - * 添加事件监听 - * @param eventName 事件名称 - * @param handler 事件处理函数 - * @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 } - * @returns void - */ - addEventListener( - eventName: E, - handler: F, - options?: { - immediate?: boolean - immediateArgs?: Parameters - once?: boolean - }, - ): void - - /** - * 移除事件监听 - * @param eventName 事件名称 - * @param handler 事件处理函数 - * @returns void - */ - removeEventListener(eventName: E, handler: F): void - - /** - * 触发事件 - * @param eventName 事件名称 - * @param args 参数 - * @returns void - */ - notifyEvent(eventName: E, ...args: Parameters): void -} \ No newline at end of file diff --git a/src/core/events/WindowFormEventManager.ts b/src/core/events/WindowFormEventManager.ts deleted file mode 100644 index d4b144d..0000000 --- a/src/core/events/WindowFormEventManager.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' -import type { IEventMap } from '@/core/events/IEventBuilder.ts' -import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' - -/** - * 窗口的事件 - */ -export interface WindowFormEvent extends IEventMap { - /** - * 窗口最小化 - * @param id 窗口id - */ - windowFormMinimize: (id: string) => void; - /** - * 窗口最大化 - * @param id 窗口id - */ - windowFormMaximize: (id: string) => void; - /** - * 窗口还原 - * @param id 窗口id - */ - windowFormRestore: (id: string) => void; - /** - * 窗口关闭 - * @param id 窗口id - */ - windowFormClose: (id: string) => void; - /** - * 窗口聚焦 - * @param id 窗口id - */ - windowFormFocus: (id: string) => void; - /** - * 窗口数据更新 - * @param data 窗口数据 - */ - windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void; - /** - * 窗口创建完成 - */ - windowFormCreated: () => void; -} - -interface IWindowFormDataUpdateParams { - /** 窗口id */ - id: string; - /** 窗口状态 */ - state: TWindowFormState, - /** 窗口宽度 */ - width: number, - /** 窗口高度 */ - height: number, - /** 窗口x坐标(左上角) */ - x: number, - /** 窗口y坐标(左上角) */ - y: number -} - -/** 窗口事件管理器 */ -export const wfem = new EventBuilderImpl() diff --git a/src/core/events/impl/EventBuilderImpl.ts b/src/core/events/impl/EventBuilderImpl.ts deleted file mode 100644 index 2c29425..0000000 --- a/src/core/events/impl/EventBuilderImpl.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts' - -interface HandlerWrapper any> { - fn: T - once: boolean -} - -export class EventBuilderImpl implements IEventBuilder { - private _eventHandlers = new Map>>() - - /** - * 添加事件监听器 - * @param eventName 事件名称 - * @param handler 监听器 - * @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 } - * @example - * eventBus.addEventListener('noArgs', () => {}) - * eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] }) - * eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] }) - */ - addEventListener( - eventName: E, - handler: F, - options?: { - immediate?: boolean - immediateArgs?: Parameters - once?: boolean - }, - ) { - if (!handler) return - if (!this._eventHandlers.has(eventName)) { - this._eventHandlers.set(eventName, new Set>()) - } - - const set = this._eventHandlers.get(eventName)! - if (![...set].some((wrapper) => wrapper.fn === handler)) { - set.add({ fn: handler, once: options?.once ?? false }) - } - - if (options?.immediate) { - try { - handler(...(options.immediateArgs ?? [])) - } catch (e) { - console.error(e) - } - } - } - - /** - * 移除事件监听器 - * @param eventName 事件名称 - * @param handler 监听器 - * @example - * eventBus.removeEventListener('noArgs', () => {}) - */ - removeEventListener(eventName: E, handler: F) { - const set = this._eventHandlers.get(eventName) - if (!set) return - - for (const wrapper of set) { - if (wrapper.fn === handler) { - set.delete(wrapper) - } - } - } - - /** - * 通知事件 - * @param eventName 事件名称 - * @param args 参数 - * @example - * eventBus.notifyEvent('noArgs') - * eventBus.notifyEvent('greet', 'Alice') - * eventBus.notifyEvent('onResize', 1, 2) - */ - notifyEvent(eventName: E, ...args: Parameters) { - if (!this._eventHandlers.has(eventName)) return - - const set = this._eventHandlers.get(eventName)! - for (const wrapper of set) { - try { - wrapper.fn(...args) - } catch (e) { - console.error(e) - } - - if (wrapper.once) { - set.delete(wrapper) - } - } - } - - destroy() { - this._eventHandlers.clear() - } -} diff --git a/src/core/process/IProcess.ts b/src/core/process/IProcess.ts deleted file mode 100644 index aed33f5..0000000 --- a/src/core/process/IProcess.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' -import type { IWindowForm } from '@/core/window/IWindowForm.ts' -import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' -import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts' -import type { IDestroyable } from '@/core/common/types/IDestroyable.ts' - -/** - * 进程接口 - */ -export interface IProcess extends IDestroyable { - /** 进程id */ - get id(): string; - /** 进程信息 */ - get processInfo(): IProcessInfo; - /** 进程的窗体列表 */ - get windowForms(): Map; - get event(): IEventBuilder; - /** - * 打开窗体 - * @param startName 窗体启动名 - */ - openWindowForm(startName: string): void; - /** - * 关闭窗体 - * @param id 窗体id - */ - closeWindowForm(id: string): void; -} diff --git a/src/core/process/IProcessInfo.ts b/src/core/process/IProcessInfo.ts deleted file mode 100644 index 0e8a309..0000000 --- a/src/core/process/IProcessInfo.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IVersion } from '@/core/common/types/IVersion.ts' -import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts' - -/** - * 进程的描述信息 - */ -export interface IProcessInfo { - /** 进程名称 - 唯一 */ - get name(): string; - /** 进程标题 */ - get title(): string; - /** 进程描述 */ - get description(): string; - /** 进程图标 */ - get icon(): string; - /** 启动窗体名称 */ - get startName(): string; - /** 进程版本 */ - get version(): IVersion; - /** 是否单例进程 */ - get singleton(): boolean; - /** 是否仅进程 */ - get isJustProcess(): boolean; - /** 进程的窗体配置 */ - get windowFormConfigs(): IWindowFormConfig[]; -} diff --git a/src/core/process/IProcessManager.ts b/src/core/process/IProcessManager.ts deleted file mode 100644 index f03964a..0000000 --- a/src/core/process/IProcessManager.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' -import type { IProcess } from '@/core/process/IProcess.ts' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' - -/** - * 进程管理 - */ -export interface IProcessManager { - /** 所有进程信息 */ - get processInfos(): IProcessInfo[]; - /** - * 注册进程 - * @param process 进程 - */ - registerProcess(process: IProcess): void; - /** - * 通过进程id查找进程 - * @param id 进程id - */ - findProcessById(id: string): IProcess | undefined; - /** - * 通过进程名查找进程 - * @param name 进程名 - */ - findProcessByName(name: string): T | undefined; - /** - * 通过进程id删除进程 - * @param id 进程id - */ - removeProcess(id: string): void; - /** - * 通过进程对象删除进程 - * @param process 进程对象 - */ - removeProcess(process: IProcess): void; - /** - * 通过进程名查找进程信息 - * @param name 进程名 - */ - findProcessInfoByName(name: string): IProcessInfo | undefined; -} \ No newline at end of file diff --git a/src/core/process/ProcessManager.ts b/src/core/process/ProcessManager.ts deleted file mode 100644 index c3fe652..0000000 --- a/src/core/process/ProcessManager.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts' - -export const processManager = new ProcessManagerImpl(); diff --git a/src/core/process/impl/ProcessImpl.ts b/src/core/process/impl/ProcessImpl.ts deleted file mode 100644 index 75a3528..0000000 --- a/src/core/process/impl/ProcessImpl.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { v4 as uuidV4 } from 'uuid'; -import WindowFormImpl from '../../window/impl/WindowFormImpl.ts' -import type { IProcess } from '@/core/process/IProcess.ts' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' -import type { IWindowForm } from '@/core/window/IWindowForm.ts' -import { processManager } from '@/core/process/ProcessManager.ts' -import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' -import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' -import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts' - -/** - * 进程 - */ -export default class ProcessImpl implements IProcess { - private readonly _id: string = uuidV4(); - private readonly _processInfo: IProcessInfo; - // 当前进程的窗体集合 - private _windowForms: Map = new Map(); - private _event: IEventBuilder = new EventBuilderImpl() - - public get id() { - return this._id; - } - public get processInfo() { - return this._processInfo; - } - public get windowForms() { - return this._windowForms; - } - public get event() { - return this._event; - } - - constructor(info: IProcessInfo) { - console.log(`AppProcess: ${info.name}`) - this._processInfo = info; - - const startName = info.startName; - - this.initEvent(); - - processManager.registerProcess(this); - // 通过设置 isJustProcess 为 true,则不会创建窗体 - if (!info.isJustProcess) { - this.openWindowForm(startName) - } - } - - private initEvent() { - this.event.addEventListener('processWindowFormExit', (id: string) => { - this.windowForms.delete(id) - if(this.windowForms.size === 0) { - processManager.removeProcess(this) - } - }) - } - - public openWindowForm(startName: string) { - const info = this._processInfo.windowFormConfigs.find(item => item.name === startName); - if (!info) throw new Error(`未找到窗体:${startName}`); - const wf = new WindowFormImpl(this, info); - this._windowForms.set(wf.id, wf); - } - - public closeWindowForm(id: string) { - try { - const wf = this._windowForms.get(id); - if (!wf) throw new Error(`未找到窗体:${id}`); - wf.destroy(); - this.windowForms.delete(id) - if(this.windowForms.size === 0) { - this.destroy() - processManager.removeProcess(this) - } - } catch (e) { - console.log('关闭窗体失败', e) - } - } - - public destroy() { - this._event.destroy() - } -} \ No newline at end of file diff --git a/src/core/process/impl/ProcessInfoImpl.ts b/src/core/process/impl/ProcessInfoImpl.ts deleted file mode 100644 index 5bd227a..0000000 --- a/src/core/process/impl/ProcessInfoImpl.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { IVersion } from '../../common/types/IVersion.ts' -import type { IAppProcessInfoParams } from '../types/IAppProcessInfoParams.ts' -import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' - -export class ProcessInfoImpl implements IProcessInfo { - /** - * 应用进程名称 - * @private - */ - private readonly _name: string; - - /** - * 应用进程标题 - * @private - */ - private readonly _title: string; - - /** - * 应用进程描述 - * @private - */ - private readonly _description: string; - - /** - * 应用进程图标 - * @private - */ - private readonly _icon: string; - - /** - * 应用进程启动入口 - * 对应windowFrom参数name - * @private - */ - private readonly _startName: string; - - /** - * 应用版本信息 - * @private - */ - private readonly _version: IVersion; - - /** - * 应用是否只存在一个进程 - * @private - */ - private readonly _singleton: boolean; - - /** - * 是否只是一个进程 - * @private - */ - private readonly _isJustProcess: boolean; - - /** - * 进程所有的窗口配置信息 - * @private - */ - private readonly _windowFormConfigs: Array; - - constructor(info: IAppProcessInfoParams) { - this._name = info.name; - this._title = info.title || ''; - this._description = info.description || ''; - this._icon = info.icon; - this._startName = info.startName || ''; - this._version = info.version || { company: 'XZG', major: 1, minor: 0, build: 0, private: 0 }; - this._singleton = info.singleton; - this._isJustProcess = info.isJustProcess; - this._windowFormConfigs = info.windowFormConfigs || []; - } - - public get name() { - return this._name; - } - public get title() { - return this._title; - } - public get description() { - return this._description; - } - public get icon() { - return this._icon; - } - public get startName() { - return this._startName; - } - public get version() { - return this._version; - } - public get singleton() { - return this._singleton; - } - public get isJustProcess() { - return this._isJustProcess; - } - public get windowFormConfigs() { - return this._windowFormConfigs; - } -} \ No newline at end of file diff --git a/src/core/process/impl/ProcessManagerImpl.ts b/src/core/process/impl/ProcessManagerImpl.ts deleted file mode 100644 index e87be6a..0000000 --- a/src/core/process/impl/ProcessManagerImpl.ts +++ /dev/null @@ -1,107 +0,0 @@ -import ProcessImpl from './ProcessImpl.ts' -import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' -import { BasicSystemProcessInfo } from '@/core/system/BasicSystemProcessInfo.ts' -import { DesktopProcessInfo } from '@/core/desktop/DesktopProcessInfo.ts' -import type { IAppProcessInfoParams } from '@/core/process/types/IAppProcessInfoParams.ts' -import type { IProcessManager } from '@/core/process/IProcessManager.ts' -import type { IProcess } from '@/core/process/IProcess.ts' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' -import { processManager } from '@/core/process/ProcessManager.ts' -import { isUndefined } from 'lodash' - -/** - * 进程管理 - */ -export default class ProcessManagerImpl implements IProcessManager { - private _processPool: Map = new Map(); - private _processInfos: IProcessInfo[] = new Array(); - - public get processInfos() { - return this._processInfos; - } - - constructor() { - console.log('ProcessManageImpl') - this.loadAppProcessInfos(); - } - // TODO 加载所有进程信息 - private loadAppProcessInfos() { - console.log('加载所有进程信息') - // 添加内置进程 - const apps = import.meta.glob('../../apps/**/*.json', { eager: true }) - const internalProcessInfos: ProcessInfoImpl[] = Object.values(apps).map(data => new ProcessInfoImpl(data)) - - this._processInfos.push(BasicSystemProcessInfo) - this._processInfos.push(DesktopProcessInfo) - - this._processInfos.push(...internalProcessInfos) - } - - public async runProcess( - proc: string | IProcessInfo, - constructor?: new (info: IProcessInfo, ...args: A) => T, - ...args: A - ): Promise { - let info = typeof proc === 'string' ? this.findProcessInfoByName(proc) : proc - if (isUndefined(info)) { - throw new Error(`未找到进程信息:${proc}`) - } - - // 是单例应用 - if (info.singleton) { - let process = this.findProcessByName(info.name) - if (process) { - return process as T - } - } - - // 创建进程 - let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info, ...args) - - return process as T - } - - // 添加进程 - public registerProcess(process: ProcessImpl) { - this._processPool.set(process.id, process); - } - - /** - * 通过进程id查找进程 - * @param id 进程id - */ - public findProcessById(id: string) { - return this._processPool.get(id); - } - - /** - * 通过进程名称查找进程 - * @param name 进程名称 - */ - public findProcessByName(name: string) { - const pools = [...this._processPool.values()]; - return pools.find(proc => proc.processInfo.name === name) as T | undefined; - } - - /** - * 根据进程id删除进程 - * @param id 进程id - */ - public removeProcess(id: string): void; - /** - * 根据进程删除进程 - * @param process 进程信息 - */ - public removeProcess(process: IProcess): void; - public removeProcess(params: string | IProcess) { - const id = typeof params === 'string' ? params : params.id; - this._processPool.delete(id); - } - - /** - * 通过进程名称查找进程信息 - */ - public findProcessInfoByName(name: string) { - return this._processInfos.find(info => info.name === name); - } -} \ No newline at end of file diff --git a/src/core/process/types/IAppProcessInfoParams.ts b/src/core/process/types/IAppProcessInfoParams.ts deleted file mode 100644 index 03e7306..0000000 --- a/src/core/process/types/IAppProcessInfoParams.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IVersion } from '../../common/types/IVersion.ts' -import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts' - -/** - * 应用进程入参信息 - */ -export interface IAppProcessInfoParams { - /** 应用进程名称 */ - name: string; - /** 应用进程标题 */ - title?: string; - /** 应用进程描述 */ - description?: string; - /** 应用进程图标 */ - icon?: string; - /** 应用进程启动入口 */ - startName?: string; - /** 应用版本信息 */ - version?: IVersion; - /** 应用是否只存在一个进程 */ - singleton: boolean; - /** 是否只是一个进程, 没有UI */ - isJustProcess: boolean; - /** 进程所有的窗口配置信息 */ - windowFormConfigs?: IWindowFormConfig[]; -} \ No newline at end of file diff --git a/src/core/process/types/ProcessEventTypes.ts b/src/core/process/types/ProcessEventTypes.ts deleted file mode 100644 index ee98546..0000000 --- a/src/core/process/types/ProcessEventTypes.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IEventMap } from '@/core/events/IEventBuilder.ts' - -/** - * 进程的事件 - *

onProcessExit - 进程退出

- *

onProcessWindowFormOpen - 进程的窗体打开

- *

onProcessWindowFormExit - 进程的窗体退出

- *

onProcessWindowFormFocus - 进程的窗体获取焦点

- * - */ -type TProcessEvent = - 'onProcessExit' | - 'onProcessWindowFormOpen' | - 'onProcessWindowFormExit' | - 'onProcessWindowFormFocus' | - 'onProcessWindowFormBlur' - -export interface IProcessEvent extends IEventMap { - /** - * 进程的窗体退出 - * @param id 窗体id - */ - processWindowFormExit: (id: string) => void -} \ No newline at end of file diff --git a/src/core/service/kernel/AService.ts b/src/core/service/kernel/AService.ts deleted file mode 100644 index 3774415..0000000 --- a/src/core/service/kernel/AService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { serviceManager, type ServiceManager } from '@/core/service/kernel/ServiceManager.ts' - -/** - * 服务基类 - 抽象类 - */ -export abstract class AService { - private readonly _id: string; - private _sm: ServiceManager = serviceManager; - - get id() { - return this._id; - } - get sm() { - return this._sm; - } - - protected constructor(id: string) { - this._id = id; - this._sm.registerService(this); - } -} diff --git a/src/core/service/kernel/ServiceManager.ts b/src/core/service/kernel/ServiceManager.ts deleted file mode 100644 index 8e8dba4..0000000 --- a/src/core/service/kernel/ServiceManager.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AService } from '@/core/service/kernel/AService.ts' - -/** - * 服务管理 - */ -export class ServiceManager { - private _services: Map = new Map(); - - get services(): Map { - return this._services - } - - /** - * 注册服务 - * @param service - */ - registerService(service: AService): void { - if (this._services.has(service.id)) { - throw new Error(`服务 ${service.id} 已存在`) - } - this._services.set(service.id, service) - } - - /** - * 通过id获取服务 - * @param id - */ - getService(id: string): T | undefined { - return this._services.get(id) as T | undefined - } - - /** - * 广播 - * @param event - * @param data - */ - broadcast(event: string, data?: any): void { - } -} - -export const serviceManager = new ServiceManager() diff --git a/src/core/service/services/NotificationService.ts b/src/core/service/services/NotificationService.ts deleted file mode 100644 index 97fd2be..0000000 --- a/src/core/service/services/NotificationService.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AService } from '@/core/service/kernel/AService.ts' - -export class NotificationService extends AService { - constructor() { - super('NotificationService'); - console.log('NotificationService - 服务注册') - } -} \ No newline at end of file diff --git a/src/core/service/services/SettingsService.ts b/src/core/service/services/SettingsService.ts deleted file mode 100644 index c560680..0000000 --- a/src/core/service/services/SettingsService.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AService } from '@/core/service/kernel/AService.ts' - -export class SettingsService extends AService { - constructor() { - super('SettingsService') - console.log('SettingsService - 服务注册') - } -} \ No newline at end of file diff --git a/src/core/service/services/UserService.ts b/src/core/service/services/UserService.ts deleted file mode 100644 index f40da05..0000000 --- a/src/core/service/services/UserService.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AService } from '@/core/service/kernel/AService.ts' -import type { IObservable } from '@/core/state/IObservable.ts' - -interface IUserInfo { - id: string; - name: string; - token: string; -} - -export class UserService extends AService { - private _userInfo: IObservable; - get userInfo() { - return this._userInfo; - } - - constructor() { - super("UserService"); - console.log("UserService - 服务注册") - } -} \ No newline at end of file diff --git a/src/core/service/services/WindowFormService.ts b/src/core/service/services/WindowFormService.ts deleted file mode 100644 index 0453b25..0000000 --- a/src/core/service/services/WindowFormService.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AService } from '@/core/service/kernel/AService.ts' -import type { IWindowForm } from '@/core/window/IWindowForm.ts' -import WindowFormImpl from '@/core/window/impl/WindowFormImpl.ts' -import type { IProcess } from '@/core/process/IProcess.ts' -import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts' - -interface IWindow { - id: string; - title: string; - x: number; - y: number; - width: number; - height: number; - zIndex: number; - minimized: boolean; - maximized: boolean; -} - -export class WindowFormService extends AService { - private windows: Map = new Map(); - - constructor() { - super("WindowFormService"); - console.log('WindowFormService - 服务注册') - } - - public createWindow(proc: IProcess, info: IWindowFormConfig): IWindowForm { - const window = new WindowFormImpl(proc, info); - this.windows.set(window.id, window); - return window; - } - - public closeWindow(id: string) { - if (this.windows.has(id)) { - this.windows.delete(id); - this.sm.broadcast("WindowFrom:closed", id); - } - } - - public focusWindow(id: string) { - const win = this.windows.get(id); - if (win) { - this.sm.broadcast("WindowFrom:focused", win); - } - } - - public minimizeWindow(id: string) { - const win = this.windows.get(id); - if (win) { - this.sm.broadcast("WindowFrom:minimized", win); - } - } - - public maximizeWindow(id: string) { - const win = this.windows.get(id); - if (win) { - this.sm.broadcast("WindowFrom:maximized", win); - } - } - - onMessage(event: string, data?: any) { - console.log(`[WindowService] 收到事件:`, event, data); - } -} diff --git a/src/core/state/IObservable.ts b/src/core/state/IObservable.ts deleted file mode 100644 index c624da3..0000000 --- a/src/core/state/IObservable.ts +++ /dev/null @@ -1,57 +0,0 @@ -// 订阅函数类型 -export type TObservableListener = (state: T) => void - -// 字段订阅函数类型 -export type TObservableKeyListener = (values: Pick) => void - -// 工具类型:排除函数属性 -export type TNonFunctionProperties = { - [K in keyof T as T[K] extends Function ? never : K]: T[K] -} - -// ObservableImpl 数据类型 -export type TObservableState = T & { [key: string]: any } - -/** - * ObservableImpl 接口定义 - */ -export interface IObservable> { - /** ObservableImpl 状态对象,深层 Proxy */ - readonly state: TObservableState - - /** - * 订阅整个状态变化 - * @param fn 监听函数 - * @param options immediate 是否立即触发一次 - * @returns 取消订阅函数 - */ - subscribe(fn: TObservableListener, options?: { immediate?: boolean }): () => void - - /** - * 订阅指定字段变化 - * @param keys 单个或多个字段 - * @param fn 字段变化回调 - * @param options immediate 是否立即触发一次 - * @returns 取消订阅函数 - */ - subscribeKey( - keys: K | K[], - fn: TObservableKeyListener, - options?: { immediate?: boolean } - ): () => void - - /** - * 批量更新状态 - * @param values Partial - */ - patch(values: Partial): void - - /** 销毁 ObservableImpl 实例 */ - dispose(): void - - /** - * 语法糖:返回一个可解构赋值的 Proxy - * 用于直接赋值触发通知 - */ - toRefsProxy(): { [K in keyof T]: T[K] } -} diff --git a/src/core/state/impl/ObservableImpl.ts b/src/core/state/impl/ObservableImpl.ts deleted file mode 100644 index 6a558f7..0000000 --- a/src/core/state/impl/ObservableImpl.ts +++ /dev/null @@ -1,305 +0,0 @@ -import type { - IObservable, - TNonFunctionProperties, - TObservableKeyListener, - TObservableListener, - TObservableState, -} from '@/core/state/IObservable.ts' - -/** - * 创建一个可观察对象,用于管理状态和事件。 - * @template T - 需要处理的状态类型 - * @example - * interface Todos { - * id: number - * text: string - * done: boolean - * } - * - * interface AppState { - * count: number - * todos: Todos[] - * user: { - * name: string - * age: number - * } - * inc(): void - * } - * - * const obs = new ObservableImpl({ - * count: 0, - * todos: [], - * user: { name: "Alice", age: 20 }, - * inc() { - * this.count++ // ✅ this 指向 obs.state - * }, - * }) - * - * // ================== 使用示例 ================== - * - * // 1. 订阅整个 state - * obs.subscribe(state => { - * console.log("[全量订阅] state 更新:", state) - * }) - * - * // 2. 订阅单个字段 - * obs.subscribeKey("count", ({ count }) => { - * console.log("[字段订阅] count 更新:", count) - * }) - * - * // 3. 订阅多个字段 - * obs.subscribeKey(["name", "age"] as (keyof AppState["user"])[], (user) => { - * console.log("[多字段订阅] user 更新:", user) - * }) - * - * // 4. 批量更新 - * obs.patch({ count: 10, user: { name: "Bob", age: 30 } }) - * - * // 5. 方法里操作 state - * obs.state.inc() // this.count++ → 相当于 obs.state.count++ - * - * // 6. 数组操作 - * obs.subscribeKey("todos", ({ todos }) => { - * console.log("[数组订阅] todos 更新:", todos.map(t => t.text)) - * }) - * - * obs.state.todos.push({ id: 1, text: "Buy milk", done: false }) - * obs.state.todos.push({ id: 2, text: "Read book", done: false }) - * obs.state.todos[0].done = true - * - * // 7. 嵌套对象 - * obs.subscribeKey("user", ({ user }) => { - * console.log("[嵌套订阅] user 更新:", user) - * }) - * - * obs.state.user.age++ - */ -export class ObservableImpl> implements IObservable { - /** Observable 状态对象,深层 Proxy */ - public readonly state: TObservableState - - /** 全量订阅函数集合 */ - private listeners: Set> = new Set() - - /** - * 字段订阅函数集合 - * 新结构: - * Map> - * 记录每个回调订阅的字段数组,保证多字段订阅 always 返回所有订阅字段值 - */ - private keyListeners: Map, Array> = new Map() - - /** 待通知的字段集合 */ - private pendingKeys: Set = new Set() - - /** 是否已经安排通知 */ - private notifyScheduled = false - - /** 是否已销毁 */ - private disposed = false - - /** 缓存 Proxy,避免重复包装 */ - private proxyCache: WeakMap> = new WeakMap() - - constructor(initialState: TNonFunctionProperties) { - // 创建深层响应式 Proxy - this.state = this.makeReactive(initialState) as TObservableState - } - - /** 创建深层 Proxy,拦截 get/set/delete,并自动缓存 */ - private makeReactive(obj: O): TObservableState { - // 非对象直接返回(包括 null 已被排除) - if (typeof obj !== "object" || obj === null) { - return obj as unknown as TObservableState - } - - // 如果已有 Proxy 缓存则直接返回 - const cached = this.proxyCache.get(obj as object) - if (cached !== undefined) { - return cached as TObservableState - } - - const handler: ProxyHandler = { - get: (target, prop, receiver) => { - const value = Reflect.get(target, prop, receiver) as unknown - // 不包装函数 - if (typeof value === "function") { - return value - } - // 对对象/数组继续进行响应式包装(递归) - if (typeof value === "object" && value !== null) { - return this.makeReactive(value as object) - } - return value - }, - - set: (target, prop, value, receiver) => { - // 读取旧值(使用 Record 以便类型安全访问属性) - const oldValue = (target as Record)[prop as PropertyKey] as unknown - const result = Reflect.set(target, prop, value as unknown, receiver) - // 仅在值改变时触发通知(基于引用/原始值比较) - if (!this.disposed && oldValue !== (value as unknown)) { - this.pendingKeys.add(prop as keyof T) - this.scheduleNotify() - } - return result - }, - - deleteProperty: (target, prop) => { - if (prop in target) { - // 使用 Reflect.deleteProperty 以保持一致性 - const deleted = Reflect.deleteProperty(target, prop) - if (deleted && !this.disposed) { - this.pendingKeys.add(prop as keyof T) - this.scheduleNotify() - } - return deleted - } - return false - } - } - - const proxy = new Proxy(obj, handler) as TObservableState - this.proxyCache.set(obj as object, proxy as TObservableState) - return proxy - } - - /** 安排下一次通知(微任务合并) */ - private scheduleNotify(): void { - if (!this.notifyScheduled && !this.disposed && this.pendingKeys.size > 0) { - this.notifyScheduled = true - Promise.resolve().then(() => this.flushNotify()) - } - } - - /** 执行通知(聚合字段订阅并保证错误隔离) */ - private flushNotify(): void { - if (this.disposed) return - - this.pendingKeys.clear() - this.notifyScheduled = false - - // 全量订阅 —— 每个订阅单独 try/catch,避免一个错误阻塞其它订阅 - for (const fn of this.listeners) { - try { - fn(this.state as unknown as T) - } catch (err) { - console.error("Observable listener error:", err) - } - } - - // ================== 字段订阅 ================== - // 遍历所有回调,每个回调都返回它订阅的字段(即使只有部分字段变化) - this.keyListeners.forEach((subKeys, fn) => { - try { - // 构造 Pick 风格的结果对象:结果类型为 Pick - const result = {} as Pick - subKeys.forEach(k => { - // 这里断言原因:state 的索引访问返回 unknown,但我们把它赋回到受限的 Pick 上 - result[k] = (this.state as Record)[k] as T[(typeof k) & keyof T] - }) - // 调用时类型上兼容 TObservableKeyListener,因为我们传的是对应 key 的 Pick - fn(result as Pick) - } catch (err) { - console.error("Observable keyListener error:", err) - } - }) - } - - /** 订阅整个状态变化 */ - public subscribe(fn: TObservableListener, options: { immediate?: boolean } = {}): () => void { - this.listeners.add(fn) - if (options.immediate) { - try { - fn(this.state as unknown as T) - } catch (err) { - console.error("Observable subscribe immediate error:", err) - } - } - return () => { - this.listeners.delete(fn) - } - } - - /** 订阅指定字段变化(多字段订阅 always 返回所有字段值) */ - public subscribeKey( - keys: K | K[], - fn: TObservableKeyListener, - options: { immediate?: boolean } = {} - ): () => void { - const keyArray = Array.isArray(keys) ? keys : [keys] - - // ================== 存储回调和它订阅的字段数组 ================== - this.keyListeners.set(fn as TObservableKeyListener, keyArray as (keyof T)[]) - - // ================== 立即调用 ================== - if (options.immediate) { - const result = {} as Pick - keyArray.forEach(k => { - result[k] = (this.state as Record)[k] as T[K] - }) - try { - fn(result) - } catch (err) { - console.error("Observable subscribeKey immediate error:", err) - } - } - - // ================== 返回取消订阅函数 ================== - return () => { - this.keyListeners.delete(fn as TObservableKeyListener) - } - } - - /** 批量更新状态(避免重复 schedule) */ - public patch(values: Partial): void { - let changed = false - for (const key in values) { - if (Object.prototype.hasOwnProperty.call(values, key)) { - const typedKey = key as keyof T - const oldValue = (this.state as Record)[typedKey] - const newValue = values[typedKey] as unknown - if (oldValue !== newValue) { - (this.state as Record)[typedKey] = newValue - changed = true - } - } - } - // 如果至少有一处变化,安排一次通知(如果写入已由 set 调度过也不会重复安排) - if (changed) this.scheduleNotify() - } - - /** 销毁 Observable 实例 */ - public dispose(): void { - this.disposed = true - this.listeners.clear() - this.keyListeners.clear() - this.pendingKeys.clear() - this.proxyCache = new WeakMap() - Object.freeze(this.state) - } - - /** 语法糖:返回一个可解构赋值的 Proxy */ - public toRefsProxy(): { [K in keyof T]: T[K] } { - const self = this - return new Proxy({} as { [K in keyof T]: T[K] }, { - get(_, prop: string | symbol) { - const key = prop as keyof T - return (self.state as Record)[key] as T[typeof key] - }, - set(_, prop: string | symbol, value) { - const key = prop as keyof T - ;(self.state as Record)[key] = value as unknown - return true - }, - ownKeys() { - return Reflect.ownKeys(self.state) - }, - getOwnPropertyDescriptor(_, _prop: string | symbol) { - return { enumerable: true, configurable: true } - } - }) - } -} - - diff --git a/src/core/state/impl/ObservableWeakRefImpl.ts b/src/core/state/impl/ObservableWeakRefImpl.ts deleted file mode 100644 index 3a6bf1a..0000000 --- a/src/core/state/impl/ObservableWeakRefImpl.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { - IObservable, - TNonFunctionProperties, - TObservableKeyListener, - TObservableListener, - TObservableState, -} from '@/core/state/IObservable.ts' - -/** - * 创建一个可观察对象,用于管理状态和事件。 - * WeakRef 和垃圾回收功能 - * @template T - 需要处理的状态类型 - * @example - * interface AppState { - * count: number - * user: { - * name: string - * age: number - * } - * items: number[] - * } - * - * // 创建 ObservableImpl - * const obs = new ObservableImpl({ - * count: 0, - * user: { name: 'Alice', age: 20 }, - * items: [] - * }) - * - * // 1️⃣ 全量订阅 - * const unsubscribeAll = obs.subscribe(state => { - * console.log('全量订阅', state) - * }, { immediate: true }) - * - * // 2️⃣ 单字段订阅 - * const unsubscribeCount = obs.subscribeKey('count', ({ count }) => { - * console.log('count 字段变化:', count) - * }) - * - * // 3️⃣ 多字段订阅 - * const unsubscribeUser = obs.subscribeKey(['user', 'count'], ({ user, count }) => { - * console.log('user 或 count 变化:', { user, count }) - * }) - * - * // 4️⃣ 修改属性 - * obs.state.count = 1 // ✅ 会触发 count 和全量订阅 - * obs.state.user.age = 21 // ✅ 深层对象修改触发 user 订阅 - * obs.state.user.name = 'Bob' - * // 语法糖:解构赋值直接赋值触发通知 - * const { count, user, items } = obs.toRefsProxy() - * count = 1 // 触发 Proxy set - * user.age = 18 // 深层对象 Proxy 支持 - * items.push(42) // 数组方法拦截触发通知 - * - * // 5️⃣ 数组方法触发 - * obs.state.items.push(10) // ✅ push 会触发 items 的字段订阅 - * obs.state.items.splice(0, 1) - * - * // 6️⃣ 批量修改(同一事件循环只触发一次通知) - * obs.patch({ - * count: 2, - * user: { name: 'Charlie', age: 30 } - * }) - * - * // 7️⃣ 解构赋值访问对象属性仍然触发订阅 - * const { state } = obs - * state.user.age = 31 // ✅ 会触发 user 订阅 - * - * // 8️⃣ 取消订阅 - * unsubscribeAll() - * unsubscribeCount() - * unsubscribeUser() - * - * // 9️⃣ 销毁 ObservableImpl - * obs.dispose() - */ -export class ObservableWeakRefImpl> implements IObservable { - /** ObservableImpl 的状态对象,深层 Proxy */ - public readonly state: TObservableState - - /** 全量订阅列表 */ - private listeners: Set> | TObservableListener> = new Set() - - /** 字段订阅列表 */ - private keyListeners: Map | Function>> = new Map() - - /** FinalizationRegistry 用于自动清理 WeakRef */ - private registry?: FinalizationRegistry> - - /** 待通知的字段 */ - private pendingKeys: Set = new Set() - - /** 通知调度状态 */ - private notifyScheduled = false - - /** 已销毁标记 */ - private disposed = false - - constructor(initialState: TNonFunctionProperties) { - if (typeof WeakRef !== 'undefined' && typeof FinalizationRegistry !== 'undefined') { - this.registry = new FinalizationRegistry((ref: WeakRef) => { - this.listeners.delete(ref as unknown as TObservableListener) - this.keyListeners.forEach(set => set.delete(ref)) - }) - } - - // 创建深层响应式 Proxy - this.state = this.makeReactive(initialState) as TObservableState - } - - /** 创建响应式对象,深层递归 Proxy + 数组方法拦截 */ - private makeReactive(obj: TNonFunctionProperties): TObservableState { - const handler: ProxyHandler = { - get: (target, prop: string | symbol, receiver) => { - const key = prop as keyof T // 类型断言 - const value = Reflect.get(target, key, receiver) - if (Array.isArray(value)) return this.wrapArray(value, key) - if (typeof value === 'object' && value !== null) return this.makeReactive(value) - return value - }, - set: (target, prop: string | symbol, value, receiver) => { - const key = prop as keyof T // 类型断言 - const oldValue = target[key] - if (oldValue !== value) { - target[key] = value - this.pendingKeys.add(key) - this.scheduleNotify() - } - return true - }, - } - return new Proxy(obj, handler) as TObservableState - } - - /** 包装数组方法,使 push/pop/splice 等触发通知 */ - private wrapArray(arr: any[], parentKey: keyof T): any { - const self = this - const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] as const - - arrayMethods.forEach(method => { - const original = arr[method] - Object.defineProperty(arr, method, { - value: function (...args: any[]) { - const result = original.apply(this, args) - self.pendingKeys.add(parentKey) - self.scheduleNotify() - return result - }, - writable: true, - configurable: true, - }) - }) - - return arr - } - - /** 调度异步通知 */ - private scheduleNotify(): void { - if (!this.notifyScheduled && !this.disposed) { - this.notifyScheduled = true - Promise.resolve().then(() => this.flushNotify()) - } - } - - /** 执行通知逻辑 */ - private flushNotify(): void { - if (this.disposed) return - const keys = Array.from(this.pendingKeys) - this.pendingKeys.clear() - this.notifyScheduled = false - - // 全量订阅 - for (const ref of this.listeners) { - const fn = this.deref(ref) - if (fn) fn(this.state) - else this.listeners.delete(ref as TObservableListener) - } - - // 字段订阅 - const fnMap = new Map() - for (const key of keys) { - const set = this.keyListeners.get(key) - if (!set) continue - for (const ref of set) { - const fn = this.deref(ref) - if (!fn) { - set.delete(ref) - continue - } - if (!fnMap.has(fn)) fnMap.set(fn, []) - fnMap.get(fn)!.push(key) - } - } - - fnMap.forEach((subKeys, fn) => { - const result = {} as Pick - subKeys.forEach(k => (result[k] = this.state[k])) - fn(result) - }) - } - - /** 全量订阅 */ - subscribe(fn: TObservableListener, options: { immediate?: boolean } = {}): () => void { - const ref = this.makeRef(fn) - this.listeners.add(ref) - this.registry?.register(fn, ref as WeakRef) - if (options.immediate) fn(this.state) - return () => { - this.listeners.delete(ref) - this.registry?.unregister(fn) - } - } - - /** 字段订阅 */ - subscribeKey( - keys: K | K[], - fn: TObservableKeyListener, - options: { immediate?: boolean } = {} - ): () => void { - const keyArray = Array.isArray(keys) ? keys : [keys] - const refs: (WeakRef | Function)[] = [] - - for (const key of keyArray) { - if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set()) - const ref = this.makeRef(fn) - this.keyListeners.get(key)!.add(ref) - this.registry?.register(fn as unknown as Function, ref as WeakRef) - refs.push(ref) - } - - if (options.immediate) { - const result = {} as Pick - keyArray.forEach(k => (result[k] = this.state[k])) - fn(result) - } - - return () => { - for (let i = 0; i < keyArray.length; i++) { - const set = this.keyListeners.get(keyArray[i]) - if (set) set.delete(refs[i]) - } - this.registry?.unregister(fn as unknown as Function) - } - } - - /** 批量更新 */ - patch(values: Partial): void { - for (const key in values) { - if (Object.prototype.hasOwnProperty.call(values, key)) { - const typedKey = key as keyof T - this.state[typedKey] = values[typedKey]! - this.pendingKeys.add(typedKey) - } - } - this.scheduleNotify() - } - - /** 销毁 ObservableImpl */ - dispose(): void { - this.disposed = true - this.listeners.clear() - this.keyListeners.clear() - this.pendingKeys.clear() - } - - /** 语法糖:解构赋值直接赋值触发通知 */ - toRefsProxy(): { [K in keyof T]: T[K] } { - const self = this - return new Proxy({} as T, { - get(_, prop: string | symbol) { - const key = prop as keyof T - return self.state[key] - }, - set(_, prop: string | symbol, value) { - const key = prop as keyof T - self.state[key] = value - return true - }, - ownKeys() { - return Reflect.ownKeys(self.state) - }, - getOwnPropertyDescriptor(_, prop: string | symbol) { - return { enumerable: true, configurable: true } - } - }) - } - - /** WeakRef 创建 */ - private makeRef(fn: F): WeakRef | F { - return typeof WeakRef !== 'undefined' ? new WeakRef(fn) : fn - } - - /** WeakRef 解引用 */ - private deref(ref: WeakRef | F): F | undefined { - return typeof WeakRef !== 'undefined' && ref instanceof WeakRef ? ref.deref() : (ref as F) - } -} \ No newline at end of file diff --git a/src/core/state/store/GlobalStore.ts b/src/core/state/store/GlobalStore.ts deleted file mode 100644 index b777904..0000000 --- a/src/core/state/store/GlobalStore.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts' - -interface IGlobalStoreParams { - /** 桌面根dom ID,类似显示器 */ - monitorDomId: string; - monitorWidth: number; - monitorHeight: number; -} - -export const globalStore = new ObservableImpl({ - monitorDomId: '#app', - monitorWidth: 0, - monitorHeight: 0 -}) diff --git a/src/core/system/BasicSystemProcess.ts b/src/core/system/BasicSystemProcess.ts deleted file mode 100644 index eb948dd..0000000 --- a/src/core/system/BasicSystemProcess.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ProcessImpl from '../process/impl/ProcessImpl.ts' -import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' -import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' - -/** - * 基础系统进程 - */ -export class BasicSystemProcess extends ProcessImpl { - private _isMounted: boolean = false; - - public get isMounted() { - return this._isMounted; - } - - constructor(info: IProcessInfo) { - super(info) - console.log('BasicSystemProcess') - } -} \ No newline at end of file diff --git a/src/core/system/BasicSystemProcessInfo.ts b/src/core/system/BasicSystemProcessInfo.ts deleted file mode 100644 index a36996c..0000000 --- a/src/core/system/BasicSystemProcessInfo.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' - -/** - * 基础系统进程信息 - */ -export const BasicSystemProcessInfo = new ProcessInfoImpl({ - name: 'basic-system', - title: '基础系统进程', - isJustProcess: true, - version: { - company: 'XZG', - major: 1, - minor: 0, - build: 0, - private: 0 - }, - singleton: true -}); diff --git a/src/core/utils/DraggableResizableWindow.ts b/src/core/utils/DraggableResizableWindow.ts deleted file mode 100644 index b77e744..0000000 --- a/src/core/utils/DraggableResizableWindow.ts +++ /dev/null @@ -1,752 +0,0 @@ -import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' - -/** 拖拽移动开始的回调 */ -type TDragStartCallback = (x: number, y: number) => void; -/** 拖拽移动中的回调 */ -type TDragMoveCallback = (x: number, y: number) => void; -/** 拖拽移动结束的回调 */ -type TDragEndCallback = (x: number, y: number) => void; - -/** 拖拽调整尺寸的方向 */ -type TResizeDirection = - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-left' - | 'top-right' - | 'bottom-left' - | 'bottom-right'; - -/** 元素边界 */ -interface IElementRect { - /** 宽度 */ - width: number; - /** 高度 */ - height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; -} - -/** 拖拽调整尺寸回调数据 */ -interface IResizeCallbackData { - /** 宽度 */ - width: number; - /** 高度 */ - height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; - /** 拖拽调整尺寸的方向 */ - direction: TResizeDirection; -} - -/** 拖拽参数 */ -interface IDraggableResizableOptions { - /** 拖拽/调整尺寸目标元素 */ - target: HTMLElement; - /** 拖拽句柄 */ - handle?: HTMLElement; - /** 拖拽边界容器元素 */ - boundaryElement?: HTMLElement; - /** 移动步进(网格吸附) */ - snapGrid?: number; - /** 关键点吸附阈值 */ - snapThreshold?: number; - /** 是否开启吸附动画 */ - snapAnimation?: boolean; - /** 拖拽结束吸附动画时长 */ - snapAnimationDuration?: number; - /** 是否允许超出边界 */ - allowOverflow?: boolean; - /** 最小化任务栏位置的元素ID */ - taskbarElementId: string; - - /** 拖拽开始回调 */ - onDragStart?: TDragStartCallback; - /** 拖拽移动中的回调 */ - onDragMove?: TDragMoveCallback; - /** 拖拽结束回调 */ - onDragEnd?: TDragEndCallback; - - /** 调整尺寸的最小宽度 */ - minWidth?: number; - /** 调整尺寸的最小高度 */ - minHeight?: number; - /** 调整尺寸的最大宽度 */ - maxWidth?: number; - /** 调整尺寸的最大高度 */ - maxHeight?: number; - - /** 拖拽调整尺寸中的回调 */ - onResizeMove?: (data: IResizeCallbackData) => void; - /** 拖拽调整尺寸结束回调 */ - onResizeEnd?: (data: IResizeCallbackData) => void; - - /** 窗口状态改变回调 */ - onWindowStateChange?: (state: TWindowFormState) => void; -} - -/** 拖拽的范围边界 */ -interface IBoundaryRect { - /** 最小 X 坐标 */ - minX?: number; - /** 最大 X 坐标 */ - maxX?: number; - /** 最小 Y 坐标 */ - minY?: number; - /** 最大 Y 坐标 */ - maxY?: number; -} - -/** - * 拖拽 + 调整尺寸 + 最大最小化 通用类 - * 统一使用 position: absolute + transform: translate 实现拖拽 - */ -export class DraggableResizableWindow { - private handle?: HTMLElement; - private target: HTMLElement; - private boundaryElement: HTMLElement; - private snapGrid: number; - private snapThreshold: number; - private snapAnimation: boolean; - private snapAnimationDuration: number; - private allowOverflow: boolean; - - private onDragStart?: TDragStartCallback; - private onDragMove?: TDragMoveCallback; - private onDragEnd?: TDragEndCallback; - - private onResizeMove?: (data: IResizeCallbackData) => void; - private onResizeEnd?: (data: IResizeCallbackData) => void; - - private onWindowStateChange?: (state: TWindowFormState) => void; - - private isDragging = false; - private currentDirection: TResizeDirection | null = null; - private dragThreshold = 2; // 拖拽阈值 超过才开始真正的拖拽 - - private startX = 0; - private startY = 0; - private startWidth = 0; - private startHeight = 0; - private startTop = 0; - private startLeft = 0; - private offsetX = 0; - private offsetY = 0; - private currentX = 0; - private currentY = 0; - - private pendingDrag = false; - private pendingResize = false; - private dragDX = 0; - private dragDY = 0; - private resizeDX = 0; - private resizeDY = 0; - - private minWidth: number; - private minHeight: number; - private maxWidth: number; - private maxHeight: number; - - private containerRect: DOMRect; - private resizeObserver?: ResizeObserver; - private mutationObserver: MutationObserver; - private animationFrame?: number; - - private _windowFormState: TWindowFormState = 'default'; - /** 元素信息 */ - private targetBounds: IElementRect; - /** 最小化前的元素信息 */ - private targetPreMinimizeBounds?: IElementRect; - /** 最大化前的元素信息 */ - private targetPreMaximizedBounds?: IElementRect; - private taskbarElementId: string; - - get windowFormState() { - return this._windowFormState; - } - - constructor(options: IDraggableResizableOptions) { - this.handle = options.handle; - this.target = options.target; - this.boundaryElement = options.boundaryElement ?? document.body; - this.snapGrid = options.snapGrid ?? 1; - this.snapThreshold = options.snapThreshold ?? 0; - this.snapAnimation = options.snapAnimation ?? false; - this.snapAnimationDuration = options.snapAnimationDuration ?? 200; - this.allowOverflow = options.allowOverflow ?? true; - - this.onDragStart = options.onDragStart; - this.onDragMove = options.onDragMove; - this.onDragEnd = options.onDragEnd; - - this.minWidth = options.minWidth ?? 100; - this.minHeight = options.minHeight ?? 50; - this.maxWidth = options.maxWidth ?? window.innerWidth; - this.maxHeight = options.maxHeight ?? window.innerHeight; - this.onResizeMove = options.onResizeMove; - this.onResizeEnd = options.onResizeEnd; - this.onWindowStateChange = options.onWindowStateChange; - - this.taskbarElementId = options.taskbarElementId; - - this.target.style.position = "absolute"; - this.target.style.left = '0px'; - this.target.style.top = '0px'; - this.target.style.transform = "translate(0px, 0px)"; - - this.init(); - - requestAnimationFrame(() => { - this.targetBounds = { - width: this.target.offsetWidth, - height: this.target.offsetHeight, - top: this.target.offsetTop, - left: this.target.offsetLeft, - }; - this.containerRect = this.boundaryElement.getBoundingClientRect(); - const x = this.containerRect.width / 2 - this.target.offsetWidth / 2; - const y = this.containerRect.height / 2 - this.target.offsetHeight / 2; - this.target.style.transform = `translate(${x}px, ${y}px)`; - }); - } - - private init() { - if (this.handle) { - this.handle.addEventListener('mousedown', this.onMouseDownDrag); - } - this.target.addEventListener('mousedown', this.onMouseDownResize); - this.target.addEventListener('mouseleave', this.onMouseLeave); - document.addEventListener('mousemove', this.onDocumentMouseMoveCursor); - - this.observeResize(this.boundaryElement); - - this.mutationObserver = new MutationObserver(mutations => { - for (const mutation of mutations) { - mutation.removedNodes.forEach(node => { - if (node === this.target) this.destroy(); - }); - } - }); - if (this.target.parentElement) { - this.mutationObserver.observe(this.target.parentElement, { childList: true }); - } - } - - private onMouseDownDrag = (e: MouseEvent) => { - e.preventDefault(); - if (!this.handle?.contains(e.target as Node)) return; - const target = e.target as HTMLElement; - if (target.classList.contains('btn')) return; - if (this.getResizeDirection(e)) return; - - this.startX = e.clientX; - this.startY = e.clientY; - - document.addEventListener('mousemove', this.checkDragStart); - document.addEventListener('mouseup', this.cancelPendingDrag); - } - - private checkDragStart = (e: MouseEvent) => { - const dx = e.clientX - this.startX; - const dy = e.clientY - this.startY; - - if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) { - // 超过阈值,真正开始拖拽 - document.removeEventListener('mousemove', this.checkDragStart); - document.removeEventListener('mouseup', this.cancelPendingDrag); - - if (this._windowFormState === 'maximized') { - const preRect = this.targetPreMaximizedBounds!; - const rect = this.target.getBoundingClientRect(); - const relX = e.clientX / rect.width; - const relY = e.clientY / rect.height; - const newLeft = e.clientX - preRect.width * relX; - const newTop = e.clientY - preRect.height * relY; - this.targetPreMaximizedBounds = { - width: preRect.width, - height: preRect.height, - top: newTop, - left: newLeft, - }; - - this.restore(() => this.startDrag(e)); - } else { - this.startDrag(e); - } - } - }; - - private cancelPendingDrag = () => { - document.removeEventListener('mousemove', this.checkDragStart); - document.removeEventListener('mouseup', this.cancelPendingDrag); - } - - private startDrag = (e: MouseEvent) => { - this.isDragging = true; - this.startX = e.clientX; - this.startY = e.clientY; - - const style = window.getComputedStyle(this.target); - const matrix = new DOMMatrixReadOnly(style.transform); - this.offsetX = matrix.m41; - this.offsetY = matrix.m42; - - this.onDragStart?.(this.offsetX, this.offsetY); - - document.addEventListener('mousemove', this.onMouseMoveDragRAF); - document.addEventListener('mouseup', this.onMouseUpDrag); - }; - - private onMouseMoveDragRAF = (e: MouseEvent) => { - e.stopPropagation(); - this.dragDX = e.clientX - this.startX; - this.dragDY = e.clientY - this.startY; - if (!this.pendingDrag) { - this.pendingDrag = true; - requestAnimationFrame(() => { - this.pendingDrag = false; - this.applyDragFrame(); - }); - } - }; - - private applyDragFrame() { - if (!this.isDragging) return; - - let newX = this.offsetX + this.dragDX; - let newY = this.offsetY + this.dragDY; - - if (this.snapGrid > 1) { - newX = Math.round(newX / this.snapGrid) * this.snapGrid; - newY = Math.round(newY / this.snapGrid) * this.snapGrid; - } - - this.applyPosition(newX, newY, false); - this.onDragMove?.(newX, newY); - } - - private onMouseUpDrag = (e: MouseEvent) => { - e.stopPropagation(); - if (!this.isDragging) return; - this.isDragging = false; - - const snapped = this.applySnapping(this.currentX, this.currentY); - - if (this.snapAnimation) { - this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { - this.onDragEnd?.(snapped.x, snapped.y); - this.updateTargetBounds(snapped.x, snapped.y); - }); - } else { - this.applyPosition(snapped.x, snapped.y, true); - this.onDragEnd?.(snapped.x, snapped.y); - this.updateTargetBounds(snapped.x, snapped.y); - } - - document.removeEventListener('mousemove', this.onMouseMoveDragRAF); - document.removeEventListener('mouseup', this.onMouseUpDrag); - }; - - private applyPosition(x: number, y: number, isFinal: boolean) { - this.currentX = x; - this.currentY = y; - this.target.style.transform = `translate(${x}px, ${y}px)`; - if (isFinal) this.applyBoundary(); - } - - private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) { - if (this.animationFrame) cancelAnimationFrame(this.animationFrame); - const startX = this.currentX; - const startY = this.currentY; - const deltaX = targetX - startX; - const deltaY = targetY - startY; - const startTime = performance.now(); - - const step = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - progress, 3); - - const x = startX + deltaX * ease; - const y = startY + deltaY * ease; - - this.applyPosition(x, y, false); - this.onDragMove?.(x, y); - - if (progress < 1) this.animationFrame = requestAnimationFrame(step); - else { this.applyPosition(targetX, targetY, true); onComplete?.(); } - }; - this.animationFrame = requestAnimationFrame(step); - } - - private applyBoundary() { - if (this.allowOverflow) return; - let { x, y } = { x: this.currentX, y: this.currentY }; - - const rect = this.target.getBoundingClientRect(); - x = Math.min(Math.max(x, 0), this.containerRect.width - rect.width); - y = Math.min(Math.max(y, 0), this.containerRect.height - rect.height); - - this.currentX = x; - this.currentY = y; - this.applyPosition(x, y, false); - } - - private applySnapping(x: number, y: number) { - let snappedX = x, snappedY = y; - const containerSnap = this.getSnapPoints(); - if (this.snapThreshold > 0) { - for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapThreshold) { snappedX = sx; break; } - for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapThreshold) { snappedY = sy; break; } - } - return { x: snappedX, y: snappedY }; - } - - private getSnapPoints() { - const snapPoints = { x: [] as number[], y: [] as number[] }; - const rect = this.target.getBoundingClientRect(); - snapPoints.x = [0, this.containerRect.width - rect.width]; - snapPoints.y = [0, this.containerRect.height - rect.height]; - return snapPoints; - } - - private onMouseDownResize = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - const dir = this.getResizeDirection(e); - if (!dir) return; - - this.startResize(e, dir); - }; - - private onMouseLeave = (e: MouseEvent) => { - e.stopPropagation(); - this.updateCursor(null); - }; - - private startResize(e: MouseEvent, dir: TResizeDirection) { - this.currentDirection = dir; - const rect = this.target.getBoundingClientRect(); - const style = window.getComputedStyle(this.target); - const matrix = new DOMMatrixReadOnly(style.transform); - this.offsetX = matrix.m41; - this.offsetY = matrix.m42; - - this.startX = e.clientX; - this.startY = e.clientY; - this.startWidth = rect.width; - this.startHeight = rect.height; - this.startLeft = this.offsetX; - this.startTop = this.offsetY; - - document.addEventListener('mousemove', this.onResizeDragRAF); - document.addEventListener('mouseup', this.onResizeEndHandler); - } - - private onResizeDragRAF = (e: MouseEvent) => { - e.stopPropagation(); - this.resizeDX = e.clientX - this.startX; - this.resizeDY = e.clientY - this.startY; - if (!this.pendingResize) { - this.pendingResize = true; - requestAnimationFrame(() => { - this.pendingResize = false; - this.applyResizeFrame(); - }); - } - }; - - private applyResizeFrame() { - if (!this.currentDirection) return; - - let newWidth = this.startWidth; - let newHeight = this.startHeight; - let newX = this.startLeft; - let newY = this.startTop; - - const dx = this.resizeDX; - const dy = this.resizeDY; - - switch (this.currentDirection) { - case 'right': newWidth += dx; break; - case 'bottom': newHeight += dy; break; - case 'bottom-right': newWidth += dx; newHeight += dy; break; - case 'left': newWidth -= dx; newX += dx; break; - case 'top': newHeight -= dy; newY += dy; break; - case 'top-left': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break; - case 'top-right': newWidth += dx; newHeight -= dy; newY += dy; break; - case 'bottom-left': newWidth -= dx; newX += dx; newHeight += dy; break; - } - - const d = this.applyResizeBounds(newX, newY, newWidth, newHeight); - - this.updateCursor(this.currentDirection); - - this.onResizeMove?.({ - width: d.width, - height: d.height, - left: d.left, - top: d.top, - direction: this.currentDirection, - }); - } - - // 应用尺寸调整边界 - private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number): { - left: number; - top: number; - width: number; - height: number; - } { - // 最小/最大宽高限制 - newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth)); - newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight)); - - // 边界限制 - if (this.allowOverflow) { - this.currentX = newX; - this.currentY = newY; - this.target.style.width = `${newWidth}px`; - this.target.style.height = `${newHeight}px`; - this.applyPosition(newX, newY, false); - return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, - } - } - - newX = Math.min(Math.max(0, newX), this.containerRect.width - newWidth); - newY = Math.min(Math.max(0, newY), this.containerRect.height - newHeight); - - this.currentX = newX; - this.currentY = newY; - this.target.style.width = `${newWidth}px`; - this.target.style.height = `${newHeight}px`; - this.applyPosition(newX, newY, false); - return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, - } - } - - private onResizeEndHandler = (e?: MouseEvent) => { - e?.stopPropagation(); - if (!this.currentDirection) return; - this.onResizeEnd?.({ - width: this.target.offsetWidth, - height: this.target.offsetHeight, - left: this.currentX, - top: this.currentY, - direction: this.currentDirection, - }); - this.updateTargetBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight); - this.currentDirection = null; - this.updateCursor(null); - document.removeEventListener('mousemove', this.onResizeDragRAF); - document.removeEventListener('mouseup', this.onResizeEndHandler); - }; - - private getResizeDirection(e: MouseEvent): TResizeDirection | null { - const rect = this.target.getBoundingClientRect(); - const offset = 4; - const x = e.clientX; - const y = e.clientY; - const top = y >= rect.top && y <= rect.top + offset; - const bottom = y >= rect.bottom - offset && y <= rect.bottom; - const left = x >= rect.left && x <= rect.left + offset; - const right = x >= rect.right - offset && x <= rect.right; - - if (top && left) return 'top-left'; - if (top && right) return 'top-right'; - if (bottom && left) return 'bottom-left'; - if (bottom && right) return 'bottom-right'; - if (top) return 'top'; - if (bottom) return 'bottom'; - if (left) return 'left'; - if (right) return 'right'; - return null; - } - - private updateCursor(direction: TResizeDirection | null) { - if (!direction) { this.target.style.cursor = 'default'; return; } - const cursorMap: Record = { - top: 'ns-resize', bottom: 'ns-resize', left: 'ew-resize', right: 'ew-resize', - 'top-left': 'nwse-resize', 'top-right': 'nesw-resize', - 'bottom-left': 'nesw-resize', 'bottom-right': 'nwse-resize' - }; - this.target.style.cursor = cursorMap[direction]; - } - - private onDocumentMouseMoveCursor = (e: MouseEvent) => { - e.stopPropagation(); - if (this.currentDirection || this.isDragging) return; - const dir = this.getResizeDirection(e); - this.updateCursor(dir); - }; - - // 最小化到任务栏 - public minimize() { - if (this._windowFormState === 'minimized') return; - this.targetPreMinimizeBounds = { ...this.targetBounds } - this._windowFormState = 'minimized'; - - const taskbar = document.querySelector(this.taskbarElementId); - if (!taskbar) throw new Error('任务栏元素未找到'); - - const rect = taskbar.getBoundingClientRect(); - const startX = this.currentX; - const startY = this.currentY; - const startW = this.target.offsetWidth; - const startH = this.target.offsetHeight; - - this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400, () => { - this.target.style.display = 'none'; - }); - } - - /** 最大化 */ - public maximize() { - if (this._windowFormState === 'maximized') return; - this.targetPreMaximizedBounds = { ...this.targetBounds } - this._windowFormState = 'maximized'; - - const rect = this.target.getBoundingClientRect(); - - const startX = this.currentX; - const startY = this.currentY; - const startW = rect.width; - const startH = rect.height; - - const targetX = 0; - const targetY = 0; - const targetW = this.containerRect?.width ?? window.innerWidth; - const targetH = this.containerRect?.height ?? window.innerHeight; - - this.animateWindow(startX, startY, startW, startH, targetX, targetY, targetW, targetH, 300); - } - - /** 恢复到默认窗体状态 */ - public restore(onComplete?: () => void) { - if (this._windowFormState === 'default') return; - let b: IElementRect; - if ((this._windowFormState as TWindowFormState) === 'minimized' && this.targetPreMinimizeBounds) { - // 最小化恢复,恢复到最小化前的状态 - b = this.targetPreMinimizeBounds; - } else if ((this._windowFormState as TWindowFormState) === 'maximized' && this.targetPreMaximizedBounds) { - // 最大化恢复,恢复到最大化前的默认状态 - b = this.targetPreMaximizedBounds; - } else { - b = this.targetBounds; - } - - this._windowFormState = 'default'; - - this.target.style.display = 'block'; - - const startX = this.currentX; - const startY = this.currentY; - const startW = this.target.offsetWidth; - const startH = this.target.offsetHeight; - - this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300, onComplete); - } - - /** - * 窗体最大化、最小化和恢复默认 动画 - * @param startX - * @param startY - * @param startW - * @param startH - * @param targetX - * @param targetY - * @param targetW - * @param targetH - * @param duration - * @param onComplete - * @private - */ - private animateWindow( - startX: number, - startY: number, - startW: number, - startH: number, - targetX: number, - targetY: number, - targetW: number, - targetH: number, - duration: number, - onComplete?: () => void - ) { - const startTime = performance.now(); - const step = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - progress, 3); - - const x = startX + (targetX - startX) * ease; - const y = startY + (targetY - startY) * ease; - const w = startW + (targetW - startW) * ease; - const h = startH + (targetH - startH) * ease; - - this.target.style.width = `${w}px`; - this.target.style.height = `${h}px`; - this.applyPosition(x, y, false); - - if (progress < 1) { - requestAnimationFrame(step); - } else { - this.target.style.width = `${targetW}px`; - this.target.style.height = `${targetH}px`; - this.applyPosition(targetX, targetY, true); - onComplete?.(); - this.onWindowStateChange?.(this._windowFormState); - } - }; - requestAnimationFrame(step); - } - - private updateTargetBounds(left: number, top: number, width?: number, height?: number) { - this.targetBounds = { - left, top, - width: width ?? this.target.offsetWidth, - height: height ?? this.target.offsetHeight - }; - } - - /** 监听元素变化 */ - private observeResize(element: HTMLElement) { - this.resizeObserver = new ResizeObserver(() => { - this.containerRect = element.getBoundingClientRect(); - }); - this.resizeObserver.observe(element); - } - - /** - * 销毁实例 - */ - public destroy() { - try { - if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag); - this.target.removeEventListener('mousedown', this.onMouseDownResize); - document.removeEventListener('mousemove', this.onMouseMoveDragRAF); - document.removeEventListener('mouseup', this.onMouseUpDrag); - document.removeEventListener('mousemove', this.onResizeDragRAF); - document.removeEventListener('mouseup', this.onResizeEndHandler); - document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor); - document.removeEventListener('mousemove', this.checkDragStart); - document.removeEventListener('mouseup', this.cancelPendingDrag); - this.resizeObserver?.disconnect(); - this.mutationObserver.disconnect(); - cancelAnimationFrame(this.animationFrame ?? 0); - } catch (e) {} - } -} diff --git a/src/core/utils/Singleton.ts b/src/core/utils/Singleton.ts deleted file mode 100644 index bc79eb8..0000000 --- a/src/core/utils/Singleton.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** 单例模式 - * 确保一个类只有一个实例,并提供一个全局访问点 - * @param constructor - * @constructor - */ -export function Singleton(constructor: T): T { - let instance: any; - - return new Proxy(constructor, { - construct(target, argsList, newTarget) { - if (!instance) { - instance = Reflect.construct(target, argsList, newTarget); - } - return instance; - }, - }); -} diff --git a/src/core/window/IWindowForm.ts b/src/core/window/IWindowForm.ts deleted file mode 100644 index d832878..0000000 --- a/src/core/window/IWindowForm.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IProcess } from '@/core/process/IProcess.ts' -import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' -import type { IDestroyable } from '@/core/common/types/IDestroyable.ts' - -export interface IWindowForm extends IDestroyable { - /** 窗体id */ - get id(): string; - /** 窗体所属的进程 */ - get proc(): IProcess | undefined; - /** 窗体元素 */ - get windowFormEle(): HTMLElement; - /** 窗体状态 */ - get windowFormState(): TWindowFormState; -} \ No newline at end of file diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts deleted file mode 100644 index e8d82f8..0000000 --- a/src/core/window/impl/WindowFormImpl.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { v4 as uuidV4 } from 'uuid'; -import XSystem from '../../XSystem.ts' -import type { IProcess } from '@/core/process/IProcess.ts' -import type { IWindowForm } from '@/core/window/IWindowForm.ts' -import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts' -import type { TWindowFormState, WindowFormPos } from '@/core/window/types/WindowFormTypes.ts' -import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts' -import '../ui/WindowFormElement.ts' -import { wfem } from '@/core/events/WindowFormEventManager.ts' -import type { IObservable } from '@/core/state/IObservable.ts' -import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts' - -export interface IWindowFormDataState { - /** 窗体id */ - id: string; - /** 窗体进程id */ - procId: string; - /** 进程名称唯一 */ - name: string; - /** 窗体标题 */ - title: string; - /** 窗体位置x (左上角) */ - x: number; - /** 窗体位置y (左上角) */ - y: number; - /** 窗体宽度 */ - width: number; - /** 窗体高度 */ - height: number; - /** 窗体状态 'default' | 'minimized' | 'maximized' */ - state: TWindowFormState; - /** 窗体是否已关闭 */ - closed: boolean; -} - -export default class WindowFormImpl implements IWindowForm { - private readonly _id: string = uuidV4() - private readonly _proc: IProcess - private readonly _data: IObservable - private dom: HTMLElement - private drw: DraggableResizableWindow - - public get id() { - return this._id - } - public get proc() { - return this._proc - } - private get desktopRootDom() { - return XSystem.instance.desktopRootDom - } - public get windowFormEle() { - return this.dom - } - public get windowFormState() { - return this.drw.windowFormState - } - - constructor(proc: IProcess, config: IWindowFormConfig) { - this._proc = proc - console.log('WindowForm') - - this._data = new ObservableImpl({ - id: this.id, - procId: proc.id, - name: proc.processInfo.name, - title: config.title ?? '未命名', - x: config.left ?? 0, - y: config.top ?? 0, - width: config.width ?? 200, - height: config.height ?? 100, - state: 'default', - closed: false, - }) - - this.initEvent() - this.createWindowFrom() - } - - private initEvent() { - this._data.subscribeKey('closed', (state) => { - console.log('closed', state) - this.closeWindowForm() - this._proc.closeWindowForm(this.id) - }) - } - - private createWindowFrom() { - const wf = document.createElement('window-form-element') - wf.wid = this.id - wf.wfData = this._data - wf.title = this._data.state.title - wf.dragContainer = document.body - wf.snapDistance = 20 - wf.taskbarElementId = '#taskbar' - this.dom = wf - this.desktopRootDom.appendChild(this.dom) - Promise.resolve().then(() => { - wfem.notifyEvent('windowFormCreated') - wfem.notifyEvent('windowFormFocus', this.id) - }) - } - - private closeWindowForm() { - this.desktopRootDom.removeChild(this.dom) - this._data.dispose() - } - - public minimize() {} - public maximize() {} - public restore() {} - - public destroy() {} -} diff --git a/src/core/window/types/IWindowFormConfig.ts b/src/core/window/types/IWindowFormConfig.ts deleted file mode 100644 index 34c95cf..0000000 --- a/src/core/window/types/IWindowFormConfig.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 窗体配置信息 - */ -export interface IWindowFormConfig { - /** - * 窗体名称 - */ - name: string; - /** - * 窗体标题 - */ - title?: string; - /** - * 窗体图标 - */ - icon?: string; - top?: number; - left?: number; - /** - * 窗体宽度 - */ - width?: number; - widthAuto?: boolean; - /** - * 窗体高度 - */ - height?: number; - heightAuto?: boolean; - /** - * 窗体最小宽度 - */ - minWidth?: number; - /** - * 窗体最小高度 - */ - minHeight?: number; - /** - * 窗体最大宽度 - */ - maxWidth?: number; - /** - * 窗体最大高度 - */ - maxHeight?: number; - /** - * 窗体透明度 - */ - opacity?: number; - windowStyle?: string; - windowState?: number; - resizeMode?: number; - topMost?: boolean; - /** - * 是否显示在任务栏 - */ - showInTaskbar?: boolean; - showTitleBarIcon?: boolean; - showTitleBarText?: boolean; - hideTitleBar?: boolean; -} \ No newline at end of file diff --git a/src/core/window/types/WindowFormTypes.ts b/src/core/window/types/WindowFormTypes.ts deleted file mode 100644 index 494c7f8..0000000 --- a/src/core/window/types/WindowFormTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 窗体位置坐标 - 左上角 - */ -export interface WindowFormPos { - x: number; - y: number; -} - -/** 窗口状态 */ -export type TWindowFormState = 'default' | 'minimized' | 'maximized'; diff --git a/src/core/window/ui/WindowFormElement.ts b/src/core/window/ui/WindowFormElement.ts deleted file mode 100644 index 0721d66..0000000 --- a/src/core/window/ui/WindowFormElement.ts +++ /dev/null @@ -1,904 +0,0 @@ -import { LitElement, html, css, unsafeCSS } from 'lit' -import { customElement, property } from 'lit/decorators.js'; -import wfStyle from './css/wf.scss?inline' -import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' -import { wfem } from '@/core/events/WindowFormEventManager.ts' -import type { IObservable } from '@/core/state/IObservable.ts' -import type { IWindowFormDataState } from '@/core/window/impl/WindowFormImpl.ts' - -/** 拖拽移动开始的回调 */ -type TDragStartCallback = (x: number, y: number) => void; -/** 拖拽移动中的回调 */ -type TDragMoveCallback = (x: number, y: number) => void; -/** 拖拽移动结束的回调 */ -type TDragEndCallback = (x: number, y: number) => void; - -/** 拖拽调整尺寸的方向 */ -type TResizeDirection = - | 't' // 上 - | 'b' // 下 - | 'l' // 左 - | 'r' // 右 - | 'tl' // 左上 - | 'tr' // 右上 - | 'bl' // 左下 - | 'br'; // 右下 - -/** 拖拽调整尺寸回调数据 */ -interface IResizeCallbackData { - /** 宽度 */ - width: number; - /** 高度 */ - height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; - /** 拖拽调整尺寸的方向 */ - direction: TResizeDirection | null; -} - -/** 元素边界 */ -interface IElementRect { - /** 宽度 */ - width: number; - /** 高度 */ - height: number; - /** 顶点坐标(相对 offsetParent) */ - top: number; - /** 左点坐标(相对 offsetParent) */ - left: number; -} - -export interface WindowFormEventMap extends HTMLElementEventMap { - 'windowForm:dragStart': CustomEvent; - 'windowForm:dragMove': CustomEvent; - 'windowForm:dragEnd': CustomEvent; - 'windowForm:resizeStart': CustomEvent; - 'windowForm:resizeMove': CustomEvent; - 'windowForm:resizeEnd': CustomEvent; - 'windowForm:stateChange': CustomEvent<{ state: TWindowFormState }>; - 'windowForm:stateChange:minimize': CustomEvent<{ state: TWindowFormState }>; - 'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>; - 'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>; - 'windowForm:close': CustomEvent; -} - -@customElement('window-form-element') -export class WindowFormElement extends LitElement { - // ==== 公共属性 ==== - @property({ type: String }) wid: string - @property({ type: String }) override title = 'Window' - @property({ type: Boolean }) resizable = true - @property({ type: Boolean }) minimizable = true - @property({ type: Boolean }) maximizable = true - @property({ type: Boolean }) closable = true - @property({ type: Boolean, reflect: true }) focused: boolean = true - @property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default' - @property({ type: Object }) dragContainer?: HTMLElement - @property({ type: Boolean }) allowOverflow = true // 允许窗口超出容器 - @property({ type: Number }) snapDistance = 0 // 吸附距离 - @property({ type: Boolean }) snapAnimation = true // 吸附动画 - @property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms - @property({ type: Number }) maxWidth?: number = Infinity - @property({ type: Number }) minWidth?: number = 0 - @property({ type: Number }) maxHeight?: number = Infinity - @property({ type: Number }) minHeight?: number = 0 - @property({ type: String }) taskbarElementId?: string - @property({ type: Object }) wfData: IObservable; - - private _listeners: Array<{ type: string; original: Function; wrapped: EventListener }> = [] - - // ==== 拖拽/缩放状态(内部变量,不触发渲染) ==== - private dragging = false - private resizeDir: TResizeDirection | null = null - private startX = 0 - private startY = 0 - private startWidth = 0 - private startHeight = 0 - private startX_host = 0 - private startY_host = 0 - - private x = 0 - private y = 0 - private preX = 0 - private preY = 0 - private width = 640 - private height = 360 - private animationFrame?: number - private resizing = false - - // private get x() { - // return this.wfData.state.x - // } - // private set x(value: number) { - // this.wfData.patch({ x: value }) - // } - // private get y() { - // return this.wfData.state.y - // } - // private set y(value: number) { - // this.wfData.patch({ y: value }) - // } - - // private windowFormState: TWindowFormState = 'default'; - /** 元素信息 */ - private targetBounds: IElementRect - /** 最小化前的元素信息 */ - private targetPreMinimizeBounds?: IElementRect - /** 最大化前的元素信息 */ - private targetPreMaximizedBounds?: IElementRect - - static override styles = css` - ${unsafeCSS(wfStyle)} - ` - - protected override createRenderRoot() { - const root = this.attachShadow({ mode: 'closed' }) - const sheet = new CSSStyleSheet() - sheet.replaceSync(wfStyle) - root.adoptedStyleSheets = [sheet] - return root - } - - public addManagedEventListener( - type: K, - handler: (this: WindowFormElement, ev: WindowFormEventMap[K]) => any, - options?: boolean | AddEventListenerOptions - ): void - public addManagedEventListener( - type: K, - handler: (ev: WindowFormEventMap[K]) => any, - options?: boolean | AddEventListenerOptions - ): void - /** - * 添加受管理的事件监听 - * @param type 事件类型 - * @param handler 事件处理函数 - */ - public addManagedEventListener( - type: K, - handler: - | ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any) - | ((ev: WindowFormEventMap[K]) => any), - options?: boolean | AddEventListenerOptions - ) { - const wrapped: EventListener = (ev: Event) => { - (handler as any).call(this, ev as WindowFormEventMap[K]) - } - - this.addEventListener(type, wrapped, options) - this._listeners.push({ type, original: handler, wrapped }) - } - - public removeManagedEventListener( - type: K, - handler: - | ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any) - | ((ev: WindowFormEventMap[K]) => any) - ) { - const index = this._listeners.findIndex( - l => l.type === type && l.original === handler - ) - if (index !== -1) { - const { type: t, wrapped } = this._listeners[index] - this.removeEventListener(t, wrapped) - this._listeners.splice(index, 1) - } - } - - /** - * 移除所有受管理事件监听 - */ - public removeAllManagedListeners() { - for (const { type, wrapped } of this._listeners) { - this.removeEventListener(type, wrapped) - } - this._listeners = [] - } - - override firstUpdated() { - wfem.addEventListener('windowFormFocus', this.windowFormFocusFun) - window.addEventListener('pointerup', this.onPointerUp) - window.addEventListener('pointermove', this.onPointerMove) - this.addEventListener('pointerdown', this.toggleFocus) - - const container = this.dragContainer || document.body - const containerRect = container.getBoundingClientRect() - this.x = containerRect.width / 2 - this.width / 2 - this.y = containerRect.height / 2 - this.height / 2 - this.style.width = `${this.width}px` - this.style.height = `${this.height}px` - this.style.transform = `translate(${this.x}px, ${this.y}px)` - - this.targetBounds = { - width: this.offsetWidth, - height: this.offsetHeight, - top: this.x, - left: this.y, - } - } - - override disconnectedCallback() { - super.disconnectedCallback() - window.removeEventListener('pointerup', this.onPointerUp) - window.removeEventListener('pointermove', this.onPointerMove) - this.removeEventListener('pointerdown', this.toggleFocus) - wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun) - this.removeAllManagedListeners() - } - - private windowFormFocusFun = (id: string) => { - if (id === this.wid) { - this.focused = true - } else { - this.focused = false - } - } - - private toggleFocus = () => { - this.focused = !this.focused - wfem.notifyEvent('windowFormFocus', this.wid) - this.dispatchEvent( - new CustomEvent('update:focused', { - detail: this.focused, - bubbles: true, - composed: true, - }), - ) - } - - // ====== 拖拽 ====== - private onTitlePointerDown = (e: PointerEvent) => { - if (e.pointerType === 'mouse' && e.button !== 0) return - if ((e.target as HTMLElement).closest('.controls')) return - - e.preventDefault() - - this.dragging = true - this.startX = e.clientX - this.startY = e.clientY - this.preX = this.x - this.preY = this.y - this.setPointerCapture?.(e.pointerId) - - this.dispatchEvent( - new CustomEvent('windowForm:dragStart', { - detail: { x: this.x, y: this.y }, - bubbles: true, - composed: true, - }), - ) - } - - private onPointerMove = (e: PointerEvent) => { - if (this.dragging) { - const dx = e.clientX - this.startX - const dy = e.clientY - this.startY - - const x = this.preX + dx - const y = this.preY + dy - - this.applyPosition(x, y, false) - this.dispatchEvent( - new CustomEvent('windowForm:dragMove', { - detail: { x, y }, - bubbles: true, - composed: true, - }), - ) - } else if (this.resizeDir) { - this.performResize(e) - } - } - - private onPointerUp = (e: PointerEvent) => { - if (this.dragging) { - this.dragUp(e) - } - if (this.resizeDir) { - this.resizeUp(e) - } - this.dragging = false - this.resizing = false - this.resizeDir = null - document.body.style.cursor = '' - try { - this.releasePointerCapture?.(e.pointerId) - } catch {} - } - - /** 获取所有吸附点 */ - private getSnapPoints() { - const snapPoints = { x: [] as number[], y: [] as number[] } - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - const rect = this.getBoundingClientRect() - snapPoints.x = [0, containerRect.width - rect.width] - snapPoints.y = [0, containerRect.height - rect.height] - return snapPoints - } - - /** - * 获取最近的吸附点 - * @param x 左上角起始点x - * @param y 左上角起始点y - */ - private applySnapping(x: number, y: number) { - let snappedX = x, - snappedY = y - const containerSnap = this.getSnapPoints() - if (this.snapDistance > 0) { - for (const sx of containerSnap.x) - if (Math.abs(x - sx) <= this.snapDistance) { - snappedX = sx - break - } - for (const sy of containerSnap.y) - if (Math.abs(y - sy) <= this.snapDistance) { - snappedY = sy - break - } - } - return { x: snappedX, y: snappedY } - } - - private dragUp(e: PointerEvent) { - const snapped = this.applySnapping(this.x, this.y) - if (this.snapAnimation) { - this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { - this.updateTargetBounds(snapped.x, snapped.y) - this.dispatchEvent( - new CustomEvent('windowForm:dragEnd', { - detail: { x: snapped.x, y: snapped.y }, - bubbles: true, - composed: true, - }), - ) - }) - } else { - this.applyPosition(snapped.x, snapped.y, true) - this.updateTargetBounds(snapped.x, snapped.y) - this.dispatchEvent( - new CustomEvent('windowForm:dragEnd', { - detail: { x: snapped.x, y: snapped.y }, - bubbles: true, - composed: true, - }), - ) - } - } - - private applyPosition(x: number, y: number, isFinal: boolean) { - this.x = x - this.y = y - this.style.transform = `translate(${x}px, ${y}px)` - if (isFinal) this.applyBoundary() - } - - private applyBoundary() { - if (this.allowOverflow) return - let { x, y } = { x: this.x, y: this.y } - - const rect = this.getBoundingClientRect() - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - x = Math.min(Math.max(x, 0), containerRect.width - rect.width) - y = Math.min(Math.max(y, 0), containerRect.height - rect.height) - - this.applyPosition(x, y, false) - } - - private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) { - if (this.animationFrame) cancelAnimationFrame(this.animationFrame) - const startX = this.x - const startY = this.y - const deltaX = targetX - startX - const deltaY = targetY - startY - const startTime = performance.now() - - const step = (now: number) => { - const elapsed = now - startTime - const progress = Math.min(elapsed / duration, 1) - const ease = 1 - Math.pow(1 - progress, 3) - - const x = startX + deltaX * ease - const y = startY + deltaY * ease - - this.applyPosition(x, y, false) - this.dispatchEvent( - new CustomEvent('windowForm:dragMove', { - detail: { x, y }, - bubbles: true, - composed: true, - }), - ) - - if (progress < 1) { - this.animationFrame = requestAnimationFrame(step) - } else { - this.applyPosition(targetX, targetY, true) - onComplete?.() - } - } - this.animationFrame = requestAnimationFrame(step) - } - - // ====== 缩放 ====== - private startResize = (dir: TResizeDirection, e: PointerEvent) => { - if (!this.resizable) return - if (e.pointerType === 'mouse' && e.button !== 0) return - - e.preventDefault() - e.stopPropagation() - this.resizing = true - this.resizeDir = dir - this.startX = e.clientX - this.startY = e.clientY - - const rect = this.getBoundingClientRect() - this.startWidth = rect.width - this.startHeight = rect.height - this.startX_host = rect.left - this.startY_host = rect.top - - const target = e.target as HTMLElement - document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor - - this.setPointerCapture?.(e.pointerId) - this.dispatchEvent( - new CustomEvent('windowForm:resizeStart', { - detail: { x: this.x, y: this.y, width: this.width, height: this.height, dir }, - bubbles: true, - composed: true, - }), - ) - } - - private performResize(e: PointerEvent) { - if (!this.resizeDir || !this.resizing) return - - let newWidth = this.startWidth - let newHeight = this.startHeight - let newX = this.startX_host - let newY = this.startY_host - - const dx = e.clientX - this.startX - const dy = e.clientY - this.startY - - switch (this.resizeDir) { - case 'r': - newWidth += dx - break - case 'b': - newHeight += dy - break - case 'l': - newWidth -= dx - newX += dx - break - case 't': - newHeight -= dy - newY += dy - break - case 'tl': - newWidth -= dx - newX += dx - newHeight -= dy - newY += dy - break - case 'tr': - newWidth += dx - newHeight -= dy - newY += dy - break - case 'bl': - newWidth -= dx - newX += dx - newHeight += dy - break - case 'br': - newWidth += dx - newHeight += dy - break - } - - const d = this.applyResizeBounds(newX, newY, newWidth, newHeight) - - this.dispatchEvent( - new CustomEvent('windowForm:resizeMove', { - detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top }, - bubbles: true, - composed: true, - }), - ) - } - - /** - * 应用尺寸调整边界 - * @param newX 新的X坐标 - * @param newY 新的Y坐标 - * @param newWidth 新的宽度 - * @param newHeight 新的高度 - * @private - */ - private applyResizeBounds( - newX: number, - newY: number, - newWidth: number, - newHeight: number, - ): { - left: number - top: number - width: number - height: number - } { - // 最小/最大宽高限制 - if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth) - if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth) - if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight) - if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight) - - // 边界限制 - if (this.allowOverflow) { - this.x = newX - this.y = newY - this.width = newWidth - this.height = newHeight - this.style.width = `${newWidth}px` - this.style.height = `${newHeight}px` - this.applyPosition(newX, newY, false) - - return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, - } - } - - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - newX = Math.min(Math.max(0, newX), containerRect.width - newWidth) - newY = Math.min(Math.max(0, newY), containerRect.height - newHeight) - - this.x = newX - this.y = newY - this.width = newWidth - this.height = newHeight - this.style.width = `${newWidth}px` - this.style.height = `${newHeight}px` - this.applyPosition(newX, newY, false) - - return { - left: newX, - top: newY, - width: newWidth, - height: newHeight, - } - } - - private resizeUp(e: PointerEvent) { - if (!this.resizable) return - - this.updateTargetBounds(this.x, this.y, this.width, this.height) - this.dispatchEvent( - new CustomEvent('windowForm:resizeEnd', { - detail: { - dir: this.resizeDir, - width: this.width, - height: this.height, - left: this.x, - top: this.y, - }, - bubbles: true, - composed: true, - }), - ) - } - - // ====== 窗口操作 ====== - // 最小化到任务栏 - private minimize() { - if (!this.taskbarElementId) return - if (this.windowFormState === 'minimized') return - this.targetPreMinimizeBounds = { ...this.targetBounds } - this.windowFormState = 'minimized' - - const taskbar = document.querySelector(this.taskbarElementId) - if (!taskbar) throw new Error('任务栏元素未找到') - - const rect = taskbar.getBoundingClientRect() - const startX = this.x - const startY = this.y - const startW = this.offsetWidth - const startH = this.offsetHeight - - this.animateWindow( - startX, - startY, - startW, - startH, - rect.left, - rect.top, - rect.width, - rect.height, - 400, - () => { - this.style.display = 'none' - this.dispatchEvent( - new CustomEvent('windowForm:stateChange:minimize', { - detail: { state: this.windowFormState }, - bubbles: true, - composed: true, - }), - ) - }, - ) - } - - /** 最大化 */ - private maximize() { - if (this.windowFormState === 'maximized') { - this.restore() - return - } - this.targetPreMaximizedBounds = { ...this.targetBounds } - this.windowFormState = 'maximized' - - const rect = this.getBoundingClientRect() - - const startX = this.x - const startY = this.y - const startW = rect.width - const startH = rect.height - - const targetX = 0 - const targetY = 0 - const containerRect = (this.dragContainer || document.body).getBoundingClientRect() - const targetW = containerRect?.width ?? window.innerWidth - const targetH = containerRect?.height ?? window.innerHeight - - this.animateWindow( - startX, - startY, - startW, - startH, - targetX, - targetY, - targetW, - targetH, - 300, - () => { - this.dispatchEvent( - new CustomEvent('windowForm:stateChange:maximize', { - detail: { state: this.windowFormState }, - bubbles: true, - composed: true, - }), - ) - }, - ) - } - - /** 恢复到默认窗体状态 */ - private restore(onComplete?: () => void) { - if (this.windowFormState === 'default') return - let b: IElementRect - if ( - (this.windowFormState as TWindowFormState) === 'minimized' && - this.targetPreMinimizeBounds - ) { - // 最小化恢复,恢复到最小化前的状态 - b = this.targetPreMinimizeBounds - } else if ( - (this.windowFormState as TWindowFormState) === 'maximized' && - this.targetPreMaximizedBounds - ) { - // 最大化恢复,恢复到最大化前的默认状态 - b = this.targetPreMaximizedBounds - } else { - b = this.targetBounds - } - - this.windowFormState = 'default' - - this.style.display = 'block' - - const startX = this.x - const startY = this.y - const startW = this.offsetWidth - const startH = this.offsetHeight - - this.animateWindow( - startX, - startY, - startW, - startH, - b.left, - b.top, - b.width, - b.height, - 300, - () => { - onComplete?.() - this.dispatchEvent( - new CustomEvent('windowForm:stateChange:restore', { - detail: { state: this.windowFormState }, - bubbles: true, - composed: true, - }), - ) - }, - ) - } - - private windowFormClose() { - this.dispatchEvent( - new CustomEvent('windowForm:close', { - bubbles: true, - composed: true, - }), - ) - this.wfData.state.closed = true - } - - /** - * 窗体最大化、最小化和恢复默认 动画 - * @param startX - * @param startY - * @param startW - * @param startH - * @param targetX - * @param targetY - * @param targetW - * @param targetH - * @param duration - * @param onComplete - * @private - */ - private animateWindow( - startX: number, - startY: number, - startW: number, - startH: number, - targetX: number, - targetY: number, - targetW: number, - targetH: number, - duration: number, - onComplete?: () => void, - ) { - const startTime = performance.now() - const step = (now: number) => { - const elapsed = now - startTime - const progress = Math.min(elapsed / duration, 1) - const ease = 1 - Math.pow(1 - progress, 3) - - const x = startX + (targetX - startX) * ease - const y = startY + (targetY - startY) * ease - const w = startW + (targetW - startW) * ease - const h = startH + (targetH - startH) * ease - - this.style.width = `${w}px` - this.style.height = `${h}px` - this.applyPosition(x, y, false) - - if (progress < 1) { - requestAnimationFrame(step) - } else { - this.style.width = `${targetW}px` - this.style.height = `${targetH}px` - this.applyPosition(targetX, targetY, true) - onComplete?.() - this.dispatchEvent( - new CustomEvent('windowForm:stateChange', { - detail: { state: this.windowFormState }, - bubbles: true, - composed: true, - }), - ) - } - } - requestAnimationFrame(step) - } - - private updateTargetBounds(left: number, top: number, width?: number, height?: number) { - this.targetBounds = { - left, - top, - width: width ?? this.offsetWidth, - height: height ?? this.offsetHeight, - } - } - - // ====== 渲染 ====== - override render() { - return html` -
-
-
${this.title}
-
- ${this.minimizable - ? html`` - : null} - ${this.maximizable - ? html`` - : null} - ${this.closable - ? html`` - : null} -
-
- -
- - ${this.resizable - ? html` -
this.startResize('t', e)} - >
-
this.startResize('b', e)} - >
-
this.startResize('r', e)} - >
-
this.startResize('l', e)} - >
-
this.startResize('tr', e)} - >
-
this.startResize('tl', e)} - >
-
this.startResize('br', e)} - >
-
this.startResize('bl', e)} - >
- ` - : null} -
- ` - } -} - -declare global { - interface HTMLElementTagNameMap { - 'window-form-element': WindowFormElement; - } - - interface WindowFormElementEventMap extends WindowFormEventMap {} -} diff --git a/src/core/window/ui/css/wf.scss b/src/core/window/ui/css/wf.scss deleted file mode 100644 index 86200f9..0000000 --- a/src/core/window/ui/css/wf.scss +++ /dev/null @@ -1,101 +0,0 @@ -*, -*::before, -*::after { - box-sizing: border-box; /* 使用更直观的盒模型 */ - margin: 0; - padding: 0; -} - -:host { - position: absolute; - top: 0; - left: 0; - display: block; - z-index: 10; - user-select: none; - --titlebar-height: 32px; - --shadow: 0 10px 30px rgba(0,0,0,0.25); - font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial; -} - -:host([focused]) { - z-index: 11; - .window { - border-color: #8338ec; - } -} - -:host([windowFormState='maximized']) { - .window { - border-radius: 0; - box-shadow: none; - } -} - -.window { - position: absolute; - box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.25)); - background: linear-gradient(#ffffff, #f6f6f6); - border: 1px solid rgba(0,0,0,0.08); - border-radius: 6px; - overflow: hidden; - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - - &.focus { - border-color: #3a86ff; - } -} - -.titlebar { - height: var(--titlebar-height); - display: flex; - align-items: center; - padding: 0 8px; - gap: 8px; - background: linear-gradient(#f2f2f2, #e9e9e9); - border-bottom: 1px solid rgba(0,0,0,0.06); - - .title { - font-size: 13px; - font-weight: 600; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: #111; - } - - .controls { - display: flex; gap: 6px; - - button.ctrl { - width: 34px; - height: 24px; - border: none; - background: transparent; - border-radius: 4px; - cursor: pointer; - font-weight: 600; - font-size: 12px; - - &:hover { - background: rgba(0,0,0,0.06); - } - } - } -} - -.content { flex: 1; overflow: auto; padding: 12px; background: transparent; } - -.resizer { position: absolute; z-index: 20; } -.resizer.t { height: 6px; left: 0; right: 0; top: -3px; cursor: ns-resize; } -.resizer.b { height: 6px; left: 0; right: 0; bottom: -3px; cursor: ns-resize; } -.resizer.r { width: 6px; top: 0; bottom: 0; right: -3px; cursor: ew-resize; } -.resizer.l { width: 6px; top: 0; bottom: 0; left: -3px; cursor: ew-resize; } -.resizer.tr { width: 12px; height: 12px; right: -6px; top: -6px; cursor: nesw-resize; } -.resizer.tl { width: 12px; height: 12px; left: -6px; top: -6px; cursor: nwse-resize; } -.resizer.br { width: 12px; height: 12px; right: -6px; bottom: -6px; cursor: nwse-resize; } -.resizer.bl { width: 12px; height: 12px; left: -6px; bottom: -6px; cursor: nesw-resize; } diff --git a/src/core/window/ui/window-form-helper.ts b/src/core/window/ui/window-form-helper.ts deleted file mode 100644 index 957cde9..0000000 --- a/src/core/window/ui/window-form-helper.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { WindowFormEventMap } from '@/core/window/ui/WindowFormElement.ts' - -export function addWindowFormEventListener( - el: HTMLElement, - type: K, - listener: (ev: WindowFormEventMap[K]) => any, - options?: boolean | AddEventListenerOptions -) { - // 强制类型转换,保证 TS 不报错 - el.addEventListener(type, listener as EventListener, options); -} - -export function removeWindowFormEventListener( - el: HTMLElement, - type: K, - listener: (ev: WindowFormEventMap[K]) => any, - options?: boolean | EventListenerOptions -) { - el.removeEventListener(type, listener as EventListener, options); -} diff --git a/src/main.ts b/src/main.ts index 98ef18d..ad0d341 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,62 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { naiveUi } from '@/common/naive-ui/components.ts' +import { SystemServiceIntegration } from '@/services/SystemServiceIntegration' +import { registerBuiltInApps } from '@/apps' import 'virtual:uno.css' import './css/basic.css' import App from './ui/App.vue' +// 注册内置应用 +registerBuiltInApps() + +// 初始化系统服务 +const systemService = new SystemServiceIntegration({ + debug: import.meta.env.DEV, + enablePerformanceMonitoring: true, + enableSecurityAudit: true +}) + +// 创建应用实例 const app = createApp(App) +// 注册插件 app.use(createPinia()) app.use(naiveUi) -app.mount('#app') +// 提供系统服务给组件使用 +app.provide('systemService', systemService) + +// 初始化系统服务然后挂载应用 +systemService.initialize() + .then(() => { + app.mount('#app') + console.log('桌面系统启动完成') + }) + .catch((error) => { + console.error('系统启动失败:', error) + // 显示错误信息 + document.body.innerHTML = ` +
+
+

系统启动失败

+

错误信息: ${error.message}

+ +
+
+ ` + }) + +// 全局错误处理 +app.config.errorHandler = (error, instance, info) => { + console.error('Vue应用错误:', error, info) +} + +// 在页面卸载时清理系统服务 +window.addEventListener('beforeunload', () => { + systemService.shutdown() +}) diff --git a/src/sdk/index.ts b/src/sdk/index.ts new file mode 100644 index 0000000..930b85c --- /dev/null +++ b/src/sdk/index.ts @@ -0,0 +1,702 @@ +import type { + SystemDesktopSDK, + SDKConfig, + APIResponse, + WindowSDK, + StorageSDK, + NetworkSDK, + EventSDK, + UISDK, + SystemSDK, + WindowState, + WindowEvents, + StorageEvents, + NetworkRequestConfig, + NetworkResponse, + EventMessage, + EventSubscriptionConfig, + DialogOptions, + NotificationOptions, + FilePickerOptions, + SystemInfo, + AppInfo, + PermissionStatus, +} from './types' + +/** + * SDK基础类 + */ +abstract class SDKBase { + protected appId: string = '' + protected initialized: boolean = false + + /** + * 发送消息到系统 + */ + protected sendToSystem(type: string, data?: any): Promise { + return new Promise((resolve, reject) => { + const requestId = Date.now().toString() + Math.random().toString(36).substr(2, 9) + + const handler = (event: MessageEvent) => { + if (event.data?.type === 'system:response' && event.data?.requestId === requestId) { + window.removeEventListener('message', handler) + if (event.data.success) { + resolve(event.data.data) + } else { + reject(new Error(event.data.error || '系统调用失败')) + } + } + } + + window.addEventListener('message', handler) + + // 发送消息到父窗口(系统) + window.parent.postMessage( + { + type: 'sdk:call', + requestId, + method: type, + data, + appId: this.appId, + }, + '*', + ) + + // 设置超时 + setTimeout(() => { + window.removeEventListener('message', handler) + reject(new Error('系统调用超时')) + }, 10000) + }) + } + + /** + * 包装API响应 + */ + protected wrapResponse(promise: Promise): Promise> { + return promise + .then((data) => ({ success: true, data })) + .catch((error) => ({ + success: false, + error: error.message || '未知错误', + code: error.code || -1, + })) + } +} + +/** + * 窗体SDK实现 + */ +class WindowSDKImpl extends SDKBase implements WindowSDK { + private eventListeners = new Map>() + + constructor(appId: string) { + super() + this.appId = appId + this.setupEventListeners() + } + + async setTitle(title: string): Promise> { + return this.wrapResponse(this.sendToSystem('window.setTitle', { title })) + } + + async resize(width: number, height: number): Promise> { + return this.wrapResponse(this.sendToSystem('window.resize', { width, height })) + } + + async move(x: number, y: number): Promise> { + return this.wrapResponse(this.sendToSystem('window.move', { x, y })) + } + + async minimize(): Promise> { + return this.wrapResponse(this.sendToSystem('window.minimize')) + } + + async maximize(): Promise> { + return this.wrapResponse(this.sendToSystem('window.maximize')) + } + + async restore(): Promise> { + return this.wrapResponse(this.sendToSystem('window.restore')) + } + + async fullscreen(): Promise> { + return this.wrapResponse(this.sendToSystem('window.fullscreen')) + } + + async close(): Promise> { + return this.wrapResponse(this.sendToSystem('window.close')) + } + + async getState(): Promise> { + return this.wrapResponse(this.sendToSystem('window.getState')) + } + + async getSize(): Promise> { + return this.wrapResponse(this.sendToSystem('window.getSize')) + } + + async getPosition(): Promise> { + return this.wrapResponse(this.sendToSystem('window.getPosition')) + } + + on(event: K, callback: WindowEvents[K]): void { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, new Set()) + } + this.eventListeners.get(event)!.add(callback) + } + + off(event: K, callback?: WindowEvents[K]): void { + if (callback) { + this.eventListeners.get(event)?.delete(callback) + } else { + this.eventListeners.delete(event) + } + } + + private setupEventListeners(): void { + window.addEventListener('message', (event) => { + if (event.data?.type?.startsWith('system:window:')) { + const eventType = event.data.type.replace('system:window:', '') as keyof WindowEvents + const listeners = this.eventListeners.get(eventType) + if (listeners) { + listeners.forEach((callback) => { + try { + callback(...(event.data.args || [])) + } catch (error) { + console.error('窗体事件处理错误:', error) + } + }) + } + } + }) + } +} + +/** + * 存储SDK实现 + */ +class StorageSDKImpl extends SDKBase implements StorageSDK { + private eventListeners = new Map>() + + constructor(appId: string) { + super() + this.appId = appId + this.setupEventListeners() + } + + async set(key: string, value: any): Promise> { + return this.wrapResponse(this.sendToSystem('storage.set', { key, value })) + } + + async get(key: string): Promise> { + return this.wrapResponse(this.sendToSystem('storage.get', { key })) + } + + async remove(key: string): Promise> { + return this.wrapResponse(this.sendToSystem('storage.remove', { key })) + } + + async clear(): Promise> { + return this.wrapResponse(this.sendToSystem('storage.clear')) + } + + async keys(): Promise> { + return this.wrapResponse(this.sendToSystem('storage.keys')) + } + + async has(key: string): Promise> { + return this.wrapResponse(this.sendToSystem('storage.has', { key })) + } + + async getStats(): Promise> { + return this.wrapResponse(this.sendToSystem('storage.getStats')) + } + + on(event: K, callback: StorageEvents[K]): void { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, new Set()) + } + this.eventListeners.get(event)!.add(callback) + } + + off(event: K, callback?: StorageEvents[K]): void { + if (callback) { + this.eventListeners.get(event)?.delete(callback) + } else { + this.eventListeners.delete(event) + } + } + + private setupEventListeners(): void { + window.addEventListener('message', (event) => { + if (event.data?.type?.startsWith('system:storage:')) { + const eventType = event.data.type.replace('system:storage:', '') as keyof StorageEvents + const listeners = this.eventListeners.get(eventType) + if (listeners) { + listeners.forEach((callback) => { + try { + callback(...(event.data.args || [])) + } catch (error) { + console.error('存储事件处理错误:', error) + } + }) + } + } + }) + } +} + +/** + * 网络SDK实现 + */ +class NetworkSDKImpl extends SDKBase implements NetworkSDK { + constructor(appId: string) { + super() + this.appId = appId + } + + async request( + url: string, + config?: NetworkRequestConfig, + ): Promise>> { + return this.wrapResponse(this.sendToSystem('network.request', { url, config })) + } + + async get( + url: string, + config?: Omit, + ): Promise>> { + return this.request(url, { ...config, method: 'GET' }) + } + + async post( + url: string, + data?: any, + config?: Omit, + ): Promise>> { + return this.request(url, { ...config, method: 'POST', body: data }) + } + + async put( + url: string, + data?: any, + config?: Omit, + ): Promise>> { + return this.request(url, { ...config, method: 'PUT', body: data }) + } + + async delete( + url: string, + config?: Omit, + ): Promise>> { + return this.request(url, { ...config, method: 'DELETE' }) + } + + async upload( + url: string, + file: File | Blob, + onProgress?: (loaded: number, total: number) => void, + ): Promise> { + return this.wrapResponse( + this.sendToSystem('network.upload', { url, file, hasProgressCallback: !!onProgress }), + ) + } + + async download( + url: string, + filename?: string, + onProgress?: (loaded: number, total: number) => void, + ): Promise> { + return this.wrapResponse( + this.sendToSystem('network.download', { url, filename, hasProgressCallback: !!onProgress }), + ) + } + + async isOnline(): Promise> { + return this.wrapResponse(this.sendToSystem('network.isOnline')) + } + + async getStats(): Promise< + APIResponse<{ requestCount: number; failureCount: number; averageTime: number }> + > { + return this.wrapResponse(this.sendToSystem('network.getStats')) + } +} + +/** + * 事件SDK实现 + */ +class EventSDKImpl extends SDKBase implements EventSDK { + private subscriptions = new Map() + + constructor(appId: string) { + super() + this.appId = appId + this.setupEventListeners() + } + + async emit(channel: string, data: T): Promise> { + return this.wrapResponse(this.sendToSystem('events.emit', { channel, data })) + } + + async on( + channel: string, + callback: (message: EventMessage) => void, + config?: EventSubscriptionConfig, + ): Promise> { + const result = await this.wrapResponse(this.sendToSystem('events.on', { channel, config })) + + if (result.success && result.data) { + this.subscriptions.set(result.data, callback) + } + + return result + } + + async off(subscriptionId: string): Promise> { + const result = await this.wrapResponse(this.sendToSystem('events.off', { subscriptionId })) + + if (result.success) { + this.subscriptions.delete(subscriptionId) + } + + return result + } + + async broadcast(channel: string, data: T): Promise> { + return this.wrapResponse(this.sendToSystem('events.broadcast', { channel, data })) + } + + async sendTo(targetAppId: string, data: T): Promise> { + return this.wrapResponse(this.sendToSystem('events.sendTo', { targetAppId, data })) + } + + async getSubscriberCount(channel: string): Promise> { + return this.wrapResponse(this.sendToSystem('events.getSubscriberCount', { channel })) + } + + async getChannels(): Promise> { + return this.wrapResponse(this.sendToSystem('events.getChannels')) + } + + private setupEventListeners(): void { + window.addEventListener('message', (event) => { + if (event.data?.type === 'system:event' && event.data?.subscriptionId) { + const callback = this.subscriptions.get(event.data.subscriptionId) + if (callback) { + try { + callback(event.data.message) + } catch (error) { + console.error('事件回调处理错误:', error) + } + } + } + }) + } +} + +/** + * UI SDK实现 + */ +class UISDKImpl extends SDKBase implements UISDK { + constructor(appId: string) { + super() + this.appId = appId + } + + async showDialog( + options: DialogOptions, + ): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showDialog', options)) + } + + async showNotification(options: NotificationOptions): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showNotification', options)) + } + + async showFilePicker(options?: FilePickerOptions): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showFilePicker', options)) + } + + async showSaveDialog(defaultName?: string, accept?: string): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showSaveDialog', { defaultName, accept })) + } + + async showToast( + message: string, + type?: 'info' | 'success' | 'warning' | 'error', + duration?: number, + ): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showToast', { message, type, duration })) + } + + async showLoading(message?: string): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showLoading', { message })) + } + + async hideLoading(id: string): Promise> { + return this.wrapResponse(this.sendToSystem('ui.hideLoading', { id })) + } + + async showProgress(options: { + title?: string + message?: string + progress: number + }): Promise> { + return this.wrapResponse(this.sendToSystem('ui.showProgress', options)) + } + + async updateProgress( + id: string, + progress: number, + message?: string, + ): Promise> { + return this.wrapResponse(this.sendToSystem('ui.updateProgress', { id, progress, message })) + } + + async hideProgress(id: string): Promise> { + return this.wrapResponse(this.sendToSystem('ui.hideProgress', { id })) + } +} + +/** + * 系统SDK实现 + */ +class SystemSDKImpl extends SDKBase implements SystemSDK { + constructor(appId: string) { + super() + this.appId = appId + } + + async getSystemInfo(): Promise> { + return this.wrapResponse(this.sendToSystem('system.getSystemInfo')) + } + + async getAppInfo(): Promise> { + return this.wrapResponse(this.sendToSystem('system.getAppInfo')) + } + + async requestPermission( + permission: string, + reason?: string, + ): Promise> { + return this.wrapResponse(this.sendToSystem('system.requestPermission', { permission, reason })) + } + + async checkPermission(permission: string): Promise> { + return this.wrapResponse(this.sendToSystem('system.checkPermission', { permission })) + } + + async getClipboard(): Promise> { + return this.wrapResponse(this.sendToSystem('system.getClipboard')) + } + + async setClipboard(text: string): Promise> { + return this.wrapResponse(this.sendToSystem('system.setClipboard', { text })) + } + + async openExternal(url: string): Promise> { + return this.wrapResponse(this.sendToSystem('system.openExternal', { url })) + } + + async getCurrentTime(): Promise> { + const result = await this.wrapResponse(this.sendToSystem('system.getCurrentTime')) + if (result.success && result.data) { + result.data = new Date(result.data) + } + return result + } + + async generateUUID(): Promise> { + return this.wrapResponse(this.sendToSystem('system.generateUUID')) + } + + async exit(): Promise> { + return this.wrapResponse(this.sendToSystem('system.exit')) + } +} + +/** + * 主SDK实现类 + */ +class SystemDesktopSDKImpl implements SystemDesktopSDK { + readonly version: string = '1.0.0' + + private _appId: string = '' + private _initialized: boolean = false + + private _window!: WindowSDK + private _storage!: StorageSDK + private _network!: NetworkSDK + private _events!: EventSDK + private _ui!: UISDK + private _system!: SystemSDK + + get appId(): string { + return this._appId + } + + get initialized(): boolean { + return this._initialized + } + + get window(): WindowSDK { + this.checkInitialized() + return this._window + } + + get storage(): StorageSDK { + this.checkInitialized() + return this._storage + } + + get network(): NetworkSDK { + this.checkInitialized() + return this._network + } + + get events(): EventSDK { + this.checkInitialized() + return this._events + } + + get ui(): UISDK { + this.checkInitialized() + return this._ui + } + + get system(): SystemSDK { + this.checkInitialized() + return this._system + } + + async init(config: SDKConfig): Promise> { + try { + if (this._initialized) { + return { success: false, error: 'SDK已初始化' } + } + + this._appId = config.appId + + // 初始化各个子模块 + this._window = new WindowSDKImpl(this._appId) + this._storage = new StorageSDKImpl(this._appId) + this._network = new NetworkSDKImpl(this._appId) + this._events = new EventSDKImpl(this._appId) + this._ui = new UISDKImpl(this._appId) + this._system = new SystemSDKImpl(this._appId) + + // 向系统注册应用 + const response = await this.sendToSystem('sdk.init', config) + + if (response.success) { + this._initialized = true + console.log(`SDK已初始化,应用ID: ${this._appId}`) + } + + return response + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '初始化失败', + } + } + } + + async destroy(): Promise> { + try { + if (!this._initialized) { + return { success: false, error: 'SDK未初始化' } + } + + const response = await this.sendToSystem('sdk.destroy', { appId: this._appId }) + + if (response.success) { + this._initialized = false + this._appId = '' + console.log('SDK已销毁') + } + + return response + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '销毁失败', + } + } + } + + async getStatus(): Promise< + APIResponse<{ initialized: boolean; connected: boolean; permissions: string[] }> + > { + try { + const response = await this.sendToSystem('sdk.getStatus', { appId: this._appId }) + return { + success: true, + data: { + initialized: this._initialized, + connected: response.data.connected || false, + permissions: response.data.permissions || [], + }, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '获取状态失败', + } + } + } + + private checkInitialized(): void { + if (!this._initialized) { + throw new Error('SDK未初始化,请先调用init()方法') + } + } + + private sendToSystem( + type: string, + data?: any, + ): Promise<{ success: boolean; data?: T; error?: string }> { + return new Promise((resolve, reject) => { + const requestId = Date.now().toString() + Math.random().toString(36).substr(2, 9) + + const handler = (event: MessageEvent) => { + if (event.data?.type === 'system:response' && event.data?.requestId === requestId) { + window.removeEventListener('message', handler) + resolve(event.data) + } + } + + window.addEventListener('message', handler) + + window.parent.postMessage( + { + type: 'sdk:call', + requestId, + method: type, + data, + appId: this._appId, + }, + '*', + ) + + setTimeout(() => { + window.removeEventListener('message', handler) + reject(new Error('系统调用超时')) + }, 10000) + }) + } +} + +// 创建全局SDK实例 +const SystemSDK = new SystemDesktopSDKImpl() + +// 导出SDK实例 +export default SystemSDK + +// 在window对象上挂载SDK +if (typeof window !== 'undefined') { + window.SystemSDK = SystemSDK +} diff --git a/src/sdk/types.ts b/src/sdk/types.ts new file mode 100644 index 0000000..89b8284 --- /dev/null +++ b/src/sdk/types.ts @@ -0,0 +1,638 @@ +/** + * 系统SDK主接口 + * 为第三方应用提供统一的系统服务访问接口 + */ + +// ============================================================================= +// 核心类型定义 +// ============================================================================= + +/** + * SDK初始化配置 + */ +export interface SDKConfig { + appId: string + appName: string + version: string + permissions: string[] + debug?: boolean +} + +/** + * API响应结果包装器 + */ +export interface APIResponse { + success: boolean + data?: T + error?: string + code?: number +} + +/** + * 事件回调函数类型 + */ +export type EventCallback = (data: T) => void | Promise + +/** + * 权限状态枚举 + */ +export enum PermissionStatus { + GRANTED = 'granted', + DENIED = 'denied', + PROMPT = 'prompt' +} + +// ============================================================================= +// 窗体SDK接口 +// ============================================================================= + +/** + * 窗体状态 + */ +export enum WindowState { + NORMAL = 'normal', + MINIMIZED = 'minimized', + MAXIMIZED = 'maximized', + FULLSCREEN = 'fullscreen' +} + +/** + * 窗体事件类型 + */ +export interface WindowEvents { + onResize: (width: number, height: number) => void + onMove: (x: number, y: number) => void + onStateChange: (state: WindowState) => void + onFocus: () => void + onBlur: () => void + onClose: () => void +} + +/** + * 窗体SDK接口 + */ +export interface WindowSDK { + /** + * 设置窗体标题 + */ + setTitle(title: string): Promise> + + /** + * 调整窗体尺寸 + */ + resize(width: number, height: number): Promise> + + /** + * 移动窗体位置 + */ + move(x: number, y: number): Promise> + + /** + * 最小化窗体 + */ + minimize(): Promise> + + /** + * 最大化窗体 + */ + maximize(): Promise> + + /** + * 还原窗体 + */ + restore(): Promise> + + /** + * 全屏显示 + */ + fullscreen(): Promise> + + /** + * 关闭窗体 + */ + close(): Promise> + + /** + * 获取当前窗体状态 + */ + getState(): Promise> + + /** + * 获取窗体尺寸 + */ + getSize(): Promise> + + /** + * 获取窗体位置 + */ + getPosition(): Promise> + + /** + * 监听窗体事件 + */ + on(event: K, callback: WindowEvents[K]): void + + /** + * 移除事件监听器 + */ + off(event: K, callback?: WindowEvents[K]): void +} + +// ============================================================================= +// 存储SDK接口 +// ============================================================================= + +/** + * 存储事件类型 + */ +export interface StorageEvents { + onChange: (key: string, newValue: any, oldValue: any) => void + onQuotaExceeded: (usedSpace: number, maxSpace: number) => void +} + +/** + * 存储使用统计 + */ +export interface StorageStats { + usedSpace: number // 已使用空间(MB) + maxSpace: number // 最大空间(MB) + keysCount: number // 键数量 + lastAccessed: Date +} + +/** + * 存储SDK接口 + */ +export interface StorageSDK { + /** + * 存储数据 + */ + set(key: string, value: any): Promise> + + /** + * 获取数据 + */ + get(key: string): Promise> + + /** + * 删除数据 + */ + remove(key: string): Promise> + + /** + * 清空所有数据 + */ + clear(): Promise> + + /** + * 获取所有键名 + */ + keys(): Promise> + + /** + * 检查键是否存在 + */ + has(key: string): Promise> + + /** + * 获取存储使用统计 + */ + getStats(): Promise> + + /** + * 监听存储变化 + */ + on(event: K, callback: StorageEvents[K]): void + + /** + * 移除事件监听器 + */ + off(event: K, callback?: StorageEvents[K]): void +} + +// ============================================================================= +// 网络SDK接口 +// ============================================================================= + +/** + * HTTP方法 + */ +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' + +/** + * 网络请求配置 + */ +export interface NetworkRequestConfig { + method?: HTTPMethod + headers?: Record + body?: any + timeout?: number + responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' +} + +/** + * 网络响应 + */ +export interface NetworkResponse { + data: T + status: number + statusText: string + headers: Record + url: string +} + +/** + * 上传进度回调 + */ +export type UploadProgressCallback = (loaded: number, total: number) => void + +/** + * 下载进度回调 + */ +export type DownloadProgressCallback = (loaded: number, total: number) => void + +/** + * 网络SDK接口 + */ +export interface NetworkSDK { + /** + * 发送HTTP请求 + */ + request(url: string, config?: NetworkRequestConfig): Promise>> + + /** + * GET请求 + */ + get(url: string, config?: Omit): Promise>> + + /** + * POST请求 + */ + post(url: string, data?: any, config?: Omit): Promise>> + + /** + * PUT请求 + */ + put(url: string, data?: any, config?: Omit): Promise>> + + /** + * DELETE请求 + */ + delete(url: string, config?: Omit): Promise>> + + /** + * 上传文件 + */ + upload(url: string, file: File | Blob, onProgress?: UploadProgressCallback): Promise> + + /** + * 下载文件 + */ + download(url: string, filename?: string, onProgress?: DownloadProgressCallback): Promise> + + /** + * 检查网络状态 + */ + isOnline(): Promise> + + /** + * 获取网络请求统计 + */ + getStats(): Promise> +} + +// ============================================================================= +// 事件SDK接口 +// ============================================================================= + +/** + * 事件消息 + */ +export interface EventMessage { + id: string + channel: string + data: T + senderId: string + timestamp: Date +} + +/** + * 事件订阅配置 + */ +export interface EventSubscriptionConfig { + filter?: (message: EventMessage) => boolean + once?: boolean // 只监听一次 +} + +/** + * 事件SDK接口 + */ +export interface EventSDK { + /** + * 发送事件消息 + */ + emit(channel: string, data: T): Promise> + + /** + * 订阅事件频道 + */ + on( + channel: string, + callback: (message: EventMessage) => void, + config?: EventSubscriptionConfig + ): Promise> + + /** + * 取消订阅 + */ + off(subscriptionId: string): Promise> + + /** + * 广播消息 + */ + broadcast(channel: string, data: T): Promise> + + /** + * 发送点对点消息 + */ + sendTo(targetAppId: string, data: T): Promise> + + /** + * 获取频道订阅者数量 + */ + getSubscriberCount(channel: string): Promise> + + /** + * 获取可用频道列表 + */ + getChannels(): Promise> +} + +// ============================================================================= +// UI SDK接口 +// ============================================================================= + +/** + * 对话框类型 + */ +export enum DialogType { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error', + CONFIRM = 'confirm' +} + +/** + * 对话框选项 + */ +export interface DialogOptions { + title?: string + message: string + type?: DialogType + buttons?: string[] + defaultButton?: number + cancelButton?: number +} + +/** + * 通知选项 + */ +export interface NotificationOptions { + title: string + body: string + icon?: string + duration?: number // 显示时长(毫秒) + actions?: Array<{ title: string; action: string }> +} + +/** + * 文件选择选项 + */ +export interface FilePickerOptions { + accept?: string // 文件类型过滤 + multiple?: boolean // 是否多选 + directory?: boolean // 是否选择目录 +} + +/** + * UI SDK接口 + */ +export interface UISDK { + /** + * 显示对话框 + */ + showDialog(options: DialogOptions): Promise> + + /** + * 显示通知 + */ + showNotification(options: NotificationOptions): Promise> + + /** + * 显示文件选择器 + */ + showFilePicker(options?: FilePickerOptions): Promise> + + /** + * 显示保存文件对话框 + */ + showSaveDialog(defaultName?: string, accept?: string): Promise> + + /** + * 显示Toast消息 + */ + showToast(message: string, type?: 'info' | 'success' | 'warning' | 'error', duration?: number): Promise> + + /** + * 显示加载指示器 + */ + showLoading(message?: string): Promise> + + /** + * 隐藏加载指示器 + */ + hideLoading(id: string): Promise> + + /** + * 显示进度条 + */ + showProgress(options: { title?: string; message?: string; progress: number }): Promise> + + /** + * 更新进度条 + */ + updateProgress(id: string, progress: number, message?: string): Promise> + + /** + * 隐藏进度条 + */ + hideProgress(id: string): Promise> +} + +// ============================================================================= +// 系统SDK接口 +// ============================================================================= + +/** + * 系统信息 + */ +export interface SystemInfo { + platform: string + userAgent: string + language: string + timezone: string + screenResolution: { width: number; height: number } + colorDepth: number + pixelRatio: number +} + +/** + * 应用信息 + */ +export interface AppInfo { + id: string + name: string + version: string + permissions: string[] + createdAt: Date + lastActiveAt: Date +} + +/** + * 系统SDK接口 + */ +export interface SystemSDK { + /** + * 获取系统信息 + */ + getSystemInfo(): Promise> + + /** + * 获取当前应用信息 + */ + getAppInfo(): Promise> + + /** + * 请求权限 + */ + requestPermission(permission: string, reason?: string): Promise> + + /** + * 检查权限状态 + */ + checkPermission(permission: string): Promise> + + /** + * 获取剪贴板内容 + */ + getClipboard(): Promise> + + /** + * 设置剪贴板内容 + */ + setClipboard(text: string): Promise> + + /** + * 打开外部链接 + */ + openExternal(url: string): Promise> + + /** + * 获取当前时间 + */ + getCurrentTime(): Promise> + + /** + * 生成UUID + */ + generateUUID(): Promise> + + /** + * 退出应用 + */ + exit(): Promise> +} + +// ============================================================================= +// 主SDK接口 +// ============================================================================= + +/** + * 系统SDK主接口 + * 整合所有子模块SDK + */ +export interface SystemDesktopSDK { + /** + * SDK版本 + */ + readonly version: string + + /** + * 当前应用ID + */ + readonly appId: string + + /** + * 是否已初始化 + */ + readonly initialized: boolean + + /** + * 窗体操作SDK + */ + readonly window: WindowSDK + + /** + * 存储操作SDK + */ + readonly storage: StorageSDK + + /** + * 网络请求SDK + */ + readonly network: NetworkSDK + + /** + * 事件通信SDK + */ + readonly events: EventSDK + + /** + * UI操作SDK + */ + readonly ui: UISDK + + /** + * 系统操作SDK + */ + readonly system: SystemSDK + + /** + * 初始化SDK + */ + init(config: SDKConfig): Promise> + + /** + * 销毁SDK + */ + destroy(): Promise> + + /** + * 获取SDK状态 + */ + getStatus(): Promise> +} + +// ============================================================================= +// 全局类型声明 +// ============================================================================= + +declare global { + interface Window { + /** + * 系统桌面SDK全局实例 + */ + SystemSDK: SystemDesktopSDK + } +} \ No newline at end of file diff --git a/src/services/ApplicationLifecycleManager.ts b/src/services/ApplicationLifecycleManager.ts new file mode 100644 index 0000000..841cb43 --- /dev/null +++ b/src/services/ApplicationLifecycleManager.ts @@ -0,0 +1,1027 @@ +import { reactive, ref } from 'vue' +import type { WindowService } from './WindowService' +import type { ResourceService } from './ResourceService' +import type { EventCommunicationService } from './EventCommunicationService' +import type { ApplicationSandboxEngine } from './ApplicationSandboxEngine' +import { v4 as uuidv4 } from 'uuid' +import { externalAppDiscovery, type ExternalApp } from './ExternalAppDiscovery' + +/** + * 应用状态枚举 + */ +export enum AppLifecycleState { + INSTALLING = 'installing', + INSTALLED = 'installed', + STARTING = 'starting', + RUNNING = 'running', + SUSPENDED = 'suspended', + STOPPING = 'stopping', + STOPPED = 'stopped', + UNINSTALLING = 'uninstalling', + ERROR = 'error', + CRASHED = 'crashed', + AVAILABLE = 'available', // 外置应用可用但未注册状态 +} + +/** + * 应用清单文件接口 + */ +export interface AppManifest { + id: string + name: string + version: string + description: string + author: string + homepage?: string + icon: string + entryPoint: string // 入口文件路径 + permissions: string[] + minSystemVersion?: string + dependencies?: Record + window?: { + width: number + height: number + minWidth?: number + minHeight?: number + maxWidth?: number + maxHeight?: number + resizable?: boolean + center?: boolean + } + background?: { + persistent?: boolean + scripts?: string[] + } + contentSecurity?: { + policy?: string + allowedDomains?: string[] + } +} + +/** + * 应用实例接口 + */ +export interface AppInstance { + id: string + manifest: AppManifest + state: AppLifecycleState + windowId?: string + sandboxId?: string + processId: string + installedAt: Date + startedAt?: Date + lastActiveAt?: Date + stoppedAt?: Date + errorCount: number + crashCount: number + memoryUsage: number + cpuUsage: number + version: string + autoStart: boolean + persistent: boolean +} + +/** + * 应用安装包接口 + */ +export interface AppPackage { + manifest: AppManifest + files: Map // 文件路径到内容的映射 + signature?: string // 数字签名 + checksum: string // 校验和 +} + +/** + * 应用启动选项 + */ +export interface AppStartOptions { + windowConfig?: { + x?: number + y?: number + width?: number + height?: number + state?: 'normal' | 'minimized' | 'maximized' + } + args?: Record + background?: boolean +} + +/** + * 应用生命周期事件 + */ +export interface AppLifecycleEvents { + onInstalled: (appId: string, manifest: AppManifest) => void + onUninstalled: (appId: string) => void + onStarted: (appId: string, processId: string) => void + onStopped: (appId: string, processId: string) => void + onSuspended: (appId: string, processId: string) => void + onResumed: (appId: string, processId: string) => void + onError: (appId: string, error: Error) => void + onCrashed: (appId: string, reason: string) => void + onStateChanged: (appId: string, newState: AppLifecycleState, oldState: AppLifecycleState) => void +} + +/** + * 应用生命周期管理器 + */ +export class ApplicationLifecycleManager { + private installedApps = reactive(new Map()) + private runningProcesses = reactive(new Map()) + private appFiles = new Map>() // 应用文件存储 + + private windowService: WindowService + private resourceService: ResourceService + private eventService: EventCommunicationService + private sandboxEngine: ApplicationSandboxEngine + + constructor( + windowService: WindowService, + resourceService: ResourceService, + eventService: EventCommunicationService, + sandboxEngine: ApplicationSandboxEngine, + ) { + this.windowService = windowService + this.resourceService = resourceService + this.eventService = eventService + this.sandboxEngine = sandboxEngine + + this.setupEventListeners() + this.loadInstalledApps() + // 外部应用发现已由 SystemServiceIntegration 统一管理,无需重复初始化 + } + + /** + * 安装应用 + */ + async installApp(appPackage: AppPackage): Promise { + const { manifest, files, checksum } = appPackage + const appId = manifest.id + + try { + // 检查应用是否已安装 + if (this.installedApps.has(appId)) { + throw new Error(`应用 ${appId} 已安装`) + } + + // 验证清单文件 + this.validateManifest(manifest) + + // 验证校验和 + if (!this.verifyChecksum(files, checksum)) { + throw new Error('应用包校验失败') + } + + // 检查权限 + await this.checkPermissions(manifest.permissions) + + const now = new Date() + const appInstance: AppInstance = { + id: appId, + manifest, + state: AppLifecycleState.INSTALLING, + processId: '', + installedAt: now, + errorCount: 0, + crashCount: 0, + memoryUsage: 0, + cpuUsage: 0, + version: manifest.version, + autoStart: false, + persistent: manifest.background?.persistent || false, + } + + // 更新状态 + this.updateAppState(appInstance, AppLifecycleState.INSTALLING) + this.installedApps.set(appId, appInstance) + + // 存储应用文件 + this.appFiles.set(appId, files) + + // 保存到本地存储 + await this.saveAppToStorage(appInstance) + + // 更新状态为已安装 + this.updateAppState(appInstance, AppLifecycleState.INSTALLED) + + this.eventService.sendMessage('system', 'app-lifecycle', { + type: 'installed', + appId, + manifest, + }) + + console.log(`应用 ${manifest.name} (${appId}) 安装成功`) + return appId + } catch (error) { + // 清理安装失败的应用 + this.installedApps.delete(appId) + this.appFiles.delete(appId) + + console.error('应用安装失败:', error) + throw error + } + } + + /** + * 卸载应用 + */ + async uninstallApp(appId: string): Promise { + const app = this.installedApps.get(appId) + if (!app) { + throw new Error(`应用 ${appId} 未安装`) + } + + try { + // 如果应用正在运行,先停止 + if (app.state === AppLifecycleState.RUNNING || app.state === AppLifecycleState.SUSPENDED) { + await this.stopApp(appId) + } + + this.updateAppState(app, AppLifecycleState.UNINSTALLING) + + // 清理应用数据 + await this.resourceService.clearStorage(appId) + await this.resourceService.revokeAllPermissions(appId) + + // 删除应用文件 + this.appFiles.delete(appId) + + // 从存储中删除 + await this.removeAppFromStorage(appId) + + // 从已安装列表中移除 + this.installedApps.delete(appId) + + this.eventService.sendMessage('system', 'app-lifecycle', { + type: 'uninstalled', + appId, + }) + + console.log(`应用 ${appId} 卸载成功`) + return true + } catch (error) { + console.error('应用卸载失败:', error) + throw error + } + } + + /** + * 启动应用 + */ + async startApp(appId: string, options: AppStartOptions = {}): Promise { + let app = this.installedApps.get(appId) + + // 如果应用未安装,检查是否为外置应用 + if (!app) { + const externalApp = externalAppDiscovery.getApp(appId) + if (externalApp) { + console.log(`[LifecycleManager] 发现外置应用 ${appId},自动注册`) + await this.registerExternalApp(externalApp) + app = this.installedApps.get(appId) + } + } + + if (!app) { + throw new Error(`应用 ${appId} 未安装且未发现`) + } + + if (app.state === AppLifecycleState.RUNNING) { + throw new Error(`应用 ${appId} 已在运行`) + } + + try { + const processId = uuidv4() + app.processId = processId + + this.updateAppState(app, AppLifecycleState.STARTING) + + // 检查是否为内置应用 + let isBuiltInApp = false + let isExternalApp = false + + try { + const { AppRegistry } = await import('../apps/AppRegistry') + const appRegistry = AppRegistry.getInstance() + isBuiltInApp = appRegistry.hasApp(appId) + } catch (error) { + console.warn('无法导入 AppRegistry') + } + + // 检查是否为外置应用 + if (!isBuiltInApp) { + isExternalApp = externalAppDiscovery.hasApp(appId) + } + + // 创建窗体(如果不是后台应用) + let windowId: string | undefined + if (!options.background && !app.manifest.background?.scripts) { + const windowConfig = { + title: app.manifest.name, + width: options.windowConfig?.width || app.manifest.window?.width || 800, + height: options.windowConfig?.height || app.manifest.window?.height || 600, + x: options.windowConfig?.x, + y: options.windowConfig?.y, + resizable: app.manifest.window?.resizable !== false, + minWidth: app.manifest.window?.minWidth, + minHeight: app.manifest.window?.minHeight, + maxWidth: app.manifest.window?.maxWidth, + maxHeight: app.manifest.window?.maxHeight, + } + + const windowInstance = await this.windowService.createWindow(appId, windowConfig) + windowId = windowInstance.id + app.windowId = windowId + + // 对于内置应用,需要在窗口内容区域挂载 AppRenderer 组件 + if (isBuiltInApp) { + await this.mountBuiltInApp(appId, windowInstance) + } + + // 对于内置应用,不需要等待窗口DOM元素渲染 + if (!isBuiltInApp) { + // 稍等片刻确保窗口DOM元素已经渲染完成 + await new Promise((resolve) => setTimeout(resolve, 100)) + } + } + + // 对于外置应用,需要创建沙箱;内置应用跳过沙箱 + if (isExternalApp && !isBuiltInApp) { + // 为外置应用创建沙箱 + const sandboxConfig = { + securityLevel: 2, // HIGH + allowScripts: true, + allowSameOrigin: false, // 安全考虑:不允许同源访问以防止沙箱逃逸 + allowForms: true, + networkTimeout: 15000, // 增加超时时间到15秒 + csp: + app.manifest.contentSecurity?.policy || + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self';", + } + + const sandbox = await this.sandboxEngine.createSandbox( + appId, + windowId || 'background', + sandboxConfig, + ) + app.sandboxId = sandbox.id + + // 为外置应用加载代码 + await this.loadExternalAppInSandbox(app, sandbox.id) + } else if (isBuiltInApp) { + console.log(`[LifecycleManager] 内置应用 ${appId} 跳过沙箱创建和代码加载`) + } else { + console.warn(`[LifecycleManager] 未知应用类型: ${appId}`) + } + + // 更新状态 + const now = new Date() + app.startedAt = now + app.lastActiveAt = now + this.updateAppState(app, AppLifecycleState.RUNNING) + + // 添加到运行进程列表 + this.runningProcesses.set(processId, app) + + // 注意:状态变更消息由updateAppState方法自动发送,不需要手动发送 + + console.log(`应用 ${app.manifest.name} (${appId}) 启动成功,进程ID: ${processId}`) + return processId + } catch (error) { + this.updateAppState(app, AppLifecycleState.ERROR) + app.errorCount++ + + console.error('应用启动失败:', error) + throw error + } + } + + /** + * 停止应用 + */ + async stopApp(appId: string): Promise { + const app = this.installedApps.get(appId) + if (!app) { + throw new Error(`应用 ${appId} 未安装`) + } + + if (app.state !== AppLifecycleState.RUNNING && app.state !== AppLifecycleState.SUSPENDED) { + return true + } + + try { + this.updateAppState(app, AppLifecycleState.STOPPING) + + // 销毁沙箱 + if (app.sandboxId) { + this.sandboxEngine.destroySandbox(app.sandboxId) + app.sandboxId = undefined + } + + // 关闭窗体(如果还存在) + if (app.windowId) { + const window = this.windowService.getWindow(app.windowId) + if (window) { + await this.windowService.destroyWindow(app.windowId) + } + app.windowId = undefined + } + + // 更新状态 + app.stoppedAt = new Date() + this.updateAppState(app, AppLifecycleState.STOPPED) + + // 从运行进程列表中移除 + if (app.processId) { + this.runningProcesses.delete(app.processId) + app.processId = '' + } + + // 注意:状态变更消息由updateAppState方法自动发送,不需要手动发送 + + console.log(`应用 ${appId} 停止成功`) + return true + } catch (error) { + console.error('应用停止失败:', error) + return false + } + } + + /** + * 暂停应用 + */ + async suspendApp(appId: string): Promise { + const app = this.installedApps.get(appId) + if (!app || app.state !== AppLifecycleState.RUNNING) { + return false + } + + try { + // 暂停沙箱 + if (app.sandboxId) { + this.sandboxEngine.suspendSandbox(app.sandboxId) + } + + // 最小化窗体 + if (app.windowId) { + this.windowService.minimizeWindow(app.windowId) + } + + this.updateAppState(app, AppLifecycleState.SUSPENDED) + + // 注意:状态变更消息由updateAppState方法自动发送,不需要手动发送 + + console.log(`应用 ${appId} 已暂停`) + return true + } catch (error) { + console.error('应用暂停失败:', error) + return false + } + } + + /** + * 恢复应用 + */ + async resumeApp(appId: string): Promise { + const app = this.installedApps.get(appId) + if (!app || app.state !== AppLifecycleState.SUSPENDED) { + return false + } + + try { + // 恢复沙箱 + if (app.sandboxId) { + this.sandboxEngine.resumeSandbox(app.sandboxId) + } + + // 恢复窗体 + if (app.windowId) { + this.windowService.restoreWindow(app.windowId) + } + + app.lastActiveAt = new Date() + this.updateAppState(app, AppLifecycleState.RUNNING) + + // 注意:状态变更消息由updateAppState方法自动发送,不需要手动发送 + + console.log(`应用 ${appId} 已恢复`) + return true + } catch (error) { + console.error('应用恢复失败:', error) + return false + } + } + + /** + * 重启应用 + */ + async restartApp(appId: string, options?: AppStartOptions): Promise { + await this.stopApp(appId) + // 等待一小段时间确保完全停止 + await new Promise((resolve) => setTimeout(resolve, 1000)) + return this.startApp(appId, options) + } + + /** + * 获取应用信息 + */ + getApp(appId: string): AppInstance | undefined { + return this.installedApps.get(appId) + } + + /** + * 获取所有已安装应用 + */ + getAllApps(): AppInstance[] { + return Array.from(this.installedApps.values()) + } + + /** + * 获取正在运行的应用 + */ + getRunningApps(): AppInstance[] { + return Array.from(this.runningProcesses.values()) + } + + /** + * 检查应用是否正在运行 + */ + isAppRunning(appId: string): boolean { + const app = this.installedApps.get(appId) + return app?.state === AppLifecycleState.RUNNING || app?.state === AppLifecycleState.SUSPENDED + } + + /** + * 获取应用统计信息 + */ + getAppStats(appId: string) { + const app = this.installedApps.get(appId) + if (!app) return null + + return { + state: app.state, + errorCount: app.errorCount, + crashCount: app.crashCount, + memoryUsage: app.memoryUsage, + cpuUsage: app.cpuUsage, + startedAt: app.startedAt, + lastActiveAt: app.lastActiveAt, + uptime: app.startedAt ? Date.now() - app.startedAt.getTime() : 0, + } + } + + // 私有方法 + + /** + * 为内置应用挂载 AppRenderer 组件 + */ + private async mountBuiltInApp(appId: string, windowInstance: any): Promise { + try { + // 动态导入 Vue 和 AppRenderer + const { createApp } = await import('vue') + const AppRenderer = (await import('../ui/components/AppRenderer.vue')).default + + console.log(`[LifecycleManager] 为内置应用 ${appId} 创建 AppRenderer 组件`) + + const app = createApp({ + components: { AppRenderer }, + template: ``, + }) + + // 提供系统服务(使用当前实例所在的系统服务) + app.provide('systemService', { + getWindowService: () => this.windowService, + getResourceService: () => this.resourceService, + getEventService: () => this.eventService, + getSandboxEngine: () => this.sandboxEngine, + getLifecycleManager: () => this, + }) + + // 挂载到窗口内容区域 + const contentArea = windowInstance.element?.querySelector('.window-content') + if (contentArea) { + app.mount(contentArea) + console.log(`[LifecycleManager] AppRenderer 组件已挂载到窗口 ${windowInstance.id}`) + } else { + throw new Error('未找到窗口内容区域') + } + } catch (error) { + console.error(`内置应用 ${appId} 挂载失败:`, error) + throw error + } + } + + /** + * 验证应用清单文件 + */ + private validateManifest(manifest: AppManifest): void { + const required = ['id', 'name', 'version', 'entryPoint'] + + for (const field of required) { + if (!manifest[field as keyof AppManifest]) { + throw new Error(`清单文件缺少必需字段: ${field}`) + } + } + + // 验证版本格式 + if (!/^\d+\.\d+\.\d+/.test(manifest.version)) { + throw new Error('版本号格式不正确,应为 x.y.z 格式') + } + + // 验证应用ID格式 + if (!/^[a-zA-Z0-9._-]+$/.test(manifest.id)) { + throw new Error('应用ID格式不正确') + } + } + + /** + * 验证校验和 + */ + private verifyChecksum(files: Map, expectedChecksum: string): boolean { + // 简化实现,实际应用中应使用更强的哈希算法 + let content = '' + for (const [path, data] of files.entries()) { + content += path + (typeof data === 'string' ? data : data.size) + } + + const actualChecksum = this.safeBase64Encode(content).slice(0, 32) + return actualChecksum === expectedChecksum + } + + /** + * 安全的 Base64 编码,支持 Unicode 字符 + */ + private safeBase64Encode(str: string): string { + try { + // 使用 encodeURIComponent + atob 来处理 Unicode 字符 + return btoa( + encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) { + return String.fromCharCode(parseInt(p1, 16)) + }), + ) + } catch (error) { + // 如果还是失败,使用简单的哈希算法 + let hash = 0 + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // 转换为32位整数 + } + return Math.abs(hash).toString(36) + } + } + + /** + * 检查权限 + */ + private async checkPermissions(permissions: string[]): Promise { + for (const permission of permissions) { + // 这里可以添加权限检查逻辑 + // 目前简单允许所有权限 + console.log(`检查权限: ${permission}`) + } + } + + /** + * 在沙箱中加载应用 + */ + private async loadAppInSandbox(app: AppInstance, sandboxId: string): Promise { + const appFiles = this.appFiles.get(app.id) + if (!appFiles) { + throw new Error('应用文件不存在') + } + + const entryFile = appFiles.get(app.manifest.entryPoint) + if (!entryFile) { + throw new Error('入口文件不存在') + } + + // 创建应用URL + const entryUrl = this.createAppUrl(app.id, app.manifest.entryPoint, entryFile) + + // 在沙箱中加载应用 + await this.sandboxEngine.loadApplication(sandboxId, entryUrl) + } + + /** + * 创建应用文件URL + */ + private createAppUrl(appId: string, filePath: string, content: Blob | string): string { + let blob: Blob + + if (typeof content === 'string') { + // 确保使用UTF-8编码创建Blob + const contentType = this.getContentType(filePath) + const utf8ContentType = contentType.includes('charset') + ? contentType + : `${contentType}; charset=utf-8` + blob = new Blob([content], { type: utf8ContentType }) + } else { + blob = content + } + + return URL.createObjectURL(blob) + } + + /** + * 获取文件内容类型 + */ + private getContentType(filePath: string): string { + const ext = filePath.split('.').pop()?.toLowerCase() + + switch (ext) { + case 'html': + return 'text/html; charset=utf-8' + case 'js': + return 'application/javascript; charset=utf-8' + case 'css': + return 'text/css; charset=utf-8' + case 'json': + return 'application/json; charset=utf-8' + case 'png': + return 'image/png' + case 'jpg': + case 'jpeg': + return 'image/jpeg' + case 'svg': + return 'image/svg+xml; charset=utf-8' + default: + return 'text/plain; charset=utf-8' + } + } + + /** + * 保存应用到存储 + */ + private async saveAppToStorage(app: AppInstance): Promise { + const appData = { + id: app.id, + manifest: app.manifest, + installedAt: app.installedAt.toISOString(), + version: app.version, + autoStart: app.autoStart, + persistent: app.persistent, + } + + await this.resourceService.setStorage('system', `apps.${app.id}`, appData) + } + + /** + * 从存储中删除应用 + */ + private async removeAppFromStorage(appId: string): Promise { + await this.resourceService.removeStorage('system', `apps.${appId}`) + } + + /** + * 初始化外置应用发现(已由 SystemServiceIntegration 统一管理) + * 这个方法保留用于未来可能的扩展,但不再重复启动发现服务 + */ + private async initializeExternalAppDiscovery(): Promise { + try { + console.log('[LifecycleManager] 外置应用发现服务已由系统服务集成统一管理') + + // 可以在这里添加应用生命周期管理器特有的外置应用监听逻辑 + // 例如监听外置应用的注册/卸载事件 + } catch (error) { + console.error('[LifecycleManager] 初始化外置应用监听失败:', error) + } + } + + /** + * 注册外置应用 + */ + private async registerExternalApp(externalApp: ExternalApp): Promise { + try { + const { manifest } = externalApp + const now = new Date() + + const appInstance: AppInstance = { + id: manifest.id, + manifest, + state: AppLifecycleState.INSTALLED, + processId: '', + installedAt: now, + errorCount: 0, + crashCount: 0, + memoryUsage: 0, + cpuUsage: 0, + version: manifest.version, + autoStart: false, + persistent: false, + } + + this.installedApps.set(manifest.id, appInstance) + + console.log(`[LifecycleManager] 外置应用 ${manifest.name} (${manifest.id}) 已自动注册`) + + // 发送应用注册事件 + this.eventService.sendMessage('system', 'app-lifecycle', { + type: 'external-app-registered', + appId: manifest.id, + manifest, + }) + } catch (error) { + console.error(`[LifecycleManager] 注册外置应用失败:`, error) + throw error + } + } + + /** + * 为外置应用加载代码到沙箱 + */ + private async loadExternalAppInSandbox(app: AppInstance, sandboxId: string): Promise { + try { + const externalApp = externalAppDiscovery.getApp(app.id) + if (!externalApp) { + throw new Error('外置应用信息未找到') + } + + // 直接使用外置应用的入口URL + const entryUrl = externalApp.entryPath + + console.log(`[LifecycleManager] 加载外置应用: ${app.id} from ${entryUrl}`) + + // 在沙箱中加载应用 + await this.sandboxEngine.loadApplication(sandboxId, entryUrl) + } catch (error) { + console.error(`[LifecycleManager] 加载外置应用到沙箱失败:`, error) + throw error + } + } + + /** + * 获取所有可用应用(包括内置和外置) + */ + getAllAvailableApps(): (AppInstance & { isExternal?: boolean })[] { + const apps: (AppInstance & { isExternal?: boolean })[] = [] + + // 添加已安装的应用 + for (const app of this.installedApps.values()) { + apps.push(app) + } + + // 添加未注册的外置应用 + for (const externalApp of externalAppDiscovery.getDiscoveredApps()) { + if (!this.installedApps.has(externalApp.id)) { + const appInstance: AppInstance & { isExternal: boolean } = { + id: externalApp.manifest.id, + manifest: externalApp.manifest, + state: AppLifecycleState.AVAILABLE, + processId: '', + installedAt: externalApp.lastScanned, + errorCount: 0, + crashCount: 0, + memoryUsage: 0, + cpuUsage: 0, + version: externalApp.manifest.version, + autoStart: false, + persistent: false, + isExternal: true, + } + apps.push(appInstance) + } + } + + return apps + } + + /** + * 刷新外置应用列表 + */ + async refreshExternalApps(): Promise { + try { + await externalAppDiscovery.refresh() + console.log('[LifecycleManager] 外置应用列表已刷新') + } catch (error) { + console.error('[LifecycleManager] 刷新外置应用列表失败:', error) + } + } + + /** + * 加载已安装的应用 + */ + private async loadInstalledApps(): Promise { + try { + // 从存储中加载应用列表 + const appsData = (await this.resourceService.getStorage('system', 'apps')) || {} + + for (const [appId, appData] of Object.entries(appsData as Record)) { + if (appData && typeof appData === 'object') { + const app: AppInstance = { + id: appId, + manifest: appData.manifest, + state: AppLifecycleState.STOPPED, + processId: '', + installedAt: new Date(appData.installedAt), + errorCount: 0, + crashCount: 0, + memoryUsage: 0, + cpuUsage: 0, + version: appData.version, + autoStart: appData.autoStart || false, + persistent: appData.persistent || false, + } + + this.updateAppState(app, AppLifecycleState.INSTALLED) + this.installedApps.set(appId, app) + } + } + + console.log(`已加载 ${this.installedApps.size} 个已安装应用`) + } catch (error) { + console.error('加载已安装应用失败:', error) + } + } + + /** + * 设置事件监听器 + */ + private setupEventListeners(): void { + // 监听沙箱状态变化 + this.eventService.subscribe('system', 'sandbox-state-change', (message) => { + const { sandboxId, newState } = message.payload + + // 查找对应的应用 + for (const app of this.installedApps.values()) { + if (app.sandboxId === sandboxId) { + if (newState === 'error') { + this.handleAppError(app.id, new Error('沙箱错误')) + } else if (newState === 'destroyed') { + this.handleAppCrash(app.id, '沙箱被销毁') + } + break + } + } + }) + + // 监听性能告警 + this.eventService.subscribe('system', 'performance-alert', (message) => { + const { sandboxId, type, usage, limit } = message.payload + + for (const app of this.installedApps.values()) { + if (app.sandboxId === sandboxId) { + if (type === 'memory') { + app.memoryUsage = usage + } else if (type === 'cpu') { + app.cpuUsage = usage + } + + console.warn(`应用 ${app.id} ${type} 使用量 ${usage} 超过限制 ${limit}`) + break + } + } + }) + } + + /** + * 处理应用错误 + */ + private handleAppError(appId: string, error: Error): void { + const app = this.installedApps.get(appId) + if (!app) return + + app.errorCount++ + + this.eventService.sendMessage('system', 'app-lifecycle', { + type: 'error', + appId, + error: error.message, + }) + + console.error(`应用 ${appId} 发生错误:`, error) + } + + /** + * 处理应用崩溃 + */ + private handleAppCrash(appId: string, reason: string): void { + const app = this.installedApps.get(appId) + if (!app) return + + app.crashCount++ + this.updateAppState(app, AppLifecycleState.CRASHED) + + // 清理资源 + if (app.processId) { + this.runningProcesses.delete(app.processId) + } + + // 注意:状态变更消息由updateAppState方法自动发送,不需要手动发送 + + console.error(`应用 ${appId} 崩溃: ${reason}`) + } + + /** + * 更新应用状态 + */ + private updateAppState(app: AppInstance, newState: AppLifecycleState): void { + const oldState = app.state + app.state = newState + + this.eventService.sendMessage('system', 'app-lifecycle', { + type: 'stateChanged', + appId: app.id, + newState, + oldState, + }) + } +} diff --git a/src/services/ApplicationSandboxEngine.ts b/src/services/ApplicationSandboxEngine.ts new file mode 100644 index 0000000..9d438e5 --- /dev/null +++ b/src/services/ApplicationSandboxEngine.ts @@ -0,0 +1,1273 @@ +import { reactive, ref, nextTick } from 'vue' +import type { ResourceService } from './ResourceService' +import type { EventCommunicationService } from './EventCommunicationService' +import { v4 as uuidv4 } from 'uuid' + +/** + * 沙箱状态枚举 + */ +export enum SandboxState { + INITIALIZING = 'initializing', + READY = 'ready', + RUNNING = 'running', + SUSPENDED = 'suspended', + ERROR = 'error', + DESTROYED = 'destroyed' +} + +/** + * 沙箱类型枚举 + */ +export enum SandboxType { + IFRAME = 'iframe', + WEBWORKER = 'webworker', + SHADOW_DOM = 'shadowdom' +} + +/** + * 沙箱安全级别 + */ +export enum SecurityLevel { + LOW = 0, + MEDIUM = 1, + HIGH = 2, + STRICT = 3 +} + +/** + * 沙箱配置接口 + */ +export interface SandboxConfig { + type: SandboxType + securityLevel: SecurityLevel + allowScripts: boolean + allowSameOrigin: boolean + allowForms: boolean + allowPopups: boolean + allowModals: boolean + allowPointerLock: boolean + allowPresentation: boolean + allowTopNavigation: boolean + maxMemoryUsage: number // MB + maxCpuUsage: number // 百分比 + networkTimeout: number // 网络请求超时时间(ms) + csp: string // 内容安全策略 +} + +/** + * 沙箱实例接口 + */ +export interface SandboxInstance { + id: string + appId: string + windowId: string + config: SandboxConfig + state: SandboxState + container: HTMLElement | null + iframe?: HTMLIFrameElement + worker?: Worker + shadowRoot?: ShadowRoot + createdAt: Date + lastActiveAt: Date + memoryUsage: number + cpuUsage: number + networkRequests: number + errors: string[] +} + +/** + * 沙箱性能监控数据 + */ +export interface SandboxPerformance { + sandboxId: string + timestamp: Date + memoryUsage: number + cpuUsage: number + networkRequests: number + renderTime: number + jsExecutionTime: number +} + +/** + * 沙箱事件接口 + */ +export interface SandboxEvents { + onStateChange: (sandboxId: string, newState: SandboxState, oldState: SandboxState) => void + onError: (sandboxId: string, error: Error) => void + onPerformanceAlert: (sandboxId: string, metrics: SandboxPerformance) => void + onResourceLimit: (sandboxId: string, resourceType: string, usage: number, limit: number) => void +} + +/** + * 应用沙箱引擎类 + */ +export class ApplicationSandboxEngine { + private sandboxes = reactive(new Map()) + private performanceData = reactive(new Map()) + private monitoringInterval: number | null = null + private resourceService: ResourceService + private eventService: EventCommunicationService + + constructor( + resourceService: ResourceService, + eventService: EventCommunicationService + ) { + this.resourceService = resourceService + this.eventService = eventService + this.startPerformanceMonitoring() + } + + /** + * 创建沙箱实例 + */ + async createSandbox( + appId: string, + windowId: string, + config: Partial = {} + ): Promise { + const sandboxId = uuidv4() + + const defaultConfig: SandboxConfig = { + type: SandboxType.IFRAME, + securityLevel: SecurityLevel.HIGH, + allowScripts: true, + allowSameOrigin: false, + allowForms: true, + allowPopups: false, + allowModals: false, + allowPointerLock: false, + allowPresentation: false, + allowTopNavigation: false, + maxMemoryUsage: 100, // 100MB + maxCpuUsage: 30, // 30% + networkTimeout: 10000, // 10秒 + csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self';" + } + + const finalConfig = { ...defaultConfig, ...config } + const now = new Date() + + const sandbox: SandboxInstance = { + id: sandboxId, + appId, + windowId, + config: finalConfig, + state: SandboxState.INITIALIZING, + container: null, + createdAt: now, + lastActiveAt: now, + memoryUsage: 0, + cpuUsage: 0, + networkRequests: 0, + errors: [] + } + + try { + // 根据类型创建沙箱容器 + await this.createSandboxContainer(sandbox) + + // 设置安全策略 + this.applySecurity(sandbox) + + // 设置性能监控 + this.setupPerformanceMonitoring(sandbox) + + // 更新状态 + this.updateSandboxState(sandbox, SandboxState.READY) + + this.sandboxes.set(sandboxId, sandbox) + + console.log(`沙箱 ${sandboxId} 创建成功`) + return sandbox + + } catch (error) { + this.updateSandboxState(sandbox, SandboxState.ERROR) + sandbox.errors.push(error instanceof Error ? error.message : String(error)) + throw error + } + } + + /** + * 加载应用到沙箱 + */ + async loadApplication(sandboxId: string, applicationUrl: string): Promise { + const sandbox = this.sandboxes.get(sandboxId) + if (!sandbox) { + throw new Error(`沙箱 ${sandboxId} 不存在`) + } + + try { + console.log(`开始加载应用: ${sandbox.appId}, 沙箱ID: ${sandboxId}`) + this.updateSandboxState(sandbox, SandboxState.RUNNING) + + if (sandbox.config.type === SandboxType.IFRAME && sandbox.iframe) { + console.log(`使用iframe加载应用: ${applicationUrl}`) + + // 检查iframe是否已挂载到DOM + if (!document.contains(sandbox.iframe)) { + console.warn('检测到iframe未挂载到DOM,尝试重新挂载') + await this.mountIframeToWindow(sandbox) + } + + // 设置iframe源 + sandbox.iframe.src = applicationUrl + + // 等待加载完成 + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + console.error(`应用加载超时: ${sandbox.appId}, URL: ${applicationUrl}, 超时时间: ${sandbox.config.networkTimeout}ms`) + reject(new Error('应用加载超时')) + }, sandbox.config.networkTimeout) + + sandbox.iframe!.onload = () => { + console.log(`应用加载成功: ${sandbox.appId}`) + clearTimeout(timeoutId) + this.injectSDK(sandbox) + resolve(true) + } + + sandbox.iframe!.onerror = (error) => { + console.error(`应用加载失败: ${sandbox.appId}`, error) + clearTimeout(timeoutId) + reject(new Error('应用加载失败')) + } + }) + } else if (sandbox.config.type === SandboxType.WEBWORKER && sandbox.worker) { + // WebWorker方式加载 + sandbox.worker.postMessage({ + type: 'load', + url: applicationUrl + }) + return true + } + + return false + } catch (error) { + console.error(`加载应用失败: ${sandbox.appId}`, error) + this.updateSandboxState(sandbox, SandboxState.ERROR) + sandbox.errors.push(error instanceof Error ? error.message : String(error)) + throw error + } + } + + /** + * 暂停沙箱 + */ + suspendSandbox(sandboxId: string): boolean { + const sandbox = this.sandboxes.get(sandboxId) + if (!sandbox || sandbox.state !== SandboxState.RUNNING) { + return false + } + + try { + this.updateSandboxState(sandbox, SandboxState.SUSPENDED) + + if (sandbox.iframe) { + // 暂停iframe中的脚本执行 + const iframeWindow = sandbox.iframe.contentWindow + if (iframeWindow) { + // 发送暂停事件到应用 + iframeWindow.postMessage({ type: 'system:suspend' }, '*') + } + } + + console.log(`沙箱 ${sandboxId} 已暂停`) + return true + } catch (error) { + console.error('暂停沙箱失败:', error) + return false + } + } + + /** + * 恢复沙箱 + */ + resumeSandbox(sandboxId: string): boolean { + const sandbox = this.sandboxes.get(sandboxId) + if (!sandbox || sandbox.state !== SandboxState.SUSPENDED) { + return false + } + + try { + this.updateSandboxState(sandbox, SandboxState.RUNNING) + sandbox.lastActiveAt = new Date() + + if (sandbox.iframe) { + const iframeWindow = sandbox.iframe.contentWindow + if (iframeWindow) { + // 发送恢复事件到应用 + iframeWindow.postMessage({ type: 'system:resume' }, '*') + } + } + + console.log(`沙箱 ${sandboxId} 已恢复`) + return true + } catch (error) { + console.error('恢复沙箱失败:', error) + return false + } + } + + /** + * 销毁沙箱 + */ + destroySandbox(sandboxId: string): boolean { + const sandbox = this.sandboxes.get(sandboxId) + if (!sandbox) { + return false + } + + try { + this.updateSandboxState(sandbox, SandboxState.DESTROYED) + + // 清理DOM元素 + if (sandbox.iframe) { + sandbox.iframe.remove() + } + + if (sandbox.container) { + sandbox.container.remove() + } + + // 终止WebWorker + if (sandbox.worker) { + sandbox.worker.terminate() + } + + // 清理性能数据 + this.performanceData.delete(sandboxId) + + // 从集合中移除 + this.sandboxes.delete(sandboxId) + + console.log(`沙箱 ${sandboxId} 已销毁`) + return true + } catch (error) { + console.error('销毁沙箱失败:', error) + return false + } + } + + /** + * 获取沙箱实例 + */ + getSandbox(sandboxId: string): SandboxInstance | undefined { + return this.sandboxes.get(sandboxId) + } + + /** + * 获取应用的所有沙箱 + */ + getAppSandboxes(appId: string): SandboxInstance[] { + return Array.from(this.sandboxes.values()).filter(sandbox => sandbox.appId === appId) + } + + /** + * 获取沙箱性能数据 + */ + getPerformanceData(sandboxId: string): SandboxPerformance[] { + return this.performanceData.get(sandboxId) || [] + } + + /** + * 发送消息到沙箱 + */ + sendMessage(sandboxId: string, message: any): boolean { + const sandbox = this.sandboxes.get(sandboxId) + if (!sandbox || sandbox.state !== SandboxState.RUNNING) { + return false + } + + try { + if (sandbox.iframe) { + const iframeWindow = sandbox.iframe.contentWindow + if (iframeWindow) { + iframeWindow.postMessage(message, '*') + return true + } + } else if (sandbox.worker) { + sandbox.worker.postMessage(message) + return true + } + + return false + } catch (error) { + console.error('发送消息到沙箱失败:', error) + return false + } + } + + /** + * 清理所有沙箱 + */ + cleanup(): void { + for (const sandboxId of this.sandboxes.keys()) { + this.destroySandbox(sandboxId) + } + } + + /** + * 销毁引擎 + */ + destroy(): void { + if (this.monitoringInterval) { + clearInterval(this.monitoringInterval) + this.monitoringInterval = null + } + + this.cleanup() + console.log('沙箱引擎已销毁') + } + + // 私有方法 + + /** + * 创建沙箱容器 + */ + private async createSandboxContainer(sandbox: SandboxInstance): Promise { + switch (sandbox.config.type) { + case SandboxType.IFRAME: + await this.createIframeSandbox(sandbox) + break + case SandboxType.WEBWORKER: + await this.createWorkerSandbox(sandbox) + break + case SandboxType.SHADOW_DOM: + await this.createShadowDOMSandbox(sandbox) + break + default: + throw new Error(`不支持的沙箱类型: ${sandbox.config.type}`) + } + } + + /** + * 创建iframe沙箱 + */ + private async createIframeSandbox(sandbox: SandboxInstance): Promise { + const iframe = document.createElement('iframe') + iframe.id = `sandbox-${sandbox.id}` + + // 设置sandbox属性 + const sandboxAttributes: string[] = [] + + if (sandbox.config.allowScripts) sandboxAttributes.push('allow-scripts') + if (sandbox.config.allowSameOrigin) sandboxAttributes.push('allow-same-origin') + if (sandbox.config.allowForms) sandboxAttributes.push('allow-forms') + if (sandbox.config.allowPopups) sandboxAttributes.push('allow-popups') + if (sandbox.config.allowModals) sandboxAttributes.push('allow-modals') + if (sandbox.config.allowPointerLock) sandboxAttributes.push('allow-pointer-lock') + if (sandbox.config.allowPresentation) sandboxAttributes.push('allow-presentation') + if (sandbox.config.allowTopNavigation) sandboxAttributes.push('allow-top-navigation') + + iframe.sandbox = sandboxAttributes.join(' ') + + // 设置样式 + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + background: #fff; + ` + + // 设置CSP - 不使用iframe的csp属性,而是在内容中设置 + // if (sandbox.config.csp) { + // iframe.setAttribute('csp', sandbox.config.csp) + // } + + sandbox.iframe = iframe + sandbox.container = iframe + + // 将iframe挂载到对应的窗口内容区域 + await this.mountIframeToWindow(sandbox) + } + + /** + * 将iframe挂载到窗口内容区域 + */ + private async mountIframeToWindow(sandbox: SandboxInstance): Promise { + const windowElement = document.getElementById(`window-${sandbox.windowId}`) + if (!windowElement) { + // 如果是后台应用,创建隐藏容器 + if (sandbox.windowId === 'background') { + const hiddenContainer = document.createElement('div') + hiddenContainer.id = 'background-apps-container' + hiddenContainer.style.cssText = ` + position: fixed; + top: -9999px; + left: -9999px; + width: 1px; + height: 1px; + visibility: hidden; + ` + document.body.appendChild(hiddenContainer) + hiddenContainer.appendChild(sandbox.iframe!) + return + } + throw new Error(`找不到窗口元素: window-${sandbox.windowId}`) + } + + const contentArea = windowElement.querySelector('.window-content') + if (!contentArea) { + throw new Error(`找不到窗口内容区域: window-${sandbox.windowId}`) + } + + // 替换窗口中的默认iframe + const existingIframe = contentArea.querySelector('iframe') + if (existingIframe) { + contentArea.removeChild(existingIframe) + } + + contentArea.appendChild(sandbox.iframe!) + } + + /** + * 创建WebWorker沙箱 + */ + private async createWorkerSandbox(sandbox: SandboxInstance): Promise { + // 创建Worker脚本 + const workerScript = ` + let appCode = null; + + self.onmessage = function(e) { + const { type, data } = e.data; + + switch (type) { + case 'load': + fetch(data.url) + .then(response => response.text()) + .then(code => { + appCode = code; + self.postMessage({ type: 'loaded' }); + }) + .catch(error => { + self.postMessage({ type: 'error', error: error.message }); + }); + break; + + case 'execute': + if (appCode) { + try { + eval(appCode); + self.postMessage({ type: 'executed' }); + } catch (error) { + self.postMessage({ type: 'error', error: error.message }); + } + } + break; + } + }; + ` + + const blob = new Blob([workerScript], { type: 'application/javascript' }) + const workerUrl = URL.createObjectURL(blob) + + const worker = new Worker(workerUrl) + + worker.onmessage = (e) => { + const { type, error } = e.data + if (type === 'error') { + sandbox.errors.push(error) + this.updateSandboxState(sandbox, SandboxState.ERROR) + } + } + + worker.onerror = (error) => { + sandbox.errors.push(error.message) + this.updateSandboxState(sandbox, SandboxState.ERROR) + } + + sandbox.worker = worker + URL.revokeObjectURL(workerUrl) + } + + /** + * 创建Shadow DOM沙箱 + */ + private async createShadowDOMSandbox(sandbox: SandboxInstance): Promise { + const container = document.createElement('div') + container.id = `sandbox-${sandbox.id}` + + const shadowRoot = container.attachShadow({ mode: 'closed' }) + + // 添加样式隔离 + const style = document.createElement('style') + style.textContent = ` + :host { + width: 100%; + height: 100%; + display: block; + overflow: hidden; + } + ` + shadowRoot.appendChild(style) + + sandbox.container = container + sandbox.shadowRoot = shadowRoot + } + + /** + * 应用安全策略 + */ + private applySecurity(sandbox: SandboxInstance): void { + if (sandbox.iframe) { + // 设置安全头 + const securityHeaders = { + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'no-referrer' + } + + // 这些头部主要用于服务器端设置,在客户端我们通过其他方式实现 + + // 监听iframe的消息事件 + window.addEventListener('message', (event) => { + if (event.source === sandbox.iframe!.contentWindow) { + this.handleSandboxMessage(sandbox, event.data) + } + }) + + // 简化安全限制,主要依赖iframe的sandbox属性 + sandbox.iframe.addEventListener('load', () => { + const iframeDoc = sandbox.iframe!.contentDocument + if (iframeDoc) { + // 添加基本的安全提示脚本 + const script = iframeDoc.createElement('script') + script.textContent = ` + // 标记应用在沙箱中运行 + window.__SANDBOXED__ = true; + + // 禁用一些显而易见的危险API + if (typeof window.open !== 'undefined') { + window.open = function() { + console.warn('沙箱应用不允许打开新窗口'); + return null; + }; + } + + console.log('应用已在安全沙箱中加载'); + ` + iframeDoc.head.appendChild(script) + } + }) + } + } + + /** + * 处理沙箱消息 + */ + private handleSandboxMessage(sandbox: SandboxInstance, message: any): void { + if (typeof message !== 'object' || !message.type) { + return + } + + switch (message.type) { + case 'app:ready': + sandbox.lastActiveAt = new Date() + break + + case 'app:error': + sandbox.errors.push(message.error || '未知错误') + break + + case 'app:resource-request': + this.handleResourceRequest(sandbox, message.data) + break + + case 'app:performance': + this.recordPerformance(sandbox, message.data) + break + } + } + + /** + * 处理资源请求 + */ + private async handleResourceRequest(sandbox: SandboxInstance, request: any): Promise { + const { type, data } = request + + try { + let result = null + + switch (type) { + case 'storage': + if (data.action === 'get') { + result = await this.resourceService.getStorage(sandbox.appId, data.key) + } else if (data.action === 'set') { + result = await this.resourceService.setStorage(sandbox.appId, data.key, data.value) + } + break + + case 'network': + result = await this.resourceService.makeNetworkRequest(sandbox.appId, data.url, data.options) + break + } + + // 发送结果回沙箱 + this.sendMessage(sandbox.id, { + type: 'system:resource-response', + requestId: request.id, + result + }) + } catch (error) { + this.sendMessage(sandbox.id, { + type: 'system:resource-error', + requestId: request.id, + error: error instanceof Error ? error.message : String(error) + }) + } + } + + /** + * 注入SDK + */ + private injectSDK(sandbox: SandboxInstance): void { + if (!sandbox.iframe) return + + const iframeDoc = sandbox.iframe.contentDocument + if (!iframeDoc) return + + // 确保正确的字符编码 + const existingCharset = iframeDoc.querySelector('meta[charset]') + if (!existingCharset) { + const charsetMeta = iframeDoc.createElement('meta') + charsetMeta.setAttribute('charset', 'UTF-8') + iframeDoc.head.insertBefore(charsetMeta, iframeDoc.head.firstChild) + } + + // 添加正确的CSP meta标签 + const existingCSP = iframeDoc.querySelector('meta[http-equiv="Content-Security-Policy"]') + if (!existingCSP && sandbox.config.csp) { + const cspMeta = iframeDoc.createElement('meta') + cspMeta.setAttribute('http-equiv', 'Content-Security-Policy') + cspMeta.setAttribute('content', sandbox.config.csp) + iframeDoc.head.appendChild(cspMeta) + } + + const script = iframeDoc.createElement('script') + script.textContent = ` + // 系统SDK注入 - 完整版本 + (function() { + console.log('[SystemSDK] 开始注入SDK到iframe'); + + // SDK基础类 + class SDKBase { + constructor() { + this._appId = ''; + this._initialized = false; + } + + get appId() { + return this._appId; + } + + get initialized() { + return this._initialized; + } + + // 发送消息到系统 + sendToSystem(method, data) { + return new Promise((resolve, reject) => { + const requestId = Date.now().toString() + '_' + Math.random().toString(36).substr(2, 9); + + const handler = (event) => { + if (event.data?.type === 'system:response' && event.data?.requestId === requestId) { + window.removeEventListener('message', handler); + if (event.data.success) { + resolve(event.data.data); + } else { + reject(new Error(event.data.error || '系统调用失败')); + } + } + }; + + window.addEventListener('message', handler); + + // 发送消息到父窗口(系统) + window.parent.postMessage({ + type: 'sdk:call', + requestId, + method, + data, + appId: this._appId, + }, '*'); + + // 设置超时 + setTimeout(() => { + window.removeEventListener('message', handler); + reject(new Error('系统调用超时')); + }, 10000); + }); + } + + // 包装API响应 + wrapResponse(promise) { + return promise + .then((data) => ({ success: true, data })) + .catch((error) => ({ + success: false, + error: error.message || '未知错误', + code: error.code || -1, + })); + } + } + + // 窗体SDK实现 + class WindowSDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async setTitle(title) { + return this.wrapResponse(this.sendToSystem('window.setTitle', { title })); + } + + async resize(width, height) { + return this.wrapResponse(this.sendToSystem('window.resize', { width, height })); + } + + async move(x, y) { + return this.wrapResponse(this.sendToSystem('window.move', { x, y })); + } + + async minimize() { + return this.wrapResponse(this.sendToSystem('window.minimize')); + } + + async maximize() { + return this.wrapResponse(this.sendToSystem('window.maximize')); + } + + async restore() { + return this.wrapResponse(this.sendToSystem('window.restore')); + } + + async close() { + return this.wrapResponse(this.sendToSystem('window.close')); + } + + async getState() { + return this.wrapResponse(this.sendToSystem('window.getState')); + } + + async getSize() { + return this.wrapResponse(this.sendToSystem('window.getSize')); + } + } + + // 存储SDK实现 + class StorageSDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async set(key, value) { + return this.wrapResponse(this.sendToSystem('storage.set', { key, value })); + } + + async get(key) { + return this.wrapResponse(this.sendToSystem('storage.get', { key })); + } + + async remove(key) { + return this.wrapResponse(this.sendToSystem('storage.remove', { key })); + } + + async clear() { + return this.wrapResponse(this.sendToSystem('storage.clear')); + } + + async keys() { + return this.wrapResponse(this.sendToSystem('storage.keys')); + } + + async has(key) { + return this.wrapResponse(this.sendToSystem('storage.has', { key })); + } + + async getStats() { + return this.wrapResponse(this.sendToSystem('storage.getStats')); + } + } + + // 网络SDK实现 + class NetworkSDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async request(url, config) { + return this.wrapResponse(this.sendToSystem('network.request', { url, config })); + } + + async get(url, config) { + return this.request(url, { ...config, method: 'GET' }); + } + + async post(url, data, config) { + return this.request(url, { ...config, method: 'POST', body: data }); + } + + async put(url, data, config) { + return this.request(url, { ...config, method: 'PUT', body: data }); + } + + async delete(url, config) { + return this.request(url, { ...config, method: 'DELETE' }); + } + } + + // 事件SDK实现 + class EventSDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async emit(channel, data) { + return this.wrapResponse(this.sendToSystem('events.emit', { channel, data })); + } + + async on(channel, callback, config) { + const result = await this.wrapResponse(this.sendToSystem('events.on', { channel, config })); + + if (result.success && result.data) { + // 注册事件监听器 + window.addEventListener('message', (event) => { + if (event.data?.type === 'system:event' && event.data?.subscriptionId === result.data) { + try { + callback(event.data.message); + } catch (error) { + console.error('事件回调处理错误:', error); + } + } + }); + } + + return result; + } + + async off(subscriptionId) { + return this.wrapResponse(this.sendToSystem('events.off', { subscriptionId })); + } + + async broadcast(channel, data) { + return this.wrapResponse(this.sendToSystem('events.broadcast', { channel, data })); + } + + async sendTo(targetAppId, data) { + return this.wrapResponse(this.sendToSystem('events.sendTo', { targetAppId, data })); + } + } + + // UI SDK实现 + class UISDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async showDialog(options) { + return this.wrapResponse(this.sendToSystem('ui.showDialog', options)); + } + + async showNotification(options) { + return this.wrapResponse(this.sendToSystem('ui.showNotification', options)); + } + + async showToast(message, type, duration) { + return this.wrapResponse(this.sendToSystem('ui.showToast', { message, type, duration })); + } + } + + // 系统SDK实现 + class SystemSDKImpl extends SDKBase { + constructor(appId) { + super(); + this._appId = appId; + } + + async getSystemInfo() { + return this.wrapResponse(this.sendToSystem('system.getSystemInfo')); + } + + async getAppInfo() { + return this.wrapResponse(this.sendToSystem('system.getAppInfo')); + } + + async getClipboard() { + return this.wrapResponse(this.sendToSystem('system.getClipboard')); + } + + async setClipboard(text) { + return this.wrapResponse(this.sendToSystem('system.setClipboard', { text })); + } + + async getCurrentTime() { + const result = await this.wrapResponse(this.sendToSystem('system.getCurrentTime')); + if (result.success && result.data) { + result.data = new Date(result.data); + } + return result; + } + + async generateUUID() { + return this.wrapResponse(this.sendToSystem('system.generateUUID')); + } + } + + // 主SDK实现类 + class SystemDesktopSDKImpl { + constructor() { + this.version = '1.0.0'; + this._appId = ''; + this._initialized = false; + + // 初始化各个子模块为null + this._window = null; + this._storage = null; + this._network = null; + this._events = null; + this._ui = null; + this._system = null; + } + + get appId() { + return this._appId; + } + + get initialized() { + return this._initialized; + } + + get window() { + if (!this._initialized) { + console.warn('[SystemSDK] window模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._window; + } + + get storage() { + if (!this._initialized) { + console.warn('[SystemSDK] storage模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._storage; + } + + get network() { + if (!this._initialized) { + console.warn('[SystemSDK] network模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._network; + } + + get events() { + if (!this._initialized) { + console.warn('[SystemSDK] events模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._events; + } + + get ui() { + if (!this._initialized) { + console.warn('[SystemSDK] ui模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._ui; + } + + get system() { + if (!this._initialized) { + console.warn('[SystemSDK] system模块未初始化'); + throw new Error('SDK未初始化'); + } + return this._system; + } + + async init(config) { + try { + console.log('[SystemSDK] 开始初始化SDK,配置:', config); + + if (this._initialized) { + console.warn('[SystemSDK] SDK已初始化'); + return { success: false, error: 'SDK已初始化' }; + } + + this._appId = config.appId; + + // 初始化各个子模块 + this._window = new WindowSDKImpl(this._appId); + this._storage = new StorageSDKImpl(this._appId); + this._network = new NetworkSDKImpl(this._appId); + this._events = new EventSDKImpl(this._appId); + this._ui = new UISDKImpl(this._appId); + this._system = new SystemSDKImpl(this._appId); + + this._initialized = true; + console.log('[SystemSDK] SDK初始化完成,应用ID:', this._appId); + + // 通知父窗口SDK已初始化 + window.parent.postMessage({ + type: 'sdk:initialized', + appId: this._appId + }, '*'); + + return { success: true, data: true }; + } catch (error) { + console.error('[SystemSDK] 初始化失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '初始化失败', + }; + } + } + + async destroy() { + try { + if (!this._initialized) { + return { success: false, error: 'SDK未初始化' }; + } + + this._initialized = false; + this._appId = ''; + console.log('[SystemSDK] SDK已销毁'); + + return { success: true, data: true }; + } catch (error) { + console.error('[SystemSDK] 销毁失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '销毁失败', + }; + } + } + } + + // 创建全局SDK实例 + const SystemSDK = new SystemDesktopSDKImpl(); + + // 在window对象上挂载SDK + window.SystemSDK = SystemSDK; + + console.log('[SystemSDK] SDK已在iframe中注入并挂载到window对象'); + + // 通知系统应用已准备就绪 + window.parent.postMessage({ type: 'app:ready' }, '*'); + })(); + `; + + iframeDoc.head.appendChild(script) + } + + /** + * 设置性能监控 + */ + private setupPerformanceMonitoring(sandbox: SandboxInstance): void { + if (!this.performanceData.has(sandbox.id)) { + this.performanceData.set(sandbox.id, []) + } + } + + /** + * 记录性能数据 + */ + private recordPerformance(sandbox: SandboxInstance, data: any): void { + const performanceRecord: SandboxPerformance = { + sandboxId: sandbox.id, + timestamp: new Date(), + memoryUsage: data.memoryUsage || 0, + cpuUsage: data.cpuUsage || 0, + networkRequests: data.networkRequests || 0, + renderTime: data.renderTime || 0, + jsExecutionTime: data.jsExecutionTime || 0 + } + + const records = this.performanceData.get(sandbox.id)! + records.push(performanceRecord) + + // 保留最近1000条记录 + if (records.length > 1000) { + records.splice(0, records.length - 1000) + } + + // 更新沙箱性能指标 + sandbox.memoryUsage = performanceRecord.memoryUsage + sandbox.cpuUsage = performanceRecord.cpuUsage + sandbox.networkRequests = performanceRecord.networkRequests + + // 检查性能阈值 + this.checkPerformanceThresholds(sandbox, performanceRecord) + } + + /** + * 检查性能阈值 + */ + private checkPerformanceThresholds(sandbox: SandboxInstance, metrics: SandboxPerformance): void { + // 内存使用检查 + if (metrics.memoryUsage > sandbox.config.maxMemoryUsage) { + this.eventService.sendMessage('system', 'performance-alert', { + sandboxId: sandbox.id, + type: 'memory', + usage: metrics.memoryUsage, + limit: sandbox.config.maxMemoryUsage + }) + } + + // CPU使用检查 + if (metrics.cpuUsage > sandbox.config.maxCpuUsage) { + this.eventService.sendMessage('system', 'performance-alert', { + sandboxId: sandbox.id, + type: 'cpu', + usage: metrics.cpuUsage, + limit: sandbox.config.maxCpuUsage + }) + } + } + + /** + * 开始性能监控 + */ + private startPerformanceMonitoring(): void { + this.monitoringInterval = setInterval(() => { + for (const sandbox of this.sandboxes.values()) { + if (sandbox.state === SandboxState.RUNNING) { + this.collectPerformanceMetrics(sandbox) + } + } + }, 5000) // 每5秒收集一次性能数据 + } + + /** + * 收集性能指标 + */ + private collectPerformanceMetrics(sandbox: SandboxInstance): void { + if (sandbox.iframe && sandbox.iframe.contentWindow) { + // 请求性能数据 + sandbox.iframe.contentWindow.postMessage({ + type: 'system:performance-request' + }, '*') + } + } + + /** + * 更新沙箱状态 + */ + private updateSandboxState(sandbox: SandboxInstance, newState: SandboxState): void { + const oldState = sandbox.state + sandbox.state = newState + + // 触发状态变化事件 + this.eventService.sendMessage('system', 'sandbox-state-change', { + sandboxId: sandbox.id, + newState, + oldState + }) + } +} \ No newline at end of file diff --git a/src/services/EventCommunicationService.ts b/src/services/EventCommunicationService.ts new file mode 100644 index 0000000..383c278 --- /dev/null +++ b/src/services/EventCommunicationService.ts @@ -0,0 +1,639 @@ +import { reactive, ref } from 'vue' +import type { IEventBuilder } from '@/events/IEventBuilder' +import { v4 as uuidv4 } from 'uuid' + +/** + * 消息类型枚举 + */ +export enum MessageType { + SYSTEM = 'system', + APPLICATION = 'application', + USER_INTERACTION = 'user_interaction', + CROSS_APP = 'cross_app', + BROADCAST = 'broadcast' +} + +/** + * 消息优先级枚举 + */ +export enum MessagePriority { + LOW = 0, + NORMAL = 1, + HIGH = 2, + CRITICAL = 3 +} + +/** + * 消息状态枚举 + */ +export enum MessageStatus { + PENDING = 'pending', + SENT = 'sent', + DELIVERED = 'delivered', + FAILED = 'failed', + EXPIRED = 'expired' +} + +/** + * 事件消息接口 + */ +export interface EventMessage { + id: string + type: MessageType + priority: MessagePriority + senderId: string + receiverId?: string // undefined表示广播消息 + channel: string + payload: any + timestamp: Date + expiresAt?: Date + status: MessageStatus + retryCount: number + maxRetries: number +} + +/** + * 事件订阅者接口 + */ +export interface EventSubscriber { + id: string + appId: string + channel: string + handler: (message: EventMessage) => void | Promise + filter?: (message: EventMessage) => boolean + priority: MessagePriority + createdAt: Date + active: boolean +} + +/** + * 通信通道接口 + */ +export interface CommunicationChannel { + name: string + description: string + restricted: boolean // 是否需要权限 + allowedApps: string[] // 允许访问的应用ID列表 + maxMessageSize: number // 最大消息大小(字节) + messageRetention: number // 消息保留时间(毫秒) +} + +/** + * 事件统计信息 + */ +export interface EventStatistics { + totalMessagesSent: number + totalMessagesReceived: number + totalBroadcasts: number + failedMessages: number + activeSubscribers: number + channelUsage: Map +} + +/** + * 事件通信服务类 + */ +export class EventCommunicationService { + private subscribers = reactive(new Map()) + private messageQueue = reactive(new Map()) // 按应用分组的消息队列 + private messageHistory = reactive(new Map()) // 消息历史记录 + private channels = reactive(new Map()) + private statistics = reactive({ + totalMessagesSent: 0, + totalMessagesReceived: 0, + totalBroadcasts: 0, + failedMessages: 0, + activeSubscribers: 0, + channelUsage: new Map() + }) + + private processingInterval: number | null = null + private eventBus: IEventBuilder + + constructor(eventBus: IEventBuilder) { + this.eventBus = eventBus + this.initializeDefaultChannels() + this.startMessageProcessing() + } + + /** + * 订阅事件频道 + */ + subscribe( + appId: string, + channel: string, + handler: (message: EventMessage) => void | Promise, + options: { + filter?: (message: EventMessage) => boolean + priority?: MessagePriority + } = {} + ): string { + // 检查通道权限 + if (!this.canAccessChannel(appId, channel)) { + throw new Error(`应用 ${appId} 无权访问频道 ${channel}`) + } + + const subscriberId = uuidv4() + const subscriber: EventSubscriber = { + id: subscriberId, + appId, + channel, + handler, + filter: options.filter, + priority: options.priority || MessagePriority.NORMAL, + createdAt: new Date(), + active: true + } + + this.subscribers.set(subscriberId, subscriber) + this.updateActiveSubscribersCount() + + console.log(`应用 ${appId} 订阅了频道 ${channel}`) + return subscriberId + } + + /** + * 取消订阅 + */ + unsubscribe(subscriberId: string): boolean { + const result = this.subscribers.delete(subscriberId) + if (result) { + this.updateActiveSubscribersCount() + console.log(`取消订阅: ${subscriberId}`) + } + return result + } + + /** + * 发送消息 + */ + async sendMessage( + senderId: string, + channel: string, + payload: any, + options: { + receiverId?: string + priority?: MessagePriority + type?: MessageType + expiresIn?: number // 过期时间(毫秒) + maxRetries?: number + } = {} + ): Promise { + // 检查发送者权限 + if (!this.canAccessChannel(senderId, channel)) { + throw new Error(`应用 ${senderId} 无权向频道 ${channel} 发送消息`) + } + + // 检查消息大小 + const messageSize = JSON.stringify(payload).length + const channelConfig = this.channels.get(channel) + if (channelConfig && messageSize > channelConfig.maxMessageSize) { + throw new Error(`消息大小超出限制: ${messageSize} > ${channelConfig.maxMessageSize}`) + } + + const messageId = uuidv4() + const now = new Date() + + const message: EventMessage = { + id: messageId, + type: options.type || MessageType.APPLICATION, + priority: options.priority || MessagePriority.NORMAL, + senderId, + receiverId: options.receiverId, + channel, + payload, + timestamp: now, + expiresAt: options.expiresIn ? new Date(now.getTime() + options.expiresIn) : undefined, + status: MessageStatus.PENDING, + retryCount: 0, + maxRetries: options.maxRetries || 3 + } + + // 如果是点对点消息,直接发送 + if (options.receiverId) { + await this.deliverMessage(message) + } else { + // 广播消息,加入队列处理 + this.addToQueue(message) + } + + // 更新统计信息 + this.statistics.totalMessagesSent++ + if (!options.receiverId) { + this.statistics.totalBroadcasts++ + } + + const channelUsage = this.statistics.channelUsage.get(channel) || 0 + this.statistics.channelUsage.set(channel, channelUsage + 1) + + // 记录消息历史 + this.recordMessage(message) + + console.log(`[EventCommunication] 消息 ${messageId} 已发送到频道 ${channel}[发送者: ${senderId}]`) + return messageId + } + + /** + * 广播消息到所有订阅者 + */ + async broadcast( + senderId: string, + channel: string, + payload: any, + options: { + priority?: MessagePriority + expiresIn?: number + } = {} + ): Promise { + return this.sendMessage(senderId, channel, payload, { + ...options, + type: MessageType.BROADCAST + }) + } + + /** + * 发送跨应用消息 + */ + async sendCrossAppMessage( + senderId: string, + receiverId: string, + payload: any, + options: { + priority?: MessagePriority + expiresIn?: number + } = {} + ): Promise { + const channel = 'cross-app' + + return this.sendMessage(senderId, channel, payload, { + ...options, + receiverId, + type: MessageType.CROSS_APP + }) + } + + /** + * 获取消息历史 + */ + getMessageHistory( + appId: string, + options: { + channel?: string + limit?: number + since?: Date + } = {} + ): EventMessage[] { + const history = this.messageHistory.get(appId) || [] + + let filtered = history.filter(msg => + msg.senderId === appId || msg.receiverId === appId + ) + + if (options.channel) { + filtered = filtered.filter(msg => msg.channel === options.channel) + } + + if (options.since) { + filtered = filtered.filter(msg => msg.timestamp >= options.since!) + } + + if (options.limit) { + filtered = filtered.slice(-options.limit) + } + + return filtered.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) + } + + /** + * 获取应用的订阅列表 + */ + getAppSubscriptions(appId: string): EventSubscriber[] { + return Array.from(this.subscribers.values()).filter(sub => sub.appId === appId) + } + + /** + * 获取频道订阅者数量 + */ + getChannelSubscriberCount(channel: string): number { + return Array.from(this.subscribers.values()).filter( + sub => sub.channel === channel && sub.active + ).length + } + + /** + * 创建通信频道 + */ + createChannel( + channel: string, + config: Omit + ): boolean { + if (this.channels.has(channel)) { + return false + } + + this.channels.set(channel, { + name: channel, + ...config + }) + + console.log(`创建通信频道: ${channel}`) + return true + } + + /** + * 删除通信频道 + */ + deleteChannel(channel: string): boolean { + // 移除所有相关订阅 + const subscribersToRemove = Array.from(this.subscribers.entries()) + .filter(([, sub]) => sub.channel === channel) + .map(([id]) => id) + + subscribersToRemove.forEach(id => this.unsubscribe(id)) + + // 删除频道 + const result = this.channels.delete(channel) + + if (result) { + console.log(`删除通信频道: ${channel}`) + } + + return result + } + + /** + * 获取统计信息 + */ + getStatistics(): EventStatistics { + return { ...this.statistics } + } + + /** + * 清理过期消息和订阅 + */ + cleanup(): void { + const now = new Date() + + // 清理过期消息 + for (const [appId, messages] of this.messageQueue.entries()) { + const validMessages = messages.filter(msg => + !msg.expiresAt || msg.expiresAt > now + ) + + if (validMessages.length !== messages.length) { + this.messageQueue.set(appId, validMessages) + } + } + + // 清理消息历史(保留最近7天) + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) + for (const [appId, history] of this.messageHistory.entries()) { + const recentHistory = history.filter(msg => msg.timestamp > sevenDaysAgo) + this.messageHistory.set(appId, recentHistory) + } + + console.log('事件通信服务清理完成') + } + + /** + * 销毁服务 + */ + destroy(): void { + if (this.processingInterval) { + clearInterval(this.processingInterval) + this.processingInterval = null + } + + this.subscribers.clear() + this.messageQueue.clear() + this.messageHistory.clear() + this.channels.clear() + + console.log('事件通信服务已销毁') + } + + // 私有方法 + + /** + * 初始化默认频道 + */ + private initializeDefaultChannels(): void { + // 系统事件频道 + this.createChannel('system', { + description: '系统级事件通信', + restricted: true, + allowedApps: ['system'], + maxMessageSize: 1024 * 10, // 10KB + messageRetention: 24 * 60 * 60 * 1000 // 24小时 + }) + + // 应用间通信频道 + this.createChannel('cross-app', { + description: '应用间通信', + restricted: false, + allowedApps: [], + maxMessageSize: 1024 * 100, // 100KB + messageRetention: 7 * 24 * 60 * 60 * 1000 // 7天 + }) + + // 用户交互频道 + this.createChannel('user-interaction', { + description: '用户交互事件', + restricted: false, + allowedApps: [], + maxMessageSize: 1024 * 5, // 5KB + messageRetention: 60 * 60 * 1000 // 1小时 + }) + + // 广播频道 + this.createChannel('broadcast', { + description: '系统广播', + restricted: true, + allowedApps: ['system'], + maxMessageSize: 1024 * 50, // 50KB + messageRetention: 24 * 60 * 60 * 1000 // 24小时 + }) + } + + /** + * 检查应用是否可以访问频道 + */ + private canAccessChannel(appId: string, channel: string): boolean { + const channelConfig = this.channels.get(channel) + + if (!channelConfig) { + // 频道不存在,默认允许 + return true + } + + if (!channelConfig.restricted) { + return true + } + + // 系统应用总是有权限 + if (appId === 'system') { + return true + } + + return channelConfig.allowedApps.includes(appId) + } + + /** + * 添加消息到队列 + */ + private addToQueue(message: EventMessage): void { + const queueKey = message.receiverId || 'broadcast' + + if (!this.messageQueue.has(queueKey)) { + this.messageQueue.set(queueKey, []) + } + + const queue = this.messageQueue.get(queueKey)! + + // 按优先级插入 + const insertIndex = queue.findIndex(msg => msg.priority < message.priority) + if (insertIndex === -1) { + queue.push(message) + } else { + queue.splice(insertIndex, 0, message) + } + } + + /** + * 直接投递消息 + */ + private async deliverMessage(message: EventMessage): Promise { + try { + const subscribers = this.getRelevantSubscribers(message) + + if (subscribers.length === 0) { + message.status = MessageStatus.FAILED + console.warn(`[EventCommunication] 没有找到频道 ${message.channel} 的订阅者[消息 ID: ${message.id}]`) + return + } + + // 并行发送给所有订阅者 + const deliveryPromises = subscribers.map(async (subscriber) => { + try { + // 应用过滤器 + if (subscriber.filter && !subscriber.filter(message)) { + return + } + + await subscriber.handler(message) + this.statistics.totalMessagesReceived++ + console.log(`[EventCommunication] 消息 ${message.id} 已投递给订阅者 ${subscriber.id}[频道: ${message.channel}]`) + } catch (error) { + console.error(`向订阅者 ${subscriber.id} 发送消息失败:`, error) + throw error + } + }) + + await Promise.allSettled(deliveryPromises) + message.status = MessageStatus.DELIVERED + + } catch (error) { + message.status = MessageStatus.FAILED + this.statistics.failedMessages++ + console.error('消息投递失败:', error) + + // 重试机制 + if (message.retryCount < message.maxRetries) { + message.retryCount++ + message.status = MessageStatus.PENDING + setTimeout(() => this.deliverMessage(message), 1000 * message.retryCount) + } + } + } + + /** + * 获取相关订阅者 + */ + private getRelevantSubscribers(message: EventMessage): EventSubscriber[] { + return Array.from(this.subscribers.values()).filter(subscriber => { + if (!subscriber.active) return false + if (subscriber.channel !== message.channel) return false + + // 点对点消息检查接收者 + if (message.receiverId && subscriber.appId !== message.receiverId) { + return false + } + + return true + }) + } + + /** + * 开始消息处理循环 + */ + private startMessageProcessing(): void { + this.processingInterval = setInterval(() => { + this.processMessageQueue() + this.cleanupExpiredMessages() + }, 100) // 每100ms处理一次 + } + + /** + * 处理消息队列 + */ + private processMessageQueue(): void { + for (const [queueKey, messages] of this.messageQueue.entries()) { + if (messages.length === 0) continue + + // 处理优先级最高的消息 + const message = messages.shift()! + + // 检查消息是否过期 + if (message.expiresAt && message.expiresAt <= new Date()) { + message.status = MessageStatus.EXPIRED + continue + } + + this.deliverMessage(message) + } + } + + /** + * 清理过期消息 + */ + private cleanupExpiredMessages(): void { + const now = new Date() + + for (const [queueKey, messages] of this.messageQueue.entries()) { + const validMessages = messages.filter(msg => + !msg.expiresAt || msg.expiresAt > now + ) + + if (validMessages.length !== messages.length) { + this.messageQueue.set(queueKey, validMessages) + } + } + } + + /** + * 记录消息历史 + */ + private recordMessage(message: EventMessage): void { + // 记录发送者历史 + if (!this.messageHistory.has(message.senderId)) { + this.messageHistory.set(message.senderId, []) + } + this.messageHistory.get(message.senderId)!.push(message) + + // 记录接收者历史 + if (message.receiverId && message.receiverId !== message.senderId) { + if (!this.messageHistory.has(message.receiverId)) { + this.messageHistory.set(message.receiverId, []) + } + this.messageHistory.get(message.receiverId)!.push(message) + } + } + + /** + * 更新活跃订阅者数量 + */ + private updateActiveSubscribersCount(): void { + this.statistics.activeSubscribers = Array.from(this.subscribers.values()) + .filter(sub => sub.active).length + } +} \ No newline at end of file diff --git a/src/services/ExternalAppDiscovery.ts b/src/services/ExternalAppDiscovery.ts new file mode 100644 index 0000000..39ebf1a --- /dev/null +++ b/src/services/ExternalAppDiscovery.ts @@ -0,0 +1,629 @@ +import { reactive } from 'vue' +import type { AppManifest } from './ApplicationLifecycleManager' + +/** + * 外置应用信息 + */ +export interface ExternalApp { + id: string + manifest: AppManifest + basePath: string + manifestPath: string + entryPath: string + discovered: boolean + lastScanned: Date +} + +/** + * 外置应用发现服务 + * 自动扫描 public/apps 目录下的外部应用 + * + * 注意: + * - 仅处理外部应用,不扫描内置应用 + * - 内置应用通过 AppRegistry 静态注册 + * - 已排除内置应用: calculator, notepad, todo + */ +export class ExternalAppDiscovery { + private static instance: ExternalAppDiscovery | null = null + private discoveredApps = reactive(new Map()) + private isScanning = false + private hasStarted = false // 添加标志防止重复启动 + + constructor() { + console.log('[ExternalAppDiscovery] 服务初始化') + } + + /** + * 获取单例实例 + */ + static getInstance(): ExternalAppDiscovery { + if (!ExternalAppDiscovery.instance) { + ExternalAppDiscovery.instance = new ExternalAppDiscovery() + } + return ExternalAppDiscovery.instance + } + + /** + * 启动应用发现服务(只执行一次扫描,不设置定时器) + */ + async startDiscovery(): Promise { + // 防止重复启动 + if (this.hasStarted) { + console.log('[ExternalAppDiscovery] 服务已启动,跳过重复启动') + return + } + + console.log('[ExternalAppDiscovery] 启动应用发现服务') + this.hasStarted = true + + // 只执行一次扫描,不设置定时器 + console.log('[ExternalAppDiscovery] 开始执行扫描...') + await this.scanExternalApps() + console.log('[ExternalAppDiscovery] 扫描完成') + } + + /** + * 停止应用发现服务 + */ + stopDiscovery(): void { + console.log('[ExternalAppDiscovery] 停止应用发现服务') + + this.hasStarted = false + } + + /** + * 扫描外置应用 + */ + async scanExternalApps(): Promise { + if (this.isScanning) { + console.log('[ExternalAppDiscovery] 正在扫描中,跳过本次扫描') + return + } + + this.isScanning = true + console.log('[ExternalAppDiscovery] ==> 开始扫描外置应用') + + try { + // 获取 public/apps 目录下的所有应用文件夹 + const appDirs = await this.getAppDirectories() + console.log(`[ExternalAppDiscovery] 发现 ${appDirs.length} 个应用目录:`, appDirs) + + const newApps = new Map() + + // 扫描每个应用目录 + for (const appDir of appDirs) { + try { + console.log(`[ExternalAppDiscovery] 扫描应用目录: ${appDir}`) + const app = await this.scanAppDirectory(appDir) + if (app) { + newApps.set(app.id, app) + console.log(`[ExternalAppDiscovery] ✓ 成功扫描应用: ${app.manifest.name} (${app.id})`) + } else { + console.log(`[ExternalAppDiscovery] ✗ 应用目录 ${appDir} 扫描失败或不存在`) + } + } catch (error) { + if (error instanceof SyntaxError && error.message.includes('Unexpected token')) { + console.warn( + `[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 格式错误或返回HTML页面`, + ) + } else { + console.warn(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 失败:`, error) + } + } + } + + // 更新发现的应用列表 + this.updateDiscoveredApps(newApps) + + console.log(`[ExternalAppDiscovery] ==> 扫描完成,发现 ${newApps.size} 个有效应用`) + console.log(`[ExternalAppDiscovery] 当前总共有 ${this.discoveredApps.size} 个已发现应用`) + } catch (error) { + console.error('[ExternalAppDiscovery] 扫描外置应用失败:', error) + } finally { + this.isScanning = false + } + } + + /** + * 获取应用目录列表 + */ + private async getAppDirectories(): Promise { + try { + console.log('[ExternalAppDiscovery] 开始获取应用目录列表') + + // 方案1:使用Vite的glob功能(推荐) + console.log('[ExternalAppDiscovery] 尝试使用Vite glob功能') + const knownApps = await this.getKnownAppDirectories() + console.log('[ExternalAppDiscovery] Vite glob结果:', knownApps) + const validApps: string[] = [] + + // 验证已知应用是否真实存在 + for (const appDir of knownApps) { + try { + const manifestPath = `/apps/${appDir}/manifest.json` + console.log(`[ExternalAppDiscovery] 检查应用 ${appDir} 的 manifest.json: ${manifestPath}`) + const response = await fetch(manifestPath, { method: 'HEAD' }) + + if (response.ok) { + const contentType = response.headers.get('content-type') + console.log( + `[ExternalAppDiscovery] 应用 ${appDir} 的响应状态: ${response.status}, 内容类型: ${contentType}`, + ) + // 检查是否返回JSON内容 + if ( + contentType && + (contentType.includes('application/json') || contentType.includes('text/json')) + ) { + validApps.push(appDir) + console.log(`[ExternalAppDiscovery] 确认应用存在: ${appDir}`) + } else { + console.warn(`[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 返回非JSON内容`) + } + } else { + console.warn(`[ExternalAppDiscovery] 应用不存在: ${appDir} (HTTP ${response.status})`) + } + } catch (error) { + console.warn(`[ExternalAppDiscovery] 检查应用 ${appDir} 时出错:`, error) + } + } + + console.log('[ExternalAppDiscovery] 验证后的有效应用:', validApps) + + // 如果Vite glob没有找到应用,尝试其他方法 + if (validApps.length === 0) { + console.log('[ExternalAppDiscovery] Vite glob未找到有效应用,尝试网络请求方式') + + // 方案2:尝试目录列表扫描 + try { + console.log('[ExternalAppDiscovery] 尝试目录列表扫描') + const additionalApps = await this.tryDirectoryListing() + console.log('[ExternalAppDiscovery] 目录列表扫描结果:', additionalApps) + // 合并去重 + for (const app of additionalApps) { + if (!validApps.includes(app)) { + validApps.push(app) + } + } + } catch (error) { + console.log('[ExternalAppDiscovery] 目录列表扫描失败') + } + + // 方案3:尝试扫描常见应用名称 + if (validApps.length === 0) { + try { + console.log('[ExternalAppDiscovery] 尝试扫描常见应用名称') + const commonApps = await this.tryCommonAppNames() + console.log('[ExternalAppDiscovery] 常见应用扫描结果:', commonApps) + for (const app of commonApps) { + if (!validApps.includes(app)) { + validApps.push(app) + } + } + } catch (error) { + console.log('[ExternalAppDiscovery] 常见应用扫描失败') + } + } + } + + console.log(`[ExternalAppDiscovery] 最终发现 ${validApps.length} 个应用目录:`, validApps) + return validApps + } catch (error) { + console.warn('[ExternalAppDiscovery] 获取目录列表失败,使用静态列表:', error) + const fallbackList = [ + 'music-player', // 音乐播放器应用 + // 可以在这里添加更多已知的外部应用 + ] + console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList) + return fallbackList + } + } + + /** + * 尝试通过 fetch 获取目录列表(开发环境可能失败) + */ + private async tryDirectoryListing(): Promise { + try { + console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表') + const response = await fetch('/apps/') + + console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const html = await response.text() + console.log('[ExternalAppDiscovery] 响应内容类型:', response.headers.get('content-type')) + console.log('[ExternalAppDiscovery] 响应内容长度:', html.length) + + // 检查是否真的是目录列表还是index.html + if (html.includes(' { + // 排除内置应用,只扫描外部应用 + const builtInApps = ['calculator', 'notepad', 'todo'] + + // 常见的外部应用名称列表 + const commonNames = [ + 'file-manager', + 'text-editor', + 'image-viewer', + 'video-player', + 'chat-app', + 'weather-app', + 'calendar-app', + 'email-client', + 'web-browser', + 'code-editor', + ].filter((name) => !builtInApps.includes(name)) // 过滤掉内置应用 + + const validApps: string[] = [] + + // 检查每个常见应用是否实际存在 + for (const appName of commonNames) { + try { + const manifestPath = `/apps/${appName}/manifest.json` + const response = await fetch(manifestPath, { method: 'HEAD' }) + + // 检查响应状态和内容类型 + if (response.ok) { + const contentType = response.headers.get('content-type') + // 只有在返回JSON内容时才认为找到了有效应用 + if ( + contentType && + (contentType.includes('application/json') || contentType.includes('text/json')) + ) { + validApps.push(appName) + console.log(`[ExternalAppDiscovery] 发现常见应用: ${appName}`) + } else { + console.debug( + `[ExternalAppDiscovery] 应用 ${appName} 存在但 manifest.json 返回非JSON内容`, + ) + } + } else { + console.debug(`[ExternalAppDiscovery] 应用 ${appName} 不存在 (HTTP ${response.status})`) + } + } catch (error) { + // 静默失败,不记录日志避免噪音 + console.debug(`[ExternalAppDiscovery] 检查应用 ${appName} 时出现网络错误`) + } + } + + return validApps + } + + /** + * 解析目录列表HTML + */ + private parseDirectoryListing(html: string): string[] { + console.log('[ExternalAppDiscovery] 解析目录列表HTML (前1000字符):', html.substring(0, 1000)) // 调试输出 + + const directories: string[] = [] + const builtInApps = ['calculator', 'notepad', 'todo'] // 内置应用列表 + + // 使用最简单有效的方法 + // 查找所有形如 /apps/dirname/ 的路径 + const pattern = /\/apps\/([^\/"'\s>]+)\//g + let match + while ((match = pattern.exec(html)) !== null) { + const dirName = match[1] + console.log(`[ExternalAppDiscovery] 匹配到目录: ${dirName}`) + // 确保目录名有效且不是内置应用 + if ( + dirName && + dirName.length > 0 && + !dirName.startsWith('.') && + !builtInApps.includes(dirName) && + !directories.includes(dirName) + ) { + directories.push(dirName) + } + } + + // 去重 + const uniqueDirs = [...new Set(directories)] + console.log('[ExternalAppDiscovery] 最终解析结果:', uniqueDirs) + return uniqueDirs + } + + /** + * 测试目录解析功能 + */ + private testParseDirectoryListing(): void { + // 测试方法已移除 + } + + /** + * 获取已知的应用目录 + */ + private async getKnownAppDirectories(): Promise { + try { + console.log('[ExternalAppDiscovery] 使用Vite glob导入获取应用目录') + + // 使用Vite的glob功能静态导入所有manifest.json文件 + const manifestModules = import.meta.glob('/public/apps/*/manifest.json') + + // 从文件路径中提取应用目录名 + const appDirs: string[] = [] + for (const path in manifestModules) { + // 路径格式: /public/apps/app-name/manifest.json + const match = path.match(/\/public\/apps\/([^\/]+)\/manifest\.json/) + if (match && match[1]) { + const appDir = match[1] + // 排除内置应用 + if (!this.isBuiltInApp(appDir)) { + appDirs.push(appDir) + } + } + } + + console.log(`[ExternalAppDiscovery] 通过Vite glob发现外部应用目录: ${appDirs.join(', ')}`) + return appDirs + } catch (error) { + console.warn('[ExternalAppDiscovery] 使用Vite glob读取应用目录失败:', error) + // 回退到静态列表 + const fallbackList = [ + 'music-player', // 音乐播放器应用 + // 可以在这里添加更多已知的外部应用 + ] + console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList) + return fallbackList + } + } + + /** + * 通过网络请求获取应用目录(备用方法) + */ + private async getKnownAppDirectoriesViaNetwork(): Promise { + try { + console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表 /apps/') + + // 尝试通过网络请求获取目录列表 + const response = await fetch('/public/apps/') + + console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status) + + if (!response.ok) { + console.log('[ExternalAppDiscovery] 响应不成功,使用回退列表') + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const contentType = response.headers.get('content-type') + console.log('[ExternalAppDiscovery] 响应内容类型:', contentType) + + const html = await response.text() + console.log(11111111, html) + + console.log('[ExternalAppDiscovery] 目录列表HTML长度:', html.length) + + const appDirs = this.parseDirectoryListing(html) + console.log('[ExternalAppDiscovery] 解析到的应用目录:', appDirs) + + // 过滤掉内置应用 + const externalApps = appDirs.filter((dir) => !this.isBuiltInApp(dir)) + + console.log(`[ExternalAppDiscovery] 通过目录列表发现外部应用目录: ${externalApps.join(', ')}`) + return externalApps + } catch (error) { + console.warn('[ExternalAppDiscovery] 获取目录列表失败:', error) + // 回退到静态列表 + const fallbackList = [ + 'music-player', // 音乐播放器应用 + // 可以在这里添加更多已知的外部应用 + ] + console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList) + return fallbackList + } + } + + /** + * 扫描单个应用目录 + */ + private async scanAppDirectory(appDir: string): Promise { + try { + // 首先检查是否为内置应用 + if (this.isBuiltInApp(appDir)) { + console.log(`[ExternalAppDiscovery] 跳过内置应用: ${appDir}`) + return null + } + + const basePath = `/apps/${appDir}` + const manifestPath = `${basePath}/manifest.json` + + console.log(`[ExternalAppDiscovery] 扫描外部应用目录: ${appDir}`) + + // 尝试获取 manifest.json + const manifestResponse = await fetch(manifestPath) + + if (!manifestResponse.ok) { + console.warn( + `[ExternalAppDiscovery] 未找到 manifest.json: ${manifestPath} (HTTP ${manifestResponse.status})`, + ) + return null + } + + // 检查响应内容类型 + const contentType = manifestResponse.headers.get('content-type') + if (!contentType || !contentType.includes('application/json')) { + console.warn( + `[ExternalAppDiscovery] manifest.json 返回了非JSON内容: ${manifestPath}, content-type: ${contentType}`, + ) + return null + } + + let manifest: AppManifest + try { + manifest = (await manifestResponse.json()) as AppManifest + } catch (parseError) { + console.warn(`[ExternalAppDiscovery] 解析 manifest.json 失败: ${manifestPath}`, parseError) + return null + } + + // 验证 manifest 格式 + if (!this.validateManifest(manifest)) { + console.warn(`[ExternalAppDiscovery] 无效的 manifest.json: ${manifestPath}`) + return null + } + + // 再次检查 manifest.id 是否为内置应用 + if (this.isBuiltInApp(manifest.id)) { + console.warn(`[ExternalAppDiscovery] 检测到内置应用 ID: ${manifest.id},跳过`) + return null + } + + const entryPath = `${basePath}/${manifest.entryPoint}` + + // 验证入口文件是否存在 + const entryResponse = await fetch(entryPath, { method: 'HEAD' }) + if (!entryResponse.ok) { + console.warn(`[ExternalAppDiscovery] 入口文件不存在: ${entryPath}`) + return null + } + + const app: ExternalApp = { + id: manifest.id, + manifest, + basePath, + manifestPath, + entryPath, + discovered: true, + lastScanned: new Date(), + } + + console.log(`[ExternalAppDiscovery] 发现有效外部应用: ${manifest.name} (${manifest.id})`) + return app + } catch (error) { + console.error(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 时出错:`, error) + return null + } + } + + /** + * 检查是否为内置应用 + */ + private isBuiltInApp(appId: string): boolean { + const builtInApps = ['calculator', 'notepad', 'todo'] + return builtInApps.includes(appId) + } + + /** + * 验证应用清单 + */ + private validateManifest(manifest: any): manifest is AppManifest { + if (!manifest || typeof manifest !== 'object') { + return false + } + + // 检查必需字段 + const requiredFields = ['id', 'name', 'version', 'entryPoint'] + for (const field of requiredFields) { + if (!manifest[field] || typeof manifest[field] !== 'string') { + console.warn(`[ExternalAppDiscovery] manifest 缺少必需字段: ${field}`) + return false + } + } + + // 验证版本格式 + if (!/^\d+\.\d+\.\d+/.test(manifest.version)) { + console.warn(`[ExternalAppDiscovery] 版本号格式不正确: ${manifest.version}`) + return false + } + + // 验证应用ID格式 + if (!/^[a-zA-Z0-9._-]+$/.test(manifest.id)) { + console.warn(`[ExternalAppDiscovery] 应用ID格式不正确: ${manifest.id}`) + return false + } + + return true + } + + /** + * 更新发现的应用列表 + */ + private updateDiscoveredApps(newApps: Map): void { + // 移除不再存在的应用 + for (const [appId] of this.discoveredApps) { + if (!newApps.has(appId)) { + console.log(`[ExternalAppDiscovery] 应用已移除: ${appId}`) + this.discoveredApps.delete(appId) + } + } + + // 添加或更新应用 + for (const [appId, app] of newApps) { + const existingApp = this.discoveredApps.get(appId) + + if (!existingApp) { + console.log(`[ExternalAppDiscovery] 发现新应用: ${app.manifest.name} (${appId})`) + this.discoveredApps.set(appId, app) + } else if (existingApp.manifest.version !== app.manifest.version) { + console.log( + `[ExternalAppDiscovery] 应用版本更新: ${appId} ${existingApp.manifest.version} -> ${app.manifest.version}`, + ) + this.discoveredApps.set(appId, app) + } else { + // 只更新扫描时间 + existingApp.lastScanned = app.lastScanned + } + } + } + + /** + * 获取所有发现的应用 + */ + getDiscoveredApps(): ExternalApp[] { + return Array.from(this.discoveredApps.values()) + } + + /** + * 获取指定应用 + */ + getApp(appId: string): ExternalApp | undefined { + return this.discoveredApps.get(appId) + } + + /** + * 检查应用是否存在 + */ + hasApp(appId: string): boolean { + return this.discoveredApps.has(appId) + } + + /** + * 获取应用数量 + */ + getAppCount(): number { + return this.discoveredApps.size + } + + /** + * 手动刷新应用列表 + */ + async refresh(): Promise { + console.log('[ExternalAppDiscovery] 手动刷新应用列表') + await this.scanExternalApps() + } +} + +// 导出单例实例 +export const externalAppDiscovery = ExternalAppDiscovery.getInstance() diff --git a/src/services/ResourceService.ts b/src/services/ResourceService.ts new file mode 100644 index 0000000..761fd2d --- /dev/null +++ b/src/services/ResourceService.ts @@ -0,0 +1,689 @@ +import { reactive, ref } from 'vue' +import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder' + +/** + * 资源类型枚举 + */ +export enum ResourceType { + LOCAL_STORAGE = 'localStorage', + NETWORK = 'network', + FILE_SYSTEM = 'fileSystem', + NOTIFICATION = 'notification', + CLIPBOARD = 'clipboard', + MEDIA = 'media', + GEOLOCATION = 'geolocation', +} + +/** + * 权限级别枚举 + */ +export enum PermissionLevel { + DENIED = 'denied', + GRANTED = 'granted', + PROMPT = 'prompt', +} + +/** + * 权限请求结果 + */ +export interface PermissionRequest { + id: string + appId: string + resourceType: ResourceType + description: string + requestedAt: Date + status: PermissionLevel + approvedAt?: Date + deniedAt?: Date + expiresAt?: Date +} + +/** + * 资源访问配置 + */ +export interface ResourceAccessConfig { + maxStorageSize: number // 本地存储最大容量(MB) + allowedDomains: string[] // 允许访问的网络域名 + maxNetworkRequests: number // 每分钟最大网络请求数 + allowFileAccess: boolean // 是否允许文件系统访问 + allowNotifications: boolean // 是否允许通知 + allowClipboard: boolean // 是否允许剪贴板访问 + allowMedia: boolean // 是否允许摄像头麦克风 + allowGeolocation: boolean // 是否允许地理位置 +} + +/** + * 网络请求记录 + */ +export interface NetworkRequest { + id: string + appId: string + url: string + method: string + timestamp: Date + status?: number + responseSize?: number +} + +/** + * 存储使用情况 + */ +export interface StorageUsage { + appId: string + usedSpace: number // 已使用空间(MB) + maxSpace: number // 最大空间(MB) + lastAccessed: Date +} + +/** + * 资源事件接口 + */ +export interface ResourceEvents extends IEventMap { + onPermissionRequest: (request: PermissionRequest) => void + onPermissionGranted: (appId: string, resourceType: ResourceType) => void + onPermissionDenied: (appId: string, resourceType: ResourceType) => void + onResourceQuotaExceeded: (appId: string, resourceType: ResourceType) => void + onNetworkRequest: (request: NetworkRequest) => void + onStorageChange: (appId: string, usage: StorageUsage) => void +} + +/** + * 资源管理服务类 + */ +export class ResourceService { + private permissions = reactive(new Map>()) + private networkRequests = reactive(new Map()) + private storageUsage = reactive(new Map()) + private defaultConfig: ResourceAccessConfig + private eventBus: IEventBuilder + + constructor(eventBus: IEventBuilder) { + this.eventBus = eventBus + + // 默认资源访问配置 + this.defaultConfig = { + maxStorageSize: 10, // 10MB + allowedDomains: [], + maxNetworkRequests: 60, // 每分钟60次 + allowFileAccess: false, + allowNotifications: false, + allowClipboard: false, + allowMedia: false, + allowGeolocation: false, + } + + this.initializeStorageMonitoring() + } + + /** + * 请求资源权限 + */ + async requestPermission( + appId: string, + resourceType: ResourceType, + description: string, + ): Promise { + const requestId = `${appId}-${resourceType}-${Date.now()}` + + const request: PermissionRequest = { + id: requestId, + appId, + resourceType, + description, + requestedAt: new Date(), + status: PermissionLevel.PROMPT, + } + + // 检查是否已有权限 + const existingPermission = this.getPermission(appId, resourceType) + if (existingPermission) { + if (existingPermission.status === PermissionLevel.GRANTED) { + // 检查权限是否过期 + if (!existingPermission.expiresAt || existingPermission.expiresAt > new Date()) { + return PermissionLevel.GRANTED + } + } else if (existingPermission.status === PermissionLevel.DENIED) { + return PermissionLevel.DENIED + } + } + + // 触发权限请求事件,UI层处理用户确认 + this.eventBus.notifyEvent('onPermissionRequest', request) + + // 根据资源类型的默认策略处理 + return this.handlePermissionRequest(request) + } + + /** + * 授予权限 + */ + grantPermission(appId: string, resourceType: ResourceType, expiresIn?: number): boolean { + try { + const request = this.getPermission(appId, resourceType) + if (!request) return false + + request.status = PermissionLevel.GRANTED + request.approvedAt = new Date() + + if (expiresIn) { + request.expiresAt = new Date(Date.now() + expiresIn) + } + + this.setPermission(appId, resourceType, request) + this.eventBus.notifyEvent('onPermissionGranted', appId, resourceType) + + return true + } catch (error) { + console.error('授予权限失败:', error) + return false + } + } + + /** + * 拒绝权限 + */ + denyPermission(appId: string, resourceType: ResourceType): boolean { + try { + const request = this.getPermission(appId, resourceType) + if (!request) return false + + request.status = PermissionLevel.DENIED + request.deniedAt = new Date() + + this.setPermission(appId, resourceType, request) + this.eventBus.notifyEvent('onPermissionDenied', appId, resourceType) + + return true + } catch (error) { + console.error('拒绝权限失败:', error) + return false + } + } + + /** + * 检查应用是否有指定资源权限 + */ + hasPermission(appId: string, resourceType: ResourceType): boolean { + const permission = this.getPermission(appId, resourceType) + + if (!permission || permission.status !== PermissionLevel.GRANTED) { + return false + } + + // 检查权限是否过期 + if (permission.expiresAt && permission.expiresAt <= new Date()) { + permission.status = PermissionLevel.DENIED + this.setPermission(appId, resourceType, permission) + return false + } + + return true + } + + /** + * 本地存储操作 + */ + async setStorage(appId: string, key: string, value: any): Promise { + if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) { + const permission = await this.requestPermission( + appId, + ResourceType.LOCAL_STORAGE, + '应用需要访问本地存储来保存数据', + ) + if (permission !== PermissionLevel.GRANTED) { + return false + } + } + + try { + const storageKey = `app-${appId}-${key}` + const serializedValue = JSON.stringify(value) + + // 检查存储配额 + const usage = this.getStorageUsage(appId) + const valueSize = new Blob([serializedValue]).size / (1024 * 1024) // MB + + if (usage.usedSpace + valueSize > usage.maxSpace) { + this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.LOCAL_STORAGE) + return false + } + + localStorage.setItem(storageKey, serializedValue) + + // 更新存储使用情况 + this.updateStorageUsage(appId) + + return true + } catch (error) { + console.error('存储数据失败:', error) + return false + } + } + + /** + * 获取本地存储数据 + */ + async getStorage(appId: string, key: string): Promise { + if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) { + return null + } + + try { + const storageKey = `app-${appId}-${key}` + const value = localStorage.getItem(storageKey) + + if (value === null) { + return null + } + + // 更新最后访问时间 + this.updateStorageUsage(appId) + + return JSON.parse(value) + } catch (error) { + console.error('获取存储数据失败:', error) + return null + } + } + + /** + * 删除本地存储数据 + */ + async removeStorage(appId: string, key: string): Promise { + if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) { + return false + } + + try { + const storageKey = `app-${appId}-${key}` + localStorage.removeItem(storageKey) + + // 更新存储使用情况 + this.updateStorageUsage(appId) + + return true + } catch (error) { + console.error('删除存储数据失败:', error) + return false + } + } + + /** + * 清空应用存储 + */ + async clearStorage(appId: string): Promise { + if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) { + return false + } + + try { + const prefix = `app-${appId}-` + const keysToRemove: string[] = [] + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && key.startsWith(prefix)) { + keysToRemove.push(key) + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)) + + // 重置存储使用情况 + this.resetStorageUsage(appId) + + return true + } catch (error) { + console.error('清空存储失败:', error) + return false + } + } + + /** + * 网络请求 + */ + async makeNetworkRequest( + appId: string, + url: string, + options: RequestInit = {}, + ): Promise { + if (!this.hasPermission(appId, ResourceType.NETWORK)) { + const permission = await this.requestPermission( + appId, + ResourceType.NETWORK, + `应用需要访问网络来请求数据: ${url}`, + ) + if (permission !== PermissionLevel.GRANTED) { + return null + } + } + + // 检查域名白名单 + try { + const urlObj = new URL(url) + const config = this.getAppResourceConfig(appId) + + if ( + config.allowedDomains.length > 0 && + !config.allowedDomains.some((domain) => urlObj.hostname.endsWith(domain)) + ) { + console.warn(`域名 ${urlObj.hostname} 不在白名单中`) + return null + } + + // 检查请求频率限制 + if (!this.checkNetworkRateLimit(appId)) { + this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.NETWORK) + return null + } + + // 记录网络请求 + const requestRecord: NetworkRequest = { + id: `${appId}-${Date.now()}`, + appId, + url, + method: options.method || 'GET', + timestamp: new Date(), + } + + const response = await fetch(url, options) + + // 更新请求记录 + requestRecord.status = response.status + requestRecord.responseSize = parseInt(response.headers.get('content-length') || '0') + + this.recordNetworkRequest(requestRecord) + this.eventBus.notifyEvent('onNetworkRequest', requestRecord) + + return response + } catch (error) { + console.error('网络请求失败:', error) + return null + } + } + + /** + * 显示通知 + */ + async showNotification( + appId: string, + title: string, + options?: NotificationOptions, + ): Promise { + if (!this.hasPermission(appId, ResourceType.NOTIFICATION)) { + const permission = await this.requestPermission( + appId, + ResourceType.NOTIFICATION, + '应用需要显示通知来提醒您重要信息', + ) + if (permission !== PermissionLevel.GRANTED) { + return false + } + } + + try { + if ('Notification' in window) { + // 请求浏览器通知权限 + if (Notification.permission === 'default') { + await Notification.requestPermission() + } + + if (Notification.permission === 'granted') { + new Notification(`[${appId}] ${title}`, options) + return true + } + } + return false + } catch (error) { + console.error('显示通知失败:', error) + return false + } + } + + /** + * 访问剪贴板 + */ + async getClipboard(appId: string): Promise { + if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) { + const permission = await this.requestPermission( + appId, + ResourceType.CLIPBOARD, + '应用需要访问剪贴板来读取您复制的内容', + ) + if (permission !== PermissionLevel.GRANTED) { + return null + } + } + + try { + if (navigator.clipboard && navigator.clipboard.readText) { + return await navigator.clipboard.readText() + } + return null + } catch (error) { + console.error('读取剪贴板失败:', error) + return null + } + } + + /** + * 写入剪贴板 + */ + async setClipboard(appId: string, text: string): Promise { + if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) { + const permission = await this.requestPermission( + appId, + ResourceType.CLIPBOARD, + '应用需要访问剪贴板来复制内容', + ) + if (permission !== PermissionLevel.GRANTED) { + return false + } + } + + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text) + return true + } + return false + } catch (error) { + console.error('写入剪贴板失败:', error) + return false + } + } + + /** + * 获取应用权限列表 + */ + getAppPermissions(appId: string): PermissionRequest[] { + const appPermissions = this.permissions.get(appId) + return appPermissions ? Array.from(appPermissions.values()) : [] + } + + /** + * 获取所有网络请求记录 + */ + getNetworkRequests(appId: string): NetworkRequest[] { + return this.networkRequests.get(appId) || [] + } + + /** + * 获取存储使用情况 + */ + getStorageUsage(appId: string): StorageUsage { + let usage = this.storageUsage.get(appId) + + if (!usage) { + usage = { + appId, + usedSpace: 0, + maxSpace: this.defaultConfig.maxStorageSize, + lastAccessed: new Date(), + } + this.storageUsage.set(appId, usage) + } + + return usage + } + + /** + * 获取应用资源配置 + */ + getAppResourceConfig(appId: string): ResourceAccessConfig { + // 这里可以从数据库或配置文件加载应用特定配置 + // 目前返回默认配置 + return { ...this.defaultConfig } + } + + /** + * 撤销应用所有权限 + */ + revokeAllPermissions(appId: string): boolean { + try { + this.permissions.delete(appId) + this.networkRequests.delete(appId) + this.clearStorage(appId) + return true + } catch (error) { + console.error('撤销权限失败:', error) + return false + } + } + + // 私有方法 + + /** + * 处理权限请求 + */ + private async handlePermissionRequest(request: PermissionRequest): Promise { + // 对于本地存储,默认授权 + if (request.resourceType === ResourceType.LOCAL_STORAGE) { + this.grantPermission(request.appId, request.resourceType) + return PermissionLevel.GRANTED + } + + // 其他资源需要用户确认,这里模拟用户同意 + // 实际实现中,这里应该显示权限确认对话框 + return new Promise((resolve) => { + setTimeout(() => { + // 模拟用户操作 + const userResponse = Math.random() > 0.3 // 70%的概率同意 + + if (userResponse) { + this.grantPermission(request.appId, request.resourceType, 24 * 60 * 60 * 1000) // 24小时有效 + resolve(PermissionLevel.GRANTED) + } else { + this.denyPermission(request.appId, request.resourceType) + resolve(PermissionLevel.DENIED) + } + }, 1000) + }) + } + + /** + * 获取权限记录 + */ + private getPermission(appId: string, resourceType: ResourceType): PermissionRequest | undefined { + const appPermissions = this.permissions.get(appId) + return appPermissions?.get(resourceType) + } + + /** + * 设置权限记录 + */ + private setPermission( + appId: string, + resourceType: ResourceType, + request: PermissionRequest, + ): void { + if (!this.permissions.has(appId)) { + this.permissions.set(appId, new Map()) + } + this.permissions.get(appId)!.set(resourceType, request) + } + + /** + * 检查网络请求频率限制 + */ + private checkNetworkRateLimit(appId: string): boolean { + const requests = this.networkRequests.get(appId) || [] + const now = new Date() + const oneMinuteAgo = new Date(now.getTime() - 60 * 1000) + + const recentRequests = requests.filter((req) => req.timestamp > oneMinuteAgo) + const config = this.getAppResourceConfig(appId) + + return recentRequests.length < config.maxNetworkRequests + } + + /** + * 记录网络请求 + */ + private recordNetworkRequest(request: NetworkRequest): void { + if (!this.networkRequests.has(request.appId)) { + this.networkRequests.set(request.appId, []) + } + + const requests = this.networkRequests.get(request.appId)! + requests.push(request) + + // 保留最近1000条记录 + if (requests.length > 1000) { + requests.splice(0, requests.length - 1000) + } + } + + /** + * 更新存储使用情况 + */ + private updateStorageUsage(appId: string): void { + const usage = this.getStorageUsage(appId) + usage.lastAccessed = new Date() + + // 计算实际使用空间 + let usedSpace = 0 + const prefix = `app-${appId}-` + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && key.startsWith(prefix)) { + const value = localStorage.getItem(key) + if (value) { + usedSpace += new Blob([value]).size + } + } + } + + usage.usedSpace = usedSpace / (1024 * 1024) // 转换为MB + + this.eventBus.notifyEvent('onStorageChange', appId, usage) + } + + /** + * 重置存储使用情况 + */ + private resetStorageUsage(appId: string): void { + const usage = this.getStorageUsage(appId) + usage.usedSpace = 0 + usage.lastAccessed = new Date() + + this.eventBus.notifyEvent('onStorageChange', appId, usage) + } + + /** + * 初始化存储监控 + */ + private initializeStorageMonitoring(): void { + // 监听存储变化事件 + window.addEventListener('storage', (e) => { + if (e.key && e.key.startsWith('app-')) { + const parts = e.key.split('-') + if (parts.length >= 2) { + const appId = parts[1] + this.updateStorageUsage(appId) + } + } + }) + } +} diff --git a/src/services/SystemServiceIntegration.ts b/src/services/SystemServiceIntegration.ts new file mode 100644 index 0000000..1d4d738 --- /dev/null +++ b/src/services/SystemServiceIntegration.ts @@ -0,0 +1,873 @@ +import { reactive, ref } from 'vue' +import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl' +import type { IEventBuilder } from '@/events/IEventBuilder' + +// 导入所有服务 +import { WindowService } from './WindowService' +import { ResourceService } from './ResourceService' +import { EventCommunicationService } from './EventCommunicationService' +import { ApplicationSandboxEngine } from './ApplicationSandboxEngine' +import { ApplicationLifecycleManager } from './ApplicationLifecycleManager' +import { externalAppDiscovery } from './ExternalAppDiscovery' + +/** + * 系统服务配置接口 + */ +export interface SystemServiceConfig { + debug?: boolean + maxMemoryUsage?: number + maxCpuUsage?: number + enablePerformanceMonitoring?: boolean + enableSecurityAudit?: boolean + autoCleanup?: boolean + cleanupInterval?: number +} + +/** + * 系统状态接口 + */ +export interface SystemStatus { + initialized: boolean + running: boolean + servicesStatus: { + windowService: boolean + resourceService: boolean + eventService: boolean + sandboxEngine: boolean + lifecycleManager: boolean + } + performance: { + memoryUsage: number + cpuUsage: number + activeApps: number + activeWindows: number + } + uptime: number + lastError?: string +} + +/** + * SDK调用接口 + */ +export interface SDKCall { + requestId: string + method: string + data?: any + appId: string +} + +/** + * 系统服务集成层 + * 统一管理所有核心服务,提供统一的对外接口 + */ +export class SystemServiceIntegration { + private initialized = ref(false) + private running = ref(false) + private config: SystemServiceConfig + private startTime: Date + + // 核心服务实例 + private eventBus: IEventBuilder + private windowService!: WindowService + private resourceService!: ResourceService + private eventService!: EventCommunicationService + private sandboxEngine!: ApplicationSandboxEngine + private lifecycleManager!: ApplicationLifecycleManager + + // 系统状态 + private systemStatus = reactive({ + initialized: false, + running: false, + servicesStatus: { + windowService: false, + resourceService: false, + eventService: false, + sandboxEngine: false, + lifecycleManager: false, + }, + performance: { + memoryUsage: 0, + cpuUsage: 0, + activeApps: 0, + activeWindows: 0, + }, + uptime: 0, + }) + + // 性能监控 + private cleanupInterval: number | null = null + private performanceInterval: number | null = null + + constructor(config: SystemServiceConfig = {}) { + this.config = { + debug: false, + maxMemoryUsage: 1024, // 1GB + maxCpuUsage: 80, // 80% + enablePerformanceMonitoring: true, + enableSecurityAudit: true, + autoCleanup: true, + cleanupInterval: 5 * 60 * 1000, // 5分钟 + ...config, + } + + this.startTime = new Date() + this.eventBus = new EventBuilderImpl() + + this.setupGlobalErrorHandling() + } + + /** + * 初始化系统服务 + */ + async initialize(): Promise { + if (this.initialized.value) { + throw new Error('系统服务已初始化') + } + + try { + console.log('开始初始化系统服务...') + + // 按依赖顺序初始化服务 + await this.initializeServices() + + // 设置服务间通信 + this.setupServiceCommunication() + + // 设置SDK消息处理 + this.setupSDKMessageHandling() + + // 启动性能监控 + if (this.config.enablePerformanceMonitoring) { + this.startPerformanceMonitoring() + } + + // 启动自动清理 + if (this.config.autoCleanup) { + this.startAutoCleanup() + } + + // 启动外置应用发现服务 + // 注意:外置应用发现服务统一由 SystemServiceIntegration 管理, + // ApplicationLifecycleManager 只负责使用已发现的应用,避免重复启动 + console.log('启动外置应用发现服务...') + await externalAppDiscovery.startDiscovery() + + this.initialized.value = true + this.running.value = true + this.systemStatus.initialized = true + this.systemStatus.running = true + + console.log('系统服务初始化完成') + + // 发送系统就绪事件 + this.eventService.sendMessage('system', 'system-ready', { + timestamp: new Date(), + services: Object.keys(this.systemStatus.servicesStatus), + }) + } catch (error) { + console.error('系统服务初始化失败:', error) + this.systemStatus.lastError = error instanceof Error ? error.message : String(error) + throw error + } + } + + /** + * 获取系统状态 + */ + getSystemStatus(): SystemStatus { + this.updateSystemStatus() + return { ...this.systemStatus } + } + + /** + * 获取窗体服务 + */ + getWindowService(): WindowService { + this.checkInitialized() + return this.windowService + } + + /** + * 获取资源服务 + */ + getResourceService(): ResourceService { + this.checkInitialized() + return this.resourceService + } + + /** + * 获取事件服务 + */ + getEventService(): EventCommunicationService { + this.checkInitialized() + return this.eventService + } + + /** + * 获取沙箱引擎 + */ + getSandboxEngine(): ApplicationSandboxEngine { + this.checkInitialized() + return this.sandboxEngine + } + + /** + * 获取生命周期管理器 + */ + getLifecycleManager(): ApplicationLifecycleManager { + this.checkInitialized() + return this.lifecycleManager + } + + /** + * 处理SDK调用 + */ + async handleSDKCall(call: SDKCall): Promise { + this.checkInitialized() + + const { requestId, method, data, appId } = call + + try { + this.debugLog(`处理SDK调用: ${method}`, { appId, data }) + + const result = await this.executeSDKMethod(method, data, appId) + + return { + success: true, + data: result, + requestId, + } + } catch (error) { + console.error('SDK调用失败:', error) + + return { + success: false, + error: error instanceof Error ? error.message : String(error), + requestId, + } + } + } + + /** + * 重启系统服务 + */ + async restart(): Promise { + console.log('重启系统服务...') + + await this.shutdown() + await new Promise((resolve) => setTimeout(resolve, 1000)) + await this.initialize() + + console.log('系统服务重启完成') + } + + /** + * 关闭系统服务 + */ + async shutdown(): Promise { + console.log('关闭系统服务...') + + this.running.value = false + this.systemStatus.running = false + + // 停止定时器 + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval) + this.cleanupInterval = null + } + + if (this.performanceInterval) { + clearInterval(this.performanceInterval) + this.performanceInterval = null + } + + // 停止外置应用发现服务(由 SystemServiceIntegration 统一管理) + externalAppDiscovery.stopDiscovery() + + // 按相反顺序关闭服务 + try { + if (this.lifecycleManager) { + // 停止所有运行中的应用 + const runningApps = this.lifecycleManager.getRunningApps() + for (const app of runningApps) { + await this.lifecycleManager.stopApp(app.id) + } + } + + if (this.sandboxEngine) { + this.sandboxEngine.destroy() + } + + if (this.eventService) { + this.eventService.destroy() + } + + if (this.windowService) { + // 关闭所有窗体 + const windows = this.windowService.getAllWindows() + for (const window of windows) { + await this.windowService.destroyWindow(window.id) + } + } + } catch (error) { + console.error('关闭服务时发生错误:', error) + } + + this.initialized.value = false + this.systemStatus.initialized = false + + console.log('系统服务已关闭') + } + + // 私有方法 + + /** + * 初始化所有服务 + */ + private async initializeServices(): Promise { + // 1. 初始化资源服务 + console.log('初始化资源服务...') + this.resourceService = new ResourceService(this.eventBus) + this.systemStatus.servicesStatus.resourceService = true + + // 2. 初始化事件通信服务 + console.log('初始化事件通信服务...') + this.eventService = new EventCommunicationService(this.eventBus) + this.systemStatus.servicesStatus.eventService = true + + // 3. 初始化窗体服务 + console.log('初始化窗体服务...') + this.windowService = new WindowService(this.eventBus) + this.systemStatus.servicesStatus.windowService = true + + // 4. 初始化沙箱引擎 + console.log('初始化沙箱引擎...') + this.sandboxEngine = new ApplicationSandboxEngine(this.resourceService, this.eventService) + this.systemStatus.servicesStatus.sandboxEngine = true + + // 5. 初始化生命周期管理器 + console.log('初始化生命周期管理器...') + this.lifecycleManager = new ApplicationLifecycleManager( + this.windowService, + this.resourceService, + this.eventService, + this.sandboxEngine, + ) + this.systemStatus.servicesStatus.lifecycleManager = true + } + + /** + * 设置服务间通信 + */ + private setupServiceCommunication(): void { + // 监听应用生命周期事件 + this.eventService.subscribe('system', 'app-lifecycle', (message) => { + this.debugLog('[AppLifecycle] 应用生命周期事件:', message.payload) + }) + + // 监听窗口状态变化事件 + this.eventService.subscribe('system', 'window-state-change', (message) => { + this.debugLog('[WindowState] 窗口状态变化消息已处理:', message.payload) + }) + + // 监听窗体状态变化(来自 WindowService 的 onStateChange 事件) + this.eventBus.addEventListener( + 'onStateChange', + (windowId: string, newState: string, oldState: string) => { + console.log( + `[SystemIntegration] 接收到窗体状态变化事件: ${windowId} ${oldState} -> ${newState}`, + ) + this.eventService.sendMessage('system', 'window-state-change', { + windowId, + newState, + oldState, + }) + console.log(`[SystemIntegration] 已发送 window-state-change 消息到事件通信服务`) + }, + ) + + // 监听窗体关闭事件,自动停止对应的应用 + this.eventBus.addEventListener('onClose', async (windowId: string) => { + console.log(`[SystemIntegration] 接收到窗体关闭事件: ${windowId}`) + // 查找对应的应用 + const runningApps = this.lifecycleManager.getRunningApps() + for (const app of runningApps) { + if (app.windowId === windowId) { + try { + console.log(`窗口关闭,自动停止应用: ${app.id}`) + await this.lifecycleManager.stopApp(app.id) + } catch (error) { + console.error(`停止应用 ${app.id} 失败:`, error) + } + break + } + } + }) + + // 监听资源配额超出 + this.eventBus.addEventListener( + 'onResourceQuotaExceeded', + (appId: string, resourceType: string) => { + console.log(`[SystemIntegration] 接收到资源配额超出事件: ${appId} - ${resourceType}`) + this.eventService.sendMessage('system', 'resource-quota-exceeded', { + appId, + resourceType, + }) + }, + ) + } + + /** + * 设置SDK消息处理 + */ + private setupSDKMessageHandling(): void { + // 监听来自iframe的SDK调用 + window.addEventListener('message', async (event) => { + const data = event.data + if (!data) return + + // 处理安全存储消息 + if (data.type?.startsWith('sdk:storage:')) { + await this.handleStorageMessage(event) + return + } + + // 处理其他SDK调用 + if (data.type === 'sdk:call') { + const call: SDKCall = data + const result = await this.handleSDKCall(call) + + // 发送响应回iframe + const iframe = this.findIframeBySource(event.source as Window) + if (iframe) { + iframe.contentWindow?.postMessage( + { + type: 'system:response', + ...result, + }, + '*', + ) + } + } + }) + } + + /** + * 处理安全存储消息 + */ + private async handleStorageMessage(event: MessageEvent): Promise { + const { type, requestId, appId, key, value } = event.data + + if (!requestId || !appId) { + console.warn('存储消息缺少必需参数') + return + } + + // 验证应用权限 + const app = this.lifecycleManager.getApp(appId) + if (!app) { + console.warn(`未找到应用: ${appId}`) + return + } + + let result: any = null + let success = false + + try { + switch (type) { + case 'sdk:storage:get': + result = await this.resourceService.getStorage(appId, key) + success = true + break + + case 'sdk:storage:set': + result = await this.resourceService.setStorage(appId, key, value) + success = result === true + break + + case 'sdk:storage:remove': + result = await this.resourceService.removeStorage(appId, key) + success = result === true + break + + default: + console.warn(`未知的存储操作: ${type}`) + return + } + } catch (error) { + console.error('存储操作失败:', error) + success = false + } + + // 发送响应回iframe + const iframe = this.findIframeBySource(event.source as Window) + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage( + { + type: 'system:storage-response', + requestId, + result, + success, + }, + '*', + ) + } + } + + /** + * 执行SDK方法 + */ + private async executeSDKMethod(method: string, data: any, appId: string): Promise { + const [service, action] = method.split('.') + + switch (service) { + case 'window': + return this.executeWindowMethod(action, data, appId) + + case 'storage': + return this.executeStorageMethod(action, data, appId) + + case 'network': + return this.executeNetworkMethod(action, data, appId) + + case 'events': + return this.executeEventMethod(action, data, appId) + + case 'ui': + return this.executeUIMethod(action, data, appId) + + case 'system': + return this.executeSystemMethod(action, data, appId) + + case 'sdk': + return this.executeSDKMethod(action, data, appId) + + default: + throw new Error(`未知的服务: ${service}`) + } + } + + /** + * 执行窗体相关方法 + */ + private async executeWindowMethod(action: string, data: any, appId: string): Promise { + // 查找应用的窗体 + const app = this.lifecycleManager.getApp(appId) + if (!app?.windowId) { + throw new Error('应用窗体不存在') + } + + const windowId = app.windowId + + switch (action) { + case 'setTitle': + return this.windowService.setWindowTitle(windowId, data.title) + + case 'resize': + return this.windowService.setWindowSize(windowId, data.width, data.height) + + case 'move': + // 需要实现窗体移动功能 + return true + + case 'minimize': + return this.windowService.minimizeWindow(windowId) + + case 'maximize': + return this.windowService.maximizeWindow(windowId) + + case 'restore': + return this.windowService.restoreWindow(windowId) + + case 'close': + return this.lifecycleManager.stopApp(appId) + + case 'getState': + const window = this.windowService.getWindow(windowId) + return window?.state + + case 'getSize': + const windowInfo = this.windowService.getWindow(windowId) + return { + width: windowInfo?.config.width, + height: windowInfo?.config.height, + } + + default: + throw new Error(`未知的窗体操作: ${action}`) + } + } + + /** + * 执行存储相关方法 + */ + private async executeStorageMethod(action: string, data: any, appId: string): Promise { + switch (action) { + case 'set': + return this.resourceService.setStorage(appId, data.key, data.value) + + case 'get': + return this.resourceService.getStorage(appId, data.key) + + case 'remove': + return this.resourceService.removeStorage(appId, data.key) + + case 'clear': + return this.resourceService.clearStorage(appId) + + case 'keys': + // 需要实现获取所有键的功能 + return [] + + case 'has': + const value = await this.resourceService.getStorage(appId, data.key) + return value !== null + + case 'getStats': + return this.resourceService.getStorageUsage(appId) + + default: + throw new Error(`未知的存储操作: ${action}`) + } + } + + /** + * 执行网络相关方法 + */ + private async executeNetworkMethod(action: string, data: any, appId: string): Promise { + switch (action) { + case 'request': + const response = await this.resourceService.makeNetworkRequest(appId, data.url, data.config) + return response + ? { + data: await response.text(), + status: response.status, + statusText: response.statusText, + headers: {} as Record, // 简化headers处理 + url: response.url, + } + : null + + case 'isOnline': + return navigator.onLine + + case 'getStats': + const requests = this.resourceService.getNetworkRequests(appId) + return { + requestCount: requests.length, + failureCount: requests.filter((r) => r.status && r.status >= 400).length, + averageTime: 0, // 需要实现时间统计 + } + + default: + throw new Error(`未知的网络操作: ${action}`) + } + } + + /** + * 执行事件相关方法 + */ + private async executeEventMethod(action: string, data: any, appId: string): Promise { + switch (action) { + case 'emit': + return this.eventService.sendMessage(appId, data.channel, data.data) + + case 'on': + return this.eventService.subscribe(appId, data.channel, (message) => { + // 发送事件到应用 + const app = this.lifecycleManager.getApp(appId) + if (app?.sandboxId) { + this.sandboxEngine.sendMessage(app.sandboxId, { + type: 'system:event', + subscriptionId: data.subscriptionId, + message, + }) + } + }) + + case 'off': + return this.eventService.unsubscribe(data.subscriptionId) + + case 'broadcast': + return this.eventService.broadcast(appId, data.channel, data.data) + + case 'sendTo': + return this.eventService.sendCrossAppMessage(appId, data.targetAppId, data.data) + + default: + throw new Error(`未知的事件操作: ${action}`) + } + } + + /** + * 执行UI相关方法 + */ + private async executeUIMethod(action: string, data: any, appId: string): Promise { + switch (action) { + case 'showNotification': + return this.resourceService.showNotification(appId, data.title, data) + + case 'showToast': + // 需要实现Toast功能 + console.log(`[Toast] ${data.message}`) + return 'toast-' + Date.now() + + default: + throw new Error(`未知的UI操作: ${action}`) + } + } + + /** + * 执行系统相关方法 + */ + private async executeSystemMethod(action: string, data: any, appId: string): Promise { + switch (action) { + case 'getSystemInfo': + return { + platform: navigator.platform, + userAgent: navigator.userAgent, + language: navigator.language, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + screenResolution: { + width: screen.width, + height: screen.height, + }, + colorDepth: screen.colorDepth, + pixelRatio: window.devicePixelRatio, + } + + case 'getAppInfo': + const app = this.lifecycleManager.getApp(appId) + return app + ? { + id: app.id, + name: app.manifest.name, + version: app.version, + permissions: app.manifest.permissions, + createdAt: app.installedAt, + lastActiveAt: app.lastActiveAt, + } + : null + + case 'getClipboard': + return this.resourceService.getClipboard(appId) + + case 'setClipboard': + return this.resourceService.setClipboard(appId, data.text) + + case 'getCurrentTime': + return new Date() + + case 'generateUUID': + return crypto.randomUUID() + + default: + throw new Error(`未知的系统操作: ${action}`) + } + } + + /** + * 查找消息来源的iframe + */ + private findIframeBySource(source: Window): HTMLIFrameElement | null { + const iframes = Array.from(document.querySelectorAll('iframe')) + + for (const iframe of iframes) { + if (iframe.contentWindow === source) { + return iframe + } + } + + return null + } + + /** + * 开始性能监控 + */ + private startPerformanceMonitoring(): void { + this.performanceInterval = setInterval(() => { + this.updateSystemStatus() + + // 检查性能阈值 + if (this.systemStatus.performance.memoryUsage > this.config.maxMemoryUsage!) { + this.eventService.sendMessage('system', 'performance-alert', { + type: 'memory', + usage: this.systemStatus.performance.memoryUsage, + limit: this.config.maxMemoryUsage, + }) + } + + if (this.systemStatus.performance.cpuUsage > this.config.maxCpuUsage!) { + this.eventService.sendMessage('system', 'performance-alert', { + type: 'cpu', + usage: this.systemStatus.performance.cpuUsage, + limit: this.config.maxCpuUsage, + }) + } + }, 10000) // 每10秒检查一次 + } + + /** + * 开始自动清理 + */ + private startAutoCleanup(): void { + this.cleanupInterval = setInterval(() => { + this.debugLog('执行自动清理...') + + // 清理事件服务 + this.eventService.cleanup() + + // 清理沙箱引擎缓存 + // this.sandboxEngine.cleanup() + + this.debugLog('自动清理完成') + }, this.config.cleanupInterval!) + } + + /** + * 更新系统状态 + */ + private updateSystemStatus(): void { + this.systemStatus.uptime = Date.now() - this.startTime.getTime() + this.systemStatus.performance.activeApps = this.lifecycleManager?.getRunningApps().length || 0 + this.systemStatus.performance.activeWindows = this.windowService?.getAllWindows().length || 0 + + // 简化的内存和CPU使用率计算 + this.systemStatus.performance.memoryUsage = + (performance as any).memory?.usedJSHeapSize / 1024 / 1024 || 0 + this.systemStatus.performance.cpuUsage = Math.random() * 20 // 模拟CPU使用率 + } + + /** + * 检查是否已初始化 + */ + private checkInitialized(): void { + if (!this.initialized.value) { + throw new Error('系统服务未初始化') + } + } + + /** + * 设置全局错误处理 + */ + private setupGlobalErrorHandling(): void { + window.addEventListener('error', (event) => { + console.error('全局错误:', event.error) + this.systemStatus.lastError = event.error?.message || '未知错误' + }) + + window.addEventListener('unhandledrejection', (event) => { + console.error('未处理的Promise拒绝:', event.reason) + this.systemStatus.lastError = event.reason?.message || '未处理的Promise拒绝' + }) + } + + /** + * 调试日志 + */ + private debugLog(message: string, data?: any): void { + if (this.config.debug) { + console.log(`[SystemService] ${message}`, data) + } + } +} diff --git a/src/services/WindowService.ts b/src/services/WindowService.ts new file mode 100644 index 0000000..509fe6f --- /dev/null +++ b/src/services/WindowService.ts @@ -0,0 +1,651 @@ +import { ref, reactive } from 'vue' +import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder' +import { v4 as uuidv4 } from 'uuid' + +/** + * 窗体状态枚举 + */ +export enum WindowState { + CREATING = 'creating', + LOADING = 'loading', + ACTIVE = 'active', + MINIMIZED = 'minimized', + MAXIMIZED = 'maximized', + CLOSING = 'closing', + DESTROYED = 'destroyed', + ERROR = 'error', +} + +/** + * 窗体配置接口 + */ +export interface WindowConfig { + title: string + width: number + height: number + minWidth?: number + minHeight?: number + maxWidth?: number + maxHeight?: number + resizable?: boolean + movable?: boolean + closable?: boolean + minimizable?: boolean + maximizable?: boolean + modal?: boolean + alwaysOnTop?: boolean + x?: number + y?: number +} + +/** + * 窗体实例接口 + */ +export interface WindowInstance { + id: string + appId: string + config: WindowConfig + state: WindowState + element?: HTMLElement + iframe?: HTMLIFrameElement + zIndex: number + createdAt: Date + updatedAt: Date +} + +/** + * 窗体事件接口 + */ +export interface WindowEvents extends IEventMap { + onStateChange: (windowId: string, newState: WindowState, oldState: WindowState) => void + onResize: (windowId: string, width: number, height: number) => void + onMove: (windowId: string, x: number, y: number) => void + onFocus: (windowId: string) => void + onBlur: (windowId: string) => void + onClose: (windowId: string) => void +} + +/** + * 窗体管理服务类 + */ +export class WindowService { + private windows = reactive(new Map()) + private activeWindowId = ref(null) + private nextZIndex = 1000 + private eventBus: IEventBuilder + + constructor(eventBus: IEventBuilder) { + this.eventBus = eventBus + } + + /** + * 创建新窗体 + */ + async createWindow(appId: string, config: WindowConfig): Promise { + const windowId = uuidv4() + const now = new Date() + + const windowInstance: WindowInstance = { + id: windowId, + appId, + config, + state: WindowState.CREATING, + zIndex: this.nextZIndex++, + createdAt: now, + updatedAt: now, + } + + this.windows.set(windowId, windowInstance) + + try { + // 创建窗体DOM元素 + await this.createWindowElement(windowInstance) + + // 更新状态为加载中 + this.updateWindowState(windowId, WindowState.LOADING) + + // 模拟应用加载过程 + await this.loadApplication(windowInstance) + + // 激活窗体 + this.updateWindowState(windowId, WindowState.ACTIVE) + this.setActiveWindow(windowId) + + return windowInstance + } catch (error) { + this.updateWindowState(windowId, WindowState.ERROR) + throw error + } + } + + /** + * 销毁窗体 + */ + async destroyWindow(windowId: string): Promise { + const window = this.windows.get(windowId) + if (!window) return false + + try { + this.updateWindowState(windowId, WindowState.CLOSING) + + // 清理DOM元素 + if (window.element) { + window.element.remove() + } + + // 从集合中移除 + this.windows.delete(windowId) + + // 更新活跃窗体 + if (this.activeWindowId.value === windowId) { + this.activeWindowId.value = null + // 激活最后一个窗体 + const lastWindow = Array.from(this.windows.values()).pop() + if (lastWindow) { + this.setActiveWindow(lastWindow.id) + } + } + + this.eventBus.notifyEvent('onClose', windowId) + return true + } catch (error) { + console.error('销毁窗体失败:', error) + return false + } + } + + /** + * 最小化窗体 + */ + minimizeWindow(windowId: string): boolean { + const window = this.windows.get(windowId) + if (!window || window.state === WindowState.MINIMIZED) return false + + this.updateWindowState(windowId, WindowState.MINIMIZED) + + if (window.element) { + window.element.style.display = 'none' + } + + return true + } + + /** + * 最大化窗体 + */ + maximizeWindow(windowId: string): boolean { + const window = this.windows.get(windowId) + if (!window || window.state === WindowState.MAXIMIZED) return false + + const oldState = window.state + this.updateWindowState(windowId, WindowState.MAXIMIZED) + + if (window.element) { + // 保存原始尺寸和位置 + window.element.dataset.originalWidth = window.config.width.toString() + window.element.dataset.originalHeight = window.config.height.toString() + window.element.dataset.originalX = (window.config.x || 0).toString() + window.element.dataset.originalY = (window.config.y || 0).toString() + + // 设置最大化样式 + Object.assign(window.element.style, { + position: 'fixed', + top: '0', + left: '0', + width: '100vw', + height: 'calc(100vh - 40px)', // 减去任务栏高度 + display: 'block', + }) + } + + this.setActiveWindow(windowId) + return true + } + + /** + * 还原窗体 + */ + restoreWindow(windowId: string): boolean { + const window = this.windows.get(windowId) + if (!window) return false + + const targetState = + window.state === WindowState.MINIMIZED + ? WindowState.ACTIVE + : window.state === WindowState.MAXIMIZED + ? WindowState.ACTIVE + : window.state + + this.updateWindowState(windowId, targetState) + + if (window.element) { + if (window.state === WindowState.MINIMIZED) { + window.element.style.display = 'block' + } else if (window.state === WindowState.MAXIMIZED) { + // 恢复原始尺寸和位置 + const originalWidth = window.element.dataset.originalWidth + const originalHeight = window.element.dataset.originalHeight + const originalX = window.element.dataset.originalX + const originalY = window.element.dataset.originalY + + Object.assign(window.element.style, { + width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`, + height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`, + left: originalX ? `${originalX}px` : '50%', + top: originalY ? `${originalY}px` : '50%', + transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)', + }) + } + } + + this.setActiveWindow(windowId) + return true + } + + /** + * 设置窗体标题 + */ + setWindowTitle(windowId: string, title: string): boolean { + const window = this.windows.get(windowId) + if (!window) return false + + window.config.title = title + window.updatedAt = new Date() + + // 更新DOM元素标题 + if (window.element) { + const titleElement = window.element.querySelector('.window-title') + if (titleElement) { + titleElement.textContent = title + } + } + + return true + } + + /** + * 设置窗体尺寸 + */ + setWindowSize(windowId: string, width: number, height: number): boolean { + const window = this.windows.get(windowId) + if (!window) return false + + // 检查尺寸限制 + const finalWidth = Math.max( + window.config.minWidth || 200, + Math.min(window.config.maxWidth || Infinity, width), + ) + const finalHeight = Math.max( + window.config.minHeight || 150, + Math.min(window.config.maxHeight || Infinity, height), + ) + + window.config.width = finalWidth + window.config.height = finalHeight + window.updatedAt = new Date() + + if (window.element) { + window.element.style.width = `${finalWidth}px` + window.element.style.height = `${finalHeight}px` + } + + this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight) + return true + } + + /** + * 获取窗体实例 + */ + getWindow(windowId: string): WindowInstance | undefined { + return this.windows.get(windowId) + } + + /** + * 获取所有窗体 + */ + getAllWindows(): WindowInstance[] { + return Array.from(this.windows.values()) + } + + /** + * 获取活跃窗体ID + */ + getActiveWindowId(): string | null { + return this.activeWindowId.value + } + + /** + * 设置活跃窗体 + */ + setActiveWindow(windowId: string): boolean { + const window = this.windows.get(windowId) + if (!window) return false + + this.activeWindowId.value = windowId + window.zIndex = this.nextZIndex++ + + if (window.element) { + window.element.style.zIndex = window.zIndex.toString() + } + + this.eventBus.notifyEvent('onFocus', windowId) + return true + } + + /** + * 创建窗体DOM元素 + */ + private async createWindowElement(windowInstance: WindowInstance): Promise { + const { id, config, appId } = windowInstance + + // 检查是否为内置应用 + let isBuiltInApp = false + try { + const { AppRegistry } = await import('../apps/AppRegistry') + const appRegistry = AppRegistry.getInstance() + isBuiltInApp = appRegistry.hasApp(appId) + } catch (error) { + console.warn('无法导入 AppRegistry') + } + + // 创建窗体容器 + const windowElement = document.createElement('div') + windowElement.className = 'system-window' + windowElement.id = `window-${id}` + + // 设置基本样式 + Object.assign(windowElement.style, { + position: 'fixed', + width: `${config.width}px`, + height: `${config.height}px`, + left: config.x ? `${config.x}px` : '50%', + top: config.y ? `${config.y}px` : '50%', + transform: config.x && config.y ? 'none' : 'translate(-50%, -50%)', + zIndex: windowInstance.zIndex.toString(), + backgroundColor: '#fff', + border: '1px solid #ccc', + borderRadius: '8px', + boxShadow: '0 4px 20px rgba(0,0,0,0.15)', + overflow: 'hidden', + }) + + // 创建窗体标题栏 + const titleBar = this.createTitleBar(windowInstance) + windowElement.appendChild(titleBar) + + // 创建窗体内容区域 + const contentArea = document.createElement('div') + contentArea.className = 'window-content' + contentArea.style.cssText = ` + width: 100%; + height: calc(100% - 40px); + overflow: hidden; + ` + + if (isBuiltInApp) { + // 内置应用:创建普通 div 容器,AppRenderer 组件会在这里渲染内容 + const appContainer = document.createElement('div') + appContainer.className = 'built-in-app-container' + appContainer.id = `app-container-${appId}` + appContainer.style.cssText = ` + width: 100%; + height: 100%; + background: #fff; + ` + contentArea.appendChild(appContainer) + + console.log(`[WindowService] 为内置应用 ${appId} 创建了普通容器`) + } else { + // 外部应用:创建 iframe 容器 + const iframe = document.createElement('iframe') + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + background: #fff; + ` + iframe.sandbox = 'allow-scripts allow-forms allow-popups' // 移除allow-same-origin以提高安全性 + contentArea.appendChild(iframe) + + // 保存 iframe 引用(仅对外部应用) + windowInstance.iframe = iframe + + console.log(`[WindowService] 为外部应用 ${appId} 创建了 iframe 容器`) + } + + windowElement.appendChild(contentArea) + + // 添加到页面 + document.body.appendChild(windowElement) + + // 保存引用 + windowInstance.element = windowElement + } + + /** + * 创建窗体标题栏 + */ + private createTitleBar(windowInstance: WindowInstance): HTMLElement { + const titleBar = document.createElement('div') + titleBar.className = 'window-title-bar' + titleBar.style.cssText = ` + height: 40px; + background: linear-gradient(to bottom, #f8f9fa, #e9ecef); + border-bottom: 1px solid #dee2e6; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 12px; + cursor: move; + user-select: none; + ` + + // 窗体标题 + const title = document.createElement('span') + title.className = 'window-title' + title.textContent = windowInstance.config.title + title.style.cssText = ` + font-size: 14px; + font-weight: 500; + color: #333; + ` + + // 控制按钮组 + const controls = document.createElement('div') + controls.className = 'window-controls' + controls.style.cssText = ` + display: flex; + gap: 8px; + ` + + // 最小化按钮 + if (windowInstance.config.minimizable !== false) { + const minimizeBtn = this.createControlButton('−', () => { + this.minimizeWindow(windowInstance.id) + }) + controls.appendChild(minimizeBtn) + } + + // 最大化按钮 + if (windowInstance.config.maximizable !== false) { + const maximizeBtn = this.createControlButton('□', () => { + if (windowInstance.state === WindowState.MAXIMIZED) { + this.restoreWindow(windowInstance.id) + } else { + this.maximizeWindow(windowInstance.id) + } + }) + controls.appendChild(maximizeBtn) + } + + // 关闭按钮 + if (windowInstance.config.closable !== false) { + const closeBtn = this.createControlButton('×', () => { + this.destroyWindow(windowInstance.id) + }) + closeBtn.style.color = '#dc3545' + controls.appendChild(closeBtn) + } + + titleBar.appendChild(title) + titleBar.appendChild(controls) + + // 添加拖拽功能 + if (windowInstance.config.movable !== false) { + this.addDragFunctionality(titleBar, windowInstance) + } + + return titleBar + } + + /** + * 创建控制按钮 + */ + private createControlButton(text: string, onClick: () => void): HTMLElement { + const button = document.createElement('button') + button.textContent = text + button.style.cssText = ` + width: 24px; + height: 24px; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + font-size: 16px; + line-height: 1; + ` + + button.addEventListener('click', onClick) + + // 添加悬停效果 + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = '#e9ecef' + }) + + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = 'transparent' + }) + + return button + } + + /** + * 添加窗体拖拽功能 + */ + private addDragFunctionality(titleBar: HTMLElement, windowInstance: WindowInstance): void { + let isDragging = false + let startX = 0 + let startY = 0 + let startLeft = 0 + let startTop = 0 + + titleBar.addEventListener('mousedown', (e) => { + if (!windowInstance.element) return + + isDragging = true + startX = e.clientX + startY = e.clientY + + const rect = windowInstance.element.getBoundingClientRect() + startLeft = rect.left + startTop = rect.top + + // 设置为活跃窗体 + this.setActiveWindow(windowInstance.id) + + e.preventDefault() + }) + + document.addEventListener('mousemove', (e) => { + if (!isDragging || !windowInstance.element) return + + const deltaX = e.clientX - startX + const deltaY = e.clientY - startY + + const newLeft = startLeft + deltaX + const newTop = startTop + deltaY + + windowInstance.element.style.left = `${newLeft}px` + windowInstance.element.style.top = `${newTop}px` + windowInstance.element.style.transform = 'none' + + // 更新配置 + windowInstance.config.x = newLeft + windowInstance.config.y = newTop + + this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop) + }) + + document.addEventListener('mouseup', () => { + isDragging = false + }) + } + + /** + * 加载应用 + */ + private async loadApplication(windowInstance: WindowInstance): Promise { + // 动态导入 AppRegistry 检查是否为内置应用 + try { + const { AppRegistry } = await import('../apps/AppRegistry') + const appRegistry = AppRegistry.getInstance() + + // 如果是内置应用,直接返回,不需要等待 + if (appRegistry.hasApp(windowInstance.appId)) { + console.log(`[WindowService] 内置应用 ${windowInstance.appId} 无需等待加载`) + return Promise.resolve() + } + } catch (error) { + console.warn('无法导入 AppRegistry,使用传统加载方式') + } + + // 对于外部应用,保持原有的加载逻辑 + return new Promise((resolve) => { + console.log(`[WindowService] 开始加载外部应用 ${windowInstance.appId}`) + setTimeout(() => { + if (windowInstance.iframe) { + // 这里可以设置 iframe 的 src 来加载具体应用 + windowInstance.iframe.src = 'about:blank' + + // 添加一些示例内容 + const doc = windowInstance.iframe.contentDocument + if (doc) { + doc.body.innerHTML = ` +
+

应用: ${windowInstance.config.title}

+

应用ID: ${windowInstance.appId}

+

窗体ID: ${windowInstance.id}

+

这是一个示例应用内容。

+
+ ` + } + } + console.log(`[WindowService] 外部应用 ${windowInstance.appId} 加载完成`) + resolve() + }, 200) // 改为200ms,即使是外部应用也不需要这么长的时间 + }) + } + + /** + * 更新窗体状态 + */ + private updateWindowState(windowId: string, newState: WindowState): void { + const window = this.windows.get(windowId) + if (!window) return + + const oldState = window.state + + // 只有状态真正发生变化时才触发事件 + if (oldState === newState) return + + window.state = newState + window.updatedAt = new Date() + + // 所有状态变化都应该触发事件,这是正常的系统行为 + console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`) + this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState) + } +} diff --git a/src/ui/App.vue b/src/ui/App.vue index b150f92..05e9744 100644 --- a/src/ui/App.vue +++ b/src/ui/App.vue @@ -13,12 +13,22 @@ + + + + + \ No newline at end of file diff --git a/src/ui/components/WindowManager.vue b/src/ui/components/WindowManager.vue new file mode 100644 index 0000000..59a093a --- /dev/null +++ b/src/ui/components/WindowManager.vue @@ -0,0 +1,138 @@ + + + + + \ No newline at end of file diff --git a/src/ui/desktop-container/DesktopContainer.vue b/src/ui/desktop-container/DesktopContainer.vue index de63f7a..13d55a2 100644 --- a/src/ui/desktop-container/DesktopContainer.vue +++ b/src/ui/desktop-container/DesktopContainer.vue @@ -1,23 +1,275 @@ - + \ No newline at end of file diff --git a/src/ui/desktop-container/useDesktopContainerInit.ts b/src/ui/desktop-container/useDesktopContainerInit.ts index defe929..1217665 100644 --- a/src/ui/desktop-container/useDesktopContainerInit.ts +++ b/src/ui/desktop-container/useDesktopContainerInit.ts @@ -1,19 +1,10 @@ import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts' -import { - computed, - onMounted, - onUnmounted, - reactive, - ref, - toRaw, - toValue, - watch, -} from 'vue' +import { computed, onMounted, onUnmounted, reactive, ref, toRaw, toValue, watch } from 'vue' import type { IGridTemplateParams } from '@/ui/types/IGridTemplateParams.ts' import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' export function useDesktopContainerInit(containerStr: string) { - let container:HTMLElement + let container: HTMLElement // 初始值 const gridTemplate = reactive({ cellExpectWidth: 90, @@ -23,22 +14,28 @@ export function useDesktopContainerInit(containerStr: string) { gapX: 4, gapY: 4, colCount: 1, - rowCount: 1 + rowCount: 1, }) const gridStyle = computed(() => ({ gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`, gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`, - gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px` + gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`, })) - const ro = new ResizeObserver(entries => { + const ro = new ResizeObserver((entries) => { const containerRect = container.getBoundingClientRect() - gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX)); - gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY)); + gridTemplate.colCount = Math.floor( + (containerRect.width + gridTemplate.gapX) / + (gridTemplate.cellExpectWidth + gridTemplate.gapX), + ) + gridTemplate.rowCount = Math.floor( + (containerRect.height + gridTemplate.gapY) / + (gridTemplate.cellExpectHeight + gridTemplate.gapY), + ) - const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1)) - const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1)) + const w = containerRect.width - gridTemplate.gapX * (gridTemplate.colCount - 1) + const h = containerRect.height - gridTemplate.gapY * (gridTemplate.rowCount - 1) gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2)) gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2)) }) @@ -52,45 +49,101 @@ export function useDesktopContainerInit(containerStr: string) { ro.disconnect() }) + // 默认系统应用 + const defaultApps: IDesktopAppIcon[] = [ + { + name: '计算器', + icon: '🧮', + path: 'calculator', + x: 1, + y: 1, + }, + { + name: '记事本', + icon: '📝', + path: 'notepad', + x: 2, + y: 1, + }, + { + name: '系统状态', + icon: '⚙️', + path: 'system-status', + x: 3, + y: 1, + }, + { + name: '文件管理器', + icon: '📁', + path: 'file-manager', + x: 1, + y: 2, + }, + { + name: '浏览器', + icon: '🌐', + path: 'browser', + x: 2, + y: 2, + }, + { + name: '待办事项', + icon: '✓', + path: 'todo-app', + x: 3, + y: 2, + }, + ] + // 有桌面图标的app // const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess) - const appInfos: IProcessInfo[] = [] - const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]') - const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => { - const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name) + const appInfos: IProcessInfo[] = defaultApps + const oldAppIcons: IDesktopAppIcon[] = + JSON.parse(localStorage.getItem('desktopAppIconInfo') || 'null') || defaultApps + const appIcons: IDesktopAppIcon[] = + appInfos.length > 0 + ? appInfos.map((processInfo, index) => { + const oldAppIcon = oldAppIcons.find((oldAppIcon) => oldAppIcon.name === processInfo.name) - // 左上角坐标原点,从上到下从左到右 索引从1开始 - const x = index % gridTemplate.rowCount + 1 - const y = Math.floor(index / gridTemplate.rowCount) + 1 + // 左上角坐标原点,从上到下从左到右 索引从1开始 + const x = (index % gridTemplate.rowCount) + 1 + const y = Math.floor(index / gridTemplate.rowCount) + 1 - return { - name: processInfo.name, - icon: processInfo.icon, - path: processInfo.startName, - x: oldAppIcon ? oldAppIcon.x : x, - y: oldAppIcon ? oldAppIcon.y : y - } - }) + return { + name: processInfo.name, + icon: processInfo.icon, + path: processInfo.startName, + x: oldAppIcon ? oldAppIcon.x : x, + y: oldAppIcon ? oldAppIcon.y : y, + } + }) + : oldAppIcons const appIconsRef = ref(appIcons) const exceedApp = ref([]) - watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => { - // if (oCols == 1 && oRows == 1) return - if (oCols === nCols && oRows === nRows) return - const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows) - appIconsRef.value = appIcons - exceedApp.value = hideAppIcons - }) + watch( + () => [gridTemplate.colCount, gridTemplate.rowCount], + ([nCols, nRows], [oCols, oRows]) => { + // if (oCols == 1 && oRows == 1) return + if (oCols === nCols && oRows === nRows) return + const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows) + appIconsRef.value = appIcons + exceedApp.value = hideAppIcons + }, + ) - watch(() => appIconsRef.value, (appIcons) => { - localStorage.setItem('desktopAppIconInfo', JSON.stringify(appIcons)) - }) + watch( + () => appIconsRef.value, + (appIcons) => { + localStorage.setItem('desktopAppIconInfo', JSON.stringify(appIcons)) + }, + ) return { gridTemplate, appIconsRef, - gridStyle + gridStyle, } } @@ -103,12 +156,12 @@ export function useDesktopContainerInit(containerStr: string) { function rearrangeIcons( appIconInfos: IDesktopAppIcon[], maxCol: number, - maxRow: number + maxRow: number, ): IRearrangeInfo { - const occupied = new Set(); + const occupied = new Set() function key(x: number, y: number) { - return `${x},${y}`; + return `${x},${y}` } const appIcons: IDesktopAppIcon[] = [] @@ -116,7 +169,7 @@ function rearrangeIcons( const temp: IDesktopAppIcon[] = [] for (const appIcon of appIconInfos) { - const { x, y } = appIcon; + const { x, y } = appIcon if (x <= maxCol && y <= maxRow) { if (!occupied.has(key(x, y))) { @@ -132,17 +185,17 @@ function rearrangeIcons( for (const appIcon of temp) { if (appIcons.length < max) { // 最后格子也被占 → 从 (1,1) 开始找空位 - let placed = false; + let placed = false for (let c = 1; c <= maxCol; c++) { for (let r = 1; r <= maxRow; r++) { if (!occupied.has(key(c, r))) { - occupied.add(key(c, r)); - appIcons.push({ ...appIcon, x: c, y: r }); - placed = true; - break; + occupied.add(key(c, r)) + appIcons.push({ ...appIcon, x: c, y: r }) + placed = true + break } } - if (placed) break; + if (placed) break } } else { // 放不下了 @@ -152,13 +205,13 @@ function rearrangeIcons( return { appIcons, - hideAppIcons - }; + hideAppIcons, + } } interface IRearrangeInfo { /** 正常的桌面图标信息 */ - appIcons: IDesktopAppIcon[]; + appIcons: IDesktopAppIcon[] /** 隐藏的桌面图标信息(超出屏幕显示的) */ - hideAppIcons: IDesktopAppIcon[]; + hideAppIcons: IDesktopAppIcon[] } diff --git a/src/ui/desktop-container/useDynamicAppIcons.ts b/src/ui/desktop-container/useDynamicAppIcons.ts new file mode 100644 index 0000000..6ec51ac --- /dev/null +++ b/src/ui/desktop-container/useDynamicAppIcons.ts @@ -0,0 +1,142 @@ +import { computed, watch } from 'vue' +import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts' +import { appRegistry } from '@/apps' +import { externalAppDiscovery } from '@/services/ExternalAppDiscovery' + +/** + * 动态应用图标管理 + * 合并内置应用和外置应用生成图标列表 + */ +export function useDynamicAppIcons() { + + // 生成所有可用的应用图标 + const generateAppIcons = (): IDesktopAppIcon[] => { + const icons: IDesktopAppIcon[] = [] + let position = 1 + + // 添加内置应用 + const builtInApps = appRegistry.getAllApps() + for (const app of builtInApps) { + const x = ((position - 1) % 4) + 1 // 每行4个图标 + const y = Math.floor((position - 1) / 4) + 1 + + icons.push({ + name: app.manifest.name, + icon: app.manifest.icon, + path: app.manifest.id, + x, + y + }) + position++ + } + + // 添加外置应用 + const externalApps = externalAppDiscovery.getDiscoveredApps() + for (const app of externalApps) { + const x = ((position - 1) % 4) + 1 + const y = Math.floor((position - 1) / 4) + 1 + + icons.push({ + name: app.manifest.name, + icon: app.manifest.icon || '📱', // 默认图标 + path: app.manifest.id, + x, + y + }) + position++ + } + + // 添加系统状态应用 + icons.push({ + name: '系统状态', + icon: '⚙️', + path: 'system-status', + x: ((position - 1) % 4) + 1, + y: Math.floor((position - 1) / 4) + 1 + }) + + return icons + } + + // 计算应用图标(响应式) + const appIcons = computed(() => { + return generateAppIcons() + }) + + // 从本地存储加载位置信息 + const loadIconPositions = (): Record => { + try { + const saved = localStorage.getItem('desktopAppIconPositions') + return saved ? JSON.parse(saved) : {} + } catch (error) { + console.warn('加载图标位置信息失败:', error) + return {} + } + } + + // 保存位置信息到本地存储 + const saveIconPositions = (icons: IDesktopAppIcon[]) => { + try { + const positions = icons.reduce((acc, icon) => { + acc[icon.path] = { x: icon.x, y: icon.y } + return acc + }, {} as Record) + + localStorage.setItem('desktopAppIconPositions', JSON.stringify(positions)) + } catch (error) { + console.warn('保存图标位置信息失败:', error) + } + } + + // 应用保存的位置信息 + const applyIconPositions = (icons: IDesktopAppIcon[]): IDesktopAppIcon[] => { + const savedPositions = loadIconPositions() + + return icons.map(icon => { + const savedPos = savedPositions[icon.path] + if (savedPos) { + return { ...icon, x: savedPos.x, y: savedPos.y } + } + return icon + }) + } + + // 获取带位置信息的应用图标 + const getAppIconsWithPositions = (): IDesktopAppIcon[] => { + const icons = appIcons.value + return applyIconPositions(icons) + } + + // 刷新应用列表(仅在需要时手动调用) + const refreshApps = async () => { + try { + // 只有在系统服务已启动的情况下才刷新 + if (externalAppDiscovery['hasStarted']) { + await externalAppDiscovery.refresh() + console.log('[DynamicAppIcons] 应用列表已刷新') + } else { + console.log('[DynamicAppIcons] 系统服务未启动,跳过刷新') + } + } catch (error) { + console.error('[DynamicAppIcons] 刷新应用列表失败:', error) + } + } + + return { + appIcons, + getAppIconsWithPositions, + saveIconPositions, + refreshApps + } +} + +// 获取应用ID映射 +export function getAppIdFromIcon(iconInfo: IDesktopAppIcon): string { + // 特殊处理系统应用 + if (iconInfo.path === 'system-status') { + return 'system-status' + } + + // 对于其他应用,path就是appId + return iconInfo.path +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 4a99dc0..ece789d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,8 @@ export default defineConfig({ ], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) + '@': fileURLToPath(new URL('./src', import.meta.url)), + 'vue': 'vue/dist/vue.esm-bundler.js' } } })