From 9dbc054483db48e1511a8476975a812ea00b97be Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Wed, 24 Sep 2025 16:43:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .qoder/quests/music-player-error-handling.md | 0 .../system-business-decoupling-design.md | 623 ++++++++ .../zh/content/UI组件体系/AppIcon组件.md | 204 +++ .../UI组件体系/DesktopContainer组件.md | 309 ++++ .../zh/content/UI组件体系/UI组件体系.md | 254 ++++ .../repowiki/zh/content/事件系统/事件系统.md | 262 ++++ .../zh/content/事件系统/核心事件总线.md | 207 +++ .../zh/content/事件系统/桌面事件管理器.md | 187 +++ .../窗口表单事件管理器/窗口关闭事件.md | 186 +++ .../窗口表单事件管理器/窗口创建完成事件.md | 76 + .../窗口表单事件管理器/窗口数据更新事件.md | 149 ++ .../窗口表单事件管理器/窗口最大化事件.md | 114 ++ .../窗口表单事件管理器/窗口最小化事件.md | 96 ++ .../窗口表单事件管理器/窗口聚焦事件.md | 131 ++ .../窗口表单事件管理器/窗口表单事件管理器.md | 196 +++ .../窗口表单事件管理器/窗口还原事件.md | 120 ++ .../content/响应式布局系统/响应式布局系统.md | 193 +++ .../响应式布局系统/图标重排与持久化.md | 124 ++ .../content/响应式布局系统/布局初始化逻辑.md | 221 +++ .../响应式布局系统/网格参数计算机制.md | 140 ++ .qoder/repowiki/zh/content/快速开始.md | 161 +++ .qoder/repowiki/zh/content/技术栈与依赖.md | 149 ++ .qoder/repowiki/zh/content/构建与部署.md | 182 +++ .qoder/repowiki/zh/content/状态管理.md | 209 +++ .qoder/repowiki/zh/content/项目概述.md | 275 ++++ .../repowiki/zh/meta/repowiki-metadata.json | 1 + PROJECT_SUMMARY.md | 185 +++ index.html | 4 +- public/apps/README.md | 49 + public/apps/music-player/README.md | 170 +++ public/apps/music-player/app.js | 751 ++++++++++ public/apps/music-player/index.html | 88 ++ public/apps/music-player/manifest.json | 27 + public/apps/music-player/style.css | 430 ++++++ src/apps/AppRegistry.ts | 97 ++ src/apps/calculator/Calculator.vue | 348 +++++ src/apps/components/BuiltInApp.vue | 125 ++ src/apps/index.ts | 90 ++ src/apps/notepad/Notepad.vue | 527 +++++++ src/apps/todo/Todo.vue | 658 +++++++++ src/apps/types/AppManifest.ts | 35 + src/core/XSystem.ts | 34 - src/core/apps/department/app.json | 18 - src/core/apps/department/main.vue | 9 - src/core/apps/fileManage/app.json | 18 - src/core/apps/fileManage/main.vue | 9 - src/core/apps/music/app.json | 18 - src/core/apps/music/main.vue | 9 - src/core/apps/personalCenter/app.json | 18 - src/core/apps/personalCenter/main.vue | 9 - src/core/apps/photograph/app.json | 18 - src/core/apps/photograph/main.vue | 9 - src/core/apps/recycleBin/app.json | 18 - src/core/apps/recycleBin/main.vue | 9 - src/core/apps/setting/app.json | 18 - src/core/apps/setting/main.vue | 9 - src/core/apps/tv/app.json | 18 - src/core/apps/tv/main.vue | 9 - src/core/apps/video/app.json | 18 - src/core/apps/video/main.vue | 9 - src/core/common/hooks/useObservableVue.ts | 87 -- src/core/common/naive-ui/components.ts | 10 - src/core/common/naive-ui/discrete-api.ts | 21 - src/core/common/naive-ui/theme.ts | 15 - src/core/common/types/IDestroyable.ts | 8 - src/core/common/types/IVersion.ts | 29 - src/core/desktop/DesktopProcess.ts | 82 -- src/core/desktop/DesktopProcessInfo.ts | 15 - src/core/desktop/types/IDesktopAppIcon.ts | 15 - src/core/desktop/types/IGridTemplateParams.ts | 21 - src/core/desktop/ui/DesktopComponent.vue | 95 -- src/core/desktop/ui/DesktopElement.ts | 35 - src/core/desktop/ui/components/AppIcon.vue | 55 - .../ui/components/DesktopAppIconElement.ts | 17 - src/core/desktop/ui/css/desktop.scss | 31 - src/core/desktop/ui/hooks/useDesktopInit.ts | 169 --- src/core/desktop/ui/imgs/desktop-bg-1.jpeg | Bin 49018 -> 0 bytes src/core/desktop/ui/imgs/desktop-bg-2.jpeg | Bin 59550 -> 0 bytes src/core/desktop/utils/useIconDrag.ts | 64 - src/core/events/EventManager.ts | 35 - src/core/events/IEventBuilder.ts | 47 - src/core/events/WindowFormEventManager.ts | 61 - src/core/events/impl/EventBuilderImpl.ts | 96 -- src/core/process/IProcess.ts | 28 - src/core/process/IProcessInfo.ts | 26 - src/core/process/IProcessManager.ts | 41 - src/core/process/ProcessManager.ts | 3 - src/core/process/impl/ProcessImpl.ts | 83 -- src/core/process/impl/ProcessInfoImpl.ts | 101 -- src/core/process/impl/ProcessManagerImpl.ts | 107 -- .../process/types/IAppProcessInfoParams.ts | 26 - src/core/process/types/ProcessEventTypes.ts | 24 - src/core/service/kernel/AService.ts | 21 - src/core/service/kernel/ServiceManager.ts | 41 - .../service/services/NotificationService.ts | 8 - src/core/service/services/SettingsService.ts | 8 - src/core/service/services/UserService.ts | 20 - .../service/services/WindowFormService.ts | 64 - src/core/state/IObservable.ts | 57 - src/core/state/impl/ObservableImpl.ts | 305 ---- src/core/state/impl/ObservableWeakRefImpl.ts | 297 ---- src/core/state/store/GlobalStore.ts | 14 - src/core/system/BasicSystemProcess.ts | 19 - src/core/system/BasicSystemProcessInfo.ts | 18 - src/core/utils/DraggableResizableWindow.ts | 752 ---------- src/core/utils/Singleton.ts | 17 - src/core/window/IWindowForm.ts | 14 - src/core/window/impl/WindowFormImpl.ts | 114 -- src/core/window/types/IWindowFormConfig.ts | 60 - src/core/window/types/WindowFormTypes.ts | 10 - src/core/window/ui/WindowFormElement.ts | 904 ------------ src/core/window/ui/css/wf.scss | 101 -- src/core/window/ui/window-form-helper.ts | 20 - src/main.ts | 49 +- src/sdk/index.ts | 702 +++++++++ src/sdk/types.ts | 638 +++++++++ src/services/ApplicationLifecycleManager.ts | 1027 +++++++++++++ src/services/ApplicationSandboxEngine.ts | 1273 +++++++++++++++++ src/services/EventCommunicationService.ts | 639 +++++++++ src/services/ExternalAppDiscovery.ts | 629 ++++++++ src/services/ResourceService.ts | 689 +++++++++ src/services/SystemServiceIntegration.ts | 873 +++++++++++ src/services/WindowService.ts | 651 +++++++++ src/ui/App.vue | 12 +- src/ui/components/AppRenderer.vue | 289 ++++ src/ui/components/WindowManager.vue | 138 ++ src/ui/desktop-container/DesktopContainer.vue | 270 +++- .../useDesktopContainerInit.ts | 169 ++- .../desktop-container/useDynamicAppIcons.ts | 142 ++ vite.config.ts | 3 +- 130 files changed, 16474 insertions(+), 4660 deletions(-) create mode 100644 .qoder/quests/music-player-error-handling.md create mode 100644 .qoder/quests/system-business-decoupling-design.md create mode 100644 .qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md create mode 100644 .qoder/repowiki/zh/content/UI组件体系/DesktopContainer组件.md create mode 100644 .qoder/repowiki/zh/content/UI组件体系/UI组件体系.md create mode 100644 .qoder/repowiki/zh/content/事件系统/事件系统.md create mode 100644 .qoder/repowiki/zh/content/事件系统/核心事件总线.md create mode 100644 .qoder/repowiki/zh/content/事件系统/桌面事件管理器.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口关闭事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口创建完成事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口数据更新事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最大化事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最小化事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口聚焦事件.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口表单事件管理器.md create mode 100644 .qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口还原事件.md create mode 100644 .qoder/repowiki/zh/content/响应式布局系统/响应式布局系统.md create mode 100644 .qoder/repowiki/zh/content/响应式布局系统/图标重排与持久化.md create mode 100644 .qoder/repowiki/zh/content/响应式布局系统/布局初始化逻辑.md create mode 100644 .qoder/repowiki/zh/content/响应式布局系统/网格参数计算机制.md create mode 100644 .qoder/repowiki/zh/content/快速开始.md create mode 100644 .qoder/repowiki/zh/content/技术栈与依赖.md create mode 100644 .qoder/repowiki/zh/content/构建与部署.md create mode 100644 .qoder/repowiki/zh/content/状态管理.md create mode 100644 .qoder/repowiki/zh/content/项目概述.md create mode 100644 .qoder/repowiki/zh/meta/repowiki-metadata.json create mode 100644 PROJECT_SUMMARY.md create mode 100644 public/apps/README.md create mode 100644 public/apps/music-player/README.md create mode 100644 public/apps/music-player/app.js create mode 100644 public/apps/music-player/index.html create mode 100644 public/apps/music-player/manifest.json create mode 100644 public/apps/music-player/style.css create mode 100644 src/apps/AppRegistry.ts create mode 100644 src/apps/calculator/Calculator.vue create mode 100644 src/apps/components/BuiltInApp.vue create mode 100644 src/apps/index.ts create mode 100644 src/apps/notepad/Notepad.vue create mode 100644 src/apps/todo/Todo.vue create mode 100644 src/apps/types/AppManifest.ts delete mode 100644 src/core/XSystem.ts delete mode 100644 src/core/apps/department/app.json delete mode 100644 src/core/apps/department/main.vue delete mode 100644 src/core/apps/fileManage/app.json delete mode 100644 src/core/apps/fileManage/main.vue delete mode 100644 src/core/apps/music/app.json delete mode 100644 src/core/apps/music/main.vue delete mode 100644 src/core/apps/personalCenter/app.json delete mode 100644 src/core/apps/personalCenter/main.vue delete mode 100644 src/core/apps/photograph/app.json delete mode 100644 src/core/apps/photograph/main.vue delete mode 100644 src/core/apps/recycleBin/app.json delete mode 100644 src/core/apps/recycleBin/main.vue delete mode 100644 src/core/apps/setting/app.json delete mode 100644 src/core/apps/setting/main.vue delete mode 100644 src/core/apps/tv/app.json delete mode 100644 src/core/apps/tv/main.vue delete mode 100644 src/core/apps/video/app.json delete mode 100644 src/core/apps/video/main.vue delete mode 100644 src/core/common/hooks/useObservableVue.ts delete mode 100644 src/core/common/naive-ui/components.ts delete mode 100644 src/core/common/naive-ui/discrete-api.ts delete mode 100644 src/core/common/naive-ui/theme.ts delete mode 100644 src/core/common/types/IDestroyable.ts delete mode 100644 src/core/common/types/IVersion.ts delete mode 100644 src/core/desktop/DesktopProcess.ts delete mode 100644 src/core/desktop/DesktopProcessInfo.ts delete mode 100644 src/core/desktop/types/IDesktopAppIcon.ts delete mode 100644 src/core/desktop/types/IGridTemplateParams.ts delete mode 100644 src/core/desktop/ui/DesktopComponent.vue delete mode 100644 src/core/desktop/ui/DesktopElement.ts delete mode 100644 src/core/desktop/ui/components/AppIcon.vue delete mode 100644 src/core/desktop/ui/components/DesktopAppIconElement.ts delete mode 100644 src/core/desktop/ui/css/desktop.scss delete mode 100644 src/core/desktop/ui/hooks/useDesktopInit.ts delete mode 100644 src/core/desktop/ui/imgs/desktop-bg-1.jpeg delete mode 100644 src/core/desktop/ui/imgs/desktop-bg-2.jpeg delete mode 100644 src/core/desktop/utils/useIconDrag.ts delete mode 100644 src/core/events/EventManager.ts delete mode 100644 src/core/events/IEventBuilder.ts delete mode 100644 src/core/events/WindowFormEventManager.ts delete mode 100644 src/core/events/impl/EventBuilderImpl.ts delete mode 100644 src/core/process/IProcess.ts delete mode 100644 src/core/process/IProcessInfo.ts delete mode 100644 src/core/process/IProcessManager.ts delete mode 100644 src/core/process/ProcessManager.ts delete mode 100644 src/core/process/impl/ProcessImpl.ts delete mode 100644 src/core/process/impl/ProcessInfoImpl.ts delete mode 100644 src/core/process/impl/ProcessManagerImpl.ts delete mode 100644 src/core/process/types/IAppProcessInfoParams.ts delete mode 100644 src/core/process/types/ProcessEventTypes.ts delete mode 100644 src/core/service/kernel/AService.ts delete mode 100644 src/core/service/kernel/ServiceManager.ts delete mode 100644 src/core/service/services/NotificationService.ts delete mode 100644 src/core/service/services/SettingsService.ts delete mode 100644 src/core/service/services/UserService.ts delete mode 100644 src/core/service/services/WindowFormService.ts delete mode 100644 src/core/state/IObservable.ts delete mode 100644 src/core/state/impl/ObservableImpl.ts delete mode 100644 src/core/state/impl/ObservableWeakRefImpl.ts delete mode 100644 src/core/state/store/GlobalStore.ts delete mode 100644 src/core/system/BasicSystemProcess.ts delete mode 100644 src/core/system/BasicSystemProcessInfo.ts delete mode 100644 src/core/utils/DraggableResizableWindow.ts delete mode 100644 src/core/utils/Singleton.ts delete mode 100644 src/core/window/IWindowForm.ts delete mode 100644 src/core/window/impl/WindowFormImpl.ts delete mode 100644 src/core/window/types/IWindowFormConfig.ts delete mode 100644 src/core/window/types/WindowFormTypes.ts delete mode 100644 src/core/window/ui/WindowFormElement.ts delete mode 100644 src/core/window/ui/css/wf.scss delete mode 100644 src/core/window/ui/window-form-helper.ts create mode 100644 src/sdk/index.ts create mode 100644 src/sdk/types.ts create mode 100644 src/services/ApplicationLifecycleManager.ts create mode 100644 src/services/ApplicationSandboxEngine.ts create mode 100644 src/services/EventCommunicationService.ts create mode 100644 src/services/ExternalAppDiscovery.ts create mode 100644 src/services/ResourceService.ts create mode 100644 src/services/SystemServiceIntegration.ts create mode 100644 src/services/WindowService.ts create mode 100644 src/ui/components/AppRenderer.vue create mode 100644 src/ui/components/WindowManager.vue create mode 100644 src/ui/desktop-container/useDynamicAppIcons.ts 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 719e22c9f0e13882db8309dfec96dda5a60f116e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49018 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 J)2B#7|JfTmXgdG^ 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' } } })