Compare commits

..

2 Commits

Author SHA1 Message Date
f80c1863b9 Initial commit 2025-10-21 11:10:09 +08:00
ff4791922e Initial commit 2025-10-21 11:09:40 +08:00
119 changed files with 3494 additions and 12584 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,167 +0,0 @@
# 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": 如果可能,将对象压缩到单行

View File

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

View File

@@ -1,37 +1,2 @@
# vue-desktop # vue-desktop
浏览器Chrome 84+、Edge 84+、Firefox 79+、Safari 14+
Node.jsv14+
不支持IE
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm build
```

View File

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

View File

@@ -35,7 +35,7 @@
"sass": "^1.90.0", "sass": "^1.90.0",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"unocss": "^66.4.2", "unocss": "^66.4.2",
"vite": "^7.2.4", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4" "vue-tsc": "^3.0.4"
} }

104
pnpm-lock.yaml generated
View File

@@ -38,10 +38,10 @@ importers:
version: 22.17.1 version: 22.17.1
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 6.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vitejs/plugin-vue-jsx': '@vitejs/plugin-vue-jsx':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 5.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vue/tsconfig': '@vue/tsconfig':
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3)) version: 0.7.0(typescript@5.8.3)(vue@3.5.18(typescript@5.8.3))
@@ -62,13 +62,13 @@ importers:
version: 5.8.3 version: 5.8.3
unocss: unocss:
specifier: ^66.4.2 specifier: ^66.4.2
version: 66.4.2(postcss@8.5.6)(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) version: 66.4.2(postcss@8.5.6)(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite: vite:
specifier: ^7.2.4 specifier: ^7.0.6
version: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) version: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-vue-devtools: vite-plugin-vue-devtools:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) version: 8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
vue-tsc: vue-tsc:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.5(typescript@5.8.3) version: 3.0.5(typescript@5.8.3)
@@ -1054,15 +1054,6 @@ packages:
picomatch: picomatch:
optional: true optional: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
figures@6.1.0: figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1444,10 +1435,6 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -1539,8 +1526,8 @@ packages:
peerDependencies: peerDependencies:
vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
vite@7.2.4: vite@7.1.1:
resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -2115,13 +2102,13 @@ snapshots:
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@unocss/astro@66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))': '@unocss/astro@66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))':
dependencies: dependencies:
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/reset': 66.4.2 '@unocss/reset': 66.4.2
'@unocss/vite': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/vite': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
optionalDependencies: optionalDependencies:
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
'@unocss/cli@66.4.2': '@unocss/cli@66.4.2':
dependencies: dependencies:
@@ -2252,7 +2239,7 @@ snapshots:
dependencies: dependencies:
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/vite@66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))': '@unocss/vite@66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@unocss/config': 66.4.2 '@unocss/config': 66.4.2
@@ -2263,23 +2250,23 @@ snapshots:
pathe: 2.0.3 pathe: 2.0.3
tinyglobby: 0.2.14 tinyglobby: 0.2.14
unplugin-utils: 0.2.5 unplugin-utils: 0.2.5
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
'@vitejs/plugin-vue-jsx@5.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vitejs/plugin-vue-jsx@5.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
'@rolldown/pluginutils': 1.0.0-beta.31 '@rolldown/pluginutils': 1.0.0-beta.31
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.0) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.0)
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-vue@6.0.1(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vitejs/plugin-vue@6.0.1(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29 '@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
'@volar/language-core@2.4.22': '@volar/language-core@2.4.22':
@@ -2362,14 +2349,14 @@ snapshots:
dependencies: dependencies:
'@vue/devtools-kit': 7.7.7 '@vue/devtools-kit': 7.7.7
'@vue/devtools-core@8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))': '@vue/devtools-core@8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))':
dependencies: dependencies:
'@vue/devtools-kit': 8.0.0 '@vue/devtools-kit': 8.0.0
'@vue/devtools-shared': 8.0.0 '@vue/devtools-shared': 8.0.0
mitt: 3.0.1 mitt: 3.0.1
nanoid: 5.1.5 nanoid: 5.1.5
pathe: 2.0.3 pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-hot-client: 2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- vite - vite
@@ -2637,10 +2624,6 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 picomatch: 4.0.3
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
figures@6.1.0: figures@6.1.0:
dependencies: dependencies:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
@@ -2994,11 +2977,6 @@ snapshots:
fdir: 6.4.6(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
@@ -3022,9 +3000,9 @@ snapshots:
unicorn-magic@0.3.0: {} unicorn-magic@0.3.0: {}
unocss@66.4.2(postcss@8.5.6)(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): unocss@66.4.2(postcss@8.5.6)(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
'@unocss/astro': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/astro': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
'@unocss/cli': 66.4.2 '@unocss/cli': 66.4.2
'@unocss/core': 66.4.2 '@unocss/core': 66.4.2
'@unocss/postcss': 66.4.2(postcss@8.5.6) '@unocss/postcss': 66.4.2(postcss@8.5.6)
@@ -3042,9 +3020,9 @@ snapshots:
'@unocss/transformer-compile-class': 66.4.2 '@unocss/transformer-compile-class': 66.4.2
'@unocss/transformer-directives': 66.4.2 '@unocss/transformer-directives': 66.4.2
'@unocss/transformer-variant-group': 66.4.2 '@unocss/transformer-variant-group': 66.4.2
'@unocss/vite': 66.4.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) '@unocss/vite': 66.4.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
optionalDependencies: optionalDependencies:
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
transitivePeerDependencies: transitivePeerDependencies:
- postcss - postcss
- supports-color - supports-color
@@ -3067,17 +3045,17 @@ snapshots:
evtd: 0.2.4 evtd: 0.2.4
vue: 3.5.18(typescript@5.8.3) vue: 3.5.18(typescript@5.8.3)
vite-dev-rpc@1.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-dev-rpc@1.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
birpc: 2.5.0 birpc: 2.5.0
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-hot-client: 2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-hot-client: 2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite-hot-client@2.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-hot-client@2.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-inspect@11.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-plugin-inspect@11.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
ansis: 4.1.0 ansis: 4.1.0
debug: 4.4.1 debug: 4.4.1
@@ -3087,27 +3065,27 @@ snapshots:
perfect-debounce: 1.0.0 perfect-debounce: 1.0.0
sirv: 3.0.1 sirv: 3.0.1
unplugin-utils: 0.2.5 unplugin-utils: 0.2.5
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-dev-rpc: 1.1.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-dev-rpc: 1.1.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite-plugin-vue-devtools@8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)): vite-plugin-vue-devtools@8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)):
dependencies: dependencies:
'@vue/devtools-core': 8.0.0(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3)) '@vue/devtools-core': 8.0.0(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))(vue@3.5.18(typescript@5.8.3))
'@vue/devtools-kit': 8.0.0 '@vue/devtools-kit': 8.0.0
'@vue/devtools-shared': 8.0.0 '@vue/devtools-shared': 8.0.0
execa: 9.6.0 execa: 9.6.0
sirv: 3.0.1 sirv: 3.0.1
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
vite-plugin-inspect: 11.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-plugin-inspect: 11.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
vite-plugin-vue-inspector: 5.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)) vite-plugin-vue-inspector: 5.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0))
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- supports-color - supports-color
- vue - vue
vite-plugin-vue-inspector@5.3.2(vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)): vite-plugin-vue-inspector@5.3.2(vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)):
dependencies: dependencies:
'@babel/core': 7.28.0 '@babel/core': 7.28.0
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0) '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0)
@@ -3118,18 +3096,18 @@ snapshots:
'@vue/compiler-dom': 3.5.18 '@vue/compiler-dom': 3.5.18
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.17 magic-string: 0.30.17
vite: 7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0) vite: 7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vite@7.2.4(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0): vite@7.1.1(@types/node@22.17.1)(jiti@2.5.1)(sass@1.90.0):
dependencies: dependencies:
esbuild: 0.25.8 esbuild: 0.25.8
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.4.6(picomatch@4.0.3)
picomatch: 4.0.3 picomatch: 4.0.3
postcss: 8.5.6 postcss: 8.5.6
rollup: 4.46.2 rollup: 4.46.2
tinyglobby: 0.2.15 tinyglobby: 0.2.14
optionalDependencies: optionalDependencies:
'@types/node': 22.17.1 '@types/node': 22.17.1
fsevents: 2.3.3 fsevents: 2.3.3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
src/App.vue Normal file
View File

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

View File

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

View File

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

View File

@@ -1,125 +0,0 @@
<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>

View File

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

View File

@@ -1,527 +0,0 @@
<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>

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
/** 单例模式 装饰器
* @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
}

34
src/core/XSystem.ts Normal file
View File

@@ -0,0 +1,34 @@
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)
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "department",
"title": "部门",
"description": "部门",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "部门",
"icon": "iconfont icon-setting",
"width": 200,
"height": 100
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "fileManage",
"title": "文件管理",
"description": "文件管理",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "文件管理",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "music",
"title": "音乐",
"description": "音乐",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "音乐",
"icon": "iconfont icon-setting",
"width": 200,
"height": 100
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "personalCenter",
"title": "个人中心",
"description": "个人中心",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "个人中心",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "photograph",
"title": "照片",
"description": "照片",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "照片",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "recycleBin",
"title": "回收站",
"description": "回收站",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "回收站",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "setting",
"title": "设置",
"description": "设置",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "设置",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

18
src/core/apps/tv/app.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "tv",
"title": "电视",
"description": "电视",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "电视",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,18 @@
{
"name": "video",
"title": "电影",
"description": "电影",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "电影",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

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

View File

@@ -0,0 +1,82 @@
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)
}
}

View File

@@ -0,0 +1,15 @@
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
export const DesktopProcessInfo = new ProcessInfoImpl({
name: 'desktop',
title: '桌面',
version: {
company: 'XZG',
major: 1,
minor: 0,
build: 0,
private: 0
},
singleton: true,
isJustProcess: true
})

View File

@@ -0,0 +1,95 @@
<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>

View File

@@ -0,0 +1,35 @@
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>
`
}
}

