Compare commits

...

51 Commits

Author SHA1 Message Date
a102395286 更新vite 2025-11-27 14:15:06 +08:00
81007cf938 修改组件逻辑+优化代码 2025-11-11 17:32:41 +08:00
f2bb7f3196 修改组件逻辑+优化代码 2025-11-11 17:20:43 +08:00
ce688a6834 保存 2025-10-24 11:07:27 +08:00
ced6786f86 保存 2025-10-24 10:03:54 +08:00
f2b12fbaf5 保存 2025-10-23 12:14:10 +08:00
f9fdb1a6c2 保存 2025-10-23 12:14:03 +08:00
900a72e4c9 单例模式 装饰器 2025-10-18 20:10:32 +08:00
7b12efd09c 保存 2025-10-18 19:12:18 +08:00
e54bd0a447 之前的 2025-10-11 12:10:35 +08:00
8d25c143c5 111 2025-10-11 12:05:57 +08:00
45ec0fd021 error handle 2025-10-11 11:40:41 +08:00
49d7f2c37e di 2025-10-11 11:30:44 +08:00
9a90f1258b 1 2025-10-10 13:49:55 +08:00
acecffb055 ` 2025-10-10 11:25:50 +08:00
0ca5daad3b 111 2025-10-10 10:37:11 +08:00
05882bb3d3 · 2025-10-10 10:36:53 +08:00
71d5aabb84 1 2025-10-10 10:28:36 +08:00
204dd4781b 11 2025-10-10 10:08:05 +08:00
ed0527bf27 1 2025-09-28 14:51:32 +08:00
a56197a349 1 2025-09-28 14:32:07 +08:00
f1ba609254 1 2025-09-28 14:32:00 +08:00
7b1dff9ea1 11 2025-09-28 12:34:28 +08:00
b77a20f9b0 1 2025-09-28 12:05:59 +08:00
972e76e655 优化 2025-09-25 15:31:11 +08:00
d18a3d5279 1 2025-09-25 13:36:38 +08:00
d042520b14 1 2025-09-25 12:48:29 +08:00
9dbc054483 保存 2025-09-24 16:43:10 +08:00
12f46e6f8e 保存 2025-09-24 11:30:06 +08:00
16b4b27352 优化 2025-09-22 13:23:12 +08:00
e3ff2045ea 优化 2025-09-19 11:49:04 +08:00
fd4f9aa66b WindowFormElement 2025-09-19 10:35:46 +08:00
62b4ae7379 WindowFormImpl 2025-09-19 09:57:14 +08:00
68bdabf928 修改 2025-09-17 12:22:28 +08:00
e3cbba0607 优化ObservableImpl 2025-09-17 10:11:11 +08:00
3a6f5cdbba 优化ObservableImpl 2025-09-17 10:10:25 +08:00
6eee4933e1 优化ObservableImpl 2025-09-17 09:58:27 +08:00
122fba228a WindowFormElement 2025-09-16 14:32:57 +08:00
08e08043d7 WindowFormElement 2025-09-15 16:25:15 +08:00
9a30c2b3d7 WindowFormElement 2025-09-15 15:33:58 +08:00
3401c8b737 修改 2025-09-12 14:45:32 +08:00
67728c5c55 逻辑修改 2025-09-12 12:53:04 +08:00
27b70ac35f 样式+拖拽逻辑修改 2025-09-12 12:20:40 +08:00
dc25d283d8 保存一下 2025-09-10 15:44:10 +08:00
7cbb37d542 保存一下 2025-09-09 12:03:09 +08:00
d43664c945 完善和修复一下DraggableResizableWindow的bug 2025-09-07 15:23:04 +08:00
fcf3c2933c 窗体最大化、最小化和恢复默认状态的动画 2025-09-04 21:40:00 +08:00
3d402de0af 窗体最大化、最小化和恢复默认状态 2025-09-04 21:37:04 +08:00
442a44dfc1 保存一下 2025-09-04 21:26:36 +08:00
f83dd91dbd 保存一下 2025-09-04 21:19:52 +08:00
c887853ec1 保存一下 2025-09-04 19:50:59 +08:00
115 changed files with 13450 additions and 3615 deletions

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ coverage
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
.qoder/*

View File

@@ -2,5 +2,6 @@
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"printWidth": 100 "printWidth": 100,
"trailingComma": "none"
} }

167
PRETTIER_CONFIG_GUIDE.md Normal file
View File

@@ -0,0 +1,167 @@
# Prettier 常用配置项详解
## 基础格式化选项
### semi (boolean)
- **默认值**: true
- **说明**: 在语句末尾打印分号
- **示例**:
- true: `console.log('hello');`
- false: `console.log('hello')`
### singleQuote (boolean)
- **默认值**: false
- **说明**: 使用单引号而不是双引号
- **示例**:
- true: `const str = 'hello';`
- false: `const str = "hello";`
### printWidth (number)
- **默认值**: 80
- **说明**: 指定每行代码的最大字符数,超过这个长度会自动换行
### useTabs (boolean)
- **默认值**: false
- **说明**: 使用制表符(tab)缩进而不是空格
### tabWidth (number)
- **默认值**: 2
- **说明**: 指定每个缩进级别的空格数
## 对象和数组格式化
### bracketSpacing (boolean)
- **默认值**: true
- **说明**: 在对象字面量中的括号之间打印空格
- **示例**:
- true: `{ foo: bar }`
- false: `{foo: bar}`
### bracketSameLine (boolean)
- **默认值**: false
- **说明**: 将多行 HTML 元素的 > 放在最后一行的末尾,而不是单独放在下一行
### trailingComma (string)
- **默认值**: "es5"
- **可选值**: "none" | "es5" | "all"
- **说明**: 在多行对象或数组的最后一个元素后是否添加逗号
- **示例**:
```javascript
// "none"
{
foo: 'bar'
}
// "es5"
{
foo: 'bar',
}
// "all"
{
foo: 'bar',
baz: 'qux',
}
```
## JSX 特定选项
### jsxSingleQuote (boolean)
- **默认值**: false
- **说明**: 在 JSX 中使用单引号而不是双引号
### singleAttributePerLine (boolean)
- **默认值**: false
- **说明**: 强制每个 HTML、Vue 和 JSX 属性独占一行
- **示例**:
```html
<!-- singleAttributePerLine: false -->
<button class="btn" id="submit" type="submit">Submit</button>
<!-- singleAttributePerLine: true -->
<button class="btn" id="submit" type="submit">Submit</button>
```
## 函数和箭头函数
### arrowParens (string)
- **默认值**: "always"
- **可选值**: "always" | "avoid"
- **说明**: 在箭头函数参数周围始终包含括号
- **示例**:
```javascript
// "always"
const fn = (x) => x
// "avoid"
const fn = (x) => x
const fn2 = (x, y) => x + y
```
## 其他格式化选项
### endOfLine (string)
- **默认值**: "lf"
- **可选值**: "auto" | "lf" | "crlf" | "cr"
- **说明**: 指定换行符风格
- "lf": \n (Linux/macOS)
- "crlf": \r\n (Windows)
### quoteProps (string)
- **默认值**: "as-needed"
- **可选值**: "as-needed" | "consistent" | "preserve"
- **说明**: 对象属性引用方式
- "as-needed": 仅在需要时才引用属性
- "consistent": 如果至少有一个属性需要引用,则引用所有属性
- "preserve": 保持原样
### htmlWhitespaceSensitivity (string)
- **默认值**: "css"
- **可选值**: "css" | "strict" | "ignore"
- **说明**: 指定 HTML、Vue、Angular 文件中全局空白敏感度
- "css": 尊重 CSS display 属性的默认值
- "strict": 所有空白都被认为是重要的
- "ignore": 所有空白都被认为是不重要的
### vueIndentScriptAndStyle (boolean)
- **默认值**: false
- **说明**: Vue 文件中单文件组件的 `<script>` 和 `<style>` 标签内的代码是否缩进
### experimentalTernaries (boolean)
- **默认值**: false
- **说明**: 尝试 Prettier 的新三元表达式格式化方式,在成为默认行为之前使用
### experimentalOperatorPosition (string)
- **默认值**: "end"
- **可选值**: "start" | "end"
- **说明**: 当二元表达式换行时,操作符的位置
- "start": 操作符放在新行的开头
- "end": 操作符放在前一行的末尾(默认行为)
### objectWrap (string)
- **默认值**: "preserve"
- **可选值**: "preserve" | "collapse"
- **说明**: 配置 Prettier 如何包装对象字面量
- "preserve": 如果在开括号和第一个属性之间有换行符,则保持多行格式
- "collapse": 如果可能,将对象压缩到单行

220
PROJECT_SUMMARY.md Normal file
View File

@@ -0,0 +1,220 @@
# Vue Desktop
一个基于 Vue 3 + TypeScript + Vite 的现代化桌面环境模拟器
## 🎯 项目目标
构建一个轻量级、模块化的桌面环境,支持:
- 内置应用(计算器、记事本、待办事项等)
- 外置应用加载(通过 iframe 沙箱)
- 窗口管理(创建、移动、缩放、最小化等)
- 资源管理(存储、权限控制)
- 应用生命周期管理
- 安全沙箱机制
## 🏗️ 架构实现
### 核心服务层
- **[WindowFormService](src/services/windowForm/WindowFormService.ts)** - 窗体管理服务,支持完整的窗体生命周期
- **[ResourceService](./src/services/ResourceService.ts)** - 资源管理服务,提供权限控制和资源访问
- **[ApplicationSandboxEngine](./src/services/ApplicationSandboxEngine.ts)** - 应用沙箱引擎,多层安全隔离
- **[ApplicationLifecycleManager](./src/services/ApplicationLifecycleManager.ts)** - 应用生命周期管理
- **[SystemServiceIntegration](./src/services/SystemServiceIntegration.ts)** - 系统服务集成层
### SDK接口层
- **[SystemSDK](./src/sdk/index.ts)** - 统一SDK接口为应用提供系统能力访问
### 事件系统
- **[IEventBuilder](./src/events/IEventBuilder.ts)** - 事件总线接口
- **[EventBuilderImpl](./src/events/impl/EventBuilderImpl.ts)** - 事件总线实现
### 应用管理
- **[AppRegistry](./src/apps/AppRegistry.ts)** - 应用注册中心
- **[ExternalAppDiscovery](./src/services/ExternalAppDiscovery.ts)** - 外置应用发现服务
## 📁 项目结构
```
.
├── public\apps
│ ├── music-player
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── style.css
│ └── README.md
├── src
│ ├── apps
│ │ ├── calculator
│ │ │ └── Calculator.vue
│ │ ├── components
│ │ │ └── BuiltInApp.vue
│ │ ├── notepad
│ │ │ └── Notepad.vue
│ │ ├── todo
│ │ │ └── Todo.vue
│ │ ├── types
│ │ │ └── AppManifest.ts
│ │ ├── AppRegistry.ts
│ │ └── index.ts
│ ├── common
│ │ ├── hooks
│ │ │ ├── useClickFocus.ts
│ │ │ └── useObservableVue.ts
│ │ ├── naive-ui
│ │ │ ├── components.ts
│ │ │ ├── discrete-api.ts
│ │ │ └── theme.ts
│ │ └── types
│ │ ├── IDestroyable.ts
│ │ └── IVersion.ts
│ ├── css
│ │ └── basic.css
│ ├── events
│ │ ├── impl
│ │ │ └── EventBuilderImpl.ts
│ │ └── IEventBuilder.ts
│ ├── sdk
│ │ ├── index.ts
│ │ └── types.ts
│ ├── services
│ │ ├── ApplicationLifecycleManager.ts
│ │ ├── ApplicationSandboxEngine.ts
│ │ ├── ExternalAppDiscovery.ts
│ │ ├── ResourceService.ts
│ │ ├── SystemServiceIntegration.ts
│ │ └── WindowFormService.ts
│ ├── stores
│ │ └── counter.ts
│ ├── ui
│ │ ├── components
│ │ │ ├── AppRenderer.vue
│ │ │ └── WindowManager.vue
│ │ ├── desktop-container
│ │ │ ├── AppIcon.vue
│ │ │ ├── DesktopContainer.vue
│ │ │ ├── useDesktopContainerInit.ts
│ │ │ └── useDynamicAppIcons.ts
│ │ ├── types
│ │ │ ├── IDesktopAppIcon.ts
│ │ │ ├── IGridTemplateParams.ts
│ │ │ └── WindowFormTypes.ts
│ │ └── App.vue
│ └── main.ts
├── PRETTIER_CONFIG_GUIDE.md
├── PROJECT_SUMMARY.md
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── pnpm-lock.yaml
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts
```
## 🚀 快速开始
### 环境要求
- Node.js >= 16.0.0
- pnpm >= 7.0.0
### 安装依赖
```bash
pnpm install
```
### 开发模式
```bash
pnpm dev
```
### 构建生产版本
```bash
pnpm build
```
### 预览生产构建
```bash
pnpm preview
```
## 🛠️ 开发指南
### 添加内置应用
1.`src/apps/` 目录下创建应用文件夹
2. 创建 Vue 组件文件(如 `MyApp.vue`
3.`src/apps/AppRegistry.ts` 中注册应用
### 添加外置应用
1.`public/apps/` 目录下创建应用文件夹
2. 添加 `manifest.json` 应用清单文件
3. 添加应用的 HTML/CSS/JS 文件
4. 系统会自动发现并加载该应用
### 系统服务使用
通过依赖注入获取系统服务:
```typescript
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
const systemService = inject<SystemServiceIntegration>('systemService')
```
可用服务:
- `getWindowFormService()` - 窗体服务
- `getResourceService()` - 资源服务
- `getSandboxEngine()` - 沙箱引擎
- `getLifecycleManager()` - 生命周期管理器
## 📖 技术文档
### 窗体系统
窗体系统支持完整的生命周期管理,包括创建、移动、缩放、最小化、最大化等操作。
### 资源管理
资源服务提供安全的存储访问和权限控制机制。
### 沙箱安全
应用沙箱引擎提供多层安全隔离,防止恶意代码访问系统资源。
### 应用生命周期
应用生命周期管理器负责应用的安装、启动、停止、卸载等操作。
## 🧪 测试
### 单元测试
```bash
pnpm test
```
### 端到端测试
```bash
pnpm test:e2e
```
## 📦 部署
构建产物可直接部署到任何静态文件服务器上。

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue-desktop</title> <title>Vue 桌面系统</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.6.0",
"lit": "^3.3.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@@ -34,7 +35,7 @@
"sass": "^1.90.0", "sass": "^1.90.0",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"unocss": "^66.4.2", "unocss": "^66.4.2",
"vite": "^7.0.6", "vite": "^7.2.4",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4" "vue-tsc": "^3.0.4"
} }

149
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@vueuse/core': '@vueuse/core':
specifier: ^13.6.0 specifier: ^13.6.0
version: 13.6.0(vue@3.5.18(typescript@5.8.3)) version: 13.6.0(vue@3.5.18(typescript@5.8.3))
lit:
specifier: ^3.3.1
version: 3.3.1
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@@ -35,10 +38,10 @@ importers:
version: 22.17.1 version: 22.17.1
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 6.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 5.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vue/tsconfig': '@vue/tsconfig':
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3)) version: 0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3))
@@ -59,13 +62,13 @@ importers:
version: 5.8.3 version: 5.8.3
unocss: unocss:
specifier: ^66.4.2 specifier: ^66.4.2
version: 66.4.2(postcss@8.5.6)(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) version: 66.4.2(postcss@8.5.6)(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite: vite:
specifier: ^7.0.6 specifier: ^7.2.4
version: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) version: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-vue-devtools: vite-plugin-vue-devtools:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
vue-tsc: vue-tsc:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.5(typescript@5.8.3) version: 3.0.5(typescript@5.8.3)
@@ -413,6 +416,12 @@ packages:
'@juggle/resize-observer@3.4.0': '@juggle/resize-observer@3.4.0':
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
'@lit-labs/ssr-dom-shim@1.4.0':
resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==}
'@lit/reactive-element@2.1.1':
resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==}
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@@ -649,6 +658,9 @@ packages:
'@types/node@22.17.1': '@types/node@22.17.1':
resolution: {integrity: sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==} resolution: {integrity: sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@@ -1042,6 +1054,15 @@ packages:
picomatch: picomatch:
optional: true optional: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
figures@6.1.0: figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1170,6 +1191,15 @@ packages:
kolorist@1.8.0: kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
lit-element@4.2.1:
resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==}
lit-html@3.3.1:
resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==}
lit@3.3.1:
resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==}
local-pkg@1.1.1: local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -1414,6 +1444,10 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -1505,8 +1539,8 @@ packages:
peerDependencies: peerDependencies:
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
vite@7.1.1: vite@7.2.4:
resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==} resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -1920,6 +1954,12 @@ snapshots:
'@juggle/resize-observer@3.4.0': {} '@juggle/resize-observer@3.4.0': {}
'@lit-labs/ssr-dom-shim@1.4.0': {}
'@lit/reactive-element@2.1.1':
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -2071,15 +2111,17 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/trusted-types@2.0.7': {}
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@unocss/astro@66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))': '@unocss/astro@66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))':
dependencies: dependencies:
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/reset': 66.4.2 '@unocss/reset': 66.4.2
'@unocss/vite': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/vite': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
optionalDependencies: optionalDependencies:
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
'@unocss/cli@66.4.2': '@unocss/cli@66.4.2':
dependencies: dependencies:
@@ -2210,7 +2252,7 @@ snapshots:
dependencies: dependencies:
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/vite@66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))': '@unocss/vite@66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@unocss/config': 66.4.2 '@unocss/config': 66.4.2
@@ -2221,23 +2263,23 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
tinyglobby: 0.2.14 tinyglobby: 0.2.14
unplugin-utils: 0.2.5 unplugin-utils: 0.2.5
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
'@vitejs/plugin-vue-jsx@5.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vitejs/plugin-vue-jsx@5.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@rolldown/pluginutils': 1.0.0-beta.31 '@rolldown/pluginutils': 1.0.0-beta.31
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.0) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.0)
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-vue@6.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vitejs/plugin-vue@6.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29 '@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
'@volar/language-core@2.4.22': '@volar/language-core@2.4.22':
@@ -2320,14 +2362,14 @@ snapshots:
dependencies: dependencies:
'@vue/devtools-kit': 7.7.7 '@vue/devtools-kit': 7.7.7
'@vue/devtools-core@8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vue/devtools-core@8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@vue/devtools-kit': 8.0.0 '@vue/devtools-kit': 8.0.0
'@vue/devtools-shared': 8.0.0 '@vue/devtools-shared': 8.0.0
mitt: 3.0.1 mitt: 3.0.1
nanoid: 5.1.5 nanoid: 5.1.5
pathe: 2.0.3 pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-hot-client: 2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- vite - vite
@@ -2595,6 +2637,10 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
figures@6.1.0: figures@6.1.0:
dependencies: dependencies:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
@@ -2679,6 +2725,22 @@ snapshots:
kolorist@1.8.0: {} kolorist@1.8.0: {}
lit-element@4.2.1:
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
'@lit/reactive-element': 2.1.1
lit-html: 3.3.1
lit-html@3.3.1:
dependencies:
'@types/trusted-types': 2.0.7
lit@3.3.1:
dependencies:
'@lit/reactive-element': 2.1.1
lit-element: 4.2.1
lit-html: 3.3.1
local-pkg@1.1.1: local-pkg@1.1.1:
dependencies: dependencies:
mlly: 1.7.4 mlly: 1.7.4
@@ -2932,6 +2994,11 @@ snapshots:
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
@@ -2955,9 +3022,9 @@ snapshots:
unicorn-magic@0.3.0: {} unicorn-magic@0.3.0: {}
unocss@66.4.2(postcss@8.5.6)(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): unocss@66.4.2(postcss@8.5.6)(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
'@unocss/astro': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/astro': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
'@unocss/cli': 66.4.2 '@unocss/cli': 66.4.2
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/postcss': 66.4.2(postcss@8.5.6) '@unocss/postcss': 66.4.2(postcss@8.5.6)
@@ -2975,9 +3042,9 @@ snapshots:
'@unocss/transformer-compile-class': 66.4.2 '@unocss/transformer-compile-class': 66.4.2
'@unocss/transformer-directives': 66.4.2 '@unocss/transformer-directives': 66.4.2
'@unocss/transformer-variant-group': 66.4.2 '@unocss/transformer-variant-group': 66.4.2
'@unocss/vite': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/vite': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
optionalDependencies: optionalDependencies:
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
transitivePeerDependencies: transitivePeerDependencies:
- postcss - postcss
- supports-color - supports-color
@@ -3000,17 +3067,17 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
vite-dev-rpc@1.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-dev-rpc@1.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
birpc: 2.5.0 birpc: 2.5.0
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-hot-client: 2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-hot-client: 2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite-hot-client@2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-hot-client@2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-inspect@11.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-plugin-inspect@11.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
ansis: 4.1.0 ansis: 4.1.0
debug: 4.4.1 debug: 4.4.1
@@ -3020,27 +3087,27 @@ snapshots:
perfect-debounce: 1.0.0 perfect-debounce: 1.0.0
sirv: 3.0.1 sirv: 3.0.1
unplugin-utils: 0.2.5 unplugin-utils: 0.2.5
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-dev-rpc: 1.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-dev-rpc: 1.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-plugin-vue-devtools@8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)): vite-plugin-vue-devtools@8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)):
dependencies: dependencies:
'@vue/devtools-core': 8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) '@vue/devtools-core': 8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vue/devtools-kit': 8.0.0 '@vue/devtools-kit': 8.0.0
'@vue/devtools-shared': 8.0.0 '@vue/devtools-shared': 8.0.0
execa: 9.6.0 execa: 9.6.0
sirv: 3.0.1 sirv: 3.0.1
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-inspect: 11.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-plugin-inspect: 11.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite-plugin-vue-inspector: 5.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-plugin-vue-inspector: 5.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- supports-color - supports-color
- vue - vue
vite-plugin-vue-inspector@5.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-plugin-vue-inspector@5.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0) '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0)
@@ -3051,18 +3118,18 @@ snapshots:
'@vue/compiler-dom': 3.5.18 '@vue/compiler-dom': 3.5.18
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.17 magic-string: 0.30.17
vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0): vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0):
dependencies: dependencies:
esbuild: 0.25.8 esbuild: 0.25.8
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
postcss: 8.5.6 postcss: 8.5.6
rollup: 4.46.2 rollup: 4.46.2
tinyglobby: 0.2.14 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 22.17.1 '@types/node': 22.17.1
fsevents: 2.3.3 fsevents: 2.3.3

49
public/apps/README.md Normal file
View File

@@ -0,0 +1,49 @@
# 外部应用目录
此目录用于存放外部应用非内置Vue应用
## 目录结构
外部应用应该按以下结构组织:
```
public/apps/
├── app-name/
│ ├── index.html # 应用主页面
│ ├── manifest.json # 应用清单文件
│ └── ... # 其他应用文件
└── another-app/
├── index.html
├── manifest.json
└── ...
```
## 应用清单格式
每个外部应用都应该包含一个 `manifest.json` 文件:
```json
{
"id": "app-id",
"name": "应用名称",
"version": "1.0.0",
"description": "应用描述",
"author": "作者",
"icon": "图标URL或表情符号",
"permissions": ["storage", "notification"],
"window": {
"width": 800,
"height": 600,
"resizable": true
},
"category": "应用分类",
"keywords": ["关键词1", "关键词2"]
}
```
## 安全说明
外部应用将在iframe沙箱环境中运行具有以下限制
- 无法直接访问父页面
- 通过postMessage与系统通信
- 受到严格的权限控制

View File

@@ -0,0 +1,170 @@
# 音乐播放器 - 外置应用案例
这是一个完整的外置应用案例展示了如何在Vue桌面系统中开发外置应用。
## 功能特性
### 🎵 音乐播放功能
- **音频格式支持**: 支持所有浏览器兼容的音频格式MP3、WAV、OGG等
- **播放控制**: 播放、暂停、上一曲、下一曲
- **进度控制**: 可拖拽的进度条,支持跳转到任意位置
- **音量控制**: 音量滑块调节,实时显示音量百分比
### 🎲 播放模式
- **随机播放**: 支持随机播放模式切换
- **重复播放**: 三种重复模式(关闭、单曲循环、列表循环)
- **播放列表**: 完整的播放列表管理
### 🎨 用户界面
- **现代设计**: 采用渐变色彩和毛玻璃效果
- **响应式布局**: 支持窗口大小调整
- **直观操作**: 清晰的视觉反馈和状态提示
- **窗口控制**: 最小化、最大化、关闭按钮
### ⌨️ 交互体验
- **键盘快捷键**:
- `Space`: 播放/暂停
- `←/→`: 上一曲/下一曲
- `↑/↓`: 音量增减
- **拖拽支持**: 支持文件拖拽添加(计划中)
- **状态持久化**: 播放列表自动保存
### 🔧 系统集成
- **系统SDK集成**: 与主系统的无缝集成
- **窗口控制**: 通过系统API控制窗口状态
- **系统通知**: 状态变化的系统通知
- **数据存储**: 使用系统存储API保存用户数据
## 文件结构
```
music-player/
├── manifest.json # 应用清单文件
├── index.html # 主HTML页面
├── style.css # 样式文件
├── app.js # 主要逻辑文件
└── README.md # 说明文档
```
## 技术实现
### 应用清单 (manifest.json)
定义了应用的基本信息、权限要求、窗口配置等:
- 应用ID、名称、版本信息
- 窗口大小和行为配置
- 所需权限(存储、文件读取、通知)
- 分类和关键词
### 用户界面 (index.html + style.css)
- **布局结构**: 采用Flexbox布局分为头部、主要区域、播放列表和状态栏
- **样式设计**: 使用CSS渐变、阴影、过渡动画等现代效果
- **响应式**: 针对不同屏幕尺寸优化显示
### 应用逻辑 (app.js)
核心类 `MusicPlayer` 实现了所有功能:
#### 初始化流程
1. DOM就绪检测
2. 系统SDK连接
3. 事件监听器设置
4. 界面状态初始化
#### 音频处理
- 使用HTML5 Audio API
- 事件驱动的状态管理
- 错误处理和恢复机制
#### 播放列表管理
- 文件选择和验证
- 内存中的播放队列
- 持久化存储支持
## 系统SDK集成
该应用展示了如何正确使用系统SDK
### 窗口控制
```javascript
// 窗口操作
this.systemSDK.window.minimize();
this.systemSDK.window.toggleMaximize();
this.systemSDK.window.close();
// 关闭事件监听
this.systemSDK.window.onBeforeClose(() => {
this.cleanup();
return true;
});
```
### 系统通知
```javascript
this.systemSDK.notification.show({
title: '音乐播放器',
message: '应用已启动,准备播放音乐!',
type: 'info'
});
```
### 数据存储
```javascript
// 保存数据
this.systemSDK.storage.setItem('music-player-playlist', data);
// 读取数据
const data = this.systemSDK.storage.getItem('music-player-playlist');
```
## 安全考虑
- **文件访问**: 通过文件选择器安全地访问用户文件
- **内存管理**: 及时释放对象URL避免内存泄漏
- **错误处理**: 完整的错误捕获和用户友好的错误提示
- **权限控制**: 仅请求必要的系统权限
## 性能优化
- **懒加载**: 音频文件仅在需要时加载
- **事件防抖**: 避免频繁的状态更新
- **内存回收**: 应用关闭时清理所有资源
- **DOM优化**: 高效的DOM操作和事件委托
## 扩展可能
这个案例可以进一步扩展:
1. **音频可视化**: 添加频谱显示
2. **歌词显示**: 支持LRC歌词文件
3. **均衡器**: 音频效果控制
4. **在线音乐**: 集成在线音乐服务
5. **播放历史**: 记录播放统计
6. **主题切换**: 多种UI主题
## 开发指南
### 本地测试
1. 将整个文件夹放置到 `public/apps/` 目录下
2. 启动Vue桌面系统
3. 通过应用管理器安装或直接打开应用
### 调试技巧
- 使用浏览器开发者工具调试
- 检查控制台日志了解应用状态
- 利用系统SDK的调试功能
### 部署注意事项
- 确保所有文件路径正确
- 验证清单文件格式
- 测试在不同窗口大小下的表现
## 总结
这个音乐播放器应用是一个完整的外置应用开发示例,展示了:
- 如何构建功能完整的外置应用
- 系统SDK的正确使用方法
- 现代Web技术的应用
- 良好的用户体验设计
- 安全和性能的最佳实践
通过学习这个案例,开发者可以了解外置应用的完整开发流程,并以此为基础开发自己的应用。

View File

@@ -0,0 +1,751 @@
/**
* 音乐播放器 - 外置应用案例
* 展示了如何创建一个功能完整的外置应用
*/
class MusicPlayer {
constructor() {
// 应用状态
this.isPlaying = false;
this.currentTrackIndex = 0;
this.playlist = [];
this.isShuffleMode = false;
this.repeatMode = 'none'; // none, one, all
this.volume = 0.7;
// DOM 元素
this.audioPlayer = null;
this.playPauseBtn = null;
this.progressBar = null;
this.volumeBar = null;
this.playlist_element = null;
// 系统SDK
this.systemSDK = null;
this.init();
}
/**
* 初始化应用
*/
async init() {
console.log('[音乐播放器] 初始化开始');
// 等待DOM加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupApp());
} else {
this.setupApp();
}
// 初始化系统SDK
await this.initSystemSDK();
}
/**
* 初始化系统SDK
*/
async initSystemSDK() {
try {
console.log('[音乐播放器] 开始初始化系统SDK');
// 等待系统SDK可用
let attempts = 0;
const maxAttempts = 100; // 增加尝试次数
while (!window.SystemSDK && attempts < maxAttempts) {
console.log(`[音乐播放器] 等待SystemSDK可用... (尝试 ${attempts + 1}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
if (window.SystemSDK) {
console.log('[音乐播放器] SystemSDK对象已找到开始初始化');
// 初始化SDK
const initResult = await window.SystemSDK.init({
appId: 'music-player',
appName: '音乐播放器',
version: '1.0.0',
permissions: ['storage.read', 'storage.write']
});
if (initResult.success) {
this.systemSDK = window.SystemSDK;
console.log('[音乐播放器] 系统SDK初始化成功');
// 显示系统通知
if (this.systemSDK.ui) {
try {
await this.systemSDK.ui.showNotification({
title: '音乐播放器',
message: '应用已启动,准备播放音乐!',
type: 'info'
});
} catch (error) {
console.warn('[音乐播放器] 显示通知失败:', error);
}
}
// 从本地存储恢复播放列表
await this.loadPlaylistFromStorage();
} else {
console.error('[音乐播放器] 系统SDK初始化失败:', initResult.error);
}
} else {
console.error('[音乐播放器] 系统SDK不可用已达到最大尝试次数');
}
} catch (error) {
console.error('[音乐播放器] 系统SDK初始化失败:', error);
}
}
/**
* 设置应用界面和事件
*/
setupApp() {
console.log('[音乐播放器] 设置界面');
// 获取DOM元素
this.audioPlayer = document.getElementById('audioPlayer');
this.playPauseBtn = document.getElementById('playPauseBtn');
this.progressBar = document.getElementById('progressBar');
this.volumeBar = document.getElementById('volumeBar');
this.playlist_element = document.getElementById('playlist');
// 设置音频事件
this.setupAudioEvents();
// 设置控制按钮事件
this.setupControlEvents();
// 设置窗口控制事件
this.setupWindowControls();
// 设置键盘快捷键
this.setupKeyboardShortcuts();
// 初始化音量
this.setVolume(this.volume * 100);
console.log('[音乐播放器] 应用设置完成');
}
/**
* 设置音频播放器事件
*/
setupAudioEvents() {
if (!this.audioPlayer) return;
// 播放开始
this.audioPlayer.addEventListener('play', () => {
this.isPlaying = true;
this.updatePlayButton();
this.updateStatus('正在播放');
});
// 播放暂停
this.audioPlayer.addEventListener('pause', () => {
this.isPlaying = false;
this.updatePlayButton();
this.updateStatus('已暂停');
});
// 播放结束
this.audioPlayer.addEventListener('ended', () => {
this.handleTrackEnded();
});
// 时间更新
this.audioPlayer.addEventListener('timeupdate', () => {
this.updateProgress();
});
// 加载完成
this.audioPlayer.addEventListener('loadedmetadata', () => {
this.updateTotalTime();
});
// 加载错误
this.audioPlayer.addEventListener('error', (e) => {
console.error('[音乐播放器] 播放错误:', e);
this.updateStatus('播放出错');
this.nextTrack();
});
}
/**
* 设置控制按钮事件
*/
setupControlEvents() {
// 播放/暂停
document.getElementById('playPauseBtn')?.addEventListener('click', () => {
this.togglePlayPause();
});
// 上一曲
document.getElementById('prevBtn')?.addEventListener('click', () => {
this.prevTrack();
});
// 下一曲
document.getElementById('nextBtn')?.addEventListener('click', () => {
this.nextTrack();
});
// 随机播放
document.getElementById('shuffleBtn')?.addEventListener('click', () => {
this.toggleShuffle();
});
// 重复播放
document.getElementById('repeatBtn')?.addEventListener('click', () => {
this.toggleRepeat();
});
// 进度条
this.progressBar?.addEventListener('input', () => {
this.seekTo(this.progressBar.value);
});
// 音量控制
this.volumeBar?.addEventListener('input', () => {
this.setVolume(this.volumeBar.value);
});
// 文件选择
document.getElementById('addFilesBtn')?.addEventListener('click', () => {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput')?.addEventListener('change', (e) => {
this.handleFileSelection(e.target.files);
});
// 清空播放列表
document.getElementById('clearPlaylistBtn')?.addEventListener('click', () => {
this.clearPlaylist();
});
}
/**
* 设置窗口控制事件
*/
setupWindowControls() {
// 最小化
document.getElementById('minimizeBtn')?.addEventListener('click', () => {
if (this.systemSDK) {
this.systemSDK.window.minimize();
}
});
// 最大化/还原
document.getElementById('maximizeBtn')?.addEventListener('click', () => {
if (this.systemSDK) {
this.systemSDK.window.toggleMaximize();
}
});
// 关闭
document.getElementById('closeBtn')?.addEventListener('click', () => {
if (this.systemSDK) {
this.systemSDK.window.close();
} else {
window.close();
}
});
}
/**
* 设置键盘快捷键
*/
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
switch (e.code) {
case 'Space':
e.preventDefault();
this.togglePlayPause();
break;
case 'ArrowLeft':
e.preventDefault();
this.prevTrack();
break;
case 'ArrowRight':
e.preventDefault();
this.nextTrack();
break;
case 'ArrowUp':
e.preventDefault();
this.setVolume(Math.min(100, this.volume * 100 + 5));
break;
case 'ArrowDown':
e.preventDefault();
this.setVolume(Math.max(0, this.volume * 100 - 5));
break;
}
});
}
/**
* 处理文件选择
*/
handleFileSelection(files) {
const audioFiles = Array.from(files).filter(file =>
file.type.startsWith('audio/')
);
if (audioFiles.length === 0) {
this.updateStatus('未选择音频文件');
return;
}
audioFiles.forEach(file => {
const track = {
id: Date.now() + Math.random(),
name: file.name.replace(/\.[^/.]+$/, ""),
file: file,
url: URL.createObjectURL(file),
duration: 0
};
this.playlist.push(track);
});
this.updatePlaylist();
this.savePlaylistToStorage();
this.updateStatus(`添加了 ${audioFiles.length} 首歌曲`);
// 如果是第一次添加歌曲,自动播放
if (this.playlist.length === audioFiles.length) {
this.loadTrack(0);
}
}
/**
* 播放/暂停切换
*/
togglePlayPause() {
if (!this.audioPlayer || this.playlist.length === 0) {
this.updateStatus('播放列表为空');
return;
}
if (this.isPlaying) {
this.audioPlayer.pause();
} else {
this.audioPlayer.play().catch(error => {
console.error('[音乐播放器] 播放失败:', error);
this.updateStatus('播放失败');
});
}
}
/**
* 上一曲
*/
prevTrack() {
if (this.playlist.length === 0) return;
let newIndex;
if (this.isShuffleMode) {
newIndex = Math.floor(Math.random() * this.playlist.length);
} else {
newIndex = this.currentTrackIndex - 1;
if (newIndex < 0) {
newIndex = this.playlist.length - 1;
}
}
this.loadTrack(newIndex);
}
/**
* 下一曲
*/
nextTrack() {
if (this.playlist.length === 0) return;
let newIndex;
if (this.isShuffleMode) {
newIndex = Math.floor(Math.random() * this.playlist.length);
} else {
newIndex = this.currentTrackIndex + 1;
if (newIndex >= this.playlist.length) {
newIndex = 0;
}
}
this.loadTrack(newIndex);
}
/**
* 加载指定曲目
*/
loadTrack(index) {
if (index < 0 || index >= this.playlist.length) return;
this.currentTrackIndex = index;
const track = this.playlist[index];
this.audioPlayer.src = track.url;
this.updateCurrentTrackInfo(track);
this.updatePlaylistHighlight();
if (this.isPlaying) {
this.audioPlayer.play().catch(error => {
console.error('[音乐播放器] 播放失败:', error);
});
}
}
/**
* 处理曲目播放结束
*/
handleTrackEnded() {
switch (this.repeatMode) {
case 'one':
this.audioPlayer.currentTime = 0;
this.audioPlayer.play();
break;
case 'all':
this.nextTrack();
break;
default:
if (this.currentTrackIndex < this.playlist.length - 1) {
this.nextTrack();
} else {
this.isPlaying = false;
this.updatePlayButton();
this.updateStatus('播放完成');
}
break;
}
}
/**
* 切换随机播放
*/
toggleShuffle() {
this.isShuffleMode = !this.isShuffleMode;
const btn = document.getElementById('shuffleBtn');
if (btn) {
btn.classList.toggle('active', this.isShuffleMode);
}
this.updateStatus(this.isShuffleMode ? '随机播放已开启' : '随机播放已关闭');
}
/**
* 切换重复播放模式
*/
toggleRepeat() {
const modes = ['none', 'one', 'all'];
const currentIndex = modes.indexOf(this.repeatMode);
this.repeatMode = modes[(currentIndex + 1) % modes.length];
const btn = document.getElementById('repeatBtn');
if (btn) {
btn.classList.toggle('active', this.repeatMode !== 'none');
switch (this.repeatMode) {
case 'one':
btn.textContent = '🔂';
break;
case 'all':
btn.textContent = '🔁';
break;
default:
btn.textContent = '🔁';
break;
}
}
const modeNames = { none: '关闭', one: '单曲循环', all: '列表循环' };
this.updateStatus(`重复播放: ${modeNames[this.repeatMode]}`);
}
/**
* 设置音量
*/
setVolume(value) {
this.volume = value / 100;
if (this.audioPlayer) {
this.audioPlayer.volume = this.volume;
}
const volumeValue = document.getElementById('volumeValue');
if (volumeValue) {
volumeValue.textContent = `${Math.round(value)}%`;
}
if (this.volumeBar) {
this.volumeBar.value = value;
}
}
/**
* 跳转到指定时间
*/
seekTo(percentage) {
if (this.audioPlayer && this.audioPlayer.duration) {
const time = (percentage / 100) * this.audioPlayer.duration;
this.audioPlayer.currentTime = time;
}
}
/**
* 更新播放按钮状态
*/
updatePlayButton() {
if (this.playPauseBtn) {
this.playPauseBtn.textContent = this.isPlaying ? '⏸️' : '▶️';
this.playPauseBtn.classList.toggle('playing', this.isPlaying);
}
}
/**
* 更新进度条
*/
updateProgress() {
if (this.audioPlayer && this.progressBar && this.audioPlayer.duration) {
const progress = (this.audioPlayer.currentTime / this.audioPlayer.duration) * 100;
this.progressBar.value = progress;
const currentTime = document.getElementById('currentTime');
if (currentTime) {
currentTime.textContent = this.formatTime(this.audioPlayer.currentTime);
}
}
}
/**
* 更新总时长显示
*/
updateTotalTime() {
if (this.audioPlayer) {
const totalTime = document.getElementById('totalTime');
if (totalTime) {
totalTime.textContent = this.formatTime(this.audioPlayer.duration || 0);
}
}
}
/**
* 更新当前曲目信息
*/
updateCurrentTrackInfo(track) {
const titleElement = document.getElementById('trackTitle');
const artistElement = document.getElementById('trackArtist');
const albumElement = document.getElementById('trackAlbum');
if (titleElement) titleElement.textContent = track.name;
if (artistElement) artistElement.textContent = '未知艺术家';
if (albumElement) albumElement.textContent = '未知专辑';
}
/**
* 更新播放列表显示
*/
updatePlaylist() {
if (!this.playlist_element) return;
if (this.playlist.length === 0) {
this.playlist_element.innerHTML = '<li class="playlist-empty">暂无音乐文件</li>';
this.updateTrackCount();
return;
}
this.playlist_element.innerHTML = this.playlist.map((track, index) => `
<li class="playlist-item ${index === this.currentTrackIndex ? 'playing' : ''}"
data-index="${index}">
<span class="track-number">${index + 1}</span>
<div class="track-details">
<div class="track-name">${track.name}</div>
<div class="track-duration">${this.formatTime(track.duration)}</div>
</div>
</li>
`).join('');
// 添加点击事件
this.playlist_element.querySelectorAll('.playlist-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
this.loadTrack(index);
if (!this.isPlaying) {
this.togglePlayPause();
}
});
});
this.updateTrackCount();
}
/**
* 更新播放列表高亮
*/
updatePlaylistHighlight() {
if (!this.playlist_element) return;
this.playlist_element.querySelectorAll('.playlist-item').forEach((item, index) => {
item.classList.toggle('playing', index === this.currentTrackIndex);
});
}
/**
* 清空播放列表
*/
clearPlaylist() {
this.playlist.forEach(track => {
if (track.url) {
URL.revokeObjectURL(track.url);
}
});
this.playlist = [];
this.currentTrackIndex = 0;
this.isPlaying = false;
if (this.audioPlayer) {
this.audioPlayer.pause();
this.audioPlayer.src = '';
}
this.updatePlaylist();
this.updatePlayButton();
this.updateCurrentTrackInfo({ name: '选择音乐文件开始播放' });
this.updateStatus('播放列表已清空');
this.savePlaylistToStorage();
}
/**
* 更新状态栏
*/
updateStatus(message) {
const statusText = document.getElementById('statusText');
if (statusText) {
statusText.textContent = message;
}
}
/**
* 更新歌曲数量
*/
updateTrackCount() {
const trackCount = document.getElementById('trackCount');
if (trackCount) {
trackCount.textContent = `${this.playlist.length} 首歌曲`;
}
}
/**
* 格式化时间显示
*/
formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '00:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
/**
* 保存播放列表到本地存储
*/
savePlaylistToStorage() {
try {
const playlistData = this.playlist.map(track => ({
id: track.id,
name: track.name,
duration: track.duration
}));
// 使用系统SDK进行存储操作
if (this.systemSDK && this.systemSDK.storage) {
console.log('[音乐播放器] 保存播放列表到系统存储');
this.systemSDK.storage.set('music-player-playlist', JSON.stringify(playlistData))
.then(result => {
if (result.success) {
console.log('[音乐播放器] 播放列表保存成功');
} else {
console.warn('[音乐播放器] 保存播放列表到系统存储失败:', result.error);
}
})
.catch(error => {
console.error('[音乐播放器] 保存播放列表失败:', error);
});
} else {
console.warn('[音乐播放器] 系统SDK未初始化无法保存播放列表');
}
} catch (error) {
console.error('[音乐播放器] 保存播放列表失败:', error);
}
}
/**
* 从本地存储加载播放列表
*/
async loadPlaylistFromStorage() {
try {
// 使用系统SDK进行存储操作
if (this.systemSDK && this.systemSDK.storage) {
console.log('[音乐播放器] 从系统存储加载播放列表');
const result = await this.systemSDK.storage.get('music-player-playlist');
if (result.success && result.data) {
try {
const playlistData = JSON.parse(result.data);
console.log(`[音乐播放器] 从系统存储加载了 ${playlistData.length} 首歌曲`);
// 这里可以恢复播放列表
} catch (parseError) {
console.warn('[音乐播放器] 解析播放列表数据失败:', parseError);
}
} else if (!result.success) {
console.warn('[音乐播放器] 从系统存储加载播放列表失败:', result.error);
}
} else {
console.warn('[音乐播放器] 系统SDK未初始化无法加载播放列表');
}
} catch (error) {
console.error('[音乐播放器] 加载播放列表失败:', error);
}
}
/**
* 清理资源
*/
cleanup() {
console.log('[音乐播放器] 清理资源');
// 暂停播放
if (this.audioPlayer) {
this.audioPlayer.pause();
}
// 释放对象URL
this.playlist.forEach(track => {
if (track.url) {
URL.revokeObjectURL(track.url);
}
});
// 保存状态
this.savePlaylistToStorage();
}
}
// 应用启动
let musicPlayerApp;
// 确保在DOM加载完成后启动应用
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
musicPlayerApp = new MusicPlayer();
});
} else {
musicPlayerApp = new MusicPlayer();
}
// 导出供外部使用
window.MusicPlayerApp = musicPlayerApp;

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐播放器</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="music-player">
<!-- 头部标题栏 -->
<header class="player-header">
<h1>🎵 音乐播放器</h1>
<div class="header-controls">
<button id="minimizeBtn" class="control-btn"></button>
<button id="maximizeBtn" class="control-btn">🔲</button>
<button id="closeBtn" class="control-btn"></button>
</div>
</header>
<!-- 主要播放区域 -->
<main class="player-main">
<!-- 当前播放信息 -->
<section class="current-track">
<div class="track-art">
<img id="trackImage" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkY2NzMxIi8+CjxwYXRoIGQ9Ik0zNSA3NVYyNUw2NSA1MEwzNSA3NVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==" alt="音乐封面">
</div>
<div class="track-info">
<h2 id="trackTitle">选择音乐文件开始播放</h2>
<p id="trackArtist">未知艺术家</p>
<p id="trackAlbum">未知专辑</p>
</div>
</section>
<!-- 进度控制 -->
<section class="progress-section">
<div class="time-display">
<span id="currentTime">00:00</span>
<input type="range" id="progressBar" min="0" max="100" value="0" class="progress-bar">
<span id="totalTime">00:00</span>
</div>
</section>
<!-- 播放控制 -->
<section class="controls">
<button id="shuffleBtn" class="control-btn secondary">🔀</button>
<button id="prevBtn" class="control-btn">⏮️</button>
<button id="playPauseBtn" class="control-btn primary">▶️</button>
<button id="nextBtn" class="control-btn">⏭️</button>
<button id="repeatBtn" class="control-btn secondary">🔁</button>
</section>
<!-- 音量控制 -->
<section class="volume-section">
<span class="volume-icon">🔊</span>
<input type="range" id="volumeBar" min="0" max="100" value="70" class="volume-bar">
<span id="volumeValue">70%</span>
</section>
</main>
<!-- 播放列表 -->
<aside class="playlist-section">
<div class="playlist-header">
<h3>播放列表</h3>
<div class="playlist-controls">
<input type="file" id="fileInput" accept="audio/*" multiple style="display: none;">
<button id="addFilesBtn" class="btn-secondary">添加文件</button>
<button id="clearPlaylistBtn" class="btn-secondary">清空列表</button>
</div>
</div>
<ul id="playlist" class="playlist">
<li class="playlist-empty">暂无音乐文件</li>
</ul>
</aside>
<!-- 状态栏 -->
<footer class="status-bar">
<span id="statusText">就绪</span>
<span id="trackCount">0 首歌曲</span>
</footer>
</div>
<!-- 隐藏的音频元素 -->
<audio id="audioPlayer" preload="none"></audio>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
{
"id": "music-player",
"name": "音乐播放器",
"version": "1.0.0",
"description": "一个功能丰富的音乐播放器应用,支持播放本地音乐文件",
"author": "外置应用开发者",
"entryPoint": "index.html",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iMjIiIGZpbGw9IiNGRjY3MzEiLz4KPHBhdGggZD0iTTE5IDMyVjE2TDMxIDI0TDE5IDMyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+",
"permissions": [
"storage.read",
"storage.write",
"file.read",
"system.notification"
],
"window": {
"width": 600,
"height": 400,
"minWidth": 400,
"minHeight": 300,
"resizable": true,
"minimizable": true,
"maximizable": true,
"closable": true
},
"category": "多媒体",
"keywords": ["音乐", "播放器", "媒体", "音频"]
}

