Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80c1863b9 | |||
| ff4791922e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,4 +28,3 @@ coverage
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.qoder/*
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": 如果可能,将对象压缩到单行
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 部署
|
|
||||||
|
|
||||||
构建产物可直接部署到任何静态文件服务器上。
|
|
||||||
35
README.md
35
README.md
@@ -1,37 +1,2 @@
|
|||||||
# vue-desktop
|
# vue-desktop
|
||||||
|
|
||||||
浏览器:Chrome 84+、Edge 84+、Firefox 79+、Safari 14+
|
|
||||||
|
|
||||||
Node.js:v14+
|
|
||||||
|
|
||||||
不支持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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
104
pnpm-lock.yaml
generated
@@ -38,10 +38,10 @@ importers:
|
|||||||
version: 22.17.1
|
version: 22.17.1
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.1(vite@7.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
|
||||||
|
|||||||
@@ -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与系统通信
|
|
||||||
- 受到严格的权限控制
|
|
||||||
@@ -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技术的应用
|
|
||||||
- 良好的用户体验设计
|
|
||||||
- 安全和性能的最佳实践
|
|
||||||
|
|
||||||
通过学习这个案例,开发者可以了解外置应用的完整开发流程,并以此为基础开发自己的应用。
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
|
||||||
@@ -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": ["音乐", "播放器", "媒体", "音频"]
|
|
||||||
}
|
|
||||||
@@ -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
18
src/App.vue
Normal 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>
|
||||||
@@ -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()
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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'
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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
34
src/core/XSystem.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/core/apps/department/app.json
Normal file
18
src/core/apps/department/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/department/main.vue
Normal file
9
src/core/apps/department/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
部门页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/fileManage/app.json
Normal file
18
src/core/apps/fileManage/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/fileManage/main.vue
Normal file
9
src/core/apps/fileManage/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
文件管理页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/music/app.json
Normal file
18
src/core/apps/music/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/music/main.vue
Normal file
9
src/core/apps/music/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
音乐页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/personalCenter/app.json
Normal file
18
src/core/apps/personalCenter/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/personalCenter/main.vue
Normal file
9
src/core/apps/personalCenter/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
个人中心页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/photograph/app.json
Normal file
18
src/core/apps/photograph/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/photograph/main.vue
Normal file
9
src/core/apps/photograph/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
照片页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/recycleBin/app.json
Normal file
18
src/core/apps/recycleBin/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/recycleBin/main.vue
Normal file
9
src/core/apps/recycleBin/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
回收站页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/setting/app.json
Normal file
18
src/core/apps/setting/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/setting/main.vue
Normal file
9
src/core/apps/setting/main.vue
Normal 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
18
src/core/apps/tv/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/tv/main.vue
Normal file
9
src/core/apps/tv/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
电视页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
18
src/core/apps/video/app.json
Normal file
18
src/core/apps/video/app.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
src/core/apps/video/main.vue
Normal file
9
src/core/apps/video/main.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
电影页面
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
82
src/core/desktop/DesktopProcess.ts
Normal file
82
src/core/desktop/DesktopProcess.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/core/desktop/DesktopProcessInfo.ts
Normal file
15
src/core/desktop/DesktopProcessInfo.ts
Normal 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
|
||||||
|
})
|
||||||
95
src/core/desktop/ui/DesktopComponent.vue
Normal file
95
src/core/desktop/ui/DesktopComponent.vue
Normal 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>
|
||||||
35
src/core/desktop/ui/DesktopElement.ts
Normal file
35
src/core/desktop/ui/DesktopElement.ts
Normal 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>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
17
src/core/desktop/ui/components/DesktopAppIconElement.ts
Normal file
17
src/core/desktop/ui/components/DesktopAppIconElement.ts
Normal 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>`
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/core/desktop/ui/css/desktop.scss
Normal file
31
src/core/desktop/ui/css/desktop.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/core/desktop/ui/hooks/useDesktopInit.ts
Normal file
169
src/core/desktop/ui/hooks/useDesktopInit.ts
Normal 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[];
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
64
src/core/desktop/utils/useIconDrag.ts
Normal file
64
src/core/desktop/utils/useIconDrag.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/core/events/EventManager.ts
Normal file
35
src/core/events/EventManager.ts
Normal 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 {}
|
||||||
47
src/core/events/IEventBuilder.ts
Normal file
47
src/core/events/IEventBuilder.ts
Normal 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
|
||||||
|
}
|
||||||
61
src/core/events/WindowFormEventManager.ts
Normal file
61
src/core/events/WindowFormEventManager.ts
Normal 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>()
|
||||||
@@ -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)!
|
||||||
28
src/core/process/IProcess.ts
Normal file
28
src/core/process/IProcess.ts
Normal 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;
|
||||||
|
}
|
||||||
26
src/core/process/IProcessInfo.ts
Normal file
26
src/core/process/IProcessInfo.ts
Normal 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[];
|
||||||
|
}
|
||||||
41
src/core/process/IProcessManager.ts
Normal file
41
src/core/process/IProcessManager.ts
Normal 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;
|
||||||
|
}
|
||||||
3
src/core/process/ProcessManager.ts
Normal file
3
src/core/process/ProcessManager.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts'
|
||||||
|
|
||||||
|
export const processManager = new ProcessManagerImpl();
|
||||||
83
src/core/process/impl/ProcessImpl.ts
Normal file
83
src/core/process/impl/ProcessImpl.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/core/process/impl/ProcessInfoImpl.ts
Normal file
101
src/core/process/impl/ProcessInfoImpl.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/core/process/impl/ProcessManagerImpl.ts
Normal file
107
src/core/process/impl/ProcessManagerImpl.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/core/process/types/IAppProcessInfoParams.ts
Normal file
26
src/core/process/types/IAppProcessInfoParams.ts
Normal 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[];
|
||||||
|
}
|
||||||
24
src/core/process/types/ProcessEventTypes.ts
Normal file
24
src/core/process/types/ProcessEventTypes.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/core/service/kernel/AService.ts
Normal file
21
src/core/service/kernel/AService.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/core/service/kernel/ServiceManager.ts
Normal file
41
src/core/service/kernel/ServiceManager.ts
Normal 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()
|
||||||
8
src/core/service/services/NotificationService.ts
Normal file
8
src/core/service/services/NotificationService.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { AService } from '@/core/service/kernel/AService.ts'
|
||||||
|
|
||||||
|
export class NotificationService extends AService {
|
||||||
|
constructor() {
|
||||||
|
super('NotificationService');
|
||||||
|
console.log('NotificationService - 服务注册')
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/core/service/services/SettingsService.ts
Normal file
8
src/core/service/services/SettingsService.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { AService } from '@/core/service/kernel/AService.ts'
|
||||||
|
|
||||||
|
export class SettingsService extends AService {
|
||||||
|
constructor() {
|
||||||
|
super('SettingsService')
|
||||||
|
console.log('SettingsService - 服务注册')
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/core/service/services/UserService.ts
Normal file
20
src/core/service/services/UserService.ts
Normal 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 - 服务注册")
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/core/service/services/WindowFormService.ts
Normal file
64
src/core/service/services/WindowFormService.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/core/state/IObservable.ts
Normal file
57
src/core/state/IObservable.ts
Normal 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] }
|
||||||
|
}
|
||||||
305
src/core/state/impl/ObservableImpl.ts
Normal file
305
src/core/state/impl/ObservableImpl.ts
Normal 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 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
297
src/core/state/impl/ObservableWeakRefImpl.ts
Normal file
297
src/core/state/impl/ObservableWeakRefImpl.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/core/state/store/GlobalStore.ts
Normal file
14
src/core/state/store/GlobalStore.ts
Normal 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
|
||||||
|
})
|
||||||
19
src/core/system/BasicSystemProcess.ts
Normal file
19
src/core/system/BasicSystemProcess.ts
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/core/system/BasicSystemProcessInfo.ts
Normal file
18
src/core/system/BasicSystemProcessInfo.ts
Normal 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
|
||||||
|
});
|
||||||
752
src/core/utils/DraggableResizableWindow.ts
Normal file
752
src/core/utils/DraggableResizableWindow.ts
Normal 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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/core/utils/Singleton.ts
Normal file
17
src/core/utils/Singleton.ts
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/core/window/IWindowForm.ts
Normal file
14
src/core/window/IWindowForm.ts
Normal 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;
|
||||||
|
}
|
||||||
114
src/core/window/impl/WindowFormImpl.ts
Normal file
114
src/core/window/impl/WindowFormImpl.ts
Normal 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() {}
|
||||||
|
}
|
||||||
60
src/core/window/types/IWindowFormConfig.ts
Normal file
60
src/core/window/types/IWindowFormConfig.ts
Normal 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;
|
||||||
|
}
|
||||||
10
src/core/window/types/WindowFormTypes.ts
Normal file
10
src/core/window/types/WindowFormTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 窗体位置坐标 - 左上角
|
||||||
|
*/
|
||||||
|
export interface WindowFormPos {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 窗口状态 */
|
||||||
|
export type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
20
src/core/window/ui/window-form-helper.ts
Normal file
20
src/core/window/ui/window-form-helper.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 排版元素 ===== */
|
/* ===== 排版元素 ===== */
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
61
src/main.ts
61
src/main.ts
@@ -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()
|
|
||||||
})
|
|
||||||
702
src/sdk/index.ts
702
src/sdk/index.ts
@@ -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
Reference in New Issue
Block a user