Compare commits
26 Commits
dev
...
f2bb7f3196
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ coverage
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
.qoder/*
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"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>
|
||||
<html lang="">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>vue-desktop</title>
|
||||
<title>Vue 桌面系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
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,34 +0,0 @@
|
||||
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
|
||||
import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
|
||||
import { NotificationService } from '@/core/service/services/NotificationService.ts'
|
||||
import { SettingsService } from '@/core/service/services/SettingsService.ts'
|
||||
import { WindowFormService } from '@/core/service/services/WindowFormService.ts'
|
||||
import { UserService } from '@/core/service/services/UserService.ts'
|
||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
||||
|
||||
export default class XSystem {
|
||||
private static _instance: XSystem = new XSystem()
|
||||
|
||||
private _desktopRootDom: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
console.log('XSystem')
|
||||
new NotificationService()
|
||||
new SettingsService()
|
||||
new WindowFormService()
|
||||
new UserService()
|
||||
}
|
||||
|
||||
public static get instance() {
|
||||
return this._instance
|
||||
}
|
||||
public get desktopRootDom() {
|
||||
return this._desktopRootDom
|
||||
}
|
||||
|
||||
public async initialization(dom: HTMLDivElement) {
|
||||
this._desktopRootDom = dom
|
||||
await processManager.runProcess('basic-system', BasicSystemProcess)
|
||||
await processManager.runProcess('desktop', DesktopProcess, dom)
|
||||
}
|
||||
}
|
||||
@@ -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,87 +0,0 @@
|
||||
import { reactive, onBeforeUnmount, type Reactive } from 'vue'
|
||||
import type { IObservable } from '@/core/state/IObservable.ts'
|
||||
|
||||
/**
|
||||
* Vue Hook: useObservable
|
||||
* 支持深层解构赋值,直接修改触发 ObservableImpl 通知 + Vue 响应式更新
|
||||
* @example
|
||||
* interface AppState {
|
||||
* count: number
|
||||
* user: { name: string; age: number }
|
||||
* items: number[]
|
||||
* }
|
||||
*
|
||||
* // 创建 ObservableImpl
|
||||
* const obs = new ObservableImpl<AppState>({
|
||||
* count: 0,
|
||||
* user: { name: 'Alice', age: 20 },
|
||||
* items: []
|
||||
* })
|
||||
*
|
||||
* export default defineComponent({
|
||||
* setup() {
|
||||
* // 深层解构 Hook
|
||||
* const { count, user, items } = useObservable(obs)
|
||||
*
|
||||
* const increment = () => {
|
||||
* count += 1 // 触发 ObservableImpl 通知 + Vue 更新
|
||||
* }
|
||||
*
|
||||
* const changeAge = () => {
|
||||
* user.age = 30 // 深层对象也能触发通知
|
||||
* }
|
||||
*
|
||||
* const addItem = () => {
|
||||
* items.push(42) // 数组方法拦截,触发通知
|
||||
* }
|
||||
*
|
||||
* return { count, user, items, increment, changeAge, addItem }
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export function useObservable<T extends object>(observable: IObservable<T>): Reactive<T> {
|
||||
// 创建 Vue 响应式对象
|
||||
const state = reactive({} as T)
|
||||
|
||||
/**
|
||||
* 将 ObservableImpl Proxy 映射到 Vue 响应式对象
|
||||
* 递归支持深层对象
|
||||
*/
|
||||
function mapKeys(obj: any, proxy: any) {
|
||||
(Object.keys(proxy) as (keyof typeof proxy)[]).forEach(key => {
|
||||
const value = proxy[key]
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// 递归创建子对象 Proxy
|
||||
obj[key] = reactive({} as typeof value)
|
||||
mapKeys(obj[key], value)
|
||||
} else {
|
||||
// 基本类型通过 getter/setter 同步
|
||||
Object.defineProperty(obj, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get() {
|
||||
return proxy[key]
|
||||
},
|
||||
set(val) {
|
||||
proxy[key] = val
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取 ObservableImpl 的 Proxy
|
||||
const refsProxy = observable.toRefsProxy()
|
||||
mapKeys(state, refsProxy)
|
||||
|
||||
// 订阅 ObservableImpl,保持响应式同步
|
||||
const unsubscribe = observable.subscribe(() => {
|
||||
// 空实现即可,getter/setter 已同步
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
create,
|
||||
NButton,
|
||||
NCard,
|
||||
NConfigProvider,
|
||||
} from 'naive-ui'
|
||||
|
||||
export const naiveUi = create({
|
||||
components: [NButton, NCard, NConfigProvider]
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createDiscreteApi } from 'naive-ui'
|
||||
import { configProviderProps } from './theme.ts'
|
||||
|
||||
const { message, notification, dialog, loadingBar, modal } = createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
|
||||
{
|
||||
configProviderProps: configProviderProps,
|
||||
notificationProviderProps: {
|
||||
placement: 'bottom-right',
|
||||
max: 3
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const { messageApi, notificationApi, dialogApi, loadingBarApi, modalApi } = {
|
||||
messageApi: message,
|
||||
notificationApi: notification,
|
||||
dialogApi: dialog,
|
||||
loadingBarApi: loadingBar,
|
||||
modalApi: modal
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { type ConfigProviderProps, darkTheme, dateZhCN, type GlobalTheme, lightTheme, zhCN } from 'naive-ui'
|
||||
|
||||
const lTheme: GlobalTheme = {
|
||||
...lightTheme,
|
||||
common: {
|
||||
...lightTheme.common,
|
||||
primaryColor: '#0070f3'
|
||||
}
|
||||
}
|
||||
|
||||
export const configProviderProps: ConfigProviderProps = {
|
||||
theme: lTheme,
|
||||
dateLocale: dateZhCN,
|
||||
locale: zhCN,
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* 可销毁接口
|
||||
* 销毁实例,清理副作用,让内存可以被回收
|
||||
*/
|
||||
export interface IDestroyable {
|
||||
/** 销毁实例,清理副作用,让内存可以被回收 */
|
||||
destroy(): void
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* 版本信息
|
||||
*/
|
||||
export interface IVersion {
|
||||
/**
|
||||
* 公司名称
|
||||
*/
|
||||
company: string
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
major: number
|
||||
|
||||
/**
|
||||
* 子版本号
|
||||
*/
|
||||
minor: number
|
||||
|
||||
/**
|
||||
* 修订号
|
||||
*/
|
||||
build: number
|
||||
|
||||
/**
|
||||
* 私有版本号
|
||||
*/
|
||||
private: number
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import ProcessImpl from '@/core/process/impl/ProcessImpl.ts'
|
||||
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
|
||||
import { createApp, h } from 'vue'
|
||||
import DesktopComponent from '@/core/desktop/ui/DesktopComponent.vue'
|
||||
import { naiveUi } from '@/core/common/naive-ui/components.ts'
|
||||
import { debounce } from 'lodash'
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
||||
import './ui/DesktopElement.ts'
|
||||
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
|
||||
|
||||
interface IDesktopDataState {
|
||||
/** 显示器宽度 */
|
||||
monitorWidth: number;
|
||||
/** 显示器高度 */
|
||||
monitorHeight: number;
|
||||
}
|
||||
|
||||
export class DesktopProcess extends ProcessImpl {
|
||||
/** 桌面根dom,类似显示器 */
|
||||
private readonly _monitorDom: HTMLElement
|
||||
private _isMounted: boolean = false
|
||||
private _data = new ObservableImpl<IDesktopDataState>({
|
||||
monitorWidth: 0,
|
||||
monitorHeight: 0,
|
||||
})
|
||||
|
||||
public get monitorDom() {
|
||||
return this._monitorDom
|
||||
}
|
||||
public get isMounted() {
|
||||
return this._isMounted
|
||||
}
|
||||
public get basicSystemProcess() {
|
||||
return processManager.findProcessByName<BasicSystemProcess>('basic-system')
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data
|
||||
}
|
||||
|
||||
constructor(info: IProcessInfo, dom: HTMLDivElement) {
|
||||
super(info)
|
||||
console.log('DesktopProcess')
|
||||
dom.style.position = 'relative'
|
||||
dom.style.overflow = 'hidden'
|
||||
dom.style.width = `${window.innerWidth}px`
|
||||
dom.style.height = `${window.innerHeight}px`
|
||||
|
||||
this._monitorDom = dom
|
||||
this._data.state.monitorWidth = window.innerWidth
|
||||
this._data.state.monitorHeight = window.innerHeight
|
||||
window.addEventListener('resize', this.onResize)
|
||||
|
||||
this.createDesktopUI()
|
||||
}
|
||||
|
||||
private onResize = debounce(() => {
|
||||
this._monitorDom.style.width = `${window.innerWidth}px`
|
||||
this._monitorDom.style.height = `${window.innerHeight}px`
|
||||
this._data.state.monitorWidth = window.innerWidth
|
||||
this._data.state.monitorHeight = window.innerHeight
|
||||
}, 300)
|
||||
|
||||
private createDesktopUI() {
|
||||
if (this._isMounted) return
|
||||
const app = createApp(DesktopComponent, { process: this })
|
||||
app.use(naiveUi)
|
||||
app.mount(this._monitorDom)
|
||||
this._isMounted = true
|
||||
}
|
||||
|
||||
private initDesktop(dom: HTMLDivElement) {
|
||||
const d = document.createElement('desktop-element')
|
||||
dom.appendChild(d)
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
super.destroy()
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
}
|
||||
}
|
||||
@@ -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,15 +0,0 @@
|
||||
/**
|
||||
* 桌面应用图标信息
|
||||
*/
|
||||
export interface IDesktopAppIcon {
|
||||
/** 图标name */
|
||||
name: string;
|
||||
/** 图标 */
|
||||
icon: string;
|
||||
/** 图标路径 */
|
||||
path: string;
|
||||
/** 图标在grid布局中的列 */
|
||||
x: number;
|
||||
/** 图标在grid布局中的行 */
|
||||
y: number;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* 桌面网格模板参数
|
||||
*/
|
||||
export interface IGridTemplateParams {
|
||||
/** 单元格预设宽度 */
|
||||
readonly cellExpectWidth: number
|
||||
/** 单元格预设高度 */
|
||||
readonly cellExpectHeight: number
|
||||
/** 单元格实际宽度 */
|
||||
cellRealWidth: number
|
||||
/** 单元格实际高度 */
|
||||
cellRealHeight: number
|
||||
/** 列间距 */
|
||||
gapX: number
|
||||
/** 行间距 */
|
||||
gapY: number
|
||||
/** 总列数 */
|
||||
colCount: number
|
||||
/** 总行数 */
|
||||
rowCount: number
|
||||
}
|
||||
@@ -1,95 +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 { 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'
|
||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
||||
|
||||
const props = defineProps<{ process: DesktopProcess }>()
|
||||
|
||||
props.process.data.subscribeKey(['monitorWidth', 'monitorHeight'], ({monitorWidth, monitorHeight}) => {
|
||||
console.log('onDesktopRootDomResize', monitorWidth, monitorHeight)
|
||||
notificationApi.create({
|
||||
title: '桌面通知',
|
||||
content: `桌面尺寸变化${monitorWidth}x${monitorHeight}}`,
|
||||
duration: 2000,
|
||||
})
|
||||
})
|
||||
|
||||
// props.process.data.subscribe((data) => {
|
||||
// console.log('desktopData', data.monitorWidth)
|
||||
// })
|
||||
|
||||
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) => {
|
||||
processManager.runProcess(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,35 +0,0 @@
|
||||
import { css, html, LitElement, unsafeCSS } from 'lit'
|
||||
import { customElement } from 'lit/decorators.js'
|
||||
import desktopStyle from './css/desktop.scss?inline'
|
||||
|
||||
@customElement('desktop-element')
|
||||
export class DesktopElement extends LitElement {
|
||||
static override styles = css`
|
||||
${unsafeCSS(desktopStyle)}
|
||||
`
|
||||
|
||||
private onContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log('contextmenu')
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="desktop-root" @contextmenu=${this.onContextMenu}>
|
||||
<div class="desktop-container">
|
||||
<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>
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="icon-container"
|
||||
:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1};grid-row: ${iconInfo.y}/${iconInfo.y + 1};`"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
{{ iconInfo.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
|
||||
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
|
||||
import { eventManager } from '@/core/events/EventManager.ts'
|
||||
|
||||
const { iconInfo, gridTemplate } = defineProps<{ iconInfo: IDesktopAppIcon, gridTemplate: IGridTemplateParams }>()
|
||||
|
||||
const onDragStart = (e: DragEvent) => {}
|
||||
|
||||
const onDragEnd = (e: DragEvent) => {
|
||||
const el = e.target as HTMLElement | null
|
||||
if (!el) return
|
||||
// 鼠标所在位置已存在图标元素
|
||||
const pointTarget = document.elementFromPoint(e.clientX, e.clientY)
|
||||
if (!pointTarget) return
|
||||
if (pointTarget.classList.contains('icon-container')) return
|
||||
if (!pointTarget.classList.contains('desktop-icons-container')) return
|
||||
|
||||
// 获取容器边界
|
||||
const rect = el.parentElement!.getBoundingClientRect()
|
||||
|
||||
// 鼠标相对容器左上角坐标
|
||||
const mouseX = e.clientX - rect.left
|
||||
const mouseY = e.clientY - rect.top
|
||||
|
||||
// 计算鼠标所在单元格坐标(向上取整,从1开始)
|
||||
const gridX = Math.ceil(mouseX / gridTemplate.cellRealWidth)
|
||||
const gridY = Math.ceil(mouseY / gridTemplate.cellRealHeight)
|
||||
|
||||
iconInfo.x = gridX
|
||||
iconInfo.y = gridY
|
||||
|
||||
eventManager.notifyEvent('onDesktopAppIconPos', iconInfo)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@apply flex flex-col items-center justify-center bg-gray-200;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { css, html, LitElement } from 'lit'
|
||||
|
||||
export class DesktopAppIconElement extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@apply flex flex-col items-center justify-center bg-gray-200;
|
||||
}
|
||||
`
|
||||
|
||||
override render() {
|
||||
return html`<div class="desktop-app-icon">
|
||||
<slot></slot>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box; /* 使用更直观的盒模型 */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
$taskBarHeight: 40px;
|
||||
|
||||
.desktop-root {
|
||||
@apply w-full h-full flex flex-col;
|
||||
|
||||
.desktop-container {
|
||||
@apply w-full h-full flex-1 p-2 pos-relative;
|
||||
background-image: url("../imgs/desktop-bg-2.jpeg");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: calc(100% - #{$taskBarHeight});
|
||||
}
|
||||
|
||||
.desktop-icons-container {
|
||||
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
|
||||
}
|
||||
|
||||
.task-bar {
|
||||
@apply w-full bg-gray-200 flex justify-center items-center;
|
||||
height: $taskBarHeight;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -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,47 +0,0 @@
|
||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
||||
|
||||
/**
|
||||
* 事件定义
|
||||
* @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
|
||||
*/
|
||||
export interface IEventMap {
|
||||
[key: string]: (...args: any[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件管理器接口定义
|
||||
*/
|
||||
export interface IEventBuilder<Events extends IEventMap> extends IDestroyable {
|
||||
/**
|
||||
* 添加事件监听
|
||||
* @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,61 +0,0 @@
|
||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
||||
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
|
||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
||||
|
||||
/**
|
||||
* 窗口的事件
|
||||
*/
|
||||
export interface WindowFormEvent extends IEventMap {
|
||||
/**
|
||||
* 窗口最小化
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormMinimize: (id: string) => void;
|
||||
/**
|
||||
* 窗口最大化
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormMaximize: (id: string) => void;
|
||||
/**
|
||||
* 窗口还原
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormRestore: (id: string) => void;
|
||||
/**
|
||||
* 窗口关闭
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormClose: (id: string) => void;
|
||||
/**
|
||||
* 窗口聚焦
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormFocus: (id: string) => void;
|
||||
/**
|
||||
* 窗口数据更新
|
||||
* @param data 窗口数据
|
||||
*/
|
||||
windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void;
|
||||
/**
|
||||
* 窗口创建完成
|
||||
*/
|
||||
windowFormCreated: () => void;
|
||||
}
|
||||
|
||||
interface IWindowFormDataUpdateParams {
|
||||
/** 窗口id */
|
||||
id: string;
|
||||
/** 窗口状态 */
|
||||
state: TWindowFormState,
|
||||
/** 窗口宽度 */
|
||||
width: number,
|
||||
/** 窗口高度 */
|
||||
height: number,
|
||||
/** 窗口x坐标(左上角) */
|
||||
x: number,
|
||||
/** 窗口y坐标(左上角) */
|
||||
y: number
|
||||
}
|
||||
|
||||
/** 窗口事件管理器 */
|
||||
export const wfem = new EventBuilderImpl<WindowFormEvent>()
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts'
|
||||
|
||||
interface HandlerWrapper<T extends (...args: any[]) => any> {
|
||||
fn: T
|
||||
once: boolean
|
||||
}
|
||||
|
||||
export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder<Events> {
|
||||
private _eventHandlers = new Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>()
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
* @param eventName 事件名称
|
||||
* @param handler 监听器
|
||||
* @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
|
||||
* @example
|
||||
* eventBus.addEventListener('noArgs', () => {})
|
||||
* eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
|
||||
* eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] })
|
||||
*/
|
||||
addEventListener<E extends keyof Events, F extends Events[E]>(
|
||||
eventName: E,
|
||||
handler: F,
|
||||
options?: {
|
||||
immediate?: boolean
|
||||
immediateArgs?: Parameters<F>
|
||||
once?: boolean
|
||||
},
|
||||
) {
|
||||
if (!handler) return
|
||||
if (!this._eventHandlers.has(eventName)) {
|
||||
this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>())
|
||||
}
|
||||
|
||||
const set = this._eventHandlers.get(eventName)!
|
||||
if (![...set].some((wrapper) => wrapper.fn === handler)) {
|
||||
set.add({ fn: handler, once: options?.once ?? false })
|
||||
}
|
||||
|
||||
if (options?.immediate) {
|
||||
try {
|
||||
handler(...(options.immediateArgs ?? []))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
* @param eventName 事件名称
|
||||
* @param handler 监听器
|
||||
* @example
|
||||
* eventBus.removeEventListener('noArgs', () => {})
|
||||
*/
|
||||
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F) {
|
||||
const set = this._eventHandlers.get(eventName)
|
||||
if (!set) return
|
||||
|
||||
for (const wrapper of set) {
|
||||
if (wrapper.fn === handler) {
|
||||
set.delete(wrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知事件
|
||||
* @param eventName 事件名称
|
||||
* @param args 参数
|
||||
* @example
|
||||
* eventBus.notifyEvent('noArgs')
|
||||
* eventBus.notifyEvent('greet', 'Alice')
|
||||
* eventBus.notifyEvent('onResize', 1, 2)
|
||||
*/
|
||||
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>) {
|
||||
if (!this._eventHandlers.has(eventName)) return
|
||||
|
||||
const set = this._eventHandlers.get(eventName)!
|
||||
for (const wrapper of set) {
|
||||
try {
|
||||
wrapper.fn(...args)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (wrapper.once) {
|
||||
set.delete(wrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._eventHandlers.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
||||
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
|
||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
||||
|
||||
/**
|
||||
* 进程接口
|
||||
*/
|
||||
export interface IProcess extends IDestroyable {
|
||||
/** 进程id */
|
||||
get id(): string;
|
||||
/** 进程信息 */
|
||||
get processInfo(): IProcessInfo;
|
||||
/** 进程的窗体列表 */
|
||||
get windowForms(): Map<string, IWindowForm>;
|
||||
get event(): IEventBuilder<IProcessEvent>;
|
||||
/**
|
||||
* 打开窗体
|
||||
* @param startName 窗体启动名
|
||||
*/
|
||||
openWindowForm(startName: string): void;
|
||||
/**
|
||||
* 关闭窗体
|
||||
* @param id 窗体id
|
||||
*/
|
||||
closeWindowForm(id: 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,83 +0,0 @@
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import WindowFormImpl from '../../window/impl/WindowFormImpl.ts'
|
||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
||||
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
|
||||
|
||||
/**
|
||||
* 进程
|
||||
*/
|
||||
export default class ProcessImpl implements IProcess {
|
||||
private readonly _id: string = uuidV4();
|
||||
private readonly _processInfo: IProcessInfo;
|
||||
// 当前进程的窗体集合
|
||||
private _windowForms: Map<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('processWindowFormExit', (id: string) => {
|
||||
this.windowForms.delete(id)
|
||||
if(this.windowForms.size === 0) {
|
||||
processManager.removeProcess(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public openWindowForm(startName: string) {
|
||||
const info = this._processInfo.windowFormConfigs.find(item => item.name === startName);
|
||||
if (!info) throw new Error(`未找到窗体:${startName}`);
|
||||
const wf = new WindowFormImpl(this, info);
|
||||
this._windowForms.set(wf.id, wf);
|
||||
}
|
||||
|
||||
public closeWindowForm(id: string) {
|
||||
try {
|
||||
const wf = this._windowForms.get(id);
|
||||
if (!wf) throw new Error(`未找到窗体:${id}`);
|
||||
wf.destroy();
|
||||
this.windowForms.delete(id)
|
||||
if(this.windowForms.size === 0) {
|
||||
this.destroy()
|
||||
processManager.removeProcess(this)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('关闭窗体失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this._event.destroy()
|
||||
}
|
||||
}
|
||||
@@ -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,107 +0,0 @@
|
||||
import ProcessImpl from './ProcessImpl.ts'
|
||||
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
|
||||
import { BasicSystemProcessInfo } from '@/core/system/BasicSystemProcessInfo.ts'
|
||||
import { DesktopProcessInfo } from '@/core/desktop/DesktopProcessInfo.ts'
|
||||
import type { IAppProcessInfoParams } from '@/core/process/types/IAppProcessInfoParams.ts'
|
||||
import type { IProcessManager } from '@/core/process/IProcessManager.ts'
|
||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
/**
|
||||
* 进程管理
|
||||
*/
|
||||
export default class ProcessManagerImpl implements IProcessManager {
|
||||
private _processPool: Map<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 async runProcess<T extends IProcess = IProcess, A extends any[] = any[]>(
|
||||
proc: string | IProcessInfo,
|
||||
constructor?: new (info: IProcessInfo, ...args: A) => T,
|
||||
...args: A
|
||||
): Promise<T> {
|
||||
let info = typeof proc === 'string' ? this.findProcessInfoByName(proc) : proc
|
||||
if (isUndefined(info)) {
|
||||
throw new Error(`未找到进程信息:${proc}`)
|
||||
}
|
||||
|
||||
// 是单例应用
|
||||
if (info.singleton) {
|
||||
let process = this.findProcessByName(info.name)
|
||||
if (process) {
|
||||
return process as T
|
||||
}
|
||||
}
|
||||
|
||||
// 创建进程
|
||||
let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info, ...args)
|
||||
|
||||
return process as T
|
||||
}
|
||||
|
||||
// 添加进程
|
||||
public registerProcess(process: ProcessImpl) {
|
||||
this._processPool.set(process.id, process);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过进程id查找进程
|
||||
* @param id 进程id
|
||||
*/
|
||||
public findProcessById(id: string) {
|
||||
return this._processPool.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过进程名称查找进程
|
||||
* @param name 进程名称
|
||||
*/
|
||||
public findProcessByName<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
|
||||
*/
|
||||
processWindowFormExit: (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,64 +0,0 @@
|
||||
import { AService } from '@/core/service/kernel/AService.ts'
|
||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
||||
import WindowFormImpl from '@/core/window/impl/WindowFormImpl.ts'
|
||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
||||
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
|
||||
|
||||
interface IWindow {
|
||||
id: string;
|
||||
title: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
zIndex: number;
|
||||
minimized: boolean;
|
||||
maximized: boolean;
|
||||
}
|
||||
|
||||
export class WindowFormService extends AService {
|
||||
private windows: Map<string, IWindowForm> = new Map();
|
||||
|
||||
constructor() {
|
||||
super("WindowFormService");
|
||||
console.log('WindowFormService - 服务注册')
|
||||
}
|
||||
|
||||
public createWindow(proc: IProcess, info: IWindowFormConfig): IWindowForm {
|
||||
const window = new WindowFormImpl(proc, info);
|
||||
this.windows.set(window.id, window);
|
||||
return window;
|
||||
}
|
||||
|
||||
public closeWindow(id: string) {
|
||||
if (this.windows.has(id)) {
|
||||
this.windows.delete(id);
|
||||
this.sm.broadcast("WindowFrom:closed", id);
|
||||
}
|
||||
}
|
||||
|
||||
public focusWindow(id: string) {
|
||||
const win = this.windows.get(id);
|
||||
if (win) {
|
||||
this.sm.broadcast("WindowFrom:focused", win);
|
||||
}
|
||||
}
|
||||
|
||||
public minimizeWindow(id: string) {
|
||||
const win = this.windows.get(id);
|
||||
if (win) {
|
||||
this.sm.broadcast("WindowFrom:minimized", win);
|
||||
}
|
||||
}
|
||||
|
||||
public maximizeWindow(id: string) {
|
||||
const win = this.windows.get(id);
|
||||
if (win) {
|
||||
this.sm.broadcast("WindowFrom:maximized", win);
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(event: string, data?: any) {
|
||||
console.log(`[WindowService] 收到事件:`, event, data);
|
||||
}
|
||||
}
|
||||
@@ -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,305 +0,0 @@
|
||||
import type {
|
||||
IObservable,
|
||||
TNonFunctionProperties,
|
||||
TObservableKeyListener,
|
||||
TObservableListener,
|
||||
TObservableState,
|
||||
} from '@/core/state/IObservable.ts'
|
||||
|
||||
/**
|
||||
* 创建一个可观察对象,用于管理状态和事件。
|
||||
* @template T - 需要处理的状态类型
|
||||
* @example
|
||||
* interface Todos {
|
||||
* id: number
|
||||
* text: string
|
||||
* done: boolean
|
||||
* }
|
||||
*
|
||||
* interface AppState {
|
||||
* count: number
|
||||
* todos: Todos[]
|
||||
* user: {
|
||||
* name: string
|
||||
* age: number
|
||||
* }
|
||||
* inc(): void
|
||||
* }
|
||||
*
|
||||
* const obs = new ObservableImpl<AppState>({
|
||||
* count: 0,
|
||||
* todos: [],
|
||||
* user: { name: "Alice", age: 20 },
|
||||
* inc() {
|
||||
* this.count++ // ✅ this 指向 obs.state
|
||||
* },
|
||||
* })
|
||||
*
|
||||
* // ================== 使用示例 ==================
|
||||
*
|
||||
* // 1. 订阅整个 state
|
||||
* obs.subscribe(state => {
|
||||
* console.log("[全量订阅] state 更新:", state)
|
||||
* })
|
||||
*
|
||||
* // 2. 订阅单个字段
|
||||
* obs.subscribeKey("count", ({ count }) => {
|
||||
* console.log("[字段订阅] count 更新:", count)
|
||||
* })
|
||||
*
|
||||
* // 3. 订阅多个字段
|
||||
* obs.subscribeKey(["name", "age"] as (keyof AppState["user"])[], (user) => {
|
||||
* console.log("[多字段订阅] user 更新:", user)
|
||||
* })
|
||||
*
|
||||
* // 4. 批量更新
|
||||
* obs.patch({ count: 10, user: { name: "Bob", age: 30 } })
|
||||
*
|
||||
* // 5. 方法里操作 state
|
||||
* obs.state.inc() // this.count++ → 相当于 obs.state.count++
|
||||
*
|
||||
* // 6. 数组操作
|
||||
* obs.subscribeKey("todos", ({ todos }) => {
|
||||
* console.log("[数组订阅] todos 更新:", todos.map(t => t.text))
|
||||
* })
|
||||
*
|
||||
* obs.state.todos.push({ id: 1, text: "Buy milk", done: false })
|
||||
* obs.state.todos.push({ id: 2, text: "Read book", done: false })
|
||||
* obs.state.todos[0].done = true
|
||||
*
|
||||
* // 7. 嵌套对象
|
||||
* obs.subscribeKey("user", ({ user }) => {
|
||||
* console.log("[嵌套订阅] user 更新:", user)
|
||||
* })
|
||||
*
|
||||
* obs.state.user.age++
|
||||
*/
|
||||
export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObservable<T> {
|
||||
/** Observable 状态对象,深层 Proxy */
|
||||
public readonly state: TObservableState<T>
|
||||
|
||||
/** 全量订阅函数集合 */
|
||||
private listeners: Set<TObservableListener<T>> = new Set()
|
||||
|
||||
/**
|
||||
* 字段订阅函数集合
|
||||
* 新结构:
|
||||
* Map<TObservableKeyListener, Array<keyof T>>
|
||||
* 记录每个回调订阅的字段数组,保证多字段订阅 always 返回所有订阅字段值
|
||||
*/
|
||||
private keyListeners: Map<TObservableKeyListener<T, keyof T>, Array<keyof T>> = new Map()
|
||||
|
||||
/** 待通知的字段集合 */
|
||||
private pendingKeys: Set<keyof T> = new Set()
|
||||
|
||||
/** 是否已经安排通知 */
|
||||
private notifyScheduled = false
|
||||
|
||||
/** 是否已销毁 */
|
||||
private disposed = false
|
||||
|
||||
/** 缓存 Proxy,避免重复包装 */
|
||||
private proxyCache: WeakMap<object, TObservableState<unknown>> = new WeakMap()
|
||||
|
||||
constructor(initialState: TNonFunctionProperties<T>) {
|
||||
// 创建深层响应式 Proxy
|
||||
this.state = this.makeReactive(initialState) as TObservableState<T>
|
||||
}
|
||||
|
||||
/** 创建深层 Proxy,拦截 get/set/delete,并自动缓存 */
|
||||
private makeReactive<O extends object>(obj: O): TObservableState<O> {
|
||||
// 非对象直接返回(包括 null 已被排除)
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return obj as unknown as TObservableState<O>
|
||||
}
|
||||
|
||||
// 如果已有 Proxy 缓存则直接返回
|
||||
const cached = this.proxyCache.get(obj as object)
|
||||
if (cached !== undefined) {
|
||||
return cached as TObservableState<O>
|
||||
}
|
||||
|
||||
const handler: ProxyHandler<O> = {
|
||||
get: (target, prop, receiver) => {
|
||||
const value = Reflect.get(target, prop, receiver) as unknown
|
||||
// 不包装函数
|
||||
if (typeof value === "function") {
|
||||
return value
|
||||
}
|
||||
// 对对象/数组继续进行响应式包装(递归)
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return this.makeReactive(value as object)
|
||||
}
|
||||
return value
|
||||
},
|
||||
|
||||
set: (target, prop, value, receiver) => {
|
||||
// 读取旧值(使用 Record 以便类型安全访问属性)
|
||||
const oldValue = (target as Record<PropertyKey, unknown>)[prop as PropertyKey] as unknown
|
||||
const result = Reflect.set(target, prop, value as unknown, receiver)
|
||||
// 仅在值改变时触发通知(基于引用/原始值比较)
|
||||
if (!this.disposed && oldValue !== (value as unknown)) {
|
||||
this.pendingKeys.add(prop as keyof T)
|
||||
this.scheduleNotify()
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
deleteProperty: (target, prop) => {
|
||||
if (prop in target) {
|
||||
// 使用 Reflect.deleteProperty 以保持一致性
|
||||
const deleted = Reflect.deleteProperty(target, prop)
|
||||
if (deleted && !this.disposed) {
|
||||
this.pendingKeys.add(prop as keyof T)
|
||||
this.scheduleNotify()
|
||||
}
|
||||
return deleted
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = new Proxy(obj, handler) as TObservableState<O>
|
||||
this.proxyCache.set(obj as object, proxy as TObservableState<unknown>)
|
||||
return proxy
|
||||
}
|
||||
|
||||
/** 安排下一次通知(微任务合并) */
|
||||
private scheduleNotify(): void {
|
||||
if (!this.notifyScheduled && !this.disposed && this.pendingKeys.size > 0) {
|
||||
this.notifyScheduled = true
|
||||
Promise.resolve().then(() => this.flushNotify())
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行通知(聚合字段订阅并保证错误隔离) */
|
||||
private flushNotify(): void {
|
||||
if (this.disposed) return
|
||||
|
||||
this.pendingKeys.clear()
|
||||
this.notifyScheduled = false
|
||||
|
||||
// 全量订阅 —— 每个订阅单独 try/catch,避免一个错误阻塞其它订阅
|
||||
for (const fn of this.listeners) {
|
||||
try {
|
||||
fn(this.state as unknown as T)
|
||||
} catch (err) {
|
||||
console.error("Observable listener error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 字段订阅 ==================
|
||||
// 遍历所有回调,每个回调都返回它订阅的字段(即使只有部分字段变化)
|
||||
this.keyListeners.forEach((subKeys, fn) => {
|
||||
try {
|
||||
// 构造 Pick<T, K> 风格的结果对象:结果类型为 Pick<T, (typeof subKeys)[number]>
|
||||
const result = {} as Pick<T, (typeof subKeys)[number]>
|
||||
subKeys.forEach(k => {
|
||||
// 这里断言原因:state 的索引访问返回 unknown,但我们把它赋回到受限的 Pick 上
|
||||
result[k] = (this.state as Record<keyof T, unknown>)[k] as T[(typeof k) & keyof T]
|
||||
})
|
||||
// 调用时类型上兼容 TObservableKeyListener<T, K>,因为我们传的是对应 key 的 Pick
|
||||
fn(result as Pick<T, (typeof subKeys)[number]>)
|
||||
} catch (err) {
|
||||
console.error("Observable keyListener error:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 订阅整个状态变化 */
|
||||
public subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
|
||||
this.listeners.add(fn)
|
||||
if (options.immediate) {
|
||||
try {
|
||||
fn(this.state as unknown as T)
|
||||
} catch (err) {
|
||||
console.error("Observable subscribe immediate error:", err)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
this.listeners.delete(fn)
|
||||
}
|
||||
}
|
||||
|
||||
/** 订阅指定字段变化(多字段订阅 always 返回所有字段值) */
|
||||
public subscribeKey<K extends keyof T>(
|
||||
keys: K | K[],
|
||||
fn: TObservableKeyListener<T, K>,
|
||||
options: { immediate?: boolean } = {}
|
||||
): () => void {
|
||||
const keyArray = Array.isArray(keys) ? keys : [keys]
|
||||
|
||||
// ================== 存储回调和它订阅的字段数组 ==================
|
||||
this.keyListeners.set(fn as TObservableKeyListener<T, keyof T>, keyArray as (keyof T)[])
|
||||
|
||||
// ================== 立即调用 ==================
|
||||
if (options.immediate) {
|
||||
const result = {} as Pick<T, K>
|
||||
keyArray.forEach(k => {
|
||||
result[k] = (this.state as Record<keyof T, unknown>)[k] as T[K]
|
||||
})
|
||||
try {
|
||||
fn(result)
|
||||
} catch (err) {
|
||||
console.error("Observable subscribeKey immediate error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 返回取消订阅函数 ==================
|
||||
return () => {
|
||||
this.keyListeners.delete(fn as TObservableKeyListener<T, keyof T>)
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量更新状态(避免重复 schedule) */
|
||||
public patch(values: Partial<T>): void {
|
||||
let changed = false
|
||||
for (const key in values) {
|
||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
||||
const typedKey = key as keyof T
|
||||
const oldValue = (this.state as Record<keyof T, unknown>)[typedKey]
|
||||
const newValue = values[typedKey] as unknown
|
||||
if (oldValue !== newValue) {
|
||||
(this.state as Record<keyof T, unknown>)[typedKey] = newValue
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果至少有一处变化,安排一次通知(如果写入已由 set 调度过也不会重复安排)
|
||||
if (changed) this.scheduleNotify()
|
||||
}
|
||||
|
||||
/** 销毁 Observable 实例 */
|
||||
public dispose(): void {
|
||||
this.disposed = true
|
||||
this.listeners.clear()
|
||||
this.keyListeners.clear()
|
||||
this.pendingKeys.clear()
|
||||
this.proxyCache = new WeakMap()
|
||||
Object.freeze(this.state)
|
||||
}
|
||||
|
||||
/** 语法糖:返回一个可解构赋值的 Proxy */
|
||||
public toRefsProxy(): { [K in keyof T]: T[K] } {
|
||||
const self = this
|
||||
return new Proxy({} as { [K in keyof T]: T[K] }, {
|
||||
get(_, prop: string | symbol) {
|
||||
const key = prop as keyof T
|
||||
return (self.state as Record<keyof T, unknown>)[key] as T[typeof key]
|
||||
},
|
||||
set(_, prop: string | symbol, value) {
|
||||
const key = prop as keyof T
|
||||
;(self.state as Record<keyof T, unknown>)[key] = value as unknown
|
||||
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,14 +0,0 @@
|
||||
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
|
||||
|
||||
interface IGlobalStoreParams {
|
||||
/** 桌面根dom ID,类似显示器 */
|
||||
monitorDomId: string;
|
||||
monitorWidth: number;
|
||||
monitorHeight: number;
|
||||
}
|
||||
|
||||
export const globalStore = new ObservableImpl<IGlobalStoreParams>({
|
||||
monitorDomId: '#app',
|
||||
monitorWidth: 0,
|
||||
monitorHeight: 0
|
||||
})
|
||||
@@ -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,752 +0,0 @@
|
||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
||||
|
||||
/** 拖拽移动开始的回调 */
|
||||
type TDragStartCallback = (x: number, y: number) => void;
|
||||
/** 拖拽移动中的回调 */
|
||||
type TDragMoveCallback = (x: number, y: number) => void;
|
||||
/** 拖拽移动结束的回调 */
|
||||
type TDragEndCallback = (x: number, y: number) => void;
|
||||
|
||||
/** 拖拽调整尺寸的方向 */
|
||||
type TResizeDirection =
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
|
||||
/** 元素边界 */
|
||||
interface IElementRect {
|
||||
/** 宽度 */
|
||||
width: number;
|
||||
/** 高度 */
|
||||
height: number;
|
||||
/** 顶点坐标(相对 offsetParent) */
|
||||
top: number;
|
||||
/** 左点坐标(相对 offsetParent) */
|
||||
left: number;
|
||||
}
|
||||
|
||||
/** 拖拽调整尺寸回调数据 */
|
||||
interface IResizeCallbackData {
|
||||
/** 宽度 */
|
||||
width: number;
|
||||
/** 高度 */
|
||||
height: number;
|
||||
/** 顶点坐标(相对 offsetParent) */
|
||||
top: number;
|
||||
/** 左点坐标(相对 offsetParent) */
|
||||
left: number;
|
||||
/** 拖拽调整尺寸的方向 */
|
||||
direction: TResizeDirection;
|
||||
}
|
||||
|
||||
/** 拖拽参数 */
|
||||
interface IDraggableResizableOptions {
|
||||
/** 拖拽/调整尺寸目标元素 */
|
||||
target: HTMLElement;
|
||||
/** 拖拽句柄 */
|
||||
handle?: HTMLElement;
|
||||
/** 拖拽边界容器元素 */
|
||||
boundaryElement?: HTMLElement;
|
||||
/** 移动步进(网格吸附) */
|
||||
snapGrid?: number;
|
||||
/** 关键点吸附阈值 */
|
||||
snapThreshold?: number;
|
||||
/** 是否开启吸附动画 */
|
||||
snapAnimation?: boolean;
|
||||
/** 拖拽结束吸附动画时长 */
|
||||
snapAnimationDuration?: number;
|
||||
/** 是否允许超出边界 */
|
||||
allowOverflow?: boolean;
|
||||
/** 最小化任务栏位置的元素ID */
|
||||
taskbarElementId: string;
|
||||
|
||||
/** 拖拽开始回调 */
|
||||
onDragStart?: TDragStartCallback;
|
||||
/** 拖拽移动中的回调 */
|
||||
onDragMove?: TDragMoveCallback;
|
||||
/** 拖拽结束回调 */
|
||||
onDragEnd?: TDragEndCallback;
|
||||
|
||||
/** 调整尺寸的最小宽度 */
|
||||
minWidth?: number;
|
||||
/** 调整尺寸的最小高度 */
|
||||
minHeight?: number;
|
||||
/** 调整尺寸的最大宽度 */
|
||||
maxWidth?: number;
|
||||
/** 调整尺寸的最大高度 */
|
||||
maxHeight?: number;
|
||||
|
||||
/** 拖拽调整尺寸中的回调 */
|
||||
onResizeMove?: (data: IResizeCallbackData) => void;
|
||||
/** 拖拽调整尺寸结束回调 */
|
||||
onResizeEnd?: (data: IResizeCallbackData) => void;
|
||||
|
||||
/** 窗口状态改变回调 */
|
||||
onWindowStateChange?: (state: TWindowFormState) => void;
|
||||
}
|
||||
|
||||
/** 拖拽的范围边界 */
|
||||
interface IBoundaryRect {
|
||||
/** 最小 X 坐标 */
|
||||
minX?: number;
|
||||
/** 最大 X 坐标 */
|
||||
maxX?: number;
|
||||
/** 最小 Y 坐标 */
|
||||
minY?: number;
|
||||
/** 最大 Y 坐标 */
|
||||
maxY?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽 + 调整尺寸 + 最大最小化 通用类
|
||||
* 统一使用 position: absolute + transform: translate 实现拖拽
|
||||
*/
|
||||
export class DraggableResizableWindow {
|
||||
private handle?: HTMLElement;
|
||||
private target: HTMLElement;
|
||||
private boundaryElement: HTMLElement;
|
||||
private snapGrid: number;
|
||||
private snapThreshold: number;
|
||||
private snapAnimation: boolean;
|
||||
private snapAnimationDuration: number;
|
||||
private allowOverflow: boolean;
|
||||
|
||||
private onDragStart?: TDragStartCallback;
|
||||
private onDragMove?: TDragMoveCallback;
|
||||
private onDragEnd?: TDragEndCallback;
|
||||
|
||||
private onResizeMove?: (data: IResizeCallbackData) => void;
|
||||
private onResizeEnd?: (data: IResizeCallbackData) => void;
|
||||
|
||||
private onWindowStateChange?: (state: TWindowFormState) => void;
|
||||
|
||||
private isDragging = false;
|
||||
private currentDirection: TResizeDirection | null = null;
|
||||
private dragThreshold = 2; // 拖拽阈值 超过才开始真正的拖拽
|
||||
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private startWidth = 0;
|
||||
private startHeight = 0;
|
||||
private startTop = 0;
|
||||
private startLeft = 0;
|
||||
private offsetX = 0;
|
||||
private offsetY = 0;
|
||||
private currentX = 0;
|
||||
private currentY = 0;
|
||||
|
||||
private pendingDrag = false;
|
||||
private pendingResize = false;
|
||||
private dragDX = 0;
|
||||
private dragDY = 0;
|
||||
private resizeDX = 0;
|
||||
private resizeDY = 0;
|
||||
|
||||
private minWidth: number;
|
||||
private minHeight: number;
|
||||
private maxWidth: number;
|
||||
private maxHeight: number;
|
||||
|
||||
private containerRect: DOMRect;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private mutationObserver: MutationObserver;
|
||||
private animationFrame?: number;
|
||||
|
||||
private _windowFormState: TWindowFormState = 'default';
|
||||
/** 元素信息 */
|
||||
private targetBounds: IElementRect;
|
||||
/** 最小化前的元素信息 */
|
||||
private targetPreMinimizeBounds?: IElementRect;
|
||||
/** 最大化前的元素信息 */
|
||||
private targetPreMaximizedBounds?: IElementRect;
|
||||
private taskbarElementId: string;
|
||||
|
||||
get windowFormState() {
|
||||
return this._windowFormState;
|
||||
}
|
||||
|
||||
constructor(options: IDraggableResizableOptions) {
|
||||
this.handle = options.handle;
|
||||
this.target = options.target;
|
||||
this.boundaryElement = options.boundaryElement ?? document.body;
|
||||
this.snapGrid = options.snapGrid ?? 1;
|
||||
this.snapThreshold = options.snapThreshold ?? 0;
|
||||
this.snapAnimation = options.snapAnimation ?? false;
|
||||
this.snapAnimationDuration = options.snapAnimationDuration ?? 200;
|
||||
this.allowOverflow = options.allowOverflow ?? true;
|
||||
|
||||
this.onDragStart = options.onDragStart;
|
||||
this.onDragMove = options.onDragMove;
|
||||
this.onDragEnd = options.onDragEnd;
|
||||
|
||||
this.minWidth = options.minWidth ?? 100;
|
||||
this.minHeight = options.minHeight ?? 50;
|
||||
this.maxWidth = options.maxWidth ?? window.innerWidth;
|
||||
this.maxHeight = options.maxHeight ?? window.innerHeight;
|
||||
this.onResizeMove = options.onResizeMove;
|
||||
this.onResizeEnd = options.onResizeEnd;
|
||||
this.onWindowStateChange = options.onWindowStateChange;
|
||||
|
||||
this.taskbarElementId = options.taskbarElementId;
|
||||
|
||||
this.target.style.position = "absolute";
|
||||
this.target.style.left = '0px';
|
||||
this.target.style.top = '0px';
|
||||
this.target.style.transform = "translate(0px, 0px)";
|
||||
|
||||
this.init();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.targetBounds = {
|
||||
width: this.target.offsetWidth,
|
||||
height: this.target.offsetHeight,
|
||||
top: this.target.offsetTop,
|
||||
left: this.target.offsetLeft,
|
||||
};
|
||||
this.containerRect = this.boundaryElement.getBoundingClientRect();
|
||||
const x = this.containerRect.width / 2 - this.target.offsetWidth / 2;
|
||||
const y = this.containerRect.height / 2 - this.target.offsetHeight / 2;
|
||||
this.target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
private init() {
|
||||
if (this.handle) {
|
||||
this.handle.addEventListener('mousedown', this.onMouseDownDrag);
|
||||
}
|
||||
this.target.addEventListener('mousedown', this.onMouseDownResize);
|
||||
this.target.addEventListener('mouseleave', this.onMouseLeave);
|
||||
document.addEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
||||
|
||||
this.observeResize(this.boundaryElement);
|
||||
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
for (const mutation of mutations) {
|
||||
mutation.removedNodes.forEach(node => {
|
||||
if (node === this.target) this.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
if (this.target.parentElement) {
|
||||
this.mutationObserver.observe(this.target.parentElement, { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
private onMouseDownDrag = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!this.handle?.contains(e.target as Node)) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('btn')) return;
|
||||
if (this.getResizeDirection(e)) return;
|
||||
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
|
||||
document.addEventListener('mousemove', this.checkDragStart);
|
||||
document.addEventListener('mouseup', this.cancelPendingDrag);
|
||||
}
|
||||
|
||||
private checkDragStart = (e: MouseEvent) => {
|
||||
const dx = e.clientX - this.startX;
|
||||
const dy = e.clientY - this.startY;
|
||||
|
||||
if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) {
|
||||
// 超过阈值,真正开始拖拽
|
||||
document.removeEventListener('mousemove', this.checkDragStart);
|
||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
||||
|
||||
if (this._windowFormState === 'maximized') {
|
||||
const preRect = this.targetPreMaximizedBounds!;
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const relX = e.clientX / rect.width;
|
||||
const relY = e.clientY / rect.height;
|
||||
const newLeft = e.clientX - preRect.width * relX;
|
||||
const newTop = e.clientY - preRect.height * relY;
|
||||
this.targetPreMaximizedBounds = {
|
||||
width: preRect.width,
|
||||
height: preRect.height,
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
};
|
||||
|
||||
this.restore(() => this.startDrag(e));
|
||||
} else {
|
||||
this.startDrag(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private cancelPendingDrag = () => {
|
||||
document.removeEventListener('mousemove', this.checkDragStart);
|
||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
||||
}
|
||||
|
||||
private startDrag = (e: MouseEvent) => {
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
|
||||
const style = window.getComputedStyle(this.target);
|
||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
||||
this.offsetX = matrix.m41;
|
||||
this.offsetY = matrix.m42;
|
||||
|
||||
this.onDragStart?.(this.offsetX, this.offsetY);
|
||||
|
||||
document.addEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.addEventListener('mouseup', this.onMouseUpDrag);
|
||||
};
|
||||
|
||||
private onMouseMoveDragRAF = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.dragDX = e.clientX - this.startX;
|
||||
this.dragDY = e.clientY - this.startY;
|
||||
if (!this.pendingDrag) {
|
||||
this.pendingDrag = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.pendingDrag = false;
|
||||
this.applyDragFrame();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private applyDragFrame() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
let newX = this.offsetX + this.dragDX;
|
||||
let newY = this.offsetY + this.dragDY;
|
||||
|
||||
if (this.snapGrid > 1) {
|
||||
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
|
||||
newY = Math.round(newY / this.snapGrid) * this.snapGrid;
|
||||
}
|
||||
|
||||
this.applyPosition(newX, newY, false);
|
||||
this.onDragMove?.(newX, newY);
|
||||
}
|
||||
|
||||
private onMouseUpDrag = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
|
||||
const snapped = this.applySnapping(this.currentX, this.currentY);
|
||||
|
||||
if (this.snapAnimation) {
|
||||
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
|
||||
this.onDragEnd?.(snapped.x, snapped.y);
|
||||
this.updateTargetBounds(snapped.x, snapped.y);
|
||||
});
|
||||
} else {
|
||||
this.applyPosition(snapped.x, snapped.y, true);
|
||||
this.onDragEnd?.(snapped.x, snapped.y);
|
||||
this.updateTargetBounds(snapped.x, snapped.y);
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
||||
};
|
||||
|
||||
private applyPosition(x: number, y: number, isFinal: boolean) {
|
||||
this.currentX = x;
|
||||
this.currentY = y;
|
||||
this.target.style.transform = `translate(${x}px, ${y}px)`;
|
||||
if (isFinal) this.applyBoundary();
|
||||
}
|
||||
|
||||
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
|
||||
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
||||
const startX = this.currentX;
|
||||
const startY = this.currentY;
|
||||
const deltaX = targetX - startX;
|
||||
const deltaY = targetY - startY;
|
||||
const startTime = performance.now();
|
||||
|
||||
const step = (now: number) => {
|
||||
const elapsed = now - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
const x = startX + deltaX * ease;
|
||||
const y = startY + deltaY * ease;
|
||||
|
||||
this.applyPosition(x, y, false);
|
||||
this.onDragMove?.(x, y);
|
||||
|
||||
if (progress < 1) this.animationFrame = requestAnimationFrame(step);
|
||||
else { this.applyPosition(targetX, targetY, true); onComplete?.(); }
|
||||
};
|
||||
this.animationFrame = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
private applyBoundary() {
|
||||
if (this.allowOverflow) return;
|
||||
let { x, y } = { x: this.currentX, y: this.currentY };
|
||||
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
x = Math.min(Math.max(x, 0), this.containerRect.width - rect.width);
|
||||
y = Math.min(Math.max(y, 0), this.containerRect.height - rect.height);
|
||||
|
||||
this.currentX = x;
|
||||
this.currentY = y;
|
||||
this.applyPosition(x, y, false);
|
||||
}
|
||||
|
||||
private applySnapping(x: number, y: number) {
|
||||
let snappedX = x, snappedY = y;
|
||||
const containerSnap = this.getSnapPoints();
|
||||
if (this.snapThreshold > 0) {
|
||||
for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapThreshold) { snappedX = sx; break; }
|
||||
for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapThreshold) { snappedY = sy; break; }
|
||||
}
|
||||
return { x: snappedX, y: snappedY };
|
||||
}
|
||||
|
||||
private getSnapPoints() {
|
||||
const snapPoints = { x: [] as number[], y: [] as number[] };
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
snapPoints.x = [0, this.containerRect.width - rect.width];
|
||||
snapPoints.y = [0, this.containerRect.height - rect.height];
|
||||
return snapPoints;
|
||||
}
|
||||
|
||||
private onMouseDownResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const dir = this.getResizeDirection(e);
|
||||
if (!dir) return;
|
||||
|
||||
this.startResize(e, dir);
|
||||
};
|
||||
|
||||
private onMouseLeave = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.updateCursor(null);
|
||||
};
|
||||
|
||||
private startResize(e: MouseEvent, dir: TResizeDirection) {
|
||||
this.currentDirection = dir;
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(this.target);
|
||||
const matrix = new DOMMatrixReadOnly(style.transform);
|
||||
this.offsetX = matrix.m41;
|
||||
this.offsetY = matrix.m42;
|
||||
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startWidth = rect.width;
|
||||
this.startHeight = rect.height;
|
||||
this.startLeft = this.offsetX;
|
||||
this.startTop = this.offsetY;
|
||||
|
||||
document.addEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.addEventListener('mouseup', this.onResizeEndHandler);
|
||||
}
|
||||
|
||||
private onResizeDragRAF = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.resizeDX = e.clientX - this.startX;
|
||||
this.resizeDY = e.clientY - this.startY;
|
||||
if (!this.pendingResize) {
|
||||
this.pendingResize = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.pendingResize = false;
|
||||
this.applyResizeFrame();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private applyResizeFrame() {
|
||||
if (!this.currentDirection) return;
|
||||
|
||||
let newWidth = this.startWidth;
|
||||
let newHeight = this.startHeight;
|
||||
let newX = this.startLeft;
|
||||
let newY = this.startTop;
|
||||
|
||||
const dx = this.resizeDX;
|
||||
const dy = this.resizeDY;
|
||||
|
||||
switch (this.currentDirection) {
|
||||
case 'right': newWidth += dx; break;
|
||||
case 'bottom': newHeight += dy; break;
|
||||
case 'bottom-right': newWidth += dx; newHeight += dy; break;
|
||||
case 'left': newWidth -= dx; newX += dx; break;
|
||||
case 'top': newHeight -= dy; newY += dy; break;
|
||||
case 'top-left': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break;
|
||||
case 'top-right': newWidth += dx; newHeight -= dy; newY += dy; break;
|
||||
case 'bottom-left': newWidth -= dx; newX += dx; newHeight += dy; break;
|
||||
}
|
||||
|
||||
const d = this.applyResizeBounds(newX, newY, newWidth, newHeight);
|
||||
|
||||
this.updateCursor(this.currentDirection);
|
||||
|
||||
this.onResizeMove?.({
|
||||
width: d.width,
|
||||
height: d.height,
|
||||
left: d.left,
|
||||
top: d.top,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
}
|
||||
|
||||
// 应用尺寸调整边界
|
||||
private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number): {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
// 最小/最大宽高限制
|
||||
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
|
||||
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
|
||||
|
||||
// 边界限制
|
||||
if (this.allowOverflow) {
|
||||
this.currentX = newX;
|
||||
this.currentY = newY;
|
||||
this.target.style.width = `${newWidth}px`;
|
||||
this.target.style.height = `${newHeight}px`;
|
||||
this.applyPosition(newX, newY, false);
|
||||
return {
|
||||
left: newX,
|
||||
top: newY,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
|
||||
newX = Math.min(Math.max(0, newX), this.containerRect.width - newWidth);
|
||||
newY = Math.min(Math.max(0, newY), this.containerRect.height - newHeight);
|
||||
|
||||
this.currentX = newX;
|
||||
this.currentY = newY;
|
||||
this.target.style.width = `${newWidth}px`;
|
||||
this.target.style.height = `${newHeight}px`;
|
||||
this.applyPosition(newX, newY, false);
|
||||
return {
|
||||
left: newX,
|
||||
top: newY,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
|
||||
private onResizeEndHandler = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
if (!this.currentDirection) return;
|
||||
this.onResizeEnd?.({
|
||||
width: this.target.offsetWidth,
|
||||
height: this.target.offsetHeight,
|
||||
left: this.currentX,
|
||||
top: this.currentY,
|
||||
direction: this.currentDirection,
|
||||
});
|
||||
this.updateTargetBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight);
|
||||
this.currentDirection = null;
|
||||
this.updateCursor(null);
|
||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
||||
};
|
||||
|
||||
private getResizeDirection(e: MouseEvent): TResizeDirection | null {
|
||||
const rect = this.target.getBoundingClientRect();
|
||||
const offset = 4;
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
const top = y >= rect.top && y <= rect.top + offset;
|
||||
const bottom = y >= rect.bottom - offset && y <= rect.bottom;
|
||||
const left = x >= rect.left && x <= rect.left + offset;
|
||||
const right = x >= rect.right - offset && x <= rect.right;
|
||||
|
||||
if (top && left) return 'top-left';
|
||||
if (top && right) return 'top-right';
|
||||
if (bottom && left) return 'bottom-left';
|
||||
if (bottom && right) return 'bottom-right';
|
||||
if (top) return 'top';
|
||||
if (bottom) return 'bottom';
|
||||
if (left) return 'left';
|
||||
if (right) return 'right';
|
||||
return null;
|
||||
}
|
||||
|
||||
private updateCursor(direction: TResizeDirection | null) {
|
||||
if (!direction) { this.target.style.cursor = 'default'; return; }
|
||||
const cursorMap: Record<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() {
|
||||
try {
|
||||
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
|
||||
this.target.removeEventListener('mousedown', this.onMouseDownResize);
|
||||
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
|
||||
document.removeEventListener('mouseup', this.onMouseUpDrag);
|
||||
document.removeEventListener('mousemove', this.onResizeDragRAF);
|
||||
document.removeEventListener('mouseup', this.onResizeEndHandler);
|
||||
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
|
||||
document.removeEventListener('mousemove', this.checkDragStart);
|
||||
document.removeEventListener('mouseup', this.cancelPendingDrag);
|
||||
this.resizeObserver?.disconnect();
|
||||
this.mutationObserver.disconnect();
|
||||
cancelAnimationFrame(this.animationFrame ?? 0);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
@@ -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,14 +0,0 @@
|
||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
||||
|
||||
export interface IWindowForm extends IDestroyable {
|
||||
/** 窗体id */
|
||||
get id(): string;
|
||||
/** 窗体所属的进程 */
|
||||
get proc(): IProcess | undefined;
|
||||
/** 窗体元素 */
|
||||
get windowFormEle(): HTMLElement;
|
||||
/** 窗体状态 */
|
||||
get windowFormState(): TWindowFormState;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import XSystem from '../../XSystem.ts'
|
||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
||||
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
|
||||
import type { TWindowFormState, WindowFormPos } from '@/core/window/types/WindowFormTypes.ts'
|
||||
import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts'
|
||||
import '../ui/WindowFormElement.ts'
|
||||
import { wfem } from '@/core/events/WindowFormEventManager.ts'
|
||||
import type { IObservable } from '@/core/state/IObservable.ts'
|
||||
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
|
||||
|
||||
export interface IWindowFormDataState {
|
||||
/** 窗体id */
|
||||
id: string;
|
||||
/** 窗体进程id */
|
||||
procId: string;
|
||||
/** 进程名称唯一 */
|
||||
name: string;
|
||||
/** 窗体标题 */
|
||||
title: string;
|
||||
/** 窗体位置x (左上角) */
|
||||
x: number;
|
||||
/** 窗体位置y (左上角) */
|
||||
y: number;
|
||||
/** 窗体宽度 */
|
||||
width: number;
|
||||
/** 窗体高度 */
|
||||
height: number;
|
||||
/** 窗体状态 'default' | 'minimized' | 'maximized' */
|
||||
state: TWindowFormState;
|
||||
/** 窗体是否已关闭 */
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
export default class WindowFormImpl implements IWindowForm {
|
||||
private readonly _id: string = uuidV4()
|
||||
private readonly _proc: IProcess
|
||||
private readonly _data: IObservable<IWindowFormDataState>
|
||||
private dom: HTMLElement
|
||||
private drw: DraggableResizableWindow
|
||||
|
||||
public get id() {
|
||||
return this._id
|
||||
}
|
||||
public get proc() {
|
||||
return this._proc
|
||||
}
|
||||
private get desktopRootDom() {
|
||||
return XSystem.instance.desktopRootDom
|
||||
}
|
||||
public get windowFormEle() {
|
||||
return this.dom
|
||||
}
|
||||
public get windowFormState() {
|
||||
return this.drw.windowFormState
|
||||
}
|
||||
|
||||
constructor(proc: IProcess, config: IWindowFormConfig) {
|
||||
this._proc = proc
|
||||
console.log('WindowForm')
|
||||
|
||||
this._data = new ObservableImpl<IWindowFormDataState>({
|
||||
id: this.id,
|
||||
procId: proc.id,
|
||||
name: proc.processInfo.name,
|
||||
title: config.title ?? '未命名',
|
||||
x: config.left ?? 0,
|
||||
y: config.top ?? 0,
|
||||
width: config.width ?? 200,
|
||||
height: config.height ?? 100,
|
||||
state: 'default',
|
||||
closed: false,
|
||||
})
|
||||
|
||||
this.initEvent()
|
||||
this.createWindowFrom()
|
||||
}
|
||||
|
||||
private initEvent() {
|
||||
this._data.subscribeKey('closed', (state) => {
|
||||
console.log('closed', state)
|
||||
this.closeWindowForm()
|
||||
this._proc.closeWindowForm(this.id)
|
||||
})
|
||||
}
|
||||
|
||||
private createWindowFrom() {
|
||||
const wf = document.createElement('window-form-element')
|
||||
wf.wid = this.id
|
||||
wf.wfData = this._data
|
||||
wf.title = this._data.state.title
|
||||
wf.dragContainer = document.body
|
||||
wf.snapDistance = 20
|
||||
wf.taskbarElementId = '#taskbar'
|
||||
this.dom = wf
|
||||
this.desktopRootDom.appendChild(this.dom)
|
||||
Promise.resolve().then(() => {
|
||||
wfem.notifyEvent('windowFormCreated')
|
||||
wfem.notifyEvent('windowFormFocus', this.id)
|
||||
})
|
||||
}
|
||||
|
||||
private closeWindowForm() {
|
||||
this.desktopRootDom.removeChild(this.dom)
|
||||
this._data.dispose()
|
||||
}
|
||||
|
||||
public minimize() {}
|
||||
public maximize() {}
|
||||
public restore() {}
|
||||
|
||||
public destroy() {}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { WindowFormEventMap } from '@/core/window/ui/WindowFormElement.ts'
|
||||
|
||||
export function addWindowFormEventListener<K extends keyof WindowFormEventMap>(
|
||||
el: HTMLElement,
|
||||
type: K,
|
||||
listener: (ev: WindowFormEventMap[K]) => any,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
) {
|
||||
// 强制类型转换,保证 TS 不报错
|
||||
el.addEventListener(type, listener as EventListener, options);
|
||||
}
|
||||
|
||||
export function removeWindowFormEventListener<K extends keyof WindowFormEventMap>(
|
||||
el: HTMLElement,
|
||||
type: K,
|
||||
listener: (ev: WindowFormEventMap[K]) => any,
|
||||
options?: boolean | EventListenerOptions
|
||||
) {
|
||||
el.removeEventListener(type, listener as EventListener, options);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ body {
|
||||
background-color: var(--color-light);
|
||||
-webkit-font-smoothing: antialiased; /* 字体抗锯齿 */
|
||||
text-rendering: optimizeLegibility;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== 排版元素 ===== */
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl.ts'
|
||||
import type { IEventMap } from '@/events/IEventBuilder.ts'
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
|
||||
/**
|
||||
* 桌面相关的事件
|
||||
*/
|
||||
export interface IDesktopEvent extends IEventMap {
|
||||
/**
|
||||
* 桌面应用位置改变
|
||||
*/
|
||||
desktopAppPosChange: (info: IDesktopAppIcon) => void;
|
||||
}
|
||||
|
||||
/** 窗口事件管理器 */
|
||||
export const desktopEM = new EventBuilderImpl<IDesktopEvent>()
|
||||
@@ -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,41 +1,84 @@
|
||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
||||
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
|
||||
*/
|
||||
addEventListener<E extends keyof Events, F extends Events[E]>(
|
||||
subscribe<E extends keyof Events, F extends Events[E]>(
|
||||
eventName: E,
|
||||
handler: F,
|
||||
options?: {
|
||||
immediate?: boolean
|
||||
immediateArgs?: Parameters<F>
|
||||
once?: boolean
|
||||
},
|
||||
): void
|
||||
}
|
||||
): () => void
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
* 移除事件
|
||||
* @param eventName 事件名称
|
||||
* @param handler 事件处理函数
|
||||
* @returns void
|
||||
*/
|
||||
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
|
||||
remove<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
@@ -43,5 +86,8 @@ export interface IEventBuilder<Events extends IEventMap> extends IDestroyable {
|
||||
* @param args 参数
|
||||
* @returns void
|
||||
*/
|
||||
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>): void
|
||||
notify<E extends keyof Events, F extends Events[E]>(
|
||||
eventName: E,
|
||||
...args: Parameters<F>
|
||||
): void
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl.ts'
|
||||
import type { IEventMap } from '@/events/IEventBuilder.ts'
|
||||
import type { TWindowFormState } from '@/ui/types/WindowFormTypes.ts'
|
||||
|
||||
/**
|
||||
* 窗口的事件
|
||||
*/
|
||||
export interface IWindowFormEvent extends IEventMap {
|
||||
/**
|
||||
* 窗口最小化
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormMinimize: (id: string) => void;
|
||||
/**
|
||||
* 窗口最大化
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormMaximize: (id: string) => void;
|
||||
/**
|
||||
* 窗口还原
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormRestore: (id: string) => void;
|
||||
/**
|
||||
* 窗口关闭
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormClose: (id: string) => void;
|
||||
/**
|
||||
* 窗口聚焦
|
||||
* @param id 窗口id
|
||||
*/
|
||||
windowFormFocus: (id: string) => void;
|
||||
/**
|
||||
* 窗口数据更新
|
||||
* @param data 窗口数据
|
||||
*/
|
||||
windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void;
|
||||
/**
|
||||
* 窗口创建完成
|
||||
*/
|
||||
windowFormCreated: () => void;
|
||||
}
|
||||
|
||||
interface IWindowFormDataUpdateParams {
|
||||
/** 窗口id */
|
||||
id: string;
|
||||
/** 窗口状态 */
|
||||
state: TWindowFormState,
|
||||
/** 窗口宽度 */
|
||||
width: number,
|
||||
/** 窗口高度 */
|
||||
height: number,
|
||||
/** 窗口x坐标(左上角) */
|
||||
x: number,
|
||||
/** 窗口y坐标(左上角) */
|
||||
y: number
|
||||
}
|
||||
|
||||
/** 窗口事件管理器 */
|
||||
export const wfem = new EventBuilderImpl<IWindowFormEvent>()
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts'
|
||||
import type { IEventBuilder, IEventMap } from '../IEventBuilder.ts'
|
||||
|
||||
interface HandlerWrapper<T extends (...args: any[]) => any> {
|
||||
fn: T
|
||||
@@ -13,12 +13,13 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
* @param eventName 事件名称
|
||||
* @param handler 监听器
|
||||
* @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
|
||||
* @returns 返回一个 `unsubscribe` 函数,用于移除当前监听
|
||||
* @example
|
||||
* eventBus.addEventListener('noArgs', () => {})
|
||||
* eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
|
||||
* eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] })
|
||||
* eventBus.subscribe('noArgs', () => {})
|
||||
* eventBus.subscribe('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
|
||||
* 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,
|
||||
handler: F,
|
||||
options?: {
|
||||
@@ -26,15 +27,16 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
immediateArgs?: Parameters<F>
|
||||
once?: boolean
|
||||
},
|
||||
) {
|
||||
if (!handler) return
|
||||
): () => void {
|
||||
if (!handler) return () => {}
|
||||
if (!this._eventHandlers.has(eventName)) {
|
||||
this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>())
|
||||
}
|
||||
|
||||
const set = this._eventHandlers.get(eventName)!
|
||||
const wrapper: HandlerWrapper<F> = { fn: handler, once: options?.once ?? false }
|
||||
if (![...set].some((wrapper) => wrapper.fn === handler)) {
|
||||
set.add({ fn: handler, once: options?.once ?? false })
|
||||
set.add(wrapper)
|
||||
}
|
||||
|
||||
if (options?.immediate) {
|
||||
@@ -44,6 +46,12 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
set.delete(wrapper)
|
||||
// 如果该事件下无监听器,则删除集合
|
||||
if (set.size === 0) this._eventHandlers.delete(eventName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,9 +59,9 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
* @param eventName 事件名称
|
||||
* @param handler 监听器
|
||||
* @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)
|
||||
if (!set) return
|
||||
|
||||
@@ -62,6 +70,9 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
set.delete(wrapper)
|
||||
}
|
||||
}
|
||||
if (set.size === 0) {
|
||||
this._eventHandlers.delete(eventName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,11 +80,11 @@ export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder
|
||||
* @param eventName 事件名称
|
||||
* @param args 参数
|
||||
* @example
|
||||
* eventBus.notifyEvent('noArgs')
|
||||
* eventBus.notifyEvent('greet', 'Alice')
|
||||
* eventBus.notifyEvent('onResize', 1, 2)
|
||||
* eventBus.notify('noArgs')
|
||||
* eventBus.notify('greet', 'Alice')
|
||||
* 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
|
||||
|
||||
const set = this._eventHandlers.get(eventName)!
|
||||
|
||||
48
src/main.ts
48
src/main.ts
@@ -1,15 +1,61 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { naiveUi } from '@/common/naive-ui/components.ts'
|
||||
import { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||
import { registerBuiltInApps } from '@/apps'
|
||||
|
||||
import 'virtual:uno.css'
|
||||
import './css/basic.css'
|
||||
|
||||
import App from './ui/App.vue'
|
||||
|
||||
// 注册内置应用
|
||||
registerBuiltInApps()
|
||||
|
||||
// 初始化系统服务
|
||||
const systemService = new SystemServiceIntegration({
|
||||
debug: import.meta.env.DEV
|
||||
})
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册插件
|
||||
app.use(createPinia())
|
||||
app.use(naiveUi)
|
||||
|
||||
app.mount('#app')
|
||||
// 提供系统服务给组件使用
|
||||
app.provide('systemService', systemService)
|
||||
|
||||
// 初始化系统服务然后挂载应用
|
||||
systemService
|
||||
.initialize()
|
||||
.then(() => {
|
||||
app.mount('#app')
|
||||
console.log('桌面系统启动完成')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('系统启动失败:', error)
|
||||
// 显示错误信息
|
||||
document.body.innerHTML = `
|
||||
<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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user