View File

@@ -11,8 +11,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts' import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import type { IGridTemplateParams } from '@/ui/types/IGridTemplateParams.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 { iconInfo, gridTemplate } = defineProps<{ iconInfo: IDesktopAppIcon, gridTemplate: IGridTemplateParams }>()
@@ -40,6 +41,8 @@ const onDragEnd = (e: DragEvent) => {
iconInfo.x = gridX iconInfo.x = gridX
iconInfo.y = gridY iconInfo.y = gridY
eventManager.notifyEvent('onDesktopAppIconPos', iconInfo)
} }
</script> </script>

View File

@@ -0,0 +1,17 @@
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>`
}
}

View File

@@ -0,0 +1,31 @@
*,
*::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;
}
}

View File

@@ -0,0 +1,169 @@
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import {
computed,
nextTick,
onMounted,
onUnmounted,
reactive,
ref,
toRaw,
toRefs,
toValue,
useTemplateRef,
watch,
watchEffect,
} from 'vue'
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
import { eventManager } from '@/core/events/EventManager.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
export function useDesktopInit(containerStr: string) {
let container:HTMLElement
// 初始值
const gridTemplate = reactive<IGridTemplateParams>({
cellExpectWidth: 90,
cellExpectHeight: 110,
cellRealWidth: 90,
cellRealHeight: 110,
gapX: 4,
gapY: 4,
colCount: 1,
rowCount: 1
})
const ro = new ResizeObserver(entries => {
const entry= entries[0]
const containerRect = entry.contentRect
gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX));
gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY));
const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1))
const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1))
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
})
const gridStyle = computed(() => ({
gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`,
gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`,
gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`
}))
onMounted(() => {
container = document.querySelector(containerStr)!
ro.observe(container)
})
onUnmounted(() => {
ro.unobserve(container)
ro.disconnect()
})
// 有桌面图标的app
const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess)
const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]')
const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => {
const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name)
// 左上角坐标原点,从上到下从左到右 索引从1开始
const x = index % gridTemplate.rowCount + 1
const y = Math.floor(index / gridTemplate.rowCount) + 1
return {
name: processInfo.name,
icon: processInfo.icon,
path: processInfo.startName,
x: oldAppIcon ? oldAppIcon.x : x,
y: oldAppIcon ? oldAppIcon.y : y
}
})
const appIconsRef = ref(appIcons)
const exceedApp = ref<IDesktopAppIcon[]>([])
watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => {
// if (oCols == 1 && oRows == 1) return
if (oCols === nCols && oRows === nRows) return
const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows)
appIconsRef.value = appIcons
exceedApp.value = hideAppIcons
})
eventManager.addEventListener('onDesktopAppIconPos', (iconInfo) => {
localStorage.setItem('desktopAppIconInfo', JSON.stringify(toValue(appIconsRef.value)))
})
return {
gridTemplate,
appIconsRef,
gridStyle
}
}
/**
* 重新安排图标位置
* @param appIconInfos 图标信息
* @param maxCol 列数
* @param maxRow 行数
*/
function rearrangeIcons(
appIconInfos: IDesktopAppIcon[],
maxCol: number,
maxRow: number
): IRearrangeInfo {
const occupied = new Set<string>();
function key(x: number, y: number) {
return `${x},${y}`;
}
const appIcons: IDesktopAppIcon[] = []
const hideAppIcons: IDesktopAppIcon[] = []
const temp: IDesktopAppIcon[] = []
for (const appIcon of appIconInfos) {
const { x, y } = appIcon;
if (x <= maxCol && y <= maxRow) {
if (!occupied.has(key(x, y))) {
occupied.add(key(x, y))
appIcons.push({ ...appIcon, x, y })
}
} else {
temp.push(appIcon)
}
}
const max = maxCol * maxRow
for (const appIcon of temp) {
if (appIcons.length < max) {
// 最后格子也被占 → 从 (1,1) 开始找空位
let placed = false;
for (let c = 1; c <= maxCol; c++) {
for (let r = 1; r <= maxRow; r++) {
if (!occupied.has(key(c, r))) {
occupied.add(key(c, r));
appIcons.push({ ...appIcon, x: c, y: r });
placed = true;
break;
}
}
if (placed) break;
}
} else {
// 放不下了
hideAppIcons.push(appIcon)
}
}
return {
appIcons,
hideAppIcons
};
}
interface IRearrangeInfo {
/** 正常的桌面图标信息 */
appIcons: IDesktopAppIcon[];
/** 隐藏的桌面图标信息(超出屏幕显示的) */
hideAppIcons: IDesktopAppIcon[];
}

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,64 @@
import { useDraggable } from '@vueuse/core'
import { ref } from 'vue'
export function useIconDrag(el: HTMLElement, container: HTMLElement) {
let offsetX = 0
let offsetY = 0
let containerRect = container.getBoundingClientRect()
el.addEventListener('mousedown', (e) => {
el.classList.add('dragging')
let rect = el.getBoundingClientRect()
console.log(rect)
offsetX = e.clientX - rect.left
offsetY = e.clientY - rect.top
// 临时脱离 grid用绝对定位移动
el.style.position = "absolute";
el.style.left = rect.left - containerRect.left + "px";
el.style.top = rect.top - containerRect.top + "px";
el.style.gridRow = "auto";
el.style.gridColumn = "auto";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
})
function onMouseMove(e: MouseEvent) {
if (!el) return;
el.style.left = e.clientX - containerRect.left - offsetX + "px";
el.style.top = e.clientY - containerRect.top - offsetY + "px";
}
function onMouseUp(e: MouseEvent) {
if (!el) return;
const cellWidth = 90 + 16; // 图标宽度 + gap
const cellHeight = 110 + 16;
// 计算所在行列
let col = Math.round((e.clientX - containerRect.left) / cellWidth) + 1;
let row = Math.round((e.clientY - containerRect.top) / cellHeight) + 1;
// 限制在 grid 内
const maxCols = Math.floor(containerRect.width / cellWidth);
const maxRows = Math.floor(containerRect.height / cellHeight);
col = Math.max(1, Math.min(maxCols, col));
row = Math.max(1, Math.min(maxRows, row));
console.log(col, row)
// 放回 grid
el.style.position = "relative";
el.style.left = "";
el.style.top = "";
el.style.gridRow = `${row}`;
el.style.gridColumn = `${col}`;
el.classList.remove("dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
}

View File

@@ -0,0 +1,35 @@
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
export const eventManager = new EventBuilderImpl<IAllEvent>()
/**
* 系统进程的事件
* @description
* <p>onAuthChange - 认证状态改变</p>
* <p>onThemeChange - 主题改变</p>
*/
export interface IBasicSystemEvent extends IEventMap {
/** 认证状态改变 */
onAuthChange: () => {},
/** 主题改变 */
onThemeChange: (theme: string) => void
}
/**
* 桌面进程的事件
* @description
* <p>onDesktopRootDomResize - 桌面根dom尺寸改变</p>
* <p>onDesktopProcessInitialize - 桌面进程初始化完成</p>
*/
export interface IDesktopEvent extends IEventMap {
/** 桌面根dom尺寸改变 */
onDesktopRootDomResize: (width: number, height: number) => void
/** 桌面进程初始化完成 */
onDesktopProcessInitialize: () => void
/** 桌面应用图标位置改变 */
onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void
}
export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {}

View File

@@ -0,0 +1,47 @@
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
}

View File

@@ -0,0 +1,61 @@
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>()

View File

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

View File

@@ -0,0 +1,28 @@
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;
}

