Compare commits
13 Commits
dev
...
0ca5daad3b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ca5daad3b | |||
| 05882bb3d3 | |||
| 71d5aabb84 | |||
| 204dd4781b | |||
| ed0527bf27 | |||
| a56197a349 | |||
| f1ba609254 | |||
| 7b1dff9ea1 | |||
| b77a20f9b0 | |||
| 972e76e655 | |||
| d18a3d5279 | |||
| d042520b14 | |||
| 9dbc054483 |
@@ -2,5 +2,6 @@
|
|||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
}
|
}
|
||||||
|
|||||||
338
.qoder/quests/api-field-chinese-annotation.md
Normal file
338
.qoder/quests/api-field-chinese-annotation.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# API接口字段中文注释设计文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
本文档旨在为项目中所有API接口的字段添加中文注释,以提高代码的可读性和可维护性。通过对SDK、服务层、事件系统等模块中的接口定义进行中文注释,使开发人员能够更快速地理解和使用这些接口。
|
||||||
|
|
||||||
|
## 2. 设计原则
|
||||||
|
|
||||||
|
- 保持原有代码结构不变,仅添加中文注释
|
||||||
|
- 注释内容准确描述字段的含义和用途
|
||||||
|
- 统一注释风格,确保文档一致性
|
||||||
|
- 重点注释复杂或容易误解的字段
|
||||||
|
|
||||||
|
## 3. 接口字段注释规范
|
||||||
|
|
||||||
|
### 3.1 基本注释格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 字段中文描述
|
||||||
|
*/
|
||||||
|
fieldName: fieldType
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 枚举类型注释格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 枚举类型中文描述
|
||||||
|
*/
|
||||||
|
export enum EnumName {
|
||||||
|
/**
|
||||||
|
* 枚举值中文描述
|
||||||
|
*/
|
||||||
|
VALUE1 = 'value1',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 枚举值中文描述
|
||||||
|
*/
|
||||||
|
VALUE2 = 'value2',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. SDK接口字段注释
|
||||||
|
|
||||||
|
### 4.1 核心类型定义
|
||||||
|
|
||||||
|
#### SDKConfig接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ----------- | -------------- | ---------------- |
|
||||||
|
| appId | string | 应用唯一标识符 |
|
||||||
|
| appName | string | 应用名称 |
|
||||||
|
| version | string | 应用版本号 |
|
||||||
|
| permissions | string[] | 应用所需权限列表 |
|
||||||
|
| debug | boolean (可选) | 是否开启调试模式 |
|
||||||
|
|
||||||
|
#### APIResponse接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------- | ------------- | -------------- |
|
||||||
|
| success | boolean | 请求是否成功 |
|
||||||
|
| data | T (可选) | 返回的数据内容 |
|
||||||
|
| error | string (可选) | 错误信息 |
|
||||||
|
| code | number (可选) | 错误码 |
|
||||||
|
|
||||||
|
### 4.2 窗体SDK接口字段注释
|
||||||
|
|
||||||
|
#### WindowState枚举
|
||||||
|
|
||||||
|
| 枚举值 | 中文注释 |
|
||||||
|
| ---------- | ---------- |
|
||||||
|
| NORMAL | 正常状态 |
|
||||||
|
| MINIMIZED | 最小化状态 |
|
||||||
|
| MAXIMIZED | 最大化状态 |
|
||||||
|
| FULLSCREEN | 全屏状态 |
|
||||||
|
|
||||||
|
#### WindowEvents接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------- | --------------------------------------- | -------------------- |
|
||||||
|
| onResize | (width: number, height: number) => void | 窗体尺寸变化事件回调 |
|
||||||
|
| onMove | (x: number, y: number) => void | 窗体位置移动事件回调 |
|
||||||
|
| onStateChange | (state: WindowState) => void | 窗体状态变化事件回调 |
|
||||||
|
| onFocus | () => void | 窗体获得焦点事件回调 |
|
||||||
|
| onBlur | () => void | 窗体失去焦点事件回调 |
|
||||||
|
| onClose | () => void | 窗体关闭事件回调 |
|
||||||
|
|
||||||
|
### 4.3 存储SDK接口字段注释
|
||||||
|
|
||||||
|
#### StorageStats接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------ | ------ | ------------------ |
|
||||||
|
| usedSpace | number | 已使用存储空间(MB) |
|
||||||
|
| maxSpace | number | 最大存储空间(MB) |
|
||||||
|
| keysCount | number | 存储键数量 |
|
||||||
|
| lastAccessed | Date | 最后访问时间 |
|
||||||
|
|
||||||
|
### 4.4 网络SDK接口字段注释
|
||||||
|
|
||||||
|
#### NetworkRequestConfig接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------ | -------------------------------------------------- | ------------------ |
|
||||||
|
| method | HTTPMethod (可选) | HTTP请求方法 |
|
||||||
|
| headers | Record<string, string> (可选) | 请求头信息 |
|
||||||
|
| body | any (可选) | 请求体数据 |
|
||||||
|
| timeout | number (可选) | 请求超时时间(毫秒) |
|
||||||
|
| responseType | 'json' \| 'text' \| 'blob' \| 'arrayBuffer' (可选) | 响应数据类型 |
|
||||||
|
|
||||||
|
#### NetworkResponse接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ---------- | ---------------------- | ------------ |
|
||||||
|
| data | T | 响应数据内容 |
|
||||||
|
| status | number | HTTP状态码 |
|
||||||
|
| statusText | string | HTTP状态文本 |
|
||||||
|
| headers | Record<string, string> | 响应头信息 |
|
||||||
|
| url | string | 请求URL |
|
||||||
|
|
||||||
|
### 4.5 事件SDK接口字段注释
|
||||||
|
|
||||||
|
#### EventMessage接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| --------- | ------ | -------------- |
|
||||||
|
| id | string | 消息唯一标识符 |
|
||||||
|
| channel | string | 事件频道名称 |
|
||||||
|
| data | T | 消息数据内容 |
|
||||||
|
| senderId | string | 发送方标识符 |
|
||||||
|
| timestamp | Date | 消息发送时间戳 |
|
||||||
|
|
||||||
|
#### EventSubscriptionConfig接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------ | ----------------------------------------- | -------------- |
|
||||||
|
| filter | (message: EventMessage) => boolean (可选) | 消息过滤器函数 |
|
||||||
|
| once | boolean (可选) | 是否只监听一次 |
|
||||||
|
|
||||||
|
### 4.6 UI SDK接口字段注释
|
||||||
|
|
||||||
|
#### DialogOptions接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------- | ----------------- | ------------------ |
|
||||||
|
| title | string (可选) | 对话框标题 |
|
||||||
|
| message | string | 对话框消息内容 |
|
||||||
|
| type | DialogType (可选) | 对话框类型 |
|
||||||
|
| buttons | string[] (可选) | 自定义按钮文本数组 |
|
||||||
|
| defaultButton | number (可选) | 默认按钮索引 |
|
||||||
|
| cancelButton | number (可选) | 取消按钮索引 |
|
||||||
|
|
||||||
|
#### NotificationOptions接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| -------- | ----------------------------------------------- | -------------- |
|
||||||
|
| title | string | 通知标题 |
|
||||||
|
| body | string | 通知正文内容 |
|
||||||
|
| icon | string (可选) | 通知图标URL |
|
||||||
|
| duration | number (可选) | 显示时长(毫秒) |
|
||||||
|
| actions | Array<{ title: string; action: string }> (可选) | 通知操作按钮 |
|
||||||
|
|
||||||
|
### 4.7 系统SDK接口字段注释
|
||||||
|
|
||||||
|
#### SystemInfo接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ---------------- | --------------------------------- | -------------- |
|
||||||
|
| platform | string | 运行平台信息 |
|
||||||
|
| userAgent | string | 用户代理字符串 |
|
||||||
|
| language | string | 系统语言设置 |
|
||||||
|
| timezone | string | 时区信息 |
|
||||||
|
| screenResolution | { width: number; height: number } | 屏幕分辨率 |
|
||||||
|
| colorDepth | number | 颜色深度 |
|
||||||
|
| pixelRatio | number | 像素比率 |
|
||||||
|
|
||||||
|
#### AppInfo接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------ | -------- | ---------------- |
|
||||||
|
| id | string | 应用唯一标识符 |
|
||||||
|
| name | string | 应用名称 |
|
||||||
|
| version | string | 应用版本号 |
|
||||||
|
| permissions | string[] | 应用权限列表 |
|
||||||
|
| createdAt | Date | 应用创建时间 |
|
||||||
|
| lastActiveAt | Date | 应用最后活跃时间 |
|
||||||
|
|
||||||
|
## 5. 服务层接口字段注释
|
||||||
|
|
||||||
|
### 5.1 窗体服务接口字段注释
|
||||||
|
|
||||||
|
#### WindowConfig接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ----------- | -------------- | ------------------ |
|
||||||
|
| title | string | 窗体标题 |
|
||||||
|
| width | number | 窗体宽度(像素) |
|
||||||
|
| height | number | 窗体高度(像素) |
|
||||||
|
| minWidth | number (可选) | 窗体最小宽度(像素) |
|
||||||
|
| minHeight | number (可选) | 窗体最小高度(像素) |
|
||||||
|
| maxWidth | number (可选) | 窗体最大宽度(像素) |
|
||||||
|
| maxHeight | number (可选) | 窗体最大高度(像素) |
|
||||||
|
| resizable | boolean (可选) | 是否可调整大小 |
|
||||||
|
| movable | boolean (可选) | 是否可移动 |
|
||||||
|
| closable | boolean (可选) | 是否可关闭 |
|
||||||
|
| minimizable | boolean (可选) | 是否可最小化 |
|
||||||
|
| maximizable | boolean (可选) | 是否可最大化 |
|
||||||
|
| modal | boolean (可选) | 是否为模态窗体 |
|
||||||
|
| alwaysOnTop | boolean (可选) | 是否始终置顶 |
|
||||||
|
| x | number (可选) | 窗体X坐标位置 |
|
||||||
|
| y | number (可选) | 窗体Y坐标位置 |
|
||||||
|
|
||||||
|
#### WindowInstance接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| --------- | ------------------------ | ------------------ |
|
||||||
|
| id | string | 窗体唯一标识符 |
|
||||||
|
| appId | string | 关联应用标识符 |
|
||||||
|
| config | WindowConfig | 窗体配置信息 |
|
||||||
|
| state | WindowState | 窗体当前状态 |
|
||||||
|
| element | HTMLElement (可选) | 窗体DOM元素 |
|
||||||
|
| iframe | HTMLIFrameElement (可选) | 窗体内嵌iframe元素 |
|
||||||
|
| zIndex | number | 窗体层级索引 |
|
||||||
|
| createdAt | Date | 窗体创建时间 |
|
||||||
|
| updatedAt | Date | 窗体更新时间 |
|
||||||
|
|
||||||
|
## 6. 事件系统接口字段注释
|
||||||
|
|
||||||
|
### 6.1 事件管理器接口字段注释
|
||||||
|
|
||||||
|
#### IEventMap接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------------- | ------------------------ | ---------------- |
|
||||||
|
| [key: string] | (...args: any[]) => void | 事件处理函数映射 |
|
||||||
|
|
||||||
|
#### IEventBuilder接口
|
||||||
|
|
||||||
|
该接口定义了事件管理器的基本方法,无需额外字段注释。
|
||||||
|
|
||||||
|
### 6.2 窗口事件接口字段注释
|
||||||
|
|
||||||
|
#### IWindowFormEvent接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| -------------------- | ------------------------------------------- | ---------------- |
|
||||||
|
| windowFormMinimize | (id: string) => void | 窗口最小化事件 |
|
||||||
|
| windowFormMaximize | (id: string) => void | 窗口最大化事件 |
|
||||||
|
| windowFormRestore | (id: string) => void | 窗口还原事件 |
|
||||||
|
| windowFormClose | (id: string) => void | 窗口关闭事件 |
|
||||||
|
| windowFormFocus | (id: string) => void | 窗口聚焦事件 |
|
||||||
|
| windowFormDataUpdate | (data: IWindowFormDataUpdateParams) => void | 窗口数据更新事件 |
|
||||||
|
| windowFormCreated | () => void | 窗口创建完成事件 |
|
||||||
|
|
||||||
|
#### IWindowFormDataUpdateParams接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------ | ---------------- | ----------------- |
|
||||||
|
| id | string | 窗口唯一标识符 |
|
||||||
|
| state | TWindowFormState | 窗口状态 |
|
||||||
|
| width | number | 窗口宽度 |
|
||||||
|
| height | number | 窗口高度 |
|
||||||
|
| x | number | 窗口X坐标(左上角) |
|
||||||
|
| y | number | 窗口Y坐标(左上角) |
|
||||||
|
|
||||||
|
## 7. 应用管理接口字段注释
|
||||||
|
|
||||||
|
### 7.1 应用清单接口字段注释
|
||||||
|
|
||||||
|
#### InternalAppManifest接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ----------- | -------------------------------------- | ---------------- |
|
||||||
|
| id | string | 应用唯一标识符 |
|
||||||
|
| name | string | 应用名称 |
|
||||||
|
| version | string | 应用版本号 |
|
||||||
|
| description | string | 应用描述信息 |
|
||||||
|
| author | string | 应用作者 |
|
||||||
|
| icon | string | 应用图标 |
|
||||||
|
| permissions | string[] | 应用所需权限列表 |
|
||||||
|
| window | { width: number; height: number; ... } | 窗体配置信息 |
|
||||||
|
| category | string (可选) | 应用分类 |
|
||||||
|
| keywords | string[] (可选) | 应用关键字列表 |
|
||||||
|
|
||||||
|
#### AppRegistration接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| --------- | ------------------- | -------------- |
|
||||||
|
| manifest | InternalAppManifest | 应用清单信息 |
|
||||||
|
| component | any | Vue组件 |
|
||||||
|
| isBuiltIn | boolean | 是否为内置应用 |
|
||||||
|
|
||||||
|
## 8. UI组件接口字段注释
|
||||||
|
|
||||||
|
### 8.1 桌面图标接口字段注释
|
||||||
|
|
||||||
|
#### IDesktopAppIcon接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ------ | ------ | -------------------- |
|
||||||
|
| name | string | 图标名称 |
|
||||||
|
| icon | string | 图标内容 |
|
||||||
|
| path | string | 图标路径 |
|
||||||
|
| x | number | 图标在grid布局中的列 |
|
||||||
|
| y | number | 图标在grid布局中的行 |
|
||||||
|
|
||||||
|
### 8.2 网格模板参数接口字段注释
|
||||||
|
|
||||||
|
#### IGridTemplateParams接口
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 中文注释 |
|
||||||
|
| ---------------- | ------ | -------------- |
|
||||||
|
| cellExpectWidth | number | 单元格预设宽度 |
|
||||||
|
| cellExpectHeight | number | 单元格预设高度 |
|
||||||
|
| cellRealWidth | number | 单元格实际宽度 |
|
||||||
|
| cellRealHeight | number | 单元格实际高度 |
|
||||||
|
| gapX | number | 列间距 |
|
||||||
|
| gapY | number | 行间距 |
|
||||||
|
| colCount | number | 总列数 |
|
||||||
|
| rowCount | number | 总行数 |
|
||||||
|
|
||||||
|
## 9. 实施计划
|
||||||
|
|
||||||
|
1. 对SDK模块中的所有接口字段添加中文注释
|
||||||
|
2. 对服务层模块中的接口字段添加中文注释
|
||||||
|
3. 对事件系统模块中的接口字段添加中文注释
|
||||||
|
4. 对应用管理模块中的接口字段添加中文注释
|
||||||
|
5. 对UI组件模块中的接口字段添加中文注释
|
||||||
|
6. 审查和验证所有注释的准确性和完整性
|
||||||
|
|
||||||
|
## 10. 注意事项
|
||||||
|
|
||||||
|
1. 保持原有代码逻辑不变,仅添加注释
|
||||||
|
2. 确保注释内容准确反映字段的实际用途
|
||||||
|
3. 统一注释风格,避免表述不一致
|
||||||
|
4. 对于已有注释的字段,检查并完善注释内容
|
||||||
|
5. 在添加注释时注意不要影响代码的可读性
|
||||||
461
.qoder/quests/api-unified-interface-design.md
Normal file
461
.qoder/quests/api-unified-interface-design.md
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
# 统一API接口设计文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
### 1.1 目标
|
||||||
|
|
||||||
|
为iframe中的第三方应用提供一套统一、简洁、类型安全的系统服务访问接口,使应用开发者无需关注底层通信机制,即可直接调用系统功能。
|
||||||
|
|
||||||
|
### 1.2 核心价值
|
||||||
|
|
||||||
|
- **简化开发**:封装复杂的跨域通信逻辑,提供类似本地函数调用的体验
|
||||||
|
- **类型安全**:完整的TypeScript类型定义,提供编译时检查和IDE智能提示
|
||||||
|
- **模块化设计**:按功能划分子模块,便于按需使用和维护
|
||||||
|
- **权限控制**:内置权限验证机制,确保系统安全
|
||||||
|
- **事件驱动**:支持双向通信,实现系统与应用间的实时交互
|
||||||
|
|
||||||
|
### 1.3 设计原则
|
||||||
|
|
||||||
|
1. **透明性**:隐藏底层postMessage通信细节
|
||||||
|
2. **一致性**:统一的API响应格式和错误处理机制
|
||||||
|
3. **可扩展性**:模块化架构支持功能扩展
|
||||||
|
4. **安全性**:严格的权限控制和数据验证
|
||||||
|
|
||||||
|
## 2. 系统架构
|
||||||
|
|
||||||
|
### 2.1 整体架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "主应用(系统)"
|
||||||
|
A[主应用UI] --> B[沙箱引擎]
|
||||||
|
B --> C[事件通信服务]
|
||||||
|
C --> D[系统服务层]
|
||||||
|
D --> E[窗口管理]
|
||||||
|
D --> F[存储服务]
|
||||||
|
D --> G[网络服务]
|
||||||
|
D --> H[系统服务]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "子应用(沙箱)"
|
||||||
|
I[子应用UI] --> J[SDK客户端]
|
||||||
|
K[业务逻辑] --> J
|
||||||
|
end
|
||||||
|
|
||||||
|
J --"postMessage"--> B
|
||||||
|
B --"postMessage"--> J
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style I fill:#f3e5f5
|
||||||
|
style J fill:#f3e5f5
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#f1f8e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 SDK架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class SystemDesktopSDK {
|
||||||
|
+version: string
|
||||||
|
+appId: string
|
||||||
|
+initialized: boolean
|
||||||
|
+window: WindowSDK
|
||||||
|
+storage: StorageSDK
|
||||||
|
+network: NetworkSDK
|
||||||
|
+events: EventSDK
|
||||||
|
+ui: UISDK
|
||||||
|
+system: SystemSDK
|
||||||
|
+init(config: SDKConfig) Promise~APIResponse~boolean~~
|
||||||
|
+destroy() Promise~APIResponse~boolean~~
|
||||||
|
+getStatus() Promise~APIResponse~object~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class SDKBase {
|
||||||
|
#appId: string
|
||||||
|
#initialized: boolean
|
||||||
|
#sendToSystem(type: string, data: any) Promise~T~
|
||||||
|
#wrapResponse(promise: Promise~T~) Promise~APIResponse~T~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowSDK {
|
||||||
|
<<interface>>
|
||||||
|
+setTitle(title: string) Promise~APIResponse~boolean~~
|
||||||
|
+resize(width: number, height: number) Promise~APIResponse~boolean~~
|
||||||
|
+move(x: number, y: number) Promise~APIResponse~boolean~~
|
||||||
|
+minimize() Promise~APIResponse~boolean~~
|
||||||
|
+maximize() Promise~APIResponse~boolean~~
|
||||||
|
+restore() Promise~APIResponse~boolean~~
|
||||||
|
+close() Promise~APIResponse~boolean~~
|
||||||
|
+getState() Promise~APIResponse~WindowState~~
|
||||||
|
+on(event: string, callback: Function) void
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageSDK {
|
||||||
|
<<interface>>
|
||||||
|
+set(key: string, value: any) Promise~APIResponse~boolean~~
|
||||||
|
+get(key: string) Promise~APIResponse~any~~
|
||||||
|
+remove(key: string) Promise~APIResponse~boolean~~
|
||||||
|
+clear() Promise~APIResponse~boolean~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkSDK {
|
||||||
|
<<interface>>
|
||||||
|
+request(url: string, config: NetworkRequestConfig) Promise~APIResponse~any~~
|
||||||
|
+get(url: string, config: any) Promise~APIResponse~any~~
|
||||||
|
+post(url: string, data: any, config: any) Promise~APIResponse~any~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventSDK {
|
||||||
|
<<interface>>
|
||||||
|
+emit(channel: string, data: any) Promise~APIResponse~string~~
|
||||||
|
+on(channel: string, callback: Function) Promise~APIResponse~string~~
|
||||||
|
+off(subscriptionId: string) Promise~APIResponse~boolean~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class UISDK {
|
||||||
|
<<interface>>
|
||||||
|
+showDialog(options: DialogOptions) Promise~APIResponse~object~~
|
||||||
|
+showNotification(options: NotificationOptions) Promise~APIResponse~string~~
|
||||||
|
+showToast(message: string, type: string) Promise~APIResponse~string~~
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemSDK {
|
||||||
|
<<interface>>
|
||||||
|
+getSystemInfo() Promise~APIResponse~SystemInfo~~
|
||||||
|
+getAppInfo() Promise~APIResponse~AppInfo~~
|
||||||
|
+requestPermission(permission: string) Promise~APIResponse~PermissionStatus~~
|
||||||
|
}
|
||||||
|
|
||||||
|
SDKBase <|-- SystemDesktopSDK
|
||||||
|
SystemDesktopSDK --> WindowSDK
|
||||||
|
SystemDesktopSDK --> StorageSDK
|
||||||
|
SystemDesktopSDK --> NetworkSDK
|
||||||
|
SystemDesktopSDK --> EventSDK
|
||||||
|
SystemDesktopSDK --> UISDK
|
||||||
|
SystemDesktopSDK --> SystemSDK
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 核心模块设计
|
||||||
|
|
||||||
|
### 3.1 窗口管理模块(WindowSDK)
|
||||||
|
|
||||||
|
#### 3.1.1 功能概述
|
||||||
|
|
||||||
|
提供对应用窗口的控制能力,包括窗口尺寸调整、位置移动、状态切换等。
|
||||||
|
|
||||||
|
#### 3.1.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| --------------------- | ------------ | ----------------------------- | --------------------------------- |
|
||||||
|
| setTitle(title) | 设置窗口标题 | title: string | Promise<APIResponse<boolean>> |
|
||||||
|
| resize(width, height) | 调整窗口尺寸 | width: number, height: number | Promise<APIResponse<boolean>> |
|
||||||
|
| move(x, y) | 移动窗口位置 | x: number, y: number | Promise<APIResponse<boolean>> |
|
||||||
|
| minimize() | 最小化窗口 | 无 | Promise<APIResponse<boolean>> |
|
||||||
|
| maximize() | 最大化窗口 | 无 | Promise<APIResponse<boolean>> |
|
||||||
|
| restore() | 还原窗口 | 无 | Promise<APIResponse<boolean>> |
|
||||||
|
| close() | 关闭窗口 | 无 | Promise<APIResponse<boolean>> |
|
||||||
|
| getState() | 获取窗口状态 | 无 | Promise<APIResponse<WindowState>> |
|
||||||
|
|
||||||
|
#### 3.1.3 事件监听
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监听窗口变化事件
|
||||||
|
SystemSDK.window.on('resize', (width, height) => {
|
||||||
|
console.log(`窗口尺寸变化: ${width}x${height}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移除事件监听
|
||||||
|
SystemSDK.window.off('resize')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 存储模块(StorageSDK)
|
||||||
|
|
||||||
|
#### 3.2.1 功能概述
|
||||||
|
|
||||||
|
提供应用数据的持久化存储能力,支持键值对存储、数据查询、删除等操作。
|
||||||
|
|
||||||
|
#### 3.2.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| --------------- | -------------- | ----------------------- | ------------------------------ |
|
||||||
|
| set(key, value) | 存储数据 | key: string, value: any | Promise<APIResponse<boolean>> |
|
||||||
|
| get(key) | 获取数据 | key: string | Promise<APIResponse<any>> |
|
||||||
|
| remove(key) | 删除数据 | key: string | Promise<APIResponse<boolean>> |
|
||||||
|
| clear() | 清空所有数据 | 无 | Promise<APIResponse<boolean>> |
|
||||||
|
| keys() | 获取所有键名 | 无 | Promise<APIResponse<string[]>> |
|
||||||
|
| has(key) | 检查键是否存在 | key: string | Promise<APIResponse<boolean>> |
|
||||||
|
|
||||||
|
#### 3.2.3 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 存储数据
|
||||||
|
await SystemSDK.storage.set('userSettings', { theme: 'dark', lang: 'zh' })
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const settings = await SystemSDK.storage.get('userSettings')
|
||||||
|
|
||||||
|
// 监听存储变化
|
||||||
|
SystemSDK.storage.on('onChange', (key, newValue, oldValue) => {
|
||||||
|
console.log(`存储变更: ${key} 从 ${oldValue} 变为 ${newValue}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 网络模块(NetworkSDK)
|
||||||
|
|
||||||
|
#### 3.3.1 功能概述
|
||||||
|
|
||||||
|
提供网络请求能力,支持HTTP请求、文件上传下载等操作。
|
||||||
|
|
||||||
|
#### 3.3.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| ----------------------- | ------------ | ----------------------------------------- | ------------------------------------- |
|
||||||
|
| request(url, config) | 发送HTTP请求 | url: string, config: NetworkRequestConfig | Promise<APIResponse<NetworkResponse>> |
|
||||||
|
| get(url, config) | GET请求 | url: string, config: object | Promise<APIResponse<NetworkResponse>> |
|
||||||
|
| post(url, data, config) | POST请求 | url: string, data: any, config: object | Promise<APIResponse<NetworkResponse>> |
|
||||||
|
| upload(url, file) | 上传文件 | url: string, file: File/Blob | Promise<APIResponse<NetworkResponse>> |
|
||||||
|
| download(url, filename) | 下载文件 | url: string, filename: string | Promise<APIResponse<Blob>> |
|
||||||
|
|
||||||
|
#### 3.3.3 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 发送GET请求
|
||||||
|
const response = await SystemSDK.network.get('https://api.example.com/users')
|
||||||
|
|
||||||
|
// 发送POST请求
|
||||||
|
const result = await SystemSDK.network.post('https://api.example.com/users', {
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhangsan@example.com',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const fileInput = document.querySelector('input[type="file"]')
|
||||||
|
if (fileInput.files.length > 0) {
|
||||||
|
await SystemSDK.network.upload('https://api.example.com/upload', fileInput.files[0])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 事件通信模块(EventSDK)
|
||||||
|
|
||||||
|
#### 3.4.1 功能概述
|
||||||
|
|
||||||
|
提供应用间以及应用与系统间的事件通信机制,支持广播、点对点通信等模式。
|
||||||
|
|
||||||
|
#### 3.4.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| ------------------------- | -------------- | ----------------------------------- | ----------------------------- |
|
||||||
|
| emit(channel, data) | 发送事件消息 | channel: string, data: any | Promise<APIResponse<string>> |
|
||||||
|
| on(channel, callback) | 订阅事件频道 | channel: string, callback: Function | Promise<APIResponse<string>> |
|
||||||
|
| off(subscriptionId) | 取消订阅 | subscriptionId: string | Promise<APIResponse<boolean>> |
|
||||||
|
| broadcast(channel, data) | 广播消息 | channel: string, data: any | Promise<APIResponse<string>> |
|
||||||
|
| sendTo(targetAppId, data) | 发送点对点消息 | targetAppId: string, data: any | Promise<APIResponse<string>> |
|
||||||
|
|
||||||
|
#### 3.4.3 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 订阅事件
|
||||||
|
const subscriptionId = await SystemSDK.events.on('user-login', (message) => {
|
||||||
|
console.log('用户登录:', message.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送事件
|
||||||
|
await SystemSDK.events.emit('user-action', { action: 'click', target: 'button1' })
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
await SystemSDK.events.off(subscriptionId)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 UI交互模块(UISDK)
|
||||||
|
|
||||||
|
#### 3.5.1 功能概述
|
||||||
|
|
||||||
|
提供系统级UI交互能力,如对话框、通知、文件选择器等。
|
||||||
|
|
||||||
|
#### 3.5.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| ------------------------- | -------------- | ----------------------------- | ------------------------------ |
|
||||||
|
| showDialog(options) | 显示对话框 | options: DialogOptions | Promise<APIResponse<object>> |
|
||||||
|
| showNotification(options) | 显示通知 | options: NotificationOptions | Promise<APIResponse<string>> |
|
||||||
|
| showFilePicker(options) | 显示文件选择器 | options: FilePickerOptions | Promise<APIResponse<FileList>> |
|
||||||
|
| showToast(message, type) | 显示Toast消息 | message: string, type: string | Promise<APIResponse<string>> |
|
||||||
|
|
||||||
|
#### 3.5.3 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 显示确认对话框
|
||||||
|
const result = await SystemSDK.ui.showDialog({
|
||||||
|
title: '确认操作',
|
||||||
|
message: '确定要删除这个文件吗?',
|
||||||
|
type: 'confirm',
|
||||||
|
buttons: ['取消', '确定'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.data.buttonIndex === 1) {
|
||||||
|
// 用户点击了确定
|
||||||
|
console.log('用户确认删除')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
await SystemSDK.ui.showNotification({
|
||||||
|
title: '操作完成',
|
||||||
|
body: '文件已成功上传',
|
||||||
|
icon: '/icons/success.png',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 系统信息模块(SystemSDK)
|
||||||
|
|
||||||
|
#### 3.6.1 功能概述
|
||||||
|
|
||||||
|
提供系统信息查询和基础系统操作能力。
|
||||||
|
|
||||||
|
#### 3.6.2 接口定义
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
| ----------------------------- | ---------------- | ------------------ | -------------------------------------- |
|
||||||
|
| getSystemInfo() | 获取系统信息 | 无 | Promise<APIResponse<SystemInfo>> |
|
||||||
|
| getAppInfo() | 获取当前应用信息 | 无 | Promise<APIResponse<AppInfo>> |
|
||||||
|
| requestPermission(permission) | 请求权限 | permission: string | Promise<APIResponse<PermissionStatus>> |
|
||||||
|
| getClipboard() | 获取剪贴板内容 | 无 | Promise<APIResponse<string>> |
|
||||||
|
| setClipboard(text) | 设置剪贴板内容 | text: string | Promise<APIResponse<boolean>> |
|
||||||
|
| openExternal(url) | 打开外部链接 | url: string | Promise<APIResponse<boolean>> |
|
||||||
|
|
||||||
|
## 4. 通信机制设计
|
||||||
|
|
||||||
|
### 4.1 消息协议
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as 子应用SDK
|
||||||
|
participant B as 主应用沙箱引擎
|
||||||
|
participant C as 系统服务
|
||||||
|
|
||||||
|
A->>B: postMessage(sdk:call, method, data)
|
||||||
|
B->>C: 调用相应系统服务
|
||||||
|
C-->>B: 返回处理结果
|
||||||
|
B-->>A: postMessage(system:response, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 消息格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SDK调用消息格式
|
||||||
|
{
|
||||||
|
type: 'sdk:call',
|
||||||
|
requestId: string, // 唯一请求ID
|
||||||
|
method: string, // 调用方法路径,如 "window.setTitle"
|
||||||
|
data: any, // 传递的数据
|
||||||
|
appId: string // 应用ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统响应消息格式
|
||||||
|
{
|
||||||
|
type: 'system:response',
|
||||||
|
requestId: string, // 对应的请求ID
|
||||||
|
success: boolean, // 是否成功
|
||||||
|
data: any, // 成功时的数据
|
||||||
|
error: string // 失败时的错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 错误处理机制
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 统一响应格式
|
||||||
|
interface APIResponse<T> {
|
||||||
|
success: boolean // 是否成功
|
||||||
|
data?: T // 成功时的数据
|
||||||
|
error?: string // 错误信息
|
||||||
|
code?: number // 错误码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 权限控制设计
|
||||||
|
|
||||||
|
### 5.1 权限模型
|
||||||
|
|
||||||
|
系统采用基于功能的权限控制模型,应用需要在初始化时声明所需权限。
|
||||||
|
|
||||||
|
### 5.2 权限类型
|
||||||
|
|
||||||
|
| 权限名称 | 描述 | 敏感级别 |
|
||||||
|
| ------------------- | -------------- | -------- |
|
||||||
|
| window.control | 窗口控制权限 | 中 |
|
||||||
|
| storage.read | 存储读取权限 | 低 |
|
||||||
|
| storage.write | 存储写入权限 | 中 |
|
||||||
|
| network.request | 网络请求权限 | 中 |
|
||||||
|
| event.broadcast | 事件广播权限 | 中 |
|
||||||
|
| system.clipboard | 剪贴板访问权限 | 高 |
|
||||||
|
| system.notification | 通知权限 | 低 |
|
||||||
|
|
||||||
|
### 5.3 权限申请流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[应用初始化] --> B{权限检查}
|
||||||
|
B -->|权限已授予| C[正常运行]
|
||||||
|
B -->|权限未授予| D[请求权限]
|
||||||
|
D --> E[用户授权]
|
||||||
|
E -->|允许| C
|
||||||
|
E -->|拒绝| F[限制功能]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 使用指南
|
||||||
|
|
||||||
|
### 6.1 SDK初始化
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 初始化SDK
|
||||||
|
const result = await SystemSDK.init({
|
||||||
|
appId: 'com.example.myapp',
|
||||||
|
appName: '我的应用',
|
||||||
|
version: '1.0.0',
|
||||||
|
permissions: ['storage.read', 'storage.write', 'network.request'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('SDK初始化成功')
|
||||||
|
} else {
|
||||||
|
console.error('SDK初始化失败:', result.error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 功能调用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 设置窗口标题
|
||||||
|
await SystemSDK.window.setTitle('我的应用 - 主界面')
|
||||||
|
|
||||||
|
// 存储用户数据
|
||||||
|
await SystemSDK.storage.set('userData', { name: '张三', age: 25 })
|
||||||
|
|
||||||
|
// 发送网络请求
|
||||||
|
const response = await SystemSDK.network.get('/api/user/profile')
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
await SystemSDK.ui.showNotification({
|
||||||
|
title: '数据加载完成',
|
||||||
|
body: '用户信息已更新',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 事件监听示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 监听窗口关闭事件
|
||||||
|
SystemSDK.window.on('close', async () => {
|
||||||
|
// 保存应用状态
|
||||||
|
await SystemSDK.storage.set('appState', getCurrentState())
|
||||||
|
console.log('应用状态已保存')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
SystemSDK.system.on('themeChange', (theme) => {
|
||||||
|
// 更新应用主题
|
||||||
|
updateAppTheme(theme)
|
||||||
|
})
|
||||||
|
```
|
||||||
144
.qoder/quests/module-dependency-analysis.md
Normal file
144
.qoder/quests/module-dependency-analysis.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# 模块依赖分析:EventCommunicationService 与 EventBuilderImpl 关系
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档旨在分析 Vue Desktop 项目中 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 与 [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 的关系,明确它们各自的职责以及是否需要同时存在这两个组件。
|
||||||
|
|
||||||
|
## 系统架构概览
|
||||||
|
|
||||||
|
Vue Desktop 使用分层的事件管理系统:
|
||||||
|
|
||||||
|
1. 内部事件总线 ([EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95)) - 处理组件间的直接通信
|
||||||
|
2. 应用间通信服务 ([EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638)) - 处理跨应用/模块的复杂消息传递
|
||||||
|
|
||||||
|
## 模块详细分析
|
||||||
|
|
||||||
|
### EventBuilderImpl 分析
|
||||||
|
|
||||||
|
[EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 是一个通用的事件管理器实现,提供了基础的发布/订阅模式功能:
|
||||||
|
|
||||||
|
#### 主要职责
|
||||||
|
|
||||||
|
- 提供简单的事件监听(addEventListener)和触发(notifyEvent)机制
|
||||||
|
- 支持一次性事件监听(once选项)
|
||||||
|
- 支持立即执行(immediate选项)
|
||||||
|
- 实现 [IEventBuilder](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\IEventBuilder.ts#L1-L28) 接口
|
||||||
|
|
||||||
|
#### 特点
|
||||||
|
|
||||||
|
- 轻量级,适用于组件内部通信
|
||||||
|
- 不涉及消息队列、优先级、过期时间等复杂概念
|
||||||
|
- 基于回调函数直接调用
|
||||||
|
|
||||||
|
### EventCommunicationService 分析
|
||||||
|
|
||||||
|
[EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 是一个高级消息通信系统,用于处理复杂的跨应用通信场景:
|
||||||
|
|
||||||
|
#### 主要职责
|
||||||
|
|
||||||
|
- 管理应用间的事件订阅和消息传递
|
||||||
|
- 实现消息优先级、过期时间、重试机制等功能
|
||||||
|
- 提供频道管理和权限控制
|
||||||
|
- 支持广播和点对点消息
|
||||||
|
- 维护消息历史和统计数据
|
||||||
|
|
||||||
|
#### 特点
|
||||||
|
|
||||||
|
- 功能丰富,支持复杂的消息路由和处理
|
||||||
|
- 异步处理消息队列
|
||||||
|
- 提供消息持久化和统计功能
|
||||||
|
- 支持权限验证和消息过滤
|
||||||
|
|
||||||
|
## 依赖关系分析
|
||||||
|
|
||||||
|
### 架构层级
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[SystemServiceIntegration] --> B[EventCommunicationService]
|
||||||
|
A --> C[WindowFormService]
|
||||||
|
A --> D[ResourceService]
|
||||||
|
|
||||||
|
B --> E[IEventBuilder]
|
||||||
|
C --> E
|
||||||
|
|
||||||
|
F[EventBuilderImpl] -.-> E
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖说明
|
||||||
|
|
||||||
|
1. [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 依赖 [IEventBuilder](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\IEventBuilder.ts#L1-L28) 接口,但不直接依赖具体实现
|
||||||
|
2. [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 是 [IEventBuilder](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\IEventBuilder.ts#L1-L28) 的具体实现之一
|
||||||
|
3. [SystemServiceIntegration](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\SystemServiceIntegration.ts#L36-L597) 同时使用 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 和其他服务,并传入 [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 实例
|
||||||
|
|
||||||
|
## 是否需要 EventBuilderImpl?
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
**是的,项目仍然需要 [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95)**。
|
||||||
|
|
||||||
|
### 原因分析
|
||||||
|
|
||||||
|
1. **职责分离**:
|
||||||
|
- [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 处理组件内简单事件通信
|
||||||
|
- [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 处理跨应用复杂消息传递
|
||||||
|
|
||||||
|
2. **性能考虑**:
|
||||||
|
- 内部组件通信不需要 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 的复杂特性
|
||||||
|
- [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 提供轻量级解决方案
|
||||||
|
|
||||||
|
3. **架构清晰性**:
|
||||||
|
- 保持两种不同层次的事件通信机制有助于维护架构清晰性
|
||||||
|
- 避免将所有事件都通过复杂的消息系统传递
|
||||||
|
|
||||||
|
## 正确的类型使用
|
||||||
|
|
||||||
|
### EventCommunicationService 中的类型
|
||||||
|
|
||||||
|
在 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 构造函数中,应该使用接口而不是具体实现:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
constructor(eventBus: IEventBuilder<any>)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemServiceIntegration 中的类型
|
||||||
|
|
||||||
|
在 [SystemServiceIntegration](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\SystemServiceIntegration.ts#L36-L597) 中,需要创建 [EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 实例并将其注入到依赖它的服务中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private eventBus: IEventBuilder<any>
|
||||||
|
|
||||||
|
constructor(config: SystemServiceConfig = {}) {
|
||||||
|
// 使用具体实现创建实例
|
||||||
|
this.eventBus = new EventBuilderImpl<any>()
|
||||||
|
|
||||||
|
// 注入到依赖的服务中
|
||||||
|
// EventCommunicationService 需要 IEventBuilder 接口
|
||||||
|
// WindowFormService 也需要 IEventBuilder 接口
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐实践
|
||||||
|
|
||||||
|
### 类型声明建议
|
||||||
|
|
||||||
|
对于 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 和其他服务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 接受接口而非具体实现
|
||||||
|
constructor(eventBus: IEventBuilder<any>)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实例化建议
|
||||||
|
|
||||||
|
在 [SystemServiceIntegration](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\SystemServiceIntegration.ts#L36-L597) 中:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 创建具体实例
|
||||||
|
private eventBus: IEventBuilder<any>
|
||||||
|
this.eventBus = new EventBuilderImpl<any>()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
[EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 和 [EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 在系统中有不同的职责,两者都是必要的。[EventBuilderImpl](file://c:\Users\98354\Desktop\develop\vue-desktop\src\events\impl\EventBuilderImpl.ts#L7-L95) 提供基础事件机制,[EventCommunicationService](file://c:\Users\98354\Desktop\develop\vue-desktop\src\services\EventCommunicationService.ts#L95-L638) 提供高级消息通信功能。在类型使用上,应遵循依赖倒置原则,服务依赖接口而非具体实现。
|
||||||
0
.qoder/quests/music-player-error-handling.md
Normal file
0
.qoder/quests/music-player-error-handling.md
Normal file
244
.qoder/quests/project-code-optimization.md
Normal file
244
.qoder/quests/project-code-optimization.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Vue Desktop 项目代码优化设计文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
本文档旨在分析 Vue Desktop 项目的现有代码结构,识别无用或冗余的代码,并提出优化方案。通过去除无用代码,提高项目可维护性、减少包体积、提升性能。
|
||||||
|
|
||||||
|
## 2. 项目架构分析
|
||||||
|
|
||||||
|
### 2.1 当前架构概览
|
||||||
|
|
||||||
|
Vue Desktop 采用模块化架构,主要包含以下核心模块:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[主应用] --> B[服务层]
|
||||||
|
A --> C[UI层]
|
||||||
|
A --> D[事件系统]
|
||||||
|
A --> E[应用注册中心]
|
||||||
|
|
||||||
|
B --> B1[WindowFormService]
|
||||||
|
B --> B2[ResourceService]
|
||||||
|
B --> B3[EventCommunicationService]
|
||||||
|
B --> B4[ApplicationSandboxEngine]
|
||||||
|
B --> B5[ApplicationLifecycleManager]
|
||||||
|
B --> B6[ExternalAppDiscovery]
|
||||||
|
|
||||||
|
C --> C1[DesktopContainer]
|
||||||
|
C --> C2[AppIcon]
|
||||||
|
C --> C3[AppRenderer]
|
||||||
|
|
||||||
|
D --> D1[EventManager]
|
||||||
|
D --> D2[WindowFormEventManager]
|
||||||
|
|
||||||
|
E --> E1[内置应用]
|
||||||
|
E --> E2[外部应用]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 核心模块职责
|
||||||
|
|
||||||
|
| 模块 | 职责 |
|
||||||
|
| --------------------------- | ---------------------------------- |
|
||||||
|
| WindowFormService | 管理应用窗体的创建、销毁、状态控制 |
|
||||||
|
| ResourceService | 管理应用资源访问权限和存储 |
|
||||||
|
| EventCommunicationService | 处理应用间通信 |
|
||||||
|
| ApplicationSandboxEngine | 为外部应用创建安全沙箱环境 |
|
||||||
|
| ApplicationLifecycleManager | 管理应用的完整生命周期 |
|
||||||
|
| ExternalAppDiscovery | 自动发现和注册外部应用 |
|
||||||
|
| DesktopContainer | 桌面容器,管理应用图标布局 |
|
||||||
|
| AppIcon | 应用图标组件,支持拖拽和双击启动 |
|
||||||
|
| AppRenderer | 渲染内置应用的组件 |
|
||||||
|
|
||||||
|
## 3. 无用代码识别与分析
|
||||||
|
|
||||||
|
### 3.1 重复导入和未使用导入
|
||||||
|
|
||||||
|
在多个文件中存在重复导入或未使用的导入语句,增加了不必要的代码体积。
|
||||||
|
|
||||||
|
### 3.2 冗余功能实现
|
||||||
|
|
||||||
|
1. **重复的应用发现机制**:
|
||||||
|
- `ExternalAppDiscovery` 和 `AppRegistry` 都负责应用注册,但职责不清晰
|
||||||
|
- 外部应用发现服务中存在多种扫描策略,但实际只使用一种
|
||||||
|
|
||||||
|
2. **重复的状态管理**:
|
||||||
|
- 多个服务中维护了相似的状态管理逻辑
|
||||||
|
- 事件系统存在多层封装但使用不一致
|
||||||
|
|
||||||
|
### 3.3 未使用的组件和方法
|
||||||
|
|
||||||
|
1. **未使用的UI组件**:
|
||||||
|
- `SystemStatus` 组件在桌面容器中定义但使用有限
|
||||||
|
- 部分样式和功能未被实际使用
|
||||||
|
|
||||||
|
2. **未使用的服务方法**:
|
||||||
|
- `ApplicationLifecycleManager` 中的部分生命周期方法未被调用
|
||||||
|
- `ExternalAppDiscovery` 中的测试和调试方法可以移除
|
||||||
|
|
||||||
|
### 3.4 过度工程化
|
||||||
|
|
||||||
|
1. **复杂的沙箱实现**:
|
||||||
|
- 对于内置应用不需要沙箱,但代码中仍存在相关处理逻辑
|
||||||
|
- 沙箱安全级别设置过于复杂,实际使用中只用到部分配置
|
||||||
|
|
||||||
|
2. **冗余的事件系统**:
|
||||||
|
- 存在多套事件管理机制,增加了复杂性
|
||||||
|
- 部分事件监听器未正确清理,可能导致内存泄漏
|
||||||
|
|
||||||
|
## 4. 优化方案设计
|
||||||
|
|
||||||
|
### 4.1 代码清理策略
|
||||||
|
|
||||||
|
#### 4.1.1 移除未使用的导入和变量
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[扫描代码] --> B[识别未使用导入]
|
||||||
|
B --> C[移除未使用导入]
|
||||||
|
A --> D[识别未使用变量]
|
||||||
|
D --> E[移除未使用变量]
|
||||||
|
C --> F[代码重构]
|
||||||
|
E --> F
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.1.2 清理冗余功能
|
||||||
|
|
||||||
|
1. **统一应用注册机制**:
|
||||||
|
- 明确区分内置应用和外部应用的注册方式
|
||||||
|
- 移除重复的应用发现逻辑
|
||||||
|
|
||||||
|
2. **简化沙箱实现**:
|
||||||
|
- 仅对外部应用创建沙箱
|
||||||
|
- 移除内置应用的沙箱相关代码
|
||||||
|
|
||||||
|
### 4.2 模块重构方案
|
||||||
|
|
||||||
|
#### 4.2.1 服务层优化
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[服务层重构] --> B[合并相似功能]
|
||||||
|
A --> C[移除冗余方法]
|
||||||
|
A --> D[优化依赖关系]
|
||||||
|
|
||||||
|
B --> B1[统一状态管理]
|
||||||
|
B --> B2[合并事件处理]
|
||||||
|
|
||||||
|
C --> C1[移除未调用方法]
|
||||||
|
C --> C2[简化接口设计]
|
||||||
|
|
||||||
|
D --> D1[明确依赖关系]
|
||||||
|
D --> D2[减少循环依赖]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.2 UI层优化
|
||||||
|
|
||||||
|
1. **组件精简**:
|
||||||
|
- 移除未使用的系统状态组件
|
||||||
|
- 合并功能相似的UI组件
|
||||||
|
|
||||||
|
2. **样式优化**:
|
||||||
|
- 移除未使用的CSS类和样式
|
||||||
|
- 统一设计风格和组件样式
|
||||||
|
|
||||||
|
### 4.3 性能优化措施
|
||||||
|
|
||||||
|
#### 4.3.1 减少内存占用
|
||||||
|
|
||||||
|
1. **优化事件监听器**:
|
||||||
|
- 确保所有事件监听器在组件销毁时正确移除
|
||||||
|
- 使用弱引用避免循环引用
|
||||||
|
|
||||||
|
2. **优化数据结构**:
|
||||||
|
- 使用更高效的数据结构存储应用信息
|
||||||
|
- 减少不必要的响应式数据
|
||||||
|
|
||||||
|
#### 4.3.2 提升加载性能
|
||||||
|
|
||||||
|
1. **懒加载优化**:
|
||||||
|
- 对非核心功能采用懒加载
|
||||||
|
- 优化应用启动流程
|
||||||
|
|
||||||
|
2. **资源优化**:
|
||||||
|
- 压缩和合并静态资源
|
||||||
|
- 移除未使用的资源文件
|
||||||
|
|
||||||
|
## 5. 详细优化实施计划
|
||||||
|
|
||||||
|
### 5.1 第一阶段:代码清理 (1-2天)
|
||||||
|
|
||||||
|
| 任务 | 描述 | 预期效果 |
|
||||||
|
| -------------- | --------------------------------- | ---------------- |
|
||||||
|
| 移除未使用导入 | 扫描并移除所有未使用的导入语句 | 减少代码体积 |
|
||||||
|
| 清理未使用变量 | 识别并移除未使用的变量和方法 | 提高代码可读性 |
|
||||||
|
| 移除调试代码 | 清理所有console.log和调试相关代码 | 减少生产环境代码 |
|
||||||
|
|
||||||
|
### 5.2 第二阶段:功能重构 (3-5天)
|
||||||
|
|
||||||
|
| 任务 | 描述 | 预期效果 |
|
||||||
|
| ------------ | -------------------------- | ---------------- |
|
||||||
|
| 统一应用注册 | 明确内置和外部应用注册机制 | 简化应用管理 |
|
||||||
|
| 简化沙箱实现 | 仅对外部应用创建沙箱 | 减少复杂性 |
|
||||||
|
| 优化事件系统 | 合并冗余事件处理逻辑 | 提高事件处理效率 |
|
||||||
|
|
||||||
|
### 5.3 第三阶段:性能优化 (2-3天)
|
||||||
|
|
||||||
|
| 任务 | 描述 | 预期效果 |
|
||||||
|
| ------------ | ---------------------- | ---------------- |
|
||||||
|
| 内存泄漏修复 | 确保事件监听器正确清理 | 减少内存占用 |
|
||||||
|
| 加载性能优化 | 优化应用启动和资源加载 | 提升用户体验 |
|
||||||
|
| 打包优化 | 配置代码分割和懒加载 | 减少初始加载时间 |
|
||||||
|
|
||||||
|
## 6. 风险评估与缓解措施
|
||||||
|
|
||||||
|
### 6.1 潜在风险
|
||||||
|
|
||||||
|
1. **功能丢失风险**:
|
||||||
|
- 移除代码时可能误删仍在使用的功能
|
||||||
|
- 解决方案:建立完整的测试用例,确保核心功能不受影响
|
||||||
|
|
||||||
|
2. **兼容性问题**:
|
||||||
|
- 重构可能影响现有外部应用的兼容性
|
||||||
|
- 解决方案:保持API接口稳定,提供迁移指南
|
||||||
|
|
||||||
|
3. **性能回退**:
|
||||||
|
- 优化过程中可能引入新的性能问题
|
||||||
|
- 解决方案:进行充分的性能测试和监控
|
||||||
|
|
||||||
|
### 6.2 缓解措施
|
||||||
|
|
||||||
|
1. **建立测试保障**:
|
||||||
|
- 完善单元测试和集成测试
|
||||||
|
- 建立回归测试机制
|
||||||
|
|
||||||
|
2. **渐进式重构**:
|
||||||
|
- 采用小步快跑的方式进行重构
|
||||||
|
- 每次重构后进行充分测试
|
||||||
|
|
||||||
|
3. **文档更新**:
|
||||||
|
- 及时更新相关技术文档
|
||||||
|
- 记录重要的设计变更
|
||||||
|
|
||||||
|
## 7. 验证标准
|
||||||
|
|
||||||
|
### 7.1 代码质量指标
|
||||||
|
|
||||||
|
| 指标 | 优化前 | 优化后目标 |
|
||||||
|
| -------- | --------- | ---------- |
|
||||||
|
| 代码行数 | 约15000行 | 减少15-20% |
|
||||||
|
| 包体积 | 约5MB | 减少10-15% |
|
||||||
|
| 内存占用 | 约200MB | 减少10-20% |
|
||||||
|
|
||||||
|
### 7.2 性能指标
|
||||||
|
|
||||||
|
| 指标 | 优化前 | 优化后目标 |
|
||||||
|
| ------------ | ------ | ---------- |
|
||||||
|
| 初始加载时间 | 3-5秒 | 减少20-30% |
|
||||||
|
| 应用启动时间 | 1-2秒 | 减少15-25% |
|
||||||
|
| 内存泄漏 | 存在 | 完全消除 |
|
||||||
|
|
||||||
|
## 8. 总结
|
||||||
|
|
||||||
|
通过对 Vue Desktop 项目的全面分析,我们识别出多个可以优化的方面,包括代码清理、功能重构和性能优化。实施这些优化措施将显著提高项目的可维护性、减少包体积、提升运行性能,同时保持功能完整性。
|
||||||
|
|
||||||
|
优化工作将分阶段进行,确保在提升代码质量的同时不影响现有功能。通过建立完善的测试和验证机制,我们可以有效控制风险,确保优化工作的成功实施。
|
||||||
623
.qoder/quests/system-business-decoupling-design.md
Normal file
623
.qoder/quests/system-business-decoupling-design.md
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
# 系统与业务解耦架构设计
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本设计旨在构建一个高性能的类Windows桌面前端系统,实现系统框架与业务应用的完全解耦。系统提供统一的桌面环境、窗体管理和资源访问控制,而业务应用通过标准化的SDK接口进行开发,确保双方独立演进且相互不影响。
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
|
||||||
|
- **完全隔离**:系统与应用在运行时完全隔离,应用无法直接访问系统资源
|
||||||
|
- **标准化接口**:通过统一的SDK提供标准化的系统服务接口
|
||||||
|
- **性能优先**:采用微前端沙箱、虚拟化渲染等技术确保高性能
|
||||||
|
- **框架无关**:支持任意前端框架开发的第三方应用
|
||||||
|
- **安全可控**:严格的权限控制和安全沙箱机制
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
### 系统分层架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "用户界面层"
|
||||||
|
Desktop[桌面容器]
|
||||||
|
WindowManager[窗体管理器]
|
||||||
|
TaskBar[任务栏]
|
||||||
|
SystemUI[系统UI组件]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "系统服务层"
|
||||||
|
WindowFormService[窗体服务]
|
||||||
|
ResourceService[资源服务]
|
||||||
|
EventService[事件服务]
|
||||||
|
SecurityService[安全服务]
|
||||||
|
StorageService[存储服务]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "应用运行时层"
|
||||||
|
AppContainer[应用容器]
|
||||||
|
SandboxEngine[沙箱引擎]
|
||||||
|
SDKBridge[SDK桥接层]
|
||||||
|
AppLifecycle[应用生命周期]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "第三方应用"
|
||||||
|
App1[业务应用A]
|
||||||
|
App2[业务应用B]
|
||||||
|
App3[业务应用C]
|
||||||
|
end
|
||||||
|
|
||||||
|
Desktop --> WindowManager
|
||||||
|
WindowManager --> WindowFormService
|
||||||
|
AppContainer --> SandboxEngine
|
||||||
|
SDKBridge --> SystemService[系统服务层]
|
||||||
|
App1 --> AppContainer
|
||||||
|
App2 --> AppContainer
|
||||||
|
App3 --> AppContainer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件职责
|
||||||
|
|
||||||
|
| 组件 | 职责 | 接口类型 |
|
||||||
|
| ---------- | -------------------------- | -------- |
|
||||||
|
| 桌面容器 | 管理应用图标、布局和交互 | 系统内部 |
|
||||||
|
| 窗体管理器 | 窗体创建、管理、切换和销毁 | 系统内部 |
|
||||||
|
| 应用容器 | 第三方应用运行环境隔离 | 对外SDK |
|
||||||
|
| 沙箱引擎 | 安全隔离和权限控制 | 系统内部 |
|
||||||
|
| SDK桥接层 | 系统服务的标准化接口 | 对外SDK |
|
||||||
|
|
||||||
|
## 系统核心服务
|
||||||
|
|
||||||
|
### 窗体管理服务
|
||||||
|
|
||||||
|
窗体管理服务负责所有窗体的生命周期管理,提供统一的窗体操作接口。
|
||||||
|
|
||||||
|
#### 窗体生命周期
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Creating: 创建窗体
|
||||||
|
Creating --> Loading: 加载应用
|
||||||
|
Loading --> Active: 激活显示
|
||||||
|
Active --> Minimized: 最小化
|
||||||
|
Minimized --> Active: 恢复显示
|
||||||
|
Active --> Maximized: 最大化
|
||||||
|
Maximized --> Active: 还原
|
||||||
|
Active --> Closing: 关闭请求
|
||||||
|
Closing --> Destroyed: 销毁完成
|
||||||
|
Destroyed --> [*]
|
||||||
|
|
||||||
|
Loading --> Error: 加载失败
|
||||||
|
Error --> Destroyed: 错误处理
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 窗体服务接口规范
|
||||||
|
|
||||||
|
| 服务方法 | 参数 | 返回值 | 描述 |
|
||||||
|
| -------------- | ----------------------- | -------------- | ------------ |
|
||||||
|
| createWindow | appId, config | WindowInstance | 创建新窗体 |
|
||||||
|
| destroyWindow | windowId | boolean | 销毁指定窗体 |
|
||||||
|
| minimizeWindow | windowId | boolean | 最小化窗体 |
|
||||||
|
| maximizeWindow | windowId | boolean | 最大化窗体 |
|
||||||
|
| restoreWindow | windowId | boolean | 还原窗体 |
|
||||||
|
| setWindowTitle | windowId, title | boolean | 设置窗体标题 |
|
||||||
|
| setWindowSize | windowId, width, height | boolean | 设置窗体尺寸 |
|
||||||
|
|
||||||
|
### 资源管理服务
|
||||||
|
|
||||||
|
资源管理服务提供统一的系统资源访问控制,确保应用只能通过授权访问系统资源。
|
||||||
|
|
||||||
|
#### 资源访问控制矩阵
|
||||||
|
|
||||||
|
| 资源类型 | 默认权限 | 申请方式 | 审批流程 |
|
||||||
|
| ------------ | ------------ | -------- | -------- |
|
||||||
|
| 本地存储 | 应用独立空间 | 配置声明 | 自动授权 |
|
||||||
|
| 网络请求 | 白名单域名 | 动态申请 | 用户确认 |
|
||||||
|
| 文件系统 | 禁止访问 | 动态申请 | 用户确认 |
|
||||||
|
| 系统通知 | 禁止发送 | 动态申请 | 用户确认 |
|
||||||
|
| 剪贴板 | 禁止访问 | 动态申请 | 用户确认 |
|
||||||
|
| 摄像头麦克风 | 禁止访问 | 动态申请 | 用户确认 |
|
||||||
|
|
||||||
|
### 事件通信服务
|
||||||
|
|
||||||
|
提供系统级别的事件通信机制,支持应用间消息传递和系统事件监听。
|
||||||
|
|
||||||
|
#### 事件分类与权限
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "系统事件"
|
||||||
|
SE1[窗体状态变化]
|
||||||
|
SE2[主题切换]
|
||||||
|
SE3[网络状态变化]
|
||||||
|
SE4[系统资源变化]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "应用事件"
|
||||||
|
AE1[应用间消息]
|
||||||
|
AE2[应用状态同步]
|
||||||
|
AE3[数据共享请求]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "用户事件"
|
||||||
|
UE1[用户交互]
|
||||||
|
UE2[快捷键触发]
|
||||||
|
UE3[拖拽操作]
|
||||||
|
end
|
||||||
|
|
||||||
|
SE1 --> 只读监听
|
||||||
|
SE2 --> 只读监听
|
||||||
|
AE1 --> 权限验证
|
||||||
|
AE2 --> 权限验证
|
||||||
|
UE1 --> 应用内处理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 应用沙箱机制
|
||||||
|
|
||||||
|
### 沙箱隔离策略
|
||||||
|
|
||||||
|
应用运行在完全隔离的沙箱环境中,通过多重隔离机制确保安全性。
|
||||||
|
|
||||||
|
#### 沙箱隔离层次
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "物理隔离层"
|
||||||
|
IFrame[IFrame容器]
|
||||||
|
ShadowDOM[Shadow DOM]
|
||||||
|
CSP[内容安全策略]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "逻辑隔离层"
|
||||||
|
Namespace[命名空间隔离]
|
||||||
|
Memory[内存隔离]
|
||||||
|
Storage[存储隔离]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "网络隔离层"
|
||||||
|
CORS[跨域限制]
|
||||||
|
Proxy[代理拦截]
|
||||||
|
Whitelist[白名单过滤]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "API隔离层"
|
||||||
|
SDKProxy[SDK代理]
|
||||||
|
Permission[权限验证]
|
||||||
|
Audit[审计日志]
|
||||||
|
end
|
||||||
|
|
||||||
|
IFrame --> Namespace
|
||||||
|
ShadowDOM --> Memory
|
||||||
|
CSP --> Storage
|
||||||
|
Namespace --> SDKProxy
|
||||||
|
Memory --> Permission
|
||||||
|
Storage --> Audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 沙箱性能优化
|
||||||
|
|
||||||
|
#### 虚拟化渲染机制
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start[应用启动] --> CheckCache{检查缓存}
|
||||||
|
CheckCache -->|存在| LoadCache[加载缓存实例]
|
||||||
|
CheckCache -->|不存在| CreateSandbox[创建沙箱]
|
||||||
|
|
||||||
|
CreateSandbox --> PreloadResources[预加载资源]
|
||||||
|
PreloadResources --> InitSDK[初始化SDK]
|
||||||
|
InitSDK --> MountApp[挂载应用]
|
||||||
|
|
||||||
|
LoadCache --> ValidateCache{验证缓存有效性}
|
||||||
|
ValidateCache -->|有效| RestoreApp[恢复应用状态]
|
||||||
|
ValidateCache -->|无效| CreateSandbox
|
||||||
|
|
||||||
|
MountApp --> AppReady[应用就绪]
|
||||||
|
RestoreApp --> AppReady
|
||||||
|
|
||||||
|
AppReady --> Monitor[性能监控]
|
||||||
|
Monitor --> OptimizeMemory[内存优化]
|
||||||
|
OptimizeMemory --> CheckIdle{检查空闲状态}
|
||||||
|
CheckIdle -->|空闲| SuspendApp[挂起应用]
|
||||||
|
CheckIdle -->|活跃| Monitor
|
||||||
|
|
||||||
|
SuspendApp --> CheckActivate{检查激活请求}
|
||||||
|
CheckActivate -->|激活| RestoreApp
|
||||||
|
CheckActivate -->|继续空闲| SuspendApp
|
||||||
|
```
|
||||||
|
|
||||||
|
## SDK接口设计
|
||||||
|
|
||||||
|
### SDK架构组成
|
||||||
|
|
||||||
|
SDK作为应用与系统间的唯一通信桥梁,提供完整的系统服务接口。
|
||||||
|
|
||||||
|
#### SDK模块划分
|
||||||
|
|
||||||
|
| 模块名称 | 功能描述 | 主要接口 |
|
||||||
|
| ----------- | -------- | --------------------------------------- |
|
||||||
|
| WindowSDK | 窗体操作 | setTitle, resize, minimize, maximize |
|
||||||
|
| StorageSDK | 数据存储 | get, set, remove, clear |
|
||||||
|
| EventSDK | 事件通信 | emit, on, off, broadcast |
|
||||||
|
| UISDK | UI组件 | showDialog, showNotification, showToast |
|
||||||
|
| NetworkSDK | 网络请求 | request, upload, download |
|
||||||
|
| ResourceSDK | 资源访问 | getFile, saveFile, getClipboard |
|
||||||
|
|
||||||
|
### SDK使用示例
|
||||||
|
|
||||||
|
#### 窗体操作SDK
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 应用中使用窗体SDK的示例
|
||||||
|
interface WindowSDK {
|
||||||
|
// 设置窗体标题
|
||||||
|
setTitle(title: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 调整窗体尺寸
|
||||||
|
resize(width: number, height: number): Promise<boolean>
|
||||||
|
|
||||||
|
// 最小化窗体
|
||||||
|
minimize(): Promise<boolean>
|
||||||
|
|
||||||
|
// 最大化窗体
|
||||||
|
maximize(): Promise<boolean>
|
||||||
|
|
||||||
|
// 监听窗体状态变化
|
||||||
|
onStateChange(callback: (state: WindowState) => void): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储服务SDK
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 应用存储SDK接口定义
|
||||||
|
interface StorageSDK {
|
||||||
|
// 存储数据
|
||||||
|
set(key: string, value: any): Promise<boolean>
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
get(key: string): Promise<any>
|
||||||
|
|
||||||
|
// 删除数据
|
||||||
|
remove(key: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 清空存储
|
||||||
|
clear(): Promise<boolean>
|
||||||
|
|
||||||
|
// 监听存储变化
|
||||||
|
onChanged(callback: (key: string, newValue: any, oldValue: any) => void): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SDK权限控制机制
|
||||||
|
|
||||||
|
#### 权限申请流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App as 第三方应用
|
||||||
|
participant SDK as SDK桥接层
|
||||||
|
participant Security as 安全服务
|
||||||
|
participant User as 用户界面
|
||||||
|
participant System as 系统服务
|
||||||
|
|
||||||
|
App->>SDK: 调用需权限的API
|
||||||
|
SDK->>Security: 检查权限状态
|
||||||
|
|
||||||
|
alt 已授权
|
||||||
|
Security->>SDK: 返回已授权
|
||||||
|
SDK->>System: 调用系统服务
|
||||||
|
System->>SDK: 返回执行结果
|
||||||
|
SDK->>App: 返回结果
|
||||||
|
else 未授权
|
||||||
|
Security->>User: 显示权限申请弹窗
|
||||||
|
User->>Security: 用户确认/拒绝
|
||||||
|
alt 用户同意
|
||||||
|
Security->>SDK: 授权通过
|
||||||
|
SDK->>System: 调用系统服务
|
||||||
|
System->>SDK: 返回执行结果
|
||||||
|
SDK->>App: 返回结果
|
||||||
|
else 用户拒绝
|
||||||
|
Security->>SDK: 权限被拒绝
|
||||||
|
SDK->>App: 返回权限错误
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 应用生命周期管理
|
||||||
|
|
||||||
|
### 应用状态管理
|
||||||
|
|
||||||
|
应用在系统中具有完整的生命周期管理,确保资源的合理分配和回收。
|
||||||
|
|
||||||
|
#### 应用状态转换图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Installing: 安装应用
|
||||||
|
Installing --> Installed: 安装完成
|
||||||
|
Installing --> InstallFailed: 安装失败
|
||||||
|
InstallFailed --> [*]
|
||||||
|
|
||||||
|
Installed --> Starting: 启动应用
|
||||||
|
Starting --> Running: 运行中
|
||||||
|
Starting --> StartFailed: 启动失败
|
||||||
|
StartFailed --> Installed
|
||||||
|
|
||||||
|
Running --> Suspended: 挂起
|
||||||
|
Suspended --> Running: 恢复
|
||||||
|
Running --> Stopping: 停止
|
||||||
|
Stopping --> Installed: 已停止
|
||||||
|
|
||||||
|
Installed --> Uninstalling: 卸载应用
|
||||||
|
Uninstalling --> [*]: 卸载完成
|
||||||
|
```
|
||||||
|
|
||||||
|
### 应用资源管理
|
||||||
|
|
||||||
|
#### 内存管理策略
|
||||||
|
|
||||||
|
| 应用状态 | 内存策略 | 清理时机 | 恢复方式 |
|
||||||
|
| ---------- | -------------- | -------- | ---------- |
|
||||||
|
| Running | 完整内存保持 | 不清理 | 无需恢复 |
|
||||||
|
| Suspended | 压缩非关键数据 | 5分钟后 | 延迟加载 |
|
||||||
|
| Background | 最小化内存占用 | 立即清理 | 重新初始化 |
|
||||||
|
| Stopped | 完全释放内存 | 立即清理 | 完整重启 |
|
||||||
|
|
||||||
|
#### 性能监控指标
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "性能指标"
|
||||||
|
CPU[CPU使用率]
|
||||||
|
Memory[内存占用]
|
||||||
|
Network[网络请求]
|
||||||
|
Render[渲染性能]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "监控阈值"
|
||||||
|
CPULimit[CPU < 30%]
|
||||||
|
MemoryLimit[内存 < 100MB]
|
||||||
|
NetworkLimit[请求 < 10/s]
|
||||||
|
RenderLimit[FPS > 30]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "优化策略"
|
||||||
|
Throttle[请求节流]
|
||||||
|
Cache[智能缓存]
|
||||||
|
Lazy[懒加载]
|
||||||
|
Virtual[虚拟滚动]
|
||||||
|
end
|
||||||
|
|
||||||
|
CPU --> CPULimit
|
||||||
|
Memory --> MemoryLimit
|
||||||
|
Network --> NetworkLimit
|
||||||
|
Render --> RenderLimit
|
||||||
|
|
||||||
|
CPULimit --> Throttle
|
||||||
|
MemoryLimit --> Cache
|
||||||
|
NetworkLimit --> Throttle
|
||||||
|
RenderLimit --> Virtual
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流架构
|
||||||
|
|
||||||
|
### 系统数据流
|
||||||
|
|
||||||
|
系统采用单向数据流架构,确保数据的一致性和可追踪性。
|
||||||
|
|
||||||
|
#### 数据流向图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph "系统层"
|
||||||
|
SystemStore[系统状态存储]
|
||||||
|
SystemEvents[系统事件总线]
|
||||||
|
SystemServices[系统服务]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "应用层"
|
||||||
|
AppState[应用状态]
|
||||||
|
AppEvents[应用事件]
|
||||||
|
AppSDK[应用SDK]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "界面层"
|
||||||
|
SystemUI[系统界面]
|
||||||
|
AppUI[应用界面]
|
||||||
|
end
|
||||||
|
|
||||||
|
SystemStore --> SystemUI
|
||||||
|
SystemEvents --> SystemUI
|
||||||
|
SystemServices --> AppSDK
|
||||||
|
AppSDK --> AppState
|
||||||
|
AppState --> AppUI
|
||||||
|
AppEvents --> SystemEvents
|
||||||
|
|
||||||
|
SystemUI --> SystemEvents
|
||||||
|
AppUI --> AppEvents
|
||||||
|
```
|
||||||
|
|
||||||
|
### 跨应用数据共享
|
||||||
|
|
||||||
|
#### 数据共享权限模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "数据提供者"
|
||||||
|
ProviderApp[应用A]
|
||||||
|
DataExport[导出数据接口]
|
||||||
|
Permission[权限配置]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "系统中介"
|
||||||
|
DataBroker[数据代理服务]
|
||||||
|
PermissionCheck[权限验证]
|
||||||
|
DataTransform[数据转换]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "数据消费者"
|
||||||
|
ConsumerApp[应用B]
|
||||||
|
DataImport[导入数据接口]
|
||||||
|
DataValidation[数据验证]
|
||||||
|
end
|
||||||
|
|
||||||
|
ProviderApp --> DataExport
|
||||||
|
DataExport --> DataBroker
|
||||||
|
Permission --> PermissionCheck
|
||||||
|
DataBroker --> PermissionCheck
|
||||||
|
PermissionCheck --> DataTransform
|
||||||
|
DataTransform --> DataImport
|
||||||
|
DataImport --> ConsumerApp
|
||||||
|
DataImport --> DataValidation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署与运维
|
||||||
|
|
||||||
|
### 应用分发机制
|
||||||
|
|
||||||
|
系统支持多种应用分发方式,确保第三方应用的便捷接入。
|
||||||
|
|
||||||
|
#### 应用打包规范
|
||||||
|
|
||||||
|
| 文件类型 | 必需性 | 描述 | 示例 |
|
||||||
|
| ------------- | ------ | ------------ | -------------------------- |
|
||||||
|
| manifest.json | 必需 | 应用清单文件 | 包含应用基本信息、权限需求 |
|
||||||
|
| index.html | 必需 | 应用入口文件 | 应用主页面 |
|
||||||
|
| assets/ | 可选 | 静态资源目录 | CSS、图片、字体等 |
|
||||||
|
| sdk/ | 可选 | SDK依赖文件 | 如需离线使用SDK |
|
||||||
|
|
||||||
|
#### 应用安装流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Upload[上传应用包] --> Validate[验证应用包]
|
||||||
|
Validate --> Extract[解压应用文件]
|
||||||
|
Extract --> ScanSecurity[安全扫描]
|
||||||
|
ScanSecurity --> CheckPermission[权限检查]
|
||||||
|
CheckPermission --> RegisterApp[注册应用]
|
||||||
|
RegisterApp --> CreateSandbox[创建沙箱环境]
|
||||||
|
CreateSandbox --> InstallComplete[安装完成]
|
||||||
|
|
||||||
|
Validate -->|验证失败| InstallFailed[安装失败]
|
||||||
|
ScanSecurity -->|安全风险| InstallFailed
|
||||||
|
CheckPermission -->|权限过度| UserConfirm[用户确认]
|
||||||
|
UserConfirm -->|同意| RegisterApp
|
||||||
|
UserConfirm -->|拒绝| InstallFailed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 系统监控与日志
|
||||||
|
|
||||||
|
#### 监控体系架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "数据采集层"
|
||||||
|
AppMetrics[应用性能指标]
|
||||||
|
SystemMetrics[系统性能指标]
|
||||||
|
UserActions[用户行为数据]
|
||||||
|
ErrorLogs[错误日志]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "数据处理层"
|
||||||
|
DataAggregator[数据聚合器]
|
||||||
|
AlertEngine[告警引擎]
|
||||||
|
Analytics[分析引擎]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "展示层"
|
||||||
|
Dashboard[监控面板]
|
||||||
|
Reports[性能报告]
|
||||||
|
Alerts[告警通知]
|
||||||
|
end
|
||||||
|
|
||||||
|
AppMetrics --> DataAggregator
|
||||||
|
SystemMetrics --> DataAggregator
|
||||||
|
UserActions --> Analytics
|
||||||
|
ErrorLogs --> AlertEngine
|
||||||
|
|
||||||
|
DataAggregator --> Dashboard
|
||||||
|
Analytics --> Reports
|
||||||
|
AlertEngine --> Alerts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全机制
|
||||||
|
|
||||||
|
### 多层安全防护
|
||||||
|
|
||||||
|
系统采用多层安全防护机制,确保系统和应用的安全运行。
|
||||||
|
|
||||||
|
#### 安全防护体系
|
||||||
|
|
||||||
|
| 防护层级 | 防护措施 | 防护目标 |
|
||||||
|
| -------- | ------------------ | -------- |
|
||||||
|
| 网络层 | HTTPS强制、CSP策略 | 传输安全 |
|
||||||
|
| 应用层 | 沙箱隔离、权限控制 | 运行安全 |
|
||||||
|
| 数据层 | 加密存储、访问控制 | 数据安全 |
|
||||||
|
| 用户层 | 身份验证、操作审计 | 使用安全 |
|
||||||
|
|
||||||
|
### 权限管理系统
|
||||||
|
|
||||||
|
#### 权限分级模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "系统权限"
|
||||||
|
SysAdmin[系统管理员]
|
||||||
|
SysOperator[系统操作员]
|
||||||
|
SysUser[系统用户]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "应用权限"
|
||||||
|
AppOwner[应用所有者]
|
||||||
|
AppUser[应用用户]
|
||||||
|
AppGuest[应用访客]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "资源权限"
|
||||||
|
ReadOnly[只读权限]
|
||||||
|
ReadWrite[读写权限]
|
||||||
|
FullControl[完全控制]
|
||||||
|
end
|
||||||
|
|
||||||
|
SysAdmin --> FullControl
|
||||||
|
SysOperator --> ReadWrite
|
||||||
|
SysUser --> ReadOnly
|
||||||
|
|
||||||
|
AppOwner --> FullControl
|
||||||
|
AppUser --> ReadWrite
|
||||||
|
AppGuest --> ReadOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化策略
|
||||||
|
|
||||||
|
### 渲染性能优化
|
||||||
|
|
||||||
|
#### 虚拟化渲染机制
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
ViewportDetection[视口检测] --> VisibilityCheck{可见性检查}
|
||||||
|
VisibilityCheck -->|可见| RenderApp[渲染应用]
|
||||||
|
VisibilityCheck -->|不可见| SuspendRender[暂停渲染]
|
||||||
|
|
||||||
|
RenderApp --> FrameOptimization[帧率优化]
|
||||||
|
FrameOptimization --> RequestAnimationFrame[RAF调度]
|
||||||
|
RequestAnimationFrame --> RenderComplete[渲染完成]
|
||||||
|
|
||||||
|
SuspendRender --> MemoryCleanup[内存清理]
|
||||||
|
MemoryCleanup --> LowPowerMode[低功耗模式]
|
||||||
|
|
||||||
|
RenderComplete --> ViewportDetection
|
||||||
|
LowPowerMode --> ViewportDetection
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内存管理优化
|
||||||
|
|
||||||
|
#### 智能内存回收策略
|
||||||
|
|
||||||
|
| 回收时机 | 回收对象 | 回收策略 | 恢复方式 |
|
||||||
|
| ---------- | ------------- | ----------- | ------------ |
|
||||||
|
| 应用切换时 | 非活跃应用DOM | 延迟5秒回收 | 重新渲染 |
|
||||||
|
| 内存告警时 | 缓存数据 | LRU算法清理 | 重新请求 |
|
||||||
|
| 长时间空闲 | 应用实例 | 序列化存储 | 反序列化恢复 |
|
||||||
|
| 系统重启时 | 所有临时数据 | 立即清理 | 重新初始化 |
|
||||||
228
.qoder/quests/third-party-sdk-integration.md
Normal file
228
.qoder/quests/third-party-sdk-integration.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# 第三方SDK集成方案设计
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档旨在设计一种新的第三方SDK集成方案,使第三方应用无需通过iframe注入的方式即可直接调用系统服务。当前系统采用iframe注入SDK的方式为第三方应用提供系统服务访问接口,这种方式虽然有效,但在安全性、性能和开发体验方面存在一些限制。
|
||||||
|
|
||||||
|
新方案将采用全局对象共享的方式实现SDK功能,避免网络通信开销,同时保持与现有系统的兼容性。
|
||||||
|
|
||||||
|
## 设计目标
|
||||||
|
|
||||||
|
1. **安全性提升**:减少iframe注入带来的安全风险
|
||||||
|
2. **性能优化**:降低SDK注入的开销
|
||||||
|
3. **开发体验改善**:提供更简洁的SDK使用方式
|
||||||
|
4. **零网络通信**:完全避免网络请求,提升响应速度
|
||||||
|
5. **向后兼容**:保持现有应用的正常运行
|
||||||
|
|
||||||
|
## 当前实现分析
|
||||||
|
|
||||||
|
### 现有架构
|
||||||
|
|
||||||
|
当前系统通过在iframe中注入SDK脚本的方式为第三方应用提供服务访问接口:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[第三方应用] --> B[iframe沙箱]
|
||||||
|
B --> C[注入SDK脚本]
|
||||||
|
C --> D[postMessage通信]
|
||||||
|
D --> E[系统服务]
|
||||||
|
E --> D
|
||||||
|
D --> C
|
||||||
|
C --> B
|
||||||
|
B --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通信机制
|
||||||
|
|
||||||
|
系统使用`postMessage` API实现跨域通信:
|
||||||
|
|
||||||
|
1. **请求发送**:SDK通过`window.parent.postMessage`发送请求到系统
|
||||||
|
2. **请求处理**:系统监听`message`事件处理SDK调用
|
||||||
|
3. **响应返回**:系统通过`iframe.contentWindow.postMessage`发送响应
|
||||||
|
|
||||||
|
### 安全机制
|
||||||
|
|
||||||
|
1. **沙箱隔离**:使用iframe的`sandbox`属性限制应用权限
|
||||||
|
2. **权限控制**:基于应用ID的权限验证机制
|
||||||
|
3. **CSP策略**:内容安全策略限制脚本执行
|
||||||
|
|
||||||
|
## 新方案设计
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
|
||||||
|
新方案将采用全局对象预定义的方式,通过系统预置接口实现系统服务访问:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[第三方应用] --> B[全局对象接口]
|
||||||
|
B --> C[系统服务]
|
||||||
|
C --> B
|
||||||
|
B --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **全局对象接口**:在应用上下文中预置系统服务接口
|
||||||
|
2. **系统服务适配器**:将接口调用转换为内部服务调用
|
||||||
|
3. **权限验证模块**:负责应用权限控制
|
||||||
|
4. **上下文管理器**:管理系统与应用间的上下文隔离
|
||||||
|
|
||||||
|
### 接口设计
|
||||||
|
|
||||||
|
#### 全局对象结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 第三方应用中直接使用预置对象
|
||||||
|
window.SystemSDK.window.setTitle('新标题')
|
||||||
|
window.SystemSDK.storage.set('key', 'value')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 可用接口
|
||||||
|
|
||||||
|
- `window.SystemSDK.window` - 窗体管理接口
|
||||||
|
- `window.SystemSDK.storage` - 存储接口
|
||||||
|
- `window.SystemSDK.network` - 网络接口
|
||||||
|
- `window.SystemSDK.events` - 事件接口
|
||||||
|
- `window.SystemSDK.ui` - UI接口
|
||||||
|
- `window.SystemSDK.system` - 系统接口
|
||||||
|
|
||||||
|
### 安全机制
|
||||||
|
|
||||||
|
1. **上下文隔离**:确保应用只能访问预置接口
|
||||||
|
2. **权限验证**:基于应用ID的权限控制
|
||||||
|
3. **接口限制**:只暴露必要的系统服务接口
|
||||||
|
4. **调用审计**:记录所有SDK接口调用
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
### 第一阶段:全局对象接口开发
|
||||||
|
|
||||||
|
1. 提取现有SDK功能到全局对象接口
|
||||||
|
2. 实现全局对象接口替代postMessage通信
|
||||||
|
3. 添加权限验证和错误处理机制
|
||||||
|
4. 编写使用文档和示例
|
||||||
|
|
||||||
|
### 第二阶段:沙箱环境改造
|
||||||
|
|
||||||
|
1. 修改应用沙箱创建逻辑
|
||||||
|
2. 实现全局对象预置机制
|
||||||
|
3. 创建系统服务适配器
|
||||||
|
4. 实现权限验证模块
|
||||||
|
|
||||||
|
### 第三阶段:兼容性处理
|
||||||
|
|
||||||
|
1. 保持iframe注入机制向后兼容
|
||||||
|
2. 提供迁移工具和文档
|
||||||
|
3. 逐步引导第三方应用迁移到新方案
|
||||||
|
|
||||||
|
## API接口设计
|
||||||
|
|
||||||
|
### 全局对象接口
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
| ------------------ | --------------- |
|
||||||
|
| `window.SystemSDK` | 系统SDK全局对象 |
|
||||||
|
|
||||||
|
### 窗体管理接口
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
| --------------------------- | ------------ |
|
||||||
|
| `SystemSDK.window.setTitle` | 设置窗体标题 |
|
||||||
|
| `SystemSDK.window.resize` | 调整窗体尺寸 |
|
||||||
|
| `SystemSDK.window.move` | 移动窗体位置 |
|
||||||
|
| `SystemSDK.window.minimize` | 最小化窗体 |
|
||||||
|
| `SystemSDK.window.maximize` | 最大化窗体 |
|
||||||
|
| `SystemSDK.window.restore` | 还原窗体 |
|
||||||
|
| `SystemSDK.window.close` | 关闭窗体 |
|
||||||
|
|
||||||
|
### 存储接口
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
| -------------------------- | -------- |
|
||||||
|
| `SystemSDK.storage.set` | 存储数据 |
|
||||||
|
| `SystemSDK.storage.get` | 获取数据 |
|
||||||
|
| `SystemSDK.storage.remove` | 删除数据 |
|
||||||
|
| `SystemSDK.storage.clear` | 清空数据 |
|
||||||
|
|
||||||
|
### 网络接口
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
| ---------------------------- | ------------ |
|
||||||
|
| `SystemSDK.network.request` | 发送HTTP请求 |
|
||||||
|
| `SystemSDK.network.upload` | 上传文件 |
|
||||||
|
| `SystemSDK.network.download` | 下载文件 |
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 错误类型定义
|
||||||
|
|
||||||
|
| 错误类型 | 说明 |
|
||||||
|
| --------------------- | ------------ |
|
||||||
|
| UnauthorizedError | 未授权访问 |
|
||||||
|
| PermissionDeniedError | 权限不足 |
|
||||||
|
| TimeoutError | 调用超时 |
|
||||||
|
| InternalError | 系统内部错误 |
|
||||||
|
|
||||||
|
### 错误响应格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface APIResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
error?: string
|
||||||
|
code?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考量
|
||||||
|
|
||||||
|
1. **零网络延迟**:完全避免网络通信延迟
|
||||||
|
2. **直接调用**:通过全局对象直接访问系统服务
|
||||||
|
3. **内存共享**:在沙箱环境中共享内存数据
|
||||||
|
4. **异步处理**:非阻塞的接口调用机制
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
1. **沙箱隔离**:在受控环境中暴露SDK接口
|
||||||
|
2. **权限控制**:基于应用ID的细粒度权限管理
|
||||||
|
3. **接口限制**:只暴露必要的系统服务接口
|
||||||
|
4. **调用审计**:记录所有SDK接口调用
|
||||||
|
5. **输入验证**:严格验证所有输入参数
|
||||||
|
|
||||||
|
## 向后兼容性
|
||||||
|
|
||||||
|
1. 保留现有的iframe注入机制
|
||||||
|
2. 提供迁移指南和工具
|
||||||
|
3. 逐步废弃旧机制而非立即移除
|
||||||
|
4. 监控两种机制的使用情况
|
||||||
|
|
||||||
|
## 测试策略
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
1. SDK库功能测试
|
||||||
|
2. 认证服务测试
|
||||||
|
3. API网关测试
|
||||||
|
4. 权限验证测试
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
|
||||||
|
1. 端到端通信测试
|
||||||
|
2. 安全机制测试
|
||||||
|
3. 性能基准测试
|
||||||
|
4. 兼容性测试
|
||||||
|
|
||||||
|
## 部署方案
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
1. 全局对象接口开发
|
||||||
|
2. Mock服务模拟系统接口
|
||||||
|
3. 单元测试覆盖
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
|
||||||
|
1. 系统集成全局对象接口
|
||||||
|
2. API服务部署到云服务器
|
||||||
|
3. 配置负载均衡和SSL证书
|
||||||
|
4. 设置监控和告警机制
|
||||||
370
.qoder/quests/ui-modularization.md
Normal file
370
.qoder/quests/ui-modularization.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# UI模块化设计方案
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
本方案旨在将当前项目中的UI组件与核心业务逻辑进行彻底分离,实现UI层只负责数据渲染,核心层专注于数据处理的架构模式。通过模块化重构,提高代码的可维护性、可测试性和可扩展性。
|
||||||
|
|
||||||
|
### 1.1 设计目标
|
||||||
|
|
||||||
|
- 实现UI层与核心业务逻辑的完全解耦
|
||||||
|
- UI组件只负责接收数据并渲染,不包含任何业务逻辑
|
||||||
|
- 核心服务层提供统一的数据接口和状态管理
|
||||||
|
- 通过响应式数据绑定实现UI与数据的自动同步
|
||||||
|
|
||||||
|
### 1.2 核心原则
|
||||||
|
|
||||||
|
- **数据驱动**: UI组件通过props接收数据,通过事件触发操作
|
||||||
|
- **单一职责**: UI组件只负责渲染,核心服务只负责数据处理
|
||||||
|
- **响应式更新**: 数据变化自动触发UI更新
|
||||||
|
- **模块化设计**: 各模块职责清晰,依赖关系明确
|
||||||
|
|
||||||
|
### 1.3 项目现状分析
|
||||||
|
|
||||||
|
当前项目已经具备良好的分层架构:
|
||||||
|
|
||||||
|
- 使用Pinia进行状态管理
|
||||||
|
- 通过依赖注入提供系统服务
|
||||||
|
- UI组件与核心服务之间通过明确定义的接口交互
|
||||||
|
- 采用Vue 3 Composition API实现逻辑复用
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[UI层] --> B[状态管理层]
|
||||||
|
B --> C[核心服务层]
|
||||||
|
C --> D[数据源]
|
||||||
|
C --> E[外部API]
|
||||||
|
B --> A
|
||||||
|
|
||||||
|
subgraph UI层
|
||||||
|
A
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 核心层
|
||||||
|
B
|
||||||
|
C
|
||||||
|
D
|
||||||
|
E
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 现有架构分析
|
||||||
|
|
||||||
|
项目当前已具备良好的分层架构,但存在部分逻辑混合的情况:
|
||||||
|
|
||||||
|
1. **UI层**:包含Vue组件和部分业务逻辑
|
||||||
|
2. **状态管理层**:使用Pinia管理全局状态
|
||||||
|
3. **核心服务层**:SystemServiceIntegration统一管理系统服务
|
||||||
|
4. **依赖注入**:通过Vue的provide/inject机制传递系统服务
|
||||||
|
|
||||||
|
### 2.3 模块划分优化
|
||||||
|
|
||||||
|
| 模块 | 职责 | 包含内容 |
|
||||||
|
| ------------ | ---------------------- | ------------------------------ |
|
||||||
|
| UI组件模块 | 负责界面展示和用户交互 | 所有Vue组件、样式文件 |
|
||||||
|
| 状态管理模块 | 管理应用状态和数据流 | Pinia stores、响应式数据 |
|
||||||
|
| 核心服务模块 | 处理业务逻辑和数据操作 | 系统服务、应用管理、窗体管理等 |
|
||||||
|
| 工具模块 | 提供通用工具函数 | 工具函数、类型定义 |
|
||||||
|
|
||||||
|
## 3. UI模块重构方案
|
||||||
|
|
||||||
|
### 3.1 当前结构分析
|
||||||
|
|
||||||
|
当前项目UI相关代码分散在以下目录中:
|
||||||
|
|
||||||
|
- `src/ui/`: 主要UI组件
|
||||||
|
- `src/apps/`: 内置应用组件
|
||||||
|
- `src/common/`: 通用UI工具和组件
|
||||||
|
|
||||||
|
### 3.2 重构后的UI模块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── ui/
|
||||||
|
│ ├── components/ # 通用UI组件
|
||||||
|
│ ├── containers/ # 容器组件
|
||||||
|
│ ├── views/ # 页面视图组件
|
||||||
|
│ ├── composables/ # UI专用组合式函数
|
||||||
|
│ ├── styles/ # 样式文件
|
||||||
|
│ └── types/ # UI相关类型定义
|
||||||
|
├── core/ # 核心业务逻辑(从UI中抽离)
|
||||||
|
├── stores/ # 状态管理
|
||||||
|
└── services/ # 核心服务层
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 UI组件分类
|
||||||
|
|
||||||
|
#### 3.3.1 展示组件(Pure Components)
|
||||||
|
|
||||||
|
展示组件只负责接收数据并渲染,不包含任何业务逻辑:
|
||||||
|
|
||||||
|
| 组件名称 | 职责 | 输入数据 | 输出事件 |
|
||||||
|
| ----------- | ------------ | ---------------------- | ------------------------------- |
|
||||||
|
| AppIcon | 应用图标展示 | iconInfo, gridTemplate | dragStart, dragEnd, doubleClick |
|
||||||
|
| AppRenderer | 应用渲染器 | appId, windowId | loaded, error |
|
||||||
|
|
||||||
|
#### 3.3.2 容器组件(Container Components)
|
||||||
|
|
||||||
|
容器组件负责连接展示组件与状态管理:
|
||||||
|
|
||||||
|
| 组件名称 | 职责 | 连接状态 | 业务逻辑 |
|
||||||
|
| ---------------- | ---------- | ----------------------- | ------------------ |
|
||||||
|
| DesktopContainer | 桌面容器 | appIcons, systemStatus | 应用启动、状态更新 |
|
||||||
|
| WindowManager | 窗口管理器 | windows | 窗口创建、销毁 |
|
||||||
|
| AppRenderer | 应用渲染器 | appComponent, iframeUrl | 应用加载、错误处理 |
|
||||||
|
|
||||||
|
## 4. 数据流设计
|
||||||
|
|
||||||
|
### 4.1 数据流向图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[核心服务] --> B[状态管理]
|
||||||
|
B --> C[UI组件]
|
||||||
|
C --> D[用户操作]
|
||||||
|
D --> E[事件处理]
|
||||||
|
E --> A
|
||||||
|
|
||||||
|
style A fill:#cde4ff
|
||||||
|
style B fill:#ffd7c1
|
||||||
|
style C fill:#d3f8d3
|
||||||
|
style D fill:#fff2c1
|
||||||
|
style E fill:#e6d7ff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 状态管理设计
|
||||||
|
|
||||||
|
项目已使用Pinia进行状态管理,建议继续沿用并扩展:
|
||||||
|
|
||||||
|
#### 4.2.1 桌面状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DesktopState {
|
||||||
|
appIcons: IDesktopAppIcon[]
|
||||||
|
gridTemplate: IGridTemplateParams
|
||||||
|
systemStatus: SystemStatus
|
||||||
|
showSystemStatus: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.2 窗口状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WindowState {
|
||||||
|
windows: BuiltInWindow[]
|
||||||
|
activeWindowId: string | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 数据获取与更新流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as UI组件
|
||||||
|
participant S as 状态管理
|
||||||
|
participant C as 核心服务
|
||||||
|
|
||||||
|
U->>S: 读取状态数据
|
||||||
|
S-->>U: 返回响应式数据
|
||||||
|
U->>U: 渲染界面
|
||||||
|
|
||||||
|
U->>C: 触发用户操作
|
||||||
|
C->>S: 更新状态
|
||||||
|
S->>U: 自动更新界面
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 依赖注入机制
|
||||||
|
|
||||||
|
项目通过Vue的provide/inject机制实现服务注入:
|
||||||
|
|
||||||
|
1. 在main.ts中创建并提供SystemServiceIntegration实例
|
||||||
|
2. UI组件通过inject获取系统服务
|
||||||
|
3. 系统服务提供统一的API访问核心功能
|
||||||
|
|
||||||
|
## 5. 接口设计
|
||||||
|
|
||||||
|
### 5.1 UI组件接口规范
|
||||||
|
|
||||||
|
#### 5.1.1 展示组件接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// AppIcon组件接口
|
||||||
|
interface AppIconProps {
|
||||||
|
iconInfo: IDesktopAppIcon
|
||||||
|
gridTemplate: IGridTemplateParams
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppIconEvents {
|
||||||
|
(e: 'dragStart', event: DragEvent): void
|
||||||
|
(e: 'dragEnd', event: DragEvent): void
|
||||||
|
(e: 'doubleClick'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppRenderer组件接口
|
||||||
|
interface AppRendererProps {
|
||||||
|
appId: string
|
||||||
|
windowId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppRendererEvents {
|
||||||
|
(e: 'loaded'): void
|
||||||
|
(e: 'error', error: Error): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.2 容器组件接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DesktopContainer组件接口
|
||||||
|
interface DesktopContainerProps {
|
||||||
|
// 无需props,直接从状态管理获取数据
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopContainerMethods {
|
||||||
|
refreshApps(): Promise<void>
|
||||||
|
runApp(appId: string): Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 状态管理接口
|
||||||
|
|
||||||
|
#### 5.2.1 桌面状态接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DesktopStore {
|
||||||
|
// 状态
|
||||||
|
appIcons: Ref<IDesktopAppIcon[]>
|
||||||
|
gridTemplate: Ref<IGridTemplateParams>
|
||||||
|
systemStatus: Ref<SystemStatus>
|
||||||
|
showSystemStatus: Ref<boolean>
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
gridStyle: ComputedRef<GridStyle>
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
updateAppIcons(icons: IDesktopAppIcon[]): void
|
||||||
|
updateSystemStatus(status: SystemStatus): void
|
||||||
|
toggleSystemStatus(show: boolean): void
|
||||||
|
saveIconPositions(): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 实现方案
|
||||||
|
|
||||||
|
### 6.1 UI模块独立化步骤
|
||||||
|
|
||||||
|
1. **创建独立的UI模块目录结构**
|
||||||
|
- 将现有的UI组件按功能重新组织
|
||||||
|
- 分离展示组件和容器组件
|
||||||
|
|
||||||
|
2. **抽离业务逻辑到核心服务**
|
||||||
|
- 将应用启动逻辑移至SystemServiceIntegration
|
||||||
|
- 将状态管理移至Pinia stores
|
||||||
|
|
||||||
|
3. **重构组件数据流**
|
||||||
|
- 使用props传递数据
|
||||||
|
- 使用emit触发事件
|
||||||
|
- 通过状态管理实现响应式更新
|
||||||
|
|
||||||
|
### 6.2 现有组件优化
|
||||||
|
|
||||||
|
1. **DesktopContainer组件**
|
||||||
|
- 保持现有的inject机制获取系统服务
|
||||||
|
- 将应用启动逻辑完全委托给系统服务
|
||||||
|
- 通过响应式数据驱动UI更新
|
||||||
|
|
||||||
|
2. **AppIcon组件**
|
||||||
|
- 保持纯展示组件特性
|
||||||
|
- 通过事件与父组件通信
|
||||||
|
|
||||||
|
3. **WindowManager组件**
|
||||||
|
- 保持现有的窗口管理逻辑
|
||||||
|
- 通过inject获取系统服务
|
||||||
|
|
||||||
|
4. **AppRenderer组件**
|
||||||
|
- 负责应用渲染逻辑
|
||||||
|
- 支持内置应用和外部应用渲染
|
||||||
|
- 内置应用使用Vue组件直接渲染
|
||||||
|
- 外部应用使用iframe加载渲染
|
||||||
|
|
||||||
|
### 6.3 核心改造点
|
||||||
|
|
||||||
|
#### 6.3.1 DesktopContainer组件改造
|
||||||
|
|
||||||
|
当前的DesktopContainer组件包含了太多业务逻辑,需要进行以下优化:
|
||||||
|
|
||||||
|
1. **保持组件结构**:无需拆分为多个组件,但需明确职责划分
|
||||||
|
2. **业务逻辑委托**:将应用启动等业务逻辑完全委托给系统服务
|
||||||
|
3. **状态管理优化**:通过响应式数据驱动UI更新
|
||||||
|
|
||||||
|
#### 6.3.2 应用启动流程优化
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户双击图标] --> B[DesktopContainer组件]
|
||||||
|
B --> C[调用系统服务]
|
||||||
|
C --> D[SystemServiceIntegration]
|
||||||
|
D --> E[ApplicationLifecycleManager]
|
||||||
|
E --> F[创建应用实例]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3.3 子应用加载机制
|
||||||
|
|
||||||
|
项目需要支持两种类型的应用加载:
|
||||||
|
|
||||||
|
1. **内置应用加载**:
|
||||||
|
- 直接使用Vue 3组件加载机制
|
||||||
|
- 通过AppRegistry获取应用组件
|
||||||
|
- 无需异步加载,立即渲染
|
||||||
|
|
||||||
|
2. **外部应用加载**:
|
||||||
|
- 使用iframe沙箱环境加载
|
||||||
|
- 通过ExternalAppDiscovery发现应用
|
||||||
|
- 异步加载应用资源
|
||||||
|
- 外部加载逻辑可暂时留空,后期实现
|
||||||
|
|
||||||
|
### 6.4 依赖注入优化
|
||||||
|
|
||||||
|
项目已通过Vue的provide/inject机制实现服务注入,建议保持现有模式并进行优化:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts - 保持现有实现
|
||||||
|
app.provide('systemService', systemService)
|
||||||
|
|
||||||
|
// UI组件中使用 - 保持现有实现
|
||||||
|
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||||
|
|
||||||
|
// 建议添加类型安全检查
|
||||||
|
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||||
|
if (!systemService) {
|
||||||
|
throw new Error('系统服务未正确注入')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 测试策略
|
||||||
|
|
||||||
|
### 7.1 UI组件测试
|
||||||
|
|
||||||
|
- 展示组件:测试不同props输入下的渲染结果
|
||||||
|
- 容器组件:测试与状态管理和服务的交互
|
||||||
|
|
||||||
|
### 7.2 状态管理测试
|
||||||
|
|
||||||
|
- 测试状态更新的正确性
|
||||||
|
- 测试响应式数据的自动更新
|
||||||
|
|
||||||
|
### 7.3 集成测试
|
||||||
|
|
||||||
|
- 测试完整的数据流和用户操作流程
|
||||||
|
- 验证UI与核心服务的交互正确性
|
||||||
|
|
||||||
|
### 7.4 现有测试策略优化
|
||||||
|
|
||||||
|
项目已具备基本的测试框架,建议:
|
||||||
|
|
||||||
|
1. 增加对展示组件的单元测试
|
||||||
|
2. 完善容器组件与服务的集成测试
|
||||||
|
3. 建立端到端测试覆盖核心用户场景
|
||||||
314
.qoder/quests/window-drag-resize-control.md
Normal file
314
.qoder/quests/window-drag-resize-control.md
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
# 窗体功能增强设计文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
### 1.1 功能目标
|
||||||
|
|
||||||
|
1. 为项目中的窗体组件添加8个方向的拖拽调整尺寸功能,增强用户交互体验
|
||||||
|
2. 修复窗体最大化、最小化、还原功能存在的问题
|
||||||
|
3. 完善窗体状态管理和事件通知机制
|
||||||
|
|
||||||
|
### 1.2 当前系统分析
|
||||||
|
|
||||||
|
通过代码分析发现,当前系统已具备以下基础功能:
|
||||||
|
|
||||||
|
- 窗体创建、销毁、移动功能
|
||||||
|
- 窗体最大化、最小化、还原功能
|
||||||
|
- 基本的事件管理系统
|
||||||
|
- 窗体状态管理和配置
|
||||||
|
|
||||||
|
但现有功能存在以下问题:
|
||||||
|
|
||||||
|
1. 缺少用户通过鼠标拖拽调整窗体尺寸的功能
|
||||||
|
2. 窗体最大化、最小化、还原功能实现不完整,缺少与事件系统的完整集成
|
||||||
|
3. 窗体状态变更时未正确触发`windowFormDataUpdate`事件通知
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 整体架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户操作] --> B[窗体功能处理器]
|
||||||
|
B --> C[窗体状态管理]
|
||||||
|
C --> D[UI更新引擎]
|
||||||
|
D --> E[界面重渲染]
|
||||||
|
C --> F[事件通知系统]
|
||||||
|
|
||||||
|
G[窗体服务] --> C
|
||||||
|
H[事件管理器] --> F
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 核心组件
|
||||||
|
|
||||||
|
| 组件名称 | 职责 | 说明 |
|
||||||
|
| --------------------- | ------------------ | -------------------------------------- |
|
||||||
|
| WindowOperationHandler | 窗体操作处理核心 | 处理最大化、最小化、还原和拖拽调整尺寸操作 |
|
||||||
|
| WindowManager | 窗体状态管理 | 管理窗体状态变更和配置更新 |
|
||||||
|
| WindowRenderer | UI更新引擎 | 应用新状态到窗体界面 |
|
||||||
|
| WindowEventManager | 事件管理器 | 发布窗体状态变更事件 |
|
||||||
|
|
||||||
|
## 3. 功能设计
|
||||||
|
|
||||||
|
### 3.1 窗体状态操作
|
||||||
|
|
||||||
|
窗体支持以下状态操作:
|
||||||
|
|
||||||
|
| 操作 | 状态变更 | 事件触发 | 说明 |
|
||||||
|
| ------ | ---------------------------- | ------------------------------------------ | ------------------------------------------ |
|
||||||
|
| 最大化 | default/minimized → maximized | windowFormMaximize, windowFormDataUpdate | 窗体占据除任务栏外的整个屏幕空间 |
|
||||||
|
| 最小化 | default/maximized → minimized | windowFormMinimize | 窗体隐藏,仅在任务栏保留图标 |
|
||||||
|
| 还原 | minimized/maximized → default | windowFormRestore, windowFormDataUpdate | 窗体恢复到正常尺寸和位置 |
|
||||||
|
|
||||||
|
### 3.2 拖拽调整尺寸方向定义
|
||||||
|
|
||||||
|
窗体支持8个方向的拖拽调整尺寸,具体如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 窗体
|
||||||
|
direction LR
|
||||||
|
A[↖] --- B[↑] --- C[↗]
|
||||||
|
D[←] --- E[窗体内容] --- F[→]
|
||||||
|
G[↙] --- H[↓] --- I[↘]
|
||||||
|
end
|
||||||
|
|
||||||
|
classDef corner fill:#f9f,stroke:#333;
|
||||||
|
classDef edge fill:#bbf,stroke:#333;
|
||||||
|
|
||||||
|
class A,C,G,I,corner
|
||||||
|
class B,D,F,H,edge
|
||||||
|
```
|
||||||
|
|
||||||
|
| 方向 | 标识符 | 影响属性 | 说明 |
|
||||||
|
| ------ | ----------- | ------------------- | ------------------ |
|
||||||
|
| 左上角 | topLeft | width, height, x, y | 同时调整宽高和位置 |
|
||||||
|
| 上边缘 | top | height, y | 调整高度和垂直位置 |
|
||||||
|
| 右上角 | topRight | width, height, y | 调整宽高和垂直位置 |
|
||||||
|
| 右边缘 | right | width | 仅调整宽度 |
|
||||||
|
| 右下角 | bottomRight | width, height | 仅调整宽高 |
|
||||||
|
| 下边缘 | bottom | height | 仅调整高度 |
|
||||||
|
| 左下角 | bottomLeft | width, height, x | 调整宽高和水平位置 |
|
||||||
|
| 左边缘 | left | width, x | 调整宽度和水平位置 |
|
||||||
|
|
||||||
|
### 3.3 交互流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as 用户
|
||||||
|
participant WH as 窗体句柄
|
||||||
|
participant WOH as 操作处理器
|
||||||
|
participant WM as 窗体管理器
|
||||||
|
participant WR as 窗体渲染器
|
||||||
|
participant EM as 事件管理器
|
||||||
|
|
||||||
|
U->>WH: 执行操作(最大化/最小化/还原/拖拽)
|
||||||
|
WH->>WOH: 处理操作请求
|
||||||
|
WOH->>WM: 更新窗体状态
|
||||||
|
WM->>WR: 应用新状态
|
||||||
|
WM->>EM: 发布状态变更事件
|
||||||
|
EM->>EM: 发布windowFormDataUpdate事件
|
||||||
|
WR->>U: 更新界面显示
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据模型
|
||||||
|
|
||||||
|
### 4.1 窗体状态数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IWindowFormDataUpdateParams {
|
||||||
|
/** 窗口id */
|
||||||
|
id: string;
|
||||||
|
/** 窗口状态 */
|
||||||
|
state: TWindowFormState;
|
||||||
|
/** 窗口宽度 */
|
||||||
|
width: number;
|
||||||
|
/** 窗口高度 */
|
||||||
|
height: number;
|
||||||
|
/** 窗口x坐标(左上角) */
|
||||||
|
x: number;
|
||||||
|
/** 窗口y坐标(左上角) */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 窗体尺寸配置扩展
|
||||||
|
|
||||||
|
在现有`WindowConfig`接口基础上增加最小/最大尺寸限制:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --------- | ------- | ---- | -------------------------- |
|
||||||
|
| minWidth | number | 可选 | 窗体最小宽度(像素) |
|
||||||
|
| minHeight | number | 可选 | 窗体最小高度(像素) |
|
||||||
|
| maxWidth | number | 可选 | 窗体最大宽度(像素) |
|
||||||
|
| maxHeight | number | 可选 | 窗体最大高度(像素) |
|
||||||
|
| resizable | boolean | 可选 | 是否可调整尺寸,默认为true |
|
||||||
|
|
||||||
|
### 4.3 拖拽状态数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ResizeState {
|
||||||
|
/** 是否正在调整尺寸 */
|
||||||
|
isResizing: boolean;
|
||||||
|
/** 调整方向 */
|
||||||
|
direction: ResizeDirection;
|
||||||
|
/** 起始鼠标位置 */
|
||||||
|
startX: number;
|
||||||
|
/** 起始鼠标位置 */
|
||||||
|
startY: number;
|
||||||
|
/** 起始窗体宽度 */
|
||||||
|
startWidth: number;
|
||||||
|
/** 起始窗体高度 */
|
||||||
|
startHeight: number;
|
||||||
|
/** 起始窗体X坐标 */
|
||||||
|
startXPosition: number;
|
||||||
|
/** 起始窗体Y坐标 */
|
||||||
|
startYPosition: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResizeDirection =
|
||||||
|
'topLeft' | 'top' | 'topRight' |
|
||||||
|
'right' | 'bottomRight' | 'bottom' |
|
||||||
|
'bottomLeft' | 'left' | 'none';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 事件系统设计
|
||||||
|
|
||||||
|
### 5.1 修复事件触发问题
|
||||||
|
|
||||||
|
当前窗体状态变更时未正确触发`windowFormDataUpdate`事件,需要修复以下问题:
|
||||||
|
|
||||||
|
1. 在最大化操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态和尺寸信息
|
||||||
|
2. 在最小化操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态信息
|
||||||
|
3. 在还原操作完成后,应触发`windowFormDataUpdate`事件,携带窗体新状态和尺寸信息
|
||||||
|
|
||||||
|
### 5.2 新增事件定义
|
||||||
|
|
||||||
|
在现有`IWindowFormEvent`接口中添加以下事件:
|
||||||
|
|
||||||
|
| 事件名称 | 参数类型 | 触发时机 |
|
||||||
|
| --------------------- | --------------------------- | ---------------- |
|
||||||
|
| windowFormResizeStart | string | 开始调整窗体尺寸 |
|
||||||
|
| windowFormResizing | IWindowFormDataUpdateParams | 调整尺寸过程中 |
|
||||||
|
| windowFormResizeEnd | string | 完成窗体尺寸调整 |
|
||||||
|
|
||||||
|
### 5.3 事件触发流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[窗体状态变更] --> B{是否需要更新数据?}
|
||||||
|
B -->|是| C[构造更新数据]
|
||||||
|
C --> D[触发windowFormDataUpdate事件]
|
||||||
|
B -->|否| E[直接触发状态事件]
|
||||||
|
D --> F[通知监听组件]
|
||||||
|
E --> F
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 核心逻辑设计
|
||||||
|
|
||||||
|
### 6.1 窗体状态变更逻辑修复
|
||||||
|
|
||||||
|
当前窗体状态变更逻辑存在以下问题需要修复:
|
||||||
|
|
||||||
|
1. **最大化逻辑问题**:
|
||||||
|
- 未正确保存原始窗体尺寸和位置信息
|
||||||
|
- 未触发`windowFormDataUpdate`事件通知
|
||||||
|
|
||||||
|
2. **最小化逻辑问题**:
|
||||||
|
- 未触发`windowFormDataUpdate`事件通知
|
||||||
|
|
||||||
|
3. **还原逻辑问题**:
|
||||||
|
- 未正确恢复窗体尺寸和位置
|
||||||
|
- 未触发`windowFormDataUpdate`事件通知
|
||||||
|
|
||||||
|
### 6.2 边缘检测算法
|
||||||
|
|
||||||
|
窗体边缘需要划分8个可拖拽区域,每个区域宽度为8px:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 边缘区域划分
|
||||||
|
direction LR
|
||||||
|
A[8px↖] --- B[8px↑] --- C[8px↗]
|
||||||
|
D[8px←] --- E[窗体内容] --- F[8px→]
|
||||||
|
G[8px↙] --- H[8px↓] --- I[8px↘]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 尺寸计算规则
|
||||||
|
|
||||||
|
1. **最小尺寸限制**:窗体宽度不能小于minWidth,高度不能小于minHeight
|
||||||
|
2. **最大尺寸限制**:窗体宽度不能大于maxWidth,高度不能大于maxHeight
|
||||||
|
3. **位置边界**:窗体不能被拖拽到屏幕外
|
||||||
|
|
||||||
|
### 6.4 拖拽处理流程
|
||||||
|
|
||||||
|
1. 鼠标按下时检测是否在边缘区域
|
||||||
|
2. 根据鼠标位置确定拖拽方向
|
||||||
|
3. 记录初始状态数据
|
||||||
|
4. 鼠标移动时实时计算新尺寸
|
||||||
|
5. 应用新尺寸并触发重渲染
|
||||||
|
6. 鼠标释放时结束拖拽状态
|
||||||
|
|
||||||
|
## 7. UI/UX设计
|
||||||
|
|
||||||
|
### 7.1 视觉反馈
|
||||||
|
|
||||||
|
- 鼠标悬停在可拖拽边缘时,光标应变为对应方向的调整光标
|
||||||
|
- 拖拽过程中窗体应有半透明遮罩效果
|
||||||
|
- 拖拽完成后应有平滑的过渡动画
|
||||||
|
|
||||||
|
### 7.2 响应式适配
|
||||||
|
|
||||||
|
- 在不同屏幕分辨率下保持边缘区域的可点击性
|
||||||
|
- 支持触屏设备的拖拽操作
|
||||||
|
- 在窗体尺寸接近最小/最大限制时提供视觉提示
|
||||||
|
|
||||||
|
## 8. 性能优化
|
||||||
|
|
||||||
|
### 8.1 事件处理优化
|
||||||
|
|
||||||
|
- 使用防抖机制减少高频事件触发
|
||||||
|
- 在拖拽过程中暂停不必要的重渲染
|
||||||
|
- 使用requestAnimationFrame优化动画性能
|
||||||
|
|
||||||
|
### 8.2 内存管理
|
||||||
|
|
||||||
|
- 及时清理事件监听器
|
||||||
|
- 复用计算对象避免频繁创建
|
||||||
|
- 在窗体销毁时清理所有相关资源
|
||||||
|
|
||||||
|
## 9. 安全考虑
|
||||||
|
|
||||||
|
### 9.1 边界检查
|
||||||
|
|
||||||
|
- 确保窗体不能被调整到超出屏幕边界
|
||||||
|
- 验证尺寸参数的有效性
|
||||||
|
- 防止负值或异常大值的输入
|
||||||
|
|
||||||
|
### 9.2 权限控制
|
||||||
|
|
||||||
|
- 只有具有调整权限的窗体才能被调整尺寸
|
||||||
|
- 外部应用的窗体尺寸调整需通过SDK接口
|
||||||
|
|
||||||
|
## 10. 测试策略
|
||||||
|
|
||||||
|
### 10.1 单元测试
|
||||||
|
|
||||||
|
- 边缘检测算法准确性测试
|
||||||
|
- 尺寸计算边界条件测试
|
||||||
|
- 事件发布/订阅机制测试
|
||||||
|
|
||||||
|
### 10.2 集成测试
|
||||||
|
|
||||||
|
- 不同方向拖拽功能测试
|
||||||
|
- 尺寸限制功能测试
|
||||||
|
- 与现有窗体功能集成测试
|
||||||
|
|
||||||
|
### 10.3 用户体验测试
|
||||||
|
|
||||||
|
- 拖拽流畅性测试
|
||||||
|
- 视觉反馈效果测试
|
||||||
|
- 不同设备兼容性测试
|
||||||
204
.qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md
Normal file
204
.qoder/repowiki/zh/content/UI组件体系/AppIcon组件.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# AppIcon组件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心数据结构](#核心数据结构)
|
||||||
|
3. [拖拽交互机制](#拖拽交互机制)
|
||||||
|
4. [网格定位原理](#网格定位原理)
|
||||||
|
5. [事件过滤逻辑](#事件过滤逻辑)
|
||||||
|
6. [容器响应式布局](#容器响应式布局)
|
||||||
|
7. [图标重排策略](#图标重排策略)
|
||||||
|
8. [持久化存储](#持久化存储)
|
||||||
|
9. [扩展与最佳实践](#扩展与最佳实践)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
`AppIcon` 组件是桌面应用系统中的可交互图标单元,实现了基于HTML5 Drag API的拖拽功能。该组件通过精确的坐标计算和网格系统集成,允许用户将图标重新定位到桌面容器的任意有效网格位置。本文档深入解析其技术实现细节,涵盖从拖拽事件处理、坐标换算、网格定位到状态持久化的完整流程。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
## 核心数据结构
|
||||||
|
|
||||||
|
### IDesktopAppIcon 接口
|
||||||
|
定义了桌面图标的元数据结构,包含名称、图标资源、启动路径及在网格布局中的位置坐标。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IDesktopAppIcon {
|
||||||
|
name: string; // 图标显示名称
|
||||||
|
icon: string; // 图标资源路径或标识符
|
||||||
|
path: string; // 关联的应用程序启动路径
|
||||||
|
x: number; // 在grid布局中的列索引(从1开始)
|
||||||
|
y: number; // 在grid布局中的行索引(从1开始)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
该设计意图明确:`x` 和 `y` 字段直接映射到CSS Grid的`grid-column`和`grid-row`属性,实现了数据驱动的UI布局。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14)
|
||||||
|
|
||||||
|
## 拖拽交互机制
|
||||||
|
|
||||||
|
`AppIcon` 组件利用原生HTML5 Drag API实现拖拽功能:
|
||||||
|
|
||||||
|
1. **启用拖拽**:通过设置 `draggable="true"` 属性激活元素的拖拽能力。
|
||||||
|
2. **事件监听**:
|
||||||
|
- `@dragstart`:拖拽开始时触发,当前实现为空函数,保留未来扩展空间。
|
||||||
|
- `@dragend`:拖拽结束时触发,执行核心的坐标计算与位置更新逻辑。
|
||||||
|
|
||||||
|
此机制无需依赖第三方库,轻量且兼容性好。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L2-L8)
|
||||||
|
|
||||||
|
## 网格定位原理
|
||||||
|
|
||||||
|
### 动态样式绑定
|
||||||
|
组件通过内联样式动态设置其在CSS Grid中的位置:
|
||||||
|
```vue
|
||||||
|
:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1}; grid-row: ${iconInfo.y}/${iconInfo.y + 1};`"
|
||||||
|
```
|
||||||
|
此表达式将 `iconInfo.x` 和 `iconInfo.y` 的值转换为Grid的列/行跨度声明,确保图标精准占据一个网格单元。
|
||||||
|
|
||||||
|
### 坐标换算过程
|
||||||
|
`onDragEnd` 事件处理器的核心任务是将鼠标绝对坐标转换为相对网格坐标:
|
||||||
|
|
||||||
|
1. **获取容器边界**:使用 `getBoundingClientRect()` 获取父容器的绝对位置和尺寸。
|
||||||
|
2. **计算相对坐标**:通过 `e.clientX - rect.left` 和 `e.clientY - rect.top` 得到鼠标相对于容器左上角的偏移量。
|
||||||
|
3. **确定目标网格**:利用 `cellRealWidth` 和 `cellRealHeight` 进行除法运算并向上取整(`Math.ceil`),得到目标网格的行列索引。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[拖拽结束] --> B{获取鼠标<br/>clientX/clientY}
|
||||||
|
B --> C{获取容器<br/>getBoundingClientRect}
|
||||||
|
C --> D[计算鼠标相对<br/>容器坐标]
|
||||||
|
D --> E[用cellRealWidth/Height<br/>计算网格索引]
|
||||||
|
E --> F[更新iconInfo.x/y]
|
||||||
|
F --> G[触发UI重渲染]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L10-L38)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L10-L38)
|
||||||
|
|
||||||
|
## 事件过滤逻辑
|
||||||
|
|
||||||
|
为了防止图标被错误地放置在其他图标之上,`onDragEnd` 方法实现了关键的事件过滤:
|
||||||
|
|
||||||
|
1. 使用 `document.elementFromPoint(e.clientX, e.clientY)` 检测鼠标终点位置的DOM元素。
|
||||||
|
2. 如果该元素是另一个 `.icon-container`,则立即返回,不执行任何位置更新。
|
||||||
|
|
||||||
|
此逻辑确保了“空位投放”原则,提升了用户体验,避免了图标的视觉重叠。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L13-L17)
|
||||||
|
|
||||||
|
## 容器响应式布局
|
||||||
|
|
||||||
|
### IGridTemplateParams 接口
|
||||||
|
该接口定义了网格系统的动态参数,是实现响应式布局的关键。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IGridTemplateParams {
|
||||||
|
cellExpectWidth: number; // 单元格预设宽度
|
||||||
|
cellExpectHeight: number; // 单元格预设高度
|
||||||
|
cellRealWidth: number; // 单元格实际宽度
|
||||||
|
cellRealHeight: number; // 单元格实际高度
|
||||||
|
gapX: number; // 列间距
|
||||||
|
gapY: number; // 行间距
|
||||||
|
colCount: number; // 总列数
|
||||||
|
rowCount: number; // 总行数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实际尺寸计算
|
||||||
|
`useDesktopContainerInit` 函数通过 `ResizeObserver` 监听容器尺寸变化,并动态计算 `cellRealWidth` 和 `cellRealHeight`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1));
|
||||||
|
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2));
|
||||||
|
```
|
||||||
|
|
||||||
|
此计算考虑了所有列间距的总和,确保了网格单元的实际尺寸能完美填充容器,无边距误差。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L3-L20)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L30-L38)
|
||||||
|
|
||||||
|
## 图标重排策略
|
||||||
|
|
||||||
|
当网格的行列数发生变化时,`rearrangeIcons` 函数负责智能地重新分配图标位置:
|
||||||
|
|
||||||
|
1. **优先保留原位**:遍历所有图标,若其原位置在新网格范围内且未被占用,则保留在原位。
|
||||||
|
2. **寻找空位**:对于超出范围或位置冲突的图标,从 `(1,1)` 开始扫描,为其寻找第一个可用的空网格。
|
||||||
|
3. **隐藏溢出图标**:如果所有网格均被占用,则将无法安置的图标放入 `hideAppIcons` 数组。
|
||||||
|
|
||||||
|
此策略保证了用户自定义布局的最大程度保留,同时优雅地处理了空间不足的情况。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant DC as DesktopContainer
|
||||||
|
participant UDCI as useDesktopContainerInit
|
||||||
|
participant RI as rearrangeIcons
|
||||||
|
DC->>UDCI : 监听gridTemplate变化
|
||||||
|
UDCI->>RI : 调用rearrangeIcons()
|
||||||
|
loop 遍历每个图标
|
||||||
|
RI->>RI : 检查是否在新网格内且位置空闲
|
||||||
|
alt 是
|
||||||
|
RI->>RI : 保留在原位
|
||||||
|
else 否
|
||||||
|
RI->>RI : 加入临时队列
|
||||||
|
end
|
||||||
|
end
|
||||||
|
loop 处理临时队列
|
||||||
|
RI->>RI : 扫描网格寻找空位
|
||||||
|
alt 找到空位
|
||||||
|
RI->>RI : 分配新位置
|
||||||
|
else 无空位
|
||||||
|
RI->>RI : 加入hideAppIcons
|
||||||
|
end
|
||||||
|
end
|
||||||
|
RI-->>UDCI : 返回appIcons和hideAppIcons
|
||||||
|
UDCI->>DC : 更新appIconsRef
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L77-L80)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L77-L80)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
## 持久化存储
|
||||||
|
|
||||||
|
用户的图标布局偏好通过 `localStorage` 实现持久化:
|
||||||
|
|
||||||
|
1. **写入**:使用 `watch` 监听 `appIconsRef.value` 的变化,一旦有更新,立即将整个数组序列化为JSON字符串并存入 `localStorage` 键名为 `'desktopAppIconInfo'` 的条目中。
|
||||||
|
2. **读取**:在初始化时,尝试从 `localStorage` 中读取 `'desktopAppIconInfo'`,若存在则使用存储的位置信息覆盖默认布局。
|
||||||
|
|
||||||
|
这确保了用户关闭页面后再次打开时,仍能看到上次调整好的桌面布局。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L88-L90)
|
||||||
|
|
||||||
|
## 扩展与最佳实践
|
||||||
|
|
||||||
|
### 自定义图标样式
|
||||||
|
可通过修改 `<style scoped>` 中的 `.icon-container` 类来定制图标外观,例如添加背景图片、阴影或动画效果。
|
||||||
|
|
||||||
|
### 扩展拖拽行为
|
||||||
|
可在 `onDragStart` 回调中添加逻辑,如设置拖拽图像 (`e.dataTransfer.setDragImage`) 或传输自定义数据。
|
||||||
|
|
||||||
|
### 避免跨容器拖拽冲突
|
||||||
|
**最佳方案**:为每个可拖拽区域(容器)维护独立的 `gridTemplate` 状态和图标集合。在 `onDragEnd` 事件中,首先确认 `pointTarget` 是否属于当前容器的 `.desktop-icons-container`,如果不是,则忽略此次拖拽操作,防止图标被错误地投放到其他容器中。
|
||||||
309
.qoder/repowiki/zh/content/UI组件体系/DesktopContainer组件.md
Normal file
309
.qoder/repowiki/zh/content/UI组件体系/DesktopContainer组件.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# DesktopContainer组件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [App.vue](file://src/ui/App.vue)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心功能分析](#核心功能分析)
|
||||||
|
3. [响应式网格布局初始化](#响应式网格布局初始化)
|
||||||
|
4. [ResizeObserver尺寸监听机制](#resizeobserver尺寸监听机制)
|
||||||
|
5. [应用图标状态管理与持久化](#应用图标状态管理与持久化)
|
||||||
|
6. [模板渲染与事件处理](#模板渲染与事件处理)
|
||||||
|
7. [与父组件的数据流关系](#与父组件的数据流关系)
|
||||||
|
8. [自定义容器集成示例](#自定义容器集成示例)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
`DesktopContainer` 是 Vue 桌面应用的核心容器组件,负责管理桌面图标的布局、状态和交互。该组件通过组合式函数 `useDesktopContainerInit` 实现了动态响应式网格系统,并结合 `localStorage` 提供图标位置的持久化存储能力。作为桌面环境的主视图容器,它与 `App.vue` 父组件构成清晰的数据流结构,为上层应用提供稳定可靠的桌面管理服务。
|
||||||
|
|
||||||
|
## 核心功能分析
|
||||||
|
|
||||||
|
`DesktopContainer` 组件承担着桌面环境的核心职责,主要包括:
|
||||||
|
- 初始化并维护一个基于 CSS Grid 的响应式布局系统
|
||||||
|
- 动态计算网格行列数及单元格实际尺寸以适应容器变化
|
||||||
|
- 管理所有桌面应用图标的元数据及其在网格中的坐标位置
|
||||||
|
- 通过本地存储实现用户自定义图标准置的持久化
|
||||||
|
- 提供标准化的应用启动接口(双击事件)
|
||||||
|
- 支持拖拽重排功能并与子组件 `AppIcon` 协同工作
|
||||||
|
|
||||||
|
该组件的设计体现了关注点分离原则,将复杂的布局逻辑封装在独立的组合式函数中,保持了模板的简洁性和可维护性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 响应式网格布局初始化
|
||||||
|
|
||||||
|
### useDesktopContainerInit组合式函数
|
||||||
|
|
||||||
|
`useDesktopContainerInit` 函数是整个桌面布局系统的核心引擎,接收一个 CSS 选择器字符串 `containerStr` 作为参数,用于定位需要监控尺寸变化的 DOM 容器元素。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IGridTemplateParams {
|
||||||
|
+cellExpectWidth : number
|
||||||
|
+cellExpectHeight : number
|
||||||
|
+cellRealWidth : number
|
||||||
|
+cellRealHeight : number
|
||||||
|
+gapX : number
|
||||||
|
+gapY : number
|
||||||
|
+colCount : number
|
||||||
|
+rowCount : number
|
||||||
|
}
|
||||||
|
class useDesktopContainerInit {
|
||||||
|
-container : HTMLElement
|
||||||
|
-gridTemplate : IGridTemplateParams
|
||||||
|
-ro : ResizeObserver
|
||||||
|
-appIconsRef : Ref~Array~
|
||||||
|
-exceedApp : Ref~Array~
|
||||||
|
+return gridStyle : ComputedRef
|
||||||
|
}
|
||||||
|
useDesktopContainerInit --> IGridTemplateParams : "uses"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L1-L20)
|
||||||
|
|
||||||
|
### gridTemplate参数计算逻辑
|
||||||
|
|
||||||
|
`gridTemplate` 对象采用 `reactive` 声明为响应式数据,包含以下关键属性:
|
||||||
|
- `cellExpectWidth` 和 `cellExpectHeight`:单元格期望尺寸(默认90x110px)
|
||||||
|
- `gapX` 和 `gapY`:行列间距(默认4px)
|
||||||
|
- `colCount` 和 `rowCount`:动态计算的总行列数
|
||||||
|
|
||||||
|
初始状态下,行列数设为1,随后由 `ResizeObserver` 根据容器实际尺寸重新计算。
|
||||||
|
|
||||||
|
### gridStyle动态生成机制
|
||||||
|
|
||||||
|
通过 `computed` 属性 `gridStyle` 动态生成应用于容器的内联样式:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始计算]) --> CalcColumns["构建 gridTemplateColumns<br/>repeat(colCount, minmax(cellExpectWidth + 'px', 1fr))"]
|
||||||
|
CalcColumns --> CalcRows["构建 gridTemplateRows<br/>repeat(rowCount, minmax(cellExpectHeight + 'px', 1fr))"]
|
||||||
|
CalcRows --> SetGap["设置 gap: gapY + 'px' gapX + 'px'"]
|
||||||
|
SetGap --> ReturnStyle["返回样式对象"]
|
||||||
|
ReturnStyle --> End([完成])
|
||||||
|
```
|
||||||
|
|
||||||
|
此计算属性确保了每当 `gridTemplate` 中的任何字段发生变化时,都能立即生成正确的 CSS Grid 样式规则。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## ResizeObserver尺寸监听机制
|
||||||
|
|
||||||
|
### 尺寸监听流程
|
||||||
|
|
||||||
|
`ResizeObserver` 被用来监听传入选择器所匹配容器的尺寸变化,其回调函数执行以下步骤:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant RO as ResizeObserver
|
||||||
|
participant CT as Container
|
||||||
|
participant GT as gridTemplate
|
||||||
|
RO->>CT : getBoundingClientRect()
|
||||||
|
CT-->>RO : 返回容器矩形信息
|
||||||
|
RO->>GT : 计算 colCount
|
||||||
|
RO->>GT : 计算 rowCount
|
||||||
|
RO->>GT : 计算 cellRealWidth
|
||||||
|
RO->>GT : 计算 cellRealHeight
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L38-L52)
|
||||||
|
|
||||||
|
### 行列数计算公式
|
||||||
|
|
||||||
|
根据容器当前宽度和高度,使用如下数学公式计算最优行列分布:
|
||||||
|
|
||||||
|
```
|
||||||
|
colCount = floor((width + gapX) / (cellExpectWidth + gapX))
|
||||||
|
rowCount = floor((height + gapY) / (cellExpectHeight + gapY))
|
||||||
|
```
|
||||||
|
|
||||||
|
这种算法确保即使在存在间隙的情况下也能最大化利用可用空间。
|
||||||
|
|
||||||
|
### 实际单元格尺寸调整
|
||||||
|
|
||||||
|
考虑到间隙对总可用空间的影响,实际单元格尺寸通过以下方式精确计算:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const w = containerRect.width - (gapX * (colCount - 1))
|
||||||
|
const h = containerRect.height - (gapY * (rowCount - 1))
|
||||||
|
cellRealWidth = w / colCount
|
||||||
|
cellRealHeight = h / rowCount
|
||||||
|
```
|
||||||
|
|
||||||
|
最终结果保留两位小数,保证视觉上的平滑过渡。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L38-L52)
|
||||||
|
|
||||||
|
## 应用图标状态管理与持久化
|
||||||
|
|
||||||
|
### appIconsRef状态管理
|
||||||
|
|
||||||
|
`appIconsRef` 是一个 `ref` 类型的响应式数组,存储所有桌面应用图标的配置信息。每个图标对象遵循 `IDesktopAppIcon` 接口规范:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
IDesktopAppIcon {
|
||||||
|
string name PK
|
||||||
|
string icon
|
||||||
|
string path
|
||||||
|
int x
|
||||||
|
int y
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L1-L15)
|
||||||
|
|
||||||
|
初始图标数据来源于两个渠道:
|
||||||
|
1. 当前运行的应用进程列表(模拟为空数组)
|
||||||
|
2. `localStorage` 中保存的历史图标位置信息
|
||||||
|
|
||||||
|
系统优先使用历史记录中的坐标,若无则按顺序自动分配。
|
||||||
|
|
||||||
|
### localStorage持久化机制
|
||||||
|
|
||||||
|
通过 `watch` 监听器实现自动持久化:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[appIconsRef变化] --> B{触发watch}
|
||||||
|
B --> C[序列化为JSON字符串]
|
||||||
|
C --> D[存入localStorage]
|
||||||
|
D --> E[键名为'desktopAppIconInfo']
|
||||||
|
```
|
||||||
|
|
||||||
|
同时,在初始化时从 `localStorage` 读取已有数据,实现跨会话的状态恢复。
|
||||||
|
|
||||||
|
### 图标重排逻辑
|
||||||
|
|
||||||
|
当窗口大小导致网格行列数变化时,`rearrangeIcons` 函数会被调用,执行智能重排算法:
|
||||||
|
|
||||||
|
1. 优先保留原有有效位置的图标
|
||||||
|
2. 为移出可视区域的图标寻找新的空闲位置
|
||||||
|
3. 若无足够空间,则将其加入 `exceedApp` 隐藏列表
|
||||||
|
|
||||||
|
该机制确保用户体验的一致性,避免图标因窗口缩放而丢失。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L60-L94)
|
||||||
|
|
||||||
|
## 模板渲染与事件处理
|
||||||
|
|
||||||
|
### v-for循环渲染机制
|
||||||
|
|
||||||
|
组件模板使用 `v-for` 指令遍历 `appIconsRef` 数组,为每个图标实例化一个 `AppIcon` 子组件:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<AppIcon
|
||||||
|
v-for="(appIcon, i) in appIconsRef"
|
||||||
|
:key="i"
|
||||||
|
:iconInfo="appIcon"
|
||||||
|
:gridTemplate="gridTemplate"
|
||||||
|
@dblclick="runApp(appIcon)"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`:key` 使用索引值确保渲染性能,`iconInfo` 和 `gridTemplate` 作为 props 向下传递必要数据。
|
||||||
|
|
||||||
|
### runApp双击事件扩展点
|
||||||
|
|
||||||
|
`@dblclick` 事件绑定到 `runApp` 方法,目前为空实现,作为未来功能扩展的预留接口:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const runApp = (appIcon: IDesktopAppIcon) => {}
|
||||||
|
```
|
||||||
|
|
||||||
|
此处可集成应用启动逻辑,如进程管理器调用、窗口创建等。
|
||||||
|
|
||||||
|
### AppIcon组件协同
|
||||||
|
|
||||||
|
`AppIcon` 组件接收父级传递的 `gridTemplate` 参数,结合自身 `x/y` 坐标计算绝对位置:
|
||||||
|
|
||||||
|
```css
|
||||||
|
grid-column: ${x}/${x + 1};
|
||||||
|
grid-row: ${y}/${y + 1};
|
||||||
|
```
|
||||||
|
|
||||||
|
并实现拖拽功能,在释放时根据鼠标位置更新坐标,形成闭环控制。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
## 与父组件的数据流关系
|
||||||
|
|
||||||
|
### 挂载关系分析
|
||||||
|
|
||||||
|
`DesktopContainer` 被直接嵌入 `App.vue` 的模板结构中,位于 `.desktop-bg` 容器内部:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A[App.vue] --> B[desktop-root]
|
||||||
|
B --> C[desktop-bg]
|
||||||
|
C --> D[DesktopContainer]
|
||||||
|
B --> E[task-bar]
|
||||||
|
```
|
||||||
|
|
||||||
|
这种层级结构明确了其作为主内容区核心组件的地位。
|
||||||
|
|
||||||
|
### 数据流路径
|
||||||
|
|
||||||
|
数据流动遵循典型的 Vue 单向数据流模式:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[onMounted Hook] --> B[查询DOM容器]
|
||||||
|
B --> C[启动ResizeObserver]
|
||||||
|
C --> D[触发尺寸计算]
|
||||||
|
D --> E[更新gridTemplate]
|
||||||
|
E --> F[computed生成gridStyle]
|
||||||
|
F --> G[模板应用样式]
|
||||||
|
H[localStorage读取] --> I[初始化appIconsRef]
|
||||||
|
I --> J[渲染AppIcon列表]
|
||||||
|
J --> K[用户交互]
|
||||||
|
K --> L[更新appIconsRef]
|
||||||
|
L --> M[watch触发持久化]
|
||||||
|
```
|
||||||
|
|
||||||
|
整个过程无需向上通信,完全由组合式函数内部闭环处理。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [App.vue](file://src/ui/App.vue#L1-L52)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
|
||||||
|
## 自定义容器集成示例
|
||||||
|
|
||||||
|
要在其他环境中复用 `DesktopContainer`,可通过修改 `containerStr` 参数指定不同的监听目标:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在自定义组件中使用
|
||||||
|
const { appIconsRef, gridStyle, gridTemplate } = useDesktopContainerInit('#custom-desktop-container')
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的 HTML 结构需包含匹配的选择器:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="custom-desktop-container" :style="gridStyle">
|
||||||
|
<!-- AppIcon will be rendered here -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
注意确保目标元素具有明确的尺寸(宽高),否则 `ResizeObserver` 无法正确计算布局参数。此外,建议保持原有的 CSS 类名以继承样式定义。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
254
.qoder/repowiki/zh/content/UI组件体系/UI组件体系.md
Normal file
254
.qoder/repowiki/zh/content/UI组件体系/UI组件体系.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# UI组件体系
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
- [App.vue](file://src/ui/App.vue)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心数据模型](#核心数据模型)
|
||||||
|
3. [主容器布局机制](#主容器布局机制)
|
||||||
|
4. [可拖拽图标实现](#可拖拽图标实现)
|
||||||
|
5. [组件层级与数据流](#组件层级与数据流)
|
||||||
|
6. [模板使用示例](#模板使用示例)
|
||||||
|
7. [自定义扩展建议](#自定义扩展建议)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
本文档深入解析Vue桌面应用的UI组件结构与交互逻辑。重点阐述`DesktopContainer.vue`作为主容器的动态网格布局机制,以及`AppIcon.vue`如何实现可拖拽的应用图标功能。通过分析`IDesktopAppIcon`和`IGridTemplateParams`接口定义,说明图标数据模型和网格参数的设计原理。同时解释从根组件`App.vue`到子组件`DesktopContainer`再到`AppIcon`的父子关系及数据传递方式,并提供实际使用示例与扩展建议。
|
||||||
|
|
||||||
|
## 核心数据模型
|
||||||
|
|
||||||
|
### 桌面应用图标接口 (IDesktopAppIcon)
|
||||||
|
该接口定义了桌面图标的元数据结构,包含名称、图标资源路径、启动路径及其在网格中的位置坐标。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IDesktopAppIcon {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
path: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明:**
|
||||||
|
- `name`: 图标显示名称
|
||||||
|
- `icon`: 图标资源路径
|
||||||
|
- `path`: 应用启动路径
|
||||||
|
- `x`: 在网格布局中的列索引(从1开始)
|
||||||
|
- `y`: 在网格布局中的行索引(从1开始)
|
||||||
|
|
||||||
|
此接口用于统一管理所有桌面图标的配置信息,并支持持久化存储至`localStorage`。
|
||||||
|
|
||||||
|
### 网格模板参数接口 (IGridTemplateParams)
|
||||||
|
该接口定义了网格布局的核心计算参数,支持响应式调整。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IGridTemplateParams {
|
||||||
|
readonly cellExpectWidth: number
|
||||||
|
readonly cellExpectHeight: number
|
||||||
|
cellRealWidth: number
|
||||||
|
cellRealHeight: number
|
||||||
|
gapX: number
|
||||||
|
gapY: number
|
||||||
|
colCount: number
|
||||||
|
rowCount: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明:**
|
||||||
|
- `cellExpectWidth/Height`: 单元格预设宽高
|
||||||
|
- `cellRealWidth/Height`: 实际渲染时的单元格宽高
|
||||||
|
- `gapX/Y`: 列/行间距
|
||||||
|
- `colCount/rowCount`: 当前容器可容纳的总行列数
|
||||||
|
|
||||||
|
这些参数由`ResizeObserver`监听容器尺寸变化后动态计算得出,确保布局自适应。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L3-L20)
|
||||||
|
|
||||||
|
## 主容器布局机制
|
||||||
|
|
||||||
|
`DesktopContainer.vue`组件负责构建整个桌面的网格布局系统。其核心是通过组合式API `useDesktopContainerInit` 初始化并维护网格状态。
|
||||||
|
|
||||||
|
### 响应式网格生成
|
||||||
|
组件利用CSS Grid布局特性,通过计算属性`gridStyle`动态生成`grid-template-columns`和`grid-template-rows`样式规则:
|
||||||
|
|
||||||
|
```css
|
||||||
|
gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 容器尺寸监听
|
||||||
|
使用`ResizeObserver` API实时监听`.desktop-icons-container`容器的尺寸变化,重新计算:
|
||||||
|
- 可容纳的行列数量 (`colCount`, `rowCount`)
|
||||||
|
- 单元格实际尺寸 (`cellRealWidth`, `cellRealHeight`)
|
||||||
|
|
||||||
|
### 图标初始化与持久化
|
||||||
|
首次加载时从`localStorage`读取历史图标位置信息,结合应用列表进行映射初始化。当图标位置变更时自动同步回本地存储。
|
||||||
|
|
||||||
|
### 超出边界处理
|
||||||
|
当窗口缩放导致图标超出可视区域时,`rearrangeIcons`函数会智能重排图标:
|
||||||
|
- 优先放置于原始位置
|
||||||
|
- 若冲突则寻找最近空位
|
||||||
|
- 实在无法容纳则暂存于`exceedApp`数组
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([组件挂载]) --> Observe["创建ResizeObserver"]
|
||||||
|
Observe --> InitGrid["初始化网格参数"]
|
||||||
|
InitGrid --> LoadStorage["从localStorage加载图标位置"]
|
||||||
|
LoadStorage --> MapIcons["映射应用信息与图标"]
|
||||||
|
MapIcons --> Render["渲染AppIcon组件"]
|
||||||
|
Resize["容器尺寸变化"] --> Recalculate["重新计算行列数"]
|
||||||
|
Recalculate --> Rearrange["调用rearrangeIcons重排"]
|
||||||
|
Rearrange --> UpdateState["更新appIconsRef状态"]
|
||||||
|
UpdateState --> SyncStorage["同步至localStorage"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 可拖拽图标实现
|
||||||
|
|
||||||
|
`AppIcon.vue`组件实现了完整的拖拽交互逻辑,允许用户自由调整图标位置。
|
||||||
|
|
||||||
|
### 拖拽事件绑定
|
||||||
|
在模板中为图标容器启用原生HTML5拖拽API:
|
||||||
|
```html
|
||||||
|
<div draggable="true" @dragstart="onDragStart" @dragend="onDragEnd">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 位置计算逻辑
|
||||||
|
在`dragend`事件中执行以下步骤:
|
||||||
|
1. 获取鼠标相对于容器的坐标
|
||||||
|
2. 根据单元格实际尺寸换算为网格坐标(向上取整)
|
||||||
|
3. 更新`iconInfo.x`和`iconInfo.y`属性
|
||||||
|
|
||||||
|
### 防止重复放置
|
||||||
|
通过检查目标元素是否已是其他图标容器来避免无效操作:
|
||||||
|
```javascript
|
||||||
|
if (pointTarget.classList.contains('icon-container')) return
|
||||||
|
```
|
||||||
|
|
||||||
|
### 样式定位
|
||||||
|
使用CSS Grid的`grid-column`和`grid-row`属性将图标精确定位于指定网格单元:
|
||||||
|
```css
|
||||||
|
grid-column: ${x}/${x + 1}; grid-row: ${y}/${y + 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
## 组件层级与数据流
|
||||||
|
|
||||||
|
整个UI体系遵循清晰的父子组件层级结构,数据沿树状结构单向流动。
|
||||||
|
|
||||||
|
### 组件层级关系
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[App.vue] --> B[DesktopContainer.vue]
|
||||||
|
B --> C[AppIcon.vue]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据传递路径
|
||||||
|
1. **App.vue** → **DesktopContainer.vue**: 通过直接嵌入`<DesktopContainer/>`标签建立父子关系
|
||||||
|
2. **DesktopContainer.vue** → **AppIcon.vue**:
|
||||||
|
- 使用`v-for`遍历`appIconsRef`数组创建多个实例
|
||||||
|
- 通过`defineProps`接收`iconInfo`和`gridTemplate`两个关键参数
|
||||||
|
- 绑定双击事件`@dblclick="runApp"`实现应用启动
|
||||||
|
|
||||||
|
### 状态管理特点
|
||||||
|
- **集中式初始化**:所有状态在`useDesktopContainerInit`中统一创建
|
||||||
|
- **响应式驱动**:基于Vue的`ref`和`reactive`实现自动更新
|
||||||
|
- **本地持久化**:重要状态如图标位置通过`localStorage`保存
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App as App.vue
|
||||||
|
participant DC as DesktopContainer.vue
|
||||||
|
participant AI as AppIcon.vue
|
||||||
|
App->>DC : 渲染组件
|
||||||
|
DC->>DC : 执行useDesktopContainerInit()
|
||||||
|
DC->>DC : 初始化gridTemplate/appIconsRef
|
||||||
|
DC->>AI : v-for循环渲染多个实例
|
||||||
|
DC->>AI : 传递iconInfo和gridTemplate props
|
||||||
|
AI->>AI : 用户拖拽图标
|
||||||
|
AI->>AI : 计算新坐标并更新iconInfo
|
||||||
|
AI->>DC : 触发watch监听
|
||||||
|
DC->>DC : 同步至localStorage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [App.vue](file://src/ui/App.vue#L1-L52)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [App.vue](file://src/ui/App.vue#L1-L52)
|
||||||
|
|
||||||
|
## 模板使用示例
|
||||||
|
|
||||||
|
以下是如何在项目中正确使用这些组件的标准范例:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 在任意父组件中引入DesktopContainer -->
|
||||||
|
<template>
|
||||||
|
<div class="desktop-wrapper">
|
||||||
|
<DesktopContainer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DesktopContainer from '@/ui/desktop-container/DesktopContainer.vue'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
每个`AppIcon`实例由`DesktopContainer`内部自动创建,无需手动实例化。只需确保:
|
||||||
|
1. 应用列表数据已正确注入`useDesktopContainerInit`
|
||||||
|
2. 图标资源配置路径准确无误
|
||||||
|
3. localStorage中保留历史位置记录以实现记忆功能
|
||||||
|
|
||||||
|
## 自定义扩展建议
|
||||||
|
|
||||||
|
### 样式定制
|
||||||
|
可通过覆盖SCSS变量或添加自定义类名修改图标外观:
|
||||||
|
```scss
|
||||||
|
.icon-container {
|
||||||
|
@apply bg-transparent hover:bg-blue-100;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 功能增强
|
||||||
|
- 添加右键菜单支持
|
||||||
|
- 实现图标排序功能(按名称、时间等)
|
||||||
|
- 增加动画过渡效果(拖拽、进入/离开)
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
- 对大量图标实施虚拟滚动
|
||||||
|
- 使用Web Worker处理复杂重排算法
|
||||||
|
- 添加防抖机制优化频繁resize场景
|
||||||
|
|
||||||
|
### 接口拓展
|
||||||
|
可继承`IDesktopAppIcon`接口增加新字段:
|
||||||
|
```typescript
|
||||||
|
interface ExtendedIcon extends IDesktopAppIcon {
|
||||||
|
tooltip?: string;
|
||||||
|
category?: 'productivity' | 'entertainment' | 'development';
|
||||||
|
lastLaunched?: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
以上扩展应在保持原有接口兼容性的前提下进行,确保不影响现有功能。
|
||||||
262
.qoder/repowiki/zh/content/事件系统/事件系统.md
Normal file
262
.qoder/repowiki/zh/content/事件系统/事件系统.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# 事件系统
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心组件分析](#核心组件分析)
|
||||||
|
3. [接口契约规范](#接口契约规范)
|
||||||
|
4. [事件管理器扩展实现](#事件管理器扩展实现)
|
||||||
|
5. [跨组件通信实例](#跨组件通信实例)
|
||||||
|
6. [错误处理与内存泄漏防范](#错误处理与内存泄漏防范)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
本文档全面记录了自定义事件总线系统的实现原理与使用方法。基于 `EventBuilderImpl` 类,详细解释 `addEventListener`、`removeEventListener` 和 `notifyEvent` 三个核心方法的工作机制,包括 `once`、`immediate` 等选项的行为特征。文档描述了 `IEventBuilder` 接口的契约规范,并分析了 `DesktopEventManager` 和 `WindowFormEventManager` 如何继承和扩展基础事件功能。同时提供跨组件通信的实际用例,并说明错误处理和内存泄漏防范措施。
|
||||||
|
|
||||||
|
## 核心组件分析
|
||||||
|
|
||||||
|
### EventBuilderImpl 实现机制
|
||||||
|
|
||||||
|
`EventBuilderImpl` 是事件总线系统的核心实现类,采用泛型设计支持类型安全的事件管理。其内部通过 `Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>` 结构存储事件处理器,确保高效的事件查找与去重。
|
||||||
|
|
||||||
|
#### addEventListener 方法工作机制
|
||||||
|
该方法用于注册事件监听器,具有以下特性:
|
||||||
|
- **去重机制**:在添加前检查是否已存在相同处理器函数,避免重复绑定
|
||||||
|
- **即时执行**:当 `options.immediate` 为 `true` 时,立即同步执行一次处理器
|
||||||
|
- **单次监听**:通过 `options.once` 标记,使监听器在触发后自动移除
|
||||||
|
- **类型安全**:利用 TypeScript 泛型约束确保事件名与处理器参数类型匹配
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始]) --> ValidateHandler["验证处理器非空"]
|
||||||
|
ValidateHandler --> HasEvent{"是否存在事件队列?"}
|
||||||
|
HasEvent --> |否| CreateSet["创建新的处理器集合"]
|
||||||
|
CreateSet --> AddToSet
|
||||||
|
HasEvent --> |是| GetSet["获取现有集合"]
|
||||||
|
GetSet --> AddToSet
|
||||||
|
AddToSet --> CheckDuplicate{"是否已存在?"}
|
||||||
|
CheckDuplicate --> |否| AddWrapper["添加包装器(once标记)"]
|
||||||
|
AddWrapper --> CheckImmediate{"是否立即执行?"}
|
||||||
|
CheckImmediate --> |是| ExecuteNow["立即执行处理器"]
|
||||||
|
CheckImmediate --> |否| End([结束])
|
||||||
|
ExecuteNow --> End
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L20-L46)
|
||||||
|
|
||||||
|
#### removeEventListener 方法工作机制
|
||||||
|
该方法通过遍历对应事件的处理器集合,精确匹配并删除指定的处理器函数引用,实现精准解绑。
|
||||||
|
|
||||||
|
#### notifyEvent 方法工作机制
|
||||||
|
通知方法按顺序调用所有注册的处理器,并处理以下特殊情况:
|
||||||
|
- 自动清理标记为 `once` 的监听器
|
||||||
|
- 捕获并记录处理器执行中的异常,防止中断其他监听器
|
||||||
|
- 支持任意数量的参数传递
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Publisher as "发布者"
|
||||||
|
participant EventBus as "EventBus"
|
||||||
|
participant Handler1 as "处理器1"
|
||||||
|
participant Handler2 as "处理器2"
|
||||||
|
Publisher->>EventBus : notifyEvent('event', args)
|
||||||
|
EventBus->>EventBus : 获取处理器集合
|
||||||
|
loop 每个处理器
|
||||||
|
EventBus->>Handler1 : 执行处理器
|
||||||
|
Handler1-->>EventBus : 完成
|
||||||
|
EventBus->>EventBus : 检查once标记
|
||||||
|
alt 是单次监听
|
||||||
|
EventBus->>EventBus : 从集合中移除
|
||||||
|
end
|
||||||
|
EventBus->>Handler2 : 执行处理器
|
||||||
|
Handler2-->>EventBus : 完成
|
||||||
|
end
|
||||||
|
EventBus-->>Publisher : 通知完成
|
||||||
|
Note over Handler1,Handler2 : 异常被捕获,不影响其他处理器执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L75-L90)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 接口契约规范
|
||||||
|
|
||||||
|
### IEventBuilder 接口定义
|
||||||
|
`IEventBuilder` 接口定义了事件管理器的标准行为契约,继承自 `IDestroyable` 接口以支持资源清理。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IEventBuilder {
|
||||||
|
<<interface>>
|
||||||
|
+addEventListener(eventName, handler, options) : void
|
||||||
|
+removeEventListener(eventName, handler) : void
|
||||||
|
+notifyEvent(eventName, ...args) : void
|
||||||
|
}
|
||||||
|
class IDestroyable {
|
||||||
|
<<interface>>
|
||||||
|
+destroy() : void
|
||||||
|
}
|
||||||
|
IEventBuilder --|> IDestroyable : 继承
|
||||||
|
class EventBuilderImpl {
|
||||||
|
-_eventHandlers : Map<string, Set<HandlerWrapper>>
|
||||||
|
+addEventListener()
|
||||||
|
+removeEventListener()
|
||||||
|
+notifyEvent()
|
||||||
|
+destroy()
|
||||||
|
}
|
||||||
|
EventBuilderImpl ..|> IEventBuilder : 实现
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts#L13-L46)
|
||||||
|
|
||||||
|
接口方法参数说明:
|
||||||
|
|
||||||
|
| 方法 | 参数 | 类型 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| addEventListener | eventName | keyof Events | 事件名称 |
|
||||||
|
| | handler | F extends Events[E] | 事件处理器函数 |
|
||||||
|
| | options | {immediate?: boolean, immediateArgs?: Parameters<F>, once?: boolean} | 配置选项 |
|
||||||
|
| removeEventListener | eventName | keyof Events | 事件名称 |
|
||||||
|
| | handler | F extends Events[E] | 要移除的处理器函数 |
|
||||||
|
| notifyEvent | eventName | keyof Events | 事件名称 |
|
||||||
|
| | ...args | Parameters<F> | 传递给处理器的参数 |
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts#L1-L46)
|
||||||
|
|
||||||
|
## 事件管理器扩展实现
|
||||||
|
|
||||||
|
### DesktopEventManager 桌面事件管理
|
||||||
|
`DesktopEventManager` 通过实例化 `EventBuilderImpl<IDesktopEvent>` 创建专用的桌面事件总线,定义了桌面应用相关的特定事件类型。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IDesktopEvent {
|
||||||
|
<<interface>>
|
||||||
|
+desktopAppPosChange(info : IDesktopAppIcon) : void
|
||||||
|
}
|
||||||
|
class desktopEM {
|
||||||
|
+instance of EventBuilderImpl<IDesktopEvent>
|
||||||
|
}
|
||||||
|
desktopEM ..> IDesktopEvent : 类型约束
|
||||||
|
desktopEM ..> EventBuilderImpl : 实例化
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L1-L16)
|
||||||
|
|
||||||
|
### WindowFormEventManager 窗口表单事件管理
|
||||||
|
`WindowFormEventManager` 提供了窗口生命周期管理的完整事件体系,涵盖最小化、最大化、关闭、聚焦等状态变化。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IWindowFormEvent {
|
||||||
|
<<interface>>
|
||||||
|
+windowFormMinimize(id : string) : void
|
||||||
|
+windowFormMaximize(id : string) : void
|
||||||
|
+windowFormRestore(id : string) : void
|
||||||
|
+windowFormClose(id : string) : void
|
||||||
|
+windowFormFocus(id : string) : void
|
||||||
|
+windowFormDataUpdate(data : IWindowFormDataUpdateParams) : void
|
||||||
|
+windowFormCreated() : void
|
||||||
|
}
|
||||||
|
class wfem {
|
||||||
|
+instance of EventBuilderImpl<IWindowFormEvent>
|
||||||
|
}
|
||||||
|
class IWindowFormDataUpdateParams {
|
||||||
|
+id : string
|
||||||
|
+state : TWindowFormState
|
||||||
|
+width : number
|
||||||
|
+height : number
|
||||||
|
+x : number
|
||||||
|
+y : number
|
||||||
|
}
|
||||||
|
wfem ..> IWindowFormEvent : 类型约束
|
||||||
|
IWindowFormEvent ..> IWindowFormDataUpdateParams : 引用
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L1-L61)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L1-L16)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L1-L61)
|
||||||
|
|
||||||
|
## 跨组件通信实例
|
||||||
|
|
||||||
|
### 桌面应用位置更新场景
|
||||||
|
```typescript
|
||||||
|
// 发布者组件
|
||||||
|
const updatePosition = (iconInfo) => {
|
||||||
|
desktopEM.notifyEvent('desktopAppPosChange', iconInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅者组件
|
||||||
|
desktopEM.addEventListener('desktopAppPosChange', (info) => {
|
||||||
|
console.log('应用位置更新:', info.name, info.x, info.y)
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
immediateArgs: [currentIconInfo]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 窗口状态变更场景
|
||||||
|
```typescript
|
||||||
|
// 窗口最小化
|
||||||
|
wfem.notifyEvent('windowFormMinimize', 'win123')
|
||||||
|
|
||||||
|
// 监听窗口最小化(仅一次)
|
||||||
|
wfem.addEventListener('windowFormMinimize', (id) => {
|
||||||
|
addToTaskbar(id)
|
||||||
|
}, {
|
||||||
|
once: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听所有窗口数据更新
|
||||||
|
wfem.addEventListener('windowFormDataUpdate', (data) => {
|
||||||
|
saveWindowState(data)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L1-L16)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L1-L61)
|
||||||
|
|
||||||
|
## 错误处理与内存泄漏防范
|
||||||
|
|
||||||
|
### 错误处理机制
|
||||||
|
系统在关键执行路径上均包含异常捕获:
|
||||||
|
- 处理器执行时捕获异常,防止中断其他监听器
|
||||||
|
- 控制台输出错误信息便于调试
|
||||||
|
- 不抛出异常保证事件总线稳定性
|
||||||
|
|
||||||
|
### 内存泄漏防范措施
|
||||||
|
1. **及时解绑**:使用 `removeEventListener` 移除不再需要的监听器
|
||||||
|
2. **单次监听**:对只需执行一次的逻辑使用 `once: true` 选项
|
||||||
|
3. **资源清理**:实现 `destroy()` 方法清空所有事件处理器
|
||||||
|
4. **实例管理**:通过单例模式管理事件总线实例生命周期
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[组件挂载] --> B[添加事件监听]
|
||||||
|
B --> C[设置once或immediate]
|
||||||
|
C --> D[组件运行]
|
||||||
|
D --> E{组件卸载?}
|
||||||
|
E --> |是| F[调用removeEventListener]
|
||||||
|
F --> G[或调用destroy清理所有]
|
||||||
|
G --> H[防止内存泄漏]
|
||||||
|
E --> |否| D
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L92-L94)
|
||||||
207
.qoder/repowiki/zh/content/事件系统/核心事件总线.md
Normal file
207
.qoder/repowiki/zh/content/事件系统/核心事件总线.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# 核心事件总线
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用的文件**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts)
|
||||||
|
- [IDestroyable.ts](file://src/common/types/IDestroyable.ts)
|
||||||
|
- [EventManager.ts](file://src/events/EventManager.ts)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心数据结构设计](#核心数据结构设计)
|
||||||
|
3. [泛型类型安全机制](#泛型类型安全机制)
|
||||||
|
4. [事件监听器管理](#事件监听器管理)
|
||||||
|
- [addEventListener 方法详解](#addeventlistener-方法详解)
|
||||||
|
- [removeEventListener 方法详解](#removeeventlistener-方法详解)
|
||||||
|
5. [事件通知与分发](#事件通知与分发)
|
||||||
|
6. [资源清理与内存泄漏防范](#资源清理与内存泄漏防范)
|
||||||
|
7. [实际应用示例](#实际应用示例)
|
||||||
|
8. [总结](#总结)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
`EventBuilderImpl` 类是本系统事件机制的核心实现,提供了一个类型安全、功能完整的事件总线系统。该类实现了 `IEventBuilder` 接口,遵循发布-订阅模式,支持事件的注册、移除和触发,并具备立即执行、一次性监听等高级特性。通过继承 `IDestroyable` 接口,它还提供了资源清理能力,有效防止内存泄漏。
|
||||||
|
|
||||||
|
该事件系统被多个模块广泛使用,包括全局事件管理器(`eventManager`)、桌面事件管理器(`desktopEM`)和窗口表单事件管理器(`wfem`),构成了整个应用的通信骨架。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts#L13-L46)
|
||||||
|
- [IDestroyable.ts](file://src/common/types/IDestroyable.ts#L4-L7)
|
||||||
|
|
||||||
|
## 核心数据结构设计
|
||||||
|
|
||||||
|
`EventBuilderImpl` 采用 `Map` 和 `Set` 的组合来高效管理事件监听器,这种设计在性能和内存使用上达到了良好的平衡。
|
||||||
|
|
||||||
|
其核心是一个私有成员 `_eventHandlers`,其类型为 `Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>`。这个数据结构的含义是:
|
||||||
|
- **外层 Map**:以事件名称(`keyof Events`)作为键,确保每个事件名对应一个独立的监听器集合。
|
||||||
|
- **内层 Set**:存储特定事件的所有监听器包装对象(`HandlerWrapper`)。使用 `Set` 而非数组可以天然避免重复添加同一个监听函数,并且插入和删除操作的时间复杂度为 O(1)。
|
||||||
|
|
||||||
|
`HandlerWrapper` 是一个简单的接口,包含两个属性:`fn`(原始的监听函数)和 `once`(布尔值,标记是否为一次性监听器)。这种包装方式将业务逻辑(函数本身)与元数据(如 `once` 标志)分离,使得事件管理更加灵活。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class EventBuilderImpl~Events~ {
|
||||||
|
-_eventHandlers : Map~keyof Events, Set~HandlerWrapper~Events[keyof Events]~~~
|
||||||
|
+addEventListener()
|
||||||
|
+removeEventListener()
|
||||||
|
+notifyEvent()
|
||||||
|
+destroy()
|
||||||
|
}
|
||||||
|
class HandlerWrapper~T~ {
|
||||||
|
+fn : T
|
||||||
|
+once : boolean
|
||||||
|
}
|
||||||
|
EventBuilderImpl --> "0..*" HandlerWrapper : 包含
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L15)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L15)
|
||||||
|
|
||||||
|
## 泛型类型安全机制
|
||||||
|
|
||||||
|
`EventBuilderImpl` 类通过 TypeScript 的泛型系统实现了严格的类型安全。其定义为 `class EventBuilderImpl<Events extends IEventMap>`,其中 `Events` 是一个必须符合 `IEventMap` 接口约束的泛型类型。
|
||||||
|
|
||||||
|
`IEventMap` 接口定义了事件映射的基本结构:一个索引签名 `[key: string]: (...args: any[]) => void`,表示键是字符串类型的事件名,值是任意参数的无返回值函数。
|
||||||
|
|
||||||
|
当实例化 `EventBuilderImpl` 时,需要传入一个具体的事件接口。例如,在 `WindowFormEventManager.ts` 中定义了 `IWindowFormEvent` 接口,并将其作为泛型参数:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const wfem = new EventBuilderImpl<IWindowFormEvent>()
|
||||||
|
```
|
||||||
|
|
||||||
|
这种设计带来了以下优势:
|
||||||
|
1. **编译时检查**:在调用 `addEventListener` 或 `notifyEvent` 时,TypeScript 编译器会根据 `IWindowFormEvent` 的定义检查事件名和参数类型是否正确。
|
||||||
|
2. **智能提示**:开发者在编写代码时能获得准确的事件名和参数类型提示。
|
||||||
|
3. **防止错误**:无法订阅未在接口中定义的事件,也无法传递错误类型的参数。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts#L4-L7)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L5-L59)
|
||||||
|
|
||||||
|
## 事件监听器管理
|
||||||
|
|
||||||
|
### addEventListener 方法详解
|
||||||
|
|
||||||
|
`addEventListener` 方法用于向事件总线注册新的监听器。它接收三个参数:事件名、处理函数和可选的配置项。
|
||||||
|
|
||||||
|
#### 配置项行为逻辑
|
||||||
|
- **immediate (立即执行)**:如果设置为 `true`,则在添加监听器后立即同步执行一次该函数。
|
||||||
|
- **immediateArgs (立即执行参数)**:为 `immediate` 执行阶段提供参数。若未指定,则使用空数组。
|
||||||
|
- **once (一次性监听)**:如果设置为 `true`,则该监听器在第一次被触发后自动从事件队列中移除。
|
||||||
|
|
||||||
|
#### 实现细节
|
||||||
|
1. **空值检查**:首先检查 `handler` 是否存在,避免无效监听器。
|
||||||
|
2. **惰性初始化**:如果这是该事件的第一个监听器,则创建一个新的 `Set` 来存放后续的 `HandlerWrapper`。
|
||||||
|
3. **去重机制**:在添加前,通过遍历 `Set` 并比较 `wrapper.fn === handler` 来确保不会重复添加相同的函数引用。
|
||||||
|
4. **异常捕获**:在执行 `immediate` 回调时,使用 `try-catch` 捕获任何可能抛出的异常,并将其输出到控制台,防止因单个监听器的错误而中断主流程。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始]) --> CheckHandler["检查 handler 是否为空"]
|
||||||
|
CheckHandler --> |否| InitSet["检查事件对应的 Set 是否存在"]
|
||||||
|
InitSet --> |否| CreateSet["创建新的 Set"]
|
||||||
|
CreateSet --> GetSet["获取事件对应的 Set"]
|
||||||
|
InitSet --> |是| GetSet
|
||||||
|
GetSet --> CheckDuplicate["检查是否已存在相同 handler"]
|
||||||
|
CheckDuplicate --> |否| AddWrapper["添加 HandlerWrapper 到 Set"]
|
||||||
|
AddWrapper --> CheckImmediate["检查 immediate 选项"]
|
||||||
|
CheckImmediate --> |是| ExecuteNow["立即执行 handler"]
|
||||||
|
ExecuteNow --> End([结束])
|
||||||
|
CheckImmediate --> |否| End
|
||||||
|
CheckHandler --> |是| End
|
||||||
|
CheckDuplicate --> |是| End
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L20-L46)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L20-L46)
|
||||||
|
|
||||||
|
### removeEventListener 方法详解
|
||||||
|
|
||||||
|
`removeEventListener` 方法负责从事件队列中移除指定的监听器。
|
||||||
|
|
||||||
|
其实现的关键在于**引用比对去重机制**。由于监听器是以 `HandlerWrapper` 对象的形式存储在 `Set` 中的,直接比较 `Set` 中的对象与传入的 `handler` 函数是不相等的。因此,该方法会遍历 `Set` 中的每一个 `wrapper`,并使用 `wrapper.fn === handler` 来精确匹配原始的函数引用。
|
||||||
|
|
||||||
|
一旦找到匹配项,就调用 `set.delete(wrapper)` 将其从 `Set` 中移除。这种基于引用的比对确保了只有完全相同的函数实例才会被移除,保证了操作的准确性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L55-L64)
|
||||||
|
|
||||||
|
## 事件通知与分发
|
||||||
|
|
||||||
|
`notifyEvent` 方法是事件系统的触发点,负责通知所有订阅了特定事件的监听器。
|
||||||
|
|
||||||
|
其工作流程如下:
|
||||||
|
1. **存在性检查**:首先检查是否存在该事件名对应的监听器集合,若不存在则直接返回。
|
||||||
|
2. **批量通知**:遍历该事件名对应 `Set` 中的所有 `HandlerWrapper`。
|
||||||
|
3. **异常捕获**:在调用每个监听器的 `fn(...args)` 时,使用 `try-catch` 块包裹,确保单个监听器的错误不会影响其他监听器的执行。
|
||||||
|
4. **once 监听器清理**:在成功调用一个监听器后,检查其 `once` 标志。如果为 `true`,则立即将其从 `Set` 中删除,实现一次性监听的功能。
|
||||||
|
|
||||||
|
这种“先通知,后清理”的策略保证了即使在 `once` 监听器执行过程中有其他代码尝试移除它,也不会产生竞态条件。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Publisher as 事件发布者
|
||||||
|
participant EventBus as EventBuilderImpl
|
||||||
|
participant ListenerA as 监听器 A (once : true)
|
||||||
|
participant ListenerB as 监听器 B (once : false)
|
||||||
|
Publisher->>EventBus : notifyEvent('click', x, y)
|
||||||
|
EventBus->>ListenerA : 执行 fn(x, y)
|
||||||
|
ListenerA-->>EventBus : 完成
|
||||||
|
EventBus->>EventBus : 检查 wrapper.once == true
|
||||||
|
EventBus->>EventBus : 从 Set 中删除 ListenerA
|
||||||
|
EventBus->>ListenerB : 执行 fn(x, y)
|
||||||
|
ListenerB-->>EventBus : 完成
|
||||||
|
EventBus-->>Publisher : 通知完成
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L75-L90)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L75-L90)
|
||||||
|
|
||||||
|
## 资源清理与内存泄漏防范
|
||||||
|
|
||||||
|
`EventBuilderImpl` 通过多种策略有效防范内存泄漏:
|
||||||
|
|
||||||
|
1. **显式销毁接口**:通过实现 `IDestroyable` 接口,提供了 `destroy()` 方法。调用此方法会清空 `_eventHandlers` Map,释放所有对监听器函数的引用,使它们可以被垃圾回收器回收。这对于长生命周期的应用或动态创建/销毁的组件至关重要。
|
||||||
|
|
||||||
|
2. **监听器去重**:`addEventListener` 方法中的去重逻辑防止了同一函数被多次添加,避免了不必要的内存占用和重复执行。
|
||||||
|
|
||||||
|
3. **once 监听器自动清理**:`once` 选项确保了一次性监听器在执行后立即被移除,无需手动清理。
|
||||||
|
|
||||||
|
4. **异常隔离**:`try-catch` 机制保证了事件分发过程的健壮性,防止因监听器内部错误导致整个事件系统崩溃。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L92-L94)
|
||||||
|
- [IDestroyable.ts](file://src/common/types/IDestroyable.ts#L4-L7)
|
||||||
|
|
||||||
|
## 实际应用示例
|
||||||
|
|
||||||
|
以下是 `EventBuilderImpl` 在项目中的具体应用实例:
|
||||||
|
|
||||||
|
- **全局事件管理器**:在 `EventManager.ts` 中,`eventManager` 实例被用来管理认证状态改变 (`onAuthChange`) 和主题切换 (`onThemeChange`) 等全局事件。
|
||||||
|
- **桌面事件管理器**:在 `DesktopEventManager.ts` 中,`desktopEM` 实例用于响应桌面应用图标位置的变化 (`desktopAppPosChange`)。
|
||||||
|
- **窗口表单事件管理器**:在 `WindowFormEventManager.ts` 中,`wfem` 实例监控窗口的最小化、最大化、关闭等生命周期事件。
|
||||||
|
|
||||||
|
这些预定义的事件管理器实例化了 `EventBuilderImpl`,并通过具体的事件接口(如 `IWindowFormEvent`)锁定了可用的事件集,为不同模块提供了清晰、类型安全的通信契约。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventManager.ts](file://src/events/EventManager.ts#L3-L34)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L3-L15)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L3-L60)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
`EventBuilderImpl` 作为一个轻量级但功能完备的事件总线实现,通过精心设计的数据结构(`Map`+`Set`)和 TypeScript 泛型,实现了高性能、类型安全的事件管理。其 `addEventListener`、`removeEventListener` 和 `notifyEvent` 方法构成了一个健壮的发布-订阅循环,而 `immediate`、`once` 等选项以及异常捕获机制则增强了其实用性和可靠性。最后,通过 `IDestroyable` 接口提供的 `destroy` 方法,确保了资源的可管理性,有效防止了内存泄漏,是构建可维护前端应用的理想选择。
|
||||||
187
.qoder/repowiki/zh/content/事件系统/桌面事件管理器.md
Normal file
187
.qoder/repowiki/zh/content/事件系统/桌面事件管理器.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<cite>
|
||||||
|
**本文档中引用的文件**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [引言](#引言)
|
||||||
|
2. [核心事件总线架构](#核心事件总线架构)
|
||||||
|
3. [桌面事件接口定义](#桌面事件接口定义)
|
||||||
|
4. [事件管理器实例化与类型注入](#事件管理器实例化与类型注入)
|
||||||
|
5. [应用图标位置变化事件语义解析](#应用图标位置变化事件语义解析)
|
||||||
|
6. [桌面容器中的事件监听实践](#桌面容器中的事件监听实践)
|
||||||
|
7. [拖拽结束时的事件发布机制](#拖拽结束时的事件发布机制)
|
||||||
|
8. [总结](#总结)
|
||||||
|
|
||||||
|
## 引言
|
||||||
|
|
||||||
|
本文档详细阐述了`DesktopEventManager`如何基于核心事件总线构建特定领域的事件系统。通过分析`IDesktopEvent`接口、`desktopEM`实例化过程以及在`useDesktopContainerInit`中的实际用例,全面揭示该事件管理器的设计原理与运行机制。
|
||||||
|
|
||||||
|
## 核心事件总线架构
|
||||||
|
|
||||||
|
`DesktopEventManager`并非独立实现事件机制,而是基于一个通用的核心事件总线——`EventBuilderImpl`类进行领域特化。这种设计模式实现了关注点分离:底层提供统一的事件订阅与通知能力,上层定义具体业务语义。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IEventBuilder~Events~ {
|
||||||
|
<<interface>>
|
||||||
|
+addEventListener(eventName, handler, options)
|
||||||
|
+removeEventListener(eventName, handler)
|
||||||
|
+notifyEvent(eventName, ...args)
|
||||||
|
}
|
||||||
|
class EventBuilderImpl~Events~ {
|
||||||
|
-_eventHandlers : Map<keyof Events, Set<HandlerWrapper>>
|
||||||
|
+addEventListener()
|
||||||
|
+removeEventListener()
|
||||||
|
+notifyEvent()
|
||||||
|
+destroy()
|
||||||
|
}
|
||||||
|
class IDesktopEvent {
|
||||||
|
<<interface>>
|
||||||
|
+desktopAppPosChange(info : IDesktopAppIcon)
|
||||||
|
}
|
||||||
|
class DesktopEventManager {
|
||||||
|
+desktopEM : EventBuilderImpl<IDesktopEvent>
|
||||||
|
}
|
||||||
|
EventBuilderImpl --> IEventBuilder : "implements"
|
||||||
|
DesktopEventManager ..> IDesktopEvent : "defines"
|
||||||
|
DesktopEventManager ..> EventBuilderImpl : "instantiates"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L7-L15)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L7-L15)
|
||||||
|
|
||||||
|
## 桌面事件接口定义
|
||||||
|
|
||||||
|
`IDesktopEvent`接口继承自泛型事件映射接口`IEventMap`,专门用于声明桌面环境下的各类事件。其核心成员是`desktopAppPosChange`事件,明确表达了“桌面应用位置改变”的业务语义。
|
||||||
|
|
||||||
|
该接口采用TypeScript的索引签名与函数类型组合方式,为每个事件名称绑定对应的回调函数签名,确保类型安全。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L7-L12)
|
||||||
|
|
||||||
|
## 事件管理器实例化与类型注入
|
||||||
|
|
||||||
|
`desktopEM`是一个全局单例实例,通过`new EventBuilderImpl<IDesktopEvent>()`创建。此处的关键在于泛型参数`IDesktopEvent`的注入,它将通用的`EventBuilderImpl`约束为仅支持处理`IDesktopEvent`所定义的事件类型。
|
||||||
|
|
||||||
|
这种类型注入机制保证了:
|
||||||
|
- 订阅时只能监听`desktopAppPosChange`等预定义事件
|
||||||
|
- 发布时必须提供符合`IDesktopAppIcon`结构的数据
|
||||||
|
- 编译期即可捕获类型错误,提升代码健壮性
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Code as "源码"
|
||||||
|
participant Compiler as "TypeScript编译器"
|
||||||
|
participant Runtime as "运行时"
|
||||||
|
Code->>Compiler : const desktopEM = new EventBuilderImpl<IDesktopEvent>()
|
||||||
|
Compiler->>Compiler : 类型检查:验证IDesktopEvent结构
|
||||||
|
Compiler-->>Code : 返回类型安全的事件管理器实例
|
||||||
|
Code->>Runtime : desktopEM.addEventListener('desktopAppPosChange', handler)
|
||||||
|
Runtime->>Runtime : 存储事件处理器
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L15-L15)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L15-L15)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 应用图标位置变化事件语义解析
|
||||||
|
|
||||||
|
`desktopAppPosChange`事件承载着桌面应用图标位置变更的核心语义。其参数类型`IDesktopAppIcon`精确描述了图标的元数据:
|
||||||
|
|
||||||
|
| 属性 | 类型 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| `name` | string | 图标名称,唯一标识 |
|
||||||
|
| `icon` | string | 图标资源路径或标识符 |
|
||||||
|
| `path` | string | 点击后启动的应用路径 |
|
||||||
|
| `x` | number | 在网格布局中的列索引(从1开始) |
|
||||||
|
| `y` | number | 在网格布局中的行索引(从1开始) |
|
||||||
|
|
||||||
|
当用户拖动图标并释放时,系统会构造包含新坐标(x,y)的`IDesktopAppIcon`对象,并通过`desktopEM.notifyEvent`触发此事件,通知所有监听者更新UI状态。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L11-L11)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14)
|
||||||
|
|
||||||
|
## 桌面容器中的事件监听实践
|
||||||
|
|
||||||
|
在`useDesktopContainerInit`这一组合式函数中,展示了如何在桌面容器初始化过程中订阅`desktopAppPosChange`事件以实现UI同步更新。
|
||||||
|
|
||||||
|
虽然当前代码未直接展示订阅逻辑,但根据上下文可推断出典型使用模式如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["组件挂载 onMounted"] --> B["订阅 desktopAppPosChange 事件"]
|
||||||
|
B --> C["接收 IDesktopAppIcon 数据"]
|
||||||
|
C --> D["更新 appIconsRef 状态"]
|
||||||
|
D --> E["触发 Vue 响应式更新"]
|
||||||
|
E --> F["UI 自动重渲染"]
|
||||||
|
G["图标拖拽结束"] --> H["发布 desktopAppPosChange 事件"]
|
||||||
|
H --> B
|
||||||
|
```
|
||||||
|
|
||||||
|
理想情况下,在`onMounted`钩子内应调用:
|
||||||
|
```ts
|
||||||
|
desktopEM.addEventListener('desktopAppPosChange', (info) => {
|
||||||
|
const index = appIconsRef.value.findIndex(icon => icon.name === info.name);
|
||||||
|
if (index !== -1) {
|
||||||
|
appIconsRef.value[index] = { ...info };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
从而建立从事件到视图的完整响应链路。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L0-L164)
|
||||||
|
|
||||||
|
## 拖拽结束时的事件发布机制
|
||||||
|
|
||||||
|
`desktopEM`的事件发布时机严格遵循用户交互生命周期。在应用图标的拖拽组件中,应在拖拽结束(dragend)事件处理器中调用:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
desktopEM.notifyEvent('desktopAppPosChange', currentIconInfo);
|
||||||
|
```
|
||||||
|
|
||||||
|
此时传递的`currentIconInfo`必须是完整的`IDesktopAppIcon`对象,包含更新后的`x`和`y`坐标。该调用会遍历所有注册的监听器,并按顺序执行其回调函数,实现多播通知。
|
||||||
|
|
||||||
|
发布流程的关键特性包括:
|
||||||
|
- **同步执行**:所有监听器在同一线程内依次调用
|
||||||
|
- **异常隔离**:单个监听器错误不会中断其他监听器执行
|
||||||
|
- **一次性监听支持**:可通过`once: true`选项注册只触发一次的监听器
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant DragComponent as "拖拽组件"
|
||||||
|
participant DesktopEM as "desktopEM"
|
||||||
|
participant ListenerA as "监听器A"
|
||||||
|
participant ListenerB as "监听器B"
|
||||||
|
DragComponent->>DesktopEM : notifyEvent('desktopAppPosChange', info)
|
||||||
|
DesktopEM->>ListenerA : 执行回调函数
|
||||||
|
ListenerA-->>DesktopEM : 完成
|
||||||
|
DesktopEM->>ListenerB : 执行回调函数
|
||||||
|
ListenerB-->>DesktopEM : 完成
|
||||||
|
DesktopEM-->>DragComponent : 通知完成
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L75-L90)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts#L15-L15)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L75-L90)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
`DesktopEventManager`通过泛型化继承核心事件总线`EventBuilderImpl`,成功构建了一个类型安全、语义清晰的领域事件系统。`IDesktopEvent`接口明确定义了`desktopAppPosChange`事件及其`IDesktopAppIcon`参数结构,`desktopEM`实例作为全局事件枢纽,在拖拽结束时精准发布坐标变更事件。尽管当前`useDesktopContainerInit`中尚未体现订阅逻辑,但整个架构已为实现响应式桌面UI奠定了坚实基础。
|
||||||
186
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口关闭事件.md
Normal file
186
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口关闭事件.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 窗口关闭事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [DesktopEventManager.ts](file://src/events/DesktopEventManager.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心职责与生命周期管理](#核心职责与生命周期管理)
|
||||||
|
3. [事件定义与接口规范](#事件定义与接口规范)
|
||||||
|
4. [事件注册与监听机制](#事件注册与监听机制)
|
||||||
|
5. [事件广播与跨组件通信](#事件广播与跨组件通信)
|
||||||
|
6. [资源清理与内存泄漏防护](#资源清理与内存泄漏防护)
|
||||||
|
7. [完整示例:带确认对话框的关闭流程](#完整示例:带确认对话框的关闭流程)
|
||||||
|
8. [总结](#总结)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
`windowFormClose` 事件是 Vue 桌面应用中用于管理窗口实例销毁的核心机制。当用户点击窗口右上角的关闭按钮时,系统会触发该事件,并携带目标窗口的唯一标识符(ID),以精确地定位并销毁对应的窗口实例。此事件不仅负责 UI 层面的 DOM 移除,还承担着状态存储更新、资源释放以及防止内存泄漏的重要职责。
|
||||||
|
|
||||||
|
本文档将深入解析 `windowFormClose` 事件的整个生命周期,涵盖其定义、注册、广播及处理过程,并说明如何通过 `wfem`(Window Form Event Manager)进行监听器注册,集成用户确认逻辑,并确保系统的安全性和一致性。
|
||||||
|
|
||||||
|
## 核心职责与生命周期管理
|
||||||
|
|
||||||
|
`windowFormClose` 事件在窗口关闭流程中扮演着中枢角色,其生命周期贯穿从用户交互到最终资源回收的全过程:
|
||||||
|
|
||||||
|
1. **触发阶段**:由用户点击关闭按钮发起,事件携带窗口 ID 作为参数。
|
||||||
|
2. **广播阶段**:通过 `wfem.notifyEvent('windowFormClose', id)` 将事件分发至所有注册的监听器。
|
||||||
|
3. **处理阶段**:各组件根据自身业务逻辑响应事件,执行如显示确认对话框、保存未提交数据等操作。
|
||||||
|
4. **清理阶段**:完成必要检查后,执行 DOM 节点移除、状态 store 更新和相关资源释放。
|
||||||
|
5. **终止阶段**:确保所有引用被清除,避免因闭包或事件监听器残留导致的内存泄漏。
|
||||||
|
|
||||||
|
该事件的设计遵循单一职责原则,专注于“关闭”这一核心动作,同时通过松耦合的发布-订阅模式实现跨组件协作。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L27-L27)
|
||||||
|
|
||||||
|
## 事件定义与接口规范
|
||||||
|
|
||||||
|
`windowFormClose` 事件在 `IWindowFormEvent` 接口中明确定义,位于 `src/events/WindowFormEventManager.ts` 文件中。该接口继承自通用事件映射类型 `IEventMap`,并声明了多个与窗口操作相关的事件处理器签名。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IWindowFormEvent extends IEventMap {
|
||||||
|
/**
|
||||||
|
* 窗口关闭
|
||||||
|
* @param id 窗口id
|
||||||
|
*/
|
||||||
|
windowFormClose: (id: string) => void;
|
||||||
|
// 其他事件...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
此定义明确了以下关键信息:
|
||||||
|
- **事件名称**:`windowFormClose`
|
||||||
|
- **参数类型**:单个字符串类型的 `id`,用于唯一标识待关闭的窗口实例。
|
||||||
|
- **返回类型**:`void`,表示该事件不期望返回值,主要用于触发副作用。
|
||||||
|
|
||||||
|
这种强类型的接口设计保证了事件使用的安全性,编译器能够在开发阶段捕获类型错误。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IWindowFormEvent {
|
||||||
|
+windowFormMinimize(id : string) void
|
||||||
|
+windowFormMaximize(id : string) void
|
||||||
|
+windowFormRestore(id : string) void
|
||||||
|
+windowFormClose(id : string) void
|
||||||
|
+windowFormFocus(id : string) void
|
||||||
|
+windowFormDataUpdate(data : IWindowFormDataUpdateParams) void
|
||||||
|
+windowFormCreated() void
|
||||||
|
}
|
||||||
|
class IEventMap {
|
||||||
|
<<interface>>
|
||||||
|
}
|
||||||
|
IWindowFormEvent --|> IEventMap : 继承
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L7-L42)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L7-L42)
|
||||||
|
|
||||||
|
## 事件注册与监听机制
|
||||||
|
|
||||||
|
系统使用 `wfem`(`WindowFormEventManager` 的实例)作为 `windowFormClose` 事件的管理中心。开发者可通过 `addEventListener` 方法注册监听器,语法如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wfem.addEventListener('windowFormClose', (id) => {
|
||||||
|
// 处理关闭逻辑
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`wfem` 是基于 `EventBuilderImpl` 类构建的事件总线实例,它提供了类型安全的事件注册、移除和通知功能。`addEventListener` 方法支持可选参数,例如 `immediate` 可用于在注册时立即执行一次回调,`once` 则确保监听器仅响应一次事件。
|
||||||
|
|
||||||
|
这种机制允许任意组件在需要时订阅关闭事件,而无需直接依赖具体的窗口管理逻辑,实现了高度的解耦。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant UI as UI组件
|
||||||
|
participant Wfem as wfem(事件管理器)
|
||||||
|
participant Listener as 关闭监听器
|
||||||
|
User->>UI : 点击关闭按钮
|
||||||
|
UI->>Wfem : notifyEvent("windowFormClose", windowId)
|
||||||
|
Wfem->>Listener : 执行所有注册的监听器
|
||||||
|
Listener-->>Wfem : 返回
|
||||||
|
Wfem-->>UI : 事件处理完毕
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
|
||||||
|
## 事件广播与跨组件通信
|
||||||
|
|
||||||
|
`windowFormClose` 事件的广播能力由 `EventBuilderImpl` 类的 `notifyEvent` 方法实现。该方法遍历内部存储的所有监听器,并按顺序调用它们。这种发布-订阅模式是实现跨组件通信的关键。
|
||||||
|
|
||||||
|
其优势在于:
|
||||||
|
- **低耦合**:发送者(如窗口控件)无需知道接收者的存在,只需关注事件的发出。
|
||||||
|
- **高内聚**:每个监听器只需关注与自己相关的业务逻辑,例如一个监听器负责弹出确认框,另一个负责清理定时器。
|
||||||
|
- **可扩展性**:可以轻松添加新的监听器来增强关闭流程的功能,而无需修改现有代码。
|
||||||
|
|
||||||
|
事件广播确保了所有相关方都能及时收到关闭通知,从而协同完成复杂的清理任务,维护了系统状态的一致性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A[窗口A] --> |notifyEvent| B(wfem)
|
||||||
|
C[窗口B] --> |notifyEvent| B
|
||||||
|
D[其他组件] --> |notifyEvent| B
|
||||||
|
B --> E[监听器1]
|
||||||
|
B --> F[监听器2]
|
||||||
|
B --> G[监听器N]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 资源清理与内存泄漏防护
|
||||||
|
|
||||||
|
正确处理 `windowFormClose` 事件是防止内存泄漏的核心环节。一个完整的清理流程应包括:
|
||||||
|
|
||||||
|
1. **DOM 移除**:从虚拟 DOM 和真实 DOM 中卸载窗口及其子组件。
|
||||||
|
2. **状态更新**:在全局状态 store(如 Pinia 或 Vuex)中移除对应窗口的状态条目。
|
||||||
|
3. **事件解绑**:移除该窗口实例上注册的所有自定义事件监听器。
|
||||||
|
4. **资源释放**:清除与该窗口关联的定时器(`setInterval`, `setTimeout`)、WebSocket 连接或其他长生命周期对象。
|
||||||
|
|
||||||
|
`EventBuilderImpl` 内部通过 `Set` 数据结构管理监听器,并在每次事件通知后检查 `once` 标志,自动清理一次性监听器。此外,`destroy` 方法可用于彻底清空所有事件处理器,为整个事件管理器的销毁提供支持。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 完整示例:带确认对话框的关闭流程
|
||||||
|
|
||||||
|
以下是一个典型的 `windowFormClose` 事件监听器实现,集成了用户确认对话框和资源释放逻辑:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wfem.addEventListener('windowFormClose', async (id) => {
|
||||||
|
const shouldClose = await showConfirmDialog(`确定要关闭窗口 ${id} 吗?`);
|
||||||
|
if (!shouldClose) return; // 用户取消,中断关闭流程
|
||||||
|
|
||||||
|
// 1. 清理特定于该窗口的资源
|
||||||
|
cleanupWindowResources(id);
|
||||||
|
|
||||||
|
// 2. 从状态store中移除窗口记录
|
||||||
|
windowStore.removeWindow(id);
|
||||||
|
|
||||||
|
// 3. 触发DOM移除(通常由组件自身在状态变更后自动处理)
|
||||||
|
console.log(`窗口 ${id} 已成功关闭并清理`);
|
||||||
|
}, { once: false }); // 保持监听器长期有效
|
||||||
|
```
|
||||||
|
|
||||||
|
在此示例中:
|
||||||
|
- 使用异步函数等待用户确认。
|
||||||
|
- 若用户取消,则直接返回,阻止后续清理操作。
|
||||||
|
- 按照逻辑顺序执行资源清理、状态更新等步骤。
|
||||||
|
- 监听器设置为持久性(`once: false`),以便能响应未来可能发生的同类型事件。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
`windowFormClose` 事件是 Vue 桌面应用中窗口管理模块的基石。它通过清晰的接口定义、高效的事件广播机制和严谨的资源清理策略,确保了窗口实例能够被安全、可靠地销毁。借助 `wfem` 事件总线,系统实现了组件间的松耦合通信,使得复杂的关闭流程可以被分解为多个独立、可维护的监听器。遵循本文档所述的最佳实践,可以有效避免内存泄漏,保障应用的长期稳定运行。
|
||||||
76
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口创建完成事件.md
Normal file
76
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口创建完成事件.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 窗口创建完成事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowFormService.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [事件语义与调用时机](#事件语义与调用时机)
|
||||||
|
2. [应用场景分析](#应用场景分析)
|
||||||
|
3. [事件监听代码范例](#事件监听代码范例)
|
||||||
|
4. [生命周期时序关系](#生命周期时序关系)
|
||||||
|
|
||||||
|
## 事件语义与调用时机
|
||||||
|
|
||||||
|
`windowFormCreated` 事件在新窗口实例成功挂载并完成首次渲染后触发,标志着窗口已完全初始化并可交互。该事件不携带任何参数,作为全局窗口创建完成的信号。
|
||||||
|
|
||||||
|
根据 `WindowFormEventManager.ts` 中的定义,此事件是 `IWindowFormEvent` 接口的一部分,由 `wfem` 事件管理器负责分发。虽然当前实现中未直接显示触发逻辑,但结合 `WindowFormService.ts` 的窗口创建流程可知,该事件应在 `createWindow` 方法执行完毕、DOM 元素已添加至页面且应用内容加载完成后被通知。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L41-L41)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L83-L118)
|
||||||
|
|
||||||
|
## 应用场景分析
|
||||||
|
|
||||||
|
### 启动引导
|
||||||
|
在系统启动或模块初始化过程中,通过监听 `windowFormCreated` 事件可以确保所有核心窗口均已准备就绪,从而安全地执行后续引导逻辑,如自动聚焦主窗口或初始化关联组件。
|
||||||
|
|
||||||
|
### 任务栏更新
|
||||||
|
当新窗口创建完成后,任务栏组件可通过监听该事件实时更新其窗口列表,确保用户界面状态与实际运行情况保持同步。
|
||||||
|
|
||||||
|
### 快捷方式激活
|
||||||
|
从桌面快捷方式启动应用时,该事件可用于确认目标窗口已成功打开,进而执行焦点切换或动画展示等增强用户体验的操作。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L41-L41)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L83-L118)
|
||||||
|
|
||||||
|
## 事件监听代码范例
|
||||||
|
|
||||||
|
以下为监听 `windowFormCreated` 事件并执行后续操作的典型代码模式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { wfem } from '@/events/WindowFormEventManager'
|
||||||
|
|
||||||
|
// 监听窗口创建完成事件
|
||||||
|
wfem.addEventListener('windowFormCreated', () => {
|
||||||
|
// 执行日志记录
|
||||||
|
console.log('新窗口创建完成')
|
||||||
|
|
||||||
|
// 执行自动聚焦或其他初始化操作
|
||||||
|
// focusMainWindow()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
此类监听器常用于执行一次性初始化任务,例如设置默认焦点、注册快捷键、加载用户偏好设置或发送性能监控指标。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L83-L118)
|
||||||
|
|
||||||
|
## 生命周期时序关系
|
||||||
|
|
||||||
|
`windowFormCreated` 事件作为窗口生命周期的起点,在以下关键阶段之后发生:
|
||||||
|
1. **CREATING**:窗口对象创建
|
||||||
|
2. **LOADING**:DOM 元素构建与插入
|
||||||
|
3. **ACTIVE**:窗口激活并获得焦点
|
||||||
|
|
||||||
|
它早于任何用户交互事件(如 `windowFormFocus`)和状态变更事件(如 `windowFormMinimize`),是首个表示窗口已进入稳定可用状态的全局事件。
|
||||||
|
|
||||||
|
与其他事件相比,`windowFormCreated` 是唯一无参数的创建完成信号,而其他事件如 `windowFormDataUpdate` 则携带具体的状态数据。这种设计使其成为理想的初始化钩子点。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L41-L41)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L83-L118)
|
||||||
149
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口数据更新事件.md
Normal file
149
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口数据更新事件.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 窗口数据更新事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowFormService.ts)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [IWindowFormDataUpdateParams接口字段语义解析](#iwindowformdataupdateparams接口字段语义解析)
|
||||||
|
3. [TWindowFormState类型关联说明](#twindowformstatetype类型关联说明)
|
||||||
|
4. [窗口拖拽、缩放与状态切换中的统一元数据发送机制](#窗口拖拽缩放与状态切换中的统一元数据发送机制)
|
||||||
|
5. [接收端批量更新UI的TypeScript示例](#接收端批量更新ui的typescript示例)
|
||||||
|
6. [高频更新性能优化建议:防抖策略](#高频更新性能优化建议防抖策略)
|
||||||
|
7. [窗口状态实时同步与持久化存储支持机制](#窗口状态实时同步与持久化存储支持机制)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
`windowFormDataUpdate` 事件是桌面应用中用于传递窗口最新元数据的核心通信机制。该事件在窗口发生位置移动、尺寸调整或状态变更(如最小化、最大化)时触发,通过 `IWindowFormDataUpdateParams` 接口统一封装窗口的ID、状态、尺寸和坐标信息,并广播至所有监听者。此设计实现了窗口状态变化的集中通知与响应,为UI同步、布局管理及状态持久化提供了基础支撑。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L37-L37)
|
||||||
|
|
||||||
|
## IWindowFormDataUpdateParams接口字段语义解析
|
||||||
|
`IWindowFormDataUpdateParams` 接口定义了窗口更新事件所携带的数据结构,包含以下关键字段:
|
||||||
|
|
||||||
|
- **id**: 字符串类型,表示窗口的唯一标识符。该ID由系统生成并贯穿窗口生命周期,用于精确识别和定位特定窗口实例。
|
||||||
|
- **state**: 枚举类型 `TWindowFormState`,表示当前窗口的状态,包括 `'default'`(默认)、`'minimized'`(最小化)、`'maximized'`(最大化)三种可能值。
|
||||||
|
- **width**: 数字类型,表示窗口当前的宽度(单位:像素),反映窗口水平方向的实际尺寸。
|
||||||
|
- **height**: 数字类型,表示窗口当前的高度(单位:像素),反映窗口垂直方向的实际尺寸。
|
||||||
|
- **x**: 数字类型,表示窗口左上角相对于屏幕原点的X轴坐标(单位:像素),用于确定窗口的水平位置。
|
||||||
|
- **y**: 数字类型,表示窗口左上角相对于屏幕原点的Y轴坐标(单位:像素),用于确定窗口的垂直位置。
|
||||||
|
|
||||||
|
这些字段共同构成了窗口的完整元数据快照,确保接收方能够准确还原窗口的视觉表现和行为状态。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L44-L57)
|
||||||
|
|
||||||
|
## TWindowFormState类型关联说明
|
||||||
|
`TWindowFormState` 是一个字符串联合类型,明确定义了窗口可处于的三种核心状态:
|
||||||
|
- `'default'`: 窗口处于正常显示状态,既非最大化也非最小化。
|
||||||
|
- `'minimized'`: 窗口被最小化,通常从主视图隐藏,仅在任务栏或启动器中保留图标。
|
||||||
|
- `'maximized'`: 窗口被最大化,占据除任务栏外的整个屏幕空间。
|
||||||
|
|
||||||
|
该类型与 `IWindowFormDataUpdateParams.state` 字段直接关联,作为事件数据的一部分,在窗口状态发生变化时(例如用户点击最大化按钮),新的状态值将被封装进事件参数中并广播出去。这种强类型的约束保证了状态传递的准确性与一致性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
|
||||||
|
## 窗口拖拽、缩放与状态切换中的统一元数据发送机制
|
||||||
|
无论窗口因何种操作而改变其外观或状态,系统均通过 `windowFormDataUpdate` 事件统一发送最新的元数据。具体流程如下:
|
||||||
|
|
||||||
|
1. **拖拽操作**:当用户拖动窗口标题栏时,鼠标移动事件会持续更新窗口的 `x` 和 `y` 坐标。在每次坐标更新后,系统调用 `setActiveWindow` 激活该窗口,并最终通过事件总线触发 `windowFormDataUpdate`,附带更新后的坐标、尺寸及当前状态。
|
||||||
|
2. **缩放操作**:当用户调整窗口大小时,`setWindowSize` 方法被调用以更新 `width` 和 `height`。此方法不仅修改DOM样式,还会通知事件总线,从而触发包含新尺寸的 `windowFormDataUpdate` 事件。
|
||||||
|
3. **状态切换**:无论是最小化、最大化还是还原操作,都会先调用 `updateWindowState` 修改内部状态机,随后根据新状态调整DOM表现(如隐藏元素或全屏展示)。最后,系统发出 `windowFormDataUpdate` 事件,其中 `state` 字段反映最新状态,同时附带相应的坐标和尺寸信息。
|
||||||
|
|
||||||
|
这一机制确保了所有窗口变更都通过单一入口进行通知,简化了状态管理逻辑,避免了多事件源导致的不一致问题。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[用户操作] --> B{操作类型}
|
||||||
|
B --> |拖拽| C[更新 x, y]
|
||||||
|
B --> |缩放| D[更新 width, height]
|
||||||
|
B --> |状态切换| E[更新 state]
|
||||||
|
C --> F[调用 setActiveWindow]
|
||||||
|
D --> G[调用 setWindowSize]
|
||||||
|
E --> H[调用 updateWindowState]
|
||||||
|
F --> I[触发 windowFormDataUpdate]
|
||||||
|
G --> I
|
||||||
|
H --> I
|
||||||
|
I --> J[接收端更新UI]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L512-L552)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L248-L304)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L179-L213)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L512-L552)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L248-L304)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L179-L213)
|
||||||
|
|
||||||
|
## 接收端批量更新UI的TypeScript示例
|
||||||
|
接收 `windowFormDataUpdate` 事件后,应使用接收到的完整数据对象一次性批量更新UI,避免逐个属性设置带来的性能开销和视觉闪烁。以下为推荐的处理方式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wfem.addEventListener('windowFormDataUpdate', (data) => {
|
||||||
|
const { id, state, width, height, x, y } = data;
|
||||||
|
const windowElement = document.getElementById(`window-${id}`);
|
||||||
|
|
||||||
|
if (!windowElement) return;
|
||||||
|
|
||||||
|
// 批量更新样式属性
|
||||||
|
Object.assign(windowElement.style, {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
display: state === 'minimized' ? 'none' : 'block'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据状态添加CSS类以支持主题化样式
|
||||||
|
windowElement.classList.toggle('maximized', state === 'maximized');
|
||||||
|
windowElement.classList.toggle('minimized', state === 'minimized');
|
||||||
|
windowElement.classList.toggle('default', state === 'default');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
上述代码利用 `Object.assign` 对目标DOM元素的样式进行原子性更新,确保浏览器只进行一次重排(reflow)和重绘(repaint),从而提升渲染效率。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L37-L37)
|
||||||
|
|
||||||
|
## 高频更新性能优化建议:防抖策略
|
||||||
|
由于拖拽和缩放操作会产生大量连续的 `windowFormDataUpdate` 事件,若不对处理函数加以节流,可能导致UI线程阻塞或频繁重绘,影响用户体验。为此,建议对接收端的事件处理器应用防抖(debounce)技术:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function debounce<T extends (...args: any[]) => void>(func: T, delay: number): T {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => func.apply(this, args), delay);
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedHandler = debounce((data: IWindowFormDataUpdateParams) => {
|
||||||
|
// 执行UI更新逻辑
|
||||||
|
}, 16); // 约60fps的间隔
|
||||||
|
|
||||||
|
wfem.addEventListener('windowFormDataUpdate', debouncedHandler);
|
||||||
|
```
|
||||||
|
|
||||||
|
通过设置约16毫秒的延迟(对应60Hz刷新率),可以有效过滤掉中间过渡状态,仅处理最终稳定的位置或尺寸,显著降低计算负担并提升流畅度。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L44-L57)
|
||||||
|
|
||||||
|
## 窗口状态实时同步与持久化存储支持机制
|
||||||
|
`windowFormDataUpdate` 事件不仅是UI更新的驱动源,也为跨组件通信和状态持久化提供了可靠的数据通道。任何需要感知窗口状态的模块(如任务栏、窗口管理器、布局保存服务)均可订阅此事件,实现状态的实时同步。
|
||||||
|
|
||||||
|
此外,通过监听该事件,可将窗口的 `id`、`state`、`width`、`height`、`x`、`y` 等元数据记录到本地存储(如localStorage)或远程服务器,实现用户偏好的记忆功能。例如,在用户下次启动应用时,系统可根据存储的历史数据恢复窗口的原始位置和大小,提供一致的使用体验。
|
||||||
|
|
||||||
|
该事件的设计使得状态采集与业务逻辑解耦,任何持久化逻辑只需作为事件观察者存在,无需侵入窗口控制核心代码,符合关注点分离原则。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L37-L37)
|
||||||
|
- [WindowFormService.ts](file://src/services/WindowService.ts#L67-L118)
|
||||||
114
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最大化事件.md
Normal file
114
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最大化事件.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 窗口最大化事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [basic.css](file://src/css/basic.css)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [引言](#引言)
|
||||||
|
2. [事件触发机制](#事件触发机制)
|
||||||
|
3. [状态映射与UI响应](#状态映射与ui响应)
|
||||||
|
4. [CSS视觉变换实现](#css视觉变换实现)
|
||||||
|
5. [TypeScript代码范例](#typescript代码范例)
|
||||||
|
6. [多窗口协调机制](#多窗口协调机制)
|
||||||
|
7. [总结](#总结)
|
||||||
|
|
||||||
|
## 引言
|
||||||
|
本节全面阐述`windowFormMaximize`事件的触发条件与交互逻辑。当用户点击窗口最大化按钮时,系统将广播该事件并携带窗口ID至所有订阅者,驱动UI进入全屏布局模式。此机制是桌面级应用中实现窗口管理的核心部分。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L1-L60)
|
||||||
|
|
||||||
|
## 事件触发机制
|
||||||
|
`windowFormMaximize`事件定义于`IWindowFormEvent`接口中,其函数签名接受一个字符串类型的窗口ID参数:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
windowFormMaximize: (id: string) => void;
|
||||||
|
```
|
||||||
|
|
||||||
|
该事件由全局事件管理器`wfem`(即`EventBuilderImpl<IWindowFormEvent>`实例)负责分发。当用户交互触发最大化行为时,调用`notifyEvent('windowFormMaximize', windowId)`方法,向所有监听此事件的组件广播通知。
|
||||||
|
|
||||||
|
事件系统基于观察者模式实现,支持动态注册和注销监听器,并可通过配置项实现立即执行、单次监听等功能。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L17-L17)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 状态映射与UI响应
|
||||||
|
`windowFormMaximize`事件与`TWindowFormState`类型中的`maximized`状态存在直接映射关系。`TWindowFormState`定义如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
||||||
|
```
|
||||||
|
|
||||||
|
当事件被触发后,相关组件会更新对应窗口的状态为`maximized`,并通过响应式机制驱动视图重绘。通常结合`windowFormDataUpdate`事件同步窗口尺寸与位置信息,确保状态一致性。
|
||||||
|
|
||||||
|
状态变更不仅影响当前窗口的显示模式,还可能触发任务栏图标高亮、Z轴层级调整等副作用,以保证用户体验的一致性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L50-L55)
|
||||||
|
|
||||||
|
## CSS视觉变换实现
|
||||||
|
窗口最大化过程中的视觉效果主要通过CSS变换实现。虽然具体样式未在核心逻辑文件中体现,但可推断其依赖以下机制:
|
||||||
|
|
||||||
|
- 使用`transform: scale()`或`width/height: 100%`实现尺寸扩展
|
||||||
|
- 配合过渡动画(transition)实现平滑缩放
|
||||||
|
- 利用`z-index`控制多窗口堆叠顺序
|
||||||
|
- 可能结合`position: fixed`或`absolute`脱离文档流进行定位
|
||||||
|
|
||||||
|
基础样式文件`basic.css`提供了通用的盒模型重置、字体设置及响应式支持,为窗口动画提供稳定的样式环境。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [basic.css](file://src/css/basic.css#L1-L134)
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
|
||||||
|
## TypeScript代码范例
|
||||||
|
使用`wfem`监听并处理最大化行为的标准TypeScript代码范例如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 订阅窗口最大化事件
|
||||||
|
wfem.addEventListener('windowFormMaximize', (id: string) => {
|
||||||
|
// 更新窗口状态
|
||||||
|
const window = getWindowById(id);
|
||||||
|
if (window) {
|
||||||
|
window.state = 'maximized';
|
||||||
|
// 触发DOM重绘
|
||||||
|
redrawWindow(window);
|
||||||
|
// 持久化状态(可选)
|
||||||
|
saveWindowState(id, 'maximized');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送最大化事件
|
||||||
|
function maximizeWindow(id: string) {
|
||||||
|
wfem.notifyEvent('windowFormMaximize', id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议在状态变更后调用重绘函数,并考虑将用户偏好(如是否最大化)持久化至`localStorage`,以便页面刷新后恢复。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L70-L75)
|
||||||
|
|
||||||
|
## 多窗口协调机制
|
||||||
|
在多窗口堆叠场景下,`windowFormMaximize`事件起到关键协调作用。当某一窗口最大化时:
|
||||||
|
|
||||||
|
- 其他非最小化窗口应自动退至后台
|
||||||
|
- 最大化窗口获得最高`z-index`层级
|
||||||
|
- 任务栏对应图标处于激活状态
|
||||||
|
- 若存在模态窗口,则需特殊处理避免遮挡
|
||||||
|
|
||||||
|
事件广播机制确保所有关注窗口状态的模块(如任务栏、窗口管理器、快捷键服务)能同步响应,维持系统整体状态一致。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L17-L17)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
`windowFormMaximize`事件作为窗口控制系统的重要组成部分,实现了从用户操作到UI响应的完整闭环。它通过标准化的事件总线机制解耦组件间通信,结合类型安全的状态定义与CSS视觉变换,构建出流畅且可维护的桌面级交互体验。在复杂多窗口环境中,该事件有效协调各组件行为,保障系统稳定性与一致性。
|
||||||
96
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最小化事件.md
Normal file
96
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口最小化事件.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 窗口最小化事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用的文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [事件机制概述](#事件机制概述)
|
||||||
|
2. [事件触发与参数传递](#事件触发与参数传递)
|
||||||
|
3. [状态同步与枚举值应用](#状态同步与枚举值应用)
|
||||||
|
4. [监听器注册与Pinia状态更新](#监听器注册与pinia状态更新)
|
||||||
|
5. [事件解耦设计分析](#事件解耦设计分析)
|
||||||
|
|
||||||
|
## 事件机制概述
|
||||||
|
|
||||||
|
`windowFormMinimize` 事件是窗口管理系统中的核心交互事件之一,用于响应用户点击窗口最小化按钮的操作。该事件通过全局事件管理器 `wfem` 进行广播,采用发布-订阅模式实现组件间的松耦合通信。事件定义在 `IWindowFormEvent` 接口中,并由 `EventBuilderImpl` 类提供具体的事件注册与通知能力。
|
||||||
|
|
||||||
|
此事件机制的设计使得任意组件均可独立监听窗口最小化行为,而无需直接依赖窗口实例本身,从而提升了系统的可维护性与扩展性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L12-L12)
|
||||||
|
|
||||||
|
## 事件触发与参数传递
|
||||||
|
|
||||||
|
当用户点击窗口的最小化按钮时,系统会调用 `wfem.notifyEvent('windowFormMinimize', id)` 方法触发该事件。其中 `id` 为窗口的唯一标识符(字符串类型),作为事件回调函数的参数传递给所有监听者。
|
||||||
|
|
||||||
|
事件的接口定义如下:
|
||||||
|
```ts
|
||||||
|
windowFormMinimize: (id: string) => void;
|
||||||
|
```
|
||||||
|
这确保了所有监听器都能接收到被最小化的窗口ID,进而执行相应的UI动画或逻辑处理。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L12-L12)
|
||||||
|
|
||||||
|
## 状态同步与枚举值应用
|
||||||
|
|
||||||
|
在接收到 `windowFormMinimize` 事件后,系统通常会将对应窗口的状态更新为 `'minimized'`。该状态值来源于 `TWindowFormState` 枚举类型,其定义如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type TWindowFormState = 'default' | 'minimized' | 'maximized';
|
||||||
|
```
|
||||||
|
|
||||||
|
通过将窗口状态设置为 `'minimized'`,可以驱动视图层进行相应的渲染更新,例如隐藏窗口内容、在任务栏显示缩略图标等。这种基于枚举的状态管理模式增强了代码的可读性和类型安全性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
|
||||||
|
## 监听器注册与Pinia状态更新
|
||||||
|
|
||||||
|
在Vue组件中,可通过 `wfem.addEventListener` 方法注册对 `windowFormMinimize` 事件的监听。典型用法如下所示:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
wfem.addEventListener('windowFormMinimize', (id: string) => {
|
||||||
|
// 更新Pinia store中的窗口状态
|
||||||
|
const windowStore = useWindowStore();
|
||||||
|
windowStore.updateState(id, 'minimized');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
上述代码展示了如何在事件回调中获取窗口ID并更新Pinia状态仓库中的窗口状态字段。这种方式实现了视图与状态的分离,符合现代前端架构的最佳实践。
|
||||||
|
|
||||||
|
尽管当前项目中未包含 `useWindowStore` 的具体实现,但基于现有技术栈(Vue + Pinia)可合理推断其存在类似的窗口状态管理模块。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
|
||||||
|
## 事件解耦设计分析
|
||||||
|
|
||||||
|
`windowFormMinimize` 事件体现了典型的事件驱动解耦设计。多个窗口组件可以各自注册监听器,独立响应最小化事件,而无需彼此知晓或直接通信。事件的发布者仅需调用 `notifyEvent`,由 `EventBuilderImpl` 负责遍历所有订阅者并执行回调。
|
||||||
|
|
||||||
|
该设计的优势包括:
|
||||||
|
- **高内聚低耦合**:各组件专注于自身逻辑,不依赖其他窗口的状态。
|
||||||
|
- **易于扩展**:新增窗口组件只需添加监听器即可参与事件流。
|
||||||
|
- **便于测试**:事件监听逻辑可单独单元测试,无需完整UI环境。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[用户点击最小化按钮] --> B[触发 windowFormMinimize 事件]
|
||||||
|
B --> C{通知所有监听者}
|
||||||
|
C --> D[窗口A监听器: 执行最小化动画]
|
||||||
|
C --> E[窗口B监听器: 更新状态为 minimized]
|
||||||
|
C --> F[任务栏组件: 显示最小化图标]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L12-L12)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L50-L75)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L50-L75)
|
||||||
131
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口聚焦事件.md
Normal file
131
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口聚焦事件.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<cite>
|
||||||
|
**本文档中引用的文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [useClickFocus.ts](file://src/common/hooks/useClickFocus.ts)
|
||||||
|
- [useObservableVue.ts](file://src/common/hooks/useObservableVue.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [窗口聚焦事件](#窗口聚焦事件)
|
||||||
|
2. [用户交互背景](#用户交互背景)
|
||||||
|
3. [技术实现机制](#技术实现机制)
|
||||||
|
4. [多窗口层级管理](#多窗口层级管理)
|
||||||
|
5. [Vue组件中的事件监听](#vue组件中的事件监听)
|
||||||
|
6. [响应式状态更新](#响应式状态更新)
|
||||||
|
7. [用户体验影响](#用户体验影响)
|
||||||
|
|
||||||
|
## 窗口聚焦事件
|
||||||
|
|
||||||
|
`windowFormFocus` 事件是桌面级Vue应用中用于管理窗口激活状态的核心事件。当用户点击或通过其他方式激活某个窗口时,该事件被触发并广播被激活窗口的唯一标识符(id),从而驱动UI层面对应的视觉反馈和状态变更。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L32-L32)
|
||||||
|
|
||||||
|
## 用户交互背景
|
||||||
|
|
||||||
|
在模拟传统桌面操作系统的Web应用中,用户期望拥有与原生应用一致的窗口交互体验。当用户点击一个处于非顶层的窗口时,该窗口应当自动移动到所有其他窗口的前面,并获得输入焦点。这种行为符合用户的直觉认知,即“所见即所得”的交互原则。`windowFormFocus` 事件正是为了捕捉这一关键的用户意图而设计的,它作为用户操作与系统响应之间的桥梁,确保了界面行为的可预测性和一致性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L30-L34)
|
||||||
|
|
||||||
|
## 技术实现机制
|
||||||
|
|
||||||
|
`windowFormFocus` 事件的技术实现依赖于一个名为 `wfem` (Window Form Event Manager) 的全局事件管理器实例,该实例基于 `EventBuilderImpl` 类构建,提供了一套完整的发布-订阅模式接口。
|
||||||
|
|
||||||
|
事件的触发流程如下:
|
||||||
|
1. **事件定义**:在 `IWindowFormEvent` 接口中明确定义了 `windowFormFocus(id: string)` 方法签名。
|
||||||
|
2. **事件触发**:当检测到用户对特定窗口的点击操作时,业务逻辑代码会调用 `wfem.notifyEvent('windowFormFocus', windowId)` 来广播该事件。
|
||||||
|
3. **事件监听**:任何需要对此事件做出反应的组件都可以通过 `wfem.addEventListener('windowFormFocus', callback)` 注册一个回调函数。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant Window as 窗口组件
|
||||||
|
participant Hook as useClickFocus Hook
|
||||||
|
participant EventManager as wfem (事件管理器)
|
||||||
|
User->>Window : 点击窗口
|
||||||
|
Window->>Hook : 触发内部处理
|
||||||
|
Hook->>EventManager : 调用 notifyEvent('windowFormFocus', id)
|
||||||
|
EventManager->>EventManager : 遍历所有监听器
|
||||||
|
loop 通知所有订阅者
|
||||||
|
EventManager-->>Subscriber : 执行注册的回调函数
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [useClickFocus.ts](file://src/common/hooks/useClickFocus.ts#L13-L64)
|
||||||
|
|
||||||
|
## 多窗口层级管理
|
||||||
|
|
||||||
|
在存在多个重叠窗口的应用场景下,维护正确的渲染顺序至关重要。`windowFormFocus` 事件在此扮演了核心角色。每当该事件被触发,其携带的窗口ID会被传递给负责管理窗口栈的逻辑模块。该模块会根据ID找到对应的窗口元素,并将其CSS `z-index` 属性提升至最高层级,同时移除之前处于顶层窗口的焦点样式,并为当前窗口应用新的焦点样式(如高亮边框)。这一系列操作确保了被激活的窗口始终位于最前端,解决了视觉遮挡问题,使用户能够清晰地识别出当前正在操作的对象。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L30-L34)
|
||||||
|
|
||||||
|
## Vue组件中的事件监听
|
||||||
|
|
||||||
|
在Vue组件中监听 `windowFormFocus` 事件是一个标准的异步编程实践。开发者可以在组件的 `setup` 函数或 `onMounted` 生命周期钩子中,使用 `wfem.addEventListener` 方法注册一个监听器。最佳实践是将此逻辑封装在一个自定义Hook中,以保证代码的复用性和可维护性。例如,可以创建一个 `useWindowFocusListener` Hook,在组件挂载时订阅事件,并在组件卸载前 (`onBeforeUnmount`) 及时移除监听器,防止内存泄漏。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[组件挂载] --> B["调用 wfem.addEventListener('windowFormFocus', callback)"]
|
||||||
|
B --> C[事件管理器注册回调]
|
||||||
|
D[用户点击窗口] --> E["触发 wfem.notifyEvent('windowFormFocus', id)"]
|
||||||
|
E --> F[事件管理器遍历并执行所有回调]
|
||||||
|
F --> G[组件中的回调函数被调用]
|
||||||
|
G --> H[更新组件内部状态]
|
||||||
|
I[组件卸载] --> J["调用 wfem.removeEventListener 移除监听"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [useClickFocus.ts](file://src/common/hooks/useClickFocus.ts#L13-L64)
|
||||||
|
|
||||||
|
## 响应式状态更新
|
||||||
|
|
||||||
|
为了实现高效且直观的状态管理,建议结合 `useObservableVue` 这一自定义Hook来处理 `windowFormFocus` 事件带来的状态变化。`useObservableVue` 将一个实现了 `IObservable<T>` 接口的可观察对象转换为Vue的响应式对象。当 `windowFormFocus` 事件触发时,其回调函数可以直接修改存储在可观察对象中的 `activeWindow` 状态。由于 `useObservableVue` 内部建立了双向同步机制,对可观察对象的任何修改都会立即反映到Vue的响应式系统中,从而自动触发相关组件的重新渲染,无需手动调用 `forceUpdate` 或进行复杂的 `$emit` 操作。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IObservable~T~ {
|
||||||
|
<<interface>>
|
||||||
|
+subscribe(callback) : () => void
|
||||||
|
+toRefsProxy() : T
|
||||||
|
}
|
||||||
|
class ObservableImpl~T~ {
|
||||||
|
-state : T
|
||||||
|
-observers : Set~() => void~
|
||||||
|
+subscribe(callback) : () => void
|
||||||
|
+toRefsProxy() : T
|
||||||
|
+notify() : void
|
||||||
|
}
|
||||||
|
class useObservableVue {
|
||||||
|
+useObservable(observable) : Reactive~T~
|
||||||
|
}
|
||||||
|
class Component {
|
||||||
|
+setup()
|
||||||
|
+template
|
||||||
|
}
|
||||||
|
IObservable~T~ <|.. ObservableImpl~T~ : 实现
|
||||||
|
useObservableVue --> IObservable~T~ : 使用
|
||||||
|
Component --> useObservableVue : 调用
|
||||||
|
ObservableImpl~T~ --> Component : 通过响应式更新视图
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useObservableVue.ts](file://src/common/hooks/useObservableVue.ts#L13-L87)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L1-L96)
|
||||||
|
|
||||||
|
## 用户体验影响
|
||||||
|
|
||||||
|
`windowFormFocus` 事件对桌面级应用的用户体验具有决定性的影响。它直接决定了应用是否能提供流畅、自然的多任务操作环境。一个正确实现的窗口聚焦机制能够:
|
||||||
|
- **降低认知负荷**:用户无需思考哪个窗口是活动的,视觉反馈清晰明确。
|
||||||
|
- **提高操作效率**:通过简单的点击即可切换上下文,符合肌肉记忆。
|
||||||
|
- **增强应用可信度**:其行为与用户熟悉的操作系统保持一致,提升了应用的专业感和可靠性。
|
||||||
|
|
||||||
|
反之,如果该事件处理不当,例如出现窗口无法置顶、焦点丢失或样式错乱等问题,将严重破坏用户对应用的信任,导致挫败感和使用障碍。因此,确保 `windowFormFocus` 事件的稳定性和准确性是构建高质量桌面级Web应用的关键基石。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L30-L34)
|
||||||
196
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口表单事件管理器.md
Normal file
196
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口表单事件管理器.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# 窗口表单事件管理器
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用的文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [窗口生命周期事件体系](#窗口生命周期事件体系)
|
||||||
|
3. [IWindowFormDataUpdateParams数据结构分析](#iwindowformdataupdateparams数据结构分析)
|
||||||
|
4. [wfem实例共享通信机制](#wfem实例共享通信机制)
|
||||||
|
5. [Vue组件中事件监听代码范例](#vue组件中事件监听代码范例)
|
||||||
|
6. [事件解耦带来的低耦合优势](#事件解耦带来的低耦合优势)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
本系统通过`WindowFormEventManager`模块提供了一套完整的窗口生命周期事件管理体系,用于统一管理桌面应用中各类窗口的状态变化与用户交互行为。该体系基于观察者模式实现,支持多个窗口组件之间高效、安全地进行状态同步和通信。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L1-L60)
|
||||||
|
|
||||||
|
## 窗口生命周期事件体系
|
||||||
|
系统定义了七种核心窗口事件,分别对应不同的用户操作或状态变更场景:
|
||||||
|
|
||||||
|
### windowFormMinimize(窗口最小化)
|
||||||
|
当用户点击窗口最小化按钮时触发,通知相关组件当前窗口已进入最小化状态。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L10-L13)
|
||||||
|
|
||||||
|
### windowFormMaximize(窗口最大化)
|
||||||
|
当用户点击窗口最大化按钮时触发,表示窗口已切换至全屏展示模式。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L15-L18)
|
||||||
|
|
||||||
|
### windowFormRestore(窗口还原)
|
||||||
|
在窗口处于最大化或最小化状态下,用户执行还原操作时触发,表明窗口恢复为正常尺寸。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L20-L23)
|
||||||
|
|
||||||
|
### windowFormClose(窗口关闭)
|
||||||
|
用户请求关闭窗口时触发,可用于执行清理逻辑或确认提示。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L25-L28)
|
||||||
|
|
||||||
|
### windowFormFocus(窗口聚焦)
|
||||||
|
当窗口获得焦点(即被激活)时触发,常用于高亮显示当前活动窗口。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L30-L33)
|
||||||
|
|
||||||
|
### windowFormDataUpdate(窗口数据更新)
|
||||||
|
每当窗口的位置、大小或状态发生变化时触发,携带完整的窗口元信息。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L35-L39)
|
||||||
|
|
||||||
|
### windowFormCreated(窗口创建完成)
|
||||||
|
新窗口初始化完毕后触发一次,标志窗口已准备就绪可交互。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L41-L43)
|
||||||
|
|
||||||
|
## IWindowFormDataUpdateParams数据结构分析
|
||||||
|
此接口定义了窗口状态更新时传递的数据结构,各字段含义如下:
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| id | string | 唯一标识一个窗口实例的ID |
|
||||||
|
| state | TWindowFormState | 当前窗口的视觉状态 |
|
||||||
|
| width | number | 窗口当前像素宽度 |
|
||||||
|
| height | number | 窗口当前像素高度 |
|
||||||
|
| x | number | 窗口左上角相对于屏幕的X坐标 |
|
||||||
|
| y | number | 窗口左上角相对于屏幕的Y坐标 |
|
||||||
|
|
||||||
|
其中,`TWindowFormState` 是一个联合类型,定义于 `WindowFormTypes.ts` 文件中,包含三种可能取值:
|
||||||
|
- `'default'`:默认状态(正常大小)
|
||||||
|
- `'minimized'`:最小化状态
|
||||||
|
- `'maximized'`:最大化状态
|
||||||
|
|
||||||
|
这些字段共同构成了窗口的完整状态快照,便于其他组件根据最新布局信息做出响应。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IWindowFormDataUpdateParams {
|
||||||
|
+id : string
|
||||||
|
+state : TWindowFormState
|
||||||
|
+width : number
|
||||||
|
+height : number
|
||||||
|
+x : number
|
||||||
|
+y : number
|
||||||
|
}
|
||||||
|
class TWindowFormState {
|
||||||
|
<<type>>
|
||||||
|
'default'
|
||||||
|
'minimized'
|
||||||
|
'maximized'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L44-L57)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L44-L57)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L9-L9)
|
||||||
|
|
||||||
|
## wfem实例共享通信机制
|
||||||
|
`wfem` 是一个全局唯一的 `EventBuilderImpl<IWindowFormEvent>` 实例,作为事件中心被所有窗口组件共享使用。其工作原理如下:
|
||||||
|
|
||||||
|
1. **事件注册**:任意组件可通过 `wfem.addEventListener(eventName, handler)` 订阅特定事件。
|
||||||
|
2. **事件分发**:当窗口状态改变时,调用 `wfem.notifyEvent(eventName, args)` 广播事件。
|
||||||
|
3. **跨组件通信**:不同组件间无需直接引用,仅通过事件总线即可实现松散耦合的状态同步。
|
||||||
|
|
||||||
|
这种设计使得各个窗口组件可以独立开发、测试和维护,同时又能实时感知彼此的状态变化。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant ComponentA as 组件A
|
||||||
|
participant ComponentB as 组件B
|
||||||
|
participant Wfem as wfem事件中心
|
||||||
|
ComponentA->>Wfem : addEventListener("focus", handler)
|
||||||
|
ComponentB->>Wfem : addEventListener("close", handler)
|
||||||
|
Note over Wfem : 多个组件注册监听
|
||||||
|
Window->>Wfem : notifyEvent("focus", "win123")
|
||||||
|
Wfem->>ComponentA : 执行handler("win123")
|
||||||
|
Window->>Wfem : notifyEvent("close", "win123")
|
||||||
|
Wfem->>ComponentB : 执行handler("win123")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L1-L95)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
|
||||||
|
## Vue组件中事件监听代码范例
|
||||||
|
以下是在Vue 3组件中监听窗口聚焦与关闭事件的典型用法:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { wfem } from '@/events/WindowFormEventManager';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
const handleFocus = (id: string) => {
|
||||||
|
console.log(`窗口 ${id} 获得焦点`);
|
||||||
|
// 更新UI状态,如添加边框高亮
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (id: string) => {
|
||||||
|
console.log(`窗口 ${id} 已关闭`);
|
||||||
|
// 清理资源,移除DOM节点等
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
wfem.addEventListener('windowFormFocus', handleFocus);
|
||||||
|
wfem.addEventListener('windowFormClose', handleClose);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
wfem.removeEventListener('windowFormFocus', handleFocus);
|
||||||
|
wfem.removeEventListener('windowFormClose', handleClose);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
上述代码展示了如何在组件挂载时注册事件监听,并在卸载前正确移除,避免内存泄漏。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L30-L39)
|
||||||
|
|
||||||
|
## 事件解耦带来的低耦合优势
|
||||||
|
采用事件驱动架构后,各组件之间的依赖关系显著降低,具体优势包括:
|
||||||
|
|
||||||
|
- **独立性增强**:组件无需知道谁会发送或接收事件,只需关注自身职责。
|
||||||
|
- **可维护性提升**:修改某一组件不会影响其他不相关的模块。
|
||||||
|
- **扩展性强**:新增功能只需监听已有事件,无需改动现有逻辑。
|
||||||
|
- **测试更简单**:可单独对每个组件进行单元测试,模拟事件输入即可验证行为。
|
||||||
|
|
||||||
|
通过 `wfem` 这一统一事件通道,系统实现了高度模块化的设计,有利于长期迭代和团队协作开发。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IEventBuilder.ts](file://src/events/IEventBuilder.ts#L1-L46)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L1-L95)
|
||||||
120
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口还原事件.md
Normal file
120
.qoder/repowiki/zh/content/事件系统/窗口表单事件管理器/窗口还原事件.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# 窗口还原事件
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用的文件**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心设计与运行机制](#核心设计与运行机制)
|
||||||
|
3. [事件协同关系分析](#事件协同关系分析)
|
||||||
|
4. [Vue组件中的实践应用](#vue组件中的实践应用)
|
||||||
|
5. [在窗口状态机中的角色](#在窗口状态机中的角色)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
`windowFormRestore` 事件是桌面应用中用于处理窗口从最大化或最小化状态恢复为正常尺寸的关键事件。该事件通过传递窗口唯一标识符(id)来定位目标窗口实例,并触发相应的UI布局重置逻辑。结合 `windowFormDataUpdate` 事件,可实现窗口位置与尺寸信息的同步更新,确保用户界面状态的一致性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L22-L22)
|
||||||
|
|
||||||
|
## 核心设计与运行机制
|
||||||
|
`windowFormRestore` 事件定义于 `IWindowFormEvent` 接口中,其函数签名接受一个字符串类型的 `id` 参数,用以标识被操作的窗口实例。当用户点击“还原”按钮或通过快捷键执行还原操作时,系统将调用事件管理器 `wfem` 的 `notifyEvent` 方法,广播该事件。
|
||||||
|
|
||||||
|
事件的实际通知行为由通用事件构建器 `EventBuilderImpl` 实现,它维护了一个基于 `Map` 的监听器集合,支持动态添加、移除和触发事件回调。整个流程具备良好的解耦性和扩展性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[用户触发窗口还原] --> B[调用 wfem.notifyEvent("windowFormRestore", id)]
|
||||||
|
B --> C{是否存在监听器?}
|
||||||
|
C --> |是| D[遍历执行所有注册的 handler]
|
||||||
|
C --> |否| E[无操作]
|
||||||
|
D --> F[各组件响应还原逻辑]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L22-L22)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L7-L60)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L7-L95)
|
||||||
|
|
||||||
|
## 事件协同关系分析
|
||||||
|
`windowFormRestore` 通常与 `windowFormDataUpdate` 事件协同工作。前者仅通知“还原”动作的发生,而后者负责携带具体的窗口状态数据(包括坐标 x/y 和宽高 width/height),实现精确的状态同步。
|
||||||
|
|
||||||
|
例如,在窗口还原过程中:
|
||||||
|
1. 首先触发 `windowFormRestore(id)`,通知所有监听者窗口即将还原;
|
||||||
|
2. 紧接着发送 `windowFormDataUpdate({ id, state: 'default', x, y, width, height })`,提供新的布局参数;
|
||||||
|
3. UI 组件接收到数据后,更新 DOM 元素的位置与尺寸。
|
||||||
|
|
||||||
|
这种分离设计提高了系统的灵活性和可维护性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as 用户
|
||||||
|
participant WM as 窗口管理器
|
||||||
|
participant EM as 事件管理器(wfem)
|
||||||
|
participant UI as UI组件
|
||||||
|
User->>WM : 执行还原操作
|
||||||
|
WM->>EM : notifyEvent("windowFormRestore", id)
|
||||||
|
EM->>UI : 执行所有 windowFormRestore 回调
|
||||||
|
WM->>EM : notifyEvent("windowFormDataUpdate", {id, state, x, y, width, height})
|
||||||
|
EM->>UI : 执行所有 windowFormDataUpdate 回调
|
||||||
|
UI->>UI : 更新DOM样式与位置
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L37-L37)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L5-L9)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L37-L37)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L5-L9)
|
||||||
|
|
||||||
|
## Vue组件中的实践应用
|
||||||
|
在 Vue 组件中,可通过 `wfem.addEventListener` 监听 `windowFormRestore` 事件,并结合响应式数据重置 UI 布局。建议使用 CSS 过渡动画提升用户体验。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class WindowComponent {
|
||||||
|
+windowId : string
|
||||||
|
+position : WindowFormPos
|
||||||
|
+size : {width : number, height : number}
|
||||||
|
+mounted()
|
||||||
|
+beforeUnmount()
|
||||||
|
}
|
||||||
|
class EventManager {
|
||||||
|
+addEventListener(event, handler)
|
||||||
|
+removeEventListener(event, handler)
|
||||||
|
+notifyEvent(event, ...args)
|
||||||
|
}
|
||||||
|
WindowComponent --> EventManager : 使用 wfem
|
||||||
|
note right of WindowComponent
|
||||||
|
在 mounted 中注册事件监听,
|
||||||
|
在 beforeUnmount 中清理资源
|
||||||
|
end note
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L1-L5)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L60-L60)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L1-L10)
|
||||||
|
|
||||||
|
## 在窗口状态机中的角色
|
||||||
|
`windowFormRestore` 是窗口状态机中从 `maximized` 或 `minimized` 转换至 `default` 状态的关键触发点。它标志着窗口生命周期中的一个重要转变节点,常伴随以下行为:
|
||||||
|
- 恢复原始位置与尺寸
|
||||||
|
- 重新激活窗口焦点
|
||||||
|
- 触发内容区域重渲染
|
||||||
|
- 更新任务栏图标状态
|
||||||
|
|
||||||
|
该事件与其他窗口控制事件(如 `windowFormMinimize`, `windowFormMaximize`)共同构成了完整的状态转换网络,支撑起复杂的桌面交互体验。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [WindowFormEventManager.ts](file://src/events/WindowFormEventManager.ts#L7-L42)
|
||||||
|
- [WindowFormTypes.ts](file://src/ui/types/WindowFormTypes.ts#L5-L9)
|
||||||
193
.qoder/repowiki/zh/content/响应式布局系统/响应式布局系统.md
Normal file
193
.qoder/repowiki/zh/content/响应式布局系统/响应式布局系统.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<cite>
|
||||||
|
**本文档中引用的文件**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [basic.css](file://src/css/basic.css)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心组件分析](#核心组件分析)
|
||||||
|
3. [响应式网格布局机制](#响应式网格布局机制)
|
||||||
|
4. [图标位置管理与持久化](#图标位置管理与持久化)
|
||||||
|
5. [视觉呈现与基础样式](#视觉呈现与基础样式)
|
||||||
|
6. [依赖关系图](#依赖关系图)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本技术文档深入剖析了基于CSS Grid的动态桌面布局系统。该系统以`useDesktopContainerInit`这一核心Vue组合式函数(Hook)为基础,实现了高度响应式的桌面容器功能。通过集成ResizeObserver API、Vue的响应式系统以及localStorage,该系统能够根据容器尺寸动态调整网格布局,并智能地重新排列桌面图标,同时将用户自定义的布局状态持久化存储。整体架构结合了现代前端框架特性与原生Web API,构建了一个灵活、可扩展且用户体验良好的虚拟桌面环境。
|
||||||
|
|
||||||
|
## 核心组件分析
|
||||||
|
|
||||||
|
系统的核心逻辑封装在`useDesktopContainerInit`函数中,该函数作为Vue 3的组合式API被`DesktopContainer.vue`组件所调用。其主要职责是初始化并管理整个桌面容器的状态,包括网格模板参数、计算样式以及桌面图标数据。该函数返回一个包含`gridTemplate`、`appIconsRef`和`gridStyle`三个关键属性的对象,为上层组件提供完整的布局控制能力。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class useDesktopContainerInit {
|
||||||
|
+container : HTMLElement
|
||||||
|
+gridTemplate : IGridTemplateParams
|
||||||
|
+gridStyle : ComputedRef~Object~
|
||||||
|
+ro : ResizeObserver
|
||||||
|
+appIconsRef : Ref~Array<IDesktopAppIcon>~
|
||||||
|
+exceedApp : Ref~Array<IDesktopAppIcon>~
|
||||||
|
+useDesktopContainerInit(containerStr : string) : Object
|
||||||
|
}
|
||||||
|
class IGridTemplateParams {
|
||||||
|
+cellExpectWidth : number
|
||||||
|
+cellExpectHeight : number
|
||||||
|
+cellRealWidth : number
|
||||||
|
+cellRealHeight : number
|
||||||
|
+gapX : number
|
||||||
|
+gapY : number
|
||||||
|
+colCount : number
|
||||||
|
+rowCount : number
|
||||||
|
}
|
||||||
|
class IDesktopAppIcon {
|
||||||
|
+name : string
|
||||||
|
+icon : string
|
||||||
|
+path : string
|
||||||
|
+x : number
|
||||||
|
+y : number
|
||||||
|
}
|
||||||
|
useDesktopContainerInit --> IGridTemplateParams : "包含"
|
||||||
|
useDesktopContainerInit --> IDesktopAppIcon : "管理"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L3-L20)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 响应式网格布局机制
|
||||||
|
|
||||||
|
### 动态网格计算流程
|
||||||
|
|
||||||
|
系统的响应式能力源于对`ResizeObserver`的巧妙运用。当`DesktopContainer`组件挂载时,`useDesktopContainerInit`会创建一个`ResizeObserver`实例,并将其绑定到指定的容器元素(如`.desktop-icons-container`)。每当容器的尺寸发生变化,观察者回调就会被触发,执行一系列精确的计算来更新网格布局。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[容器尺寸变化] --> B{ResizeObserver 触发}
|
||||||
|
B --> C[获取容器实际宽高]
|
||||||
|
C --> D[计算列数 colCount]
|
||||||
|
D --> E[计算行数 rowCount]
|
||||||
|
E --> F[计算单元格实际宽度 cellRealWidth]
|
||||||
|
F --> G[计算单元格实际高度 cellRealHeight]
|
||||||
|
G --> H[更新 gridTemplate 响应式对象]
|
||||||
|
H --> I[computed 自动更新 gridStyle]
|
||||||
|
I --> J[DOM 中的 style 属性更新]
|
||||||
|
J --> K[浏览器重绘,应用新布局]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L37-L58)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L37-L58)
|
||||||
|
|
||||||
|
### `gridStyle` 计算属性详解
|
||||||
|
|
||||||
|
`gridStyle`是一个由`computed`创建的计算属性,它直接决定了桌面容器的CSS Grid样式。该属性依赖于`gridTemplate`中的`colCount`、`rowCount`、`cellExpectWidth`、`cellExpectHeight`、`gapX`和`gapY`等值。其核心作用是将这些数值动态地转换为标准的CSS Grid声明:
|
||||||
|
|
||||||
|
- **`gridTemplateColumns`**: 使用`repeat()`函数生成指定数量的列轨道,每列的最小值为预设宽度(`cellExpectWidth`),最大值为`1fr`,确保了列的弹性伸缩。
|
||||||
|
- **`gridTemplateRows`**: 与列同理,生成指定数量的行轨道。
|
||||||
|
- **`gap`**: 设置行与列之间的间距。
|
||||||
|
|
||||||
|
这种设计使得布局的任何变化都能立即反映在UI上,实现了真正的数据驱动视图。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L25-L35)
|
||||||
|
|
||||||
|
## 图标位置管理与持久化
|
||||||
|
|
||||||
|
### 图标初始定位与重排算法
|
||||||
|
|
||||||
|
系统通过`appIconsRef`这个`ref`对象来管理所有桌面图标的集合。在初始化时,函数会尝试从`localStorage`中读取之前保存的图标位置信息(键名为`desktopAppIconInfo`)。对于每个应用,它首先检查是否有历史记录,如果有则使用历史坐标;如果没有,则根据当前的网格行列数进行默认的蛇形排列。
|
||||||
|
|
||||||
|
当网格的行列数因容器大小改变而发生变化时,`watch`监听器会被激活,调用`rearrangeIcons`函数对图标进行智能重排。该算法的核心逻辑如下:
|
||||||
|
1. 遍历现有图标,优先保留那些仍在新网格范围内的图标。
|
||||||
|
2. 对于超出新网格范围或需要移动的图标,尝试在新的网格空间内寻找空闲的位置进行放置。
|
||||||
|
3. 如果没有足够的空间,则将无法显示的图标放入`exceedApp`数组中(可用于后续的“更多”菜单展示)。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Container as DesktopContainer.vue
|
||||||
|
participant Hook as useDesktopContainerInit.ts
|
||||||
|
participant Observer as ResizeObserver
|
||||||
|
participant Storage as localStorage
|
||||||
|
Observer->>Hook : resize事件触发
|
||||||
|
Hook->>Hook : 计算新colCount, rowCount
|
||||||
|
Hook->>Hook : 更新gridTemplate响应式对象
|
||||||
|
Hook->>Hook : watch检测到变化
|
||||||
|
Hook->>Hook : 调用rearrangeIcons()
|
||||||
|
Hook->>Hook : 返回新的appIcons和hideAppIcons
|
||||||
|
Hook->>Hook : 更新appIconsRef.value
|
||||||
|
Hook->>Storage : 将appIconsRef.value序列化并存入localStorage
|
||||||
|
Hook-->>Container : 提供更新后的appIconsRef和gridStyle
|
||||||
|
Container->>Container : Vue自动更新DOM
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L78-L94)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L78-L94)
|
||||||
|
|
||||||
|
### 布局持久化实现
|
||||||
|
|
||||||
|
为了保证用户的个性化设置不丢失,系统利用`localStorage`实现了布局的持久化。通过另一个`watch`监听器,每当`appIconsRef`的值发生改变(无论是因为重排还是用户拖拽),都会立即将最新的图标数组序列化为JSON字符串,并存储到`localStorage`中。当下次页面加载时,初始化代码会优先读取这段存储的数据,从而恢复用户上次的桌面布局。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L88-L92)
|
||||||
|
|
||||||
|
## 视觉呈现与基础样式
|
||||||
|
|
||||||
|
### AppIcon 组件的样式绑定
|
||||||
|
|
||||||
|
`AppIcon.vue`组件负责渲染单个桌面图标。它通过`style`属性直接绑定了`grid-column`和`grid-row`这两个CSS Grid属性,其值来源于`iconInfo`对象的`x`和`y`坐标。例如,`grid-column: 2 / 3`表示该图标占据第2列。这种绑定方式使得图标的物理位置完全由其数据模型决定,实现了布局的动态化。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L2-L8)
|
||||||
|
|
||||||
|
### basic.css 的基础样式作用
|
||||||
|
|
||||||
|
`basic.css`文件提供了整个应用的基础样式规则,为动态布局奠定了视觉基调。它包含了:
|
||||||
|
- **盒模型重置**: 统一使用`border-box`,简化尺寸计算。
|
||||||
|
- **根元素变量**: 定义了字体、颜色、间距等CSS自定义属性,便于全局主题管理。
|
||||||
|
- **基础元素样式**: 对`body`、`a`、`button`等元素进行了基础美化。
|
||||||
|
- **实用工具类**: 如`.container`用于创建居中的内容区域。
|
||||||
|
- **响应式支持**: 包含了针对减少动画偏好的媒体查询。
|
||||||
|
|
||||||
|
虽然`DesktopContainer`和`AppIcon`组件使用了`scoped`样式,但`basic.css`提供的全局基础样式确保了整个应用的一致性和可用性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [basic.css](file://src/css/basic.css#L1-L134)
|
||||||
|
|
||||||
|
## 依赖关系图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[basic.css] --> |提供基础样式| B(DesktopContainer.vue)
|
||||||
|
C[useDesktopContainerInit.ts] --> |导出核心逻辑| B
|
||||||
|
B --> |使用| C
|
||||||
|
B --> |渲染| D(AppIcon.vue)
|
||||||
|
D --> |接收props| C
|
||||||
|
C --> |读写| E[localStorage]
|
||||||
|
C --> |监听| F[ResizeObserver]
|
||||||
|
F --> |响应| G[容器尺寸变化]
|
||||||
|
C --> |类型定义| H[IGridTemplateParams.ts]
|
||||||
|
C --> |类型定义| I[IDesktopAppIcon.ts]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
- [basic.css](file://src/css/basic.css#L1-L134)
|
||||||
124
.qoder/repowiki/zh/content/响应式布局系统/图标重排与持久化.md
Normal file
124
.qoder/repowiki/zh/content/响应式布局系统/图标重排与持久化.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 图标重排与持久化
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [appIconsRef的创建过程](#appiconsref的创建过程)
|
||||||
|
2. [localStorage数据同步机制](#localstorage数据同步机制)
|
||||||
|
3. [网格变化监听与重排响应](#网格变化监听与重排响应)
|
||||||
|
4. [rearrangeIcons算法详解](#rearrangeicons算法详解)
|
||||||
|
5. [布局状态持久化策略](#布局状态持久化策略)
|
||||||
|
|
||||||
|
## appIconsRef的创建过程
|
||||||
|
|
||||||
|
`appIconsRef` 是一个 Vue 响应式引用,用于管理桌面图标的布局状态。其创建过程始于 `useDesktopContainerInit` 函数的调用,该函数接收容器选择器字符串作为参数并初始化核心布局逻辑。
|
||||||
|
|
||||||
|
在初始化过程中,系统首先从 `localStorage` 中读取键为 `desktopAppIconInfo` 的存储项,尝试恢复之前保存的图标位置信息。若存在历史数据,则解析为 `oldAppIcons` 数组;否则使用空数组作为默认值。随后,系统遍历当前可用的应用程序列表(`appInfos`),为每个应用创建对应的桌面图标对象。
|
||||||
|
|
||||||
|
对于每个新生成的图标,系统优先检查是否存在同名的历史图标记录。如果存在,则继承其坐标(x, y);若不存在,则根据当前网格的行列数按索引自动分配初始坐标:
|
||||||
|
- 列坐标 x = 当前索引 % 行数 + 1
|
||||||
|
- 行坐标 y = floor(当前索引 / 行数) + 1
|
||||||
|
|
||||||
|
最终,这些图标数据被封装为响应式引用 `appIconsRef`,供视图层绑定使用。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L74-L94)
|
||||||
|
|
||||||
|
## localStorage数据同步机制
|
||||||
|
|
||||||
|
系统通过双向数据绑定机制实现 `appIconsRef` 与 `localStorage` 的实时同步。当用户对桌面图标进行拖拽、重排等操作导致布局变更时,Vue 的响应式系统会触发相应的监听器,将最新状态持久化到本地存储中。
|
||||||
|
|
||||||
|
具体而言,系统注册了一个针对 `appIconsRef.value` 的 `watch` 监听器。每当图标数组内容发生变化(如新增、删除或位置调整),该监听器便会执行回调函数,将更新后的 `appIcons` 数组序列化为 JSON 字符串,并通过 `localStorage.setItem('desktopAppIconInfo', ...)` 方法写入浏览器本地存储。
|
||||||
|
|
||||||
|
此机制确保了用户在刷新页面或重新打开应用后,能够恢复上次关闭时的桌面布局,实现了跨会话的个性化配置记忆功能。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L89-L92)
|
||||||
|
|
||||||
|
## 网格变化监听与重排响应
|
||||||
|
|
||||||
|
为了适应不同屏幕尺寸和窗口大小的变化,系统利用 `ResizeObserver` API 实时监测桌面容器的尺寸变动,并动态计算最优的网格列数(`colCount`)和行数(`rowCount`)。当这些参数发生改变时,系统需要智能地重新排列所有图标以避免重叠或溢出。
|
||||||
|
|
||||||
|
为此,系统设置了一个复合监听器 `watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ...)`, 专门监控 `colCount` 和 `rowCount` 的联合变化。一旦检测到新的网格维度,监听器立即调用 `rearrangeIcons` 函数,传入当前图标列表及新的行列限制,执行自动重排逻辑。
|
||||||
|
|
||||||
|
该监听器包含优化判断:若新旧行列数完全一致,则直接返回,避免不必要的重排计算,提升性能效率。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L86-L88)
|
||||||
|
|
||||||
|
## rearrangeIcons算法详解
|
||||||
|
|
||||||
|
`rearrangeIcons` 函数是整个图标管理系统的核心算法,负责处理图标冲突、寻找空闲位置以及管理超出可视范围的图标。其输入为原始图标数组和目标网格的最大行列数,输出为包含正常显示图标和隐藏图标的结构体。
|
||||||
|
|
||||||
|
### 冲突检测与占用标记
|
||||||
|
|
||||||
|
算法首先创建一个 `Set<string>` 类型的 `occupied` 集合,用于记录已被占用的网格单元。通过辅助函数 `key(x, y)` 将二维坐标转换为唯一字符串标识(如 `"1,2"`),实现高效的哈希查找。
|
||||||
|
|
||||||
|
### 分阶段处理流程
|
||||||
|
|
||||||
|
1. **第一阶段:保留有效位置**
|
||||||
|
- 遍历所有图标,筛选出位于当前网格范围内的图标(即 `x ≤ maxCol && y ≤ maxRow`)
|
||||||
|
- 检查目标位置是否已被占用,若未占用则将其加入结果数组 `appIcons` 并标记为已占用
|
||||||
|
- 对于位置无效或冲突的图标,则暂存至临时数组 `temp`
|
||||||
|
|
||||||
|
2. **第二阶段:填补空位**
|
||||||
|
- 遍历 `temp` 数组中的待安置图标
|
||||||
|
- 若当前已放置图标数量小于网格总容量(`maxCol * maxRow`),则从左上角 `(1,1)` 开始逐行扫描,寻找第一个空闲位置进行安置
|
||||||
|
- 一旦找到合适位置,立即跳出内层循环,继续处理下一个图标
|
||||||
|
|
||||||
|
3. **第三阶段:处理溢出图标**
|
||||||
|
- 若网格已满且仍有剩余图标无法安置,则将其归类至 `hideAppIcons` 数组
|
||||||
|
- 这些图标将在 UI 层面被隐藏,防止界面混乱
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始重排]) --> ValidatePosition["验证图标位置有效性"]
|
||||||
|
ValidatePosition --> InRange{"是否在网格范围内?"}
|
||||||
|
InRange --> |是| CheckOccupied["检查位置是否被占用"]
|
||||||
|
InRange --> |否| ToTemp["加入临时数组 temp"]
|
||||||
|
CheckOccupied --> IsFree{"位置空闲?"}
|
||||||
|
IsFree --> |是| PlaceIcon["放置图标并标记占用"]
|
||||||
|
IsFree --> |否| ToTemp
|
||||||
|
PlaceIcon --> NextIcon["处理下一个图标"]
|
||||||
|
ToTemp --> NextIcon
|
||||||
|
NextIcon --> AllProcessed{"所有图标处理完毕?"}
|
||||||
|
AllProcessed --> |否| ValidatePosition
|
||||||
|
AllProcessed --> |是| FillEmpty["填补空位"]
|
||||||
|
FillEmpty --> HasSpace{"仍有空位?"}
|
||||||
|
HasSpace --> |是| FindSlot["从(1,1)开始寻找空位"]
|
||||||
|
HasSpace --> |否| HideExcess["隐藏超出图标"]
|
||||||
|
FindSlot --> CanPlace{"能否放置?"}
|
||||||
|
CanPlace --> |是| UpdateAppIcons["更新 appIcons 数组"]
|
||||||
|
CanPlace --> |否| HideExcess
|
||||||
|
UpdateAppIcons --> MoreTemp{"temp 数组为空?"}
|
||||||
|
MoreTemp --> |否| FillEmpty
|
||||||
|
MoreTemp --> |是| ReturnResult["返回结果: appIcons + hideAppIcons"]
|
||||||
|
HideExcess --> ReturnResult
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
## 布局状态持久化策略
|
||||||
|
|
||||||
|
系统的布局持久化策略建立在 Vue 的响应式系统与浏览器本地存储的协同工作之上。`appIconsRef` 作为单一数据源(Single Source of Truth),集中管理所有图标的坐标信息。任何对图标的修改操作(无论是用户交互还是程序逻辑)都会反映到该引用上。
|
||||||
|
|
||||||
|
通过 `watch` 监听器,系统实现了从内存状态到持久化存储的单向同步。这种设计具有以下优势:
|
||||||
|
|
||||||
|
- **自动同步**:无需手动调用保存方法,所有变更自动记录
|
||||||
|
- **原子性保证**:每次写入都是完整的数组快照,避免部分更新导致的数据不一致
|
||||||
|
- **跨会话恢复**:页面刷新后可通过 `localStorage.getItem('desktopAppIconInfo')` 重建初始状态
|
||||||
|
- **容错处理**:使用 `JSON.parse(... || '[]')` 确保解析失败时返回安全默认值
|
||||||
|
|
||||||
|
该策略构成了完整的“读取→运行→修改→保存”闭环,保障了用户体验的一致性和数据的安全性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L89-L92)
|
||||||
221
.qoder/repowiki/zh/content/响应式布局系统/布局初始化逻辑.md
Normal file
221
.qoder/repowiki/zh/content/响应式布局系统/布局初始化逻辑.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# 布局初始化逻辑
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [核心组件分析](#核心组件分析)
|
||||||
|
3. [生命周期与容器初始化](#生命周期与容器初始化)
|
||||||
|
4. [DOM查询机制与containerStr参数](#dom查询机制与containerstr参数)
|
||||||
|
5. [网格模板初始状态设计](#网格模板初始状态设计)
|
||||||
|
6. [ResizeObserver响应式尺寸监听](#resizeobserver响应式尺寸监听)
|
||||||
|
7. [图标重排逻辑](#图标重排逻辑)
|
||||||
|
8. [依赖关系图](#依赖关系图)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
`useDesktopContainerInit` 是一个 Vue 3 组合式 API Hook,用于在桌面容器组件挂载时初始化其布局系统。该 Hook 负责建立基于 CSS Grid 的动态网格布局,并通过 `ResizeObserver` 实现响应式行为,确保桌面图标能根据容器尺寸自动调整排列方式。
|
||||||
|
|
||||||
|
## 核心组件分析
|
||||||
|
|
||||||
|
### useDesktopContainerInit 功能概览
|
||||||
|
此 Hook 封装了桌面容器的核心初始化逻辑,包括:
|
||||||
|
- 容器元素的 DOM 查询与绑定
|
||||||
|
- 网格布局参数的初始化与响应式管理
|
||||||
|
- 容器尺寸变化的监听与处理
|
||||||
|
- 桌面图标的持久化存储与位置管理
|
||||||
|
- 图标超出可视区域时的隐藏策略
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 生命周期与容器初始化
|
||||||
|
|
||||||
|
### onMounted 中的初始化流程
|
||||||
|
`useDesktopContainerInit` 利用 Vue 的 `onMounted` 生命周期钩子,在组件挂载完成后立即执行容器初始化操作。这一时机确保了对应的 DOM 元素已经存在于页面中,可以安全地进行查询和观察。
|
||||||
|
|
||||||
|
在 `onMounted` 回调中,Hook 首先通过 `document.querySelector(containerStr)` 获取指定的容器元素引用,并将其赋值给局部变量 `container`。随后,立即调用 `ResizeObserver` 的 `observe` 方法开始监听该容器的尺寸变化。
|
||||||
|
|
||||||
|
当组件被卸载时,`onUnmounted` 钩子会确保清理工作正确执行:停止对容器的观察(`unobserve`)并断开 `ResizeObserver` 实例(`disconnect`),防止内存泄漏。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Component as "DesktopContainer组件"
|
||||||
|
participant Hook as "useDesktopContainerInit"
|
||||||
|
participant DOM as "浏览器DOM"
|
||||||
|
participant Observer as "ResizeObserver"
|
||||||
|
Component->>Hook : setup()
|
||||||
|
Hook->>Hook : 初始化gridTemplate等响应式数据
|
||||||
|
Component->>Component : 渲染完成
|
||||||
|
Component->>Hook : onMounted触发
|
||||||
|
Hook->>DOM : querySelector(containerStr)
|
||||||
|
DOM-->>Hook : 返回容器元素
|
||||||
|
Hook->>Observer : new ResizeObserver(callback)
|
||||||
|
Hook->>Observer : observe(container)
|
||||||
|
Note over Hook,Observer : 开始监听容器尺寸变化
|
||||||
|
loop 每次窗口或容器大小改变
|
||||||
|
Observer->>Observer : 触发回调
|
||||||
|
Observer->>Hook : 执行回调函数
|
||||||
|
Hook->>Hook : 重新计算gridTemplate参数
|
||||||
|
Hook->>Hook : 更新gridStyle
|
||||||
|
end
|
||||||
|
Component->>Component : 组件即将销毁
|
||||||
|
Component->>Hook : onUnmounted触发
|
||||||
|
Hook->>Observer : unobserve(container)
|
||||||
|
Hook->>Observer : disconnect()
|
||||||
|
Note over Hook,Observer : 清理资源,防止内存泄漏
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L70-L85)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L70-L85)
|
||||||
|
|
||||||
|
## DOM查询机制与containerStr参数
|
||||||
|
|
||||||
|
### containerStr 参数的作用
|
||||||
|
`containerStr` 是传递给 `useDesktopContainerInit` 函数的一个字符串参数,它代表一个 CSS 选择器。该选择器用于定位需要初始化的桌面容器 DOM 元素。
|
||||||
|
|
||||||
|
在当前实现中,`DesktopContainer.vue` 组件传入的值为 `.desktop-icons-container`,这是一个类选择器,指向模板中具有该类名的 `<div>` 元素。这种设计使得 Hook 具有良好的通用性,可以在不同的容器上复用,只需传入相应的选择器即可。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 DesktopContainer.vue 中的调用示例
|
||||||
|
const { appIconsRef, gridStyle, gridTemplate } = useDesktopContainerInit('.desktop-icons-container')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L10-L13)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14)
|
||||||
|
|
||||||
|
## 网格模板初始状态设计
|
||||||
|
|
||||||
|
### gridTemplate 初始值的设计意图
|
||||||
|
`gridTemplate` 对象使用 Vue 的 `reactive` 函数创建,使其成为一个响应式对象。其初始状态的设计体现了以下几个关键考量:
|
||||||
|
|
||||||
|
| 属性 | 默认值 | 设计意图 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| `cellExpectWidth` | 90 | 预设每个单元格的理想宽度(像素),作为网格列宽的基础 |
|
||||||
|
| `cellExpectHeight` | 110 | 预设每个单元格的理想高度(像素),作为网格行高的基础 |
|
||||||
|
| `gapX` / `gapY` | 4 | 设置行列之间的间隙,提供视觉呼吸空间,避免图标紧贴 |
|
||||||
|
| `colCount` / `rowCount` | 1 | 初始行列数设为1,表示最小的网格结构,将在首次ResizeObserver回调中被实际尺寸覆盖 |
|
||||||
|
|
||||||
|
这些默认值共同定义了一个合理的起始布局,即使在 `ResizeObserver` 回调执行前,也能保证界面有一个基本的、可预测的显示状态。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class IGridTemplateParams {
|
||||||
|
+readonly cellExpectWidth : number
|
||||||
|
+readonly cellExpectHeight : number
|
||||||
|
+cellRealWidth : number
|
||||||
|
+cellRealHeight : number
|
||||||
|
+gapX : number
|
||||||
|
+gapY : number
|
||||||
|
+colCount : number
|
||||||
|
+rowCount : number
|
||||||
|
}
|
||||||
|
class useDesktopContainerInit {
|
||||||
|
-container : HTMLElement
|
||||||
|
-gridTemplate : IGridTemplateParams
|
||||||
|
-gridStyle : ComputedRef
|
||||||
|
-ro : ResizeObserver
|
||||||
|
+useDesktopContainerInit(containerStr : string) : Object
|
||||||
|
}
|
||||||
|
useDesktopContainerInit --> IGridTemplateParams : "包含"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L4-L20)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L16-L32)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L16-L32)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L4-L20)
|
||||||
|
|
||||||
|
## ResizeObserver响应式尺寸监听
|
||||||
|
|
||||||
|
### 回调中的尺寸计算逻辑
|
||||||
|
`ResizeObserver` 的回调函数是实现响应式布局的核心。每当容器的尺寸发生变化时,该回调都会被触发,执行以下关键步骤:
|
||||||
|
|
||||||
|
1. **获取容器实际尺寸**:通过 `container.getBoundingClientRect()` 获取容器当前的精确几何信息。
|
||||||
|
2. **计算行列数量**:利用容器宽度和预设单元格宽度(含间隙)计算出应显示的列数和行数。
|
||||||
|
```javascript
|
||||||
|
colCount = Math.floor((width + gapX) / (expectWidth + gapX))
|
||||||
|
```
|
||||||
|
3. **计算实际单元格尺寸**:在确定了行列数后,重新分配容器内部空间,计算出每个单元格的实际宽高,确保网格完全填充容器且无多余空白。
|
||||||
|
```javascript
|
||||||
|
realWidth = (totalWidth - totalGapX) / colCount
|
||||||
|
```
|
||||||
|
|
||||||
|
这种“期望尺寸 -> 计算行列 -> 反推实际尺寸”的模式,既保证了图标的相对大小一致性,又实现了完美的空间利用率。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L45-L65)
|
||||||
|
|
||||||
|
## 图标重排逻辑
|
||||||
|
|
||||||
|
### rearrangeIcons 函数的工作机制
|
||||||
|
当网格的行列数发生变化时,`watch` 监听器会触发 `rearrangeIcons` 函数,负责重新安排所有桌面图标的可见性和位置。
|
||||||
|
|
||||||
|
该函数的主要逻辑如下:
|
||||||
|
1. 创建一个 `Set` 来记录已被占用的网格坐标 `(x,y)`。
|
||||||
|
2. 遍历所有图标,优先将位于新网格范围内的图标保留在原位。
|
||||||
|
3. 对于超出新网格范围的图标,尝试在网格内寻找空闲位置进行安置。
|
||||||
|
4. 如果网格已满,则将无法放置的图标放入 `hideAppIcons` 数组,这些图标将被隐藏。
|
||||||
|
|
||||||
|
这确保了用户界面的连续性,尽可能保留用户的原有布局习惯。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[开始重排图标] --> B{遍历所有图标}
|
||||||
|
B --> C["检查(x,y)是否在maxCol/maxRow范围内"]
|
||||||
|
C --> |是| D["检查该位置是否已被占用"]
|
||||||
|
D --> |否| E["将图标加入appIcons数组<br/>标记位置为已占用"]
|
||||||
|
D --> |是| F["跳过,不添加"]
|
||||||
|
C --> |否| G["将图标暂存到temp数组"]
|
||||||
|
G --> H{遍历temp数组}
|
||||||
|
H --> I["检查appIcons数组是否已满"]
|
||||||
|
I --> |否| J["在网格中寻找第一个空位"]
|
||||||
|
J --> K["将图标放入空位<br/>标记位置为已占用"]
|
||||||
|
K --> L["加入appIcons数组"]
|
||||||
|
I --> |是| M["加入hideAppIcons数组"]
|
||||||
|
M --> N[结束]
|
||||||
|
L --> N
|
||||||
|
E --> N
|
||||||
|
F --> N
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
## 依赖关系图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "UI层"
|
||||||
|
DesktopContainerVue["DesktopContainer.vue"]
|
||||||
|
AppIconVue["AppIcon.vue"]
|
||||||
|
UseHook["useDesktopContainerInit.ts"]
|
||||||
|
end
|
||||||
|
subgraph "类型定义"
|
||||||
|
IGridTemplate["IGridTemplateParams.ts"]
|
||||||
|
IDesktopAppIcon["IDesktopAppIcon.ts"]
|
||||||
|
end
|
||||||
|
DesktopContainerVue --> UseHook : "调用"
|
||||||
|
UseHook --> IGridTemplate : "导入接口"
|
||||||
|
UseHook --> IDesktopAppIcon : "导入接口"
|
||||||
|
DesktopContainerVue --> AppIconVue : "使用组件"
|
||||||
|
UseHook --> AppIconVue : "通过返回值传递数据"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
140
.qoder/repowiki/zh/content/响应式布局系统/网格参数计算机制.md
Normal file
140
.qoder/repowiki/zh/content/响应式布局系统/网格参数计算机制.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# 网格参数计算机制
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档引用文件**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [basic.css](file://src/css/basic.css)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [引言](#引言)
|
||||||
|
2. [核心数据结构定义](#核心数据结构定义)
|
||||||
|
3. [ResizeObserver中的动态网格计算逻辑](#resizeobserver中的动态网格计算逻辑)
|
||||||
|
4. [实际单元格尺寸的精确控制](#实际单元格尺寸的精确控制)
|
||||||
|
5. [gridStyle样式对象的生成与绑定](#gridstyle样式对象的生成与绑定)
|
||||||
|
6. [图标重排机制分析](#图标重排机制分析)
|
||||||
|
7. [总结](#总结)
|
||||||
|
|
||||||
|
## 引言
|
||||||
|
本项目通过Vue 3组合式API实现了一个响应式的桌面图标容器布局系统。其核心在于利用`ResizeObserver`监听容器尺寸变化,动态计算并调整CSS Grid布局的行列数及单元格大小。该机制确保在不同屏幕尺寸和窗口缩放情况下,桌面图标能够自适应排列,同时保持良好的视觉一致性与交互体验。
|
||||||
|
|
||||||
|
## 核心数据结构定义
|
||||||
|
系统通过接口`IGridTemplateParams`定义了网格布局所需的核心参数集合:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface IGridTemplateParams {
|
||||||
|
readonly cellExpectWidth: number; // 预期单元格宽度
|
||||||
|
readonly cellExpectHeight: number; // 预期单元格高度
|
||||||
|
cellRealWidth: number; // 实际单元格宽度
|
||||||
|
cellRealHeight: number; // 实际单元格高度
|
||||||
|
gapX: number; // 列间距
|
||||||
|
gapY: number; // 行间距
|
||||||
|
colCount: number; // 总列数
|
||||||
|
rowCount: number; // 总行数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这些参数构成了整个动态网格计算的基础,其中预期尺寸为设计基准值,而实际尺寸则由运行时环境动态决定。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L3-L20)
|
||||||
|
|
||||||
|
## ResizeObserver中的动态网格计算逻辑
|
||||||
|
当容器尺寸发生变化时,`ResizeObserver`回调函数会触发重新计算流程。关键步骤如下:
|
||||||
|
|
||||||
|
1. **获取容器几何信息**:通过`getBoundingClientRect()`获取当前容器的实际宽高。
|
||||||
|
2. **计算列数(colCount)**:
|
||||||
|
```ts
|
||||||
|
gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX));
|
||||||
|
```
|
||||||
|
3. **计算行数(rowCount)**:
|
||||||
|
```ts
|
||||||
|
gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY));
|
||||||
|
```
|
||||||
|
|
||||||
|
此算法的本质是将容器总宽度(或高度)加上一个间隙值,再除以“单个单元格宽度+列间距”,从而避免因浮点误差导致最后一列无法完整显示的问题。使用`Math.floor`向下取整保证结果为有效整数。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L39-L40)
|
||||||
|
|
||||||
|
## 实际单元格尺寸的精确控制
|
||||||
|
在确定了行列数量后,系统进一步计算每个单元格的实际像素尺寸,以充分利用可用空间并消除边缘空白。
|
||||||
|
|
||||||
|
### 计算公式推导
|
||||||
|
- **可用总宽度** = 容器宽度 - 所有列间隙之和
|
||||||
|
即:`w = containerRect.width - gapX * (colCount - 1)`
|
||||||
|
- **每列实际宽度** = 可用总宽度 ÷ 列数
|
||||||
|
即:`cellRealWidth = w / colCount`
|
||||||
|
|
||||||
|
同理可得行方向上的计算:
|
||||||
|
- `h = containerRect.height - gapY * (rowCount - 1)`
|
||||||
|
- `cellRealHeight = h / rowCount`
|
||||||
|
|
||||||
|
### 浮点数精度控制
|
||||||
|
由于浏览器渲染对小数像素的支持有限,直接使用浮点值可能导致布局抖动或错位。因此系统采用`toFixed(2)`保留两位小数,并通过`Number()`转换回数值类型,既保证精度又提升渲染稳定性。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
|
||||||
|
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
|
||||||
|
```
|
||||||
|
|
||||||
|
这种处理方式平衡了空间利用率与视觉平滑性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L41-L42)
|
||||||
|
|
||||||
|
## gridStyle样式对象的生成与绑定
|
||||||
|
`gridStyle`是一个基于Vue `computed`的响应式计算属性,负责将`gridTemplate`中的参数转化为标准的CSS Grid样式规则。
|
||||||
|
|
||||||
|
### 样式对象结构
|
||||||
|
```ts
|
||||||
|
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`
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键特性说明
|
||||||
|
- **`minmax()`函数应用**:确保每列最小宽度不低于`cellExpectWidth`,但允许在空间充足时扩展至等分的`1fr`比例。
|
||||||
|
- **动态重复语法**:`repeat(colCount, ...)`自动构建指定数量的轨道定义。
|
||||||
|
- **双向间隙设置**:`gap`属性分别设置横向与纵向间距。
|
||||||
|
|
||||||
|
该样式对象最终通过`:style="gridStyle"`绑定到`.desktop-icons-container`元素上,实现视图层的实时更新。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[容器尺寸变化] --> B{ResizeObserver触发}
|
||||||
|
B --> C[计算colCount/rrowCount]
|
||||||
|
C --> D[计算cellRealWidth/Height]
|
||||||
|
D --> E[更新gridTemplate响应式对象]
|
||||||
|
E --> F[gridStyle重新计算]
|
||||||
|
F --> G[DOM样式自动更新]
|
||||||
|
G --> H[完成布局重绘]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L20-L30)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L20-L30)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1)
|
||||||
|
|
||||||
|
## 图标重排机制分析
|
||||||
|
每当行列数发生变更时,系统会调用`rearrangeIcons`函数对所有图标进行位置重分配,确保其不超出可视范围且无重叠。
|
||||||
|
|
||||||
|
### 重排策略
|
||||||
|
1. **优先保留原有坐标**:若图标的原位置仍在新网格范围内且未被占用,则保留原位。
|
||||||
|
2. **空位填充机制**:对于越界或冲突的图标,遍历网格寻找首个可用空位(从左上角开始)。
|
||||||
|
3. **溢出图标管理**:当网格已满时,超出部分存入`hideAppIcons`数组,可用于后续提示用户。
|
||||||
|
|
||||||
|
该机制保障了用户体验的一致性,避免图标因窗口缩放而丢失或错乱。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L102-L156)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
本系统的动态网格计算机制充分体现了响应式设计的思想。通过结合`ResizeObserver`、Vue响应式系统与CSS Grid布局,实现了从容器尺寸→行列数量→单元格尺寸→样式绑定→图标定位的完整闭环。特别是在实际尺寸计算中对间隙总和的扣除与浮点精度的控制,展现了对细节的高度关注。整体架构清晰、逻辑严谨,具备良好的可维护性与扩展性。
|
||||||
161
.qoder/repowiki/zh/content/快速开始.md
Normal file
161
.qoder/repowiki/zh/content/快速开始.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 快速开始
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**本文档中引用的文件**
|
||||||
|
- [README.md](file://README.md)
|
||||||
|
- [package.json](file://package.json)
|
||||||
|
- [vite.config.ts](file://vite.config.ts)
|
||||||
|
- [main.ts](file://src/main.ts)
|
||||||
|
- [index.html](file://index.html)
|
||||||
|
- [tsconfig.app.json](file://tsconfig.app.json)
|
||||||
|
- [tsconfig.json](file://tsconfig.json)
|
||||||
|
- [tsconfig.node.json](file://tsconfig.node.json)
|
||||||
|
- [env.d.ts](file://env.d.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [开发环境准备](#开发环境准备)
|
||||||
|
3. [项目初始化与依赖安装](#项目初始化与依赖安装)
|
||||||
|
4. [启动开发服务器](#启动开发服务器)
|
||||||
|
5. [构建生产版本](#构建生产版本)
|
||||||
|
6. [package.json 脚本命令详解](#packagejson-脚本命令详解)
|
||||||
|
7. [常见问题排查](#常见问题排查)
|
||||||
|
8. [首次运行效果验证](#首次运行效果验证)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
`vue-desktop` 是一个基于 Vue 3 和 Vite 构建的桌面风格前端项目,模拟操作系统桌面界面,支持图标布局、窗口管理等功能。本指南旨在帮助开发者快速在本地搭建开发环境,完成依赖安装、服务启动和构建发布等核心操作,并提供新手友好的故障排查建议。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L0-L37)
|
||||||
|
|
||||||
|
## 开发环境准备
|
||||||
|
|
||||||
|
在开始之前,请确保您的开发机器满足以下最低要求:
|
||||||
|
|
||||||
|
- **浏览器支持**:Chrome 84+、Edge 84+、Firefox 79+、Safari 14+
|
||||||
|
- **Node.js 版本**:根据 `package.json` 中的 `engines` 字段,推荐使用 Node.js v20.19.0 或更高版本(v22.12.0 及以上),不支持 IE 浏览器。
|
||||||
|
- **包管理工具**:项目使用 `pnpm` 进行依赖管理,需提前安装 pnpm。
|
||||||
|
|
||||||
|
推荐开发环境为 VSCode 配合 Volar 插件,以获得最佳的 `.vue` 文件类型支持和开发体验。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L5-L7)
|
||||||
|
- [README.md](file://README.md#L0-L6)
|
||||||
|
|
||||||
|
## 项目初始化与依赖安装
|
||||||
|
|
||||||
|
进入项目根目录后,执行以下命令安装项目所需的所有依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令将根据 `package.json` 和 `pnpm-lock.yaml` 安装所有生产与开发依赖,包括 Vue 3、Pinia、Naive UI、Vite、TypeScript 等核心库。
|
||||||
|
|
||||||
|
安装完成后,您可以在 `node_modules/` 目录下看到所有已安装的依赖包。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L30-L31)
|
||||||
|
- [package.json](file://package.json#L10-L41)
|
||||||
|
|
||||||
|
## 启动开发服务器
|
||||||
|
|
||||||
|
安装完成后,可通过以下命令启动本地开发服务器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
此命令实际执行的是 `vite` 命令,启动基于 Vite 的开发服务器,具备热更新(HMR)功能,修改代码后浏览器会自动刷新。
|
||||||
|
|
||||||
|
默认情况下,开发服务器将在 `http://localhost:5173` 启动。打开浏览器访问该地址即可查看应用界面。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L33-L34)
|
||||||
|
- [package.json](file://package.json#L12)
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L0-L29)
|
||||||
|
|
||||||
|
## 构建生产版本
|
||||||
|
|
||||||
|
当需要发布应用时,运行以下命令进行生产环境构建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令会依次执行:
|
||||||
|
1. `type-check`:使用 `vue-tsc` 对项目进行类型检查
|
||||||
|
2. `build-only`:调用 `vite build` 编译并压缩代码,输出至 `dist/` 目录
|
||||||
|
|
||||||
|
构建完成后,`dist/` 文件夹将包含所有静态资源,可部署到任意静态服务器。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L35-L37)
|
||||||
|
- [package.json](file://package.json#L13-L15)
|
||||||
|
|
||||||
|
## package.json 脚本命令详解
|
||||||
|
|
||||||
|
以下是 `package.json` 中定义的主要脚本及其作用:
|
||||||
|
|
||||||
|
| 脚本名称 | 命令内容 | 功能说明 |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| `dev` | `vite` | 启动 Vite 开发服务器,支持热重载 |
|
||||||
|
| `build` | `run-p type-check "build-only {@}" --` | 并行执行类型检查和构建任务 |
|
||||||
|
| `preview` | `vite preview` | 在本地预览 `dist/` 目录中的生产构建结果 |
|
||||||
|
| `build-only` | `vite build` | 仅执行构建,不进行类型检查 |
|
||||||
|
| `type-check` | `vue-tsc --build` | 使用 `vue-tsc` 对 `.vue` 文件进行类型检查 |
|
||||||
|
| `format` | `prettier --write src/` | 使用 Prettier 格式化 `src/` 目录下的代码 |
|
||||||
|
|
||||||
|
这些脚本通过 `npm-run-all2` 实现并发执行(如 `run-p`),提升构建效率。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L11-L20)
|
||||||
|
|
||||||
|
## 常见问题排查
|
||||||
|
|
||||||
|
### Node.js 版本不兼容
|
||||||
|
|
||||||
|
如果运行 `pnpm install` 时报错提示 Node.js 版本不符合要求,请检查当前版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -v
|
||||||
|
```
|
||||||
|
|
||||||
|
确保版本符合 `^20.19.0 || >=22.12.0` 范围。建议使用 [nvm](https://github.com/nvm-sh/nvm) 管理多个 Node.js 版本。
|
||||||
|
|
||||||
|
### 依赖安装失败
|
||||||
|
|
||||||
|
若 `pnpm install` 失败,尝试以下步骤:
|
||||||
|
1. 清除缓存:`pnpm store prune`
|
||||||
|
2. 检查网络,必要时配置镜像源
|
||||||
|
3. 删除 `node_modules` 和 `pnpm-lock.yaml` 后重新安装
|
||||||
|
|
||||||
|
### 类型检查错误
|
||||||
|
|
||||||
|
运行 `pnpm type-check` 时可能出现类型错误,通常是由于 `.vue` 文件的 TypeScript 类型未被正确识别。确保已安装 Volar 插件并禁用 Vetur。
|
||||||
|
|
||||||
|
### 构建失败
|
||||||
|
|
||||||
|
构建失败可能由语法错误或路径别名问题引起。检查 `vite.config.ts` 中的 `@` 别名配置是否正确指向 `src/` 目录。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L5-L7)
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L18-L22)
|
||||||
|
- [tsconfig.app.json](file://tsconfig.app.json#L0-L24)
|
||||||
|
- [env.d.ts](file://env.d.ts#L0-L1)
|
||||||
|
|
||||||
|
## 首次运行效果验证
|
||||||
|
|
||||||
|
成功运行 `pnpm dev` 并在浏览器中打开 `http://localhost:5173` 后,您应看到一个类桌面风格的界面,包含网格布局的应用图标容器。页面标题为“vue-desktop”,DOM 结构中 `<div id="app">` 已被 Vue 应用挂载。
|
||||||
|
|
||||||
|
主组件 `App.vue` 通过 `main.ts` 初始化,使用了 Pinia 状态管理和 Naive UI 组件库。界面响应式适配窗口大小变化,图标按网格自动排列。
|
||||||
|
|
||||||
|
若能看到图标容器且无控制台报错,则表示环境搭建成功。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [index.html](file://index.html#L0-L13)
|
||||||
|
- [main.ts](file://src/main.ts#L0-L15)
|
||||||
|
- [src/ui/desktop-container/DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L0-L22)
|
||||||
|
- [src/ui/desktop-container/useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L0-L37)
|
||||||
149
.qoder/repowiki/zh/content/技术栈与依赖.md
Normal file
149
.qoder/repowiki/zh/content/技术栈与依赖.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 技术栈与依赖
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [package.json](file://package.json)
|
||||||
|
- [vite.config.ts](file://vite.config.ts)
|
||||||
|
- [tsconfig.json](file://tsconfig.json)
|
||||||
|
- [uno.config.ts](file://uno.config.ts)
|
||||||
|
- [src/main.ts](file://src/main.ts)
|
||||||
|
- [src/common/naive-ui/components.ts](file://src/common/naive-ui/components.ts)
|
||||||
|
- [src/stores/counter.ts](file://src/stores/counter.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [核心框架:Vue 3](#核心框架vue-3)
|
||||||
|
2. [状态管理:Pinia](#状态管理pinia)
|
||||||
|
3. [UI 组件库:Naive UI](#ui-组件库naive-ui)
|
||||||
|
4. [原子化样式:UnoCSS](#原子化样式unocss)
|
||||||
|
5. [构建工具:Vite](#构建工具vite)
|
||||||
|
6. [类型安全:TypeScript](#类型安全typescript)
|
||||||
|
7. [关键依赖项版本与选择理由](#关键依赖项版本与选择理由)
|
||||||
|
|
||||||
|
## 核心框架:Vue 3
|
||||||
|
|
||||||
|
本项目采用 Vue 3 作为核心前端框架,利用其组合式 API(Composition API)实现更灵活、可复用的逻辑组织。在 `main.ts` 文件中通过 `createApp` 初始化应用实例,并挂载根组件 `App.vue`,构成整个应用的入口。
|
||||||
|
|
||||||
|
Vue 3 提供了响应式系统、虚拟 DOM 渲染机制以及强大的组件化能力,使得桌面级 Web 应用的开发更加高效和模块化。结合 Vite 的现代构建流程,实现了极快的冷启动和热更新体验。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [src/main.ts](file://src/main.ts#L1-L15)
|
||||||
|
|
||||||
|
## 状态管理:Pinia
|
||||||
|
|
||||||
|
Pinia 作为 Vue 官方推荐的状态管理库,在本项目中用于集中管理全局状态。通过 `defineStore` 创建具有命名空间的 store 实例,如 `useCounterStore`,支持使用 `ref` 和 `computed` 构建响应式状态,并导出操作方法以供组件调用。
|
||||||
|
|
||||||
|
在 `main.ts` 中通过 `app.use(createPinia())` 注册 Pinia 插件,使所有组件均可通过 `useXXXStore()` 获取共享状态,避免了传统 Vuex 的冗余配置,提升了开发效率与类型推断准确性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class useCounterStore {
|
||||||
|
+count : Ref<number>
|
||||||
|
+doubleCount : ComputedRef<number>
|
||||||
|
+increment() : void
|
||||||
|
}
|
||||||
|
useCounterStore --> ref : "uses"
|
||||||
|
useCounterStore --> computed : "uses"
|
||||||
|
useCounterStore --> defineStore : "created via"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [src/stores/counter.ts](file://src/stores/counter.ts#L3-L11)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [src/stores/counter.ts](file://src/stores/counter.ts#L1-L13)
|
||||||
|
- [src/main.ts](file://src/main.ts#L8-L9)
|
||||||
|
|
||||||
|
## UI 组件库:Naive UI
|
||||||
|
|
||||||
|
项目集成 Naive UI 作为 UI 组件解决方案,提供高质量、可定制的 Vue 3 组件集。为优化打包体积并实现按需引入,项目在 `src/common/naive-ui/components.ts` 中手动创建了一个轻量化的 UI 实例,仅注册所需组件(如 `NButton`, `NCard`, `NConfigProvider`),并通过 `create()` 方法生成可被 `app.use()` 注册的插件对象。
|
||||||
|
|
||||||
|
此外,项目还通过 `discrete-api.ts` 导出独立使用的 API(如 message、modal),并在 `theme.ts` 中扩展默认主题,自定义主色调为 `#0070f3`,确保视觉风格统一。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class naiveUi {
|
||||||
|
+components : Array<Component>
|
||||||
|
}
|
||||||
|
naiveUi --> NButton : "includes"
|
||||||
|
naiveUi --> NCard : "includes"
|
||||||
|
naiveUi --> NConfigProvider : "includes"
|
||||||
|
naiveUi --> create : "created via"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [src/common/naive-ui/components.ts](file://src/common/naive-ui/components.ts#L1-L11)
|
||||||
|
- [src/common/naive-ui/theme.ts](file://src/common/naive-ui/theme.ts#L2-L14)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [src/common/naive-ui/components.ts](file://src/common/naive-ui/components.ts#L1-L11)
|
||||||
|
|
||||||
|
## 原子化样式:UnoCSS
|
||||||
|
|
||||||
|
UnoCSS 是一个即时(on-the-fly)原子化 CSS 引擎,取代传统的 Tailwind CSS 实现方式。它允许开发者直接在模板中使用类名(如 `flex`, `p-4`, `text-lg`),并在构建时动态生成最小化 CSS,极大提升样式编写效率并减少冗余代码。
|
||||||
|
|
||||||
|
项目通过 `unocss/vite` 插件集成 UnoCSS,并在 `uno.config.ts` 中启用 `transformerVariantGroup` 和 `transformerDirectives`,支持嵌套语法(如 `hover:(bg-red-500 text-white)`)和 `@apply` 指令,增强可读性与灵活性。同时,`virtual:uno.css` 被引入 `main.ts`,确保样式正确注入。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [uno.config.ts](file://uno.config.ts#L1-L10)
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L15-L16)
|
||||||
|
- [src/main.ts](file://src/main.ts#L6)
|
||||||
|
|
||||||
|
## 构建工具:Vite
|
||||||
|
|
||||||
|
Vite 作为现代前端构建工具,基于 ES 模块原生支持,显著提升了开发服务器的启动速度与热更新性能。项目通过 `vite.config.ts` 配置核心插件:
|
||||||
|
|
||||||
|
- `@vitejs/plugin-vue`:支持 `.vue` 单文件组件解析
|
||||||
|
- `@vitejs/plugin-vue-jsx`:支持 JSX/TSX 语法
|
||||||
|
- `vite-plugin-vue-devtools`:集成 Vue DevTools 调试工具
|
||||||
|
- `unocss/vite`:集成 UnoCSS 样式引擎
|
||||||
|
|
||||||
|
配置中还设置了路径别名 `@` 指向 `src` 目录,便于模块导入。Vite 的开箱即用特性与 TypeScript、JSX、Sass 等技术无缝集成,构成了高效的开发环境基础。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["用户请求 /"] --> B[Vite Dev Server]
|
||||||
|
B --> C{资源类型}
|
||||||
|
C --> |HTML| D[index.html]
|
||||||
|
C --> |.vue 文件| E[Vite 解析 SFC]
|
||||||
|
E --> F[编译 template/script/style]
|
||||||
|
F --> G[返回 ES Module]
|
||||||
|
C --> |TypeScript| H[Vite 实时转译]
|
||||||
|
H --> I[浏览器执行]
|
||||||
|
C --> |UnoCSS 类名| J[UnoCSS 动态生成 CSS]
|
||||||
|
J --> K[注入 style 标签]
|
||||||
|
I & K --> L[页面渲染完成]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
|
||||||
|
## 类型安全:TypeScript
|
||||||
|
|
||||||
|
TypeScript 在项目中承担关键的角色,提供静态类型检查以预防运行时错误。项目采用分层配置策略,`tsconfig.json` 通过 `references` 引入 `tsconfig.node.json` 和 `tsconfig.app.json`,分别处理 Node.js 环境与应用代码的类型定义。
|
||||||
|
|
||||||
|
`.vue` 文件的类型支持通过 `vue-tsc` 实现,替代标准 `tsc` 进行类型检查,确保模板中的 props、emits 等具备完整类型推断。配合 VSCode 与 Volar 插件,开发者可在编辑器中获得精准的智能提示与错误检测。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [tsconfig.json](file://tsconfig.json#L1-L12)
|
||||||
|
- [README.md](file://README.md#L12-L15)
|
||||||
|
|
||||||
|
## 关键依赖项版本与选择理由
|
||||||
|
|
||||||
|
| 依赖包 | 版本范围 | 选择理由 |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| vue | ^3.5.18 | 使用最新稳定版 Vue 3,支持 Composition API 与 Fragment |
|
||||||
|
| pinia | ^3.0.3 | 官方状态管理库,轻量、类型友好、API 简洁 |
|
||||||
|
| naive-ui | ^2.42.0 | 功能丰富、文档完善、支持 Tree-shaking 的 Vue 3 组件库 |
|
||||||
|
| unocss | ^66.4.2 | 即时原子化 CSS,性能优于传统方案,支持高度定制 |
|
||||||
|
| vite | ^7.0.6 | 极速 HMR、原生 ES Modules 支持、生态成熟 |
|
||||||
|
| typescript | ~5.8.0 | 精确控制 TS 版本,避免意外升级导致兼容问题 |
|
||||||
|
| vue-tsc | ^3.0.4 | 专为 Vue + TypeScript 设计的类型检查工具,支持 `<script setup>` 和模板类型校验 |
|
||||||
|
|
||||||
|
这些依赖共同构建了一个现代化、高性能、类型安全的前端开发体系,为项目的长期维护与扩展提供了坚实基础。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L1-L43)
|
||||||
182
.qoder/repowiki/zh/content/构建与部署.md
Normal file
182
.qoder/repowiki/zh/content/构建与部署.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 构建与部署
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document**
|
||||||
|
- [vite.config.ts](file://vite.config.ts)
|
||||||
|
- [uno.config.ts](file://uno.config.ts)
|
||||||
|
- [package.json](file://package.json)
|
||||||
|
- [README.md](file://README.md)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [构建流程概述](#构建流程概述)
|
||||||
|
2. [开发构建与生产构建的区别](#开发构建与生产构建的区别)
|
||||||
|
3. [Vite 配置解析](#vite-配置解析)
|
||||||
|
4. [UnoCSS 自定义配置](#unocss-自定义配置)
|
||||||
|
5. [构建脚本执行机制](#构建脚本执行机制)
|
||||||
|
6. [静态资源输出结构](#静态资源输出结构)
|
||||||
|
7. [部署最佳实践](#部署最佳实践)
|
||||||
|
|
||||||
|
## 构建流程概述
|
||||||
|
|
||||||
|
本项目基于 Vite 构建工具实现现代化前端工程化流程,结合 Vue 3 和 UnoCSS 原子化 CSS 框架。整体构建流程围绕 `vite` 命令展开,通过 `package.json` 中定义的脚本入口驱动不同环境下的构建行为。
|
||||||
|
|
||||||
|
构建系统支持开发模式热重载、类型检查、代码格式化及生产级优化打包。核心配置文件包括 `vite.config.ts`(构建主配置)、`uno.config.ts`(样式引擎配置)以及 `package.json`(脚本命令定义),三者协同完成从源码到可部署静态资源的转换过程。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
- [package.json](file://package.json#L1-L43)
|
||||||
|
- [README.md](file://README.md#L1-L38)
|
||||||
|
|
||||||
|
## 开发构建与生产构建的区别
|
||||||
|
|
||||||
|
开发构建与生产构建在目标、性能优化和输出内容上存在显著差异:
|
||||||
|
|
||||||
|
| 特性 | 开发构建 | 生产构建 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| **启动命令** | `pnpm dev` | `pnpm build` |
|
||||||
|
| **主要目的** | 快速启动、热更新 | 生成优化后的静态资源 |
|
||||||
|
| **代码压缩** | 否 | 是(Terser 或 SWC) |
|
||||||
|
| **Source Map** | 完整映射 | 可选或隐藏 |
|
||||||
|
| **环境变量** | `.env.development` | `.env.production` |
|
||||||
|
| **依赖预构建** | 动态处理 | 静态分析并缓存 |
|
||||||
|
| **HMR 支持** | 是 | 否 |
|
||||||
|
|
||||||
|
开发构建侧重于提升开发者体验,启用热模块替换(HMR)以实现实时反馈;而生产构建则专注于性能优化,对 JavaScript、CSS 进行压缩与 Tree Shaking,移除调试代码,并生成适合 CDN 分发的静态文件。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L6-L14)
|
||||||
|
- [README.md](file://README.md#L30-L37)
|
||||||
|
|
||||||
|
## Vite 配置解析
|
||||||
|
|
||||||
|
`vite.config.ts` 是项目的构建核心配置文件,定义了插件集成、路径别名、编译选项等关键设置。
|
||||||
|
|
||||||
|
### 插件集成
|
||||||
|
配置中注册了多个 Vite 插件:
|
||||||
|
- `@vitejs/plugin-vue`:支持 Vue 单文件组件解析,配置了自定义元素忽略规则(`isCustomElement: tag => tag.endsWith('-element')`),避免将特定标签误解析为 Vue 组件。
|
||||||
|
- `@vitejs/plugin-vue-jsx`:启用 JSX/TSX 语法支持。
|
||||||
|
- `vite-plugin-vue-devtools`:集成 Vue DevTools 调试工具。
|
||||||
|
- `unocss/vite`:引入 UnoCSS 样式引擎,实现原子化 CSS 按需生成。
|
||||||
|
|
||||||
|
### 路径别名
|
||||||
|
通过 `resolve.alias` 配置 `@` 指向 `src` 目录,简化模块导入路径,提升代码可读性与维护性。
|
||||||
|
|
||||||
|
### 环境变量处理
|
||||||
|
Vite 原生支持 `.env` 文件加载,根据 `import.meta.env.MODE` 区分不同环境,并自动注入 `import.meta.env.VITE_*` 前缀的环境变量。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["启动 vite"] --> B{开发模式?}
|
||||||
|
B --> |是| C["加载 vite.config.ts"]
|
||||||
|
C --> D["应用插件: vue, vueJsx, devtools, unocss"]
|
||||||
|
D --> E["设置路径别名 @ → src/"]
|
||||||
|
E --> F["启动开发服务器 + HMR"]
|
||||||
|
B --> |否| G["执行 vite build"]
|
||||||
|
G --> H["编译 TS + 处理 SFC"]
|
||||||
|
H --> I["应用 UnoCSS 转换"]
|
||||||
|
I --> J["压缩 JS/CSS"]
|
||||||
|
J --> K["输出 dist/ 目录"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
|
||||||
|
## UnoCSS 自定义配置
|
||||||
|
|
||||||
|
`uno.config.ts` 文件用于定制 UnoCSS 的行为,当前配置启用了两个重要转换器:
|
||||||
|
|
||||||
|
- `transformerVariantGroup()`:允许使用括号语法进行变体分组,例如 `hover:(bg-red-500 text-white)`,提升类名书写效率。
|
||||||
|
- `transformerDirectives()`:支持在 CSS 中使用 `@apply` 指令复用原子类,以及 `@screen` 响应式控制。
|
||||||
|
|
||||||
|
该配置确保了在模板中可以灵活使用组合式类名,同时保持样式的可维护性和简洁性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class UnoCSSConfig {
|
||||||
|
+transformers : Transformer[]
|
||||||
|
}
|
||||||
|
class TransformerVariantGroup {
|
||||||
|
+name : "variant-group"
|
||||||
|
+transform()
|
||||||
|
}
|
||||||
|
class TransformerDirectives {
|
||||||
|
+name : "directives"
|
||||||
|
+transform()
|
||||||
|
}
|
||||||
|
UnoCSSConfig --> TransformerVariantGroup : "包含"
|
||||||
|
UnoCSSConfig --> TransformerDirectives : "包含"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [uno.config.ts](file://uno.config.ts#L1-L10)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [uno.config.ts](file://uno.config.ts#L1-L10)
|
||||||
|
|
||||||
|
## 构建脚本执行机制
|
||||||
|
|
||||||
|
`package.json` 中的 `scripts` 字段定义了标准化的构建命令:
|
||||||
|
|
||||||
|
- `dev`:直接运行 `vite`,启动开发服务器。
|
||||||
|
- `build`:并行执行类型检查与构建任务,使用 `run-p` 实现并发控制。
|
||||||
|
- `build-only`:仅执行 `vite build`,生成生产资源。
|
||||||
|
- `type-check`:调用 `vue-tsc --build` 进行完整的 TypeScript 类型校验。
|
||||||
|
- `preview`:启动本地预览服务器,模拟生产环境。
|
||||||
|
|
||||||
|
这种设计实现了构建流程的解耦与并行化,提升了 CI/CD 流水线效率。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L6-L14)
|
||||||
|
|
||||||
|
## 静态资源输出结构
|
||||||
|
|
||||||
|
生产构建完成后,Vite 将输出默认的 `dist/` 目录,其典型结构如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── assets/ # 打包后的 JS、CSS 资源
|
||||||
|
│ ├── index.[hash].js
|
||||||
|
│ └── style.[hash].css
|
||||||
|
├── index.html # 入口 HTML 文件
|
||||||
|
└── favicon.ico # 可选图标
|
||||||
|
```
|
||||||
|
|
||||||
|
所有资源均经过哈希命名以支持长期缓存,HTML 文件自动注入正确的资源引用路径。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [vite.config.ts](file://vite.config.ts#L1-L30)
|
||||||
|
|
||||||
|
## 部署最佳实践
|
||||||
|
|
||||||
|
### 静态服务器部署
|
||||||
|
将 `dist/` 目录内容部署至 Nginx、Apache 或 Caddy 等静态服务器时,需确保:
|
||||||
|
- 正确设置 MIME 类型。
|
||||||
|
- 启用 Gzip/Brotli 压缩。
|
||||||
|
- 配置 SPA 路由回退(将所有非资源请求重定向至 `index.html`)。
|
||||||
|
|
||||||
|
示例 Nginx 配置片段:
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CDN 部署建议
|
||||||
|
- 利用 CDN 的边缘缓存能力,设置合理的缓存策略(如 `max-age=31536000` 对带哈希的静态资源)。
|
||||||
|
- 启用 HTTPS 和 HTTP/2。
|
||||||
|
- 使用版本化目录(如 `/v1.0.0/`)便于灰度发布与回滚。
|
||||||
|
|
||||||
|
### 自动化部署
|
||||||
|
推荐结合 GitHub Actions、GitLab CI 或 Jenkins 实现自动化构建与部署流水线,包含以下阶段:
|
||||||
|
1. 依赖安装
|
||||||
|
2. 类型检查
|
||||||
|
3. 生产构建
|
||||||
|
4. 构建产物验证
|
||||||
|
5. 自动上传至对象存储或 CDN
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L30-L37)
|
||||||
209
.qoder/repowiki/zh/content/状态管理.md
Normal file
209
.qoder/repowiki/zh/content/状态管理.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# 状态管理
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [counter.ts](file://src/stores/counter.ts)
|
||||||
|
- [main.ts](file://src/main.ts)
|
||||||
|
- [App.vue](file://src/ui/App.vue)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [项目结构分析](#项目结构分析)
|
||||||
|
3. [Pinia状态管理模式应用](#pinia状态管理模式应用)
|
||||||
|
4. [Counter Store详解](#counter-store详解)
|
||||||
|
5. [组件中使用Store](#组件中使用store)
|
||||||
|
6. [全局状态同步与扩展用途](#全局状态同步与扩展用途)
|
||||||
|
7. [状态持久化策略](#状态持久化策略)
|
||||||
|
8. [结论](#结论)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本项目采用Pinia作为Vue 3的官方推荐状态管理库,实现跨组件的状态共享和管理。文档将详细说明基于Pinia的状态管理模式在桌面系统中的应用,以counter store为例展示store的定义、state暴露和actions使用方法,并探讨其在整个系统中的潜在扩展用途及状态持久化策略。
|
||||||
|
|
||||||
|
## 项目结构分析
|
||||||
|
|
||||||
|
项目遵循典型的Vue + TypeScript架构,状态管理相关代码集中存放在`src/stores`目录下。核心入口文件为`src/main.ts`,负责初始化Pinia实例并将其挂载到Vue应用上。UI组件分布在`src/ui`目录中,通过模块化方式组织桌面容器和应用图标等界面元素。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "核心模块"
|
||||||
|
Stores["src/stores"]
|
||||||
|
UI["src/ui"]
|
||||||
|
Main["src/main.ts"]
|
||||||
|
end
|
||||||
|
Main --> Stores : "导入并使用"
|
||||||
|
Main --> UI : "作为根组件"
|
||||||
|
UI --> Stores : "调用store"
|
||||||
|
Stores --> counter["counter.ts"]
|
||||||
|
UI --> DesktopContainer["DesktopContainer.vue"]
|
||||||
|
DesktopContainer --> AppIcon["AppIcon.vue"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [main.ts](file://src/main.ts#L1-L16)
|
||||||
|
- [stores/counter.ts](file://src/stores/counter.ts#L1-L13)
|
||||||
|
- [ui/App.vue](file://src/ui/App.vue#L1-L53)
|
||||||
|
|
||||||
|
## Pinia状态管理模式应用
|
||||||
|
|
||||||
|
Pinia在本项目中被用作全局状态管理中心,通过`createPinia()`创建实例并在主应用中注册,使得所有组件都能访问统一的状态树。这种模式解决了传统Vue应用中深层嵌套组件间通信困难的问题,实现了状态的集中管理和响应式更新。
|
||||||
|
|
||||||
|
### 初始化过程
|
||||||
|
|
||||||
|
Pinia实例在`main.ts`中完成初始化和注册:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
此配置确保了Pinia插件在整个应用生命周期内可用,为后续的store创建和使用奠定了基础。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [main.ts](file://src/main.ts#L1-L16)
|
||||||
|
|
||||||
|
## Counter Store详解
|
||||||
|
|
||||||
|
`counter.ts`文件定义了一个典型的Pinia store示例——useCounterStore,展示了组合式API风格的store定义方式。
|
||||||
|
|
||||||
|
### Store定义
|
||||||
|
|
||||||
|
`useCounterStore`通过`defineStore`函数创建,采用箭头函数语法封装内部逻辑。该store包含以下核心要素:
|
||||||
|
- **State**: 使用`ref`定义的响应式数据`count`
|
||||||
|
- **Getters**: 通过`computed`创建的派生属性`doubleCount`
|
||||||
|
- **Actions**: 可修改state的方法`increment`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
这种定义方式充分利用了Vue 3的Composition API特性,使代码更加直观和易于理解。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [counter.ts](file://src/stores/counter.ts#L3-L11)
|
||||||
|
|
||||||
|
### State暴露机制
|
||||||
|
|
||||||
|
Store通过返回对象的方式暴露其内部状态和方法。这种方式实现了良好的封装性,同时保持了使用的便捷性。外部组件可以通过调用`useCounterStore()`获取store实例,进而访问`count`、`doubleCount`和`increment`等属性和方法。
|
||||||
|
|
||||||
|
## 组件中使用Store
|
||||||
|
|
||||||
|
虽然当前代码库中未直接展示counter store在组件中的使用,但可以推断出标准的使用模式。
|
||||||
|
|
||||||
|
### 引入与使用
|
||||||
|
|
||||||
|
在任何Vue组件中,可通过导入`useCounterStore`并在`setup`函数中调用它来使用store:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCounterStore } from '@/stores/counter'
|
||||||
|
|
||||||
|
const counter = useCounterStore()
|
||||||
|
// 使用 counter.count, counter.doubleCount, counter.increment()
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 潜在应用场景
|
||||||
|
|
||||||
|
尽管counter store目前仅作为示例存在,但它可被扩展用于多种场景:
|
||||||
|
- 桌面图标的数量统计
|
||||||
|
- 应用启动次数追踪
|
||||||
|
- 用户交互行为计数
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Component as "Vue组件"
|
||||||
|
participant Store as "Pinia Store"
|
||||||
|
participant State as "响应式状态"
|
||||||
|
Component->>Store : 调用useCounterStore()
|
||||||
|
Store-->>Component : 返回store实例
|
||||||
|
Component->>Store : 访问count值
|
||||||
|
Store-->>Component : 提供响应式count
|
||||||
|
Component->>Store : 调用increment()
|
||||||
|
Store->>State : 修改count.value
|
||||||
|
State-->>Store : 触发更新
|
||||||
|
Store-->>Component : 自动更新视图
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources**
|
||||||
|
- [counter.ts](file://src/stores/counter.ts#L3-L11)
|
||||||
|
- [App.vue](file://src/ui/App.vue#L1-L53)
|
||||||
|
|
||||||
|
## 全局状态同步与扩展用途
|
||||||
|
|
||||||
|
Pinia提供的全局状态管理能力在桌面系统中有广泛的扩展潜力。
|
||||||
|
|
||||||
|
### 多组件状态共享
|
||||||
|
|
||||||
|
设想多个桌面组件需要共享某些状态(如主题设置、布局参数、用户偏好等),可通过创建相应的store实现:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 示例:theme.store.ts
|
||||||
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
|
const darkMode = ref(false)
|
||||||
|
const fontSize = ref(14)
|
||||||
|
|
||||||
|
function toggleDarkMode() {
|
||||||
|
darkMode.value = !darkMode.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { darkMode, fontSize, toggleDarkMode }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
此类store可被任务栏、桌面容器、应用窗口等多个组件同时引用,确保状态的一致性。
|
||||||
|
|
||||||
|
### 系统级状态管理
|
||||||
|
|
||||||
|
更复杂的系统状态(如窗口管理、应用状态、事件总线)也可通过Pinia进行统一管理,形成清晰的状态流架构。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L24)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L53)
|
||||||
|
|
||||||
|
## 状态持久化策略
|
||||||
|
|
||||||
|
当前项目尚未实现状态持久化功能,但从技术角度看有多种可行方案。
|
||||||
|
|
||||||
|
### 可行的持久化方案
|
||||||
|
|
||||||
|
1. **浏览器存储**: 利用localStorage或sessionStorage保存关键状态
|
||||||
|
2. **IndexedDB**: 对于复杂数据结构,可使用IndexedDB进行持久化
|
||||||
|
3. **第三方插件**: 使用`pinia-plugin-persistedstate`等成熟解决方案
|
||||||
|
|
||||||
|
### 实现建议
|
||||||
|
|
||||||
|
若需实现counter store的持久化,可在定义store时添加持久化配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 配置示例(当前未实现)
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
// ...原有逻辑
|
||||||
|
}, {
|
||||||
|
persist: true // 启用持久化
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
值得注意的是,项目中存在`useObservableVue.ts`这一自定义Hook,表明团队可能倾向于使用自定义的状态管理方案配合Pinia使用,这为未来实现更复杂的状态持久化和同步机制提供了灵活性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [useObservableVue.ts](file://src/common/hooks/useObservableVue.ts#L1-L43)
|
||||||
|
- [counter.ts](file://src/stores/counter.ts#L1-L13)
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
本项目通过Pinia实现了现代化的Vue状态管理,`counter.ts`中的useCounterStore展示了清晰的组合式API用法。虽然当前store的应用较为基础,但其架构设计为未来的功能扩展留下了充足空间。通过合理利用Pinia的模块化特性,可逐步构建完整的全局状态管理体系,支撑起复杂桌面应用的需求。同时,结合浏览器原生存储机制或专用插件,可进一步完善状态持久化能力,提升用户体验。
|
||||||
275
.qoder/repowiki/zh/content/项目概述.md
Normal file
275
.qoder/repowiki/zh/content/项目概述.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# 项目概述
|
||||||
|
|
||||||
|
<cite>
|
||||||
|
**Referenced Files in This Document **
|
||||||
|
- [README.md](file://README.md)
|
||||||
|
- [package.json](file://package.json)
|
||||||
|
- [main.ts](file://src/main.ts)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts)
|
||||||
|
- [counter.ts](file://src/stores/counter.ts)
|
||||||
|
- [EventManager.ts](file://src/events/EventManager.ts)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts)
|
||||||
|
</cite>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [简介](#简介)
|
||||||
|
2. [项目结构](#项目结构)
|
||||||
|
3. [核心组件](#核心组件)
|
||||||
|
4. [架构概览](#架构概览)
|
||||||
|
5. [详细组件分析](#详细组件分析)
|
||||||
|
6. [依赖分析](#依赖分析)
|
||||||
|
7. [性能考量](#性能考量)
|
||||||
|
8. [故障排除指南](#故障排除指南)
|
||||||
|
9. [结论](#结论)
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
`vue-desktop` 是一个基于 Vue 3 的桌面风格 Web 应用,旨在模拟操作系统桌面环境的用户体验。该项目通过现代化的前端技术栈实现了可拖拽的应用图标、响应式网格布局和高效的状态管理等关键特性。其设计目标是为用户提供直观、交互性强的Web界面,同时为开发者提供清晰的代码结构和可扩展的架构。
|
||||||
|
|
||||||
|
本项目采用 MVVM(Model-View-ViewModel)模式结合 Vue 3 的 Composition API,实现了逻辑与视图的分离,提升了代码的可维护性和复用性。对于初学者而言,该项目提供了理解现代Vue应用开发的良好范例;对于高级开发者,则展示了复杂状态管理和事件系统的设计思路。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [README.md](file://README.md#L1-L37)
|
||||||
|
- [package.json](file://package.json#L1-L42)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
`vue-desktop` 项目的目录结构体现了功能模块化的设计理念,各目录职责分明:
|
||||||
|
|
||||||
|
- `src/common`: 存放通用工具函数、自定义Hook和类型定义
|
||||||
|
- `src/css`: 全局样式文件
|
||||||
|
- `src/events`: 事件管理系统,实现组件间解耦通信
|
||||||
|
- `src/stores`: 状态管理模块,使用 Pinia 进行全局状态管理
|
||||||
|
- `src/ui`: 用户界面组件,包含桌面容器、应用图标等核心UI元素
|
||||||
|
- `src/main.ts`: 应用入口文件,负责初始化Vue实例和插件
|
||||||
|
|
||||||
|
这种分层结构使得项目易于理解和维护,新开发者可以快速定位到特定功能的实现位置。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "源码目录"
|
||||||
|
Common[src/common<br/>通用工具与类型]
|
||||||
|
CSS[src/css<br/>全局样式]
|
||||||
|
Events[src/events<br/>事件系统]
|
||||||
|
Stores[src/stores<br/>状态管理]
|
||||||
|
UI[src/ui<br/>用户界面]
|
||||||
|
Main[src/main.ts<br/>应用入口]
|
||||||
|
end
|
||||||
|
Main --> UI
|
||||||
|
Main --> Stores
|
||||||
|
Main --> Events
|
||||||
|
Main --> CSS
|
||||||
|
Main --> Common
|
||||||
|
UI --> Events
|
||||||
|
UI --> Stores
|
||||||
|
UI --> Common
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [main.ts](file://src/main.ts#L1-L15)
|
||||||
|
- [project_structure](file://#L1-L20)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [project_structure](file://#L1-L20)
|
||||||
|
|
||||||
|
## 核心组件
|
||||||
|
项目的核心功能由几个关键组件协同实现:`DesktopContainer` 作为桌面主容器管理整体布局,`AppIcon` 组件代表可交互的应用图标,`useDesktopContainerInit` Hook 负责初始化桌面参数和响应式逻辑,而 `counter` Store 则演示了基础的状态管理机制。
|
||||||
|
|
||||||
|
这些组件共同构成了桌面环境的基础,其中 `DesktopContainer` 和 `AppIcon` 通过 Composition API 实现了高度的灵活性和可复用性。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
- [counter.ts](file://src/stores/counter.ts#L3-L11)
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
`vue-desktop` 采用了典型的现代前端架构模式,以 Vue 3 的 Composition API 为核心,结合 Pinia 进行状态管理,并通过自定义事件系统实现组件间的松耦合通信。
|
||||||
|
|
||||||
|
整个应用从 `main.ts` 启动,创建 Vue 实例并注册 Pinia 和 Naive UI 等插件。UI 层主要由 `DesktopContainer` 和 `AppIcon` 组成,前者使用 `useDesktopContainerInit` Hook 初始化响应式数据,后者则实现了图标的拖拽功能。状态管理通过 Pinia Store 实现,事件通信则依赖于自定义的事件总线机制。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[main.ts] --> B[Vue App]
|
||||||
|
B --> C[Pinia Store]
|
||||||
|
B --> D[Naive UI]
|
||||||
|
B --> E[DesktopContainer]
|
||||||
|
E --> F[AppIcon]
|
||||||
|
E --> G[useDesktopContainerInit]
|
||||||
|
C --> H[counter]
|
||||||
|
E --> I[EventManager]
|
||||||
|
G --> J[ResizeObserver]
|
||||||
|
F --> K[Drag Events]
|
||||||
|
style A fill:#f9f,stroke:#333
|
||||||
|
style B fill:#bbf,stroke:#333
|
||||||
|
style C fill:#f96,stroke:#333
|
||||||
|
style E fill:#6f9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [main.ts](file://src/main.ts#L1-L15)
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 详细组件分析
|
||||||
|
|
||||||
|
### 桌面容器分析
|
||||||
|
`DesktopContainer` 组件是整个应用的核心容器,负责管理桌面的网格布局和应用图标的渲染。它通过 Composition API 的 `setup` 语法引入 `useDesktopContainerInit` Hook 来获取响应式数据,并使用 Vue 的 `v-for` 指令遍历渲染 `AppIcon` 组件。
|
||||||
|
|
||||||
|
该组件的关键特性包括:
|
||||||
|
- 响应式网格布局,能根据容器大小自动调整行列数
|
||||||
|
- 双击事件处理,用于启动应用程序
|
||||||
|
- 与子组件的 props 通信机制
|
||||||
|
|
||||||
|
#### 组件关系图
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class DesktopContainer {
|
||||||
|
+appIconsRef : Ref~Array~
|
||||||
|
+gridStyle : ComputedRef
|
||||||
|
+gridTemplate : Reactive
|
||||||
|
+runApp(appIcon) : void
|
||||||
|
}
|
||||||
|
class AppIcon {
|
||||||
|
+iconInfo : Prop
|
||||||
|
+gridTemplate : Prop
|
||||||
|
+onDragStart(e) : void
|
||||||
|
+onDragEnd(e) : void
|
||||||
|
}
|
||||||
|
class useDesktopContainerInit {
|
||||||
|
+useDesktopContainerInit(containerStr) : Object
|
||||||
|
+rearrangeIcons() : Object
|
||||||
|
}
|
||||||
|
DesktopContainer --> AppIcon : "v-for 渲染"
|
||||||
|
DesktopContainer --> useDesktopContainerInit : "组合式API调用"
|
||||||
|
useDesktopContainerInit ..> IGridTemplateParams : "实现"
|
||||||
|
useDesktopContainerInit ..> IDesktopAppIcon : "使用"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
- [IGridTemplateParams.ts](file://src/ui/types/IGridTemplateParams.ts#L1-L20)
|
||||||
|
- [IDesktopAppIcon.ts](file://src/ui/types/IDesktopAppIcon.ts#L3-L14)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [DesktopContainer.vue](file://src/ui/desktop-container/DesktopContainer.vue#L1-L23)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
### 应用图标分析
|
||||||
|
`AppIcon` 组件实现了桌面图标的可视化和交互功能,支持拖拽操作来重新排列图标位置。当用户拖拽图标并释放时,组件会计算鼠标位置对应的网格坐标,并更新图标的 `x` 和 `y` 属性。
|
||||||
|
|
||||||
|
该组件利用 HTML5 的 Drag and Drop API 实现拖拽功能,并通过 `document.elementFromPoint` 方法检测鼠标释放时的位置元素,确保图标只能放置在有效的桌面区域内。
|
||||||
|
|
||||||
|
#### 拖拽流程图
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start([开始拖拽]) --> DragStart["onDragStart事件触发"]
|
||||||
|
DragStart --> Wait["等待拖拽结束"]
|
||||||
|
Wait --> DragEnd["onDragEnd事件触发"]
|
||||||
|
DragEnd --> CheckTarget["检查鼠标下方元素"]
|
||||||
|
CheckTarget --> Valid{"是否为有效区域?"}
|
||||||
|
Valid --> |否| End([放弃移动])
|
||||||
|
Valid --> |是| Calculate["计算网格坐标"]
|
||||||
|
Calculate --> Update["更新图标位置(x,y)"]
|
||||||
|
Update --> Persist["持久化到localStorage"]
|
||||||
|
Persist --> End
|
||||||
|
style Start fill:#f9f,stroke:#333
|
||||||
|
style End fill:#f9f,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
|
||||||
|
### 状态与事件系统分析
|
||||||
|
项目中的状态管理采用 Pinia 实现,`counter.ts` 文件展示了最基本的 Store 定义方式,包含响应式状态、计算属性和修改方法。事件系统则通过 `EventManager` 和 `EventBuilderImpl` 类构建了一个类型安全的事件总线,支持添加、移除和触发事件。
|
||||||
|
|
||||||
|
这种设计模式实现了组件间的解耦,使得不同部分的代码可以通过事件进行通信,而不需要直接引用彼此。
|
||||||
|
|
||||||
|
#### 事件系统类图
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class EventManager {
|
||||||
|
+eventManager : EventBuilderImpl
|
||||||
|
}
|
||||||
|
class EventBuilderImpl {
|
||||||
|
-_eventHandlers : Map
|
||||||
|
+addEventListener()
|
||||||
|
+removeEventListener()
|
||||||
|
+notifyEvent()
|
||||||
|
+destroy()
|
||||||
|
}
|
||||||
|
class IEventBuilder {
|
||||||
|
<<interface>>
|
||||||
|
+addEventListener()
|
||||||
|
+removeEventListener()
|
||||||
|
+notifyEvent()
|
||||||
|
}
|
||||||
|
EventManager --> EventBuilderImpl : "实例化"
|
||||||
|
EventBuilderImpl ..|> IEventBuilder : "实现"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [EventManager.ts](file://src/events/EventManager.ts#L4-L4)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L1-L96)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [EventManager.ts](file://src/events/EventManager.ts#L4-L4)
|
||||||
|
- [EventBuilderImpl.ts](file://src/events/impl/EventBuilderImpl.ts#L1-L96)
|
||||||
|
|
||||||
|
## 依赖分析
|
||||||
|
`vue-desktop` 项目的依赖体系清晰地分为生产依赖和开发依赖两大类。生产依赖主要包括 Vue 3 核心库、Pinia 状态管理、Lodash 工具函数和 UUID 生成器等。开发依赖则包含了 Vite 构建工具、TypeScript 支持、Prettier 代码格式化等开发辅助工具。
|
||||||
|
|
||||||
|
值得注意的是,项目使用了 UnoCSS 进行原子化CSS管理,以及 Naive UI 作为UI组件库,这些选择体现了对性能和开发效率的重视。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Vue 3] --> B[vue-desktop]
|
||||||
|
C[Pinia] --> B
|
||||||
|
D[Lodash] --> B
|
||||||
|
E[UUID] --> B
|
||||||
|
F[Naive UI] --> B
|
||||||
|
G[Vite] --> B
|
||||||
|
H[TypeScript] --> B
|
||||||
|
I[UnoCSS] --> B
|
||||||
|
style B fill:#6f9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
**Diagram sources **
|
||||||
|
- [package.json](file://package.json#L1-L42)
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [package.json](file://package.json#L1-L42)
|
||||||
|
|
||||||
|
## 性能考量
|
||||||
|
`vue-desktop` 在性能方面采取了多项优化措施:
|
||||||
|
|
||||||
|
1. **响应式布局优化**:通过 `ResizeObserver` 监听容器尺寸变化,避免了频繁的重排重绘。
|
||||||
|
2. **内存管理**:在组件卸载时正确清理事件监听器和观察者,防止内存泄漏。
|
||||||
|
3. **本地存储**:使用 `localStorage` 持久化图标位置信息,减少重复计算。
|
||||||
|
4. **计算属性缓存**:利用 Vue 的 `computed` 特性缓存网格样式计算结果。
|
||||||
|
|
||||||
|
这些优化确保了即使在大量图标的情况下,应用仍能保持流畅的用户体验。
|
||||||
|
|
||||||
|
## 故障排除指南
|
||||||
|
当遇到常见问题时,可以参考以下解决方案:
|
||||||
|
|
||||||
|
- **图标无法拖拽**:检查浏览器是否支持 HTML5 Drag and Drop API,确认元素的 `draggable` 属性已正确设置。
|
||||||
|
- **布局错乱**:验证容器宽度是否被正确设置,检查 CSS 样式是否有冲突。
|
||||||
|
- **状态不更新**:确保使用了正确的响应式API(ref/reactive),检查 Pinia Store 的使用方式。
|
||||||
|
- **事件未触发**:确认事件监听器已正确注册,检查事件名称拼写是否一致。
|
||||||
|
|
||||||
|
**Section sources**
|
||||||
|
- [AppIcon.vue](file://src/ui/desktop-container/AppIcon.vue#L1-L52)
|
||||||
|
- [useDesktopContainerInit.ts](file://src/ui/desktop-container/useDesktopContainerInit.ts#L14-L94)
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
`vue-desktop` 项目成功实现了一个功能完整的桌面风格Web应用,展示了 Vue 3 生态系统的强大能力。通过 MVVM 架构和 Composition API 的结合,项目实现了良好的代码组织和可维护性。响应式网格布局和拖拽功能为用户提供了直观的交互体验,而 Pinia 和自定义事件系统则确保了复杂状态的有效管理。
|
||||||
|
|
||||||
|
该项目不仅适合作为学习现代前端开发的范例,也为构建类似的操作系统模拟器或仪表盘应用提供了有价值的参考。随着需求的发展,可以在此基础上扩展更多功能,如窗口管理、任务栏、系统托盘等,进一步完善桌面环境的模拟。
|
||||||
1
.qoder/repowiki/zh/meta/repowiki-metadata.json
Normal file
1
.qoder/repowiki/zh/meta/repowiki-metadata.json
Normal file
File diff suppressed because one or more lines are too long
167
PRETTIER_CONFIG_GUIDE.md
Normal file
167
PRETTIER_CONFIG_GUIDE.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Prettier 常用配置项详解
|
||||||
|
|
||||||
|
## 基础格式化选项
|
||||||
|
|
||||||
|
### semi (boolean)
|
||||||
|
|
||||||
|
- **默认值**: true
|
||||||
|
- **说明**: 在语句末尾打印分号
|
||||||
|
- **示例**:
|
||||||
|
- true: `console.log('hello');`
|
||||||
|
- false: `console.log('hello')`
|
||||||
|
|
||||||
|
### singleQuote (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 使用单引号而不是双引号
|
||||||
|
- **示例**:
|
||||||
|
- true: `const str = 'hello';`
|
||||||
|
- false: `const str = "hello";`
|
||||||
|
|
||||||
|
### printWidth (number)
|
||||||
|
|
||||||
|
- **默认值**: 80
|
||||||
|
- **说明**: 指定每行代码的最大字符数,超过这个长度会自动换行
|
||||||
|
|
||||||
|
### useTabs (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 使用制表符(tab)缩进而不是空格
|
||||||
|
|
||||||
|
### tabWidth (number)
|
||||||
|
|
||||||
|
- **默认值**: 2
|
||||||
|
- **说明**: 指定每个缩进级别的空格数
|
||||||
|
|
||||||
|
## 对象和数组格式化
|
||||||
|
|
||||||
|
### bracketSpacing (boolean)
|
||||||
|
|
||||||
|
- **默认值**: true
|
||||||
|
- **说明**: 在对象字面量中的括号之间打印空格
|
||||||
|
- **示例**:
|
||||||
|
- true: `{ foo: bar }`
|
||||||
|
- false: `{foo: bar}`
|
||||||
|
|
||||||
|
### bracketSameLine (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 将多行 HTML 元素的 > 放在最后一行的末尾,而不是单独放在下一行
|
||||||
|
|
||||||
|
### trailingComma (string)
|
||||||
|
|
||||||
|
- **默认值**: "es5"
|
||||||
|
- **可选值**: "none" | "es5" | "all"
|
||||||
|
- **说明**: 在多行对象或数组的最后一个元素后是否添加逗号
|
||||||
|
- **示例**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// "none"
|
||||||
|
{
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
// "es5"
|
||||||
|
{
|
||||||
|
foo: 'bar',
|
||||||
|
}
|
||||||
|
|
||||||
|
// "all"
|
||||||
|
{
|
||||||
|
foo: 'bar',
|
||||||
|
baz: 'qux',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSX 特定选项
|
||||||
|
|
||||||
|
### jsxSingleQuote (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 在 JSX 中使用单引号而不是双引号
|
||||||
|
|
||||||
|
### singleAttributePerLine (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 强制每个 HTML、Vue 和 JSX 属性独占一行
|
||||||
|
- **示例**:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- singleAttributePerLine: false -->
|
||||||
|
<button class="btn" id="submit" type="submit">Submit</button>
|
||||||
|
|
||||||
|
<!-- singleAttributePerLine: true -->
|
||||||
|
<button class="btn" id="submit" type="submit">Submit</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 函数和箭头函数
|
||||||
|
|
||||||
|
### arrowParens (string)
|
||||||
|
|
||||||
|
- **默认值**: "always"
|
||||||
|
- **可选值**: "always" | "avoid"
|
||||||
|
- **说明**: 在箭头函数参数周围始终包含括号
|
||||||
|
- **示例**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// "always"
|
||||||
|
const fn = (x) => x
|
||||||
|
|
||||||
|
// "avoid"
|
||||||
|
const fn = (x) => x
|
||||||
|
const fn2 = (x, y) => x + y
|
||||||
|
```
|
||||||
|
|
||||||
|
## 其他格式化选项
|
||||||
|
|
||||||
|
### endOfLine (string)
|
||||||
|
|
||||||
|
- **默认值**: "lf"
|
||||||
|
- **可选值**: "auto" | "lf" | "crlf" | "cr"
|
||||||
|
- **说明**: 指定换行符风格
|
||||||
|
- "lf": \n (Linux/macOS)
|
||||||
|
- "crlf": \r\n (Windows)
|
||||||
|
|
||||||
|
### quoteProps (string)
|
||||||
|
|
||||||
|
- **默认值**: "as-needed"
|
||||||
|
- **可选值**: "as-needed" | "consistent" | "preserve"
|
||||||
|
- **说明**: 对象属性引用方式
|
||||||
|
- "as-needed": 仅在需要时才引用属性
|
||||||
|
- "consistent": 如果至少有一个属性需要引用,则引用所有属性
|
||||||
|
- "preserve": 保持原样
|
||||||
|
|
||||||
|
### htmlWhitespaceSensitivity (string)
|
||||||
|
|
||||||
|
- **默认值**: "css"
|
||||||
|
- **可选值**: "css" | "strict" | "ignore"
|
||||||
|
- **说明**: 指定 HTML、Vue、Angular 文件中全局空白敏感度
|
||||||
|
- "css": 尊重 CSS display 属性的默认值
|
||||||
|
- "strict": 所有空白都被认为是重要的
|
||||||
|
- "ignore": 所有空白都被认为是不重要的
|
||||||
|
|
||||||
|
### vueIndentScriptAndStyle (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: Vue 文件中单文件组件的 `<script>` 和 `<style>` 标签内的代码是否缩进
|
||||||
|
|
||||||
|
### experimentalTernaries (boolean)
|
||||||
|
|
||||||
|
- **默认值**: false
|
||||||
|
- **说明**: 尝试 Prettier 的新三元表达式格式化方式,在成为默认行为之前使用
|
||||||
|
|
||||||
|
### experimentalOperatorPosition (string)
|
||||||
|
|
||||||
|
- **默认值**: "end"
|
||||||
|
- **可选值**: "start" | "end"
|
||||||
|
- **说明**: 当二元表达式换行时,操作符的位置
|
||||||
|
- "start": 操作符放在新行的开头
|
||||||
|
- "end": 操作符放在前一行的末尾(默认行为)
|
||||||
|
|
||||||
|
### objectWrap (string)
|
||||||
|
|
||||||
|
- **默认值**: "preserve"
|
||||||
|
- **可选值**: "preserve" | "collapse"
|
||||||
|
- **说明**: 配置 Prettier 如何包装对象字面量
|
||||||
|
- "preserve": 如果在开括号和第一个属性之间有换行符,则保持多行格式
|
||||||
|
- "collapse": 如果可能,将对象压缩到单行
|
||||||
220
PROJECT_SUMMARY.md
Normal file
220
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Vue Desktop
|
||||||
|
|
||||||
|
一个基于 Vue 3 + TypeScript + Vite 的现代化桌面环境模拟器
|
||||||
|
|
||||||
|
## 🎯 项目目标
|
||||||
|
|
||||||
|
构建一个轻量级、模块化的桌面环境,支持:
|
||||||
|
|
||||||
|
- 内置应用(计算器、记事本、待办事项等)
|
||||||
|
- 外置应用加载(通过 iframe 沙箱)
|
||||||
|
- 窗口管理(创建、移动、缩放、最小化等)
|
||||||
|
- 资源管理(存储、权限控制)
|
||||||
|
- 应用生命周期管理
|
||||||
|
- 安全沙箱机制
|
||||||
|
|
||||||
|
## 🏗️ 架构实现
|
||||||
|
|
||||||
|
### 核心服务层
|
||||||
|
|
||||||
|
- **[WindowFormService](src/services/WindowFormService.ts)** - 窗体管理服务,支持完整的窗体生命周期
|
||||||
|
- **[ResourceService](./src/services/ResourceService.ts)** - 资源管理服务,提供权限控制和资源访问
|
||||||
|
- **[ApplicationSandboxEngine](./src/services/ApplicationSandboxEngine.ts)** - 应用沙箱引擎,多层安全隔离
|
||||||
|
- **[ApplicationLifecycleManager](./src/services/ApplicationLifecycleManager.ts)** - 应用生命周期管理
|
||||||
|
- **[SystemServiceIntegration](./src/services/SystemServiceIntegration.ts)** - 系统服务集成层
|
||||||
|
|
||||||
|
### SDK接口层
|
||||||
|
|
||||||
|
- **[SystemSDK](./src/sdk/index.ts)** - 统一SDK接口,为应用提供系统能力访问
|
||||||
|
|
||||||
|
### 事件系统
|
||||||
|
|
||||||
|
- **[IEventBuilder](./src/events/IEventBuilder.ts)** - 事件总线接口
|
||||||
|
- **[EventBuilderImpl](./src/events/impl/EventBuilderImpl.ts)** - 事件总线实现
|
||||||
|
|
||||||
|
### 应用管理
|
||||||
|
|
||||||
|
- **[AppRegistry](./src/apps/AppRegistry.ts)** - 应用注册中心
|
||||||
|
- **[ExternalAppDiscovery](./src/services/ExternalAppDiscovery.ts)** - 外置应用发现服务
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── public\apps
|
||||||
|
│ ├── music-player
|
||||||
|
│ │ ├── README.md
|
||||||
|
│ │ ├── app.js
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── manifest.json
|
||||||
|
│ │ └── style.css
|
||||||
|
│ └── README.md
|
||||||
|
├── src
|
||||||
|
│ ├── apps
|
||||||
|
│ │ ├── calculator
|
||||||
|
│ │ │ └── Calculator.vue
|
||||||
|
│ │ ├── components
|
||||||
|
│ │ │ └── BuiltInApp.vue
|
||||||
|
│ │ ├── notepad
|
||||||
|
│ │ │ └── Notepad.vue
|
||||||
|
│ │ ├── todo
|
||||||
|
│ │ │ └── Todo.vue
|
||||||
|
│ │ ├── types
|
||||||
|
│ │ │ └── AppManifest.ts
|
||||||
|
│ │ ├── AppRegistry.ts
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ ├── common
|
||||||
|
│ │ ├── hooks
|
||||||
|
│ │ │ ├── useClickFocus.ts
|
||||||
|
│ │ │ └── useObservableVue.ts
|
||||||
|
│ │ ├── naive-ui
|
||||||
|
│ │ │ ├── components.ts
|
||||||
|
│ │ │ ├── discrete-api.ts
|
||||||
|
│ │ │ └── theme.ts
|
||||||
|
│ │ └── types
|
||||||
|
│ │ ├── IDestroyable.ts
|
||||||
|
│ │ └── IVersion.ts
|
||||||
|
│ ├── css
|
||||||
|
│ │ └── basic.css
|
||||||
|
│ ├── events
|
||||||
|
│ │ ├── impl
|
||||||
|
│ │ │ └── EventBuilderImpl.ts
|
||||||
|
│ │ └── IEventBuilder.ts
|
||||||
|
│ ├── sdk
|
||||||
|
│ │ ├── index.ts
|
||||||
|
│ │ └── types.ts
|
||||||
|
│ ├── services
|
||||||
|
│ │ ├── ApplicationLifecycleManager.ts
|
||||||
|
│ │ ├── ApplicationSandboxEngine.ts
|
||||||
|
│ │ ├── ExternalAppDiscovery.ts
|
||||||
|
│ │ ├── ResourceService.ts
|
||||||
|
│ │ ├── SystemServiceIntegration.ts
|
||||||
|
│ │ └── WindowFormService.ts
|
||||||
|
│ ├── stores
|
||||||
|
│ │ └── counter.ts
|
||||||
|
│ ├── ui
|
||||||
|
│ │ ├── components
|
||||||
|
│ │ │ ├── AppRenderer.vue
|
||||||
|
│ │ │ └── WindowManager.vue
|
||||||
|
│ │ ├── desktop-container
|
||||||
|
│ │ │ ├── AppIcon.vue
|
||||||
|
│ │ │ ├── DesktopContainer.vue
|
||||||
|
│ │ │ ├── useDesktopContainerInit.ts
|
||||||
|
│ │ │ └── useDynamicAppIcons.ts
|
||||||
|
│ │ ├── types
|
||||||
|
│ │ │ ├── IDesktopAppIcon.ts
|
||||||
|
│ │ │ ├── IGridTemplateParams.ts
|
||||||
|
│ │ │ └── WindowFormTypes.ts
|
||||||
|
│ │ └── App.vue
|
||||||
|
│ └── main.ts
|
||||||
|
├── PRETTIER_CONFIG_GUIDE.md
|
||||||
|
├── PROJECT_SUMMARY.md
|
||||||
|
├── README.md
|
||||||
|
├── env.d.ts
|
||||||
|
├── index.html
|
||||||
|
├── package.json
|
||||||
|
├── pnpm-lock.yaml
|
||||||
|
├── tsconfig.app.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── tsconfig.node.json
|
||||||
|
├── uno.config.ts
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 16.0.0
|
||||||
|
- pnpm >= 7.0.0
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 开发指南
|
||||||
|
|
||||||
|
### 添加内置应用
|
||||||
|
|
||||||
|
1. 在 `src/apps/` 目录下创建应用文件夹
|
||||||
|
2. 创建 Vue 组件文件(如 `MyApp.vue`)
|
||||||
|
3. 在 `src/apps/AppRegistry.ts` 中注册应用
|
||||||
|
|
||||||
|
### 添加外置应用
|
||||||
|
|
||||||
|
1. 在 `public/apps/` 目录下创建应用文件夹
|
||||||
|
2. 添加 `manifest.json` 应用清单文件
|
||||||
|
3. 添加应用的 HTML/CSS/JS 文件
|
||||||
|
4. 系统会自动发现并加载该应用
|
||||||
|
|
||||||
|
### 系统服务使用
|
||||||
|
|
||||||
|
通过依赖注入获取系统服务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||||
|
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||||
|
```
|
||||||
|
|
||||||
|
可用服务:
|
||||||
|
|
||||||
|
- `getWindowFormService()` - 窗体服务
|
||||||
|
- `getResourceService()` - 资源服务
|
||||||
|
- `getSandboxEngine()` - 沙箱引擎
|
||||||
|
- `getLifecycleManager()` - 生命周期管理器
|
||||||
|
|
||||||
|
## 📖 技术文档
|
||||||
|
|
||||||
|
### 窗体系统
|
||||||
|
|
||||||
|
窗体系统支持完整的生命周期管理,包括创建、移动、缩放、最小化、最大化等操作。
|
||||||
|
|
||||||
|
### 资源管理
|
||||||
|
|
||||||
|
资源服务提供安全的存储访问和权限控制机制。
|
||||||
|
|
||||||
|
### 沙箱安全
|
||||||
|
|
||||||
|
应用沙箱引擎提供多层安全隔离,防止恶意代码访问系统资源。
|
||||||
|
|
||||||
|
### 应用生命周期
|
||||||
|
|
||||||
|
应用生命周期管理器负责应用的安装、启动、停止、卸载等操作。
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 端到端测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 部署
|
||||||
|
|
||||||
|
构建产物可直接部署到任何静态文件服务器上。
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>vue-desktop</title>
|
<title>Vue 桌面系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
49
public/apps/README.md
Normal file
49
public/apps/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 外部应用目录
|
||||||
|
|
||||||
|
此目录用于存放外部应用(非内置Vue应用)。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
外部应用应该按以下结构组织:
|
||||||
|
|
||||||
|
```
|
||||||
|
public/apps/
|
||||||
|
├── app-name/
|
||||||
|
│ ├── index.html # 应用主页面
|
||||||
|
│ ├── manifest.json # 应用清单文件
|
||||||
|
│ └── ... # 其他应用文件
|
||||||
|
└── another-app/
|
||||||
|
├── index.html
|
||||||
|
├── manifest.json
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 应用清单格式
|
||||||
|
|
||||||
|
每个外部应用都应该包含一个 `manifest.json` 文件:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "app-id",
|
||||||
|
"name": "应用名称",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "应用描述",
|
||||||
|
"author": "作者",
|
||||||
|
"icon": "图标URL或表情符号",
|
||||||
|
"permissions": ["storage", "notification"],
|
||||||
|
"window": {
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"resizable": true
|
||||||
|
},
|
||||||
|
"category": "应用分类",
|
||||||
|
"keywords": ["关键词1", "关键词2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全说明
|
||||||
|
|
||||||
|
外部应用将在iframe沙箱环境中运行,具有以下限制:
|
||||||
|
- 无法直接访问父页面
|
||||||
|
- 通过postMessage与系统通信
|
||||||
|
- 受到严格的权限控制
|
||||||
170
public/apps/music-player/README.md
Normal file
170
public/apps/music-player/README.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# 音乐播放器 - 外置应用案例
|
||||||
|
|
||||||
|
这是一个完整的外置应用案例,展示了如何在Vue桌面系统中开发外置应用。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 🎵 音乐播放功能
|
||||||
|
- **音频格式支持**: 支持所有浏览器兼容的音频格式(MP3、WAV、OGG等)
|
||||||
|
- **播放控制**: 播放、暂停、上一曲、下一曲
|
||||||
|
- **进度控制**: 可拖拽的进度条,支持跳转到任意位置
|
||||||
|
- **音量控制**: 音量滑块调节,实时显示音量百分比
|
||||||
|
|
||||||
|
### 🎲 播放模式
|
||||||
|
- **随机播放**: 支持随机播放模式切换
|
||||||
|
- **重复播放**: 三种重复模式(关闭、单曲循环、列表循环)
|
||||||
|
- **播放列表**: 完整的播放列表管理
|
||||||
|
|
||||||
|
### 🎨 用户界面
|
||||||
|
- **现代设计**: 采用渐变色彩和毛玻璃效果
|
||||||
|
- **响应式布局**: 支持窗口大小调整
|
||||||
|
- **直观操作**: 清晰的视觉反馈和状态提示
|
||||||
|
- **窗口控制**: 最小化、最大化、关闭按钮
|
||||||
|
|
||||||
|
### ⌨️ 交互体验
|
||||||
|
- **键盘快捷键**:
|
||||||
|
- `Space`: 播放/暂停
|
||||||
|
- `←/→`: 上一曲/下一曲
|
||||||
|
- `↑/↓`: 音量增减
|
||||||
|
- **拖拽支持**: 支持文件拖拽添加(计划中)
|
||||||
|
- **状态持久化**: 播放列表自动保存
|
||||||
|
|
||||||
|
### 🔧 系统集成
|
||||||
|
- **系统SDK集成**: 与主系统的无缝集成
|
||||||
|
- **窗口控制**: 通过系统API控制窗口状态
|
||||||
|
- **系统通知**: 状态变化的系统通知
|
||||||
|
- **数据存储**: 使用系统存储API保存用户数据
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
music-player/
|
||||||
|
├── manifest.json # 应用清单文件
|
||||||
|
├── index.html # 主HTML页面
|
||||||
|
├── style.css # 样式文件
|
||||||
|
├── app.js # 主要逻辑文件
|
||||||
|
└── README.md # 说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 应用清单 (manifest.json)
|
||||||
|
定义了应用的基本信息、权限要求、窗口配置等:
|
||||||
|
- 应用ID、名称、版本信息
|
||||||
|
- 窗口大小和行为配置
|
||||||
|
- 所需权限(存储、文件读取、通知)
|
||||||
|
- 分类和关键词
|
||||||
|
|
||||||
|
### 用户界面 (index.html + style.css)
|
||||||
|
- **布局结构**: 采用Flexbox布局,分为头部、主要区域、播放列表和状态栏
|
||||||
|
- **样式设计**: 使用CSS渐变、阴影、过渡动画等现代效果
|
||||||
|
- **响应式**: 针对不同屏幕尺寸优化显示
|
||||||
|
|
||||||
|
### 应用逻辑 (app.js)
|
||||||
|
核心类 `MusicPlayer` 实现了所有功能:
|
||||||
|
|
||||||
|
#### 初始化流程
|
||||||
|
1. DOM就绪检测
|
||||||
|
2. 系统SDK连接
|
||||||
|
3. 事件监听器设置
|
||||||
|
4. 界面状态初始化
|
||||||
|
|
||||||
|
#### 音频处理
|
||||||
|
- 使用HTML5 Audio API
|
||||||
|
- 事件驱动的状态管理
|
||||||
|
- 错误处理和恢复机制
|
||||||
|
|
||||||
|
#### 播放列表管理
|
||||||
|
- 文件选择和验证
|
||||||
|
- 内存中的播放队列
|
||||||
|
- 持久化存储支持
|
||||||
|
|
||||||
|
## 系统SDK集成
|
||||||
|
|
||||||
|
该应用展示了如何正确使用系统SDK:
|
||||||
|
|
||||||
|
### 窗口控制
|
||||||
|
```javascript
|
||||||
|
// 窗口操作
|
||||||
|
this.systemSDK.window.minimize();
|
||||||
|
this.systemSDK.window.toggleMaximize();
|
||||||
|
this.systemSDK.window.close();
|
||||||
|
|
||||||
|
// 关闭事件监听
|
||||||
|
this.systemSDK.window.onBeforeClose(() => {
|
||||||
|
this.cleanup();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 系统通知
|
||||||
|
```javascript
|
||||||
|
this.systemSDK.notification.show({
|
||||||
|
title: '音乐播放器',
|
||||||
|
message: '应用已启动,准备播放音乐!',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据存储
|
||||||
|
```javascript
|
||||||
|
// 保存数据
|
||||||
|
this.systemSDK.storage.setItem('music-player-playlist', data);
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
const data = this.systemSDK.storage.getItem('music-player-playlist');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
- **文件访问**: 通过文件选择器安全地访问用户文件
|
||||||
|
- **内存管理**: 及时释放对象URL避免内存泄漏
|
||||||
|
- **错误处理**: 完整的错误捕获和用户友好的错误提示
|
||||||
|
- **权限控制**: 仅请求必要的系统权限
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
- **懒加载**: 音频文件仅在需要时加载
|
||||||
|
- **事件防抖**: 避免频繁的状态更新
|
||||||
|
- **内存回收**: 应用关闭时清理所有资源
|
||||||
|
- **DOM优化**: 高效的DOM操作和事件委托
|
||||||
|
|
||||||
|
## 扩展可能
|
||||||
|
|
||||||
|
这个案例可以进一步扩展:
|
||||||
|
|
||||||
|
1. **音频可视化**: 添加频谱显示
|
||||||
|
2. **歌词显示**: 支持LRC歌词文件
|
||||||
|
3. **均衡器**: 音频效果控制
|
||||||
|
4. **在线音乐**: 集成在线音乐服务
|
||||||
|
5. **播放历史**: 记录播放统计
|
||||||
|
6. **主题切换**: 多种UI主题
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 本地测试
|
||||||
|
1. 将整个文件夹放置到 `public/apps/` 目录下
|
||||||
|
2. 启动Vue桌面系统
|
||||||
|
3. 通过应用管理器安装或直接打开应用
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
- 使用浏览器开发者工具调试
|
||||||
|
- 检查控制台日志了解应用状态
|
||||||
|
- 利用系统SDK的调试功能
|
||||||
|
|
||||||
|
### 部署注意事项
|
||||||
|
- 确保所有文件路径正确
|
||||||
|
- 验证清单文件格式
|
||||||
|
- 测试在不同窗口大小下的表现
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个音乐播放器应用是一个完整的外置应用开发示例,展示了:
|
||||||
|
|
||||||
|
- 如何构建功能完整的外置应用
|
||||||
|
- 系统SDK的正确使用方法
|
||||||
|
- 现代Web技术的应用
|
||||||
|
- 良好的用户体验设计
|
||||||
|
- 安全和性能的最佳实践
|
||||||
|
|
||||||
|
通过学习这个案例,开发者可以了解外置应用的完整开发流程,并以此为基础开发自己的应用。
|
||||||
751
public/apps/music-player/app.js
Normal file
751
public/apps/music-player/app.js
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
/**
|
||||||
|
* 音乐播放器 - 外置应用案例
|
||||||
|
* 展示了如何创建一个功能完整的外置应用
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MusicPlayer {
|
||||||
|
constructor() {
|
||||||
|
// 应用状态
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.currentTrackIndex = 0;
|
||||||
|
this.playlist = [];
|
||||||
|
this.isShuffleMode = false;
|
||||||
|
this.repeatMode = 'none'; // none, one, all
|
||||||
|
this.volume = 0.7;
|
||||||
|
|
||||||
|
// DOM 元素
|
||||||
|
this.audioPlayer = null;
|
||||||
|
this.playPauseBtn = null;
|
||||||
|
this.progressBar = null;
|
||||||
|
this.volumeBar = null;
|
||||||
|
this.playlist_element = null;
|
||||||
|
|
||||||
|
// 系统SDK
|
||||||
|
this.systemSDK = null;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应用
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
console.log('[音乐播放器] 初始化开始');
|
||||||
|
|
||||||
|
// 等待DOM加载完成
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => this.setupApp());
|
||||||
|
} else {
|
||||||
|
this.setupApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化系统SDK
|
||||||
|
await this.initSystemSDK();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化系统SDK
|
||||||
|
*/
|
||||||
|
async initSystemSDK() {
|
||||||
|
try {
|
||||||
|
console.log('[音乐播放器] 开始初始化系统SDK');
|
||||||
|
|
||||||
|
// 等待系统SDK可用
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 100; // 增加尝试次数
|
||||||
|
|
||||||
|
while (!window.SystemSDK && attempts < maxAttempts) {
|
||||||
|
console.log(`[音乐播放器] 等待SystemSDK可用... (尝试 ${attempts + 1}/${maxAttempts})`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.SystemSDK) {
|
||||||
|
console.log('[音乐播放器] SystemSDK对象已找到,开始初始化');
|
||||||
|
|
||||||
|
// 初始化SDK
|
||||||
|
const initResult = await window.SystemSDK.init({
|
||||||
|
appId: 'music-player',
|
||||||
|
appName: '音乐播放器',
|
||||||
|
version: '1.0.0',
|
||||||
|
permissions: ['storage.read', 'storage.write']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initResult.success) {
|
||||||
|
this.systemSDK = window.SystemSDK;
|
||||||
|
console.log('[音乐播放器] 系统SDK初始化成功');
|
||||||
|
|
||||||
|
// 显示系统通知
|
||||||
|
if (this.systemSDK.ui) {
|
||||||
|
try {
|
||||||
|
await this.systemSDK.ui.showNotification({
|
||||||
|
title: '音乐播放器',
|
||||||
|
message: '应用已启动,准备播放音乐!',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[音乐播放器] 显示通知失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储恢复播放列表
|
||||||
|
await this.loadPlaylistFromStorage();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('[音乐播放器] 系统SDK初始化失败:', initResult.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('[音乐播放器] 系统SDK不可用,已达到最大尝试次数');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[音乐播放器] 系统SDK初始化失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置应用界面和事件
|
||||||
|
*/
|
||||||
|
setupApp() {
|
||||||
|
console.log('[音乐播放器] 设置界面');
|
||||||
|
|
||||||
|
// 获取DOM元素
|
||||||
|
this.audioPlayer = document.getElementById('audioPlayer');
|
||||||
|
this.playPauseBtn = document.getElementById('playPauseBtn');
|
||||||
|
this.progressBar = document.getElementById('progressBar');
|
||||||
|
this.volumeBar = document.getElementById('volumeBar');
|
||||||
|
this.playlist_element = document.getElementById('playlist');
|
||||||
|
|
||||||
|
// 设置音频事件
|
||||||
|
this.setupAudioEvents();
|
||||||
|
|
||||||
|
// 设置控制按钮事件
|
||||||
|
this.setupControlEvents();
|
||||||
|
|
||||||
|
// 设置窗口控制事件
|
||||||
|
this.setupWindowControls();
|
||||||
|
|
||||||
|
// 设置键盘快捷键
|
||||||
|
this.setupKeyboardShortcuts();
|
||||||
|
|
||||||
|
// 初始化音量
|
||||||
|
this.setVolume(this.volume * 100);
|
||||||
|
|
||||||
|
console.log('[音乐播放器] 应用设置完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置音频播放器事件
|
||||||
|
*/
|
||||||
|
setupAudioEvents() {
|
||||||
|
if (!this.audioPlayer) return;
|
||||||
|
|
||||||
|
// 播放开始
|
||||||
|
this.audioPlayer.addEventListener('play', () => {
|
||||||
|
this.isPlaying = true;
|
||||||
|
this.updatePlayButton();
|
||||||
|
this.updateStatus('正在播放');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放暂停
|
||||||
|
this.audioPlayer.addEventListener('pause', () => {
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.updatePlayButton();
|
||||||
|
this.updateStatus('已暂停');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 播放结束
|
||||||
|
this.audioPlayer.addEventListener('ended', () => {
|
||||||
|
this.handleTrackEnded();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 时间更新
|
||||||
|
this.audioPlayer.addEventListener('timeupdate', () => {
|
||||||
|
this.updateProgress();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载完成
|
||||||
|
this.audioPlayer.addEventListener('loadedmetadata', () => {
|
||||||
|
this.updateTotalTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载错误
|
||||||
|
this.audioPlayer.addEventListener('error', (e) => {
|
||||||
|
console.error('[音乐播放器] 播放错误:', e);
|
||||||
|
this.updateStatus('播放出错');
|
||||||
|
this.nextTrack();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置控制按钮事件
|
||||||
|
*/
|
||||||
|
setupControlEvents() {
|
||||||
|
// 播放/暂停
|
||||||
|
document.getElementById('playPauseBtn')?.addEventListener('click', () => {
|
||||||
|
this.togglePlayPause();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上一曲
|
||||||
|
document.getElementById('prevBtn')?.addEventListener('click', () => {
|
||||||
|
this.prevTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下一曲
|
||||||
|
document.getElementById('nextBtn')?.addEventListener('click', () => {
|
||||||
|
this.nextTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 随机播放
|
||||||
|
document.getElementById('shuffleBtn')?.addEventListener('click', () => {
|
||||||
|
this.toggleShuffle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重复播放
|
||||||
|
document.getElementById('repeatBtn')?.addEventListener('click', () => {
|
||||||
|
this.toggleRepeat();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
this.progressBar?.addEventListener('input', () => {
|
||||||
|
this.seekTo(this.progressBar.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
|
this.volumeBar?.addEventListener('input', () => {
|
||||||
|
this.setVolume(this.volumeBar.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件选择
|
||||||
|
document.getElementById('addFilesBtn')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('fileInput').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('fileInput')?.addEventListener('change', (e) => {
|
||||||
|
this.handleFileSelection(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空播放列表
|
||||||
|
document.getElementById('clearPlaylistBtn')?.addEventListener('click', () => {
|
||||||
|
this.clearPlaylist();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置窗口控制事件
|
||||||
|
*/
|
||||||
|
setupWindowControls() {
|
||||||
|
// 最小化
|
||||||
|
document.getElementById('minimizeBtn')?.addEventListener('click', () => {
|
||||||
|
if (this.systemSDK) {
|
||||||
|
this.systemSDK.window.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最大化/还原
|
||||||
|
document.getElementById('maximizeBtn')?.addEventListener('click', () => {
|
||||||
|
if (this.systemSDK) {
|
||||||
|
this.systemSDK.window.toggleMaximize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
document.getElementById('closeBtn')?.addEventListener('click', () => {
|
||||||
|
if (this.systemSDK) {
|
||||||
|
this.systemSDK.window.close();
|
||||||
|
} else {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置键盘快捷键
|
||||||
|
*/
|
||||||
|
setupKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.target.tagName === 'INPUT') return;
|
||||||
|
|
||||||
|
switch (e.code) {
|
||||||
|
case 'Space':
|
||||||
|
e.preventDefault();
|
||||||
|
this.togglePlayPause();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
this.prevTrack();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
this.nextTrack();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setVolume(Math.min(100, this.volume * 100 + 5));
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setVolume(Math.max(0, this.volume * 100 - 5));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件选择
|
||||||
|
*/
|
||||||
|
handleFileSelection(files) {
|
||||||
|
const audioFiles = Array.from(files).filter(file =>
|
||||||
|
file.type.startsWith('audio/')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (audioFiles.length === 0) {
|
||||||
|
this.updateStatus('未选择音频文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioFiles.forEach(file => {
|
||||||
|
const track = {
|
||||||
|
id: Date.now() + Math.random(),
|
||||||
|
name: file.name.replace(/\.[^/.]+$/, ""),
|
||||||
|
file: file,
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
duration: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.playlist.push(track);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updatePlaylist();
|
||||||
|
this.savePlaylistToStorage();
|
||||||
|
this.updateStatus(`添加了 ${audioFiles.length} 首歌曲`);
|
||||||
|
|
||||||
|
// 如果是第一次添加歌曲,自动播放
|
||||||
|
if (this.playlist.length === audioFiles.length) {
|
||||||
|
this.loadTrack(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播放/暂停切换
|
||||||
|
*/
|
||||||
|
togglePlayPause() {
|
||||||
|
if (!this.audioPlayer || this.playlist.length === 0) {
|
||||||
|
this.updateStatus('播放列表为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPlaying) {
|
||||||
|
this.audioPlayer.pause();
|
||||||
|
} else {
|
||||||
|
this.audioPlayer.play().catch(error => {
|
||||||
|
console.error('[音乐播放器] 播放失败:', error);
|
||||||
|
this.updateStatus('播放失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上一曲
|
||||||
|
*/
|
||||||
|
prevTrack() {
|
||||||
|
if (this.playlist.length === 0) return;
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
if (this.isShuffleMode) {
|
||||||
|
newIndex = Math.floor(Math.random() * this.playlist.length);
|
||||||
|
} else {
|
||||||
|
newIndex = this.currentTrackIndex - 1;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = this.playlist.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTrack(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下一曲
|
||||||
|
*/
|
||||||
|
nextTrack() {
|
||||||
|
if (this.playlist.length === 0) return;
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
if (this.isShuffleMode) {
|
||||||
|
newIndex = Math.floor(Math.random() * this.playlist.length);
|
||||||
|
} else {
|
||||||
|
newIndex = this.currentTrackIndex + 1;
|
||||||
|
if (newIndex >= this.playlist.length) {
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTrack(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载指定曲目
|
||||||
|
*/
|
||||||
|
loadTrack(index) {
|
||||||
|
if (index < 0 || index >= this.playlist.length) return;
|
||||||
|
|
||||||
|
this.currentTrackIndex = index;
|
||||||
|
const track = this.playlist[index];
|
||||||
|
|
||||||
|
this.audioPlayer.src = track.url;
|
||||||
|
this.updateCurrentTrackInfo(track);
|
||||||
|
this.updatePlaylistHighlight();
|
||||||
|
|
||||||
|
if (this.isPlaying) {
|
||||||
|
this.audioPlayer.play().catch(error => {
|
||||||
|
console.error('[音乐播放器] 播放失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理曲目播放结束
|
||||||
|
*/
|
||||||
|
handleTrackEnded() {
|
||||||
|
switch (this.repeatMode) {
|
||||||
|
case 'one':
|
||||||
|
this.audioPlayer.currentTime = 0;
|
||||||
|
this.audioPlayer.play();
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
this.nextTrack();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (this.currentTrackIndex < this.playlist.length - 1) {
|
||||||
|
this.nextTrack();
|
||||||
|
} else {
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.updatePlayButton();
|
||||||
|
this.updateStatus('播放完成');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换随机播放
|
||||||
|
*/
|
||||||
|
toggleShuffle() {
|
||||||
|
this.isShuffleMode = !this.isShuffleMode;
|
||||||
|
const btn = document.getElementById('shuffleBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('active', this.isShuffleMode);
|
||||||
|
}
|
||||||
|
this.updateStatus(this.isShuffleMode ? '随机播放已开启' : '随机播放已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换重复播放模式
|
||||||
|
*/
|
||||||
|
toggleRepeat() {
|
||||||
|
const modes = ['none', 'one', 'all'];
|
||||||
|
const currentIndex = modes.indexOf(this.repeatMode);
|
||||||
|
this.repeatMode = modes[(currentIndex + 1) % modes.length];
|
||||||
|
|
||||||
|
const btn = document.getElementById('repeatBtn');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('active', this.repeatMode !== 'none');
|
||||||
|
switch (this.repeatMode) {
|
||||||
|
case 'one':
|
||||||
|
btn.textContent = '🔂';
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
btn.textContent = '🔁';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
btn.textContent = '🔁';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeNames = { none: '关闭', one: '单曲循环', all: '列表循环' };
|
||||||
|
this.updateStatus(`重复播放: ${modeNames[this.repeatMode]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置音量
|
||||||
|
*/
|
||||||
|
setVolume(value) {
|
||||||
|
this.volume = value / 100;
|
||||||
|
if (this.audioPlayer) {
|
||||||
|
this.audioPlayer.volume = this.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeValue = document.getElementById('volumeValue');
|
||||||
|
if (volumeValue) {
|
||||||
|
volumeValue.textContent = `${Math.round(value)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.volumeBar) {
|
||||||
|
this.volumeBar.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到指定时间
|
||||||
|
*/
|
||||||
|
seekTo(percentage) {
|
||||||
|
if (this.audioPlayer && this.audioPlayer.duration) {
|
||||||
|
const time = (percentage / 100) * this.audioPlayer.duration;
|
||||||
|
this.audioPlayer.currentTime = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新播放按钮状态
|
||||||
|
*/
|
||||||
|
updatePlayButton() {
|
||||||
|
if (this.playPauseBtn) {
|
||||||
|
this.playPauseBtn.textContent = this.isPlaying ? '⏸️' : '▶️';
|
||||||
|
this.playPauseBtn.classList.toggle('playing', this.isPlaying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新进度条
|
||||||
|
*/
|
||||||
|
updateProgress() {
|
||||||
|
if (this.audioPlayer && this.progressBar && this.audioPlayer.duration) {
|
||||||
|
const progress = (this.audioPlayer.currentTime / this.audioPlayer.duration) * 100;
|
||||||
|
this.progressBar.value = progress;
|
||||||
|
|
||||||
|
const currentTime = document.getElementById('currentTime');
|
||||||
|
if (currentTime) {
|
||||||
|
currentTime.textContent = this.formatTime(this.audioPlayer.currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新总时长显示
|
||||||
|
*/
|
||||||
|
updateTotalTime() {
|
||||||
|
if (this.audioPlayer) {
|
||||||
|
const totalTime = document.getElementById('totalTime');
|
||||||
|
if (totalTime) {
|
||||||
|
totalTime.textContent = this.formatTime(this.audioPlayer.duration || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前曲目信息
|
||||||
|
*/
|
||||||
|
updateCurrentTrackInfo(track) {
|
||||||
|
const titleElement = document.getElementById('trackTitle');
|
||||||
|
const artistElement = document.getElementById('trackArtist');
|
||||||
|
const albumElement = document.getElementById('trackAlbum');
|
||||||
|
|
||||||
|
if (titleElement) titleElement.textContent = track.name;
|
||||||
|
if (artistElement) artistElement.textContent = '未知艺术家';
|
||||||
|
if (albumElement) albumElement.textContent = '未知专辑';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新播放列表显示
|
||||||
|
*/
|
||||||
|
updatePlaylist() {
|
||||||
|
if (!this.playlist_element) return;
|
||||||
|
|
||||||
|
if (this.playlist.length === 0) {
|
||||||
|
this.playlist_element.innerHTML = '<li class="playlist-empty">暂无音乐文件</li>';
|
||||||
|
this.updateTrackCount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playlist_element.innerHTML = this.playlist.map((track, index) => `
|
||||||
|
<li class="playlist-item ${index === this.currentTrackIndex ? 'playing' : ''}"
|
||||||
|
data-index="${index}">
|
||||||
|
<span class="track-number">${index + 1}</span>
|
||||||
|
<div class="track-details">
|
||||||
|
<div class="track-name">${track.name}</div>
|
||||||
|
<div class="track-duration">${this.formatTime(track.duration)}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// 添加点击事件
|
||||||
|
this.playlist_element.querySelectorAll('.playlist-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const index = parseInt(item.dataset.index);
|
||||||
|
this.loadTrack(index);
|
||||||
|
if (!this.isPlaying) {
|
||||||
|
this.togglePlayPause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateTrackCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新播放列表高亮
|
||||||
|
*/
|
||||||
|
updatePlaylistHighlight() {
|
||||||
|
if (!this.playlist_element) return;
|
||||||
|
|
||||||
|
this.playlist_element.querySelectorAll('.playlist-item').forEach((item, index) => {
|
||||||
|
item.classList.toggle('playing', index === this.currentTrackIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空播放列表
|
||||||
|
*/
|
||||||
|
clearPlaylist() {
|
||||||
|
this.playlist.forEach(track => {
|
||||||
|
if (track.url) {
|
||||||
|
URL.revokeObjectURL(track.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playlist = [];
|
||||||
|
this.currentTrackIndex = 0;
|
||||||
|
this.isPlaying = false;
|
||||||
|
|
||||||
|
if (this.audioPlayer) {
|
||||||
|
this.audioPlayer.pause();
|
||||||
|
this.audioPlayer.src = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePlaylist();
|
||||||
|
this.updatePlayButton();
|
||||||
|
this.updateCurrentTrackInfo({ name: '选择音乐文件开始播放' });
|
||||||
|
this.updateStatus('播放列表已清空');
|
||||||
|
this.savePlaylistToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态栏
|
||||||
|
*/
|
||||||
|
updateStatus(message) {
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新歌曲数量
|
||||||
|
*/
|
||||||
|
updateTrackCount() {
|
||||||
|
const trackCount = document.getElementById('trackCount');
|
||||||
|
if (trackCount) {
|
||||||
|
trackCount.textContent = `${this.playlist.length} 首歌曲`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间显示
|
||||||
|
*/
|
||||||
|
formatTime(seconds) {
|
||||||
|
if (!seconds || isNaN(seconds)) return '00:00';
|
||||||
|
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存播放列表到本地存储
|
||||||
|
*/
|
||||||
|
savePlaylistToStorage() {
|
||||||
|
try {
|
||||||
|
const playlistData = this.playlist.map(track => ({
|
||||||
|
id: track.id,
|
||||||
|
name: track.name,
|
||||||
|
duration: track.duration
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用系统SDK进行存储操作
|
||||||
|
if (this.systemSDK && this.systemSDK.storage) {
|
||||||
|
console.log('[音乐播放器] 保存播放列表到系统存储');
|
||||||
|
this.systemSDK.storage.set('music-player-playlist', JSON.stringify(playlistData))
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[音乐播放器] 播放列表保存成功');
|
||||||
|
} else {
|
||||||
|
console.warn('[音乐播放器] 保存播放列表到系统存储失败:', result.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[音乐播放器] 保存播放列表失败:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('[音乐播放器] 系统SDK未初始化,无法保存播放列表');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[音乐播放器] 保存播放列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地存储加载播放列表
|
||||||
|
*/
|
||||||
|
async loadPlaylistFromStorage() {
|
||||||
|
try {
|
||||||
|
// 使用系统SDK进行存储操作
|
||||||
|
if (this.systemSDK && this.systemSDK.storage) {
|
||||||
|
console.log('[音乐播放器] 从系统存储加载播放列表');
|
||||||
|
const result = await this.systemSDK.storage.get('music-player-playlist');
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
try {
|
||||||
|
const playlistData = JSON.parse(result.data);
|
||||||
|
console.log(`[音乐播放器] 从系统存储加载了 ${playlistData.length} 首歌曲`);
|
||||||
|
// 这里可以恢复播放列表
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('[音乐播放器] 解析播放列表数据失败:', parseError);
|
||||||
|
}
|
||||||
|
} else if (!result.success) {
|
||||||
|
console.warn('[音乐播放器] 从系统存储加载播放列表失败:', result.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[音乐播放器] 系统SDK未初始化,无法加载播放列表');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[音乐播放器] 加载播放列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
console.log('[音乐播放器] 清理资源');
|
||||||
|
|
||||||
|
// 暂停播放
|
||||||
|
if (this.audioPlayer) {
|
||||||
|
this.audioPlayer.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放对象URL
|
||||||
|
this.playlist.forEach(track => {
|
||||||
|
if (track.url) {
|
||||||
|
URL.revokeObjectURL(track.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
this.savePlaylistToStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用启动
|
||||||
|
let musicPlayerApp;
|
||||||
|
|
||||||
|
// 确保在DOM加载完成后启动应用
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
musicPlayerApp = new MusicPlayer();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
musicPlayerApp = new MusicPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出供外部使用
|
||||||
|
window.MusicPlayerApp = musicPlayerApp;
|
||||||
88
public/apps/music-player/index.html
Normal file
88
public/apps/music-player/index.html
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>音乐播放器</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="music-player">
|
||||||
|
<!-- 头部标题栏 -->
|
||||||
|
<header class="player-header">
|
||||||
|
<h1>🎵 音乐播放器</h1>
|
||||||
|
<div class="header-controls">
|
||||||
|
<button id="minimizeBtn" class="control-btn">➖</button>
|
||||||
|
<button id="maximizeBtn" class="control-btn">🔲</button>
|
||||||
|
<button id="closeBtn" class="control-btn">❌</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主要播放区域 -->
|
||||||
|
<main class="player-main">
|
||||||
|
<!-- 当前播放信息 -->
|
||||||
|
<section class="current-track">
|
||||||
|
<div class="track-art">
|
||||||
|
<img id="trackImage" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkY2NzMxIi8+CjxwYXRoIGQ9Ik0zNSA3NVYyNUw2NSA1MEwzNSA3NVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==" alt="音乐封面">
|
||||||
|
</div>
|
||||||
|
<div class="track-info">
|
||||||
|
<h2 id="trackTitle">选择音乐文件开始播放</h2>
|
||||||
|
<p id="trackArtist">未知艺术家</p>
|
||||||
|
<p id="trackAlbum">未知专辑</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 进度控制 -->
|
||||||
|
<section class="progress-section">
|
||||||
|
<div class="time-display">
|
||||||
|
<span id="currentTime">00:00</span>
|
||||||
|
<input type="range" id="progressBar" min="0" max="100" value="0" class="progress-bar">
|
||||||
|
<span id="totalTime">00:00</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 播放控制 -->
|
||||||
|
<section class="controls">
|
||||||
|
<button id="shuffleBtn" class="control-btn secondary">🔀</button>
|
||||||
|
<button id="prevBtn" class="control-btn">⏮️</button>
|
||||||
|
<button id="playPauseBtn" class="control-btn primary">▶️</button>
|
||||||
|
<button id="nextBtn" class="control-btn">⏭️</button>
|
||||||
|
<button id="repeatBtn" class="control-btn secondary">🔁</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<section class="volume-section">
|
||||||
|
<span class="volume-icon">🔊</span>
|
||||||
|
<input type="range" id="volumeBar" min="0" max="100" value="70" class="volume-bar">
|
||||||
|
<span id="volumeValue">70%</span>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 播放列表 -->
|
||||||
|
<aside class="playlist-section">
|
||||||
|
<div class="playlist-header">
|
||||||
|
<h3>播放列表</h3>
|
||||||
|
<div class="playlist-controls">
|
||||||
|
<input type="file" id="fileInput" accept="audio/*" multiple style="display: none;">
|
||||||
|
<button id="addFilesBtn" class="btn-secondary">添加文件</button>
|
||||||
|
<button id="clearPlaylistBtn" class="btn-secondary">清空列表</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul id="playlist" class="playlist">
|
||||||
|
<li class="playlist-empty">暂无音乐文件</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<footer class="status-bar">
|
||||||
|
<span id="statusText">就绪</span>
|
||||||
|
<span id="trackCount">0 首歌曲</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的音频元素 -->
|
||||||
|
<audio id="audioPlayer" preload="none"></audio>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
public/apps/music-player/manifest.json
Normal file
27
public/apps/music-player/manifest.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"id": "music-player",
|
||||||
|
"name": "音乐播放器",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "一个功能丰富的音乐播放器应用,支持播放本地音乐文件",
|
||||||
|
"author": "外置应用开发者",
|
||||||
|
"entryPoint": "index.html",
|
||||||
|
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iMjIiIGZpbGw9IiNGRjY3MzEiLz4KPHBhdGggZD0iTTE5IDMyVjE2TDMxIDI0TDE5IDMyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+",
|
||||||
|
"permissions": [
|
||||||
|
"storage.read",
|
||||||
|
"storage.write",
|
||||||
|
"file.read",
|
||||||
|
"system.notification"
|
||||||
|
],
|
||||||
|
"window": {
|
||||||
|
"width": 600,
|
||||||
|
"height": 400,
|
||||||
|
"minWidth": 400,
|
||||||
|
"minHeight": 300,
|
||||||
|
"resizable": true,
|
||||||
|
"minimizable": true,
|
||||||
|
"maximizable": true,
|
||||||
|
"closable": true
|
||||||
|
},
|
||||||
|
"category": "多媒体",
|
||||||
|
"keywords": ["音乐", "播放器", "媒体", "音频"]
|
||||||
|
}
|
||||||
430
public/apps/music-player/style.css
Normal file
430
public/apps/music-player/style.css
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
/* 音乐播放器样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-player {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部标题栏 */
|
||||||
|
.player-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(90deg, #FF6B35, #F7931E);
|
||||||
|
color: white;
|
||||||
|
user-select: none;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls .control-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls .control-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主播放区域 */
|
||||||
|
.player-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前播放信息 */
|
||||||
|
.current-track {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-art {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-art img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度控制 */
|
||||||
|
.progress-section {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #e1e1e1;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FF6B35;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
box-shadow: 0 3px 8px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FF6B35;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 播放控制 */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: rgba(255, 107, 53, 0.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary {
|
||||||
|
background: linear-gradient(135deg, #FF6B35, #F7931E);
|
||||||
|
color: white;
|
||||||
|
font-size: 32px;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.secondary {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.active {
|
||||||
|
color: #FF6B35;
|
||||||
|
background: rgba(255, 107, 53, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音量控制 */
|
||||||
|
.volume-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #e1e1e1;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-bar::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FF6B35;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-bar::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FF6B35;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#volumeValue {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 播放列表 */
|
||||||
|
.playlist-section {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
max-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item:hover {
|
||||||
|
background: rgba(255, 107, 53, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item.playing {
|
||||||
|
background: rgba(255, 107, 53, 0.15);
|
||||||
|
color: #FF6B35;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item .track-number {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
min-width: 20px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item .track-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item .track-name {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item .track-duration {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态栏 */
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.current-track {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-art img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary.playing {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
94
src/apps/AppRegistry.ts
Normal file
94
src/apps/AppRegistry.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import type { AppRegistration } from './types/AppManifest'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用注册中心
|
||||||
|
* 管理所有内置应用的注册和获取
|
||||||
|
*/
|
||||||
|
export class AppRegistry {
|
||||||
|
private static instance: AppRegistry | null = null
|
||||||
|
private apps = reactive(new Map<string, AppRegistration>())
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): AppRegistry {
|
||||||
|
if (!AppRegistry.instance) {
|
||||||
|
AppRegistry.instance = new AppRegistry()
|
||||||
|
}
|
||||||
|
return AppRegistry.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册内置应用
|
||||||
|
*/
|
||||||
|
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 = AppRegistry.getInstance()
|
||||||
344
src/apps/calculator/Calculator.vue
Normal file
344
src/apps/calculator/Calculator.vue
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<BuiltInApp app-id="calculator" title="计算器">
|
||||||
|
<div class="calculator">
|
||||||
|
<div class="display">
|
||||||
|
<input v-model="displayValue" type="text" readonly class="display-input" :class="{ 'error': hasError }">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<!-- 第一行 -->
|
||||||
|
<button @click="clear" class="btn btn-clear">C</button>
|
||||||
|
<button @click="deleteLast" class="btn btn-operation">←</button>
|
||||||
|
<button @click="appendOperation('/')" class="btn btn-operation">÷</button>
|
||||||
|
<button @click="appendOperation('*')" class="btn btn-operation">×</button>
|
||||||
|
|
||||||
|
<!-- 第二行 -->
|
||||||
|
<button @click="appendNumber('7')" class="btn btn-number">7</button>
|
||||||
|
<button @click="appendNumber('8')" class="btn btn-number">8</button>
|
||||||
|
<button @click="appendNumber('9')" class="btn btn-number">9</button>
|
||||||
|
<button @click="appendOperation('-')" class="btn btn-operation">-</button>
|
||||||
|
|
||||||
|
<!-- 第三行 -->
|
||||||
|
<button @click="appendNumber('4')" class="btn btn-number">4</button>
|
||||||
|
<button @click="appendNumber('5')" class="btn btn-number">5</button>
|
||||||
|
<button @click="appendNumber('6')" class="btn btn-number">6</button>
|
||||||
|
<button @click="appendOperation('+')" class="btn btn-operation">+</button>
|
||||||
|
|
||||||
|
<!-- 第四行 -->
|
||||||
|
<button @click="appendNumber('1')" class="btn btn-number">1</button>
|
||||||
|
<button @click="appendNumber('2')" class="btn btn-number">2</button>
|
||||||
|
<button @click="appendNumber('3')" class="btn btn-number">3</button>
|
||||||
|
<button @click="calculate" class="btn btn-equals" rowspan="2">=</button>
|
||||||
|
|
||||||
|
<!-- 第五行 -->
|
||||||
|
<button @click="appendNumber('0')" class="btn btn-number btn-zero">0</button>
|
||||||
|
<button @click="appendNumber('.')" class="btn btn-number">.</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BuiltInApp>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, inject } from 'vue'
|
||||||
|
import BuiltInApp from '../components/BuiltInApp.vue'
|
||||||
|
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||||
|
|
||||||
|
// 直接获取系统服务 - 无需通过SDK
|
||||||
|
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||||
|
|
||||||
|
const displayValue = ref('0')
|
||||||
|
const hasError = ref(false)
|
||||||
|
const lastResult = ref<number | null>(null)
|
||||||
|
const shouldResetDisplay = ref(false)
|
||||||
|
|
||||||
|
// 直接使用系统存储服务保存历史记录
|
||||||
|
const saveHistory = async (expression: string, result: string) => {
|
||||||
|
try {
|
||||||
|
if (systemService) {
|
||||||
|
const resourceService = systemService.getResourceService()
|
||||||
|
const history = await resourceService.getStorage('calculator', 'history') || []
|
||||||
|
history.push({
|
||||||
|
expression,
|
||||||
|
result,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
// 只保存最近30条记录
|
||||||
|
if (history.length > 30) {
|
||||||
|
history.shift()
|
||||||
|
}
|
||||||
|
await resourceService.setStorage('calculator', 'history', history)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存历史记录失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除事件服务相关代码
|
||||||
|
// // 直接使用事件服务发送通知
|
||||||
|
// const showNotification = (message: string) => {
|
||||||
|
// if (systemService) {
|
||||||
|
// const eventService = systemService.getEventService()
|
||||||
|
// eventService.sendMessage('calculator', 'user-interaction', {
|
||||||
|
// type: 'notification',
|
||||||
|
// message,
|
||||||
|
// timestamp: new Date()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 添加数字
|
||||||
|
const appendNumber = (num: string) => {
|
||||||
|
hasError.value = false
|
||||||
|
|
||||||
|
if (shouldResetDisplay.value) {
|
||||||
|
displayValue.value = '0'
|
||||||
|
shouldResetDisplay.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num === '.') {
|
||||||
|
if (!displayValue.value.includes('.')) {
|
||||||
|
displayValue.value += num
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (displayValue.value === '0') {
|
||||||
|
displayValue.value = num
|
||||||
|
} else {
|
||||||
|
displayValue.value += num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加运算符
|
||||||
|
const appendOperation = (op: string) => {
|
||||||
|
hasError.value = false
|
||||||
|
shouldResetDisplay.value = false
|
||||||
|
|
||||||
|
const lastChar = displayValue.value.slice(-1)
|
||||||
|
const operations = ['+', '-', '*', '/']
|
||||||
|
|
||||||
|
// 如果最后一个字符是运算符,替换它
|
||||||
|
if (operations.includes(lastChar)) {
|
||||||
|
displayValue.value = displayValue.value.slice(0, -1) + op
|
||||||
|
} else {
|
||||||
|
displayValue.value += op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算结果
|
||||||
|
const calculate = async () => {
|
||||||
|
try {
|
||||||
|
hasError.value = false
|
||||||
|
let expression = displayValue.value
|
||||||
|
.replace(/×/g, '*')
|
||||||
|
.replace(/÷/g, '/')
|
||||||
|
|
||||||
|
// 简单的表达式验证
|
||||||
|
if (/[+\-*/]$/.test(expression)) {
|
||||||
|
return // 以运算符结尾,不计算
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalExpression = displayValue.value
|
||||||
|
const result = eval(expression)
|
||||||
|
|
||||||
|
if (!isFinite(result)) {
|
||||||
|
throw new Error('除零错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
displayValue.value = result.toString()
|
||||||
|
lastResult.value = result
|
||||||
|
shouldResetDisplay.value = true
|
||||||
|
|
||||||
|
// 保存历史记录
|
||||||
|
await saveHistory(originalExpression, result.toString())
|
||||||
|
|
||||||
|
// 移除事件服务相关代码
|
||||||
|
// // 发送通知
|
||||||
|
// showNotification(`计算结果: ${result}`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
hasError.value = true
|
||||||
|
displayValue.value = '错误'
|
||||||
|
setTimeout(() => {
|
||||||
|
clear()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空
|
||||||
|
const clear = () => {
|
||||||
|
displayValue.value = '0'
|
||||||
|
hasError.value = false
|
||||||
|
lastResult.value = null
|
||||||
|
shouldResetDisplay.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除最后一个字符
|
||||||
|
const deleteLast = () => {
|
||||||
|
hasError.value = false
|
||||||
|
|
||||||
|
if (shouldResetDisplay.value) {
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayValue.value.length > 1) {
|
||||||
|
displayValue.value = displayValue.value.slice(0, -1)
|
||||||
|
} else {
|
||||||
|
displayValue.value = '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘事件处理
|
||||||
|
const handleKeyboard = (event: KeyboardEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const key = event.key
|
||||||
|
|
||||||
|
if (/[0-9.]/.test(key)) {
|
||||||
|
appendNumber(key)
|
||||||
|
} else if (['+', '-', '*', '/'].includes(key)) {
|
||||||
|
appendOperation(key)
|
||||||
|
} else if (key === 'Enter' || key === '=') {
|
||||||
|
calculate()
|
||||||
|
} else if (key === 'Escape' || key === 'c' || key === 'C') {
|
||||||
|
clear()
|
||||||
|
} else if (key === 'Backspace') {
|
||||||
|
deleteLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 添加键盘事件监听
|
||||||
|
document.addEventListener('keydown', handleKeyboard)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calculator {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 32px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 20px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-input.error {
|
||||||
|
color: #e74c3c;
|
||||||
|
border-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-template-rows: repeat(5, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-number {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-number:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-operation {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-operation:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equals {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equals:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-zero {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.calculator {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-input {
|
||||||
|
height: 60px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
height: 280px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
src/apps/components/BuiltInApp.vue
Normal file
125
src/apps/components/BuiltInApp.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="built-in-app" :class="[`app-${appId}`, { 'fullscreen': isFullscreen }]">
|
||||||
|
<div class="app-container">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, onMounted, onUnmounted, ref, provide } from 'vue'
|
||||||
|
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
appId: string
|
||||||
|
title?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||||
|
|
||||||
|
// 为子组件提供应用上下文
|
||||||
|
const appContext = {
|
||||||
|
appId: props.appId,
|
||||||
|
title: props.title || props.appId,
|
||||||
|
systemService,
|
||||||
|
// 直接暴露系统服务的方法,简化调用
|
||||||
|
storage: systemService?.getResourceService(),
|
||||||
|
// 移除 events: systemService?.getEventService(),
|
||||||
|
lifecycle: systemService?.getLifecycleManager(),
|
||||||
|
window: {
|
||||||
|
setTitle: (title: string) => {
|
||||||
|
if (window.SystemSDK) {
|
||||||
|
window.SystemSDK.window.setTitle(title)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleFullscreen: () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供应用上下文给子组件
|
||||||
|
provide('appContext', appContext)
|
||||||
|
|
||||||
|
// SDK初始化 - 简化版本
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK) {
|
||||||
|
await window.SystemSDK.init({
|
||||||
|
appId: props.appId,
|
||||||
|
appName: props.title || props.appId,
|
||||||
|
version: '1.0.0',
|
||||||
|
permissions: ['storage', 'notification']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (props.title) {
|
||||||
|
await window.SystemSDK.window.setTitle(props.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`内置应用 ${props.appId} 初始化成功`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`内置应用 ${props.appId} 初始化失败:`, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
window.SystemSDK.destroy?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换全屏模式
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
toggleFullscreen,
|
||||||
|
appContext
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.built-in-app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: white;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 应用特定样式 */
|
||||||
|
.app-calculator {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-notepad {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-todo {
|
||||||
|
/* 待办事项应用样式 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
99
src/apps/index.ts
Normal file
99
src/apps/index.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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: {
|
||||||
|
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: {
|
||||||
|
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: {
|
||||||
|
width: 600,
|
||||||
|
height: 700,
|
||||||
|
minWidth: 400,
|
||||||
|
minHeight: 500,
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
category: 'productivity',
|
||||||
|
keywords: ['待办事项', '任务管理', 'todo', 'task', 'productivity'],
|
||||||
|
},
|
||||||
|
// 使用动态导入实现懒加载
|
||||||
|
component: async () => {
|
||||||
|
const { default: Todo } = await import('./todo/Todo.vue')
|
||||||
|
return markRaw(Todo)
|
||||||
|
},
|
||||||
|
isBuiltIn: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('内置应用注册完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出应用注册中心
|
||||||
|
export { appRegistry } from './AppRegistry'
|
||||||
|
export type { InternalAppManifest, AppRegistration } from './types/AppManifest'
|
||||||
527
src/apps/notepad/Notepad.vue
Normal file
527
src/apps/notepad/Notepad.vue
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
<template>
|
||||||
|
<BuiltInApp app-id="notepad" title="记事本">
|
||||||
|
<div class="notepad">
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button @click="newFile" class="btn btn-secondary">
|
||||||
|
<span class="icon">📄</span>
|
||||||
|
新建
|
||||||
|
</button>
|
||||||
|
<button @click="saveFile" class="btn btn-primary" :disabled="!hasChanges">
|
||||||
|
<span class="icon">💾</span>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button @click="openFile" class="btn btn-secondary">
|
||||||
|
<span class="icon">📂</span>
|
||||||
|
打开
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<select v-model="fontSize" @change="updateFontSize" class="font-size-select">
|
||||||
|
<option value="12">12px</option>
|
||||||
|
<option value="14">14px</option>
|
||||||
|
<option value="16">16px</option>
|
||||||
|
<option value="18">18px</option>
|
||||||
|
<option value="20">20px</option>
|
||||||
|
<option value="24">24px</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="toggleWordWrap"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
:class="{ active: wordWrap }"
|
||||||
|
>
|
||||||
|
<span class="icon">📄</span>
|
||||||
|
自动换行
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-info">
|
||||||
|
<span class="filename">{{ currentFileName }}</span>
|
||||||
|
<span v-if="hasChanges" class="unsaved">*</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-container">
|
||||||
|
<textarea
|
||||||
|
ref="editorRef"
|
||||||
|
v-model="content"
|
||||||
|
class="editor"
|
||||||
|
:style="editorStyle"
|
||||||
|
placeholder="开始输入文本..."
|
||||||
|
@input="onContentChange"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status-left">
|
||||||
|
<span>行: {{ currentLine }}</span>
|
||||||
|
<span>列: {{ currentColumn }}</span>
|
||||||
|
<span>字符数: {{ characterCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-right">
|
||||||
|
<span>{{ fileEncoding }}</span>
|
||||||
|
<span>{{ lastSaved ? `上次保存: ${formatTime(lastSaved)}` : '未保存' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表对话框 -->
|
||||||
|
<div v-if="showFileList" class="modal-overlay" @click="showFileList = false">
|
||||||
|
<div class="modal" @click.stop>
|
||||||
|
<h3>选择文件</h3>
|
||||||
|
<div class="file-list">
|
||||||
|
<div
|
||||||
|
v-for="file in savedFiles"
|
||||||
|
:key="file.name"
|
||||||
|
class="file-item"
|
||||||
|
@click="loadFile(file.name)"
|
||||||
|
>
|
||||||
|
<span class="file-icon">📄</span>
|
||||||
|
<span class="file-name">{{ file.name }}</span>
|
||||||
|
<span class="file-date">{{ formatTime(file.date) }}</span>
|
||||||
|
<button @click.stop="deleteFile(file.name)" class="btn-delete">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button @click="showFileList = false" class="btn btn-secondary">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BuiltInApp>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
|
import BuiltInApp from '../components/BuiltInApp.vue'
|
||||||
|
|
||||||
|
const editorRef = ref<HTMLTextAreaElement>()
|
||||||
|
const content = ref('')
|
||||||
|
const currentFileName = ref('新文档.txt')
|
||||||
|
const hasChanges = ref(false)
|
||||||
|
const fontSize = ref('16')
|
||||||
|
const wordWrap = ref(true)
|
||||||
|
const lastSaved = ref<Date | null>(null)
|
||||||
|
const fileEncoding = ref('UTF-8')
|
||||||
|
const showFileList = ref(false)
|
||||||
|
const savedFiles = ref<Array<{name: string, date: Date}>>([])
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const editorStyle = computed(() => ({
|
||||||
|
fontSize: fontSize.value + 'px',
|
||||||
|
whiteSpace: wordWrap.value ? 'pre-wrap' : 'pre'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const characterCount = computed(() => content.value.length)
|
||||||
|
|
||||||
|
const currentLine = ref(1)
|
||||||
|
const currentColumn = ref(1)
|
||||||
|
|
||||||
|
// 获取光标位置
|
||||||
|
const updateCursorPosition = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
|
||||||
|
const textarea = editorRef.value
|
||||||
|
const text = textarea.value
|
||||||
|
const position = textarea.selectionStart
|
||||||
|
|
||||||
|
const lines = text.substring(0, position).split('\n')
|
||||||
|
currentLine.value = lines.length
|
||||||
|
currentColumn.value = lines[lines.length - 1].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容变化处理
|
||||||
|
const onContentChange = () => {
|
||||||
|
hasChanges.value = true
|
||||||
|
updateCursorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件
|
||||||
|
const newFile = async () => {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
if (confirm('当前文档未保存,是否保存?')) {
|
||||||
|
await saveFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.value = ''
|
||||||
|
currentFileName.value = '新文档.txt'
|
||||||
|
hasChanges.value = false
|
||||||
|
lastSaved.value = null
|
||||||
|
updateTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
const saveFile = async () => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
await window.SystemSDK.storage.set(`notepad_${currentFileName.value}`, content.value)
|
||||||
|
hasChanges.value = false
|
||||||
|
lastSaved.value = new Date()
|
||||||
|
|
||||||
|
// 更新文件列表
|
||||||
|
await updateFilesList()
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
if (window.SystemSDK.ui?.showNotification) {
|
||||||
|
await window.SystemSDK.ui.showNotification({
|
||||||
|
title: '保存成功',
|
||||||
|
body: `文件 "${currentFileName.value}" 已保存`,
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('系统SDK未初始化,无法保存文件')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存文件失败:', error)
|
||||||
|
alert('保存文件失败: ' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
const openFile = async () => {
|
||||||
|
await updateFilesList()
|
||||||
|
showFileList.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载文件
|
||||||
|
const loadFile = async (fileName: string) => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
const result = await window.SystemSDK.storage.get(`notepad_${fileName}`)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
content.value = result.data
|
||||||
|
currentFileName.value = fileName
|
||||||
|
hasChanges.value = false
|
||||||
|
lastSaved.value = new Date()
|
||||||
|
showFileList.value = false
|
||||||
|
updateTitle()
|
||||||
|
} else {
|
||||||
|
alert('文件不存在或为空')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开文件失败:', error)
|
||||||
|
alert('打开文件失败: ' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
const deleteFile = async (fileName: string) => {
|
||||||
|
if (confirm(`确定要删除文件 "${fileName}" 吗?`)) {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
await window.SystemSDK.storage.remove(`notepad_${fileName}`)
|
||||||
|
await updateFilesList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除文件失败:', error)
|
||||||
|
alert('删除文件失败: ' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新文件列表
|
||||||
|
const updateFilesList = async () => {
|
||||||
|
// 这里需要实现获取所有以 notepad_ 开头的存储键
|
||||||
|
// 由于当前SDK没有提供列出所有键的功能,我们先使用一个简化的实现
|
||||||
|
savedFiles.value = []
|
||||||
|
|
||||||
|
// 临时实现:尝试加载一些常见的文件名
|
||||||
|
const commonNames = ['新文档.txt', '笔记.txt', '备忘录.txt', '临时.txt']
|
||||||
|
for (const name of commonNames) {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
const result = await window.SystemSDK.storage.get(`notepad_${name}`)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
savedFiles.value.push({
|
||||||
|
name,
|
||||||
|
date: new Date() // 暂时使用当前时间
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字体大小
|
||||||
|
const updateFontSize = () => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换自动换行
|
||||||
|
const toggleWordWrap = () => {
|
||||||
|
wordWrap.value = !wordWrap.value
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新标题
|
||||||
|
const updateTitle = () => {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
const title = `记事本 - ${currentFileName.value}${hasChanges.value ? ' *' : ''}`
|
||||||
|
window.SystemSDK.window.setTitle(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (date: Date) => {
|
||||||
|
return date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理键盘快捷键
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 's':
|
||||||
|
event.preventDefault()
|
||||||
|
saveFile()
|
||||||
|
break
|
||||||
|
case 'n':
|
||||||
|
event.preventDefault()
|
||||||
|
newFile()
|
||||||
|
break
|
||||||
|
case 'o':
|
||||||
|
event.preventDefault()
|
||||||
|
openFile()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新光标位置
|
||||||
|
nextTick(() => {
|
||||||
|
updateCursorPosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化光标位置
|
||||||
|
updateCursorPosition()
|
||||||
|
|
||||||
|
// 监听编辑器点击和键盘事件
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.addEventListener('click', updateCursorPosition)
|
||||||
|
editorRef.value.addEventListener('keyup', updateCursorPosition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notepad {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsaved {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: 'Courier New', 'Consolas', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-left,
|
||||||
|
.status-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #f8f9fa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
658
src/apps/todo/Todo.vue
Normal file
658
src/apps/todo/Todo.vue
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
<template>
|
||||||
|
<BuiltInApp app-id="todo" title="待办事项">
|
||||||
|
<div class="todo-app">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📝 待办事项</h1>
|
||||||
|
<div class="stats">
|
||||||
|
总计: <span class="count">{{ todos.length }}</span> |
|
||||||
|
已完成: <span class="count">{{ completedCount }}</span> |
|
||||||
|
待办: <span class="count">{{ pendingCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-todo">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
v-model="newTodoText"
|
||||||
|
type="text"
|
||||||
|
class="todo-input"
|
||||||
|
placeholder="添加新的待办事项..."
|
||||||
|
maxlength="200"
|
||||||
|
@keypress.enter="addTodo"
|
||||||
|
>
|
||||||
|
<button @click="addTodo" class="add-btn" :disabled="!newTodoText.trim()">
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<button
|
||||||
|
v-for="filter in filters"
|
||||||
|
:key="filter.key"
|
||||||
|
class="filter-btn"
|
||||||
|
:class="{ active: currentFilter === filter.key }"
|
||||||
|
@click="setFilter(filter.key)"
|
||||||
|
>
|
||||||
|
{{ filter.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo-list" v-if="filteredTodos.length > 0">
|
||||||
|
<TransitionGroup name="todo-list" tag="div">
|
||||||
|
<div
|
||||||
|
v-for="todo in filteredTodos"
|
||||||
|
:key="todo.id"
|
||||||
|
class="todo-item"
|
||||||
|
:class="{ 'completed': todo.completed }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="todo-checkbox"
|
||||||
|
:checked="todo.completed"
|
||||||
|
@change="toggleTodo(todo.id)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="todo-content" v-if="!todo.editing">
|
||||||
|
<span class="todo-text" :class="{ 'completed': todo.completed }">
|
||||||
|
{{ todo.text }}
|
||||||
|
</span>
|
||||||
|
<span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo-edit" v-else>
|
||||||
|
<input
|
||||||
|
v-model="todo.editText"
|
||||||
|
type="text"
|
||||||
|
class="edit-input"
|
||||||
|
@keypress.enter="saveEdit(todo)"
|
||||||
|
@keyup.esc="cancelEdit(todo)"
|
||||||
|
@blur="saveEdit(todo)"
|
||||||
|
ref="editInputs"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo-actions">
|
||||||
|
<button
|
||||||
|
v-if="!todo.editing"
|
||||||
|
@click="startEdit(todo)"
|
||||||
|
class="edit-btn"
|
||||||
|
title="编辑"
|
||||||
|
>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteTodo(todo.id)"
|
||||||
|
class="delete-btn"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<h3>{{ getEmptyMessage() }}</h3>
|
||||||
|
<p>{{ getEmptyDescription() }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量操作 -->
|
||||||
|
<div class="bulk-actions" v-if="todos.length > 0">
|
||||||
|
<button @click="markAllCompleted" class="bulk-btn">
|
||||||
|
全部完成
|
||||||
|
</button>
|
||||||
|
<button @click="clearCompleted" class="bulk-btn" v-if="completedCount > 0">
|
||||||
|
清除已完成 ({{ completedCount }})
|
||||||
|
</button>
|
||||||
|
<button @click="clearAll" class="bulk-btn bulk-danger">
|
||||||
|
清空全部
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BuiltInApp>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
|
import BuiltInApp from '../components/BuiltInApp.vue'
|
||||||
|
|
||||||
|
interface Todo {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
completed: boolean
|
||||||
|
createdAt: Date
|
||||||
|
completedAt?: Date
|
||||||
|
editing?: boolean
|
||||||
|
editText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'pending' | 'completed'
|
||||||
|
|
||||||
|
const newTodoText = ref('')
|
||||||
|
const todos = ref<Todo[]>([])
|
||||||
|
const currentFilter = ref<FilterType>('all')
|
||||||
|
const editInputs = ref<HTMLInputElement[]>([])
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ key: 'all' as FilterType, label: '全部' },
|
||||||
|
{ key: 'pending' as FilterType, label: '待办' },
|
||||||
|
{ key: 'completed' as FilterType, label: '已完成' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const completedCount = computed(() =>
|
||||||
|
todos.value.filter(todo => todo.completed).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const pendingCount = computed(() =>
|
||||||
|
todos.value.filter(todo => !todo.completed).length
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredTodos = computed(() => {
|
||||||
|
switch (currentFilter.value) {
|
||||||
|
case 'completed':
|
||||||
|
return todos.value.filter(todo => todo.completed)
|
||||||
|
case 'pending':
|
||||||
|
return todos.value.filter(todo => !todo.completed)
|
||||||
|
default:
|
||||||
|
return todos.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const addTodo = async () => {
|
||||||
|
const text = newTodoText.value.trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
const todo: Todo = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
text,
|
||||||
|
completed: false,
|
||||||
|
createdAt: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
todos.value.unshift(todo)
|
||||||
|
newTodoText.value = ''
|
||||||
|
|
||||||
|
await saveTodos()
|
||||||
|
showNotification('✅ 待办事项已添加', `"${text}" 已添加到您的待办列表`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTodo = async (id: string) => {
|
||||||
|
const todo = todos.value.find(t => t.id === id)
|
||||||
|
if (todo) {
|
||||||
|
todo.completed = !todo.completed
|
||||||
|
todo.completedAt = todo.completed ? new Date() : undefined
|
||||||
|
|
||||||
|
await saveTodos()
|
||||||
|
|
||||||
|
if (todo.completed) {
|
||||||
|
showNotification('🎉 任务完成', `"${todo.text}" 已完成!`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = (todo: Todo) => {
|
||||||
|
todo.editing = true
|
||||||
|
todo.editText = todo.text
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const input = editInputs.value?.find(input =>
|
||||||
|
input && input.closest('.todo-item')?.getAttribute('data-id') === todo.id
|
||||||
|
)
|
||||||
|
input?.focus()
|
||||||
|
input?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEdit = async (todo: Todo) => {
|
||||||
|
if (todo.editText && todo.editText.trim() !== todo.text) {
|
||||||
|
todo.text = todo.editText.trim()
|
||||||
|
await saveTodos()
|
||||||
|
}
|
||||||
|
|
||||||
|
todo.editing = false
|
||||||
|
todo.editText = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = (todo: Todo) => {
|
||||||
|
todo.editing = false
|
||||||
|
todo.editText = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTodo = async (id: string) => {
|
||||||
|
if (confirm('确定要删除这个待办事项吗?')) {
|
||||||
|
todos.value = todos.value.filter(t => t.id !== id)
|
||||||
|
await saveTodos()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilter = (filter: FilterType) => {
|
||||||
|
currentFilter.value = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
const markAllCompleted = async () => {
|
||||||
|
todos.value.forEach(todo => {
|
||||||
|
if (!todo.completed) {
|
||||||
|
todo.completed = true
|
||||||
|
todo.completedAt = new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await saveTodos()
|
||||||
|
showNotification('✅ 全部完成', '所有待办事项已标记为完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCompleted = async () => {
|
||||||
|
if (confirm(`确定要删除 ${completedCount.value} 个已完成的待办事项吗?`)) {
|
||||||
|
todos.value = todos.value.filter(todo => !todo.completed)
|
||||||
|
await saveTodos()
|
||||||
|
showNotification('🗑️ 清理完成', '已清除所有已完成的待办事项')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAll = async () => {
|
||||||
|
if (confirm('确定要清空所有待办事项吗?此操作不可恢复。')) {
|
||||||
|
todos.value = []
|
||||||
|
await saveTodos()
|
||||||
|
showNotification('🗑️ 清空完成', '所有待办事项已清空')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTodos = async () => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
const result = await window.SystemSDK.storage.get('todos')
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const savedTodos = JSON.parse(result.data)
|
||||||
|
todos.value = savedTodos.map((todo: any) => ({
|
||||||
|
...todo,
|
||||||
|
createdAt: new Date(todo.createdAt),
|
||||||
|
completedAt: todo.completedAt ? new Date(todo.completedAt) : undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTodos = async () => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK && window.SystemSDK.initialized) {
|
||||||
|
await window.SystemSDK.storage.set('todos', JSON.stringify(todos.value))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存待办事项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNotification = async (title: string, message: string) => {
|
||||||
|
try {
|
||||||
|
if (window.SystemSDK?.ui?.showNotification) {
|
||||||
|
await window.SystemSDK.ui.showNotification({
|
||||||
|
title,
|
||||||
|
body: message,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('显示通知失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffMins = Math.floor(diffMs / (1000 * 60))
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (diffMins < 1) return '刚刚'
|
||||||
|
if (diffMins < 60) return `${diffMins}分钟前`
|
||||||
|
if (diffHours < 24) return `${diffHours}小时前`
|
||||||
|
if (diffDays < 7) return `${diffDays}天前`
|
||||||
|
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEmptyMessage = () => {
|
||||||
|
switch (currentFilter.value) {
|
||||||
|
case 'completed':
|
||||||
|
return '还没有已完成的任务'
|
||||||
|
case 'pending':
|
||||||
|
return '没有待办事项'
|
||||||
|
default:
|
||||||
|
return '还没有待办事项'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEmptyDescription = () => {
|
||||||
|
switch (currentFilter.value) {
|
||||||
|
case 'completed':
|
||||||
|
return '完成一些任务后,它们会出现在这里'
|
||||||
|
case 'pending':
|
||||||
|
return '所有任务都已完成,真棒!'
|
||||||
|
default:
|
||||||
|
return '添加一个新的待办事项开始管理您的任务吧!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadTodos()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.todo-app {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-todo {
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover:not(:disabled) {
|
||||||
|
background: #5a67d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover:not(.active) {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item.completed {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-checkbox {
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-text {
|
||||||
|
font-size: 16px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-text.completed {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-edit {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn,
|
||||||
|
.delete-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover,
|
||||||
|
.delete-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-actions {
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-btn:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-danger {
|
||||||
|
color: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-danger:hover {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
.todo-list-enter-active,
|
||||||
|
.todo-list-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list-move {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-todo {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-actions {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
src/apps/types/AppManifest.ts
Normal file
104
src/apps/types/AppManifest.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* 内置应用清单接口
|
||||||
|
*/
|
||||||
|
export interface InternalAppManifest {
|
||||||
|
/**
|
||||||
|
* 应用唯一标识符
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* 应用名称
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
/**
|
||||||
|
* 应用版本号
|
||||||
|
*/
|
||||||
|
version: string
|
||||||
|
/**
|
||||||
|
* 应用描述信息
|
||||||
|
*/
|
||||||
|
description: string
|
||||||
|
/**
|
||||||
|
* 应用作者
|
||||||
|
*/
|
||||||
|
author: string
|
||||||
|
/**
|
||||||
|
* 应用图标
|
||||||
|
*/
|
||||||
|
icon: string
|
||||||
|
/**
|
||||||
|
* 应用所需权限列表
|
||||||
|
*/
|
||||||
|
permissions: string[]
|
||||||
|
/**
|
||||||
|
* 窗体配置信息
|
||||||
|
*/
|
||||||
|
window: {
|
||||||
|
/**
|
||||||
|
* 窗体宽度
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* 窗体高度
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
/**
|
||||||
|
* 窗体最小宽度
|
||||||
|
*/
|
||||||
|
minWidth?: number
|
||||||
|
/**
|
||||||
|
* 窗体最小高度
|
||||||
|
*/
|
||||||
|
minHeight?: number
|
||||||
|
/**
|
||||||
|
* 窗体最大宽度
|
||||||
|
*/
|
||||||
|
maxWidth?: number
|
||||||
|
/**
|
||||||
|
* 窗体最大高度
|
||||||
|
*/
|
||||||
|
maxHeight?: number
|
||||||
|
/**
|
||||||
|
* 是否可调整大小
|
||||||
|
*/
|
||||||
|
resizable?: boolean
|
||||||
|
/**
|
||||||
|
* 是否可最小化
|
||||||
|
*/
|
||||||
|
minimizable?: boolean
|
||||||
|
/**
|
||||||
|
* 是否可最大化
|
||||||
|
*/
|
||||||
|
maximizable?: boolean
|
||||||
|
/**
|
||||||
|
* 是否可关闭
|
||||||
|
*/
|
||||||
|
closable?: boolean
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 应用分类
|
||||||
|
*/
|
||||||
|
category?: string
|
||||||
|
/**
|
||||||
|
* 应用关键字列表
|
||||||
|
*/
|
||||||
|
keywords?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用注册信息
|
||||||
|
*/
|
||||||
|
export interface AppRegistration {
|
||||||
|
/**
|
||||||
|
* 应用清单信息
|
||||||
|
*/
|
||||||
|
manifest: InternalAppManifest
|
||||||
|
/**
|
||||||
|
* Vue组件或异步加载函数
|
||||||
|
*/
|
||||||
|
component: any // Vue组件或返回Promise<Vue组件>的函数
|
||||||
|
/**
|
||||||
|
* 是否为内置应用
|
||||||
|
*/
|
||||||
|
isBuiltIn: boolean
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
|
|
||||||
import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
|
|
||||||
import { NotificationService } from '@/core/service/services/NotificationService.ts'
|
|
||||||
import { SettingsService } from '@/core/service/services/SettingsService.ts'
|
|
||||||
import { WindowFormService } from '@/core/service/services/WindowFormService.ts'
|
|
||||||
import { UserService } from '@/core/service/services/UserService.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
|
|
||||||
export default class XSystem {
|
|
||||||
private static _instance: XSystem = new XSystem()
|
|
||||||
|
|
||||||
private _desktopRootDom: HTMLElement;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
console.log('XSystem')
|
|
||||||
new NotificationService()
|
|
||||||
new SettingsService()
|
|
||||||
new WindowFormService()
|
|
||||||
new UserService()
|
|
||||||
}
|
|
||||||
|
|
||||||
public static get instance() {
|
|
||||||
return this._instance
|
|
||||||
}
|
|
||||||
public get desktopRootDom() {
|
|
||||||
return this._desktopRootDom
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initialization(dom: HTMLDivElement) {
|
|
||||||
this._desktopRootDom = dom
|
|
||||||
await processManager.runProcess('basic-system', BasicSystemProcess)
|
|
||||||
await processManager.runProcess('desktop', DesktopProcess, dom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "department",
|
|
||||||
"title": "部门",
|
|
||||||
"description": "部门",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "部门",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 200,
|
|
||||||
"height": 100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
部门页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "fileManage",
|
|
||||||
"title": "文件管理",
|
|
||||||
"description": "文件管理",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "文件管理",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
文件管理页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "music",
|
|
||||||
"title": "音乐",
|
|
||||||
"description": "音乐",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "音乐",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 200,
|
|
||||||
"height": 100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
音乐页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "personalCenter",
|
|
||||||
"title": "个人中心",
|
|
||||||
"description": "个人中心",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "个人中心",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
个人中心页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "photograph",
|
|
||||||
"title": "照片",
|
|
||||||
"description": "照片",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "照片",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
照片页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "recycleBin",
|
|
||||||
"title": "回收站",
|
|
||||||
"description": "回收站",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "回收站",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
回收站页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "setting",
|
|
||||||
"title": "设置",
|
|
||||||
"description": "设置",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "设置",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
设置APP页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tv",
|
|
||||||
"title": "电视",
|
|
||||||
"description": "电视",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "电视",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
电视页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "video",
|
|
||||||
"title": "电影",
|
|
||||||
"description": "电影",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"startName": "main",
|
|
||||||
"singleton": true,
|
|
||||||
"isJustProcess": false,
|
|
||||||
"windowFormConfigs": [
|
|
||||||
{
|
|
||||||
"name": "main",
|
|
||||||
"title": "电影",
|
|
||||||
"icon": "iconfont icon-setting",
|
|
||||||
"width": 800,
|
|
||||||
"height": 600
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
电影页面
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { reactive, onBeforeUnmount, type Reactive } from 'vue'
|
|
||||||
import type { IObservable } from '@/core/state/IObservable.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue Hook: useObservable
|
|
||||||
* 支持深层解构赋值,直接修改触发 ObservableImpl 通知 + Vue 响应式更新
|
|
||||||
* @example
|
|
||||||
* interface AppState {
|
|
||||||
* count: number
|
|
||||||
* user: { name: string; age: number }
|
|
||||||
* items: number[]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 创建 ObservableImpl
|
|
||||||
* const obs = new ObservableImpl<AppState>({
|
|
||||||
* count: 0,
|
|
||||||
* user: { name: 'Alice', age: 20 },
|
|
||||||
* items: []
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* export default defineComponent({
|
|
||||||
* setup() {
|
|
||||||
* // 深层解构 Hook
|
|
||||||
* const { count, user, items } = useObservable(obs)
|
|
||||||
*
|
|
||||||
* const increment = () => {
|
|
||||||
* count += 1 // 触发 ObservableImpl 通知 + Vue 更新
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* const changeAge = () => {
|
|
||||||
* user.age = 30 // 深层对象也能触发通知
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* const addItem = () => {
|
|
||||||
* items.push(42) // 数组方法拦截,触发通知
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* return { count, user, items, increment, changeAge, addItem }
|
|
||||||
* }
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
export function useObservable<T extends object>(observable: IObservable<T>): Reactive<T> {
|
|
||||||
// 创建 Vue 响应式对象
|
|
||||||
const state = reactive({} as T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 ObservableImpl Proxy 映射到 Vue 响应式对象
|
|
||||||
* 递归支持深层对象
|
|
||||||
*/
|
|
||||||
function mapKeys(obj: any, proxy: any) {
|
|
||||||
(Object.keys(proxy) as (keyof typeof proxy)[]).forEach(key => {
|
|
||||||
const value = proxy[key]
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
// 递归创建子对象 Proxy
|
|
||||||
obj[key] = reactive({} as typeof value)
|
|
||||||
mapKeys(obj[key], value)
|
|
||||||
} else {
|
|
||||||
// 基本类型通过 getter/setter 同步
|
|
||||||
Object.defineProperty(obj, key, {
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
get() {
|
|
||||||
return proxy[key]
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
proxy[key] = val
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 ObservableImpl 的 Proxy
|
|
||||||
const refsProxy = observable.toRefsProxy()
|
|
||||||
mapKeys(state, refsProxy)
|
|
||||||
|
|
||||||
// 订阅 ObservableImpl,保持响应式同步
|
|
||||||
const unsubscribe = observable.subscribe(() => {
|
|
||||||
// 空实现即可,getter/setter 已同步
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
unsubscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
create,
|
|
||||||
NButton,
|
|
||||||
NCard,
|
|
||||||
NConfigProvider,
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
export const naiveUi = create({
|
|
||||||
components: [NButton, NCard, NConfigProvider]
|
|
||||||
})
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { createDiscreteApi } from 'naive-ui'
|
|
||||||
import { configProviderProps } from './theme.ts'
|
|
||||||
|
|
||||||
const { message, notification, dialog, loadingBar, modal } = createDiscreteApi(
|
|
||||||
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
|
|
||||||
{
|
|
||||||
configProviderProps: configProviderProps,
|
|
||||||
notificationProviderProps: {
|
|
||||||
placement: 'bottom-right',
|
|
||||||
max: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const { messageApi, notificationApi, dialogApi, loadingBarApi, modalApi } = {
|
|
||||||
messageApi: message,
|
|
||||||
notificationApi: notification,
|
|
||||||
dialogApi: dialog,
|
|
||||||
loadingBarApi: loadingBar,
|
|
||||||
modalApi: modal
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { type ConfigProviderProps, darkTheme, dateZhCN, type GlobalTheme, lightTheme, zhCN } from 'naive-ui'
|
|
||||||
|
|
||||||
const lTheme: GlobalTheme = {
|
|
||||||
...lightTheme,
|
|
||||||
common: {
|
|
||||||
...lightTheme.common,
|
|
||||||
primaryColor: '#0070f3'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const configProviderProps: ConfigProviderProps = {
|
|
||||||
theme: lTheme,
|
|
||||||
dateLocale: dateZhCN,
|
|
||||||
locale: zhCN,
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* 可销毁接口
|
|
||||||
* 销毁实例,清理副作用,让内存可以被回收
|
|
||||||
*/
|
|
||||||
export interface IDestroyable {
|
|
||||||
/** 销毁实例,清理副作用,让内存可以被回收 */
|
|
||||||
destroy(): void
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* 版本信息
|
|
||||||
*/
|
|
||||||
export interface IVersion {
|
|
||||||
/**
|
|
||||||
* 公司名称
|
|
||||||
*/
|
|
||||||
company: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 版本号
|
|
||||||
*/
|
|
||||||
major: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 子版本号
|
|
||||||
*/
|
|
||||||
minor: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修订号
|
|
||||||
*/
|
|
||||||
build: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私有版本号
|
|
||||||
*/
|
|
||||||
private: number
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import ProcessImpl from '@/core/process/impl/ProcessImpl.ts'
|
|
||||||
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
|
|
||||||
import { createApp, h } from 'vue'
|
|
||||||
import DesktopComponent from '@/core/desktop/ui/DesktopComponent.vue'
|
|
||||||
import { naiveUi } from '@/core/common/naive-ui/components.ts'
|
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
import './ui/DesktopElement.ts'
|
|
||||||
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
|
|
||||||
|
|
||||||
interface IDesktopDataState {
|
|
||||||
/** 显示器宽度 */
|
|
||||||
monitorWidth: number;
|
|
||||||
/** 显示器高度 */
|
|
||||||
monitorHeight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DesktopProcess extends ProcessImpl {
|
|
||||||
/** 桌面根dom,类似显示器 */
|
|
||||||
private readonly _monitorDom: HTMLElement
|
|
||||||
private _isMounted: boolean = false
|
|
||||||
private _data = new ObservableImpl<IDesktopDataState>({
|
|
||||||
monitorWidth: 0,
|
|
||||||
monitorHeight: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
public get monitorDom() {
|
|
||||||
return this._monitorDom
|
|
||||||
}
|
|
||||||
public get isMounted() {
|
|
||||||
return this._isMounted
|
|
||||||
}
|
|
||||||
public get basicSystemProcess() {
|
|
||||||
return processManager.findProcessByName<BasicSystemProcess>('basic-system')
|
|
||||||
}
|
|
||||||
|
|
||||||
get data() {
|
|
||||||
return this._data
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(info: IProcessInfo, dom: HTMLDivElement) {
|
|
||||||
super(info)
|
|
||||||
console.log('DesktopProcess')
|
|
||||||
dom.style.position = 'relative'
|
|
||||||
dom.style.overflow = 'hidden'
|
|
||||||
dom.style.width = `${window.innerWidth}px`
|
|
||||||
dom.style.height = `${window.innerHeight}px`
|
|
||||||
|
|
||||||
this._monitorDom = dom
|
|
||||||
this._data.state.monitorWidth = window.innerWidth
|
|
||||||
this._data.state.monitorHeight = window.innerHeight
|
|
||||||
window.addEventListener('resize', this.onResize)
|
|
||||||
|
|
||||||
this.createDesktopUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResize = debounce(() => {
|
|
||||||
this._monitorDom.style.width = `${window.innerWidth}px`
|
|
||||||
this._monitorDom.style.height = `${window.innerHeight}px`
|
|
||||||
this._data.state.monitorWidth = window.innerWidth
|
|
||||||
this._data.state.monitorHeight = window.innerHeight
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
private createDesktopUI() {
|
|
||||||
if (this._isMounted) return
|
|
||||||
const app = createApp(DesktopComponent, { process: this })
|
|
||||||
app.use(naiveUi)
|
|
||||||
app.mount(this._monitorDom)
|
|
||||||
this._isMounted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private initDesktop(dom: HTMLDivElement) {
|
|
||||||
const d = document.createElement('desktop-element')
|
|
||||||
dom.appendChild(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
override destroy() {
|
|
||||||
super.destroy()
|
|
||||||
window.removeEventListener('resize', this.onResize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
|
|
||||||
|
|
||||||
export const DesktopProcessInfo = new ProcessInfoImpl({
|
|
||||||
name: 'desktop',
|
|
||||||
title: '桌面',
|
|
||||||
version: {
|
|
||||||
company: 'XZG',
|
|
||||||
major: 1,
|
|
||||||
minor: 0,
|
|
||||||
build: 0,
|
|
||||||
private: 0
|
|
||||||
},
|
|
||||||
singleton: true,
|
|
||||||
isJustProcess: true
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* 桌面应用图标信息
|
|
||||||
*/
|
|
||||||
export interface IDesktopAppIcon {
|
|
||||||
/** 图标name */
|
|
||||||
name: string;
|
|
||||||
/** 图标 */
|
|
||||||
icon: string;
|
|
||||||
/** 图标路径 */
|
|
||||||
path: string;
|
|
||||||
/** 图标在grid布局中的列 */
|
|
||||||
x: number;
|
|
||||||
/** 图标在grid布局中的行 */
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* 桌面网格模板参数
|
|
||||||
*/
|
|
||||||
export interface IGridTemplateParams {
|
|
||||||
/** 单元格预设宽度 */
|
|
||||||
readonly cellExpectWidth: number
|
|
||||||
/** 单元格预设高度 */
|
|
||||||
readonly cellExpectHeight: number
|
|
||||||
/** 单元格实际宽度 */
|
|
||||||
cellRealWidth: number
|
|
||||||
/** 单元格实际高度 */
|
|
||||||
cellRealHeight: number
|
|
||||||
/** 列间距 */
|
|
||||||
gapX: number
|
|
||||||
/** 行间距 */
|
|
||||||
gapY: number
|
|
||||||
/** 总列数 */
|
|
||||||
colCount: number
|
|
||||||
/** 总行数 */
|
|
||||||
rowCount: number
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-config-provider
|
|
||||||
:config-provider-props="configProviderProps"
|
|
||||||
class="w-full h-full pos-relative"
|
|
||||||
>
|
|
||||||
<div class="desktop-root" @contextmenu="onContextMenu">
|
|
||||||
<div class="desktop-bg">
|
|
||||||
<div class="desktop-icons-container" :style="gridStyle">
|
|
||||||
<AppIcon
|
|
||||||
v-for="(appIcon, i) in appIconsRef"
|
|
||||||
:key="i"
|
|
||||||
:iconInfo="appIcon"
|
|
||||||
:gridTemplate="gridTemplate"
|
|
||||||
@dblclick="runApp(appIcon)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="task-bar">
|
|
||||||
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">
|
|
||||||
测试
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-config-provider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
|
|
||||||
import { notificationApi } from '@/core/common/naive-ui/discrete-api.ts'
|
|
||||||
import { configProviderProps } from '@/core/common/naive-ui/theme.ts'
|
|
||||||
import { useDesktopInit } from '@/core/desktop/ui/hooks/useDesktopInit.ts'
|
|
||||||
import AppIcon from '@/core/desktop/ui/components/AppIcon.vue'
|
|
||||||
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
|
|
||||||
import { eventManager } from '@/core/events/EventManager.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
|
|
||||||
const props = defineProps<{ process: DesktopProcess }>()
|
|
||||||
|
|
||||||
props.process.data.subscribeKey(['monitorWidth', 'monitorHeight'], ({monitorWidth, monitorHeight}) => {
|
|
||||||
console.log('onDesktopRootDomResize', monitorWidth, monitorHeight)
|
|
||||||
notificationApi.create({
|
|
||||||
title: '桌面通知',
|
|
||||||
content: `桌面尺寸变化${monitorWidth}x${monitorHeight}}`,
|
|
||||||
duration: 2000,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// props.process.data.subscribe((data) => {
|
|
||||||
// console.log('desktopData', data.monitorWidth)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const { appIconsRef, gridStyle, gridTemplate } = useDesktopInit('.desktop-icons-container')
|
|
||||||
|
|
||||||
// eventManager.addEventListener('onDesktopRootDomResize', (width, height) => {
|
|
||||||
// console.log(width, height)
|
|
||||||
// notificationApi.create({
|
|
||||||
// title: '桌面通知',
|
|
||||||
// content: `桌面尺寸变化${width}x${height}}`,
|
|
||||||
// duration: 2000,
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
const onContextMenu = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
const runApp = (appIcon: IDesktopAppIcon) => {
|
|
||||||
processManager.runProcess(appIcon.name)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
$taskBarHeight: 40px;
|
|
||||||
.desktop-root {
|
|
||||||
@apply w-full h-full flex flex-col;
|
|
||||||
|
|
||||||
.desktop-bg {
|
|
||||||
@apply w-full h-full flex-1 p-2 pos-relative;
|
|
||||||
background-image: url('imgs/desktop-bg-2.jpeg');
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
height: calc(100% - #{$taskBarHeight});
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-icons-container {
|
|
||||||
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-bar {
|
|
||||||
@apply w-full bg-gray-200 flex justify-center items-center;
|
|
||||||
height: $taskBarHeight;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { css, html, LitElement, unsafeCSS } from 'lit'
|
|
||||||
import { customElement } from 'lit/decorators.js'
|
|
||||||
import desktopStyle from './css/desktop.scss?inline'
|
|
||||||
|
|
||||||
@customElement('desktop-element')
|
|
||||||
export class DesktopElement extends LitElement {
|
|
||||||
static override styles = css`
|
|
||||||
${unsafeCSS(desktopStyle)}
|
|
||||||
`
|
|
||||||
|
|
||||||
private onContextMenu = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
console.log('contextmenu')
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
return html`
|
|
||||||
<div class="desktop-root" @contextmenu=${this.onContextMenu}>
|
|
||||||
<div class="desktop-container">
|
|
||||||
<div class="desktop-icons-container"
|
|
||||||
:style="gridStyle">
|
|
||||||
<AppIcon v-for="(appIcon, i) in appIconsRef" :key="i"
|
|
||||||
:iconInfo="appIcon" :gridTemplate="gridTemplate"
|
|
||||||
@dblclick="runApp(appIcon)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="task-bar">
|
|
||||||
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">测试</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="icon-container"
|
|
||||||
:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1};grid-row: ${iconInfo.y}/${iconInfo.y + 1};`"
|
|
||||||
draggable="true"
|
|
||||||
@dragstart="onDragStart"
|
|
||||||
@dragend="onDragEnd"
|
|
||||||
>
|
|
||||||
{{ iconInfo.name }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
|
|
||||||
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
|
|
||||||
import { eventManager } from '@/core/events/EventManager.ts'
|
|
||||||
|
|
||||||
const { iconInfo, gridTemplate } = defineProps<{ iconInfo: IDesktopAppIcon, gridTemplate: IGridTemplateParams }>()
|
|
||||||
|
|
||||||
const onDragStart = (e: DragEvent) => {}
|
|
||||||
|
|
||||||
const onDragEnd = (e: DragEvent) => {
|
|
||||||
const el = e.target as HTMLElement | null
|
|
||||||
if (!el) return
|
|
||||||
// 鼠标所在位置已存在图标元素
|
|
||||||
const pointTarget = document.elementFromPoint(e.clientX, e.clientY)
|
|
||||||
if (!pointTarget) return
|
|
||||||
if (pointTarget.classList.contains('icon-container')) return
|
|
||||||
if (!pointTarget.classList.contains('desktop-icons-container')) return
|
|
||||||
|
|
||||||
// 获取容器边界
|
|
||||||
const rect = el.parentElement!.getBoundingClientRect()
|
|
||||||
|
|
||||||
// 鼠标相对容器左上角坐标
|
|
||||||
const mouseX = e.clientX - rect.left
|
|
||||||
const mouseY = e.clientY - rect.top
|
|
||||||
|
|
||||||
// 计算鼠标所在单元格坐标(向上取整,从1开始)
|
|
||||||
const gridX = Math.ceil(mouseX / gridTemplate.cellRealWidth)
|
|
||||||
const gridY = Math.ceil(mouseY / gridTemplate.cellRealHeight)
|
|
||||||
|
|
||||||
iconInfo.x = gridX
|
|
||||||
iconInfo.y = gridY
|
|
||||||
|
|
||||||
eventManager.notifyEvent('onDesktopAppIconPos', iconInfo)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.icon-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
@apply flex flex-col items-center justify-center bg-gray-200;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { css, html, LitElement } from 'lit'
|
|
||||||
|
|
||||||
export class DesktopAppIconElement extends LitElement {
|
|
||||||
static override styles = css`
|
|
||||||
:host {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
@apply flex flex-col items-center justify-center bg-gray-200;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
return html`<div class="desktop-app-icon">
|
|
||||||
<slot></slot>
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box; /* 使用更直观的盒模型 */
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$taskBarHeight: 40px;
|
|
||||||
|
|
||||||
.desktop-root {
|
|
||||||
@apply w-full h-full flex flex-col;
|
|
||||||
|
|
||||||
.desktop-container {
|
|
||||||
@apply w-full h-full flex-1 p-2 pos-relative;
|
|
||||||
background-image: url("../imgs/desktop-bg-2.jpeg");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
height: calc(100% - #{$taskBarHeight});
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-icons-container {
|
|
||||||
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-bar {
|
|
||||||
@apply w-full bg-gray-200 flex justify-center items-center;
|
|
||||||
height: $taskBarHeight;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
nextTick,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
reactive,
|
|
||||||
ref,
|
|
||||||
toRaw,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
useTemplateRef,
|
|
||||||
watch,
|
|
||||||
watchEffect,
|
|
||||||
} from 'vue'
|
|
||||||
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
|
|
||||||
import { eventManager } from '@/core/events/EventManager.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
|
|
||||||
export function useDesktopInit(containerStr: string) {
|
|
||||||
let container:HTMLElement
|
|
||||||
// 初始值
|
|
||||||
const gridTemplate = reactive<IGridTemplateParams>({
|
|
||||||
cellExpectWidth: 90,
|
|
||||||
cellExpectHeight: 110,
|
|
||||||
cellRealWidth: 90,
|
|
||||||
cellRealHeight: 110,
|
|
||||||
gapX: 4,
|
|
||||||
gapY: 4,
|
|
||||||
colCount: 1,
|
|
||||||
rowCount: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const ro = new ResizeObserver(entries => {
|
|
||||||
const entry= entries[0]
|
|
||||||
const containerRect = entry.contentRect
|
|
||||||
gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX));
|
|
||||||
gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY));
|
|
||||||
|
|
||||||
const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1))
|
|
||||||
const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1))
|
|
||||||
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
|
|
||||||
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
|
|
||||||
})
|
|
||||||
|
|
||||||
const gridStyle = computed(() => ({
|
|
||||||
gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`,
|
|
||||||
gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`,
|
|
||||||
gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`
|
|
||||||
}))
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
container = document.querySelector(containerStr)!
|
|
||||||
ro.observe(container)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
ro.unobserve(container)
|
|
||||||
ro.disconnect()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 有桌面图标的app
|
|
||||||
const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess)
|
|
||||||
const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]')
|
|
||||||
const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => {
|
|
||||||
const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name)
|
|
||||||
|
|
||||||
// 左上角坐标原点,从上到下从左到右 索引从1开始
|
|
||||||
const x = index % gridTemplate.rowCount + 1
|
|
||||||
const y = Math.floor(index / gridTemplate.rowCount) + 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: processInfo.name,
|
|
||||||
icon: processInfo.icon,
|
|
||||||
path: processInfo.startName,
|
|
||||||
x: oldAppIcon ? oldAppIcon.x : x,
|
|
||||||
y: oldAppIcon ? oldAppIcon.y : y
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const appIconsRef = ref(appIcons)
|
|
||||||
const exceedApp = ref<IDesktopAppIcon[]>([])
|
|
||||||
|
|
||||||
watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => {
|
|
||||||
// if (oCols == 1 && oRows == 1) return
|
|
||||||
if (oCols === nCols && oRows === nRows) return
|
|
||||||
const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows)
|
|
||||||
appIconsRef.value = appIcons
|
|
||||||
exceedApp.value = hideAppIcons
|
|
||||||
})
|
|
||||||
|
|
||||||
eventManager.addEventListener('onDesktopAppIconPos', (iconInfo) => {
|
|
||||||
localStorage.setItem('desktopAppIconInfo', JSON.stringify(toValue(appIconsRef.value)))
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridTemplate,
|
|
||||||
appIconsRef,
|
|
||||||
gridStyle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新安排图标位置
|
|
||||||
* @param appIconInfos 图标信息
|
|
||||||
* @param maxCol 列数
|
|
||||||
* @param maxRow 行数
|
|
||||||
*/
|
|
||||||
function rearrangeIcons(
|
|
||||||
appIconInfos: IDesktopAppIcon[],
|
|
||||||
maxCol: number,
|
|
||||||
maxRow: number
|
|
||||||
): IRearrangeInfo {
|
|
||||||
const occupied = new Set<string>();
|
|
||||||
|
|
||||||
function key(x: number, y: number) {
|
|
||||||
return `${x},${y}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appIcons: IDesktopAppIcon[] = []
|
|
||||||
const hideAppIcons: IDesktopAppIcon[] = []
|
|
||||||
const temp: IDesktopAppIcon[] = []
|
|
||||||
|
|
||||||
for (const appIcon of appIconInfos) {
|
|
||||||
const { x, y } = appIcon;
|
|
||||||
|
|
||||||
if (x <= maxCol && y <= maxRow) {
|
|
||||||
if (!occupied.has(key(x, y))) {
|
|
||||||
occupied.add(key(x, y))
|
|
||||||
appIcons.push({ ...appIcon, x, y })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
temp.push(appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = maxCol * maxRow
|
|
||||||
for (const appIcon of temp) {
|
|
||||||
if (appIcons.length < max) {
|
|
||||||
// 最后格子也被占 → 从 (1,1) 开始找空位
|
|
||||||
let placed = false;
|
|
||||||
for (let c = 1; c <= maxCol; c++) {
|
|
||||||
for (let r = 1; r <= maxRow; r++) {
|
|
||||||
if (!occupied.has(key(c, r))) {
|
|
||||||
occupied.add(key(c, r));
|
|
||||||
appIcons.push({ ...appIcon, x: c, y: r });
|
|
||||||
placed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (placed) break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 放不下了
|
|
||||||
hideAppIcons.push(appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
appIcons,
|
|
||||||
hideAppIcons
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRearrangeInfo {
|
|
||||||
/** 正常的桌面图标信息 */
|
|
||||||
appIcons: IDesktopAppIcon[];
|
|
||||||
/** 隐藏的桌面图标信息(超出屏幕显示的) */
|
|
||||||
hideAppIcons: IDesktopAppIcon[];
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -1,64 +0,0 @@
|
|||||||
import { useDraggable } from '@vueuse/core'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
export function useIconDrag(el: HTMLElement, container: HTMLElement) {
|
|
||||||
let offsetX = 0
|
|
||||||
let offsetY = 0
|
|
||||||
let containerRect = container.getBoundingClientRect()
|
|
||||||
|
|
||||||
el.addEventListener('mousedown', (e) => {
|
|
||||||
el.classList.add('dragging')
|
|
||||||
|
|
||||||
let rect = el.getBoundingClientRect()
|
|
||||||
console.log(rect)
|
|
||||||
offsetX = e.clientX - rect.left
|
|
||||||
offsetY = e.clientY - rect.top
|
|
||||||
|
|
||||||
// 临时脱离 grid,用绝对定位移动
|
|
||||||
el.style.position = "absolute";
|
|
||||||
el.style.left = rect.left - containerRect.left + "px";
|
|
||||||
el.style.top = rect.top - containerRect.top + "px";
|
|
||||||
el.style.gridRow = "auto";
|
|
||||||
el.style.gridColumn = "auto";
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", onMouseMove);
|
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
|
||||||
})
|
|
||||||
|
|
||||||
function onMouseMove(e: MouseEvent) {
|
|
||||||
if (!el) return;
|
|
||||||
el.style.left = e.clientX - containerRect.left - offsetX + "px";
|
|
||||||
el.style.top = e.clientY - containerRect.top - offsetY + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseUp(e: MouseEvent) {
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const cellWidth = 90 + 16; // 图标宽度 + gap
|
|
||||||
const cellHeight = 110 + 16;
|
|
||||||
|
|
||||||
// 计算所在行列
|
|
||||||
let col = Math.round((e.clientX - containerRect.left) / cellWidth) + 1;
|
|
||||||
let row = Math.round((e.clientY - containerRect.top) / cellHeight) + 1;
|
|
||||||
|
|
||||||
// 限制在 grid 内
|
|
||||||
const maxCols = Math.floor(containerRect.width / cellWidth);
|
|
||||||
const maxRows = Math.floor(containerRect.height / cellHeight);
|
|
||||||
col = Math.max(1, Math.min(maxCols, col));
|
|
||||||
row = Math.max(1, Math.min(maxRows, row));
|
|
||||||
|
|
||||||
console.log(col, row)
|
|
||||||
|
|
||||||
// 放回 grid
|
|
||||||
el.style.position = "relative";
|
|
||||||
el.style.left = "";
|
|
||||||
el.style.top = "";
|
|
||||||
el.style.gridRow = `${row}`;
|
|
||||||
el.style.gridColumn = `${col}`;
|
|
||||||
|
|
||||||
el.classList.remove("dragging");
|
|
||||||
|
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
|
||||||
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
|
|
||||||
|
|
||||||
export const eventManager = new EventBuilderImpl<IAllEvent>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统进程的事件
|
|
||||||
* @description
|
|
||||||
* <p>onAuthChange - 认证状态改变</p>
|
|
||||||
* <p>onThemeChange - 主题改变</p>
|
|
||||||
*/
|
|
||||||
export interface IBasicSystemEvent extends IEventMap {
|
|
||||||
/** 认证状态改变 */
|
|
||||||
onAuthChange: () => {},
|
|
||||||
/** 主题改变 */
|
|
||||||
onThemeChange: (theme: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 桌面进程的事件
|
|
||||||
* @description
|
|
||||||
* <p>onDesktopRootDomResize - 桌面根dom尺寸改变</p>
|
|
||||||
* <p>onDesktopProcessInitialize - 桌面进程初始化完成</p>
|
|
||||||
*/
|
|
||||||
export interface IDesktopEvent extends IEventMap {
|
|
||||||
/** 桌面根dom尺寸改变 */
|
|
||||||
onDesktopRootDomResize: (width: number, height: number) => void
|
|
||||||
/** 桌面进程初始化完成 */
|
|
||||||
onDesktopProcessInitialize: () => void
|
|
||||||
/** 桌面应用图标位置改变 */
|
|
||||||
onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 事件定义
|
|
||||||
* @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
|
|
||||||
*/
|
|
||||||
export interface IEventMap {
|
|
||||||
[key: string]: (...args: any[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 事件管理器接口定义
|
|
||||||
*/
|
|
||||||
export interface IEventBuilder<Events extends IEventMap> extends IDestroyable {
|
|
||||||
/**
|
|
||||||
* 添加事件监听
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param handler 事件处理函数
|
|
||||||
* @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
addEventListener<E extends keyof Events, F extends Events[E]>(
|
|
||||||
eventName: E,
|
|
||||||
handler: F,
|
|
||||||
options?: {
|
|
||||||
immediate?: boolean
|
|
||||||
immediateArgs?: Parameters<F>
|
|
||||||
once?: boolean
|
|
||||||
},
|
|
||||||
): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除事件监听
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param handler 事件处理函数
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触发事件
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param args 参数
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>): void
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
|
||||||
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 窗口的事件
|
|
||||||
*/
|
|
||||||
export interface WindowFormEvent extends IEventMap {
|
|
||||||
/**
|
|
||||||
* 窗口最小化
|
|
||||||
* @param id 窗口id
|
|
||||||
*/
|
|
||||||
windowFormMinimize: (id: string) => void;
|
|
||||||
/**
|
|
||||||
* 窗口最大化
|
|
||||||
* @param id 窗口id
|
|
||||||
*/
|
|
||||||
windowFormMaximize: (id: string) => void;
|
|
||||||
/**
|
|
||||||
* 窗口还原
|
|
||||||
* @param id 窗口id
|
|
||||||
*/
|
|
||||||
windowFormRestore: (id: string) => void;
|
|
||||||
/**
|
|
||||||
* 窗口关闭
|
|
||||||
* @param id 窗口id
|
|
||||||
*/
|
|
||||||
windowFormClose: (id: string) => void;
|
|
||||||
/**
|
|
||||||
* 窗口聚焦
|
|
||||||
* @param id 窗口id
|
|
||||||
*/
|
|
||||||
windowFormFocus: (id: string) => void;
|
|
||||||
/**
|
|
||||||
* 窗口数据更新
|
|
||||||
* @param data 窗口数据
|
|
||||||
*/
|
|
||||||
windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void;
|
|
||||||
/**
|
|
||||||
* 窗口创建完成
|
|
||||||
*/
|
|
||||||
windowFormCreated: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IWindowFormDataUpdateParams {
|
|
||||||
/** 窗口id */
|
|
||||||
id: string;
|
|
||||||
/** 窗口状态 */
|
|
||||||
state: TWindowFormState,
|
|
||||||
/** 窗口宽度 */
|
|
||||||
width: number,
|
|
||||||
/** 窗口高度 */
|
|
||||||
height: number,
|
|
||||||
/** 窗口x坐标(左上角) */
|
|
||||||
x: number,
|
|
||||||
/** 窗口y坐标(左上角) */
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 窗口事件管理器 */
|
|
||||||
export const wfem = new EventBuilderImpl<WindowFormEvent>()
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts'
|
|
||||||
|
|
||||||
interface HandlerWrapper<T extends (...args: any[]) => any> {
|
|
||||||
fn: T
|
|
||||||
once: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder<Events> {
|
|
||||||
private _eventHandlers = new Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加事件监听器
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param handler 监听器
|
|
||||||
* @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
|
|
||||||
* @example
|
|
||||||
* eventBus.addEventListener('noArgs', () => {})
|
|
||||||
* eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
|
|
||||||
* eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] })
|
|
||||||
*/
|
|
||||||
addEventListener<E extends keyof Events, F extends Events[E]>(
|
|
||||||
eventName: E,
|
|
||||||
handler: F,
|
|
||||||
options?: {
|
|
||||||
immediate?: boolean
|
|
||||||
immediateArgs?: Parameters<F>
|
|
||||||
once?: boolean
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (!handler) return
|
|
||||||
if (!this._eventHandlers.has(eventName)) {
|
|
||||||
this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = this._eventHandlers.get(eventName)!
|
|
||||||
if (![...set].some((wrapper) => wrapper.fn === handler)) {
|
|
||||||
set.add({ fn: handler, once: options?.once ?? false })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.immediate) {
|
|
||||||
try {
|
|
||||||
handler(...(options.immediateArgs ?? []))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除事件监听器
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param handler 监听器
|
|
||||||
* @example
|
|
||||||
* eventBus.removeEventListener('noArgs', () => {})
|
|
||||||
*/
|
|
||||||
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F) {
|
|
||||||
const set = this._eventHandlers.get(eventName)
|
|
||||||
if (!set) return
|
|
||||||
|
|
||||||
for (const wrapper of set) {
|
|
||||||
if (wrapper.fn === handler) {
|
|
||||||
set.delete(wrapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知事件
|
|
||||||
* @param eventName 事件名称
|
|
||||||
* @param args 参数
|
|
||||||
* @example
|
|
||||||
* eventBus.notifyEvent('noArgs')
|
|
||||||
* eventBus.notifyEvent('greet', 'Alice')
|
|
||||||
* eventBus.notifyEvent('onResize', 1, 2)
|
|
||||||
*/
|
|
||||||
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>) {
|
|
||||||
if (!this._eventHandlers.has(eventName)) return
|
|
||||||
|
|
||||||
const set = this._eventHandlers.get(eventName)!
|
|
||||||
for (const wrapper of set) {
|
|
||||||
try {
|
|
||||||
wrapper.fn(...args)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wrapper.once) {
|
|
||||||
set.delete(wrapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._eventHandlers.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
|
||||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
|
|
||||||
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程接口
|
|
||||||
*/
|
|
||||||
export interface IProcess extends IDestroyable {
|
|
||||||
/** 进程id */
|
|
||||||
get id(): string;
|
|
||||||
/** 进程信息 */
|
|
||||||
get processInfo(): IProcessInfo;
|
|
||||||
/** 进程的窗体列表 */
|
|
||||||
get windowForms(): Map<string, IWindowForm>;
|
|
||||||
get event(): IEventBuilder<IProcessEvent>;
|
|
||||||
/**
|
|
||||||
* 打开窗体
|
|
||||||
* @param startName 窗体启动名
|
|
||||||
*/
|
|
||||||
openWindowForm(startName: string): void;
|
|
||||||
/**
|
|
||||||
* 关闭窗体
|
|
||||||
* @param id 窗体id
|
|
||||||
*/
|
|
||||||
closeWindowForm(id: string): void;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { IVersion } from '@/core/common/types/IVersion.ts'
|
|
||||||
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程的描述信息
|
|
||||||
*/
|
|
||||||
export interface IProcessInfo {
|
|
||||||
/** 进程名称 - 唯一 */
|
|
||||||
get name(): string;
|
|
||||||
/** 进程标题 */
|
|
||||||
get title(): string;
|
|
||||||
/** 进程描述 */
|
|
||||||
get description(): string;
|
|
||||||
/** 进程图标 */
|
|
||||||
get icon(): string;
|
|
||||||
/** 启动窗体名称 */
|
|
||||||
get startName(): string;
|
|
||||||
/** 进程版本 */
|
|
||||||
get version(): IVersion;
|
|
||||||
/** 是否单例进程 */
|
|
||||||
get singleton(): boolean;
|
|
||||||
/** 是否仅进程 */
|
|
||||||
get isJustProcess(): boolean;
|
|
||||||
/** 进程的窗体配置 */
|
|
||||||
get windowFormConfigs(): IWindowFormConfig[];
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
|
|
||||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程管理
|
|
||||||
*/
|
|
||||||
export interface IProcessManager {
|
|
||||||
/** 所有进程信息 */
|
|
||||||
get processInfos(): IProcessInfo[];
|
|
||||||
/**
|
|
||||||
* 注册进程
|
|
||||||
* @param process 进程
|
|
||||||
*/
|
|
||||||
registerProcess(process: IProcess): void;
|
|
||||||
/**
|
|
||||||
* 通过进程id查找进程
|
|
||||||
* @param id 进程id
|
|
||||||
*/
|
|
||||||
findProcessById(id: string): IProcess | undefined;
|
|
||||||
/**
|
|
||||||
* 通过进程名查找进程
|
|
||||||
* @param name 进程名
|
|
||||||
*/
|
|
||||||
findProcessByName<T extends IProcess = IProcess>(name: string): T | undefined;
|
|
||||||
/**
|
|
||||||
* 通过进程id删除进程
|
|
||||||
* @param id 进程id
|
|
||||||
*/
|
|
||||||
removeProcess(id: string): void;
|
|
||||||
/**
|
|
||||||
* 通过进程对象删除进程
|
|
||||||
* @param process 进程对象
|
|
||||||
*/
|
|
||||||
removeProcess(process: IProcess): void;
|
|
||||||
/**
|
|
||||||
* 通过进程名查找进程信息
|
|
||||||
* @param name 进程名
|
|
||||||
*/
|
|
||||||
findProcessInfoByName(name: string): IProcessInfo | undefined;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts'
|
|
||||||
|
|
||||||
export const processManager = new ProcessManagerImpl();
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
|
||||||
import WindowFormImpl from '../../window/impl/WindowFormImpl.ts'
|
|
||||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
|
|
||||||
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
|
|
||||||
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程
|
|
||||||
*/
|
|
||||||
export default class ProcessImpl implements IProcess {
|
|
||||||
private readonly _id: string = uuidV4();
|
|
||||||
private readonly _processInfo: IProcessInfo;
|
|
||||||
// 当前进程的窗体集合
|
|
||||||
private _windowForms: Map<string, IWindowForm> = new Map();
|
|
||||||
private _event: IEventBuilder<IProcessEvent> = new EventBuilderImpl<IProcessEvent>()
|
|
||||||
|
|
||||||
public get id() {
|
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
public get processInfo() {
|
|
||||||
return this._processInfo;
|
|
||||||
}
|
|
||||||
public get windowForms() {
|
|
||||||
return this._windowForms;
|
|
||||||
}
|
|
||||||
public get event() {
|
|
||||||
return this._event;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(info: IProcessInfo) {
|
|
||||||
console.log(`AppProcess: ${info.name}`)
|
|
||||||
this._processInfo = info;
|
|
||||||
|
|
||||||
const startName = info.startName;
|
|
||||||
|
|
||||||
this.initEvent();
|
|
||||||
|
|
||||||
processManager.registerProcess(this);
|
|
||||||
// 通过设置 isJustProcess 为 true,则不会创建窗体
|
|
||||||
if (!info.isJustProcess) {
|
|
||||||
this.openWindowForm(startName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initEvent() {
|
|
||||||
this.event.addEventListener('processWindowFormExit', (id: string) => {
|
|
||||||
this.windowForms.delete(id)
|
|
||||||
if(this.windowForms.size === 0) {
|
|
||||||
processManager.removeProcess(this)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public openWindowForm(startName: string) {
|
|
||||||
const info = this._processInfo.windowFormConfigs.find(item => item.name === startName);
|
|
||||||
if (!info) throw new Error(`未找到窗体:${startName}`);
|
|
||||||
const wf = new WindowFormImpl(this, info);
|
|
||||||
this._windowForms.set(wf.id, wf);
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeWindowForm(id: string) {
|
|
||||||
try {
|
|
||||||
const wf = this._windowForms.get(id);
|
|
||||||
if (!wf) throw new Error(`未找到窗体:${id}`);
|
|
||||||
wf.destroy();
|
|
||||||
this.windowForms.delete(id)
|
|
||||||
if(this.windowForms.size === 0) {
|
|
||||||
this.destroy()
|
|
||||||
processManager.removeProcess(this)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('关闭窗体失败', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy() {
|
|
||||||
this._event.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import type { IVersion } from '../../common/types/IVersion.ts'
|
|
||||||
import type { IAppProcessInfoParams } from '../types/IAppProcessInfoParams.ts'
|
|
||||||
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
|
|
||||||
export class ProcessInfoImpl implements IProcessInfo {
|
|
||||||
/**
|
|
||||||
* 应用进程名称
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用进程标题
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _title: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用进程描述
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _description: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用进程图标
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _icon: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用进程启动入口
|
|
||||||
* 对应windowFrom参数name
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _startName: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用版本信息
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _version: IVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用是否只存在一个进程
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _singleton: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否只是一个进程
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _isJustProcess: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程所有的窗口配置信息
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private readonly _windowFormConfigs: Array<IWindowFormConfig>;
|
|
||||||
|
|
||||||
constructor(info: IAppProcessInfoParams) {
|
|
||||||
this._name = info.name;
|
|
||||||
this._title = info.title || '';
|
|
||||||
this._description = info.description || '';
|
|
||||||
this._icon = <string> info.icon;
|
|
||||||
this._startName = info.startName || '';
|
|
||||||
this._version = info.version || { company: 'XZG', major: 1, minor: 0, build: 0, private: 0 };
|
|
||||||
this._singleton = info.singleton;
|
|
||||||
this._isJustProcess = info.isJustProcess;
|
|
||||||
this._windowFormConfigs = info.windowFormConfigs || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get name() {
|
|
||||||
return this._name;
|
|
||||||
}
|
|
||||||
public get title() {
|
|
||||||
return this._title;
|
|
||||||
}
|
|
||||||
public get description() {
|
|
||||||
return this._description;
|
|
||||||
}
|
|
||||||
public get icon() {
|
|
||||||
return this._icon;
|
|
||||||
}
|
|
||||||
public get startName() {
|
|
||||||
return this._startName;
|
|
||||||
}
|
|
||||||
public get version() {
|
|
||||||
return this._version;
|
|
||||||
}
|
|
||||||
public get singleton() {
|
|
||||||
return this._singleton;
|
|
||||||
}
|
|
||||||
public get isJustProcess() {
|
|
||||||
return this._isJustProcess;
|
|
||||||
}
|
|
||||||
public get windowFormConfigs() {
|
|
||||||
return this._windowFormConfigs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import ProcessImpl from './ProcessImpl.ts'
|
|
||||||
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
|
|
||||||
import { BasicSystemProcessInfo } from '@/core/system/BasicSystemProcessInfo.ts'
|
|
||||||
import { DesktopProcessInfo } from '@/core/desktop/DesktopProcessInfo.ts'
|
|
||||||
import type { IAppProcessInfoParams } from '@/core/process/types/IAppProcessInfoParams.ts'
|
|
||||||
import type { IProcessManager } from '@/core/process/IProcessManager.ts'
|
|
||||||
import type { IProcess } from '@/core/process/IProcess.ts'
|
|
||||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
|
||||||
import { processManager } from '@/core/process/ProcessManager.ts'
|
|
||||||
import { isUndefined } from 'lodash'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程管理
|
|
||||||
*/
|
|
||||||
export default class ProcessManagerImpl implements IProcessManager {
|
|
||||||
private _processPool: Map<string, IProcess> = new Map<string, IProcess>();
|
|
||||||
private _processInfos: IProcessInfo[] = new Array<ProcessInfoImpl>();
|
|
||||||
|
|
||||||
public get processInfos() {
|
|
||||||
return this._processInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
console.log('ProcessManageImpl')
|
|
||||||
this.loadAppProcessInfos();
|
|
||||||
}
|
|
||||||
// TODO 加载所有进程信息
|
|
||||||
private loadAppProcessInfos() {
|
|
||||||
console.log('加载所有进程信息')
|
|
||||||
// 添加内置进程
|
|
||||||
const apps = import.meta.glob<IAppProcessInfoParams>('../../apps/**/*.json', { eager: true })
|
|
||||||
const internalProcessInfos: ProcessInfoImpl[] = Object.values(apps).map(data => new ProcessInfoImpl(data))
|
|
||||||
|
|
||||||
this._processInfos.push(BasicSystemProcessInfo)
|
|
||||||
this._processInfos.push(DesktopProcessInfo)
|
|
||||||
|
|
||||||
this._processInfos.push(...internalProcessInfos)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runProcess<T extends IProcess = IProcess, A extends any[] = any[]>(
|
|
||||||
proc: string | IProcessInfo,
|
|
||||||
constructor?: new (info: IProcessInfo, ...args: A) => T,
|
|
||||||
...args: A
|
|
||||||
): Promise<T> {
|
|
||||||
let info = typeof proc === 'string' ? this.findProcessInfoByName(proc) : proc
|
|
||||||
if (isUndefined(info)) {
|
|
||||||
throw new Error(`未找到进程信息:${proc}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 是单例应用
|
|
||||||
if (info.singleton) {
|
|
||||||
let process = this.findProcessByName(info.name)
|
|
||||||
if (process) {
|
|
||||||
return process as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建进程
|
|
||||||
let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info, ...args)
|
|
||||||
|
|
||||||
return process as T
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加进程
|
|
||||||
public registerProcess(process: ProcessImpl) {
|
|
||||||
this._processPool.set(process.id, process);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过进程id查找进程
|
|
||||||
* @param id 进程id
|
|
||||||
*/
|
|
||||||
public findProcessById(id: string) {
|
|
||||||
return this._processPool.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过进程名称查找进程
|
|
||||||
* @param name 进程名称
|
|
||||||
*/
|
|
||||||
public findProcessByName<T extends IProcess = IProcess>(name: string) {
|
|
||||||
const pools = [...this._processPool.values()];
|
|
||||||
return pools.find(proc => proc.processInfo.name === name) as T | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据进程id删除进程
|
|
||||||
* @param id 进程id
|
|
||||||
*/
|
|
||||||
public removeProcess(id: string): void;
|
|
||||||
/**
|
|
||||||
* 根据进程删除进程
|
|
||||||
* @param process 进程信息
|
|
||||||
*/
|
|
||||||
public removeProcess(process: IProcess): void;
|
|
||||||
public removeProcess(params: string | IProcess) {
|
|
||||||
const id = typeof params === 'string' ? params : params.id;
|
|
||||||
this._processPool.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过进程名称查找进程信息
|
|
||||||
*/
|
|
||||||
public findProcessInfoByName(name: string) {
|
|
||||||
return this._processInfos.find(info => info.name === name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { IVersion } from '../../common/types/IVersion.ts'
|
|
||||||
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用进程入参信息
|
|
||||||
*/
|
|
||||||
export interface IAppProcessInfoParams {
|
|
||||||
/** 应用进程名称 */
|
|
||||||
name: string;
|
|
||||||
/** 应用进程标题 */
|
|
||||||
title?: string;
|
|
||||||
/** 应用进程描述 */
|
|
||||||
description?: string;
|
|
||||||
/** 应用进程图标 */
|
|
||||||
icon?: string;
|
|
||||||
/** 应用进程启动入口 */
|
|
||||||
startName?: string;
|
|
||||||
/** 应用版本信息 */
|
|
||||||
version?: IVersion;
|
|
||||||
/** 应用是否只存在一个进程 */
|
|
||||||
singleton: boolean;
|
|
||||||
/** 是否只是一个进程, 没有UI */
|
|
||||||
isJustProcess: boolean;
|
|
||||||
/** 进程所有的窗口配置信息 */
|
|
||||||
windowFormConfigs?: IWindowFormConfig[];
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user