View File

@@ -0,0 +1,430 @@
/* 音乐播放器样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
overflow: hidden;
height: 100vh;
}
.music-player {
display: flex;
flex-direction: column;
height: 100vh;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
/* 头部标题栏 */
.player-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background: linear-gradient(90deg, #FF6B35, #F7931E);
color: white;
user-select: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.player-header h1 {
font-size: 18px;
font-weight: 600;
}
.header-controls {
display: flex;
gap: 8px;
}
.header-controls .control-btn {
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.header-controls .control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
/* 主播放区域 */
.player-main {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
}
/* 当前播放信息 */
.current-track {
display: flex;
align-items: center;
gap: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.track-art {
flex-shrink: 0;
}
.track-art img {
width: 80px;
height: 80px;
border-radius: 8px;
object-fit: cover;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.track-info h2 {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
color: #2c3e50;
}
.track-info p {
font-size: 14px;
color: #7f8c8d;
margin-bottom: 3px;
}
/* 进度控制 */
.progress-section {
background: rgba(255, 255, 255, 0.7);
padding: 15px 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.time-display {
display: flex;
align-items: center;
gap: 15px;
}
.time-display span {
font-size: 12px;
color: #666;
font-weight: 500;
min-width: 40px;
}
.progress-bar {
flex: 1;
height: 6px;
border-radius: 3px;
background: #e1e1e1;
outline: none;
cursor: pointer;
appearance: none;
}
.progress-bar::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #FF6B35;
cursor: pointer;
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
transition: all 0.2s ease;
}
.progress-bar::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 3px 8px rgba(255, 107, 53, 0.4);
}
.progress-bar::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #FF6B35;
cursor: pointer;
border: none;
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
}
/* 播放控制 */
.controls {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.control-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 12px;
border-radius: 50%;
transition: all 0.2s ease;
user-select: none;
}
.control-btn:hover {
background: rgba(255, 107, 53, 0.1);
transform: scale(1.1);
}
.control-btn.primary {
background: linear-gradient(135deg, #FF6B35, #F7931E);
color: white;
font-size: 32px;
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
}
.control-btn.primary:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
}
.control-btn.secondary {
font-size: 18px;
color: #666;
}
.control-btn.active {
color: #FF6B35;
background: rgba(255, 107, 53, 0.1);
}
/* 音量控制 */
.volume-section {
display: flex;
align-items: center;
gap: 10px;
padding: 15px 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.volume-icon {
font-size: 18px;
color: #666;
}
.volume-bar {
flex: 1;
height: 4px;
border-radius: 2px;
background: #e1e1e1;
outline: none;
cursor: pointer;
appearance: none;
}
.volume-bar::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #FF6B35;
cursor: pointer;
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
}
.volume-bar::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #FF6B35;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
}
#volumeValue {
font-size: 12px;
color: #666;
min-width: 30px;
}
/* 播放列表 */
.playlist-section {
background: rgba(255, 255, 255, 0.9);
border-top: 1px solid rgba(0, 0, 0, 0.05);
max-height: 200px;
display: flex;
flex-direction: column;
}
.playlist-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.playlist-header h3 {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.playlist-controls {
display: flex;
gap: 10px;
}
.btn-secondary {
padding: 6px 12px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
color: #666;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: #e9ecef;
border-color: #adb5bd;
}
.playlist {
flex: 1;
overflow-y: auto;
list-style: none;
}
.playlist-item {
display: flex;
align-items: center;
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.playlist-item:hover {
background: rgba(255, 107, 53, 0.1);
}
.playlist-item.playing {
background: rgba(255, 107, 53, 0.15);
color: #FF6B35;
font-weight: 500;
}
.playlist-item .track-number {
font-size: 12px;
color: #999;
min-width: 20px;
margin-right: 15px;
}
.playlist-item .track-details {
flex: 1;
}
.playlist-item .track-name {
font-size: 14px;
margin-bottom: 2px;
}
.playlist-item .track-duration {
font-size: 11px;
color: #999;
}
.playlist-empty {
padding: 20px;
text-align: center;
color: #999;
font-style: italic;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background: #f8f9fa;
border-top: 1px solid rgba(0, 0, 0, 0.05);
font-size: 12px;
color: #666;
}
/* 响应式设计 */
@media (max-width: 500px) {
.current-track {
flex-direction: column;
text-align: center;
}
.track-art img {
width: 60px;
height: 60px;
}
.controls {
gap: 15px;
}
.control-btn {
font-size: 20px;
padding: 10px;
}
.control-btn.primary {
font-size: 28px;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #999;
}
/* 动画效果 */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.control-btn.primary.playing {
animation: pulse 2s infinite;
}

View File

@@ -1,18 +0,0 @@
<template>
<div id="root" ref="root-dom" class="w-100vw h-100vh"></div>
</template>
<script setup lang="ts">
import XSystem from '@/core/XSystem.ts'
import { onMounted, useTemplateRef } from 'vue'
const dom = useTemplateRef<HTMLDivElement>('root-dom')
onMounted(() => {
XSystem.instance.initialization(dom.value!);
})
</script>
<style scoped>
</style>

84
src/apps/AppRegistry.ts Normal file
View File

@@ -0,0 +1,84 @@
import type { AppRegistration } from './types/AppManifest'
import { reactive } from 'vue'
/**
* 应用注册中心
* 管理所有内置应用的注册和获取
*/
export class AppRegistry {
private apps = reactive(new Map<string, AppRegistration>())
/**
* 注册内置应用
*/
registerApp(registration: AppRegistration): void {
// 使用 markRaw 标记组件,避免被设为响应式
// 注意对于异步组件我们不立即标记为raw而是在实际加载时处理
const safeRegistration = {
...registration,
component: registration.component
}
this.apps.set(registration.manifest.id, safeRegistration)
console.log(`已注册内置应用: ${registration.manifest.name}`)
}
/**
* 获取应用
*/
getApp(appId: string): AppRegistration | undefined {
return this.apps.get(appId)
}
/**
* 获取所有应用
*/
getAllApps(): AppRegistration[] {
return Array.from(this.apps.values())
}
/**
* 获取所有内置应用
*/
getBuiltInApps(): AppRegistration[] {
return Array.from(this.apps.values()).filter((app) => app.isBuiltIn)
}
/**
* 检查应用是否存在
*/
hasApp(appId: string): boolean {
return this.apps.has(appId)
}
/**
* 按类别获取应用
*/
getAppsByCategory(category: string): AppRegistration[] {
return Array.from(this.apps.values()).filter((app) => app.manifest.category === category)
}
/**
* 搜索应用
*/
searchApps(query: string): AppRegistration[] {
const lowercaseQuery = query.toLowerCase()
return Array.from(this.apps.values()).filter((app) => {
const manifest = app.manifest
return (
manifest.name.toLowerCase().includes(lowercaseQuery) ||
manifest.description.toLowerCase().includes(lowercaseQuery) ||
manifest.keywords?.some((keyword) => keyword.toLowerCase().includes(lowercaseQuery))
)
})
}
/**
* 清空所有应用
*/
clear(): void {
this.apps.clear()
}
}
// 导出单例实例
export const appRegistry = new AppRegistry()

View File

@@ -0,0 +1,344 @@
<template>
<BuiltInApp app-id="calculator" title="计算器">
<div class="calculator">
<div class="display">
<input v-model="displayValue" type="text" readonly class="display-input" :class="{ 'error': hasError }">
</div>
<div class="buttons">
<!-- 第一行 -->
<button @click="clear" class="btn btn-clear">C</button>
<button @click="deleteLast" class="btn btn-operation"></button>
<button @click="appendOperation('/')" class="btn btn-operation">÷</button>
<button @click="appendOperation('*')" class="btn btn-operation">×</button>
<!-- 第二行 -->
<button @click="appendNumber('7')" class="btn btn-number">7</button>
<button @click="appendNumber('8')" class="btn btn-number">8</button>
<button @click="appendNumber('9')" class="btn btn-number">9</button>
<button @click="appendOperation('-')" class="btn btn-operation">-</button>
<!-- 第三行 -->
<button @click="appendNumber('4')" class="btn btn-number">4</button>
<button @click="appendNumber('5')" class="btn btn-number">5</button>
<button @click="appendNumber('6')" class="btn btn-number">6</button>
<button @click="appendOperation('+')" class="btn btn-operation">+</button>
<!-- 第四行 -->
<button @click="appendNumber('1')" class="btn btn-number">1</button>
<button @click="appendNumber('2')" class="btn btn-number">2</button>
<button @click="appendNumber('3')" class="btn btn-number">3</button>
<button @click="calculate" class="btn btn-equals" rowspan="2">=</button>
<!-- 第五行 -->
<button @click="appendNumber('0')" class="btn btn-number btn-zero">0</button>
<button @click="appendNumber('.')" class="btn btn-number">.</button>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, onMounted, inject } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
// 直接获取系统服务 - 无需通过SDK
const systemService = inject<SystemServiceIntegration>('systemService')
const displayValue = ref('0')
const hasError = ref(false)
const lastResult = ref<number | null>(null)
const shouldResetDisplay = ref(false)
// 直接使用系统存储服务保存历史记录
const saveHistory = async (expression: string, result: string) => {
try {
if (systemService) {
const resourceService = systemService.getResourceService()
const history = await resourceService.getStorage('calculator', 'history') || []
history.push({
expression,
result,
timestamp: new Date().toISOString()
})
// 只保存最近30条记录
if (history.length > 30) {
history.shift()
}
await resourceService.setStorage('calculator', 'history', history)
}
} catch (error) {
console.error('保存历史记录失败:', error)
}
}
// 移除事件服务相关代码
// // 直接使用事件服务发送通知
// const showNotification = (message: string) => {
// if (systemService) {
// const eventService = systemService.getEventService()
// eventService.sendMessage('calculator', 'user-interaction', {
// type: 'notification',
// message,
// timestamp: new Date()
// })
// }
// }
// 添加数字
const appendNumber = (num: string) => {
hasError.value = false
if (shouldResetDisplay.value) {
displayValue.value = '0'
shouldResetDisplay.value = false
}
if (num === '.') {
if (!displayValue.value.includes('.')) {
displayValue.value += num
}
} else {
if (displayValue.value === '0') {
displayValue.value = num
} else {
displayValue.value += num
}
}
}
// 添加运算符
const appendOperation = (op: string) => {
hasError.value = false
shouldResetDisplay.value = false
const lastChar = displayValue.value.slice(-1)
const operations = ['+', '-', '*', '/']
// 如果最后一个字符是运算符,替换它
if (operations.includes(lastChar)) {
displayValue.value = displayValue.value.slice(0, -1) + op
} else {
displayValue.value += op
}
}
// 计算结果
const calculate = async () => {
try {
hasError.value = false
let expression = displayValue.value
.replace(/×/g, '*')
.replace(/÷/g, '/')
// 简单的表达式验证
if (/[+\-*/]$/.test(expression)) {
return // 以运算符结尾,不计算
}
const originalExpression = displayValue.value
const result = eval(expression)
if (!isFinite(result)) {
throw new Error('除零错误')
}
displayValue.value = result.toString()
lastResult.value = result
shouldResetDisplay.value = true
// 保存历史记录
await saveHistory(originalExpression, result.toString())
// 移除事件服务相关代码
// // 发送通知
// showNotification(`计算结果: ${result}`)
} catch (error) {
hasError.value = true
displayValue.value = '错误'
setTimeout(() => {
clear()
}, 1000)
}
}
// 清空
const clear = () => {
displayValue.value = '0'
hasError.value = false
lastResult.value = null
shouldResetDisplay.value = false
}
// 删除最后一个字符
const deleteLast = () => {
hasError.value = false
if (shouldResetDisplay.value) {
clear()
return
}
if (displayValue.value.length > 1) {
displayValue.value = displayValue.value.slice(0, -1)
} else {
displayValue.value = '0'
}
}
// 键盘事件处理
const handleKeyboard = (event: KeyboardEvent) => {
event.preventDefault()
const key = event.key
if (/[0-9.]/.test(key)) {
appendNumber(key)
} else if (['+', '-', '*', '/'].includes(key)) {
appendOperation(key)
} else if (key === 'Enter' || key === '=') {
calculate()
} else if (key === 'Escape' || key === 'c' || key === 'C') {
clear()
} else if (key === 'Backspace') {
deleteLast()
}
}
onMounted(() => {
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyboard)
})
</script>
<style scoped>
.calculator {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
max-width: 320px;
margin: 20px auto;
}
.display {
margin-bottom: 20px;
}
.display-input {
width: 100%;
height: 80px;
font-size: 32px;
text-align: right;
padding: 0 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: #f8f9fa;
color: #333;
outline: none;
font-family: 'Courier New', monospace;
box-sizing: border-box;
}
.display-input.error {
color: #e74c3c;
border-color: #e74c3c;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 12px;
height: 320px;
}
.btn {
border: none;
border-radius: 8px;
font-size: 20px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
display: flex;
align-items: center;
justify-content: center;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
}
.btn-number {
background: #ffffff;
color: #333;
border: 2px solid #e0e0e0;
}
.btn-number:hover {
background: #f8f9fa;
border-color: #007bff;
}
.btn-operation {
background: #007bff;
color: white;
}
.btn-operation:hover {
background: #0056b3;
}
.btn-clear {
background: #dc3545;
color: white;
}
.btn-clear:hover {
background: #c82333;
}
.btn-equals {
background: #28a745;
color: white;
grid-row: span 2;
}
.btn-equals:hover {
background: #218838;
}
.btn-zero {
grid-column: span 2;
}
/* 响应式设计 */
@media (max-width: 400px) {
.calculator {
margin: 10px;
padding: 15px;
}
.display-input {
height: 60px;
font-size: 24px;
}
.buttons {
height: 280px;
gap: 8px;
}
.btn {
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<div class="built-in-app" :class="[`app-${appId}`, { 'fullscreen': isFullscreen }]">
<div class="app-container">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { inject, onMounted, onUnmounted, ref, provide } from 'vue'
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
const props = defineProps<{
appId: string
title?: string
}>()
const isFullscreen = ref(false)
const systemService = inject<SystemServiceIntegration>('systemService')
// 为子组件提供应用上下文
const appContext = {
appId: props.appId,
title: props.title || props.appId,
systemService,
// 直接暴露系统服务的方法,简化调用
storage: systemService?.getResourceService(),
// 移除 events: systemService?.getEventService(),
lifecycle: systemService?.getLifecycleManager(),
window: {
setTitle: (title: string) => {
if (window.SystemSDK) {
window.SystemSDK.window.setTitle(title)
}
},
toggleFullscreen: () => {
isFullscreen.value = !isFullscreen.value
}
}
}
// 提供应用上下文给子组件
provide('appContext', appContext)
// SDK初始化 - 简化版本
onMounted(async () => {
try {
if (window.SystemSDK) {
await window.SystemSDK.init({
appId: props.appId,
appName: props.title || props.appId,
version: '1.0.0',
permissions: ['storage', 'notification']
})
if (props.title) {
await window.SystemSDK.window.setTitle(props.title)
}
console.log(`内置应用 ${props.appId} 初始化成功`)
}
} catch (error) {
console.error(`内置应用 ${props.appId} 初始化失败:`, error)
}
})
// 清理
onUnmounted(() => {
if (window.SystemSDK && window.SystemSDK.initialized) {
window.SystemSDK.destroy?.()
}
})
// 切换全屏模式
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
}
// 暴露方法给父组件
defineExpose({
toggleFullscreen,
appContext
})
</script>
<style scoped>
.built-in-app {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
}
/* 应用特定样式 */
.app-calculator {
max-width: 400px;
margin: 0 auto;
}
.app-notepad {
padding: 10px;
}
.app-todo {
/* 待办事项应用样式 */
}
</style>

102
src/apps/index.ts Normal file
View File

@@ -0,0 +1,102 @@
import { appRegistry } from './AppRegistry'
import { markRaw } from 'vue'
/**
* 注册所有内置应用
*/
export function registerBuiltInApps() {
// 注册计算器应用
appRegistry.registerApp({
manifest: {
id: 'calculator',
name: '计算器',
version: '1.0.0',
description: '简单而功能强大的计算器,支持基本数学运算',
author: 'System',
icon: '🧮',
permissions: ['storage'],
window: {
title: '计算器',
width: 400,
height: 600,
minWidth: 320,
minHeight: 480,
resizable: true,
minimizable: true,
maximizable: false,
},
category: 'utilities',
keywords: ['计算器', '数学', '运算', 'calculator', 'math'],
},
// 使用动态导入实现懒加载
component: async () => {
const { default: Calculator } = await import('./calculator/Calculator.vue')
return markRaw(Calculator)
},
isBuiltIn: true,
})
// 注册记事本应用
appRegistry.registerApp({
manifest: {
id: 'notepad',
name: '记事本',
version: '1.0.0',
description: '功能丰富的文本编辑器,支持文件管理和多种编辑选项',
author: 'System',
icon: '📝',
permissions: ['storage', 'notification'],
window: {
title: '记事本',
width: 800,
height: 600,
minWidth: 400,
minHeight: 300,
resizable: true,
},
category: 'productivity',
keywords: ['记事本', '文本编辑', '笔记', 'notepad', 'text', 'editor'],
},
// 使用动态导入实现懒加载
component: async () => {
const { default: Notepad } = await import('./notepad/Notepad.vue')
return markRaw(Notepad)
},
isBuiltIn: true,
})
// 注册待办事项应用
appRegistry.registerApp({
manifest: {
id: 'todo',
name: '待办事项',
version: '1.0.0',
description: '高效的任务管理工具,帮助您组织和跟踪日常任务',
author: 'System',
icon: '✅',
permissions: ['storage', 'notification'],
window: {
title: '待办事项',
width: 600,
height: 700,
minWidth: 400,
minHeight: 500,
resizable: true,
},
category: 'productivity',
keywords: ['待办事项', '任务管理', 'todo', 'task', 'productivity'],
},
// 使用动态导入实现懒加载
component: async () => {
const { default: Todo } = await import('./todo/Todo.vue')
return markRaw(Todo)
},
isBuiltIn: true,
})
console.log('内置应用注册完成')
}
// 导出应用注册中心
export { appRegistry } from './AppRegistry'
export type { InternalAppManifest, AppRegistration } from './types/AppManifest'

View File

@@ -0,0 +1,527 @@
<template>
<BuiltInApp app-id="notepad" title="记事本">
<div class="notepad">
<div class="toolbar">
<div class="toolbar-group">
<button @click="newFile" class="btn btn-secondary">
<span class="icon">📄</span>
新建
</button>
<button @click="saveFile" class="btn btn-primary" :disabled="!hasChanges">
<span class="icon">💾</span>
保存
</button>
<button @click="openFile" class="btn btn-secondary">
<span class="icon">📂</span>
打开
</button>
</div>
<div class="toolbar-group">
<select v-model="fontSize" @change="updateFontSize" class="font-size-select">
<option value="12">12px</option>
<option value="14">14px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
<option value="24">24px</option>
</select>
<button
@click="toggleWordWrap"
class="btn btn-secondary"
:class="{ active: wordWrap }"
>
<span class="icon">📄</span>
自动换行
</button>
</div>
<div class="file-info">
<span class="filename">{{ currentFileName }}</span>
<span v-if="hasChanges" class="unsaved">*</span>
</div>
</div>
<div class="editor-container">
<textarea
ref="editorRef"
v-model="content"
class="editor"
:style="editorStyle"
placeholder="开始输入文本..."
@input="onContentChange"
@keydown="handleKeydown"
></textarea>
</div>
<div class="status-bar">
<div class="status-left">
<span>: {{ currentLine }}</span>
<span>: {{ currentColumn }}</span>
<span>字符数: {{ characterCount }}</span>
</div>
<div class="status-right">
<span>{{ fileEncoding }}</span>
<span>{{ lastSaved ? `上次保存: ${formatTime(lastSaved)}` : '未保存' }}</span>
</div>
</div>
</div>
<!-- 文件列表对话框 -->
<div v-if="showFileList" class="modal-overlay" @click="showFileList = false">
<div class="modal" @click.stop>
<h3>选择文件</h3>
<div class="file-list">
<div
v-for="file in savedFiles"
:key="file.name"
class="file-item"
@click="loadFile(file.name)"
>
<span class="file-icon">📄</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-date">{{ formatTime(file.date) }}</span>
<button @click.stop="deleteFile(file.name)" class="btn-delete">🗑</button>
</div>
</div>
<div class="modal-actions">
<button @click="showFileList = false" class="btn btn-secondary">取消</button>
</div>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
const editorRef = ref<HTMLTextAreaElement>()
const content = ref('')
const currentFileName = ref('新文档.txt')
const hasChanges = ref(false)
const fontSize = ref('16')
const wordWrap = ref(true)
const lastSaved = ref<Date | null>(null)
const fileEncoding = ref('UTF-8')
const showFileList = ref(false)
const savedFiles = ref<Array<{name: string, date: Date}>>([])
// 计算属性
const editorStyle = computed(() => ({
fontSize: fontSize.value + 'px',
whiteSpace: wordWrap.value ? 'pre-wrap' : 'pre'
}))
const characterCount = computed(() => content.value.length)
const currentLine = ref(1)
const currentColumn = ref(1)
// 获取光标位置
const updateCursorPosition = () => {
if (!editorRef.value) return
const textarea = editorRef.value
const text = textarea.value
const position = textarea.selectionStart
const lines = text.substring(0, position).split('\n')
currentLine.value = lines.length
currentColumn.value = lines[lines.length - 1].length + 1
}
// 内容变化处理
const onContentChange = () => {
hasChanges.value = true
updateCursorPosition()
}
// 新建文件
const newFile = async () => {
if (hasChanges.value) {
if (confirm('当前文档未保存,是否保存?')) {
await saveFile()
}
}
content.value = ''
currentFileName.value = '新文档.txt'
hasChanges.value = false
lastSaved.value = null
updateTitle()
}
// 保存文件
const saveFile = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.set(`notepad_${currentFileName.value}`, content.value)
hasChanges.value = false
lastSaved.value = new Date()
// 更新文件列表
await updateFilesList()
// 显示通知
if (window.SystemSDK.ui?.showNotification) {
await window.SystemSDK.ui.showNotification({
title: '保存成功',
body: `文件 "${currentFileName.value}" 已保存`,
duration: 2000
})
}
} else {
alert('系统SDK未初始化无法保存文件')
}
} catch (error) {
console.error('保存文件失败:', error)
alert('保存文件失败: ' + (error as Error).message)
}
}
// 打开文件
const openFile = async () => {
await updateFilesList()
showFileList.value = true
}
// 加载文件
const loadFile = async (fileName: string) => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get(`notepad_${fileName}`)
if (result.success && result.data) {
content.value = result.data
currentFileName.value = fileName
hasChanges.value = false
lastSaved.value = new Date()
showFileList.value = false
updateTitle()
} else {
alert('文件不存在或为空')
}
}
} catch (error) {
console.error('打开文件失败:', error)
alert('打开文件失败: ' + (error as Error).message)
}
}
// 删除文件
const deleteFile = async (fileName: string) => {
if (confirm(`确定要删除文件 "${fileName}" 吗?`)) {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.remove(`notepad_${fileName}`)
await updateFilesList()
}
} catch (error) {
console.error('删除文件失败:', error)
alert('删除文件失败: ' + (error as Error).message)
}
}
}
// 更新文件列表
const updateFilesList = async () => {
// 这里需要实现获取所有以 notepad_ 开头的存储键
// 由于当前SDK没有提供列出所有键的功能我们先使用一个简化的实现
savedFiles.value = []
// 临时实现:尝试加载一些常见的文件名
const commonNames = ['新文档.txt', '笔记.txt', '备忘录.txt', '临时.txt']
for (const name of commonNames) {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get(`notepad_${name}`)
if (result.success && result.data) {
savedFiles.value.push({
name,
date: new Date() // 暂时使用当前时间
})
}
}
} catch (error) {
// 忽略错误
}
}
}
// 更新字体大小
const updateFontSize = () => {
if (editorRef.value) {
editorRef.value.focus()
}
}
// 切换自动换行
const toggleWordWrap = () => {
wordWrap.value = !wordWrap.value
if (editorRef.value) {
editorRef.value.focus()
}
}
// 更新标题
const updateTitle = () => {
if (window.SystemSDK && window.SystemSDK.initialized) {
const title = `记事本 - ${currentFileName.value}${hasChanges.value ? ' *' : ''}`
window.SystemSDK.window.setTitle(title)
}
}
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleString('zh-CN')
}
// 处理键盘快捷键
const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey) {
switch (event.key) {
case 's':
event.preventDefault()
saveFile()
break
case 'n':
event.preventDefault()
newFile()
break
case 'o':
event.preventDefault()
openFile()
break
}
}
// 更新光标位置
nextTick(() => {
updateCursorPosition()
})
}
onMounted(() => {
// 初始化光标位置
updateCursorPosition()
// 监听编辑器点击和键盘事件
if (editorRef.value) {
editorRef.value.addEventListener('click', updateCursorPosition)
editorRef.value.addEventListener('keyup', updateCursorPosition)
}
})
</script>
<style scoped>
.notepad {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
flex-wrap: wrap;
gap: 10px;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
background: #e9ecef;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #007bff;
color: white;
border-color: #007bff;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn.active {
background: #007bff;
color: white;
}
.font-size-select {
padding: 4px 8px;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
}
.file-info {
display: flex;
align-items: center;
font-size: 12px;
color: #6c757d;
}
.filename {
font-weight: 500;
}
.unsaved {
color: #dc3545;
margin-left: 4px;
}
.editor-container {
flex: 1;
position: relative;
}
.editor {
width: 100%;
height: 100%;
border: none;
padding: 16px;
font-family: 'Courier New', 'Consolas', monospace;
font-size: 16px;
line-height: 1.5;
resize: none;
outline: none;
background: white;
color: #333;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
font-size: 11px;
color: #6c757d;
}
.status-left,
.status-right {
display: flex;
gap: 12px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
padding: 20px;
min-width: 400px;
max-width: 600px;
max-height: 80vh;
overflow: auto;
}
.modal h3 {
margin: 0 0 16px 0;
color: #333;
}
.file-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 4px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: background 0.2s;
}
.file-item:hover {
background: #f8f9fa;
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
margin-right: 8px;
font-size: 16px;
}
.file-name {
flex: 1;
font-weight: 500;
}
.file-date {
font-size: 11px;
color: #6c757d;
margin-right: 8px;
}
.btn-delete {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.2s;
}
.btn-delete:hover {
opacity: 1;
}
.modal-actions {
margin-top: 16px;
text-align: right;
}
.icon {
font-size: 14px;
}
</style>