View File

@@ -0,0 +1,26 @@
import type { IVersion } from '@/core/common/types/IVersion.ts'
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
/**
* 进程的描述信息
*/
export interface IProcessInfo {
/** 进程名称 - 唯一 */
get name(): string;
/** 进程标题 */
get title(): string;
/** 进程描述 */
get description(): string;
/** 进程图标 */
get icon(): string;
/** 启动窗体名称 */
get startName(): string;
/** 进程版本 */
get version(): IVersion;
/** 是否单例进程 */
get singleton(): boolean;
/** 是否仅进程 */
get isJustProcess(): boolean;
/** 进程的窗体配置 */
get windowFormConfigs(): IWindowFormConfig[];
}

View File

@@ -0,0 +1,41 @@
import type { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
/**
* 进程管理
*/
export interface IProcessManager {
/** 所有进程信息 */
get processInfos(): IProcessInfo[];
/**
* 注册进程
* @param process 进程
*/
registerProcess(process: IProcess): void;
/**
* 通过进程id查找进程
* @param id 进程id
*/
findProcessById(id: string): IProcess | undefined;
/**
* 通过进程名查找进程
* @param name 进程名
*/
findProcessByName<T extends IProcess = IProcess>(name: string): T | undefined;
/**
* 通过进程id删除进程
* @param id 进程id
*/
removeProcess(id: string): void;
/**
* 通过进程对象删除进程
* @param process 进程对象
*/
removeProcess(process: IProcess): void;
/**
* 通过进程名查找进程信息
* @param name 进程名
*/
findProcessInfoByName(name: string): IProcessInfo | undefined;
}

View File

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

View File

@@ -0,0 +1,83 @@
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()
}
}

View File

@@ -0,0 +1,101 @@
import type { IVersion } from '../../common/types/IVersion.ts'
import type { IAppProcessInfoParams } from '../types/IAppProcessInfoParams.ts'
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
export class ProcessInfoImpl implements IProcessInfo {
/**
* 应用进程名称
* @private
*/
private readonly _name: string;
/**
* 应用进程标题
* @private
*/
private readonly _title: string;
/**
* 应用进程描述
* @private
*/
private readonly _description: string;
/**
* 应用进程图标
* @private
*/
private readonly _icon: string;
/**
* 应用进程启动入口
* 对应windowFrom参数name
* @private
*/
private readonly _startName: string;
/**
* 应用版本信息
* @private
*/
private readonly _version: IVersion;
/**
* 应用是否只存在一个进程
* @private
*/
private readonly _singleton: boolean;
/**
* 是否只是一个进程
* @private
*/
private readonly _isJustProcess: boolean;
/**
* 进程所有的窗口配置信息
* @private
*/
private readonly _windowFormConfigs: Array<IWindowFormConfig>;
constructor(info: IAppProcessInfoParams) {
this._name = info.name;
this._title = info.title || '';
this._description = info.description || '';
this._icon = <string> info.icon;
this._startName = info.startName || '';
this._version = info.version || { company: 'XZG', major: 1, minor: 0, build: 0, private: 0 };
this._singleton = info.singleton;
this._isJustProcess = info.isJustProcess;
this._windowFormConfigs = info.windowFormConfigs || [];
}
public get name() {
return this._name;
}
public get title() {
return this._title;
}
public get description() {
return this._description;
}
public get icon() {
return this._icon;
}
public get startName() {
return this._startName;
}
public get version() {
return this._version;
}
public get singleton() {
return this._singleton;
}
public get isJustProcess() {
return this._isJustProcess;
}
public get windowFormConfigs() {
return this._windowFormConfigs;
}
}

View File

@@ -0,0 +1,107 @@
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);
}
}

View File

@@ -0,0 +1,26 @@
import type { IVersion } from '../../common/types/IVersion.ts'
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
/**
* 应用进程入参信息
*/
export interface IAppProcessInfoParams {
/** 应用进程名称 */
name: string;
/** 应用进程标题 */
title?: string;
/** 应用进程描述 */
description?: string;
/** 应用进程图标 */
icon?: string;
/** 应用进程启动入口 */
startName?: string;
/** 应用版本信息 */
version?: IVersion;
/** 应用是否只存在一个进程 */
singleton: boolean;
/** 是否只是一个进程, 没有UI */
isJustProcess: boolean;
/** 进程所有的窗口配置信息 */
windowFormConfigs?: IWindowFormConfig[];
}

View File

@@ -0,0 +1,24 @@
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
}

View File

@@ -0,0 +1,21 @@
import { serviceManager, type ServiceManager } from '@/core/service/kernel/ServiceManager.ts'
/**
* 服务基类 - 抽象类
*/
export abstract class AService {
private readonly _id: string;
private _sm: ServiceManager = serviceManager;
get id() {
return this._id;
}
get sm() {
return this._sm;
}
protected constructor(id: string) {
this._id = id;
this._sm.registerService(this);
}
}

