Compare commits
40 Commits
3401c8b737
...
ai-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| a102395286 | |||
| 81007cf938 | |||
| f2bb7f3196 | |||
| ce688a6834 | |||
| ced6786f86 | |||
| f2b12fbaf5 | |||
| f9fdb1a6c2 | |||
| 900a72e4c9 | |||
| 7b12efd09c | |||
| e54bd0a447 | |||
| 8d25c143c5 | |||
| 45ec0fd021 | |||
| 49d7f2c37e | |||
| 9a90f1258b | |||
| acecffb055 | |||
| 0ca5daad3b | |||
| 05882bb3d3 | |||
| 71d5aabb84 | |||
| 204dd4781b | |||
| ed0527bf27 | |||
| a56197a349 | |||
| f1ba609254 | |||
| 7b1dff9ea1 | |||
| b77a20f9b0 | |||
| 972e76e655 | |||
| d18a3d5279 | |||
| d042520b14 | |||
| 9dbc054483 | |||
| 12f46e6f8e | |||
| 16b4b27352 | |||
| e3ff2045ea | |||
| fd4f9aa66b | |||
| 62b4ae7379 | |||
| 68bdabf928 | |||
| e3cbba0607 | |||
| 3a6f5cdbba | |||
| 6eee4933e1 | |||
| 122fba228a | |||
| 08e08043d7 | |||
| 9a30c2b3d7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ coverage
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.qoder/*
|
||||||
|
|||||||
@@ -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
167
PRETTIER_CONFIG_GUIDE.md
Normal 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
220
PROJECT_SUMMARY.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 部署
|
||||||
|
|
||||||
|
构建产物可直接部署到任何静态文件服务器上。
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -35,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"
|
||||||
}
|
}
|
||||||
|
|||||||
104
pnpm-lock.yaml
generated
104
pnpm-lock.yaml
generated
@@ -38,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))
|
||||||
@@ -62,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)
|
||||||
@@ -1054,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'}
|
||||||
@@ -1435,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'}
|
||||||
@@ -1526,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:
|
||||||
@@ -2102,13 +2115,13 @@ snapshots:
|
|||||||
|
|
||||||
'@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:
|
||||||
@@ -2239,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
|
||||||
@@ -2250,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':
|
||||||
@@ -2349,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
|
||||||
@@ -2624,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
|
||||||
@@ -2977,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
|
||||||
@@ -3000,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)
|
||||||
@@ -3020,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
|
||||||
@@ -3045,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
|
||||||
@@ -3065,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)
|
||||||
@@ -3096,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
49
public/apps/README.md
Normal 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与系统通信
|
||||||
|
- 受到严格的权限控制
|
||||||
170
public/apps/music-player/README.md
Normal file
170
public/apps/music-player/README.md
Normal 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技术的应用
|
||||||
|
- 良好的用户体验设计
|
||||||
|
- 安全和性能的最佳实践
|
||||||
|
|
||||||
|
通过学习这个案例,开发者可以了解外置应用的完整开发流程,并以此为基础开发自己的应用。
|
||||||
751
public/apps/music-player/app.js
Normal file
751
public/apps/music-player/app.js
Normal 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;
|
||||||
88
public/apps/music-player/index.html
Normal file
88
public/apps/music-player/index.html
Normal 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>
|
||||||
27
public/apps/music-player/manifest.json
Normal file
27
public/apps/music-player/manifest.json
Normal 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": ["音乐", "播放器", "媒体", "音频"]
|
||||||
|
}
|
||||||
430
public/apps/music-player/style.css
Normal file
430
public/apps/music-player/style.css
Normal 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;
|
||||||
|
}
|
||||||
18
src/App.vue
18
src/App.vue
@@ -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
84
src/apps/AppRegistry.ts
Normal 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()
|
||||||
344
src/apps/calculator/Calculator.vue
Normal file
344
src/apps/calculator/Calculator.vue
Normal 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>
|
||||||
125
src/apps/components/BuiltInApp.vue
Normal file
125
src/apps/components/BuiltInApp.vue
Normal 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
102
src/apps/index.ts
Normal 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'
|
||||||
527
src/apps/notepad/Notepad.vue
Normal file
527
src/apps/notepad/Notepad.vue
Normal 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
658
src/apps/todo/Todo.vue
Normal 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>
|
||||||
65
src/apps/types/AppManifest.ts
Normal file
65
src/apps/types/AppManifest.ts
Normal 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
|
||||||
|
}
|
||||||
65
src/common/hooks/useClickFocus.ts
Normal file
65
src/common/hooks/useClickFocus.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
8
src/common/types/IDestroyable.ts
Normal file
8
src/common/types/IDestroyable.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* 可销毁接口
|
||||||
|
* 销毁实例,清理副作用,让内存可以被回收
|
||||||
|
*/
|
||||||
|
export interface IDestroyable {
|
||||||
|
/** 销毁实例,清理副作用,让内存可以被回收 */
|
||||||
|
destroy(): void
|
||||||
|
}
|
||||||
24
src/common/utils/sington.ts
Normal file
24
src/common/utils/sington.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
部门页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
文件管理页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
音乐页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
个人中心页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
照片页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
回收站页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
设置APP页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
电视页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
电影页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
@@ -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>
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
|
||||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程接口
|
|
||||||
*/
|
|
||||||
export interface IProcess {
|
|
||||||
/** 进程id */
|
|
||||||
get id(): string;
|
|
||||||
/** 进程信息 */
|
|
||||||
get processInfo(): IProcessInfo;
|
|
||||||
/** 进程的窗体列表 */
|
|
||||||
get windowForms(): Map<string, IWindowForm>;
|
|
||||||
get event(): IEventBuilder<IProcessEvent>;
|
|
||||||
/**
|
|
||||||
* 打开窗体
|
|
||||||
* @param startName 窗体启动名
|
|
||||||
*/
|
|
||||||
openWindowForm(startName: string): void;
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts'
|
|
||||||
|
|
||||||
export const processManager = new ProcessManagerImpl();
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
|
||||||
import WindowFormImpl from '../../window/impl/WindowFormImpl.ts'
|
|
||||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
|
||||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程
|
|
||||||
*/
|
|
||||||
export default class ProcessImpl implements IProcess {
|
|
||||||
private readonly _id: string = uuidV4();
|
|
||||||
private readonly _processInfo: IProcessInfo;
|
|
||||||
// 当前进程的窗体集合
|
|
||||||
private _windowForms: Map<string, IWindowForm> = new Map();
|
|
||||||
private _event: IEventBuilder<IProcessEvent> = new EventBuilderImpl<IProcessEvent>()
|
|
||||||
|
|
||||||
public get id() {
|
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
public get processInfo() {
|
|
||||||
return this._processInfo;
|
|
||||||
}
|
|
||||||
public get windowForms() {
|
|
||||||
return this._windowForms;
|
|
||||||
}
|
|
||||||
public get event() {
|
|
||||||
return this._event;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(info: IProcessInfo) {
|
|
||||||
console.log(`AppProcess: ${info.name}`)
|
|
||||||
this._processInfo = info;
|
|
||||||
|
|
||||||
const startName = info.startName;
|
|
||||||
|
|
||||||
this.initEvent();
|
|
||||||
|
|
||||||
processManager.registerProcess(this);
|
|
||||||
// 通过设置 isJustProcess 为 true,则不会创建窗体
|
|
||||||
if (!info.isJustProcess) {
|
|
||||||
this.openWindowForm(startName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initEvent() {
|
|
||||||
this.event.addEventListener('onProcessWindowFormExit', (id: string) => {
|
|
||||||
this.windowForms.delete(id)
|
|
||||||
if(this.windowForms.size === 0) {
|
|
||||||
processManager.removeProcess(this)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public openWindowForm(startName: string) {
|
|
||||||
const info = this._processInfo.windowFormConfigs.find(item => item.name === startName);
|
|
||||||
if (!info) throw new Error(`未找到窗体:${startName}`);
|
|
||||||
const window = new WindowFormImpl(this, info);
|
|
||||||
this._windowForms.set(window.id, window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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[];
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程的事件
|
|
||||||
* <p>onProcessExit - 进程退出</p>
|
|
||||||
* <p>onProcessWindowFormOpen - 进程的窗体打开</p>
|
|
||||||
* <p>onProcessWindowFormExit - 进程的窗体退出</p>
|
|
||||||
* <p>onProcessWindowFormFocus - 进程的窗体获取焦点</p>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
type TProcessEvent =
|
|
||||||
'onProcessExit' |
|
|
||||||
'onProcessWindowFormOpen' |
|
|
||||||
'onProcessWindowFormExit' |
|
|
||||||
'onProcessWindowFormFocus' |
|
|
||||||
'onProcessWindowFormBlur'
|
|
||||||
|
|
||||||
export interface IProcessEvent extends IEventMap {
|
|
||||||
/**
|
|
||||||
* 进程的窗体退出
|
|
||||||
* @param id 窗体id
|
|
||||||
*/
|
|
||||||
onProcessWindowFormExit: (id: string) => void
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { AService } from '@/core/service/kernel/AService.ts'
|
|
||||||
|
|
||||||
export class NotificationService extends AService {
|
|
||||||
constructor() {
|
|
||||||
super('NotificationService');
|
|
||||||
console.log('NotificationService - 服务注册')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { AService } from '@/core/service/kernel/AService.ts'
|
|
||||||
|
|
||||||
export class SettingsService extends AService {
|
|
||||||
constructor() {
|
|
||||||
super('SettingsService')
|
|
||||||
console.log('SettingsService - 服务注册')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 - 服务注册")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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] }
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
@@ -1,737 +0,0 @@
|
|||||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
|
||||||
|
|
||||||
/** 拖拽移动开始的回调 */
|
|
||||||
type TDragStartCallback = (x: number, y: number) => void;
|
|
||||||
/** 拖拽移动中的回调 */
|
|
||||||
type TDragMoveCallback = (x: number, y: number) => void;
|
|
||||||
/** 拖拽移动结束的回调 */
|
|
||||||
type TDragEndCallback = (x: number, y: number) => void;
|
|
||||||
|
|
||||||
/** 拖拽调整尺寸的方向 */
|
|
||||||
type TResizeDirection =
|
|
||||||
| 'top'
|
|
||||||
| 'bottom'
|
|
||||||
| 'left'
|
|
||||||
| 'right'
|
|
||||||
| 'top-left'
|
|
||||||
| 'top-right'
|
|
||||||
| 'bottom-left'
|
|
||||||
| 'bottom-right';
|
|
||||||
|
|
||||||
/** 元素边界 */
|
|
||||||
interface IElementRect {
|
|
||||||
/** 宽度 */
|
|
||||||
width: number;
|
|
||||||
/** 高度 */
|
|
||||||
height: number;
|
|
||||||
/** 顶点坐标(相对 offsetParent) */
|
|
||||||
top: number;
|
|
||||||
/** 左点坐标(相对 offsetParent) */
|
|
||||||
left: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拖拽调整尺寸回调数据 */
|
|
||||||
interface IResizeCallbackData {
|
|
||||||
/** 宽度 */
|
|
||||||
width: number;
|
|
||||||
/** 高度 */
|
|
||||||
height: number;
|
|
||||||
/** 顶点坐标(相对 offsetParent) */
|
|
||||||
top: number;
|
|
||||||
/** 左点坐标(相对 offsetParent) */
|
|
||||||
left: number;
|
|
||||||
/** 拖拽调整尺寸的方向 */
|
|
||||||
direction: TResizeDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拖拽参数 */
|
|
||||||
interface IDraggableResizableOptions {
|
|
||||||
/** 拖拽/调整尺寸目标元素 */
|
|
||||||
target: HTMLElement;
|
|
||||||
/** 拖拽句柄 */
|
|
||||||
handle?: HTMLElement;
|
|
||||||
/** 拖拽边界容器元素 */
|
|
||||||
boundaryElement?: HTMLElement;
|
|
||||||
/** 移动步进(网格吸附) */
|
|
||||||
snapGrid?: number;
|
|
||||||
/** 关键点吸附阈值 */
|
|
||||||
snapThreshold?: number;
|
|
||||||
/** 是否开启吸附动画 */
|
|
||||||
snapAnimation?: boolean;
|
|
||||||
/** 拖拽结束吸附动画时长 */
|
|
||||||
snapAnimationDuration?: number;
|
|
||||||
/** 是否允许超出边界 */
|
|
||||||
allowOverflow?: boolean;
|
|
||||||
/** 最小化任务栏位置的元素ID */
|
|
||||||
taskbarElementId: string;
|
|
||||||
|
|
||||||
/** 拖拽开始回调 */
|
|
||||||
onDragStart?: TDragStartCallback;
|
|
||||||
/** 拖拽移动中的回调 */
|
|
||||||
onDragMove?: TDragMoveCallback;
|
|
||||||
/** 拖拽结束回调 */
|
|
||||||
onDragEnd?: TDragEndCallback;
|
|
||||||
|
|
||||||
/** 调整尺寸的最小宽度 */
|
|
||||||
minWidth?: number;
|
|
||||||
/** 调整尺寸的最小高度 */
|
|
||||||
minHeight?: number;
|
|
||||||
/** 调整尺寸的最大宽度 */
|
|
||||||
maxWidth?: number;
|
|
||||||
/** 调整尺寸的最大高度 */
|
|
||||||
maxHeight?: number;
|
|
||||||
|
|
||||||
/** 拖拽调整尺寸中的回调 */
|
|
||||||
onResizeMove?: (data: IResizeCallbackData) => void;
|
|
||||||
/** 拖拽调整尺寸结束回调 */
|
|
||||||
onResizeEnd?: (data: IResizeCallbackData) => void;
|
|
||||||
|
|
||||||
/** 窗口状态改变回调 */
|
|
||||||
onWindowStateChange?: (state: TWindowFormState) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拖拽的范围边界 */
|
|
||||||
interface IBoundaryRect {
|
|
||||||
/** 最小 X 坐标 */
|
|
||||||
minX?: number;
|
|
||||||
/** 最大 X 坐标 */
|
|
||||||
maxX?: number;
|
|
||||||
/** 最小 Y 坐标 */
|
|
||||||
minY?: number;
|
|
||||||
/** 最大 Y 坐标 */
|
|
||||||
maxY?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽 + 调整尺寸 + 最大最小化 通用类
|
|
||||||
* 统一使用 position: absolute + transform: translate 实现拖拽
|
|
||||||
*/
|
|
||||||
export class DraggableResizableWindow {
|
|
||||||
private handle?: HTMLElement;
|
|
||||||
private target: HTMLElement;
|
|
||||||
private boundaryElement: HTMLElement;
|
|
||||||
private snapGrid: number;
|
|
||||||
private snapThreshold: number;
|
|
||||||
private snapAnimation: boolean;
|
|
||||||
private snapAnimationDuration: number;
|
|
||||||
private allowOverflow: boolean;
|
|
||||||
|
|
||||||
private onDragStart?: TDragStartCallback;
|
|
||||||
private onDragMove?: TDragMoveCallback;
|
|
||||||
private onDragEnd?: TDragEndCallback;
|
|
||||||
|
|
||||||
private onResizeMove?: (data: IResizeCallbackData) => void;
|
|
||||||
private onResizeEnd?: (data: IResizeCallbackData) => void;
|
|
||||||
|
|
||||||
private onWindowStateChange?: (state: TWindowFormState) => void;
|
|
||||||
|
|
||||||
private isDragging = false;
|
|
||||||
private currentDirection: TResizeDirection | null = null;
|
|
||||||
private dragThreshold = 2; // 拖拽阈值 超过才开始真正的拖拽
|
|
||||||
|
|
||||||
private startX = 0;
|
|
||||||
private startY = 0;
|
|
||||||
private startWidth = 0;
|
|
||||||
private startHeight = 0;
|
|
||||||
private startTop = 0;
|
|
||||||
private startLeft = 0;
|
|
||||||
private offsetX = 0;
|
|
||||||
private offsetY = 0;
|
|
||||||
private currentX = 0;
|
|
||||||
private currentY = 0;
|
|
||||||
|
|
||||||
private pendingDrag = false;
|
|
||||||
private pendingResize = false;
|
|
||||||
private dragDX = 0;
|
|
||||||
private dragDY = 0;
|
|
||||||
private resizeDX = 0;
|
|
||||||
private resizeDY = 0;
|
|
||||||
|
|
||||||
private minWidth: number;
|
|
||||||
private minHeight: number;
|
|
||||||
private maxWidth: number;
|
|
||||||
private maxHeight: number;
|
|
||||||
|
|
||||||
private containerRect: DOMRect;
|
|
||||||
private resizeObserver?: ResizeObserver;
|
|
||||||
private mutationObserver: MutationObserver;
|
|
||||||
private animationFrame?: number;
|
|
||||||
|
|
||||||
private _windowFormState: TWindowFormState = 'default';
|
|
||||||
/** 元素信息 */
|
|
||||||
private targetBounds: IElementRect;
|
|
||||||
/** 最小化前的元素信息 */
|
|
||||||
private targetPreMinimizeBounds?: IElementRect;
|
|
||||||
/** 最大化前的元素信息 */
|
|
||||||
private targetPreMaximizedBounds?: IElementRect;
|
|
||||||
private taskbarElementId: string;
|
|
||||||
|
|
||||||
get windowFormState() {
|
|
||||||
return this._windowFormState;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options: IDraggableResizableOptions) {
|
|
||||||
this.handle = options.handle;
|
|
||||||
this.target = options.target;
|
|
||||||
this.boundaryElement = options.boundaryElement ?? document.body;
|
|
||||||
this.snapGrid = options.snapGrid ?? 1;
|
|
||||||
this.snapThreshold = options.snapThreshold ?? 0;
|
|
||||||
this.snapAnimation = options.snapAnimation ?? false;
|
|
||||||
this.snapAnimationDuration = options.snapAnimationDuration ?? 200;
|
|
||||||
this.allowOverflow = options.allowOverflow ?? true;
|
|
||||||
|
|
||||||
this.onDragStart = options.onDragStart;
|
|
||||||
this.onDragMove = options.onDragMove;
|
|
||||||
this.onDragEnd = options.onDragEnd;
|
|
||||||
|
|
||||||
this.minWidth = options.minWidth ?? 100;
|
|
||||||
this.minHeight = options.minHeight ?? 50;
|
|
||||||
this.maxWidth = options.maxWidth ?? window.innerWidth;
|
|
||||||
this.maxHeight = options.maxHeight ?? window.innerHeight;
|
|
||||||
this.onResizeMove = options.onResizeMove;
|
|
||||||
this.onResizeEnd = options.onResizeEnd;
|
|
||||||
this.onWindowStateChange = options.onWindowStateChange;
|
|
||||||
|
|
||||||
this.taskbarElementId = options.taskbarElementId;
|
|
||||||
|
|
||||||
this.target.style.position = "absolute";
|
|
||||||
this.target.style.left = '0px';
|
|
||||||
this.target.style.top = '0px';
|
|
||||||
this.target.style.transform = "translate(0px, 0px)";
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.targetBounds = {
|
|
||||||
width: this.target.offsetWidth,
|
|
||||||
height: this.target.offsetHeight,
|
|
||||||
top: this.target.offsetTop,
|
|
||||||
left: this.target.offsetLeft,
|
|
||||||
};
|
|
||||||
this.containerRect = this.boundaryElement.getBoundingClientRect();
|
|
||||||
const x = this.containerRect.width / 2 - this.target.offsetWidth / 2;
|
|
||||||
const y = this.containerRect.height / 2 - this.target.offsetHeight / 2;
|
|
||||||
this.target.style.transform = `translate(${x}px, ${y}px)`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
if (this.handle) {
|
|
||||||
this.handle.addEventListener('mousedown', this.onMouseDownDrag);
|
|
||||||
}
|
|
||||||
this.target.addEventListener('mousedown', this.onMouseDownResize);
|
|
||||||
this.target.addEventListener('mouseleave', this.onMouseLeave);
|
|
||||||
document.addEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
|
||||||
|
|
||||||
this.observeResize(this.boundaryElement);
|
|
||||||
|
|
||||||
this.mutationObserver = new MutationObserver(mutations => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
mutation.removedNodes.forEach(node => {
|
|
||||||
if (node === this.target) this.destroy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (this.target.parentElement) {
|
|
||||||
this.mutationObserver.observe(this.target.parentElement, { childList: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseDownDrag = (e: MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!this.handle?.contains(e.target as Node)) return;
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.classList.contains('btn')) return;
|
|
||||||
if (this.getResizeDirection(e)) return;
|
|
||||||
|
|
||||||
this.startX = e.clientX;
|
|
||||||
this.startY = e.clientY;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.checkDragStart);
|
|
||||||
document.addEventListener('mouseup', this.cancelPendingDrag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkDragStart = (e: MouseEvent) => {
|
|
||||||
const dx = e.clientX - this.startX;
|
|
||||||
const dy = e.clientY - this.startY;
|
|
||||||
|
|
||||||
if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) {
|
|
||||||
// 超过阈值,真正开始拖拽
|
|
||||||
document.removeEventListener('mousemove', this.checkDragStart);
|
|
||||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
|
||||||
|
|
||||||
if (this._windowFormState === 'maximized') {
|
|
||||||
const preRect = this.targetPreMaximizedBounds!;
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
const relX = e.clientX / rect.width;
|
|
||||||
const relY = e.clientY / rect.height;
|
|
||||||
const newLeft = e.clientX - preRect.width * relX;
|
|
||||||
const newTop = e.clientY - preRect.height * relY;
|
|
||||||
this.targetPreMaximizedBounds = {
|
|
||||||
width: preRect.width,
|
|
||||||
height: preRect.height,
|
|
||||||
top: newTop,
|
|
||||||
left: newLeft,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.restore(() => this.startDrag(e));
|
|
||||||
} else {
|
|
||||||
this.startDrag(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private cancelPendingDrag = () => {
|
|
||||||
document.removeEventListener('mousemove', this.checkDragStart);
|
|
||||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startDrag = (e: MouseEvent) => {
|
|
||||||
this.isDragging = true;
|
|
||||||
this.startX = e.clientX;
|
|
||||||
this.startY = e.clientY;
|
|
||||||
|
|
||||||
const style = window.getComputedStyle(this.target);
|
|
||||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
|
||||||
this.offsetX = matrix.m41;
|
|
||||||
this.offsetY = matrix.m42;
|
|
||||||
|
|
||||||
this.onDragStart?.(this.offsetX, this.offsetY);
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.onMouseMoveDragRAF);
|
|
||||||
document.addEventListener('mouseup', this.onMouseUpDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMouseMoveDragRAF = (e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.dragDX = e.clientX - this.startX;
|
|
||||||
this.dragDY = e.clientY - this.startY;
|
|
||||||
if (!this.pendingDrag) {
|
|
||||||
this.pendingDrag = true;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.pendingDrag = false;
|
|
||||||
this.applyDragFrame();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private applyDragFrame() {
|
|
||||||
if (!this.isDragging) return;
|
|
||||||
|
|
||||||
let newX = this.offsetX + this.dragDX;
|
|
||||||
let newY = this.offsetY + this.dragDY;
|
|
||||||
|
|
||||||
if (this.snapGrid > 1) {
|
|
||||||
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
|
|
||||||
newY = Math.round(newY / this.snapGrid) * this.snapGrid;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applyPosition(newX, newY, false);
|
|
||||||
this.onDragMove?.(newX, newY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseUpDrag = (e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!this.isDragging) return;
|
|
||||||
this.isDragging = false;
|
|
||||||
|
|
||||||
const snapped = this.applySnapping(this.currentX, this.currentY);
|
|
||||||
|
|
||||||
if (this.snapAnimation) {
|
|
||||||
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
|
|
||||||
this.onDragEnd?.(snapped.x, snapped.y);
|
|
||||||
this.updateTargetBounds(snapped.x, snapped.y);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.applyPosition(snapped.x, snapped.y, true);
|
|
||||||
this.onDragEnd?.(snapped.x, snapped.y);
|
|
||||||
this.updateTargetBounds(snapped.x, snapped.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
|
||||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
private applyPosition(x: number, y: number, isFinal: boolean) {
|
|
||||||
this.currentX = x;
|
|
||||||
this.currentY = y;
|
|
||||||
this.target.style.transform = `translate(${x}px, ${y}px)`;
|
|
||||||
if (isFinal) this.applyBoundary();
|
|
||||||
}
|
|
||||||
|
|
||||||
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
|
|
||||||
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
||||||
const startX = this.currentX;
|
|
||||||
const startY = this.currentY;
|
|
||||||
const deltaX = targetX - startX;
|
|
||||||
const deltaY = targetY - startY;
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
const step = (now: number) => {
|
|
||||||
const elapsed = now - startTime;
|
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
|
||||||
const ease = 1 - Math.pow(1 - progress, 3);
|
|
||||||
|
|
||||||
const x = startX + deltaX * ease;
|
|
||||||
const y = startY + deltaY * ease;
|
|
||||||
|
|
||||||
this.applyPosition(x, y, false);
|
|
||||||
this.onDragMove?.(x, y);
|
|
||||||
|
|
||||||
if (progress < 1) this.animationFrame = requestAnimationFrame(step);
|
|
||||||
else { this.applyPosition(targetX, targetY, true); this.onDragMove?.(targetX, targetY); onComplete?.(); }
|
|
||||||
};
|
|
||||||
this.animationFrame = requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyBoundary() {
|
|
||||||
if (this.allowOverflow) return;
|
|
||||||
let { x, y } = { x: this.currentX, y: this.currentY };
|
|
||||||
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
x = Math.min(Math.max(x, 0), this.containerRect.width - rect.width);
|
|
||||||
y = Math.min(Math.max(y, 0), this.containerRect.height - rect.height);
|
|
||||||
|
|
||||||
this.currentX = x;
|
|
||||||
this.currentY = y;
|
|
||||||
this.applyPosition(x, y, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySnapping(x: number, y: number) {
|
|
||||||
let snappedX = x, snappedY = y;
|
|
||||||
const containerSnap = this.getSnapPoints();
|
|
||||||
if (this.snapThreshold > 0) {
|
|
||||||
for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapThreshold) { snappedX = sx; break; }
|
|
||||||
for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapThreshold) { snappedY = sy; break; }
|
|
||||||
}
|
|
||||||
return { x: snappedX, y: snappedY };
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSnapPoints() {
|
|
||||||
const snapPoints = { x: [] as number[], y: [] as number[] };
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
snapPoints.x = [0, this.containerRect.width - rect.width];
|
|
||||||
snapPoints.y = [0, this.containerRect.height - rect.height];
|
|
||||||
return snapPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseDownResize = (e: MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const dir = this.getResizeDirection(e);
|
|
||||||
if (!dir) return;
|
|
||||||
|
|
||||||
this.startResize(e, dir);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onMouseLeave = (e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.updateCursor(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
private startResize(e: MouseEvent, dir: TResizeDirection) {
|
|
||||||
this.currentDirection = dir;
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
const style = window.getComputedStyle(this.target);
|
|
||||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
|
||||||
this.offsetX = matrix.m41;
|
|
||||||
this.offsetY = matrix.m42;
|
|
||||||
|
|
||||||
this.startX = e.clientX;
|
|
||||||
this.startY = e.clientY;
|
|
||||||
this.startWidth = rect.width;
|
|
||||||
this.startHeight = rect.height;
|
|
||||||
this.startLeft = this.offsetX;
|
|
||||||
this.startTop = this.offsetY;
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', this.onResizeDragRAF);
|
|
||||||
document.addEventListener('mouseup', this.onResizeEndHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResizeDragRAF = (e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.resizeDX = e.clientX - this.startX;
|
|
||||||
this.resizeDY = e.clientY - this.startY;
|
|
||||||
if (!this.pendingResize) {
|
|
||||||
this.pendingResize = true;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.pendingResize = false;
|
|
||||||
this.applyResizeFrame();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private applyResizeFrame() {
|
|
||||||
if (!this.currentDirection) return;
|
|
||||||
|
|
||||||
let newWidth = this.startWidth;
|
|
||||||
let newHeight = this.startHeight;
|
|
||||||
let newX = this.startLeft;
|
|
||||||
let newY = this.startTop;
|
|
||||||
|
|
||||||
const dx = this.resizeDX;
|
|
||||||
const dy = this.resizeDY;
|
|
||||||
|
|
||||||
switch (this.currentDirection) {
|
|
||||||
case 'right': newWidth += dx; break;
|
|
||||||
case 'bottom': newHeight += dy; break;
|
|
||||||
case 'bottom-right': newWidth += dx; newHeight += dy; break;
|
|
||||||
case 'left': newWidth -= dx; newX += dx; break;
|
|
||||||
case 'top': newHeight -= dy; newY += dy; break;
|
|
||||||
case 'top-left': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break;
|
|
||||||
case 'top-right': newWidth += dx; newHeight -= dy; newY += dy; break;
|
|
||||||
case 'bottom-left': newWidth -= dx; newX += dx; newHeight += dy; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
|
|
||||||
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
|
|
||||||
|
|
||||||
this.applyResizeBounds(newX, newY, newWidth, newHeight);
|
|
||||||
|
|
||||||
this.updateCursor(this.currentDirection);
|
|
||||||
|
|
||||||
this.onResizeMove?.({
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
|
||||||
left: newX,
|
|
||||||
top: newY,
|
|
||||||
direction: this.currentDirection,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用尺寸调整边界
|
|
||||||
private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number) {
|
|
||||||
// 最小/最大宽高限制
|
|
||||||
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
|
|
||||||
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
|
|
||||||
|
|
||||||
// 边界限制
|
|
||||||
if (this.allowOverflow) {
|
|
||||||
this.currentX = newX;
|
|
||||||
this.currentY = newY;
|
|
||||||
this.target.style.width = `${newWidth}px`;
|
|
||||||
this.target.style.height = `${newHeight}px`;
|
|
||||||
this.applyPosition(newX, newY, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newX = Math.min(Math.max(0, newX), this.containerRect.width - newWidth);
|
|
||||||
newY = Math.min(Math.max(0, newY), this.containerRect.height - newHeight);
|
|
||||||
|
|
||||||
this.currentX = newX;
|
|
||||||
this.currentY = newY;
|
|
||||||
this.target.style.width = `${newWidth}px`;
|
|
||||||
this.target.style.height = `${newHeight}px`;
|
|
||||||
this.applyPosition(newX, newY, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResizeEndHandler = (e?: MouseEvent) => {
|
|
||||||
e?.stopPropagation();
|
|
||||||
if (!this.currentDirection) return;
|
|
||||||
this.onResizeEnd?.({
|
|
||||||
width: this.target.offsetWidth,
|
|
||||||
height: this.target.offsetHeight,
|
|
||||||
left: this.currentX,
|
|
||||||
top: this.currentY,
|
|
||||||
direction: this.currentDirection,
|
|
||||||
});
|
|
||||||
this.updateTargetBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight);
|
|
||||||
this.currentDirection = null;
|
|
||||||
this.updateCursor(null);
|
|
||||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
|
||||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
private getResizeDirection(e: MouseEvent): TResizeDirection | null {
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
const offset = 4;
|
|
||||||
const x = e.clientX;
|
|
||||||
const y = e.clientY;
|
|
||||||
const top = y >= rect.top && y <= rect.top + offset;
|
|
||||||
const bottom = y >= rect.bottom - offset && y <= rect.bottom;
|
|
||||||
const left = x >= rect.left && x <= rect.left + offset;
|
|
||||||
const right = x >= rect.right - offset && x <= rect.right;
|
|
||||||
|
|
||||||
if (top && left) return 'top-left';
|
|
||||||
if (top && right) return 'top-right';
|
|
||||||
if (bottom && left) return 'bottom-left';
|
|
||||||
if (bottom && right) return 'bottom-right';
|
|
||||||
if (top) return 'top';
|
|
||||||
if (bottom) return 'bottom';
|
|
||||||
if (left) return 'left';
|
|
||||||
if (right) return 'right';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateCursor(direction: TResizeDirection | null) {
|
|
||||||
if (!direction) { this.target.style.cursor = 'default'; return; }
|
|
||||||
const cursorMap: Record<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) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (this.currentDirection || this.isDragging) return;
|
|
||||||
const dir = this.getResizeDirection(e);
|
|
||||||
this.updateCursor(dir);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 最小化到任务栏
|
|
||||||
public minimize() {
|
|
||||||
if (this._windowFormState === 'minimized') return;
|
|
||||||
this.targetPreMinimizeBounds = { ...this.targetBounds }
|
|
||||||
this._windowFormState = 'minimized';
|
|
||||||
|
|
||||||
const taskbar = document.querySelector(this.taskbarElementId);
|
|
||||||
if (!taskbar) throw new Error('任务栏元素未找到');
|
|
||||||
|
|
||||||
const rect = taskbar.getBoundingClientRect();
|
|
||||||
const startX = this.currentX;
|
|
||||||
const startY = this.currentY;
|
|
||||||
const startW = this.target.offsetWidth;
|
|
||||||
const startH = this.target.offsetHeight;
|
|
||||||
|
|
||||||
this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400, () => {
|
|
||||||
this.target.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 最大化 */
|
|
||||||
public maximize() {
|
|
||||||
if (this._windowFormState === 'maximized') return;
|
|
||||||
this.targetPreMaximizedBounds = { ...this.targetBounds }
|
|
||||||
this._windowFormState = 'maximized';
|
|
||||||
|
|
||||||
const rect = this.target.getBoundingClientRect();
|
|
||||||
|
|
||||||
const startX = this.currentX;
|
|
||||||
const startY = this.currentY;
|
|
||||||
const startW = rect.width;
|
|
||||||
const startH = rect.height;
|
|
||||||
|
|
||||||
const targetX = 0;
|
|
||||||
const targetY = 0;
|
|
||||||
const targetW = this.containerRect?.width ?? window.innerWidth;
|
|
||||||
const targetH = this.containerRect?.height ?? window.innerHeight;
|
|
||||||
|
|
||||||
this.animateWindow(startX, startY, startW, startH, targetX, targetY, targetW, targetH, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 恢复到默认窗体状态 */
|
|
||||||
public restore(onComplete?: () => void) {
|
|
||||||
if (this._windowFormState === 'default') return;
|
|
||||||
let b: IElementRect;
|
|
||||||
if ((this._windowFormState as TWindowFormState) === 'minimized' && this.targetPreMinimizeBounds) {
|
|
||||||
// 最小化恢复,恢复到最小化前的状态
|
|
||||||
b = this.targetPreMinimizeBounds;
|
|
||||||
} else if ((this._windowFormState as TWindowFormState) === 'maximized' && this.targetPreMaximizedBounds) {
|
|
||||||
// 最大化恢复,恢复到最大化前的默认状态
|
|
||||||
b = this.targetPreMaximizedBounds;
|
|
||||||
} else {
|
|
||||||
b = this.targetBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._windowFormState = 'default';
|
|
||||||
|
|
||||||
this.target.style.display = 'block';
|
|
||||||
|
|
||||||
const startX = this.currentX;
|
|
||||||
const startY = this.currentY;
|
|
||||||
const startW = this.target.offsetWidth;
|
|
||||||
const startH = this.target.offsetHeight;
|
|
||||||
|
|
||||||
this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300, onComplete);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 窗体最大化、最小化和恢复默认 动画
|
|
||||||
* @param startX
|
|
||||||
* @param startY
|
|
||||||
* @param startW
|
|
||||||
* @param startH
|
|
||||||
* @param targetX
|
|
||||||
* @param targetY
|
|
||||||
* @param targetW
|
|
||||||
* @param targetH
|
|
||||||
* @param duration
|
|
||||||
* @param onComplete
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private animateWindow(
|
|
||||||
startX: number,
|
|
||||||
startY: number,
|
|
||||||
startW: number,
|
|
||||||
startH: number,
|
|
||||||
targetX: number,
|
|
||||||
targetY: number,
|
|
||||||
targetW: number,
|
|
||||||
targetH: number,
|
|
||||||
duration: number,
|
|
||||||
onComplete?: () => void
|
|
||||||
) {
|
|
||||||
const startTime = performance.now();
|
|
||||||
const step = (now: number) => {
|
|
||||||
const elapsed = now - startTime;
|
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
|
||||||
const ease = 1 - Math.pow(1 - progress, 3);
|
|
||||||
|
|
||||||
const x = startX + (targetX - startX) * ease;
|
|
||||||
const y = startY + (targetY - startY) * ease;
|
|
||||||
const w = startW + (targetW - startW) * ease;
|
|
||||||
const h = startH + (targetH - startH) * ease;
|
|
||||||
|
|
||||||
this.target.style.width = `${w}px`;
|
|
||||||
this.target.style.height = `${h}px`;
|
|
||||||
this.applyPosition(x, y, false);
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
} else {
|
|
||||||
this.target.style.width = `${targetW}px`;
|
|
||||||
this.target.style.height = `${targetH}px`;
|
|
||||||
this.applyPosition(targetX, targetY, true);
|
|
||||||
onComplete?.();
|
|
||||||
this.onWindowStateChange?.(this._windowFormState);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateTargetBounds(left: number, top: number, width?: number, height?: number) {
|
|
||||||
this.targetBounds = {
|
|
||||||
left, top,
|
|
||||||
width: width ?? this.target.offsetWidth,
|
|
||||||
height: height ?? this.target.offsetHeight
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听元素变化 */
|
|
||||||
private observeResize(element: HTMLElement) {
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.containerRect = element.getBoundingClientRect();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁实例
|
|
||||||
*/
|
|
||||||
public destroy() {
|
|
||||||
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
|
|
||||||
this.target.removeEventListener('mousedown', this.onMouseDownResize);
|
|
||||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
|
||||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
|
||||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
|
||||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
|
||||||
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
|
||||||
document.removeEventListener('mousemove', this.checkDragStart);
|
|
||||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
this.mutationObserver.disconnect();
|
|
||||||
cancelAnimationFrame(this.animationFrame ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
|
||||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
|
||||||
|
|
||||||
export interface IWindowForm {
|
|
||||||
/** 窗体id */
|
|
||||||
get id(): string;
|
|
||||||
/** 窗体所属的进程 */
|
|
||||||
get proc(): IProcess | undefined;
|
|
||||||
/** 窗体元素 */
|
|
||||||
get windowFormEle(): HTMLElement;
|
|
||||||
/** 窗体状态 */
|
|
||||||
get windowFormState(): TWindowFormState;
|
|
||||||
/** 关闭窗体 */
|
|
||||||
closeWindowForm(): void;
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
$titleBarHeight: 40px;
|
|
||||||
|
|
||||||
/* 窗体容器 */
|
|
||||||
.window {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid #666;
|
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
|
||||||
background-color: #ffffff;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题栏 */
|
|
||||||
.title-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
width: 100%;
|
|
||||||
height: $titleBarHeight;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: block;
|
|
||||||
padding: 0 5px;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: $titleBarHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 24px;
|
|
||||||
color: black;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply bg-gray-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 窗体内容 */
|
|
||||||
.window-content {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - #{$titleBarHeight});
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
@@ -1,118 +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 { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts'
|
|
||||||
import '../css/window-form.scss'
|
|
||||||
|
|
||||||
export default class WindowFormImpl implements IWindowForm {
|
|
||||||
private readonly _id: string = uuidV4();
|
|
||||||
private readonly _procId: string;
|
|
||||||
private dom: HTMLElement;
|
|
||||||
private drw: DraggableResizableWindow;
|
|
||||||
private pos: WindowFormPos = { x: 0, y: 0 };
|
|
||||||
private width: number;
|
|
||||||
private height: number;
|
|
||||||
|
|
||||||
public get id() {
|
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
public get proc() {
|
|
||||||
return processManager.findProcessById(this._procId)
|
|
||||||
}
|
|
||||||
private get desktopRootDom() {
|
|
||||||
return XSystem.instance.desktopRootDom;
|
|
||||||
}
|
|
||||||
public get windowFormEle() {
|
|
||||||
return this.dom;
|
|
||||||
}
|
|
||||||
public get windowFormState() {
|
|
||||||
return this.drw.windowFormState
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(proc: IProcess, config: IWindowFormConfig) {
|
|
||||||
this._procId = proc.id;
|
|
||||||
console.log('WindowForm')
|
|
||||||
this.pos = {
|
|
||||||
x: config.left ?? 0,
|
|
||||||
y: config.top ?? 0
|
|
||||||
}
|
|
||||||
this.width = config.width ?? 200;
|
|
||||||
this.height = config.height ?? 100;
|
|
||||||
|
|
||||||
this.createWindowFrom();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createWindowFrom() {
|
|
||||||
this.dom = this.createWindowFormEle();
|
|
||||||
this.dom.style.position = 'absolute';
|
|
||||||
this.dom.style.width = `${this.width}px`;
|
|
||||||
this.dom.style.height = `${this.height}px`;
|
|
||||||
this.dom.style.zIndex = '10';
|
|
||||||
|
|
||||||
const header = this.dom.querySelector('.title-bar') as HTMLElement;
|
|
||||||
const content = this.dom.querySelector('.window-content') as HTMLElement;
|
|
||||||
this.drw = new DraggableResizableWindow({
|
|
||||||
target: this.dom,
|
|
||||||
handle: header,
|
|
||||||
snapAnimation: true,
|
|
||||||
snapThreshold: 20,
|
|
||||||
boundaryElement: document.body,
|
|
||||||
taskbarElementId: '#taskbar',
|
|
||||||
onWindowStateChange: (state) => {
|
|
||||||
if (state === 'maximized') {
|
|
||||||
this.dom.style.borderRadius = '0px';
|
|
||||||
} else {
|
|
||||||
this.dom.style.borderRadius = '5px';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.desktopRootDom.appendChild(this.dom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeWindowForm() {
|
|
||||||
this.drw.destroy();
|
|
||||||
this.desktopRootDom.removeChild(this.dom);
|
|
||||||
this.proc?.event.notifyEvent('onProcessWindowFormExit', this.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private createWindowFormEle() {
|
|
||||||
const template = document.createElement('template');
|
|
||||||
template.innerHTML = `
|
|
||||||
<div class="window">
|
|
||||||
<div class="title-bar">
|
|
||||||
<div class="title">我的窗口</div>
|
|
||||||
<div class="window-controls">
|
|
||||||
<div class="minimize btn" title="最小化"">-</div>
|
|
||||||
<div class="maximize btn" title="最大化">□</div>
|
|
||||||
<div class="close btn" title="关闭">×</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="window-content"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
const fragment = template.content.cloneNode(true) as DocumentFragment;
|
|
||||||
const windowElement = fragment.firstElementChild as HTMLElement
|
|
||||||
|
|
||||||
windowElement.querySelector('.btn.minimize')
|
|
||||||
?.addEventListener('click', () => this.drw.minimize());
|
|
||||||
|
|
||||||
windowElement.querySelector('.btn.maximize')
|
|
||||||
?.addEventListener('click', () => {
|
|
||||||
if (this.drw.windowFormState === 'maximized') {
|
|
||||||
this.drw.restore()
|
|
||||||
} else {
|
|
||||||
this.drw.maximize()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
windowElement.querySelector('.btn.close')
|
|
||||||
?.addEventListener('click', () => this.closeWindowForm());
|
|
||||||
|
|
||||||
return windowElement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
* 窗体位置坐标 - 左上角
|
|
||||||
*/
|
|
||||||
export interface WindowFormPos {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 窗口状态 */
|
|
||||||
export type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 排版元素 ===== */
|
/* ===== 排版元素 ===== */
|
||||||
|
|||||||
93
src/events/IEventBuilder.ts
Normal file
93
src/events/IEventBuilder.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
61
src/main.ts
61
src/main.ts
@@ -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
702
src/sdk/index.ts
Normal 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
809
src/sdk/types.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
570
src/services/ApplicationLifecycleManager.ts
Normal file
570
src/services/ApplicationLifecycleManager.ts
Normal 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
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
}
|
||||||
1207
src/services/ApplicationSandboxEngine.ts
Normal file
1207
src/services/ApplicationSandboxEngine.ts
Normal file
File diff suppressed because it is too large
Load Diff
629
src/services/ExternalAppDiscovery.ts
Normal file
629
src/services/ExternalAppDiscovery.ts
Normal 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()
|
||||||
689
src/services/ResourceService.ts
Normal file
689
src/services/ResourceService.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
767
src/services/SystemServiceIntegration.ts
Normal file
767
src/services/SystemServiceIntegration.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
430
src/services/windowForm/WindowFormDataManager.ts
Normal file
430
src/services/windowForm/WindowFormDataManager.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/services/windowForm/WindowFormEventBinder.ts
Normal file
79
src/services/windowForm/WindowFormEventBinder.ts
Normal 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() {}
|
||||||
|
}
|
||||||
119
src/services/windowForm/WindowFormRenderer.ts
Normal file
119
src/services/windowForm/WindowFormRenderer.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/services/windowForm/WindowFormService.ts
Normal file
119
src/services/windowForm/WindowFormService.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
1225
src/services/windowForm/WindowFormServiceOld.ts
Normal file
1225
src/services/windowForm/WindowFormServiceOld.ts
Normal file
File diff suppressed because it is too large
Load Diff
27
src/services/windowForm/utils.ts
Normal file
27
src/services/windowForm/utils.ts
Normal 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
Reference in New Issue
Block a user