658
src/apps/todo/Todo.vue Normal file
View File

@@ -0,0 +1,658 @@
<template>
<BuiltInApp app-id="todo" title="待办事项">
<div class="todo-app">
<div class="header">
<h1>📝 待办事项</h1>
<div class="stats">
总计: <span class="count">{{ todos.length }}</span> |
已完成: <span class="count">{{ completedCount }}</span> |
待办: <span class="count">{{ pendingCount }}</span>
</div>
</div>
<div class="add-todo">
<div class="input-group">
<input
v-model="newTodoText"
type="text"
class="todo-input"
placeholder="添加新的待办事项..."
maxlength="200"
@keypress.enter="addTodo"
>
<button @click="addTodo" class="add-btn" :disabled="!newTodoText.trim()">
添加
</button>
</div>
</div>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.key"
class="filter-btn"
:class="{ active: currentFilter === filter.key }"
@click="setFilter(filter.key)"
>
{{ filter.label }}
</button>
</div>
<div class="todo-list" v-if="filteredTodos.length > 0">
<TransitionGroup name="todo-list" tag="div">
<div
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
:class="{ 'completed': todo.completed }"
>
<input
type="checkbox"
class="todo-checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
>
<div class="todo-content" v-if="!todo.editing">
<span class="todo-text" :class="{ 'completed': todo.completed }">
{{ todo.text }}
</span>
<span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
</div>
<div class="todo-edit" v-else>
<input
v-model="todo.editText"
type="text"
class="edit-input"
@keypress.enter="saveEdit(todo)"
@keyup.esc="cancelEdit(todo)"
@blur="saveEdit(todo)"
ref="editInputs"
>
</div>
<div class="todo-actions">
<button
v-if="!todo.editing"
@click="startEdit(todo)"
class="edit-btn"
title="编辑"
>
</button>
<button
@click="deleteTodo(todo.id)"
class="delete-btn"
title="删除"
>
🗑
</button>
</div>
</div>
</TransitionGroup>
</div>
<div v-else class="empty-state">
<div class="empty-icon">📋</div>
<h3>{{ getEmptyMessage() }}</h3>
<p>{{ getEmptyDescription() }}</p>
</div>
<!-- 批量操作 -->
<div class="bulk-actions" v-if="todos.length > 0">
<button @click="markAllCompleted" class="bulk-btn">
全部完成
</button>
<button @click="clearCompleted" class="bulk-btn" v-if="completedCount > 0">
清除已完成 ({{ completedCount }})
</button>
<button @click="clearAll" class="bulk-btn bulk-danger">
清空全部
</button>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
interface Todo {
id: string
text: string
completed: boolean
createdAt: Date
completedAt?: Date
editing?: boolean
editText?: string
}
type FilterType = 'all' | 'pending' | 'completed'
const newTodoText = ref('')
const todos = ref<Todo[]>([])
const currentFilter = ref<FilterType>('all')
const editInputs = ref<HTMLInputElement[]>([])
const filters = [
{ key: 'all' as FilterType, label: '全部' },
{ key: 'pending' as FilterType, label: '待办' },
{ key: 'completed' as FilterType, label: '已完成' }
]
// 计算属性
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
)
const pendingCount = computed(() =>
todos.value.filter(todo => !todo.completed).length
)
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'completed':
return todos.value.filter(todo => todo.completed)
case 'pending':
return todos.value.filter(todo => !todo.completed)
default:
return todos.value
}
})
// 方法
const addTodo = async () => {
const text = newTodoText.value.trim()
if (!text) return
const todo: Todo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date()
}
todos.value.unshift(todo)
newTodoText.value = ''
await saveTodos()
showNotification('✅ 待办事项已添加', `"${text}" 已添加到您的待办列表`)
}
const toggleTodo = async (id: string) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
todo.completedAt = todo.completed ? new Date() : undefined
await saveTodos()
if (todo.completed) {
showNotification('🎉 任务完成', `"${todo.text}" 已完成!`)
}
}
}
const startEdit = (todo: Todo) => {
todo.editing = true
todo.editText = todo.text
nextTick(() => {
const input = editInputs.value?.find(input =>
input && input.closest('.todo-item')?.getAttribute('data-id') === todo.id
)
input?.focus()
input?.select()
})
}
const saveEdit = async (todo: Todo) => {
if (todo.editText && todo.editText.trim() !== todo.text) {
todo.text = todo.editText.trim()
await saveTodos()
}
todo.editing = false
todo.editText = undefined
}
const cancelEdit = (todo: Todo) => {
todo.editing = false
todo.editText = undefined
}
const deleteTodo = async (id: string) => {
if (confirm('确定要删除这个待办事项吗?')) {
todos.value = todos.value.filter(t => t.id !== id)
await saveTodos()
}
}
const setFilter = (filter: FilterType) => {
currentFilter.value = filter
}
const markAllCompleted = async () => {
todos.value.forEach(todo => {
if (!todo.completed) {
todo.completed = true
todo.completedAt = new Date()
}
})
await saveTodos()
showNotification('✅ 全部完成', '所有待办事项已标记为完成')
}
const clearCompleted = async () => {
if (confirm(`确定要删除 ${completedCount.value} 个已完成的待办事项吗?`)) {
todos.value = todos.value.filter(todo => !todo.completed)
await saveTodos()
showNotification('🗑️ 清理完成', '已清除所有已完成的待办事项')
}
}
const clearAll = async () => {
if (confirm('确定要清空所有待办事项吗?此操作不可恢复。')) {
todos.value = []
await saveTodos()
showNotification('🗑️ 清空完成', '所有待办事项已清空')
}
}
const loadTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get('todos')
if (result.success && result.data) {
const savedTodos = JSON.parse(result.data)
todos.value = savedTodos.map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
completedAt: todo.completedAt ? new Date(todo.completedAt) : undefined
}))
}
}
} catch (error) {
console.error('加载待办事项失败:', error)
}
}
const saveTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.set('todos', JSON.stringify(todos.value))
}
} catch (error) {
console.error('保存待办事项失败:', error)
}
}
const showNotification = async (title: string, message: string) => {
try {
if (window.SystemSDK?.ui?.showNotification) {
await window.SystemSDK.ui.showNotification({
title,
body: message,
duration: 3000
})
}
} catch (error) {
console.error('显示通知失败:', error)
}
}
const formatDate = (date: Date) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
if (diffDays < 7) return `${diffDays}天前`
return date.toLocaleDateString('zh-CN')
}
const getEmptyMessage = () => {
switch (currentFilter.value) {
case 'completed':
return '还没有已完成的任务'
case 'pending':
return '没有待办事项'
default:
return '还没有待办事项'
}
}
const getEmptyDescription = () => {
switch (currentFilter.value) {
case 'completed':
return '完成一些任务后,它们会出现在这里'
case 'pending':
return '所有任务都已完成,真棒!'
default:
return '添加一个新的待办事项开始管理您的任务吧!'
}
}
onMounted(async () => {
await loadTodos()
})
</script>
<style scoped>
.todo-app {
height: 100%;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
.header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
font-weight: 600;
}
.stats {
font-size: 14px;
opacity: 0.9;
}
.count {
font-weight: 600;
}
.add-todo {
padding: 20px;
background: white;
border-bottom: 1px solid #e9ecef;
}
.input-group {
display: flex;
gap: 10px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.todo-input:focus {
border-color: #667eea;
}
.add-btn {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
font-weight: 500;
}
.add-btn:hover:not(:disabled) {
background: #5a67d8;
}
.add-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.filters {
padding: 0 20px 20px;
display: flex;
gap: 10px;
justify-content: center;
background: white;
}
.filter-btn {
padding: 8px 16px;
border: 1px solid #e9ecef;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.filter-btn:hover:not(.active) {
background: #f8f9fa;
}
.todo-list {
flex: 1;
overflow-y: auto;
padding: 0 20px;
background: white;
}
.todo-item {
display: flex;
align-items: center;
padding: 16px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border-left: 4px solid #667eea;
}
.todo-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.todo-item.completed {
opacity: 0.7;
border-left-color: #28a745;
}
.todo-checkbox {
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #28a745;
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.todo-text {
font-size: 16px;
transition: color 0.3s;
line-height: 1.4;
}
.todo-text.completed {
text-decoration: line-through;
color: #6c757d;
}
.todo-date {
font-size: 12px;
color: #6c757d;
}
.todo-edit {
flex: 1;
margin-right: 12px;
}
.edit-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #667eea;
border-radius: 4px;
font-size: 16px;
outline: none;
}
.todo-actions {
display: flex;
gap: 8px;
}
.edit-btn,
.delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px;
}
.edit-btn:hover,
.delete-btn:hover {
opacity: 1;
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #6c757d;
background: white;
padding: 40px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-state h3 {
margin-bottom: 8px;
color: #495057;
}
.bulk-actions {
padding: 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.bulk-btn {
padding: 8px 16px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.bulk-btn:hover {
background: #f8f9fa;
}
.bulk-danger {
color: #dc3545;
border-color: #dc3545;
}
.bulk-danger:hover {
background: #dc3545;
color: white;
}
/* 动画 */
.todo-list-enter-active,
.todo-list-leave-active {
transition: all 0.3s ease;
}
.todo-list-enter-from {
opacity: 0;
transform: translateY(20px);
}
.todo-list-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.todo-list-move {
transition: transform 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
padding: 15px;
}
.header h1 {
font-size: 20px;
}
.add-todo {
padding: 15px;
}
.input-group {
flex-direction: column;
}
.filters {
padding: 0 15px 15px;
flex-wrap: wrap;
}
.todo-list {
padding: 0 15px;
}
.todo-item {
padding: 12px;
}
.bulk-actions {
padding: 15px;
}
}
</style>

View File

@@ -0,0 +1,65 @@
import type { WindowConfig } from '@/services/windowForm/WindowFormService.ts'
/**
* 内置应用清单接口
*/
export interface InternalAppManifest {
/**
* 应用唯一标识符
*/
id: string
/**
* 应用名称
*/
name: string
/**
* 应用版本号
*/
version: string
/**
* 应用描述信息
*/
description: string
/**
* 应用作者
*/
author: string
/**
* 应用图标
*/
icon: string
/**
* 应用所需权限列表
*/
permissions: string[]
/**
* 窗体配置信息
*/
window: WindowConfig
/**
* 应用分类
*/
category?: string
/**
* 应用关键字列表
*/
keywords?: string[]
}
/**
* 应用注册信息
*/
export interface AppRegistration {
/**
* 应用清单信息
*/
manifest: InternalAppManifest
/**
* Vue组件或异步加载函数
*/
component: any // Vue组件或返回Promise<Vue组件>的函数
/**
* 是否为内置应用
*/
isBuiltIn: boolean
}

View File

@@ -0,0 +1,65 @@
import { onMounted, onBeforeUnmount, type Ref, watch } from "vue";
interface ClickFocusOptions {
once?: boolean; // 点击一次后解绑
}
/**
* Vue3 Hook点击自身聚焦 / 点击外部失焦
* @param targetRef 元素的 ref
* @param onFocus 点击自身回调
* @param onBlur 点击外部回调
* @param options 配置项 { once }
*/
export function useClickFocus(
targetRef: Ref<HTMLElement | null>,
onFocus?: () => void,
onBlur?: () => void,
options: ClickFocusOptions = {}
) {
let cleanupFn: (() => void) | null = null;
const bindEvents = (el: HTMLElement) => {
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
const handleClick = (e: MouseEvent) => {
if (!el.isConnected) return cleanup();
if (el.contains(e.target as Node)) {
el.focus();
onFocus?.();
} else {
el.blur();
onBlur?.();
}
if (options.once) cleanup();
};
document.addEventListener("click", handleClick);
cleanupFn = () => {
document.removeEventListener("click", handleClick);
cleanupFn = null;
};
};
const cleanup = () => {
cleanupFn?.();
};
onMounted(() => {
const el = targetRef.value;
if (el) bindEvents(el);
});
onBeforeUnmount(() => {
cleanup();
});
// 支持动态 ref 变化
watch(targetRef, (newEl, oldEl) => {
if (oldEl) cleanup();
if (newEl) bindEvents(newEl);
});
}

View File

@@ -0,0 +1,8 @@
/**
* 可销毁接口
* 销毁实例,清理副作用,让内存可以被回收
*/
export interface IDestroyable {
/** 销毁实例,清理副作用,让内存可以被回收 */
destroy(): void
}

View File

@@ -0,0 +1,24 @@
/** 单例模式 装饰器
* @param clz
* @returns
*/
export function SingletonPattern<T extends new (...args: any[]) => any>(clz: T) {
// 添加判断,确保装饰器只能用于类
if (typeof clz !== 'function') {
throw new Error('单例模式装饰器只能修饰在类上')
}
let instance: InstanceType<T>
const proxy = new Proxy(clz, {
construct(target, args, newTarget) {
if (!instance) {
instance = Reflect.construct(target, args, newTarget)
}
return instance
}
})
proxy.prototype.constructor = proxy
return proxy
}

View File

@@ -1,78 +0,0 @@
import ProcessImpl from './process/impl/ProcessImpl.ts'
import { isUndefined } from 'lodash'
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import { ObservableWeakRefImpl } from '@/core/state/impl/ObservableWeakRefImpl.ts'
import type { IObservable } from '@/core/state/IObservable.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'
interface IGlobalState {
isLogin: boolean
}
export default class XSystem {
private static _instance: XSystem = new XSystem()
private _globalState: IObservable<IGlobalState> = new ObservableWeakRefImpl<IGlobalState>({
isLogin: false
})
private _desktopRootDom: HTMLElement;
constructor() {
console.log('XSystem')
new NotificationService()
new SettingsService()
new WindowFormService()
new UserService()
}
public static get instance() {
return this._instance
}
public get globalState() {
return this._globalState
}
public get desktopRootDom() {
return this._desktopRootDom
}
public initialization(dom: HTMLDivElement) {
this._desktopRootDom = dom
this.run('basic-system', BasicSystemProcess).then(() => {
this.run('desktop', DesktopProcess).then((proc) => {
proc.mount(dom)
// console.log(dom.querySelector('.desktop-root'))
})
})
}
// 运行进程
public async run<T extends IProcess = IProcess>(
proc: string | IProcessInfo,
constructor?: new (info: IProcessInfo) => T,
): Promise<T> {
let info = typeof proc === 'string' ? processManager.findProcessInfoByName(proc)! : proc
if (isUndefined(info)) {
throw new Error(`未找到进程信息:${proc}`)
}
// 是单例应用
if (info.singleton) {
let process = processManager.findProcessByName(info.name)
if (process) {
return process as T
}
}
// 创建进程
let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info)
return process as T
}
}

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
部门页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
文件管理页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
音乐页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
个人中心页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
照片页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
回收站页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
设置APP页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
电视页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -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
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
电影页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,96 +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 { eventManager } from '@/core/events/EventManager.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
export class DesktopProcess extends ProcessImpl {
private _desktopRootDom: HTMLElement;
private _isMounted: boolean = false;
private _width: number = 0;
private _height: number = 0;
private _pendingResize: boolean = false;
public get desktopRootDom() {
return this._desktopRootDom;
}
public get isMounted() {
return this._isMounted;
}
public get basicSystemProcess() {
return processManager.findProcessByName<BasicSystemProcess>('basic-system')
}
public get width() {
return this._width;
}
public set width(value: number) {
if (this._height === value) return;
if (!this._isMounted) return;
this._width = value;
this._desktopRootDom.style.width = value >= 0 ? `${value}px` : '100%';
this.scheduleResizeEvent()
}
public get height() {
return this._height;
}
public set height(value: number) {
if (this._height === value) return;
if (!this._isMounted) return;
this._height = value;
this._desktopRootDom.style.height = value >= 0 ? `${value}px` : '100%';
this.scheduleResizeEvent()
}
private scheduleResizeEvent() {
if (this._pendingResize) return;
this._pendingResize = true;
Promise.resolve().then(() => {
if (this._pendingResize) {
this._pendingResize = false;
console.log('onDesktopRootDomResize')
eventManager.notifyEvent('onDesktopRootDomResize', this._width, this._height);
}
});
}
constructor(info: IProcessInfo) {
super(info)
console.log('DesktopProcess')
}
public mount(dom: HTMLDivElement) {
console.log('DesktopProcess: start mount')
if (this._isMounted) return
this._width = window.innerWidth
this._height = window.innerHeight
window.addEventListener(
'resize',
debounce(() => {
this.width = window.innerWidth
this.height = window.innerHeight
}, 300)
)
dom.style.zIndex = '0';
dom.style.width = `${this._width}px`
dom.style.height = `${this._height}px`
dom.style.position = 'relative'
dom.style.overflow = 'hidden'
this._desktopRootDom = dom
const app = createApp(DesktopComponent, { process: this })
app.use(naiveUi)
app.mount(dom)
this._isMounted = true
}
}

View File

@@ -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
})