View File

@@ -0,0 +1,41 @@
import type { AService } from '@/core/service/kernel/AService.ts'
/**
* 服务管理
*/
export class ServiceManager {
private _services: Map<string, AService> = new Map<string, AService>();
get services(): Map<string, AService> {
return this._services
}
/**
* 注册服务
* @param service
*/
registerService(service: AService): void {
if (this._services.has(service.id)) {
throw new Error(`服务 ${service.id} 已存在`)
}
this._services.set(service.id, service)
}
/**
* 通过id获取服务
* @param id
*/
getService<T extends AService>(id: string): T | undefined {
return this._services.get(id) as T | undefined
}
/**
* 广播
* @param event
* @param data
*/
broadcast(event: string, data?: any): void {
}
}
export const serviceManager = new ServiceManager()

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import { AService } from '@/core/service/kernel/AService.ts'
import type { IObservable } from '@/core/state/IObservable.ts'
interface IUserInfo {
id: string;
name: string;
token: string;
}
export class UserService extends AService {
private _userInfo: IObservable<IUserInfo>;
get userInfo() {
return this._userInfo;
}
constructor() {
super("UserService");
console.log("UserService - 服务注册")
}
}

View File

@@ -0,0 +1,64 @@
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);
}
}

View File

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

View File

@@ -0,0 +1,305 @@
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 }
}
})
}
}

View File

@@ -0,0 +1,297 @@
import type {
IObservable,
TNonFunctionProperties,
TObservableKeyListener,
TObservableListener,
TObservableState,
} from '@/core/state/IObservable.ts'
/**
* 创建一个可观察对象,用于管理状态和事件。
* WeakRef 和垃圾回收功能
* @template T - 需要处理的状态类型
* @example
* interface AppState {
* count: number
* user: {
* name: string
* age: number
* }
* items: number[]
* }
*
* // 创建 ObservableImpl
* const obs = new ObservableImpl<AppState>({
* count: 0,
* user: { name: 'Alice', age: 20 },
* items: []
* })
*
* // 1⃣ 全量订阅
* const unsubscribeAll = obs.subscribe(state => {
* console.log('全量订阅', state)
* }, { immediate: true })
*
* // 2⃣ 单字段订阅
* const unsubscribeCount = obs.subscribeKey('count', ({ count }) => {
* console.log('count 字段变化:', count)
* })
*
* // 3⃣ 多字段订阅
* const unsubscribeUser = obs.subscribeKey(['user', 'count'], ({ user, count }) => {
* console.log('user 或 count 变化:', { user, count })
* })
*
* // 4⃣ 修改属性
* obs.state.count = 1 // ✅ 会触发 count 和全量订阅
* obs.state.user.age = 21 // ✅ 深层对象修改触发 user 订阅
* obs.state.user.name = 'Bob'
* // 语法糖:解构赋值直接赋值触发通知
* const { count, user, items } = obs.toRefsProxy()
* count = 1 // 触发 Proxy set
* user.age = 18 // 深层对象 Proxy 支持
* items.push(42) // 数组方法拦截触发通知
*
* // 5⃣ 数组方法触发
* obs.state.items.push(10) // ✅ push 会触发 items 的字段订阅
* obs.state.items.splice(0, 1)
*
* // 6⃣ 批量修改(同一事件循环只触发一次通知)
* obs.patch({
* count: 2,
* user: { name: 'Charlie', age: 30 }
* })
*
* // 7⃣ 解构赋值访问对象属性仍然触发订阅
* const { state } = obs
* state.user.age = 31 // ✅ 会触发 user 订阅
*
* // 8⃣ 取消订阅
* unsubscribeAll()
* unsubscribeCount()
* unsubscribeUser()
*
* // 9⃣ 销毁 ObservableImpl
* obs.dispose()
*/
export class ObservableWeakRefImpl<T extends TNonFunctionProperties<T>> implements IObservable<T> {
/** ObservableImpl 的状态对象,深层 Proxy */
public readonly state: TObservableState<T>
/** 全量订阅列表 */
private listeners: Set<WeakRef<TObservableListener<T>> | TObservableListener<T>> = new Set()
/** 字段订阅列表 */
private keyListeners: Map<keyof T, Set<WeakRef<Function> | Function>> = new Map()
/** FinalizationRegistry 用于自动清理 WeakRef */
private registry?: FinalizationRegistry<WeakRef<Function>>
/** 待通知的字段 */
private pendingKeys: Set<keyof T> = new Set()
/** 通知调度状态 */
private notifyScheduled = false
/** 已销毁标记 */
private disposed = false
constructor(initialState: TNonFunctionProperties<T>) {
if (typeof WeakRef !== 'undefined' && typeof FinalizationRegistry !== 'undefined') {
this.registry = new FinalizationRegistry((ref: WeakRef<Function>) => {
this.listeners.delete(ref as unknown as TObservableListener<T>)
this.keyListeners.forEach(set => set.delete(ref))
})
}
// 创建深层响应式 Proxy
this.state = this.makeReactive(initialState) as TObservableState<T>
}
/** 创建响应式对象,深层递归 Proxy + 数组方法拦截 */
private makeReactive(obj: TNonFunctionProperties<T>): TObservableState<T> {
const handler: ProxyHandler<any> = {
get: (target, prop: string | symbol, receiver) => {
const key = prop as keyof T // 类型断言
const value = Reflect.get(target, key, receiver)
if (Array.isArray(value)) return this.wrapArray(value, key)
if (typeof value === 'object' && value !== null) return this.makeReactive(value)
return value
},
set: (target, prop: string | symbol, value, receiver) => {
const key = prop as keyof T // 类型断言
const oldValue = target[key]
if (oldValue !== value) {
target[key] = value
this.pendingKeys.add(key)
this.scheduleNotify()
}
return true
},
}
return new Proxy(obj, handler) as TObservableState<T>
}
/** 包装数组方法,使 push/pop/splice 等触发通知 */
private wrapArray(arr: any[], parentKey: keyof T): any {
const self = this
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] as const
arrayMethods.forEach(method => {
const original = arr[method]
Object.defineProperty(arr, method, {
value: function (...args: any[]) {
const result = original.apply(this, args)
self.pendingKeys.add(parentKey)
self.scheduleNotify()
return result
},
writable: true,
configurable: true,
})
})
return arr
}
/** 调度异步通知 */
private scheduleNotify(): void {
if (!this.notifyScheduled && !this.disposed) {
this.notifyScheduled = true
Promise.resolve().then(() => this.flushNotify())
}
}
/** 执行通知逻辑 */
private flushNotify(): void {
if (this.disposed) return
const keys = Array.from(this.pendingKeys)
this.pendingKeys.clear()
this.notifyScheduled = false
// 全量订阅
for (const ref of this.listeners) {
const fn = this.deref(ref)
if (fn) fn(this.state)
else this.listeners.delete(ref as TObservableListener<T>)
}
// 字段订阅
const fnMap = new Map<Function, (keyof T)[]>()
for (const key of keys) {
const set = this.keyListeners.get(key)
if (!set) continue
for (const ref of set) {
const fn = this.deref(ref)
if (!fn) {
set.delete(ref)
continue
}
if (!fnMap.has(fn)) fnMap.set(fn, [])
fnMap.get(fn)!.push(key)
}
}
fnMap.forEach((subKeys, fn) => {
const result = {} as Pick<T, typeof subKeys[number]>
subKeys.forEach(k => (result[k] = this.state[k]))
fn(result)
})
}
/** 全量订阅 */
subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
const ref = this.makeRef(fn)
this.listeners.add(ref)
this.registry?.register(fn, ref as WeakRef<Function>)
if (options.immediate) fn(this.state)
return () => {
this.listeners.delete(ref)
this.registry?.unregister(fn)
}
}
/** 字段订阅 */
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {}
): () => void {
const keyArray = Array.isArray(keys) ? keys : [keys]
const refs: (WeakRef<Function> | Function)[] = []
for (const key of keyArray) {
if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set())
const ref = this.makeRef(fn)
this.keyListeners.get(key)!.add(ref)
this.registry?.register(fn as unknown as Function, ref as WeakRef<Function>)
refs.push(ref)
}
if (options.immediate) {
const result = {} as Pick<T, K>
keyArray.forEach(k => (result[k] = this.state[k]))
fn(result)
}
return () => {
for (let i = 0; i < keyArray.length; i++) {
const set = this.keyListeners.get(keyArray[i])
if (set) set.delete(refs[i])
}
this.registry?.unregister(fn as unknown as Function)
}
}
/** 批量更新 */
patch(values: Partial<T>): void {
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const typedKey = key as keyof T
this.state[typedKey] = values[typedKey]!
this.pendingKeys.add(typedKey)
}
}
this.scheduleNotify()
}
/** 销毁 ObservableImpl */
dispose(): void {
this.disposed = true
this.listeners.clear()
this.keyListeners.clear()
this.pendingKeys.clear()
}
/** 语法糖:解构赋值直接赋值触发通知 */
toRefsProxy(): { [K in keyof T]: T[K] } {
const self = this
return new Proxy({} as T, {
get(_, prop: string | symbol) {
const key = prop as keyof T
return self.state[key]
},
set(_, prop: string | symbol, value) {
const key = prop as keyof T
self.state[key] = value
return true
},
ownKeys() {
return Reflect.ownKeys(self.state)
},
getOwnPropertyDescriptor(_, prop: string | symbol) {
return { enumerable: true, configurable: true }
}
})
}
/** WeakRef 创建 */
private makeRef<F extends Function>(fn: F): WeakRef<F> | F {
return typeof WeakRef !== 'undefined' ? new WeakRef(fn) : fn
}
/** WeakRef 解引用 */
private deref<F extends Function>(ref: WeakRef<F> | F): F | undefined {
return typeof WeakRef !== 'undefined' && ref instanceof WeakRef ? ref.deref() : (ref as F)
}
}

