204 lines
8.5 KiB
Markdown
204 lines
8.5 KiB
Markdown
# 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`,如果不是,则忽略此次拖拽操作,防止图标被错误地投放到其他容器中。 |