View File

@@ -1,80 +0,0 @@
<template>
<n-config-provider
:config-provider-props="configProviderProps"
class="w-full h-full pos-relative"
>
<div class="desktop-root" @contextmenu="onContextMenu">
<div class="desktop-bg">
<div class="desktop-icons-container"
:style="gridStyle">
<AppIcon v-for="(appIcon, i) in appIconsRef" :key="i"
:iconInfo="appIcon" :gridTemplate="gridTemplate"
@dblclick="runApp(appIcon)"
/>
</div>
</div>
<div class="task-bar">
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">测试</div>
</div>
</div>
</n-config-provider>
</template>
<script setup lang="ts">
import type { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
import XSystem from '@/core/XSystem.ts'
import { notificationApi } from '@/core/common/naive-ui/discrete-api.ts'
import { configProviderProps } from '@/core/common/naive-ui/theme.ts'
import { useDesktopInit } from '@/core/desktop/ui/hooks/useDesktopInit.ts'
import AppIcon from '@/core/desktop/ui/components/AppIcon.vue'
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import { eventManager } from '@/core/events/EventManager.ts'
const props = defineProps<{ process: DesktopProcess }>()
const { appIconsRef, gridStyle, gridTemplate } = useDesktopInit('.desktop-icons-container')
eventManager.addEventListener('onDesktopRootDomResize',
(width, height) => {
console.log(width, height)
notificationApi.create({
title: '桌面通知',
content: `桌面尺寸变化${width}x${height}}`,
duration: 2000,
})
},
)
const onContextMenu = (e: MouseEvent) => {
e.preventDefault()
}
const runApp = (appIcon: IDesktopAppIcon) => {
XSystem.instance.run(appIcon.name)
}
</script>
<style lang="scss" scoped>
$taskBarHeight: 40px;
.desktop-root {
@apply w-full h-full flex flex-col;
.desktop-bg {
@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;
}
}
</style>

View File

@@ -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<IGridTemplateParams>({
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<IDesktopAppIcon[]>([])
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<string>();
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[];
}

View File

@@ -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);
}
}

View File

@@ -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<IAllEvent>()
/**
* 系统进程的事件
* @description
* <p>onAuthChange - 认证状态改变</p>
* <p>onThemeChange - 主题改变</p>
*/
export interface IBasicSystemEvent extends IEventMap {
/** 认证状态改变 */
onAuthChange: () => {},
/** 主题改变 */
onThemeChange: (theme: string) => void
}
/**
* 桌面进程的事件
* @description
* <p>onDesktopRootDomResize - 桌面根dom尺寸改变</p>
* <p>onDesktopProcessInitialize - 桌面进程初始化完成</p>
*/
export interface IDesktopEvent extends IEventMap {
/** 桌面根dom尺寸改变 */
onDesktopRootDomResize: (width: number, height: number) => void
/** 桌面进程初始化完成 */
onDesktopProcessInitialize: () => void
/** 桌面应用图标位置改变 */
onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void
}
export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {}

View File

@@ -1,45 +0,0 @@
/**
* 事件定义
* @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
*/
export interface IEventMap {
[key: string]: (...args: any[]) => void
}
/**
* 事件管理器接口定义
*/
export interface IEventBuilder<Events extends IEventMap> {
/**
* 添加事件监听
* @param eventName 事件名称
* @param handler 事件处理函数
* @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
* @returns void
*/
addEventListener<E extends keyof Events, F extends Events[E]>(
eventName: E,
handler: F,
options?: {
immediate?: boolean
immediateArgs?: Parameters<F>
once?: boolean
},
): void
/**
* 移除事件监听
* @param eventName 事件名称
* @param handler 事件处理函数
* @returns void
*/
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
/**
* 触发事件
* @param eventName 事件名称
* @param args 参数
* @returns void
*/
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>): void
}

View File

@@ -1,19 +0,0 @@
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
/**
* 进程接口
*/
export interface IProcess {
/** 进程id */
get id(): string;
/** 进程信息 */
get processInfo(): IProcessInfo;
/** 进程的窗体列表 */
get windowForms(): Map<string, IWindowForm>;
/**
* 打开窗体
* @param startName 窗体启动名
*/
openWindowForm(startName: string): void;
}

View File

@@ -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[];
}

View File

@@ -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<T extends IProcess = IProcess>(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;
}

View File

@@ -1,3 +0,0 @@
import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts'
export const processManager = new ProcessManagerImpl();

View File