View File

@@ -0,0 +1,14 @@
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
})

View File

@@ -0,0 +1,19 @@
import ProcessImpl from '../process/impl/ProcessImpl.ts'
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
/**
* 基础系统进程
*/
export class BasicSystemProcess extends ProcessImpl {
private _isMounted: boolean = false;
public get isMounted() {
return this._isMounted;
}
constructor(info: IProcessInfo) {
super(info)
console.log('BasicSystemProcess')
}
}

View File

@@ -0,0 +1,18 @@
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
/**
* 基础系统进程信息
*/
export const BasicSystemProcessInfo = new ProcessInfoImpl({
name: 'basic-system',
title: '基础系统进程',
isJustProcess: true,
version: {
company: 'XZG',
major: 1,
minor: 0,
build: 0,
private: 0
},
singleton: true
});

View File

@@ -0,0 +1,752 @@
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) {}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
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;
}

View File

@@ -0,0 +1,114 @@
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() {}
}

View File

@@ -0,0 +1,60 @@
/**
* 窗体配置信息
*/
export interface IWindowFormConfig {
/**
* 窗体名称
*/
name: string;
/**
* 窗体标题
*/
title?: string;
/**
* 窗体图标
*/
icon?: string;
top?: number;
left?: number;
/**
* 窗体宽度
*/
width?: number;
widthAuto?: boolean;
/**
* 窗体高度
*/
height?: number;
heightAuto?: boolean;
/**
* 窗体最小宽度
*/
minWidth?: number;
/**
* 窗体最小高度
*/
minHeight?: number;
/**
* 窗体最大宽度
*/
maxWidth?: number;
/**
* 窗体最大高度
*/
maxHeight?: number;
/**
* 窗体透明度
*/
opacity?: number;
windowStyle?: string;
windowState?: number;
resizeMode?: number;
topMost?: boolean;
/**
* 是否显示在任务栏
*/
showInTaskbar?: boolean;
showTitleBarIcon?: boolean;
showTitleBarText?: boolean;
hideTitleBar?: boolean;
}

View File

@@ -0,0 +1,10 @@
/**
* 窗体位置坐标 - 左上角
*/
export interface WindowFormPos {
x: number;
y: number;
}
/** 窗口状态 */
export type TWindowFormState = 'default' | 'minimized' | 'maximized';

View File