@@ -1,46 +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'
/**
* 进程
*/
export default class ProcessImpl implements IProcess {
private readonly _id: string = uuidV4();
private readonly _processInfo: IProcessInfo;
// 当前进程的窗体集合
private _windowForms: Map<string, IWindowForm> = new Map();
public get id() {
return this._id;
}
public get processInfo() {
return this._processInfo;
}
public get windowForms() {
return this._windowForms;
}
constructor(info: IProcessInfo) {
console.log(`AppProcess: ${info.name}`)
this._processInfo = info;
const startName = info.startName;
processManager.registerProcess(this);
// 通过设置 isJustProcess 为 true则不会创建窗体
if (!info.isJustProcess) {
this.openWindowForm(startName)
}
}
public openWindowForm(startName: string) {
const info = this._processInfo.windowFormConfigs.find(item => item.name === startName);
if (!info) throw new Error(`未找到窗体:${startName}`);
const window = new WindowFormImpl(this, info);
this._windowForms.set(window.id, window);
}
}

View File

@@ -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<IWindowFormConfig>;
constructor(info: IAppProcessInfoParams) {
this._name = info.name;
this._title = info.title || '';
this._description = info.description || '';
this._icon = <string> 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;
}
}

View File

@@ -1,81 +0,0 @@
import type 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'
/**
* 进程管理
*/
export default class ProcessManagerImpl implements IProcessManager {
private _processPool: Map<string, IProcess> = new Map<string, IProcess>();
private _processInfos: IProcessInfo[] = new Array<ProcessInfoImpl>();
public get processInfos() {
return this._processInfos;
}
constructor() {
console.log('ProcessManageImpl')
this.loadAppProcessInfos();
}
// TODO 加载所有进程信息
private loadAppProcessInfos() {
console.log('加载所有进程信息')
// 添加内置进程
const apps = import.meta.glob<IAppProcessInfoParams>('../../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 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<T extends IProcess = IProcess>(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);
}
}

View File

@@ -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[];
}

View File

@@ -1,14 +0,0 @@
/**
* 进程的事件
* <p>onProcessExit - 进程退出</p>
* <p>onProcessWindowFormOpen - 进程的窗体打开</p>
* <p>onProcessWindowFormExit - 进程的窗体退出</p>
* <p>onProcessWindowFormFocus - 进程的窗体获取焦点</p>
*
*/
type TAppProcessEvent =
'onProcessExit' |
'onProcessWindowFormOpen' |
'onProcessWindowFormExit' |
'onProcessWindowFormFocus' |
'onProcessWindowFormBlur'

View File

@@ -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);
}
}

View File

@@ -1,41 +0,0 @@
import type { AService } from '@/core/service/kernel/AService.ts'
/**
* 服务管理
*/
export class ServiceManager {
private _services: Map<string, AService> = new Map<string, AService>();
get services(): Map<string, AService> {
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<T extends AService>(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()

View File

@@ -1,8 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
export class NotificationService extends AService {
constructor() {
super('NotificationService');
console.log('NotificationService - 服务注册')
}
}

View File

@@ -1,8 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
export class SettingsService extends AService {
constructor() {
super('SettingsService')
console.log('SettingsService - 服务注册')
}
}

View File

@@ -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<IUserInfo>;
get userInfo() {
return this._userInfo;
}
constructor() {
super("UserService");
console.log("UserService - 服务注册")
}
}

View File

@@ -1,80 +0,0 @@
import { AService } from '@/core/service/kernel/AService.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<string, IWindow> = new Map();
private zCounter = 1;
constructor() {
super("WindowForm");
console.log('WindowFormService - 服务注册')
}
createWindow(title: string, config?: Partial<IWindow>): IWindow {
const id = `win-${Date.now()}-${Math.random()}`;
const win: IWindow = {
id,
title,
x: config?.x ?? 100,
y: config?.y ?? 100,
width: config?.width ?? 400,
height: config?.height ?? 300,
zIndex: this.zCounter++,
minimized: false,
maximized: false
};
this.windows.set(id, win);
this.sm.broadcast("WindowFrom:created", win);
return win;
}
closeWindow(id: string) {
if (this.windows.has(id)) {
this.windows.delete(id);
this.sm.broadcast("WindowFrom:closed", id);
}
}
focusWindow(id: string) {
const win = this.windows.get(id);
if (win) {
win.zIndex = this.zCounter++;
this.sm.broadcast("WindowFrom:focused", win);
}
}
minimizeWindow(id: string) {
const win = this.windows.get(id);
if (win) {
win.minimized = true;
this.sm.broadcast("WindowFrom:minimized", win);
}
}
maximizeWindow(id: string) {
const win = this.windows.get(id);
if (win) {
win.maximized = !win.maximized;
this.sm.broadcast("WindowFrom:maximized", win);
}
}
getWindows(): IWindow[] {
return Array.from(this.windows.values()).sort((a, b) => a.zIndex - b.zIndex);
}
onMessage(event: string, data?: any) {
console.log(`[WindowService] 收到事件:`, event, data);
}
}

View File

@@ -1,57 +0,0 @@
// 订阅函数类型
export type TObservableListener<T> = (state: T) => void
// 字段订阅函数类型
export type TObservableKeyListener<T, K extends keyof T> = (values: Pick<T, K>) => void
// 工具类型:排除函数属性
export type TNonFunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K]
}
// ObservableImpl 数据类型
export type TObservableState<T> = T & { [key: string]: any }
/**
* ObservableImpl 接口定义
*/
export interface IObservable<T extends TNonFunctionProperties<T>> {
/** ObservableImpl 状态对象,深层 Proxy */
readonly state: TObservableState<T>
/**
* 订阅整个状态变化
* @param fn 监听函数
* @param options immediate 是否立即触发一次
* @returns 取消订阅函数
*/
subscribe(fn: TObservableListener<T>, options?: { immediate?: boolean }): () => void
/**
* 订阅指定字段变化
* @param keys 单个或多个字段
* @param fn 字段变化回调
* @param options immediate 是否立即触发一次
* @returns 取消订阅函数
*/
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options?: { immediate?: boolean }
): () => void
/**
* 批量更新状态
* @param values Partial<T>
*/
patch(values: Partial<T>): void
/** 销毁 ObservableImpl 实例 */
dispose(): void
/**
* 语法糖:返回一个可解构赋值的 Proxy
* 用于直接赋值触发通知
*/
toRefsProxy(): { [K in keyof T]: T[K] }
}

View File

@@ -1,257 +0,0 @@
import type {
IObservable,
TNonFunctionProperties,
TObservableKeyListener,
TObservableListener,
TObservableState,
} from '@/core/state/IObservable.ts'
/**
* 创建一个可观察对象,用于管理状态和事件。
* @template T - 需要处理的状态类型
* @example
* interface AppState {
* count: number
* user: {
* name: string
* age: number
* }
* items: number[]
* }
*
* // 创建 ObservableImpl
* const obs = new ObservableImpl<AppState>({
* 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 ObservableImpl<T extends TNonFunctionProperties<T>> implements IObservable<T> {
/** Observable 状态对象,深层 Proxy */
public readonly state: TObservableState<T>
/** 全量订阅函数集合 */
private listeners: Set<TObservableListener<T>> = new Set()
/** 字段订阅函数集合 */
private keyListeners: Map<keyof T, Set<Function>> = new Map()
/** 待通知的字段集合 */
private pendingKeys: Set<keyof T> = new Set()
/** 是否已经安排通知 */
private notifyScheduled = false
/** 是否已销毁 */
private disposed = false
constructor(initialState: TNonFunctionProperties<T>) {
// 创建深层响应式 Proxy
this.state = this.makeReactive(initialState) as TObservableState<T>
}
/** 创建深层 Proxy拦截 get/set */
private makeReactive(obj: TNonFunctionProperties<T>): TObservableState<T> {
const handler: ProxyHandler<any> = {
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<T>
}
/** 包装数组方法,使 push/pop 等触发通知 */
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 fn of this.listeners) {
fn(this.state)
}
// 字段订阅
const fnMap = new Map<Function, (keyof T)[]>()
for (const key of keys) {
const set = this.keyListeners.get(key)
if (!set) continue
for (const fn of set) {
if (!fnMap.has(fn)) fnMap.set(fn, [])
fnMap.get(fn)!.push(key)
}
}
fnMap.forEach((subKeys, fn) => {
const result = {} as Pick<T, typeof subKeys[number]>
subKeys.forEach(k => (result[k] = this.state[k]))
fn(result)
})
}
/** 订阅整个状态变化 */
subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
this.listeners.add(fn)
if (options.immediate) fn(this.state)
return () => {
this.listeners.delete(fn)
}
}
/** 订阅指定字段变化 */
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {}
): () => void {
const keyArray = Array.isArray(keys) ? keys : [keys]
for (const key of keyArray) {
if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set())
this.keyListeners.get(key)!.add(fn)
}
if (options.immediate) {
const result = {} as Pick<T, K>
keyArray.forEach(k => (result[k] = this.state[k]))
fn(result)
}
return () => {
for (const key of keyArray) {
this.keyListeners.get(key)?.delete(fn)
}
}
}
/** 批量更新状态 */
patch(values: Partial<T>): 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()
}
/** 销毁 Observable 实例 */
dispose(): void {
this.disposed = true
this.listeners.clear()
this.keyListeners.clear()
this.pendingKeys.clear()
}
/** 语法糖:返回一个可解构赋值的 Proxy */
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 }
}
})
}
}

View File

@@ -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<AppState>({
* 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<T extends TNonFunctionProperties<T>> implements IObservable<T> {
/** ObservableImpl 的状态对象,深层 Proxy */
public readonly state: TObservableState<T>
/** 全量订阅列表 */
private listeners: Set<WeakRef<TObservableListener<T>> | TObservableListener<T>> = new Set()
/** 字段订阅列表 */
private keyListeners: Map<keyof T, Set<WeakRef<Function> | Function>> = new Map()
/** FinalizationRegistry 用于自动清理 WeakRef */
private registry?: FinalizationRegistry<WeakRef<Function>>
/** 待通知的字段 */
private pendingKeys: Set<keyof T> = new Set()
/** 通知调度状态 */
private notifyScheduled = false
/** 已销毁标记 */
private disposed = false
constructor(initialState: TNonFunctionProperties<T>) {
if (typeof WeakRef !== 'undefined' && typeof FinalizationRegistry !== 'undefined') {
this.registry = new FinalizationRegistry((ref: WeakRef<Function>) => {
this.listeners.delete(ref as unknown as TObservableListener<T>)
this.keyListeners.forEach(set => set.delete(ref))
})
}
// 创建深层响应式 Proxy
this.state = this.makeReactive(initialState) as TObservableState<T>
}
/** 创建响应式对象,深层递归 Proxy + 数组方法拦截 */
private makeReactive(obj: TNonFunctionProperties<T>): TObservableState<T> {
const handler: ProxyHandler<any> = {
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<T>
}
/** 包装数组方法,使 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<T>)
}
// 字段订阅
const fnMap = new Map<Function, (keyof T)[]>()
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<T, typeof subKeys[number]>
subKeys.forEach(k => (result[k] = this.state[k]))
fn(result)
})
}
/** 全量订阅 */
subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
const ref = this.makeRef(fn)
this.listeners.add(ref)
this.registry?.register(fn, ref as WeakRef<Function>)
if (options.immediate) fn(this.state)
return () => {
this.listeners.delete(ref)
this.registry?.unregister(fn)
}
}
/** 字段订阅 */
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {}
): () => void {
const keyArray = Array.isArray(keys) ? keys : [keys]
const refs: (WeakRef<Function> | 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<Function>)
refs.push(ref)
}
if (options.immediate) {
const result = {} as Pick<T, K>
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<T>): 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<F extends Function>(fn: F): WeakRef<F> | F {
return typeof WeakRef !== 'undefined' ? new WeakRef(fn) : fn
}
/** WeakRef 解引用 */
private deref<F extends Function>(ref: WeakRef<F> | F): F | undefined {
return typeof WeakRef !== 'undefined' && ref instanceof WeakRef ? ref.deref() : (ref as F)
}
}

View File

@@ -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')
}
}

View File

@@ -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
});

View File

@@ -1,559 +0,0 @@
/** 拖拽移动开始的回调 */
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 IResizeCallbackData {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
/** 拖拽调整尺寸的方向 */
direction: TResizeDirection;
}
/** 拖拽/调整尺寸 参数 */
interface IDraggableResizableOptions {
/** 拖拽/调整尺寸目标元素 */
target: HTMLElement;
/** 拖拽句柄 */
handle?: HTMLElement;
/** 拖拽模式 */
mode?: 'transform' | 'position';
/** 拖拽边界或容器元素 */
boundary?: IBoundaryRect | HTMLElement;
/** 移动步进(网格吸附) */
snapGrid?: number;
/** 关键点吸附阈值 */
snapThreshold?: number;
/** 是否开启吸附动画 */
snapAnimation?: boolean;
/** 拖拽结束吸附动画时长 */
snapAnimationDuration?: number;
/** 是否允许超出边界 */
allowOverflow?: boolean;
/** 拖拽开始回调 */
onDragStart?: TDragStartCallback;
/** 拖拽移动中的回调 */
onDragMove?: TDragMoveCallback;
/** 拖拽结束回调 */
onDragEnd?: TDragEndCallback;
/** 调整尺寸的最小宽度 */
minWidth?: number;
/** 调整尺寸的最小高度 */
minHeight?: number;
/** 调整尺寸的最大宽度 */
maxWidth?: number;
/** 调整尺寸的最大高度 */
maxHeight?: number;
/** 拖拽调整尺寸中的回调 */
onResizeMove?: (data: IResizeCallbackData) => void;
/** 拖拽调整尺寸结束回调 */
onResizeEnd?: (data: IResizeCallbackData) => void;
}
/** 拖拽的范围边界 */
interface IBoundaryRect {
/** 最小 X 坐标 */
minX?: number;
/** 最大 X 坐标 */
maxX?: number;
/** 最小 Y 坐标 */
minY?: number;
/** 最大 Y 坐标 */
maxY?: number;
}
/**
* 拖拽 + 调整尺寸通用类
*/
export class DraggableResizable {
private handle?: HTMLElement;
private target: HTMLElement;
private boundary?: HTMLElement | IBoundaryRect;
private mode: 'transform' | 'position';
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 isDragging = false;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private currentX = 0;
private currentY = 0;
private containerRect?: DOMRect;
private resizeObserver?: ResizeObserver;
private mutationObserver: MutationObserver;
private animationFrame?: number;
private currentDirection: TResizeDirection | null = null;
private startWidth = 0;
private startHeight = 0;
private startTop = 0;
private startLeft = 0;
private minWidth: number;
private minHeight: number;
private maxWidth: number;
private maxHeight: number;
private onResizeMove?: (data: IResizeCallbackData) => void;
private onResizeEnd?: (data: IResizeCallbackData) => void;
constructor(options: IDraggableResizableOptions) {
// Drag
this.handle = options.handle;
this.target = options.target;
this.boundary = options.boundary;
this.mode = options.mode ?? 'transform';
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;
// Resize
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.init();
}
/** 初始化事件 */
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);
if (this.boundary instanceof HTMLElement) {
this.observeResize(this.boundary);
}
// 监听目标 DOM 是否被移除,自动销毁
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) => {
if (this.getResizeDirection(e)) return; // 避免和 resize 冲突
e.preventDefault();
this.startDrag(e);
};
private startDrag(e: MouseEvent) {
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
if (this.mode === 'position') {
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.offsetX = rect.left - parentRect.left;
this.offsetY = rect.top - parentRect.top;
} else {
this.offsetX = this.currentX;
this.offsetY = this.currentY;
}
document.addEventListener('mousemove', this.onMouseMoveDrag);
document.addEventListener('mouseup', this.onMouseUpDrag);
this.onDragStart?.(this.offsetX, this.offsetY);
}
private onMouseMoveDrag = (e: MouseEvent) => {
if (!this.isDragging) return;
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
let newX = this.offsetX + dx;
let newY = this.offsetY + dy;
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 = () => {
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);
});
} else {
this.applyPosition(snapped.x, snapped.y, true);
this.onDragEnd?.(snapped.x, snapped.y);
}
document.removeEventListener('mousemove', this.onMouseMoveDrag);
document.removeEventListener('mouseup', this.onMouseUpDrag);
};
private applyPosition(x: number, y: number, isFinal: boolean) {
this.currentX = x;
this.currentY = y;
if (this.mode === 'position') {
this.target.style.left = `${x}px`;
this.target.style.top = `${y}px`;
} else {
this.target.style.transform = `translate(${x}px, ${y}px)`;
}
if (isFinal) this.applyBoundary();
}
private onMouseDownResize = (e: MouseEvent) => {
const dir = this.getResizeDirection(e);
if (!dir) return;
e.preventDefault();
this.startResize(e, dir);
};
private startResize(e: MouseEvent, dir: TResizeDirection) {
this.currentDirection = dir;
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = rect.width;
this.startHeight = rect.height;
this.startTop = rect.top - parentRect.top;
this.startLeft = rect.left - parentRect.left;
document.addEventListener('mousemove', this.onResizeDrag);
document.addEventListener('mouseup', this.onResizeEndHandler);
}
private onResizeDrag = (e: MouseEvent) => {
if (!this.currentDirection) return;
let deltaX = e.clientX - this.startX;
let deltaY = e.clientY - this.startY;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newTop = this.startTop;
let newLeft = this.startLeft;
switch (this.currentDirection) {
case 'right':
newWidth = this.startWidth + deltaX;
break;
case 'bottom':
newHeight = this.startHeight + deltaY;
break;
case 'bottom-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight + deltaY;
break;
case 'left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
break;
case 'top':
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'bottom-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight + deltaY;
break;
}
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.target.style.top = `${newTop}px`;
this.target.style.left = `${newLeft}px`;
this.onResizeMove?.({
width: newWidth,
height: newHeight,
top: newTop,
left: newLeft,
direction: this.currentDirection,
});
};
private onResizeEndHandler = () => {
if (this.currentDirection) {
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.onResizeEnd?.({
width: rect.width,
height: rect.height,
top: rect.top - parentRect.top,
left: rect.left - parentRect.left,
direction: this.currentDirection,
});
}
this.currentDirection = null;
this.updateCursor(null);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
};
private getResizeDirection(e: MouseEvent): TResizeDirection | null {
const rect = this.target.getBoundingClientRect();
const offset = 8;
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<TResizeDirection, string> = {
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) => {
if (this.currentDirection || this.isDragging) return;
const dir = this.getResizeDirection(e);
this.updateCursor(dir);
};
private onMouseLeave = () => {
if (!this.currentDirection && !this.isDragging) this.updateCursor(null);
};
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);
this.onDragMove?.(targetX, targetY);
onComplete?.();
}
};
this.animationFrame = requestAnimationFrame(step);
}
private applyBoundary() {
if (!this.boundary || this.allowOverflow) return;
let { x, y } = { x: this.currentX, y: this.currentY };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
const minX = 0;
const minY = 0;
const maxX = this.containerRect.width - rect.width;
const maxY = this.containerRect.height - rect.height;
x = Math.min(Math.max(x, minX), maxX);
y = Math.min(Math.max(y, minY), maxY);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary.minX !== undefined) x = Math.max(x, this.boundary.minX);
if (this.boundary.maxX !== undefined) x = Math.min(x, this.boundary.maxX);
if (this.boundary.minY !== undefined) y = Math.max(y, this.boundary.minY);
if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY);
}
this.currentX = x;
this.currentY = y;
this.applyPosition(x, y, false);
}
private applySnapping(x: number, y: number) {
let { x: snappedX, y: snappedY } = { x, y };
// 1. 容器吸附
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;
}
}
}
// 2. 窗口吸附 TODO
return { x: snappedX, y: snappedY };
}
private getSnapPoints() {
const snapPoints = { x: [] as number[], y: [] as number[] };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
snapPoints.x = [0, this.containerRect.width - rect.width];
snapPoints.y = [0, this.containerRect.height - rect.height];
} else if (!(this.boundary instanceof HTMLElement) && this.boundary) {
if (this.boundary.minX !== undefined) snapPoints.x.push(this.boundary.minX);
if (this.boundary.maxX !== undefined) snapPoints.x.push(this.boundary.maxX);
if (this.boundary.minY !== undefined) snapPoints.y.push(this.boundary.minY);
if (this.boundary.maxY !== undefined) snapPoints.y.push(this.boundary.maxY);
}
return snapPoints;
}
private observeResize(container: HTMLElement) {
if (this.resizeObserver) this.resizeObserver.disconnect();
this.resizeObserver = new ResizeObserver(() => {
this.containerRect = container.getBoundingClientRect();
this.applyBoundary();
});
this.resizeObserver.observe(container);
this.containerRect = container.getBoundingClientRect();
}
/** 销毁实例 */
public destroy() {
// 拖拽解绑:只在 handle 上解绑
if (this.handle) {
this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
}
// 调整尺寸解绑
this.target.removeEventListener('mousedown', this.onMouseDownResize);
this.target.removeEventListener('mouseleave', this.onMouseLeave);
// 全局事件解绑
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
document.removeEventListener('mousemove', this.onMouseMoveDrag);
document.removeEventListener('mouseup', this.onMouseUpDrag);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
// 观察器清理
if (this.resizeObserver) this.resizeObserver.disconnect();
if (this.mutationObserver) this.mutationObserver.disconnect();
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
// 所有属性置空,释放内存
Object.keys(this).forEach(k => (this as any)[k] = null);
}
}

View File

@@ -1,769 +0,0 @@
/** 拖拽移动开始的回调 */
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';
/** 窗口状态 */
type WindowState = 'default' | 'minimized' | 'maximized';
/** 元素边界 */
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;
/** 拖拽模式 */
mode?: 'transform' | 'position';
/** 拖拽边界或容器元素 */
boundary?: IBoundaryRect | 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;
}
/** 拖拽的范围边界 */
interface IBoundaryRect {
/** 最小 X 坐标 */
minX?: number;
/** 最大 X 坐标 */
maxX?: number;
/** 最小 Y 坐标 */
minY?: number;
/** 最大 Y 坐标 */
maxY?: number;
}
/**
* 拖拽 + 调整尺寸 + 最大最小化 通用类
*/
export class DraggableResizableWindow {
private handle?: HTMLElement;
private target: HTMLElement;
private boundary?: HTMLElement | IBoundaryRect;
private mode: 'transform' | 'position';
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 isDragging = false;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private currentX = 0;
private currentY = 0;
private containerRect?: DOMRect;
private resizeObserver?: ResizeObserver;
private mutationObserver: MutationObserver;
private animationFrame?: number;
private currentDirection: TResizeDirection | null = null;
private startWidth = 0;
private startHeight = 0;
private startTop = 0;
private startLeft = 0;
private minWidth: number;
private minHeight: number;
private maxWidth: number;
private maxHeight: number;
private onResizeMove?: (data: IResizeCallbackData) => void;
private onResizeEnd?: (data: IResizeCallbackData) => void;
private state: WindowState = 'default';
/** 目标元素默认 bounds */
private targetDefaultBounds: IElementRect;
/** 最大化前保存 bounds */
private maximizedBounds?: IElementRect;
/** 最小化任务栏位置的元素ID */
private taskbarElementId: string;
constructor(options: IDraggableResizableOptions) {
// Drag
this.handle = options.handle;
this.target = options.target;
this.boundary = options.boundary;
this.mode = options.mode ?? 'transform';
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;
// Resize
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.targetDefaultBounds = { width: this.target.offsetWidth, height: this.target.offsetHeight, top: this.target.offsetTop, left: this.target.offsetLeft };
this.taskbarElementId = options.taskbarElementId;
this.init();
}
/** 初始化事件 */
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);
if (this.boundary instanceof HTMLElement) {
this.observeResize(this.boundary);
}
// 监听目标 DOM 是否被移除,自动销毁
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) => {
if (this.getResizeDirection(e)) return; // 避免和 resize 冲突
e.preventDefault();
this.startDrag(e);
};
private startDrag(e: MouseEvent) {
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
if (this.mode === 'position') {
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.offsetX = rect.left - parentRect.left;
this.offsetY = rect.top - parentRect.top;
} else {
this.offsetX = this.currentX;
this.offsetY = this.currentY;
}
document.addEventListener('mousemove', this.onMouseMoveDrag);
document.addEventListener('mouseup', this.onMouseUpDrag);
this.onDragStart?.(this.offsetX, this.offsetY);
}
private onMouseMoveDrag = (e: MouseEvent) => {
if (!this.isDragging) return;
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
let newX = this.offsetX + dx;
let newY = this.offsetY + dy;
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 = () => {
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.updateDefaultBounds(snapped.x, snapped.y);
});
} else {
this.applyPosition(snapped.x, snapped.y, true);
this.onDragEnd?.(snapped.x, snapped.y);
this.updateDefaultBounds(snapped.x, snapped.y);
}
document.removeEventListener('mousemove', this.onMouseMoveDrag);
document.removeEventListener('mouseup', this.onMouseUpDrag);
};
private applyPosition(x: number, y: number, isFinal: boolean) {
this.currentX = x;
this.currentY = y;
if (this.mode === 'position') {
this.target.style.left = `${x}px`;
this.target.style.top = `${y}px`;
} else {
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);
this.onDragMove?.(targetX, targetY);
onComplete?.();
}
};
this.animationFrame = requestAnimationFrame(step);
}
private applyBoundary() {
if (!this.boundary || this.allowOverflow) return;
let { x, y } = { x: this.currentX, y: this.currentY };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
const minX = 0;
const minY = 0;
const maxX = this.containerRect.width - rect.width;
const maxY = this.containerRect.height - rect.height;
x = Math.min(Math.max(x, minX), maxX);
y = Math.min(Math.max(y, minY), maxY);
} else if (!(this.boundary instanceof HTMLElement)) {
if (this.boundary.minX !== undefined) x = Math.max(x, this.boundary.minX);
if (this.boundary.maxX !== undefined) x = Math.min(x, this.boundary.maxX);
if (this.boundary.minY !== undefined) y = Math.max(y, this.boundary.minY);
if (this.boundary.maxY !== undefined) y = Math.min(y, this.boundary.maxY);
}
this.currentX = x;
this.currentY = y;
this.applyPosition(x, y, false);
}
private applySnapping(x: number, y: number) {
let { x: snappedX, y: snappedY } = { x, y };
// 1. 容器吸附
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;
}
}
}
// 2. 窗口吸附 TODO
return { x: snappedX, y: snappedY };
}
private getSnapPoints() {
const snapPoints = { x: [] as number[], y: [] as number[] };
if (this.boundary instanceof HTMLElement && this.containerRect) {
const rect = this.target.getBoundingClientRect();
snapPoints.x = [0, this.containerRect.width - rect.width];
snapPoints.y = [0, this.containerRect.height - rect.height];
} else if (!(this.boundary instanceof HTMLElement) && this.boundary) {
if (this.boundary.minX !== undefined) snapPoints.x.push(this.boundary.minX);
if (this.boundary.maxX !== undefined) snapPoints.x.push(this.boundary.maxX);
if (this.boundary.minY !== undefined) snapPoints.y.push(this.boundary.minY);
if (this.boundary.maxY !== undefined) snapPoints.y.push(this.boundary.maxY);
}
return snapPoints;
}
private onMouseDownResize = (e: MouseEvent) => {
const dir = this.getResizeDirection(e);
if (!dir) return;
e.preventDefault();
this.startResize(e, dir);
};
private startResize(e: MouseEvent, dir: TResizeDirection) {
this.currentDirection = dir;
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = rect.width;
this.startHeight = rect.height;
this.startTop = rect.top - parentRect.top;
this.startLeft = rect.left - parentRect.left;
document.addEventListener('mousemove', this.onResizeDrag);
document.addEventListener('mouseup', this.onResizeEndHandler);
}
private onResizeDrag = (e: MouseEvent) => {
if (!this.currentDirection) return;
let deltaX = e.clientX - this.startX;
let deltaY = e.clientY - this.startY;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newTop = this.startTop;
let newLeft = this.startLeft;
switch (this.currentDirection) {
case 'right':
newWidth = this.startWidth + deltaX;
break;
case 'bottom':
newHeight = this.startHeight + deltaY;
break;
case 'bottom-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight + deltaY;
break;
case 'left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
break;
case 'top':
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'top-right':
newWidth = this.startWidth + deltaX;
newHeight = this.startHeight - deltaY;
newTop = this.startTop + deltaY;
break;
case 'bottom-left':
newWidth = this.startWidth - deltaX;
newLeft = this.startLeft + deltaX;
newHeight = this.startHeight + deltaY;
break;
}
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.target.style.top = `${newTop}px`;
this.target.style.left = `${newLeft}px`;
this.onResizeMove?.({
width: newWidth,
height: newHeight,
top: newTop,
left: newLeft,
direction: this.currentDirection,
});
};
private onResizeEndHandler = () => {
if (this.currentDirection) {
const rect = this.target.getBoundingClientRect();
const parentRect = this.target.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
this.onResizeEnd?.({
width: rect.width,
height: rect.height,
top: rect.top - parentRect.top,
left: rect.left - parentRect.left,
direction: this.currentDirection,
});
this.updateDefaultBounds(rect.left - parentRect.left, rect.top - parentRect.top, rect.width, rect.height);
}
this.currentDirection = null;
this.updateCursor(null);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
};
private getResizeDirection(e: MouseEvent): TResizeDirection | null {
const rect = this.target.getBoundingClientRect();
const offset = 8;
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<TResizeDirection, string> = {
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) => {
if (this.currentDirection || this.isDragging) return;
const dir = this.getResizeDirection(e);
this.updateCursor(dir);
};
private onMouseLeave = () => {
if (!this.currentDirection && !this.isDragging) this.updateCursor(null);
};
private observeResize(container: HTMLElement) {
if (this.resizeObserver) this.resizeObserver.disconnect();
this.resizeObserver = new ResizeObserver(() => {
this.containerRect = container.getBoundingClientRect();
this.applyBoundary();
});
this.resizeObserver.observe(container);
this.containerRect = container.getBoundingClientRect();
}
/** 销毁实例 */
public destroy() {
// 拖拽解绑:只在 handle 上解绑
if (this.handle) {
this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
}
// 调整尺寸解绑
this.target.removeEventListener('mousedown', this.onMouseDownResize);
this.target.removeEventListener('mouseleave', this.onMouseLeave);
// 全局事件解绑
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
document.removeEventListener('mousemove', this.onMouseMoveDrag);
document.removeEventListener('mouseup', this.onMouseUpDrag);
document.removeEventListener('mousemove', this.onResizeDrag);
document.removeEventListener('mouseup', this.onResizeEndHandler);
// 观察器清理
if (this.resizeObserver) this.resizeObserver.disconnect();
if (this.mutationObserver) this.mutationObserver.disconnect();
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
// 所有属性置空,释放内存
Object.keys(this).forEach(k => (this as any)[k] = null);
}
public getState() {
return this.state;
}
/** 最小化到任务栏 */
public minimize() {
if (this.state === 'minimized') return;
this.state = 'minimized';
// 获取任务栏位置
const taskbarElement = document.querySelector(this.taskbarElementId)
if (!taskbarElement) throw new Error('任务栏元素未找到');
const rect = taskbarElement.getBoundingClientRect()
const targetX = rect.left;
const targetY = rect.top;
const targetWidth = rect.width;
const targetHeight = rect.height;
const startX = this.currentX;
const startY = this.currentY;
const startWidth = this.target.offsetWidth;
const startHeight = this.target.offsetHeight;
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const deltaW = targetWidth - startWidth;
const deltaH = targetHeight - startHeight;
const duration = 400; // Windows 风格稍慢
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;
const w = startWidth + deltaW * ease;
const h = startHeight + deltaH * 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 = `${targetWidth}px`;
this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
}
};
requestAnimationFrame(step);
}
/** 最大化窗口(带动画) */
public maximize(withAnimation = true) {
if (this.state === 'maximized') return;
this.state = 'maximized';
// 保存原始 bounds
const rect = this.target.getBoundingClientRect();
this.targetDefaultBounds = { width: rect.width, height: rect.height, top: rect.top, left: rect.left };
const startWidth = rect.width;
const startHeight = rect.height;
const startX = this.currentX;
const startY = this.currentY;
const targetWidth = this.containerRect?.width ?? window.innerWidth;
const targetHeight = this.containerRect?.height ?? window.innerHeight;
const targetX = 0;
const targetY = 0;
if (!withAnimation) {
this.target.style.width = `${targetWidth}px`;
this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
return;
}
// 动画过渡
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const deltaW = targetWidth - startWidth;
const deltaH = targetHeight - startHeight;
const duration = 300;
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;
const w = startWidth + deltaW * ease;
const h = startHeight + deltaH * 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 = `${targetWidth}px`;
this.target.style.height = `${targetHeight}px`;
this.applyPosition(targetX, targetY, true);
}
};
requestAnimationFrame(step);
}
/** 恢复窗口(带动画,从最大化或最小化) */
public restore(withAnimation = true) {
if (this.state === 'default') return;
this.state = 'default';
const b = this.targetDefaultBounds;
const startWidth = this.target.offsetWidth || 0;
const startHeight = this.target.offsetHeight || 0;
const startLeft = this.currentX;
const startTop = this.currentY;
if (!withAnimation) {
this.target.style.width = `${b.width}px`;
this.target.style.height = `${b.height}px`;
this.applyPosition(b.left, b.top, true);
return;
}
const deltaX = b.left - startLeft;
const deltaY = b.top - startTop;
const deltaW = b.width - startWidth;
const deltaH = b.height - startHeight;
const duration = 300;
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 = startLeft + deltaX * ease;
const y = startTop + deltaY * ease;
const w = startWidth + deltaW * ease;
const h = startHeight + deltaH * 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 = `${b.width}px`;
this.target.style.height = `${b.height}px`;
this.applyPosition(b.left, b.top, true);
}
};
requestAnimationFrame(step);
}
/** 更新默认 bounds */
private updateDefaultBounds(x?: number, y?: number, width?: number, height?: number) {
const rect = this.target.getBoundingClientRect();
this.targetDefaultBounds = {
left: x ?? rect.left,
top: y ?? rect.top,
width: width ?? rect.width,
height: height ?? rect.height,
};
}
}

View File

@@ -1,17 +0,0 @@
/** 单例模式
* 确保一个类只有一个实例,并提供一个全局访问点
* @param constructor
* @constructor
*/
export function Singleton<T extends { new (...args: any[]): any }>(constructor: T): T {
let instance: any;
return new Proxy(constructor, {
construct(target, argsList, newTarget) {
if (!instance) {
instance = Reflect.construct(target, argsList, newTarget);
}
return instance;
},
});
}

View File

@@ -1,6 +0,0 @@
import type { IProcess } from '@/core/process/IProcess.ts'
export interface IWindowForm {
get id(): string;
get proc(): IProcess | undefined;
}

View File

@@ -1,88 +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 { WindowFormPos } from '@/core/window/types/WindowFormTypes.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
import { DraggableResizable } from '@/core/utils/DraggableResizable.ts'
import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts'
export default class WindowFormImpl implements IWindowForm {
private readonly _id: string = uuidV4();
private readonly _procId: string;
private pos: WindowFormPos = { x: 0, y: 0 };
private width: number = 0;
private height: number = 0;
public get id() {
return this._id;
}
public get proc() {
return processManager.findProcessById(this._procId)
}
private get desktopRootDom() {
return XSystem.instance.desktopRootDom;
}
constructor(proc: IProcess, config: IWindowFormConfig) {
this._procId = proc.id;
console.log('WindowForm')
this.pos = {
x: config.left ?? 0,
y: config.top ?? 0
}
this.width = config.width ?? 0;
this.height = config.height ?? 0;
this.createWindowFrom();
}
public createWindowFrom() {
const dom = document.createElement('div');
dom.style.position = 'absolute';
dom.style.left = `${this.pos.x}px`;
dom.style.top = `${this.pos.y}px`;
dom.style.width = `${this.width}px`;
dom.style.height = `${this.height}px`;
dom.style.zIndex = '100';
dom.style.backgroundColor = 'white';
const div = document.createElement('div');
div.style.width = '100%';
div.style.height = '20px';
div.style.backgroundColor = 'red';
dom.appendChild(div)
const bt1 = document.createElement('button');
bt1.innerText = '最小化';
bt1.addEventListener('click', () => {
win.minimize();
})
div.appendChild(bt1)
const bt2 = document.createElement('button');
bt2.innerText = '最大化';
bt2.addEventListener('click', () => {
win.maximize();
})
div.appendChild(bt2)
const bt3 = document.createElement('button');
bt3.innerText = '关闭';
bt3.addEventListener('click', () => {
this.desktopRootDom.removeChild(dom)
win.destroy();
this.proc?.windowForms.delete(this.id);
processManager.removeProcess(this.proc!)
})
div.appendChild(bt3)
const win = new DraggableResizableWindow({
target: dom,
handle: div,
mode: 'position',
snapThreshold: 20,
boundary: document.body,
taskbarElementId: '#taskbar',
})
this.desktopRootDom.appendChild(dom);
}
}

View File

@@ -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;
}

View File

@@ -1,7 +0,0 @@
/**
* 窗体位置坐标 - 左上角
*/
export interface WindowFormPos {
x: number;
y: number;
}

View File

@@ -45,7 +45,7 @@ body {
background-color: var(--color-light); background-color: var(--color-light);
-webkit-font-smoothing: antialiased; /* 字体抗锯齿 */ -webkit-font-smoothing: antialiased; /* 字体抗锯齿 */
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
overflow-x: hidden; overflow: hidden;
} }
/* ===== 排版元素 ===== */ /* ===== 排版元素 ===== */

View File

@@ -0,0 +1,93 @@
import type { IDestroyable } from '@/common/types/IDestroyable'
import type { WindowState } from '@/services/windowForm/WindowFormService.ts'
import type { ResourceType } from '@/services/ResourceService'
/**
* 窗体数据更新参数
*/
export interface IWindowFormDataUpdateParams {
/** 窗口id */
id: string
/** 窗口状态 */
state: WindowState
/** 窗口宽度 */
width: number
/** 窗口高度 */
height: number
/** 窗口x坐标(左上角) */
x: number
/** 窗口y坐标(左上角) */
y: number
}
/**
* 事件定义
* @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
*/
export interface IEventMap {
/**
* 事件处理函数映射
*/
[key: string]: (...args: any[]) => void
}
export interface ISystemBuiltInEventMap extends IEventMap {
// 系统就绪事件
onSystemReady: (data: { timestamp: Date; services: string[] }) => void
// 窗体相关事件
onWindowStateChanged: (windowId: string, newState: string, oldState: string) => void
onWindowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void
onWindowFormResizeStart: (windowId: string) => void
onWindowFormResizing: (windowId: string, width: number, height: number) => void
onWindowFormResizeEnd: (windowId: string) => void
onWindowClose: (windowId: string) => void
// 应用生命周期事件
onAppLifecycle: (data: { appId: string; event: string; timestamp: Date }) => void
// 资源相关事件
onResourceQuotaExceeded: (appId: string, resourceType: ResourceType) => void
onPerformanceAlert: (data: { type: 'memory' | 'cpu'; usage: number; limit: number }) => void
}
/**
* 事件管理器接口定义
*/
export interface IEventBuilder<Events extends IEventMap> extends IDestroyable {
/**
* 订阅事件
* @param eventName 事件名称
* @param handler 事件处理函数
* @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
* @returns void
*/
subscribe<E extends keyof Events, F extends Events[E]>(
eventName: E,
handler: F,
options?: {
immediate?: boolean
immediateArgs?: Parameters<F>
once?: boolean
}
): () => void
/**
* 移除事件
* @param eventName 事件名称
* @param handler 事件处理函数
* @returns void
*/
remove<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
/**
* 触发事件
* @param eventName 事件名称
* @param args 参数
* @returns void
*/
notify<E extends keyof Events, F extends Events[E]>(
eventName: E,
...args: Parameters<F>
): void
}

View File

@@ -1,13 +1,11 @@
import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts' import type { IEventBuilder, IEventMap } from '../IEventBuilder.ts'
interface HandlerWrapper<T extends (...args: any[]) => any> { interface HandlerWrapper<T extends (...args: any[]) => any> {
fn: T fn: T
once: boolean once: boolean
} }
export class EventBuilderImpl<Events extends IEventMap> export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder<Events> {
implements IEventBuilder<Events>
{
private _eventHandlers = new Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>() private _eventHandlers = new Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>()
/** /**
@@ -15,28 +13,30 @@ export class EventBuilderImpl<Events extends IEventMap>
* @param eventName * @param eventName
* @param handler * @param handler
* @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 } * @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
* @returns `unsubscribe`
* @example * @example
* eventBus.addEventListener('noArgs', () => {}) * eventBus.subscribe('noArgs', () => {})
* eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] }) * eventBus.subscribe('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
* eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] }) * eventBus.subscribe('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] })
*/ */
addEventListener<E extends keyof Events, F extends Events[E]>( subscribe<E extends keyof Events, F extends Events[E]>(
eventName: E, eventName: E,
handler: F, handler: F,
options?: { options?: {
immediate?: boolean; immediate?: boolean
immediateArgs?: Parameters<F>; immediateArgs?: Parameters<F>
once?: boolean; once?: boolean
}, },
) { ): () => void {
if (!handler) return if (!handler) return () => {}
if (!this._eventHandlers.has(eventName)) { if (!this._eventHandlers.has(eventName)) {
this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>()) this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>())
} }
const set = this._eventHandlers.get(eventName)! const set = this._eventHandlers.get(eventName)!
const wrapper: HandlerWrapper<F> = { fn: handler, once: options?.once ?? false }
if (![...set].some((wrapper) => wrapper.fn === handler)) { if (![...set].some((wrapper) => wrapper.fn === handler)) {
set.add({ fn: handler, once: options?.once ?? false }) set.add(wrapper)
} }
if (options?.immediate) { if (options?.immediate) {
@@ -46,6 +46,12 @@ export class EventBuilderImpl<Events extends IEventMap>
console.error(e) console.error(e)
} }
} }
return () => {
set.delete(wrapper)
// 如果该事件下无监听器,则删除集合
if (set.size === 0) this._eventHandlers.delete(eventName)
}
} }
/** /**
@@ -53,9 +59,9 @@ export class EventBuilderImpl<Events extends IEventMap>
* @param eventName * @param eventName
* @param handler * @param handler
* @example * @example
* eventBus.removeEventListener('noArgs', () => {}) * eventBus.remove('noArgs', () => {})
*/ */
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F) { remove<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F) {
const set = this._eventHandlers.get(eventName) const set = this._eventHandlers.get(eventName)
if (!set) return if (!set) return
@@ -64,6 +70,9 @@ export class EventBuilderImpl<Events extends IEventMap>
set.delete(wrapper) set.delete(wrapper)
} }
} }
if (set.size === 0) {
this._eventHandlers.delete(eventName)
}
} }
/** /**
@@ -71,11 +80,11 @@ export class EventBuilderImpl<Events extends IEventMap>
* @param eventName * @param eventName
* @param args * @param args
* @example * @example
* eventBus.notifyEvent('noArgs') * eventBus.notify('noArgs')
* eventBus.notifyEvent('greet', 'Alice') * eventBus.notify('greet', 'Alice')
* eventBus.notifyEvent('onResize', 1, 2) * eventBus.notify('onResize', 1, 2)
*/ */
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>) { notify<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>) {
if (!this._eventHandlers.has(eventName)) return if (!this._eventHandlers.has(eventName)) return
const set = this._eventHandlers.get(eventName)! const set = this._eventHandlers.get(eventName)!
@@ -91,4 +100,8 @@ export class EventBuilderImpl<Events extends IEventMap>
} }
} }
} }
destroy() {
this._eventHandlers.clear()
}
} }

View File