@@ -1,8 +1,10 @@
import { LitElement, html, css, unsafeCSS } from 'lit' import { LitElement, html, css, unsafeCSS } from 'lit'
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import wfStyle from './css/wf.scss?inline' import wfStyle from './css/wf.scss?inline'
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
type TWindowFormState = 'default' | 'minimized' | 'maximized'; import { wfem } from '@/core/events/WindowFormEventManager.ts'
import type { IObservable } from '@/core/state/IObservable.ts'
import type { IWindowFormDataState } from '@/core/window/impl/WindowFormImpl.ts'
/** 拖拽移动开始的回调 */ /** 拖拽移动开始的回调 */
type TDragStartCallback = (x: number, y: number) => void; type TDragStartCallback = (x: number, y: number) => void;
@@ -42,13 +44,12 @@ interface IElementRect {
width: number; width: number;
/** 高度 */ /** 高度 */
height: number; height: number;
/** x坐标 */ /** 顶点坐标(相对 offsetParent */
x: number; top: number;
/** y坐标 */ /** 左点坐标(相对 offsetParent */
y: number; left: number;
} }
/** 窗口自定义事件 */
export interface WindowFormEventMap extends HTMLElementEventMap { export interface WindowFormEventMap extends HTMLElementEventMap {
'windowForm:dragStart': CustomEvent<TDragStartCallback>; 'windowForm:dragStart': CustomEvent<TDragStartCallback>;
'windowForm:dragMove': CustomEvent<TDragMoveCallback>; 'windowForm:dragMove': CustomEvent<TDragMoveCallback>;
@@ -61,7 +62,6 @@ export interface WindowFormEventMap extends HTMLElementEventMap {
'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>; 'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>; 'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>;
'windowForm:close': CustomEvent; 'windowForm:close': CustomEvent;
'windowForm:minimize': CustomEvent;
} }
@customElement('window-form-element') @customElement('window-form-element')
@@ -75,50 +75,38 @@ export class WindowFormElement extends LitElement {
@property({ type: Boolean }) closable = true @property({ type: Boolean }) closable = true
@property({ type: Boolean, reflect: true }) focused: boolean = true @property({ type: Boolean, reflect: true }) focused: boolean = true
@property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default' @property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default'
@property({ type: Object }) dragContainer?: HTMLElement // 元素的父容器 @property({ type: Object }) dragContainer?: HTMLElement
@property({ type: Boolean }) allowOverflow = true // 允许窗口超出容器
@property({ type: Number }) snapDistance = 0 // 吸附距离 @property({ type: Number }) snapDistance = 0 // 吸附距离
@property({ type: Boolean }) snapAnimation = false // 吸附动画 @property({ type: Boolean }) snapAnimation = true // 吸附动画
@property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms @property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms
@property({ type: Number }) maxWidth?: number = Infinity @property({ type: Number }) maxWidth?: number = Infinity
@property({ type: Number }) minWidth?: number = 200 @property({ type: Number }) minWidth?: number = 0
@property({ type: Number }) maxHeight?: number = Infinity @property({ type: Number }) maxHeight?: number = Infinity
@property({ type: Number }) minHeight?: number = 200 @property({ type: Number }) minHeight?: number = 0
@property({ type: String }) taskbarElementId?: string @property({ type: String }) taskbarElementId?: string
@property({ type: Object }) wfData: any; @property({ type: Object }) wfData: IObservable<IWindowFormDataState>;
private _listeners: Array<{ type: string; original: Function; wrapped: EventListener }> = [] private _listeners: Array<{ type: string; original: Function; wrapped: EventListener }> = []
// ==== 拖拽/缩放状态(内部变量,不触发渲染) ==== // ==== 拖拽/缩放状态(内部变量,不触发渲染) ====
// 自身的x坐标
private x = 0
// 自身的y坐标
private y = 0
// 自身的宽度
private width = 640
// 自身的高度
private height = 360
// 记录拖拽开始时自身x坐标
private originalX = 0
// 记录拖拽开始时自身y坐标
private originalY = 0
// 鼠标开始拖拽时自身宽度
private originalWidth = 640
// 鼠标开始拖拽时高度
private originalHeight = 360
// 鼠标开始拖拽时x坐标
private pointStartX = 0
// 鼠标开始拖拽时x坐标
private pointStartY = 0
private animationFrame?: number
// 是否拖拽状态
private dragging = false private dragging = false
// 是否缩放状态
private resizing = false
// 缩放方向
private resizeDir: TResizeDirection | null = null private resizeDir: TResizeDirection | null = null
private startX = 0
private startY = 0
private startWidth = 0
private startHeight = 0
private startX_host = 0
private startY_host = 0
private x = 0
private y = 0
private preX = 0
private preY = 0
private width = 640
private height = 360
private animationFrame?: number
private resizing = false
// private get x() { // private get x() {
// return this.wfData.state.x // return this.wfData.state.x
@@ -210,28 +198,24 @@ export class WindowFormElement extends LitElement {
} }
override firstUpdated() { override firstUpdated() {
const { width, height } = this.getBoundingClientRect() wfem.addEventListener('windowFormFocus', this.windowFormFocusFun)
this.width = width || this.width window.addEventListener('pointerup', this.onPointerUp)
this.height = height || this.height window.addEventListener('pointermove', this.onPointerMove)
this.addEventListener('pointerdown', this.toggleFocus)
const container = this.dragContainer || document.body const container = this.dragContainer || document.body
const containerRect = container.getBoundingClientRect() const containerRect = container.getBoundingClientRect()
this.x = containerRect.width / 2 - this.width / 2 this.x = containerRect.width / 2 - this.width / 2
this.y = containerRect.height / 2 - this.height / 2 this.y = containerRect.height / 2 - this.height / 2
this.style.width = `${this.width}px` this.style.width = `${this.width}px`
this.style.height = `${this.height}px` this.style.height = `${this.height}px`
this.style.transform = `translate(${this.x}px, ${this.y}px)` this.style.transform = `translate(${this.x}px, ${this.y}px)`
window.addEventListener('pointerup', this.onPointerUp)
window.addEventListener('pointermove', this.onPointerMove)
this.addEventListener('pointerdown', this.toggleFocus)
this.targetBounds = { this.targetBounds = {
width: this.width, width: this.offsetWidth,
height: this.height, height: this.offsetHeight,
x: this.x, top: this.x,
y: this.y, left: this.y,
} }
} }
@@ -240,7 +224,7 @@ export class WindowFormElement extends LitElement {
window.removeEventListener('pointerup', this.onPointerUp) window.removeEventListener('pointerup', this.onPointerUp)
window.removeEventListener('pointermove', this.onPointerMove) window.removeEventListener('pointermove', this.onPointerMove)
this.removeEventListener('pointerdown', this.toggleFocus) this.removeEventListener('pointerdown', this.toggleFocus)
// wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun) wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun)
this.removeAllManagedListeners() this.removeAllManagedListeners()
} }
@@ -254,7 +238,7 @@ export class WindowFormElement extends LitElement {
private toggleFocus = () => { private toggleFocus = () => {
this.focused = !this.focused this.focused = !this.focused
// wfem.notifyEvent('windowFormFocus', this.wid) wfem.notifyEvent('windowFormFocus', this.wid)
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('update:focused', { new CustomEvent('update:focused', {
detail: this.focused, detail: this.focused,
@@ -272,10 +256,10 @@ export class WindowFormElement extends LitElement {
e.preventDefault() e.preventDefault()
this.dragging = true this.dragging = true
this.pointStartX = e.clientX this.startX = e.clientX
this.pointStartY = e.clientY this.startY = e.clientY
this.originalX = this.x this.preX = this.x
this.originalY = this.y this.preY = this.y
this.setPointerCapture?.(e.pointerId) this.setPointerCapture?.(e.pointerId)
this.dispatchEvent( this.dispatchEvent(
@@ -287,20 +271,15 @@ export class WindowFormElement extends LitElement {
) )
} }
/**
*
* @param e
*/
private onPointerMove = (e: PointerEvent) => { private onPointerMove = (e: PointerEvent) => {
if (this.dragging) { if (this.dragging) {
const dx = e.clientX - this.pointStartX const dx = e.clientX - this.startX
const dy = e.clientY - this.pointStartY const dy = e.clientY - this.startY
const x = this.originalX + dx const x = this.preX + dx
const y = this.originalY + dy const y = this.preY + dy
const pos = this.applyBoundary(x, y, e.clientX, e.clientY) this.applyPosition(x, y, false)
this.applyPosition(pos.x, pos.y)
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:dragMove', { new CustomEvent('windowForm:dragMove', {
detail: { x, y }, detail: { x, y },
@@ -313,10 +292,6 @@ export class WindowFormElement extends LitElement {
} }
} }
/**
*
* @param e
*/
private onPointerUp = (e: PointerEvent) => { private onPointerUp = (e: PointerEvent) => {
if (this.dragging) { if (this.dragging) {
this.dragUp(e) this.dragUp(e)
@@ -344,14 +319,13 @@ export class WindowFormElement extends LitElement {
} }
/** /**
* *
* @param x x * @param x x
* @param y y * @param y y
* @returns {x: number, y: number}
*/ */
private calculateSnapping(x: number, y: number): { x: number, y: number} { private applySnapping(x: number, y: number) {
let snappedX = x let snappedX = x,
let snappedY = y snappedY = y
const containerSnap = this.getSnapPoints() const containerSnap = this.getSnapPoints()
if (this.snapDistance > 0) { if (this.snapDistance > 0) {
for (const sx of containerSnap.x) for (const sx of containerSnap.x)
@@ -368,28 +342,22 @@ export class WindowFormElement extends LitElement {
return { x: snappedX, y: snappedY } return { x: snappedX, y: snappedY }
} }
/**
*
* @param e
* @private
*/
private dragUp(e: PointerEvent) { private dragUp(e: PointerEvent) {
const snapped = this.calculateSnapping(this.x, this.y) const snapped = this.applySnapping(this.x, this.y)
if (this.snapAnimation) { if (this.snapAnimation) {
this.animateTo(this.x, this.y, snapped.x, snapped.y, this.snapAnimationDuration, this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
(x, y) => { this.updateTargetBounds(snapped.x, snapped.y)
this.applyPosition(x, y) this.dispatchEvent(
}, new CustomEvent('windowForm:dragEnd', {
(x, y) => { detail: { x: snapped.x, y: snapped.y },
this.applyPosition(snapped.x, snapped.y) bubbles: true,
this.dispatchEvent(new CustomEvent('windowForm:dragEnd', { composed: true,
detail: { x: snapped.x, y: snapped.y }, }),
bubbles: true, )
composed: true,
}))
}) })
} else { } else {
this.applyPosition(snapped.x, snapped.y) this.applyPosition(snapped.x, snapped.y, true)
this.updateTargetBounds(snapped.x, snapped.y)
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:dragEnd', { new CustomEvent('windowForm:dragEnd', {
detail: { x: snapped.x, y: snapped.y }, detail: { x: snapped.x, y: snapped.y },
@@ -398,68 +366,31 @@ export class WindowFormElement extends LitElement {
}), }),
) )
} }
this.updateTargetBounds(this.x, this.y, this.width, this.height)
} }
/** private applyPosition(x: number, y: number, isFinal: boolean) {
*
* @param x x
* @param y y
* @param pointerX X
* @param pointerY Y
* @returns { x, y }
*/
private applyBoundary(x: number, y: number, pointerX: number, pointerY: number): { x: number; y: number } {
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
// 限制指针在容器内
const limitedPointerX = Math.min(Math.max(pointerX, containerRect.left), containerRect.right)
const limitedPointerY = Math.min(Math.max(pointerY, containerRect.top), containerRect.bottom)
// 计算指针被限制后的偏移量
const dx = limitedPointerX - pointerX
const dy = limitedPointerY - pointerY
// 根据指针偏移调整窗口位置
x += dx
y += dy
return { x, y }
}
/**
*
* @param x x
* @param y y
* @private
*/
private applyPosition(x: number, y: number) {
this.x = x this.x = x
this.y = y this.y = y
this.style.transform = `translate(${x}px, ${y}px)` this.style.transform = `translate(${x}px, ${y}px)`
if (isFinal) this.applyBoundary()
} }
/** private applyBoundary() {
* if (this.allowOverflow) return
* @param startX x let { x, y } = { x: this.x, y: this.y }
* @param startY y
* @param targetX x const rect = this.getBoundingClientRect()
* @param targetY y const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
* @param duration x = Math.min(Math.max(x, 0), containerRect.width - rect.width)
* @param onMove y = Math.min(Math.max(y, 0), containerRect.height - rect.height)
* @param onComplete
* @private this.applyPosition(x, y, false)
*/ }
private animateTo(
startX: number, private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
startY: number,
targetX: number,
targetY: number,
duration: number,
onMove?: (x: number, y: number) => void,
onComplete?: (x: number, y: number) => void
) {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame) if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
const startX = this.x
const startY = this.y
const deltaX = targetX - startX const deltaX = targetX - startX
const deltaY = targetY - startY const deltaY = targetY - startY
const startTime = performance.now() const startTime = performance.now()
@@ -472,7 +403,7 @@ export class WindowFormElement extends LitElement {
const x = startX + deltaX * ease const x = startX + deltaX * ease
const y = startY + deltaY * ease const y = startY + deltaY * ease
onMove?.(x, y) this.applyPosition(x, y, false)
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:dragMove', { new CustomEvent('windowForm:dragMove', {
detail: { x, y }, detail: { x, y },
@@ -484,7 +415,8 @@ export class WindowFormElement extends LitElement {
if (progress < 1) { if (progress < 1) {
this.animationFrame = requestAnimationFrame(step) this.animationFrame = requestAnimationFrame(step)
} else { } else {
onComplete?.(targetX, targetY) this.applyPosition(targetX, targetY, true)
onComplete?.()
} }
} }
this.animationFrame = requestAnimationFrame(step) this.animationFrame = requestAnimationFrame(step)
@@ -499,12 +431,14 @@ export class WindowFormElement extends LitElement {
e.stopPropagation() e.stopPropagation()
this.resizing = true this.resizing = true
this.resizeDir = dir this.resizeDir = dir
this.pointStartX = e.clientX this.startX = e.clientX
this.pointStartY = e.clientY this.startY = e.clientY
this.originalX = this.x
this.originalY = this.y const rect = this.getBoundingClientRect()
this.originalWidth = this.width this.startWidth = rect.width
this.originalHeight = this.height this.startHeight = rect.height
this.startX_host = rect.left
this.startY_host = rect.top
const target = e.target as HTMLElement const target = e.target as HTMLElement
document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor
@@ -519,72 +453,59 @@ export class WindowFormElement extends LitElement {
) )
} }
/**
*
* @param e
* @private
*/
private performResize(e: PointerEvent) { private performResize(e: PointerEvent) {
if (!this.resizeDir || !this.resizing) return if (!this.resizeDir || !this.resizing) return
let newWidth = this.originalWidth let newWidth = this.startWidth
let newHeight = this.originalHeight let newHeight = this.startHeight
let newX = this.originalX let newX = this.startX_host
let newY = this.originalY let newY = this.startY_host
const dx = e.clientX - this.pointStartX const dx = e.clientX - this.startX
const dy = e.clientY - this.pointStartY const dy = e.clientY - this.startY
// ====== 根据方向计算临时尺寸与位置 ======
switch (this.resizeDir) { switch (this.resizeDir) {
case 'r': // 右侧 case 'r':
newWidth += dx newWidth += dx
break break
case 'b': // 下方 case 'b':
newHeight += dy newHeight += dy
break break
case 'l': // 左侧 case 'l':
newWidth -= dx newWidth -= dx
newX += dx newX += dx
break break
case 't': // 上方 case 't':
newHeight -= dy newHeight -= dy
newY += dy newY += dy
break break
case 'tl': // 左上角 case 'tl':
newWidth -= dx newWidth -= dx
newX += dx newX += dx
newHeight -= dy newHeight -= dy
newY += dy newY += dy
break break
case 'tr': // 右上角 case 'tr':
newWidth += dx newWidth += dx
newHeight -= dy newHeight -= dy
newY += dy newY += dy
break break
case 'bl': // 左下角 case 'bl':
newWidth -= dx newWidth -= dx
newX += dx newX += dx
newHeight += dy newHeight += dy
break break
case 'br': // 右下角 case 'br':
newWidth += dx newWidth += dx
newHeight += dy newHeight += dy
break break
} }
const { x, y, width, height } = this.applyResizeBounds(newX, newY, newWidth, newHeight, this.resizeDir) const d = this.applyResizeBounds(newX, newY, newWidth, newHeight)
this.x = x
this.y = y
this.width = width
this.height = height
this.style.width = `${this.width}px`
this.style.height = `${this.height}px`
this.style.transform = `translate(${this.x}px, ${this.y}px)`
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:resizeMove', { new CustomEvent('windowForm:resizeMove', {
detail: { dir: this.resizeDir, width: width, height: height, left: x, top: y }, detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top },
bubbles: true, bubbles: true,
composed: true, composed: true,
}), }),
@@ -592,107 +513,68 @@ export class WindowFormElement extends LitElement {
} }
/** /**
* *
* @param x x * @param newX X坐
* @param y y * @param newY Y坐
* @param width * @param newWidth
* @param height * @param newHeight
* @private * @private
* @returns { x: number, y: number, width: number, height: number }
*/ */
private applyResizeBounds( private applyResizeBounds(
x: number, newX: number,
y: number, newY: number,
width: number, newWidth: number,
height: number, newHeight: number,
resizeDir: TResizeDirection
): { ): {
x: number left: number
y: number top: number
width: number width: number
height: number height: number
} { } {
const { minWidth = 100, minHeight = 100, maxWidth = Infinity, maxHeight = Infinity } = this // 最小/最大宽高限制
if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth)
if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth)
if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight)
if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight)
//#region 限制最大/最小尺寸 // 边界限制
// 限制宽度 if (this.allowOverflow) {
if (width < minWidth) { this.x = newX
// 左缩时要修正X坐标否则会跳动 this.y = newY
if (resizeDir.includes('l')) x -= minWidth - width this.width = newWidth
width = minWidth this.height = newHeight
} else if (width > maxWidth) { this.style.width = `${newWidth}px`
if (resizeDir.includes('l')) x += width - maxWidth this.style.height = `${newHeight}px`
width = maxWidth this.applyPosition(newX, newY, false)
return {
left: newX,
top: newY,
width: newWidth,
height: newHeight,
}
} }
// 限制高度
if (height < minHeight) {
if (resizeDir.includes('t')) y -= minHeight - height
height = minHeight
} else if (height > maxHeight) {
if (resizeDir.includes('t')) y += height - maxHeight
height = maxHeight
}
//#endregion
//#region 限制在容器边界内
const containerRect = (this.dragContainer || document.body).getBoundingClientRect() const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
const maxLeft = containerRect.width - width newX = Math.min(Math.max(0, newX), containerRect.width - newWidth)
const maxTop = containerRect.height - height newY = Math.min(Math.max(0, newY), containerRect.height - newHeight)
// 左越界(从左侧缩放时) this.x = newX
if (x < 0) { this.y = newY
if (resizeDir.includes('l')) { this.width = newWidth
// 如果是往左拉出容器,锁定边界 this.height = newHeight
width += x // 因为x是负数相当于减小宽度 this.style.width = `${newWidth}px`
} this.style.height = `${newHeight}px`
x = 0 this.applyPosition(newX, newY, false)
}
// 顶部越界(从上侧缩放时)
if (y < 0) {
if (resizeDir.includes('t')) {
height += y // y是负数相当于减小高度
}
y = 0
}
// 右越界(从右侧缩放时)
if (x + width > containerRect.width) {
if (resizeDir.includes('r')) {
width = containerRect.width - x
} else {
x = Math.min(x, maxLeft)
}
}
// 底部越界(从下侧缩放时)
if (y + height > containerRect.height) {
if (resizeDir.includes('b')) {
height = containerRect.height - y
} else {
y = Math.min(y, maxTop)
}
}
//#endregion
// 二次防护:确保不小于最小值
width = Math.max(width, minWidth)
height = Math.max(height, minHeight)
return { return {
x, left: newX,
y, top: newY,
width, width: newWidth,
height height: newHeight,
} }
} }
/**
*
* @param e
* @private
*/
private resizeUp(e: PointerEvent) { private resizeUp(e: PointerEvent) {
if (!this.resizable) return if (!this.resizable) return
@@ -734,25 +616,21 @@ export class WindowFormElement extends LitElement {
startY, startY,
startW, startW,
startH, startH,
rect.x, rect.left,
rect.y, rect.top,
rect.width, rect.width,
rect.height, rect.height,
400, 400,
(x, y, w, h) => { () => {
this.applyWindowStyle(x, y, w, h)
},
(x, y, w, h) => {
this.applyWindowStyle(x, y, w, h)
this.style.display = 'none' this.style.display = 'none'
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:stateChange:minimize', { new CustomEvent('windowForm:stateChange:minimize', {
detail: { state: this.windowFormState }, detail: { state: this.windowFormState },
bubbles: true, bubbles: true,
composed: true, composed: true,
}) }),
) )
} },
) )
} }
@@ -788,11 +666,7 @@ export class WindowFormElement extends LitElement {
targetW, targetW,
targetH, targetH,
300, 300,
(x, y, w, h) => { () => {
this.applyWindowStyle(x, y, w, h)
},
(x, y, w, h) => {
this.applyWindowStyle(x, y, w, h)
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:stateChange:maximize', { new CustomEvent('windowForm:stateChange:maximize', {
detail: { state: this.windowFormState }, detail: { state: this.windowFormState },
@@ -838,16 +712,12 @@ export class WindowFormElement extends LitElement {
startY, startY,
startW, startW,
startH, startH,
b.x, b.left,
b.y, b.top,
b.width, b.width,
b.height, b.height,
300, 300,
(x, y, w, h) => { () => {
this.applyWindowStyle(x, y, w, h)
},
(x, y, w, h) => {
this.applyWindowStyle(x, y, w, h)
onComplete?.() onComplete?.()
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:stateChange:restore', { new CustomEvent('windowForm:stateChange:restore', {
@@ -860,24 +730,6 @@ export class WindowFormElement extends LitElement {
) )
} }
/**
*
* @param x
* @param y
* @param w
* @param h
* @private
*/
private applyWindowStyle(x: number, y: number, w: number, h: number) {
this.width = w
this.height = h
this.x = x
this.y = y
this.style.width = `${w}px`
this.style.height = `${h}px`
this.style.transform = `translate(${x}px, ${y}px)`
}
private windowFormClose() { private windowFormClose() {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:close', { new CustomEvent('windowForm:close', {
@@ -885,6 +737,7 @@ export class WindowFormElement extends LitElement {
composed: true, composed: true,
}), }),
) )
this.wfData.state.closed = true
} }
/** /**
@@ -911,8 +764,7 @@ export class WindowFormElement extends LitElement {
targetW: number, targetW: number,
targetH: number, targetH: number,
duration: number, duration: number,
onUpdating?: (x: number, y: number, w: number, h: number) => void, onComplete?: () => void,
onComplete?: (x: number, y: number, w: number, h: number) => void,
) { ) {
const startTime = performance.now() const startTime = performance.now()
const step = (now: number) => { const step = (now: number) => {
@@ -925,30 +777,33 @@ export class WindowFormElement extends LitElement {
const w = startW + (targetW - startW) * ease const w = startW + (targetW - startW) * ease
const h = startH + (targetH - startH) * ease const h = startH + (targetH - startH) * ease
onUpdating?.(x, y, w, h) this.style.width = `${w}px`
this.style.height = `${h}px`
this.applyPosition(x, y, false)
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(step) requestAnimationFrame(step)
} else { } else {
this.style.width = `${targetW}px` this.style.width = `${targetW}px`
this.style.height = `${targetH}px` this.style.height = `${targetH}px`
onComplete?.(x, y, w, h) this.applyPosition(targetX, targetY, true)
onComplete?.()
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('windowForm:stateChange', { new CustomEvent('windowForm:stateChange', {
detail: { state: this.windowFormState }, detail: { state: this.windowFormState },
bubbles: true, bubbles: true,
composed: true, composed: true,
}) }),
) )
} }
} }
requestAnimationFrame(step) requestAnimationFrame(step)
} }
private updateTargetBounds(x: number, y: number, width?: number, height?: number) { private updateTargetBounds(left: number, top: number, width?: number, height?: number) {
this.targetBounds = { this.targetBounds = {
x, left,
y, top,
width: width ?? this.offsetWidth, width: width ?? this.offsetWidth,
height: height ?? this.offsetHeight, height: height ?? this.offsetHeight,
} }

View File

@@ -0,0 +1,20 @@
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);
}

View File

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

View File

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

View File

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

View File

@@ -1,702 +0,0 @@
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
}

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