@@ -1,16 +1,61 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' 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 'virtual:uno.css'
import './css/basic.css' import './css/basic.css'
// import App from './App.vue' import App from './ui/App.vue'
// const app = createApp(App) // 注册内置应用
// registerBuiltInApps()
// app.use(createPinia())
//
// app.mount('#app')
import XSystem from '@/core/XSystem.ts' // 初始化系统服务
XSystem.instance.initialization(document.querySelector('#app')!); const systemService = new SystemServiceIntegration({
debug: import.meta.env.DEV
})
// 创建应用实例
const app = createApp(App)
// 注册插件
app.use(createPinia())
app.use(naiveUi)
// 提供系统服务给组件使用
app.provide('systemService', systemService)
// 初始化系统服务然后挂载应用
systemService
.initialize()
.then(() => {
app.mount('#app')
console.log('桌面系统启动完成')
})
.catch((error) => {
console.error('系统启动失败:', error)
// 显示错误信息
document.body.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: sans-serif;">
<div style="text-align: center; color: #e74c3c;">
<h1>系统启动失败</h1>
<p>错误信息: ${error.message}</p>
<button onclick="location.reload()" style="padding: 10px 20px; margin-top: 20px; cursor: pointer;">
重新加载
</button>
</div>
</div>
`
})
// 全局错误处理
app.config.errorHandler = (error, instance, info) => {
console.error('Vue应用错误:', error, info)
}
// 在页面卸载时清理系统服务
window.addEventListener('beforeunload', () => {
systemService.shutdown()
})

702
src/sdk/index.ts Normal file
View File

@@ -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<T = any>(type: string, data?: any): Promise<T> {
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<T>(promise: Promise<T>): Promise<APIResponse<T>> {
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<keyof WindowEvents, Set<Function>>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async setTitle(title: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.setTitle', { title }))
}
async resize(width: number, height: number): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.resize', { width, height }))
}
async move(x: number, y: number): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.move', { x, y }))
}
async minimize(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.minimize'))
}
async maximize(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.maximize'))
}
async restore(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.restore'))
}
async fullscreen(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.fullscreen'))
}
async close(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.close'))
}
async getState(): Promise<APIResponse<WindowState>> {
return this.wrapResponse(this.sendToSystem('window.getState'))
}
async getSize(): Promise<APIResponse<{ width: number; height: number }>> {
return this.wrapResponse(this.sendToSystem('window.getSize'))
}
async getPosition(): Promise<APIResponse<{ x: number; y: number }>> {
return this.wrapResponse(this.sendToSystem('window.getPosition'))
}
on<K extends keyof WindowEvents>(event: K, callback: WindowEvents[K]): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set())
}
this.eventListeners.get(event)!.add(callback)
}
off<K extends keyof WindowEvents>(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<keyof StorageEvents, Set<Function>>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async set(key: string, value: any): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.set', { key, value }))
}
async get<T = any>(key: string): Promise<APIResponse<T | null>> {
return this.wrapResponse(this.sendToSystem('storage.getWindowForm', { key }))
}
async remove(key: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.removeWindowForm', { key }))
}
async clear(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.clear'))
}
async keys(): Promise<APIResponse<string[]>> {
return this.wrapResponse(this.sendToSystem('storage.keys'))
}
async has(key: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.has', { key }))
}
async getStats(): Promise<APIResponse<any>> {
return this.wrapResponse(this.sendToSystem('storage.getStats'))
}
on<K extends keyof StorageEvents>(event: K, callback: StorageEvents[K]): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set())
}
this.eventListeners.get(event)!.add(callback)
}
off<K extends keyof StorageEvents>(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<T = any>(
url: string,
config?: NetworkRequestConfig,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.wrapResponse(this.sendToSystem('network.request', { url, config }))
}
async get<T = any>(
url: string,
config?: Omit<NetworkRequestConfig, 'method'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'GET' })
}
async post<T = any>(
url: string,
data?: any,
config?: Omit<NetworkRequestConfig, 'method' | 'body'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'POST', body: data })
}
async put<T = any>(
url: string,
data?: any,
config?: Omit<NetworkRequestConfig, 'method' | 'body'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'PUT', body: data })
}
async delete<T = any>(
url: string,
config?: Omit<NetworkRequestConfig, 'method'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'DELETE' })
}
async upload(
url: string,
file: File | Blob,
onProgress?: (loaded: number, total: number) => void,
): Promise<APIResponse<NetworkResponse>> {
return this.wrapResponse(
this.sendToSystem('network.upload', { url, file, hasProgressCallback: !!onProgress }),
)
}
async download(
url: string,
filename?: string,
onProgress?: (loaded: number, total: number) => void,
): Promise<APIResponse<Blob>> {
return this.wrapResponse(
this.sendToSystem('network.download', { url, filename, hasProgressCallback: !!onProgress }),
)
}
async isOnline(): Promise<APIResponse<boolean>> {
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<string, Function>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async emit<T = any>(channel: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.emit', { channel, data }))
}
async on<T = any>(
channel: string,
callback: (message: EventMessage<T>) => void,
config?: EventSubscriptionConfig,
): Promise<APIResponse<string>> {
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<APIResponse<boolean>> {
const result = await this.wrapResponse(this.sendToSystem('events.off', { subscriptionId }))
if (result.success) {
this.subscriptions.delete(subscriptionId)
}
return result
}
async broadcast<T = any>(channel: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.broadcast', { channel, data }))
}
async sendTo<T = any>(targetAppId: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.sendTo', { targetAppId, data }))
}
async getSubscriberCount(channel: string): Promise<APIResponse<number>> {
return this.wrapResponse(this.sendToSystem('events.getSubscriberCount', { channel }))
}
async getChannels(): Promise<APIResponse<string[]>> {
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<APIResponse<{ buttonIndex: number; inputValue?: string }>> {
return this.wrapResponse(this.sendToSystem('ui.showDialog', options))
}
async showNotification(options: NotificationOptions): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showNotification', options))
}
async showFilePicker(options?: FilePickerOptions): Promise<APIResponse<FileList | null>> {
return this.wrapResponse(this.sendToSystem('ui.showFilePicker', options))
}
async showSaveDialog(defaultName?: string, accept?: string): Promise<APIResponse<string | null>> {
return this.wrapResponse(this.sendToSystem('ui.showSaveDialog', { defaultName, accept }))
}
async showToast(
message: string,
type?: 'info' | 'success' | 'warning' | 'error',
duration?: number,
): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showToast', { message, type, duration }))
}
async showLoading(message?: string): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showLoading', { message }))
}
async hideLoading(id: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('ui.hideLoading', { id }))
}
async showProgress(options: {
title?: string
message?: string
progress: number
}): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showProgress', options))
}
async updateProgress(
id: string,
progress: number,
message?: string,
): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('ui.updateProgress', { id, progress, message }))
}
async hideProgress(id: string): Promise<APIResponse<boolean>> {
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<APIResponse<SystemInfo>> {
return this.wrapResponse(this.sendToSystem('system.getSystemInfo'))
}
async getAppInfo(): Promise<APIResponse<AppInfo>> {
return this.wrapResponse(this.sendToSystem('system.getAppInfo'))
}
async requestPermission(
permission: string,
reason?: string,
): Promise<APIResponse<PermissionStatus>> {
return this.wrapResponse(this.sendToSystem('system.requestPermission', { permission, reason }))
}
async checkPermission(permission: string): Promise<APIResponse<PermissionStatus>> {
return this.wrapResponse(this.sendToSystem('system.checkPermission', { permission }))
}
async getClipboard(): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('system.getClipboard'))
}
async setClipboard(text: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('system.setClipboard', { text }))
}
async openExternal(url: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('system.openExternal', { url }))
}
async getCurrentTime(): Promise<APIResponse<Date>> {
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<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('system.generateUUID'))
}
async exit(): Promise<APIResponse<boolean>> {
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<APIResponse<boolean>> {
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<APIResponse<boolean>> {
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<T = any>(
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
}

809
src/sdk/types.ts Normal file
View File

@@ -0,0 +1,809 @@
/**
* 系统SDK主接口
* 为第三方应用提供统一的系统服务访问接口
*/
// =============================================================================
// 核心类型定义
// =============================================================================
/**
* SDK初始化配置
*/
export interface SDKConfig {
appId: string
appName: string
version: string
permissions: string[]
debug?: boolean
}
/**
* API响应结果包装器
*/
export interface APIResponse<T = any> {
success: boolean
data?: T
error?: string
code?: number
}
/**
* 事件回调函数类型
*/
export type EventCallback<T = any> = (data: T) => void | Promise<void>
/**
* 权限状态枚举
*/
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<APIResponse<boolean>>
/**
* 调整窗体尺寸
*/
resize(width: number, height: number): Promise<APIResponse<boolean>>
/**
* 移动窗体位置
*/
move(x: number, y: number): Promise<APIResponse<boolean>>
/**
* 最小化窗体
*/
minimize(): Promise<APIResponse<boolean>>
/**
* 最大化窗体
*/
maximize(): Promise<APIResponse<boolean>>
/**
* 还原窗体
*/
restore(): Promise<APIResponse<boolean>>
/**
* 全屏显示
*/
fullscreen(): Promise<APIResponse<boolean>>
/**
* 关闭窗体
*/
close(): Promise<APIResponse<boolean>>
/**
* 获取当前窗体状态
*/
getState(): Promise<APIResponse<WindowState>>
/**
* 获取窗体尺寸
*/
getSize(): Promise<APIResponse<{ width: number; height: number }>>
/**
* 获取窗体位置
*/
getPosition(): Promise<APIResponse<{ x: number; y: number }>>
/**
* 监听窗体事件
*/
on<K extends keyof WindowEvents>(event: K, callback: WindowEvents[K]): void
/**
* 移除事件监听器
*/
off<K extends keyof WindowEvents>(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 {
/**
* 已使用存储空间(MB)
*/
usedSpace: number // 已使用空间(MB)
/**
* 最大存储空间(MB)
*/
maxSpace: number // 最大空间(MB)
/**
* 存储键数量
*/
keysCount: number // 键数量
/**
* 最后访问时间
*/
lastAccessed: Date
}
/**
* 存储SDK接口
*/
export interface StorageSDK {
/**
* 存储数据
*/
set(key: string, value: any): Promise<APIResponse<boolean>>
/**
* 获取数据
*/
get<T = any>(key: string): Promise<APIResponse<T | null>>
/**
* 删除数据
*/
remove(key: string): Promise<APIResponse<boolean>>
/**
* 清空所有数据
*/
clear(): Promise<APIResponse<boolean>>
/**
* 获取所有键名
*/
keys(): Promise<APIResponse<string[]>>
/**
* 检查键是否存在
*/
has(key: string): Promise<APIResponse<boolean>>
/**
* 获取存储使用统计
*/
getStats(): Promise<APIResponse<StorageStats>>
/**
* 监听存储变化
*/
on<K extends keyof StorageEvents>(event: K, callback: StorageEvents[K]): void
/**
* 移除事件监听器
*/
off<K extends keyof StorageEvents>(event: K, callback?: StorageEvents[K]): void
}
// =============================================================================
// 网络SDK接口
// =============================================================================
/**
* HTTP方法
*/
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
/**
* 网络请求配置
*/
/**
* 网络请求配置
*/
export interface NetworkRequestConfig {
/**
* HTTP请求方法
*/
method?: HTTPMethod
/**
* 请求头信息
*/
headers?: Record<string, string>
/**
* 请求体数据
*/
body?: any
/**
* 请求超时时间(毫秒)
*/
timeout?: number
/**
* 响应数据类型
*/
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
}
/**
* 网络响应
*/
/**
* 网络响应
*/
export interface NetworkResponse<T = any> {
/**
* 响应数据内容
*/
data: T
/**
* HTTP状态码
*/
status: number
/**
* HTTP状态文本
*/
statusText: string
/**
* 响应头信息
*/
headers: Record<string, string>
/**
* 请求URL
*/
url: string
}
/**
* 上传进度回调
*/
export type UploadProgressCallback = (loaded: number, total: number) => void
/**
* 下载进度回调
*/
export type DownloadProgressCallback = (loaded: number, total: number) => void
/**
* 网络SDK接口
*/
export interface NetworkSDK {
/**
* 发送HTTP请求
*/
request<T = any>(url: string, config?: NetworkRequestConfig): Promise<APIResponse<NetworkResponse<T>>>
/**
* GET请求
*/
get<T = any>(url: string, config?: Omit<NetworkRequestConfig, 'method'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* POST请求
*/
post<T = any>(url: string, data?: any, config?: Omit<NetworkRequestConfig, 'method' | 'body'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* PUT请求
*/
put<T = any>(url: string, data?: any, config?: Omit<NetworkRequestConfig, 'method' | 'body'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* DELETE请求
*/
delete<T = any>(url: string, config?: Omit<NetworkRequestConfig, 'method'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* 上传文件
*/
upload(url: string, file: File | Blob, onProgress?: UploadProgressCallback): Promise<APIResponse<NetworkResponse>>
/**
* 下载文件
*/
download(url: string, filename?: string, onProgress?: DownloadProgressCallback): Promise<APIResponse<Blob>>
/**
* 检查网络状态
*/
isOnline(): Promise<APIResponse<boolean>>
/**
* 获取网络请求统计
*/
getStats(): Promise<APIResponse<{ requestCount: number; failureCount: number; averageTime: number }>>
}
// =============================================================================
// 事件SDK接口
// =============================================================================
/**
* 事件消息
*/
/**
* 事件消息
*/
export interface EventMessage<T = any> {
/**
* 消息唯一标识符
*/
id: string
/**
* 事件频道名称
*/
channel: string
/**
* 消息数据内容
*/
data: T
/**
* 发送方标识符
*/
senderId: string
/**
* 消息发送时间戳
*/
timestamp: Date
}
/**
* 事件订阅配置
*/
/**
* 事件订阅配置
*/
export interface EventSubscriptionConfig {
/**
* 消息过滤器函数
*/
filter?: (message: EventMessage) => boolean
/**
* 是否只监听一次
*/
once?: boolean // 只监听一次
}
/**
* 事件SDK接口
*/
export interface EventSDK {
/**
* 发送事件消息
*/
emit<T = any>(channel: string, data: T): Promise<APIResponse<string>>
/**
* 订阅事件频道
*/
on<T = any>(
channel: string,
callback: (message: EventMessage<T>) => void,
config?: EventSubscriptionConfig
): Promise<APIResponse<string>>
/**
* 取消订阅
*/
off(subscriptionId: string): Promise<APIResponse<boolean>>
/**
* 广播消息
*/
broadcast<T = any>(channel: string, data: T): Promise<APIResponse<string>>
/**
* 发送点对点消息
*/
sendTo<T = any>(targetAppId: string, data: T): Promise<APIResponse<string>>
/**
* 获取频道订阅者数量
*/
getSubscriberCount(channel: string): Promise<APIResponse<number>>
/**
* 获取可用频道列表
*/
getChannels(): Promise<APIResponse<string[]>>
}
// =============================================================================
// 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
/**
* 通知图标URL
*/
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<APIResponse<{ buttonIndex: number; inputValue?: string }>>
/**
* 显示通知
*/
showNotification(options: NotificationOptions): Promise<APIResponse<string>>
/**
* 显示文件选择器
*/
showFilePicker(options?: FilePickerOptions): Promise<APIResponse<FileList | null>>
/**
* 显示保存文件对话框
*/
showSaveDialog(defaultName?: string, accept?: string): Promise<APIResponse<string | null>>
/**
* 显示Toast消息
*/
showToast(message: string, type?: 'info' | 'success' | 'warning' | 'error', duration?: number): Promise<APIResponse<string>>
/**
* 显示加载指示器
*/
showLoading(message?: string): Promise<APIResponse<string>>
/**
* 隐藏加载指示器
*/
hideLoading(id: string): Promise<APIResponse<boolean>>
/**
* 显示进度条
*/
showProgress(options: { title?: string; message?: string; progress: number }): Promise<APIResponse<string>>
/**
* 更新进度条
*/
updateProgress(id: string, progress: number, message?: string): Promise<APIResponse<boolean>>
/**
* 隐藏进度条
*/
hideProgress(id: string): Promise<APIResponse<boolean>>
}
// =============================================================================
// 系统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<APIResponse<SystemInfo>>
/**
* 获取当前应用信息
*/
getAppInfo(): Promise<APIResponse<AppInfo>>
/**
* 请求权限
*/
requestPermission(permission: string, reason?: string): Promise<APIResponse<PermissionStatus>>
/**
* 检查权限状态
*/
checkPermission(permission: string): Promise<APIResponse<PermissionStatus>>
/**
* 获取剪贴板内容
*/
getClipboard(): Promise<APIResponse<string>>
/**
* 设置剪贴板内容
*/
setClipboard(text: string): Promise<APIResponse<boolean>>
/**
* 打开外部链接
*/
openExternal(url: string): Promise<APIResponse<boolean>>
/**
* 获取当前时间
*/
getCurrentTime(): Promise<APIResponse<Date>>
/**
* 生成UUID
*/
generateUUID(): Promise<APIResponse<string>>
/**
* 退出应用
*/
exit(): Promise<APIResponse<boolean>>
}
// =============================================================================
// 主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<APIResponse<boolean>>
/**
* 销毁SDK
*/
destroy(): Promise<APIResponse<boolean>>
/**
* 获取SDK状态
*/
getStatus(): Promise<APIResponse<{ initialized: boolean; connected: boolean; permissions: string[] }>>
}
// =============================================================================
// 全局类型声明
// =============================================================================
declare global {
interface Window {
/**
* 系统桌面SDK全局实例
*/
SystemSDK: SystemDesktopSDK
}
}

View File

@@ -0,0 +1,570 @@
import { reactive, ref } from 'vue'
import type { ResourceService } from './ResourceService'
import type { ApplicationSandboxEngine } from './ApplicationSandboxEngine'
import { v4 as uuidv4 } from 'uuid'
import { externalAppDiscovery } from './ExternalAppDiscovery'
import type { WindowFormService } from '@/services/windowForm/WindowFormService.ts'
import { type AppRegistration, appRegistry } from '@/apps'
import windowManager from '@/ui/components/WindowManager.vue'
/**
* 应用状态枚举
*/
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<string, string>
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 AppStartOptions {
windowConfig?: {
x?: number
y?: number
width?: number
height?: number
state?: 'normal' | 'minimized' | 'maximized'
}
args?: Record<string, any>
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
}
interface BuiltInWindowForm {
id: string
appId: string
component: any
props: Record<string, any>
}
/**
* 应用生命周期管理器
*/
export class ApplicationLifecycleManager {
private installedApps = new Map<string, AppInstance>()
private runningProcesses = new Map<string, AppInstance>()
private builtInWindowForms = new Map<string, BuiltInWindowForm>()
private windowFormService: WindowFormService
private resourceService: ResourceService
private sandboxEngine: ApplicationSandboxEngine
constructor(
windowFormService: WindowFormService,
resourceService: ResourceService,
sandboxEngine: ApplicationSandboxEngine
) {
this.windowFormService = windowFormService
this.resourceService = resourceService
this.sandboxEngine = sandboxEngine
}
public getBuiltInWindowForms() {
return this.builtInWindowForms
}
/**
* 启动应用
*/
async startApp(appId: string, options: AppStartOptions = {}) {
// 检查应用是否是内置应用
if (appRegistry.hasApp(appId)) {
await this.startBuiltInApp(appId, options)
} else {
await this.startExternalApp(appId, options)
}
}
private async startBuiltInApp(appId: string, options: AppStartOptions = {}) {
const appRegistration = appRegistry.getApp(appId)
if (!appRegistration) {
console.error(`内置应用 ${appId} 不存在`)
return
}
// 检查是否已在运行
if (this.isAppRunning(appId)) {
console.log(`应用 ${appRegistration.manifest.name} 已在运行`)
return
}
const windowInstance = await this.windowFormService.createWindow(appId, appRegistration.manifest.window)
// 处理异步组件加载
let component = appRegistration.component
if (typeof component === 'function') {
try {
// 如果是函数,调用它来获取组件
component = await component()
} catch (error) {
console.error(`加载应用组件失败: ${appId}`, error)
return
}
}
// 添加窗口
const windowForm: BuiltInWindowForm = {
id: windowInstance.id,
appId,
component,
props: {
windowId: windowInstance.id,
appId
}
}
this.builtInWindowForms.set(windowInstance.id, windowForm)
console.log(this.builtInWindowForms)
}
private async startExternalApp(appId: string, options: AppStartOptions = {}) {
let app = this.installedApps.get(appId)
console.log('-----------------------------')
// 如果应用未安装,检查是否为外置应用
let isExternalApp = false
if (!app) {
const externalApp = externalAppDiscovery.getApp(appId)
if (externalApp) {
console.log(`[LifecycleManager] 发现外置应用 ${appId}`)
isExternalApp = true
// 为外部应用创建临时实例
const now = new Date()
app = {
id: externalApp.manifest.id,
manifest: externalApp.manifest,
state: AppLifecycleState.INSTALLED,
processId: '',
installedAt: now,
errorCount: 0,
crashCount: 0,
memoryUsage: 0,
cpuUsage: 0,
version: externalApp.manifest.version,
autoStart: false,
persistent: false
}
}
}
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 = true
// 创建窗体(如果不是后台应用)
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.windowFormService.createWindow(appId, windowConfig)
windowId = windowInstance.id
app.windowId = windowId
}
// 对于外置应用,需要创建沙箱;内置应用跳过沙箱
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<boolean> {
// 首先从已安装应用中查找
let app = this.installedApps.get(appId)
// 如果未找到,从运行进程列表中查找(可能是外部应用的临时实例)
if (!app) {
for (const runningApp of this.runningProcesses.values()) {
if (runningApp.id === appId) {
app = runningApp
break
}
}
}
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.windowFormService.getWindow(app.windowId)
if (window) {
await this.windowFormService.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
}
}
/**
* 获取应用信息
*/
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 {
let app;
for (const runningApp of this.runningProcesses.values()) {
if (runningApp.id === appId) {
app = runningApp
break
}
}
return app?.state === AppLifecycleState.RUNNING
}
/**
* 获取应用统计信息
*/
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
}
}
/**
* 检查权限
*/
private async checkPermissions(permissions: string[]): Promise<void> {
for (const permission of permissions) {
// 这里可以添加权限检查逻辑
// 目前简单允许所有权限
console.log(`检查权限: ${permission}`)
}
}
/**
* 为外置应用加载代码到沙箱
*/
private async loadExternalAppInSandbox(app: AppInstance, sandboxId: string): Promise<void> {
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<void> {
try {
await externalAppDiscovery.refresh()
console.log('[LifecycleManager] 外置应用列表已刷新')
} catch (error) {
console.error('[LifecycleManager] 刷新外置应用列表失败:', error)
}
}
/**
* 处理应用错误
*/
private handleAppError(appId: string, error: Error): void {
const app = this.installedApps.get(appId)
if (!app) return
app.errorCount++
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
// })
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<string, ExternalApp>())
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<void> {
// 防止重复启动
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<void> {
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<string, ExternalApp>()
// 扫描每个应用目录
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<string[]> {
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<string[]> {
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('<!DOCTYPE html') || html.includes('<html')) {
console.log('[ExternalAppDiscovery] 响应是HTML页面不是目录列表')
throw new Error('服务器返回HTML页面而不是目录列表')
}
const directories = this.parseDirectoryListing(html)
if (directories.length === 0) {
throw new Error('未从目录列表中解析到任何应用目录')
}
return directories
} catch (error) {
console.warn('[ExternalAppDiscovery] 目录列表扫描失败:', error)
throw error
}
}
/**
* 尝试扫描常见的应用名称
*/
private async tryCommonAppNames(): Promise<string[]> {
// 排除内置应用,只扫描外部应用
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<string[]> {
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<string[]> {
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<ExternalApp | null> {
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<string, ExternalApp>): 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<void> {
console.log('[ExternalAppDiscovery] 手动刷新应用列表')
await this.scanExternalApps()
}
}
// 导出单例实例
export const externalAppDiscovery = ExternalAppDiscovery.getInstance()

View File

@@ -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<string, Map<ResourceType, PermissionRequest>>())
private networkRequests = reactive(new Map<string, NetworkRequest[]>())
private storageUsage = reactive(new Map<string, StorageUsage>())
private defaultConfig: ResourceAccessConfig
private eventBus: IEventBuilder<ResourceEvents>
constructor(eventBus: IEventBuilder<ResourceEvents>) {
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<PermissionLevel> {
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.notify('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.notify('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.notify('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<boolean> {
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.notify('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<any> {
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<boolean> {
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<boolean> {
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<Response | null> {
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.notify('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.notify('onNetworkRequest', requestRecord)
return response
} catch (error) {
console.error('网络请求失败:', error)
return null
}
}
/**
* 显示通知
*/
async showNotification(
appId: string,
title: string,
options?: NotificationOptions,
): Promise<boolean> {
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<string | null> {
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<boolean> {
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<PermissionLevel> {
// 对于本地存储,默认授权
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.notify('onStorageChange', appId, usage)
}
/**
* 重置存储使用情况
*/
private resetStorageUsage(appId: string): void {
const usage = this.getStorageUsage(appId)
usage.usedSpace = 0
usage.lastAccessed = new Date()
this.eventBus.notify('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)
}
}
})
}
}

View File

@@ -0,0 +1,767 @@
import { reactive, ref } from 'vue'
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl'
import type {
IEventBuilder,
ISystemBuiltInEventMap,
IWindowFormDataUpdateParams
} from '@/events/IEventBuilder'
import type { ResourceType } from './ResourceService'
// 导入所有服务
import { WindowFormService } from './windowForm/WindowFormService.ts'
import { ResourceService } from './ResourceService'
import { ApplicationSandboxEngine } from './ApplicationSandboxEngine'
import { ApplicationLifecycleManager } from './ApplicationLifecycleManager'
import { externalAppDiscovery } from './ExternalAppDiscovery'
/**
* 系统服务配置接口
*/
export interface SystemServiceConfig {
debug?: boolean // 是否开启调试模式
autoCleanup?: boolean // 是否自动清理
cleanupInterval?: number // 自动清理间隔
}
/**
* 系统状态接口
*/
export interface SystemStatus {
initialized: boolean // 系统是否初始化完成
running: boolean // 系统是否运行中
servicesStatus: {
windowFormService: boolean // 窗体服务是否启动
resourceService: boolean // 资源服务是否启动
sandboxEngine: boolean // 沙箱引擎是否启动
lifecycleManager: boolean // 生命周期管理器是否启动
}
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<ISystemBuiltInEventMap>
private windowFormService!: WindowFormService
private resourceService!: ResourceService
private sandboxEngine!: ApplicationSandboxEngine
private lifecycleManager!: ApplicationLifecycleManager
// 系统状态
private systemStatus = reactive<SystemStatus>({
initialized: false,
running: false,
servicesStatus: {
windowFormService: false,
resourceService: false,
sandboxEngine: false,
lifecycleManager: false
},
uptime: 0
})
// 性能监控
private cleanupInterval: number | null = null
private performanceInterval: number | null = null
constructor(config: SystemServiceConfig = {}) {
this.config = {
debug: false,
autoCleanup: true,
cleanupInterval: 5 * 60 * 1000, // 5分钟
...config
}
this.startTime = new Date()
this.eventBus = new EventBuilderImpl<ISystemBuiltInEventMap>()
this.setupGlobalErrorHandling()
}
/**
* 初始化系统服务
*/
public async initialize(): Promise<void> {
if (this.initialized.value) {
throw new Error('系统服务已初始化')
}
try {
console.log('开始初始化系统服务...')
// 按依赖顺序初始化服务
await this.initializeServices()
// 设置服务间通信
this.setupServiceCommunication()
// 设置SDK消息处理
this.setupSDKMessageHandling()
// 启动自动清理
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('系统服务初始化完成')
} catch (error) {
console.error('系统服务初始化失败:', error)
this.systemStatus.lastError = error instanceof Error ? error.message : String(error)
throw error
}
}
/**
* 获取系统状态
*/
getSystemStatus(): SystemStatus {
return { ...this.systemStatus }
}
/**
* 获取窗体服务
*/
getWindowFormService(): WindowFormService {
this.checkInitialized()
return this.windowFormService
}
/**
* 获取资源服务
*/
getResourceService(): ResourceService {
this.checkInitialized()
return this.resourceService
}
/**
* 获取沙箱引擎
*/
getSandboxEngine(): ApplicationSandboxEngine {
this.checkInitialized()
return this.sandboxEngine
}
/**
* 获取生命周期管理器
*/
getLifecycleManager(): ApplicationLifecycleManager {
this.checkInitialized()
return this.lifecycleManager
}
/**
* 处理SDK调用
*/
async handleSDKCall(call: SDKCall): Promise<any> {
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<void> {
console.log('重启系统服务...')
await this.shutdown()
await new Promise((resolve) => setTimeout(resolve, 1000))
await this.initialize()
console.log('系统服务重启完成')
}
/**
* 关闭系统服务
*/
public async shutdown(): Promise<void> {
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.windowFormService) {
// 关闭所有窗体
const windows = this.windowFormService.getAllWindows()
for (const window of windows) {
await this.windowFormService.destroyWindow(window.id)
}
}
} catch (error) {
console.error('关闭服务时发生错误:', error)
}
this.initialized.value = false
this.systemStatus.initialized = false
console.log('系统服务已关闭')
}
/**
* 初始化所有服务
*/
private async initializeServices(): Promise<void> {
// 1. 初始化资源服务
console.log('初始化资源服务...')
this.resourceService = new ResourceService(this.eventBus)
this.systemStatus.servicesStatus.resourceService = true
// 3. 初始化窗体服务
console.log('初始化窗体服务...')
this.windowFormService = new WindowFormService(this.eventBus)
this.systemStatus.servicesStatus.windowFormService = true
// 4. 初始化沙箱引擎
console.log('初始化沙箱引擎...')
this.sandboxEngine = new ApplicationSandboxEngine(this.resourceService)
this.systemStatus.servicesStatus.sandboxEngine = true
// 5. 初始化生命周期管理器
console.log('初始化生命周期管理器...')
this.lifecycleManager = new ApplicationLifecycleManager(
this.windowFormService,
this.resourceService,
this.sandboxEngine
)
this.systemStatus.servicesStatus.lifecycleManager = true
}
/**
* 设置服务间通信
*/
private setupServiceCommunication(): void {
// 监听窗体状态变化(来自 windowFormService 的 onStateChange 事件)
this.eventBus.subscribe(
'onWindowStateChanged',
(windowId: string, newState: string, oldState: string) => {
console.log(
`[SystemIntegration] 接收到窗体状态变化事件: ${windowId} ${oldState} -> ${newState}`
)
}
)
// 监听窗体关闭事件,自动停止对应的应用
this.eventBus.subscribe('onWindowClose', 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.subscribe('onWindowFormDataUpdate', (data: IWindowFormDataUpdateParams) => {
console.log(`[SystemIntegration] 接收到窗体数据更新事件:`, data)
})
// 监听窗体调整尺寸开始事件
this.eventBus.subscribe('onWindowFormResizeStart', (windowId: string) => {
console.log(`[SystemIntegration] 接收到窗体调整尺寸开始事件: ${windowId}`)
})
// 监听窗体调整尺寸过程中事件
this.eventBus.subscribe(
'onWindowFormResizing',
(windowId: string, width: number, height: number) => {
console.log(`[SystemIntegration] 接收到窗体调整尺寸过程中事件: ${windowId}`, {
width,
height
})
}
)
// 监听窗体调整尺寸结束事件
this.eventBus.subscribe('onWindowFormResizeEnd', (windowId: string) => {
console.log(`[SystemIntegration] 接收到窗体调整尺寸结束事件: ${windowId}`)
})
}
/**
* 设置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<void> {
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:getWindowForm':
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:removeWindowForm':
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<any> {
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<any> {
// 查找应用的窗体
const app = this.lifecycleManager.getApp(appId)
if (!app?.windowId) {
throw new Error('应用窗体不存在')
}
const windowId = app.windowId
switch (action) {
case 'setTitle':
return this.windowFormService.setWindowTitle(windowId, data.title)
case 'resize':
return this.windowFormService.setWindowSize(windowId, data.width, data.height)
case 'move':
// 实现窗体移动功能
const window = this.windowFormService.getWindow(windowId)
if (window && window.element) {
// 更新窗体位置
window.config.x = data.x
window.config.y = data.y
window.element.style.left = `${data.x}px`
window.element.style.top = `${data.y}px`
window.element.style.transform = 'none' // 确保移除transform
return true
}
return false
case 'minimize':
return this.windowFormService.minimizeWindow(windowId)
case 'maximize':
return this.windowFormService.maximizeWindow(windowId)
case 'restore':
return this.windowFormService.restoreWindow(windowId)
case 'close':
return this.lifecycleManager.stopApp(appId)
case 'getState':
const windowInfo = this.windowFormService.getWindow(windowId)
return windowInfo?.state
case 'getSize':
const windowData = this.windowFormService.getWindow(windowId)
return {
width: windowData?.config.width,
height: windowData?.config.height
}
default:
throw new Error(`未知的窗体操作: ${action}`)
}
}
/**
* 执行存储相关方法
*/
private async executeStorageMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'set':
return this.resourceService.setStorage(appId, data.key, data.value)
case 'get':
return this.resourceService.getStorage(appId, data.key)
case 'removeWindowForm':
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<any> {
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<string, string>, // 简化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<any> {
throw new Error('事件服务已被移除')
}
/**
* 执行UI相关方法
*/
private async executeUIMethod(action: string, data: any, appId: string): Promise<any> {
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<any> {
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 startAutoCleanup(): void {
if (!this.config.cleanupInterval) return
this.cleanupInterval = setInterval(() => {
this.debugLog('执行自动清理...')
// 移除资源服务清理(方法不存在)
// this.resourceService.cleanup()
// 移除事件服务清理
// this.eventService.cleanup()
// 清理沙箱引擎缓存
// this.sandboxEngine.cleanup()
this.debugLog('自动清理完成')
}, this.config.cleanupInterval!)
}
/**
* 检查是否已初始化
*/
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)
}
}
}

View File

@@ -0,0 +1,430 @@
import { v4 as uuidv4 } from 'uuid'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
/** 窗口状态枚举 */
export enum EWindowFormState {
/** 创建中 */
CREATING = 'creating',
/** 加载中 */
LOADING = 'loading',
/** 激活 */
ACTIVE = 'active',
/** 未激活 - 在后台,失去焦点 */
INACTIVE = 'inactive',
/** 最小化 */
MINIMIZED = 'minimized',
/** 最大化状态 */
MAXIMIZED = 'maximized',
/** 关闭中 */
CLOSING = 'closing',
/** 销毁 */
DESTROYED = 'destroyed',
/** 错误 */
ERROR = 'error'
}
/** 窗口系统事件接口 */
export interface IWindowFormEvents extends IEventMap {
onWindowFormDataUpdate: (data: {
id: string
state: EWindowFormState
width: number
height: number
x: number
y: number
}) => void
/**
* 窗体创建事件
*/
onCreating: (wid: string) => void
/**
* 窗体加载事件
*/
onLoading: (wid: string) => void
/**
* 窗体加载完成事件
*/
onLoaded: (wid: string) => void
/**
* 窗体聚焦事件
*/
onFocus: (wid: string) => void
/**
* 窗体失焦事件
*/
onBlur: (wid: string) => void
/**
* 窗体激活事件
*/
onActivate: (wid: string) => void
/**
* 窗体失活事件
*/
onDeactivate: (wid: string) => void
/**
* 窗体最小化事件
*/
onMinimize: (wid: string) => void
/**
* 窗体最大化事件
*/
onMaximize: (wid: string) => void
/**
* 窗体还原、恢复事件
*/
onRestore: (wid: string) => void
/**
* 窗体关闭前事件
* @param id 窗体ID
* @param cancel
*/
onBeforeClose: (id: string, cancel: () => void) => void
/**
* 窗体关闭事件
*/
onClose: (wid: string) => void
/**
* 窗体销毁事件
*/
onDestroy: (wid: string) => void
/**
* 窗体错误事件
*/
onError: (wid: string, error: Error) => void
}
/** 窗口配置参数 */
export interface IWindowFormConfig {
/**
* 窗体标题
*/
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坐标位置
*/
x?: number
/**
* 窗体Y坐标位置
*/
y?: number
}
/** 窗口参数 */
export interface IWindowFormData {
/**
* 窗体标题
*/
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坐标位置
*/
x: number
/**
* 窗体Y坐标位置
*/
y: number
}
/** 窗口实例对象 */
export interface IWindowFormInstance {
/** 窗口ID */
id: string
/** 应用ID */
appId: string
/** 窗口参数 */
config: IWindowFormData
/** 窗口状态 */
state: EWindowFormState
/** 窗口ZIndex */
zIndex: number
/** 创建时间 - 时间戳 */
createdAt: number
/** 更新时间 - 时间戳 */
updatedAt: number
/** 窗口DOM元素 */
element?: HTMLElement
/** 窗口iframe元素 */
iframe?: HTMLIFrameElement
/** 记录事件解绑函数 */
subscriptions: (() => void)[]
}
/**
* WindowFormDataManager
* -------------------
* 窗口数据与状态的中心管理器。
* - 管理 Map<id, IWindowFormInstance>
* - 控制 ZIndex
* - 管理状态变更与事件通知
*/
export class WindowFormDataManager {
private windowForms = new Map<string, IWindowFormInstance>()
private activeWindowId: string | null = null
private nextZIndex = 1000
private wfEventBus: IEventBuilder<IWindowFormEvents>
constructor(wfEventBus: IEventBuilder<IWindowFormEvents>) {
this.wfEventBus = wfEventBus
}
/** 创建窗口实例数据对象不含DOM */
async createWindowForm(appId: string, config: IWindowFormConfig): Promise<IWindowFormInstance> {
const id = uuidv4()
const now = new Date().getTime()
const mergeConfig: IWindowFormData = {
title: config.title ?? '窗口',
width: config.width ?? 300,
height: config.height ?? 300,
minWidth: config.minWidth ?? 0,
minHeight: config.minHeight ?? 0,
maxWidth: config.maxWidth ?? window.innerWidth,
maxHeight: config.maxHeight ?? window.innerHeight,
resizable: config.resizable ?? true,
movable: config.movable ?? true,
closable: config.closable ?? true,
minimizable: config.minimizable ?? true,
maximizable: config.maximizable ?? true,
modal: config.modal ?? false,
alwaysOnTop: config.alwaysOnTop ?? false,
x: config.x ?? 0,
y: config.y ?? 0
}
const instance: IWindowFormInstance = {
id,
appId,
config: mergeConfig,
state: EWindowFormState.CREATING,
zIndex: this.nextZIndex++,
createdAt: now,
updatedAt: now,
subscriptions: []
}
this.windowForms.set(id, instance)
return instance
}
/** 获取窗口实例 */
getWindowForm(windowId: string): IWindowFormInstance | undefined {
return this.windowForms.get(windowId)
}
/** 删除窗口实例 */
removeWindowForm(windowId: string) {
this.windowForms.delete(windowId)
}
/** 更新窗口状态 */
updateState(windowId: string, newState: EWindowFormState, error?: Error) {
const win = this.windowForms.get(windowId)
if (!win) return
const old = win.state
if (old === newState) return
win.state = newState
this.wfEventBus.notify('onStateChange', windowId, newState, old)
this.notifyUpdate(win)
this.transition(win, newState, error)
}
/** 生命周期状态分发器 */
private transition(win: IWindowFormInstance, newState: EWindowFormState, error?: Error) {
const id = win.id
switch (newState) {
case EWindowFormState.CREATING:
this.wfEventBus.notify('onCreating', id)
break
case EWindowFormState.LOADING:
this.wfEventBus.notify('onLoading', id)
break
case EWindowFormState.ACTIVE:
this.wfEventBus.notify('onActivate', id)
break
case EWindowFormState.INACTIVE:
this.wfEventBus.notify('onDeactivate', id)
break
case EWindowFormState.MINIMIZED:
this.wfEventBus.notify('onMinimize', id)
break
case EWindowFormState.MAXIMIZED:
this.wfEventBus.notify('onMaximize', id)
break
case EWindowFormState.CLOSING:
this.wfEventBus.notify('onClose', id)
break
case EWindowFormState.DESTROYED:
this.wfEventBus.notify('onDestroy', id)
break
case EWindowFormState.ERROR:
this.wfEventBus.notify('onError', id, error ?? new Error('未知错误'))
break
}
}
/** 聚焦窗口 */
focus(windowId: string) {
const win = this.windowForms.get(windowId)
if (!win) return
this.activeWindowId = windowId
win.zIndex = this.nextZIndex++
if (win.element) win.element.style.zIndex = `${win.zIndex}`
this.wfEventBus.notify('onFocus', windowId)
this.notifyUpdate(win)
}
/** 最小化窗口 */
minimize(windowId: string) {
const win = this.windowForms.get(windowId)
if (!win || !win.element) return
win.element.style.display = 'none'
this.updateState(windowId, EWindowFormState.MINIMIZED)
}
/** 最大化窗口 */
maximize(windowId: string) {
const win = this.windowForms.get(windowId)
if (!win || !win.element) return
win.element.dataset.originalWidth = win.element.style.width
win.element.dataset.originalHeight = win.element.style.height
win.element.style.position = 'fixed'
Object.assign(win.element.style, {
top: '0',
left: '0',
width: '100vw',
height: '100vh'
})
this.updateState(windowId, EWindowFormState.MAXIMIZED)
}
/** 还原窗口 */
restore(windowId: string) {
const win = this.windowForms.get(windowId)
if (!win || !win.element) return
Object.assign(win.element.style, {
position: 'absolute',
width: win.element.dataset.originalWidth,
height: win.element.dataset.originalHeight
})
this.updateState(windowId, EWindowFormState.ACTIVE)
}
/** 通知窗口数据更新 */
private notifyUpdate(win: IWindowFormInstance) {
const rect = win.element?.getBoundingClientRect()
if (!rect) return
this.wfEventBus.notify('onWindowFormDataUpdate', {
id: win.id,
state: win.state,
width: rect.width,
height: rect.height,
x: rect.left,
y: rect.top
})
}
}

View File

@@ -0,0 +1,79 @@
import type { IEventBuilder } from '@/events/IEventBuilder'
import {
EWindowFormState,
type IWindowFormEvents,
type IWindowFormInstance,
WindowFormDataManager
} from '@/services/windowForm/WindowFormDataManager.ts'
/**
* WindowFormEventBinder
* ----------------------
* 管理拖拽、最小化、关闭等窗口交互事件。
*/
export class WindowFormEventBinder {
private eventBus: IEventBuilder<IWindowFormEvents>
private manager: WindowFormDataManager
constructor(eventBus: IEventBuilder<IWindowFormEvents>, manager: WindowFormDataManager) {
this.eventBus = eventBus
this.manager = manager
}
/** 为窗口绑定交互事件 */
bindWindowEvents(win: IWindowFormInstance) {
const bar = win.element?.querySelector('.window-title-bar') as HTMLElement
const btns = bar?.querySelectorAll('button[data-action]')
if (btns) {
btns.forEach(btn => {
const action = btn.getAttribute('data-action')
if (action === 'min') {
btn.addEventListener('click', () => this.manager.minimize(win.id))
} else if (action === 'max') {
btn.addEventListener('click', () => {
if (win.state === EWindowFormState.MAXIMIZED)
this.manager.restore(win.id)
else
this.manager.maximize(win.id)
})
} else if (action === 'close') {
btn.addEventListener('click', () => {
this.manager.updateState(win.id, EWindowFormState.CLOSING)
})
}
})
}
// 拖动事件
bar?.addEventListener('mousedown', e => this.startDrag(e, win))
}
/** 拖拽实现 */
private startDrag(e: MouseEvent, win: IWindowFormInstance) {
if (!win.element) return
const rect = win.element.getBoundingClientRect()
const startX = e.clientX
const startY = e.clientY
const baseX = rect.left
const baseY = rect.top
const move = (ev: MouseEvent) => {
const dx = ev.clientX - startX
const dy = ev.clientY - startY
const nx = baseX + dx
const ny = baseY + dy
win.element!.style.left = `${nx}px`
win.element!.style.top = `${ny}px`
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
dispose() {}
}

View File

@@ -0,0 +1,119 @@
import type { IEventBuilder } from '@/events/IEventBuilder'
import type { IWindowFormEvents, IWindowFormInstance } from './WindowFormDataManager.ts'
import { safeSubscribe } from '@/services/windowForm/utils.ts'
import '@/ui/webComponents/WindowFormElement.ts'
/**
* WindowFormRenderer
* ----------------------
* 负责窗口的DOM创建、销毁与视觉状态更新。
*/
export class WindowFormRenderer {
private wfEventBus: IEventBuilder<IWindowFormEvents>
constructor(wfEventBus: IEventBuilder<IWindowFormEvents>) {
this.wfEventBus = wfEventBus
}
/** 创建窗口DOM结构 */
async createWindowElement(win: IWindowFormInstance) {
const el = document.createElement('window-form-element')
el.wfData = win
win.element = el
document.body.appendChild(el)
// // 生命周期UI响应
// safeSubscribe(win, this.eventBus, 'onLoadStart', id => {
// if (id === win.id) this.showLoading(win)
// })
// safeSubscribe(win, this.eventBus, 'onLoaded', id => {
// if (id === win.id) this.hideLoading(win)
// })
// safeSubscribe(win, this.eventBus, 'onError', (id, err) => {
// if (id === win.id) this.showError(win, err)
// })
// safeSubscribe(win, this.eventBus, 'onDestroy', id => {
// if (id === win.id) this.destroy(win)
// })
}
/** 创建标题栏 */
private createTitleBar(win: IWindowFormInstance): HTMLElement {
const bar = document.createElement('div')
bar.className = 'window-title-bar'
bar.style.cssText = `
height:40px;
display:flex;
align-items:center;
justify-content:space-between;
background:linear-gradient(to bottom,#f8f9fa,#e9ecef);
border-bottom:1px solid #dee2e6;
padding:0 12px;
user-select:none;
cursor:move;
`
const title = document.createElement('span')
title.textContent = win.config.title ?? ''
title.style.fontWeight = '500'
const ctrl = document.createElement('div')
ctrl.style.display = 'flex'
ctrl.style.gap = '6px'
const makeBtn = (label: string, action: string) => {
const b = document.createElement('button')
b.dataset.action = action
b.textContent = label
b.style.cssText = `
width:24px;height:24px;border:none;
background:transparent;cursor:pointer;
border-radius:4px;font-size:14px;
`
b.addEventListener('mouseenter', () => (b.style.background = '#e9ecef'))
b.addEventListener('mouseleave', () => (b.style.background = 'transparent'))
return b
}
ctrl.append(makeBtn('', 'min'), makeBtn('□', 'max'), makeBtn('×', 'close'))
bar.append(title, ctrl)
return bar
}
/** 显示加载状态 */
private showLoading(win: IWindowFormInstance) {
const el = win.element?.querySelector('.loading-state') as HTMLElement
if (el) el.style.display = 'block'
}
/** 隐藏加载状态 */
private hideLoading(win: IWindowFormInstance) {
const el = win.element?.querySelector('.loading-state') as HTMLElement
if (el) el.style.display = 'none'
}
/** 显示错误UI */
private showError(win: IWindowFormInstance, error: Error) {
const content = win.element?.querySelector('.window-content')
if (content) {
content.innerHTML = `
<div style="padding:20px;color:#d00">
<strong>加载失败:</strong> ${error.message}
</div>
`
}
}
/** 销毁窗口DOM */
destroy(win: IWindowFormInstance) {
// 解绑所有事件
win.subscriptions.forEach(unsub => unsub())
win.subscriptions = []
// 渐隐销毁DOM
if (win.element) {
win.element.style.transition = 'opacity .2s ease'
win.element.style.opacity = '0'
setTimeout(() => win.element?.remove(), 200)
}
}
}

View File

@@ -0,0 +1,119 @@
import type { IEventBuilder } from '@/events/IEventBuilder'
import {
type IWindowFormConfig,
type IWindowFormEvents,
type IWindowFormInstance,
WindowFormDataManager,
EWindowFormState
} from './WindowFormDataManager.ts'
import { WindowFormRenderer } from './WindowFormRenderer'
import { WindowFormEventBinder } from './WindowFormEventBinder'
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl.ts'
/**
* WindowFormService
* ----------------------
* 框架入口类,整合 Manager、Renderer、EventBinder 三个子模块,
* 对外提供统一的窗口系统接口(创建、销毁、最小化、最大化、聚焦等)。
*/
export class WindowFormService {
private readonly dataManager: WindowFormDataManager
private renderer: WindowFormRenderer
private eventBinder: WindowFormEventBinder
// 窗口内部事件总线
private readonly wfEventBus: IEventBuilder<IWindowFormEvents>
constructor(eventBus: IEventBuilder<IWindowFormEvents>) {
this.wfEventBus = new EventBuilderImpl<IWindowFormEvents>()
// 1 初始化窗口数据管理器
this.dataManager = new WindowFormDataManager(this.wfEventBus)
// 2 初始化窗口渲染器
this.renderer = new WindowFormRenderer(this.wfEventBus)
// 3 初始化事件绑定模块(拖拽 + 调整大小)
this.eventBinder = new WindowFormEventBinder(this.wfEventBus, this.dataManager)
}
/**
* 创建一个窗口实例
* @param appId 应用ID
* @param config 应用窗口配置
*/
async createWindow(appId: string, config: IWindowFormConfig): Promise<IWindowFormInstance> {
// 1 创建数据实例
const instance = await this.dataManager.createWindowForm(appId, config)
// 2 加载阶段
this.dataManager.updateState(instance.id, EWindowFormState.LOADING)
try {
// 3 渲染DOM
await this.renderer.createWindowElement(instance)
// 4 绑定交互事件
this.eventBinder.bindWindowEvents(instance)
// 5 模拟加载完成后激活
this.dataManager.updateState(instance.id, EWindowFormState.ACTIVE)
this.dataManager.focus(instance.id)
this.wfEventBus.notify('onLoaded', instance.id)
return instance
} catch (err) {
// 异常状态
this.dataManager.updateState(instance.id, EWindowFormState.ERROR, err as Error)
throw err
}
}
/** 销毁窗口 */
async destroyWindow(windowId: string): Promise<void> {
const win = this.dataManager.getWindowForm(windowId)
if (!win) return
this.dataManager.updateState(windowId, EWindowFormState.CLOSING)
// 监听 onBeforeClose 的 cancel 回调
let isCanceled = false
this.wfEventBus.notify('onBeforeClose', windowId, () => {
isCanceled = true
})
if (isCanceled) return
// 确保在 DOM 销毁前清理事件
win.subscriptions.forEach(unsub => unsub())
win.subscriptions = []
this.renderer.destroy(win)
this.dataManager.updateState(windowId, EWindowFormState.DESTROYED)
this.dataManager.removeWindowForm(windowId)
}
getWindow(windowId: string): IWindowFormInstance | undefined {
return this.dataManager.getWindowForm(windowId)
}
/** 最小化窗口 */
minimize(windowId: string) {
this.dataManager.minimize(windowId)
}
/** 最大化窗口 */
maximize(windowId: string) {
this.dataManager.maximize(windowId)
}
/** 还原窗口 */
restore(windowId: string) {
this.dataManager.restore(windowId)
}
/** 聚焦窗口 */
focus(windowId: string) {
this.dataManager.focus(windowId)
}
/** 销毁全局监听器(用于系统卸载时) */
dispose() {
this.eventBinder.dispose()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
import {
EWindowFormState,
type IWindowFormEvents,
type IWindowFormInstance
} from '@/services/windowForm/WindowFormDataManager.ts'
import type { IEventBuilder } from '@/events/IEventBuilder.ts'
/**
* 订阅事件,并自动取消订阅
* @param win
* @param eventBus
* @param event
* @param handler
*/
export function safeSubscribe(
win: IWindowFormInstance,
eventBus: IEventBuilder<IWindowFormEvents>,
event: keyof IWindowFormEvents,
handler: (...args: any[]) => void
) {
const unsubscribe = eventBus.subscribe(event, (...args: any[]) => {
// 若窗口已销毁则不再执行
if (win.state === EWindowFormState.DESTROYED) return
handler(...args)
})
win.subscriptions.push(() => unsubscribe())
}

Some files were not shown because too many files have changed in this diff Show More