806 lines
28 KiB
Markdown
806 lines
28 KiB
Markdown
|
|
# 3D Viewer 工具栏实现文档
|
|||
|
|
|
|||
|
|
> **文档说明**: 本文档为 3D Viewer 工具栏的实现规范,包含 UI 设计规格、交互逻辑和参考代码片段,供开发人员复现和实现。
|
|||
|
|
|
|||
|
|
**版本**: v1.0
|
|||
|
|
**更新日期**: 2026-01-16
|
|||
|
|
**技术栈**: React + TypeScript + Ant Design + Tailwind CSS + Framer Motion
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 目录
|
|||
|
|
|
|||
|
|
1. [整体布局概览](#1-整体布局概览)
|
|||
|
|
2. [顶部中间工具栏](#2-顶部中间工具栏)
|
|||
|
|
3. [底部右侧工具栏](#3-底部右侧工具栏)
|
|||
|
|
4. [Hover 二级菜单](#4-hover-二级菜单)
|
|||
|
|
5. [右侧抽屉面板](#5-右侧抽屉面板)
|
|||
|
|
6. [状态管理](#6-状态管理)
|
|||
|
|
7. [视觉规范速查表](#7-视觉规范速查表)
|
|||
|
|
8. [组件清单](#8-组件清单)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 整体布局概览
|
|||
|
|
|
|||
|
|
### 1.1 ASCII 原型图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 顶栏:项目名/状态 | 标签栏:工作台 | 建筑主体.rvt | ... | 成员/协同/分享 │
|
|||
|
|
├────┬────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│导航│ 二级面板(已载明,默认展开,可折叠) │
|
|||
|
|
│ ├────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ │ 主工作区:Viewer │
|
|||
|
|
│ │ │
|
|||
|
|
│ │ ① 顶部中间工具(面板/列表开关,水平居中浮层) │
|
|||
|
|
│ │ ┌────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ │ 📋 视图列表 📄 属性 🔍 过滤 📌 图钉 ⤢ 全屏 │ │
|
|||
|
|
│ │ └────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ │ [ 3D Viewer 渲染区域(加载中显示文件卡片占位) ] │
|
|||
|
|
│ │ - 右侧抽屉:视图树/属性/过滤/图钉面板(点击顶部按钮打开/收起) │
|
|||
|
|
│ │ │
|
|||
|
|
│ │ ② 底部右侧工具(模型交互,圆形按钮)│
|
|||
|
|
│ │ ┌──────────────┐ │
|
|||
|
|
│ │ │ 🧊 主视图 │ │
|
|||
|
|
│ │ │ 📏 测量 │ │
|
|||
|
|
│ │ │ 🪟 剖切 │ │
|
|||
|
|
│ │ │ 🚶 漫游 │ │
|
|||
|
|
│ │ │ ⚙️ 环境设置 │ │
|
|||
|
|
│ │ └──────────────┘ │
|
|||
|
|
│ └────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
└────┴────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1.2 工具栏分区说明
|
|||
|
|
|
|||
|
|
**① 顶部中间工具栏 (面板/列表开关)**:
|
|||
|
|
- **位置**: 水平居中,顶部浮动
|
|||
|
|
- **功能**: 控制右侧抽屉面板的打开/关闭
|
|||
|
|
- **按钮数量**: 5个(视图列表、属性、过滤、图钉、全屏)
|
|||
|
|
- **交互特点**: 开关型按钮,同时只允许一个面板高亮
|
|||
|
|
|
|||
|
|
**② 底部右侧工具栏 (模型交互工具)**:
|
|||
|
|
- **位置**: 右下角固定
|
|||
|
|
- **功能**: 模型交互工具集
|
|||
|
|
- **按钮数量**: 5个(主视图、测量、剖切、漫游、环境设置)
|
|||
|
|
- **交互特点**: 圆形按钮,Hover 显示二级菜单,支持模式互斥
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 顶部中间工具栏
|
|||
|
|
|
|||
|
|
### 2.1 布局示意
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌────────────────────────────────────────────────┐
|
|||
|
|
│ 📋 视图列表 📄 属性 🔍 过滤 📌 图钉 ⤢ 全屏 │
|
|||
|
|
└────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 容器规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **定位** | 顶部居中浮动 | `absolute top-3 left-1/2 -translate-x-1/2` |
|
|||
|
|
| **层级** | 800 | `z-[800]` |
|
|||
|
|
| **背景** | 白色半透明 | `bg-white/80 dark:bg-slate-800/80` |
|
|||
|
|
| **毛玻璃** | 10px 模糊 | `backdrop-blur-xl` |
|
|||
|
|
| **圆角** | 6px | `rounded-md` |
|
|||
|
|
| **阴影** | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.04)]` |
|
|||
|
|
| **边框** | 半透明边框 | `border border-slate-200/50 dark:border-slate-700/50` |
|
|||
|
|
| **内边距** | 水平 6px, 垂直 6px | `px-1.5 py-1.5` |
|
|||
|
|
| **布局** | 水平弹性布局 | `flex items-center gap-1.5` |
|
|||
|
|
|
|||
|
|
### 2.3 按钮规范
|
|||
|
|
|
|||
|
|
#### 通用样式
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **尺寸** | 不固定(自适应内容) | `px-2 py-2 lg:px-3 lg:py-2` |
|
|||
|
|
| **圆角** | 6px | `rounded-md` |
|
|||
|
|
| **图标大小** | 18px | `size={18}` |
|
|||
|
|
| **字体** | 12px 中等字重 | `text-xs font-medium` |
|
|||
|
|
| **过渡** | 200ms 平滑过渡 | `transition-all duration-200` |
|
|||
|
|
|
|||
|
|
#### 按钮状态
|
|||
|
|
|
|||
|
|
**默认状态**:
|
|||
|
|
```css
|
|||
|
|
text-slate-600 dark:text-slate-400
|
|||
|
|
hover:bg-slate-100/60 dark:hover:bg-slate-700/60
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**激活态** (对应面板已打开):
|
|||
|
|
```css
|
|||
|
|
bg-blue-500 dark:bg-blue-600
|
|||
|
|
text-white
|
|||
|
|
shadow-md
|
|||
|
|
border border-blue-600 dark:border-blue-500
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.4 按钮列表
|
|||
|
|
|
|||
|
|
| 序号 | 按钮名称 | 图标 | 功能说明 | 面板类型 |
|
|||
|
|
|------|---------|------|---------|---------|
|
|||
|
|
| 1 | 视图列表 | `List` (lucide-react) | 打开视图树面板,显示 3D/2D 视角列表 | `views` |
|
|||
|
|
| 2 | 属性 | `FileText` | 打开属性面板,显示选中构件属性 | `properties` |
|
|||
|
|
| 3 | 过滤 | `Filter` | 打开过滤面板,按楼层/专业/类别筛选 | `filter` |
|
|||
|
|
| 4 | 图钉 | `Pin` | 打开图钉列表面板,支持状态筛选和搜索 | `pins` |
|
|||
|
|
| 5 | 全屏 | `Maximize` / `Minimize` | 直接切换全屏模式(不走抽屉) | - |
|
|||
|
|
|
|||
|
|
### 2.5 交互规则
|
|||
|
|
|
|||
|
|
1. **点击同一按钮**: toggle 打开/关闭抽屉
|
|||
|
|
2. **点击不同按钮**: 切换到对应面板,原面板自动关闭
|
|||
|
|
3. **抽屉关闭时**: 所有按钮(除全屏外)取消高亮
|
|||
|
|
4. **同时只允许一个面板高亮**
|
|||
|
|
|
|||
|
|
### 2.6 参考代码片段
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 文件: TopCenterToolbar.tsx
|
|||
|
|
export function TopCenterToolbar() {
|
|||
|
|
const { state, togglePanel } = useViewerState();
|
|||
|
|
|
|||
|
|
const buttons = [
|
|||
|
|
{
|
|||
|
|
key: 'views' as ViewerPanel,
|
|||
|
|
label: '视图列表',
|
|||
|
|
icon: List,
|
|||
|
|
onClick: () => togglePanel('views'),
|
|||
|
|
},
|
|||
|
|
// ... 其他按钮配置
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-[800]">
|
|||
|
|
<div className="flex items-center gap-1.5 bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-md shadow-[0_4px_24px_rgba(0,0,0,0.04)] border border-slate-200/50 px-1.5 py-1.5">
|
|||
|
|
{buttons.map((button) => {
|
|||
|
|
const Icon = button.icon;
|
|||
|
|
const isActive = state.activePanel === button.key && state.drawerOpen;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={button.key}
|
|||
|
|
onClick={button.onClick}
|
|||
|
|
className={clsx(
|
|||
|
|
'flex items-center gap-1.5 rounded-md transition-all duration-200',
|
|||
|
|
'text-xs font-medium px-2 py-2 lg:px-3 lg:py-2',
|
|||
|
|
isActive
|
|||
|
|
? 'bg-blue-500 text-white shadow-md border border-blue-600'
|
|||
|
|
: 'text-slate-600 hover:bg-slate-100/60'
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<Icon size={18} strokeWidth={1.5} />
|
|||
|
|
<span className="hidden lg:inline">{button.label}</span>
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 底部右侧工具栏
|
|||
|
|
|
|||
|
|
### 3.1 布局示意
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Bottom Right (横向布局)
|
|||
|
|
┌───────────────────────────────┐
|
|||
|
|
│ 🏠 📏 ✂️ 🚶 ⚙️ │
|
|||
|
|
└───────────────────────────────┘
|
|||
|
|
↑ ↑ ↑ ↑ ↑
|
|||
|
|
主 测 剖 漫 环
|
|||
|
|
视 量 切 游 境
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 容器规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **定位** | 右下角固定 | `absolute bottom-4 right-4` |
|
|||
|
|
| **层级** | 800 | `z-[800]` |
|
|||
|
|
| **布局** | 水平弹性布局 | `flex items-center gap-2.5` |
|
|||
|
|
| **背景** | 白色半透明 | `bg-white/80 dark:bg-slate-800/80` |
|
|||
|
|
| **毛玻璃** | 10px 模糊 | `backdrop-blur-xl` |
|
|||
|
|
| **圆角** | 完全圆角 | `rounded-full` |
|
|||
|
|
| **阴影** | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.06)]` |
|
|||
|
|
| **边框** | 半透明边框 | `border border-slate-200/50` |
|
|||
|
|
| **内边距** | 8px | `px-2 py-2` |
|
|||
|
|
|
|||
|
|
### 3.3 按钮规范
|
|||
|
|
|
|||
|
|
#### 通用样式
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **尺寸** | 48px × 48px | `w-12 h-12` |
|
|||
|
|
| **形状** | 圆形 | `rounded-full` |
|
|||
|
|
| **图标大小** | 18px | `size={18}` |
|
|||
|
|
| **布局** | 居中 | `flex items-center justify-center` |
|
|||
|
|
| **过渡** | 200ms 平滑 | `transition-all duration-200 ease-out` |
|
|||
|
|
|
|||
|
|
#### 按钮状态
|
|||
|
|
|
|||
|
|
**默认状态**:
|
|||
|
|
```css
|
|||
|
|
bg-white/90 dark:bg-slate-700/50
|
|||
|
|
border border-slate-300/60 dark:border-slate-600/40
|
|||
|
|
text-slate-700 dark:text-slate-200
|
|||
|
|
shadow-[0_2px_8px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.08)]
|
|||
|
|
hover:bg-white
|
|||
|
|
hover:shadow-[0_4px_12px_rgba(0,0,0,0.12),0_6px_20px_rgba(0,0,0,0.1)]
|
|||
|
|
hover:scale-[1.05]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**激活态** (模式已启用):
|
|||
|
|
```css
|
|||
|
|
bg-blue-600
|
|||
|
|
border-blue-700
|
|||
|
|
text-white
|
|||
|
|
shadow-[0_2px_8px_rgba(59,130,246,0.3),0_4px_16px_rgba(59,130,246,0.2)]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 按钮列表(从左到右)
|
|||
|
|
|
|||
|
|
| 序号 | 按钮名称 | 图标 | 功能说明 | 二级菜单 | 模式类型 |
|
|||
|
|
|------|---------|------|---------|---------|---------|
|
|||
|
|
| 1 | 主视图 | `Home` | 快捷视角切换 | ✅ 充满屏幕/缩放/爆炸图 | - |
|
|||
|
|
| 2 | 测量 | `Ruler` | 测量工具集 | ✅ 线性捕捉/垂直净高/高程测量/清除 | `measure` |
|
|||
|
|
| 3 | 剖切 | `Scissors` | 剖切平面工具 | ✅ 任意剖切/单面/多面 | `section` |
|
|||
|
|
| 4 | 漫游 | `PersonStanding` | 第一人称漫游 | ❌ 直接切换模式 | `walk` |
|
|||
|
|
| 5 | 环境设置 | `Settings` | 渲染和环境配置 | ❌ 弹出 Modal | - |
|
|||
|
|
|
|||
|
|
### 3.5 模式互斥规则
|
|||
|
|
|
|||
|
|
**互斥的模式**:
|
|||
|
|
- 测量模式 (`measure`)
|
|||
|
|
- 剖切模式 (`section`)
|
|||
|
|
- 创建图钉模式 (`pin`)
|
|||
|
|
- 漫游模式 (`walk`)
|
|||
|
|
|
|||
|
|
**互斥策略**:
|
|||
|
|
启用某模式时,自动退出其他模式,并清理对应临时 UI。
|
|||
|
|
|
|||
|
|
**示例**: 启用测量模式时,如果剖切模式正在运行,则自动退出剖切并清除剖切平面。
|
|||
|
|
|
|||
|
|
### 3.6 参考代码片段
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 文件: BottomRightToolbar.tsx
|
|||
|
|
export function BottomRightToolbar({ onOpenSettings }: BottomRightToolbarProps) {
|
|||
|
|
const { state, setMode, exitMode } = useViewerState();
|
|||
|
|
|
|||
|
|
const toolButtons = [
|
|||
|
|
{
|
|||
|
|
key: 'mainView',
|
|||
|
|
label: '主视图',
|
|||
|
|
icon: Home,
|
|||
|
|
menuItems: mainViewMenuItems, // Hover 二级菜单
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'measure',
|
|||
|
|
label: '测量',
|
|||
|
|
icon: Ruler,
|
|||
|
|
menuItems: measureMenuItems,
|
|||
|
|
mode: 'measure' as const,
|
|||
|
|
},
|
|||
|
|
// ... 其他按钮
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="absolute bottom-4 right-4 z-[800]">
|
|||
|
|
<div className="flex items-center gap-2.5 bg-white/80 backdrop-blur-xl rounded-full shadow-[0_4px_24px_rgba(0,0,0,0.06)] border border-slate-200/50 px-2 py-2">
|
|||
|
|
{toolButtons.map((button) => {
|
|||
|
|
const Icon = button.icon;
|
|||
|
|
const isActive = button.mode && state.activeMode === button.mode;
|
|||
|
|
|
|||
|
|
// 带 Hover 菜单的按钮
|
|||
|
|
return (
|
|||
|
|
<HoverMenu
|
|||
|
|
key={button.key}
|
|||
|
|
items={button.menuItems!}
|
|||
|
|
placement="top"
|
|||
|
|
trigger={
|
|||
|
|
<button
|
|||
|
|
className={clsx(
|
|||
|
|
'w-12 h-12 rounded-full flex items-center justify-center',
|
|||
|
|
'border transition-all duration-200',
|
|||
|
|
isActive
|
|||
|
|
? 'bg-blue-600 border-blue-700 text-white'
|
|||
|
|
: 'bg-white/90 border-slate-300/60 text-slate-700 hover:scale-[1.05]'
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<Icon size={18} strokeWidth={2} />
|
|||
|
|
</button>
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Hover 二级菜单
|
|||
|
|
|
|||
|
|
### 4.1 布局示意
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌───────────────────┐
|
|||
|
|
│ 📏 线性捕捉 1 │ ← 菜单项 (hover 高亮)
|
|||
|
|
│ 📐 垂直净高 2 │
|
|||
|
|
│ 📈 高程测量 3 │
|
|||
|
|
│ 🗑️ 清除测量 4 │
|
|||
|
|
└───────────────────┘
|
|||
|
|
↑
|
|||
|
|
┌──────────┐
|
|||
|
|
│ 📏 │ ← 触发按钮
|
|||
|
|
└──────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 容器规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **背景** | 白色半透明 | `bg-white/95 dark:bg-[rgba(30,41,59,0.98)]` |
|
|||
|
|
| **毛玻璃** | 10px 模糊 | `backdrop-blur-[10px]` |
|
|||
|
|
| **圆角** | 12px | `rounded-xl` |
|
|||
|
|
| **阴影** | 较强阴影 | `shadow-[0_8px_24px_rgba(0,0,0,0.12)]` |
|
|||
|
|
| **边框** | 半透明边框 | `border border-slate-200/60` |
|
|||
|
|
| **内边距** | 6px | `px-1.5 py-1.5` |
|
|||
|
|
| **布局** | 垂直列表 | `flex flex-col gap-0` |
|
|||
|
|
|
|||
|
|
### 4.3 菜单项规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **布局** | 水平弹性布局 | `flex items-center gap-2.5` |
|
|||
|
|
| **内边距** | 水平 12px, 垂直 8px | `px-3 py-2` |
|
|||
|
|
| **圆角** | 8px | `rounded-lg` |
|
|||
|
|
| **字体** | 13px 普通字重 | `text-[13px] font-normal` |
|
|||
|
|
| **文字颜色** | 深灰色 | `text-slate-700 dark:text-[rgba(255,255,255,0.85)]` |
|
|||
|
|
| **Hover 背景** | 浅灰色 | `hover:bg-slate-100 dark:hover:bg-[rgba(51,65,85,0.8)]` |
|
|||
|
|
| **过渡** | 100ms 平滑 | `transition-all duration-100 ease-out` |
|
|||
|
|
|
|||
|
|
### 4.4 触发时机
|
|||
|
|
|
|||
|
|
| 事件 | 延迟时间 | 说明 |
|
|||
|
|
|------|---------|------|
|
|||
|
|
| **Hover 进入** | 100ms | 鼠标进入按钮后 100ms 显示菜单 |
|
|||
|
|
| **Hover 离开** | 250ms | 鼠标离开「按钮+菜单」区域后 250ms 隐藏菜单 |
|
|||
|
|
|
|||
|
|
### 4.5 键盘快捷键
|
|||
|
|
|
|||
|
|
- **1-9 键**: 菜单展开时,按数字键触发对应菜单项
|
|||
|
|
- **Esc 键**: 关闭菜单并退出当前模式
|
|||
|
|
|
|||
|
|
### 4.6 参考代码片段
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 文件: HoverMenu.tsx
|
|||
|
|
export function HoverMenu({
|
|||
|
|
trigger,
|
|||
|
|
items,
|
|||
|
|
placement = 'top',
|
|||
|
|
openDelay = 100,
|
|||
|
|
closeDelay = 250,
|
|||
|
|
}: HoverMenuProps) {
|
|||
|
|
const [isOpen, setIsOpen] = useState(false);
|
|||
|
|
|
|||
|
|
const handleMouseEnter = () => {
|
|||
|
|
openTimerRef.current = setTimeout(() => {
|
|||
|
|
setIsOpen(true);
|
|||
|
|
}, openDelay);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleMouseLeave = () => {
|
|||
|
|
closeTimerRef.current = setTimeout(() => {
|
|||
|
|
setIsOpen(false);
|
|||
|
|
}, closeDelay);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
|||
|
|
{trigger}
|
|||
|
|
|
|||
|
|
<AnimatePresence>
|
|||
|
|
{isOpen && (
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 10 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
exit={{ opacity: 0, y: 10 }}
|
|||
|
|
transition={{ duration: 0.15 }}
|
|||
|
|
className="absolute bottom-full mb-3"
|
|||
|
|
>
|
|||
|
|
<div className="bg-white/95 backdrop-blur-[10px] rounded-xl shadow-[0_8px_24px_rgba(0,0,0,0.12)] border border-slate-200/60 px-1.5 py-1.5">
|
|||
|
|
{items.map((item) => (
|
|||
|
|
<button
|
|||
|
|
key={item.key}
|
|||
|
|
onClick={() => item.onClick()}
|
|||
|
|
className="flex items-center gap-2.5 px-3 py-2 rounded-lg hover:bg-slate-100 transition-all"
|
|||
|
|
>
|
|||
|
|
{item.icon}
|
|||
|
|
<span>{item.label}</span>
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
)}
|
|||
|
|
</AnimatePresence>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 右侧抽屉面板
|
|||
|
|
|
|||
|
|
### 5.1 布局示意
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌────────────────────────┐
|
|||
|
|
│ 视图列表 [×] │ ← 标题栏 (48px)
|
|||
|
|
├────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ [面板内容区域] │ ← 滚动区域
|
|||
|
|
│ │
|
|||
|
|
│ │
|
|||
|
|
└────────────────────────┘
|
|||
|
|
← 320px 宽度(可拖拽 280-400px)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 抽屉通用规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **定位** | 右侧固定 | `fixed top-0 bottom-0 right-0` (或 left-0) |
|
|||
|
|
| **宽度** | 320px(可拖拽调整) | `w-[320px]` |
|
|||
|
|
| **背景** | 纯白色 | `bg-white dark:bg-slate-900` |
|
|||
|
|
| **阴影** | 向左投影 | `shadow-2xl` |
|
|||
|
|
| **边框** | 左侧边框 | `border-l border-slate-200` (或右侧) |
|
|||
|
|
| **层级** | 800 | `z-[800]` |
|
|||
|
|
| **动画** | 300ms 弹簧动画 | Framer Motion `spring` |
|
|||
|
|
| **布局** | 垂直弹性布局 | `flex flex-col` |
|
|||
|
|
|
|||
|
|
### 5.3 标题栏规范
|
|||
|
|
|
|||
|
|
| 属性 | 值 | Tailwind CSS |
|
|||
|
|
|------|---|-------------|
|
|||
|
|
| **高度** | 48px | `h-12` |
|
|||
|
|
| **布局** | 水平两端对齐 | `flex items-center justify-between` |
|
|||
|
|
| **内边距** | 水平 16px | `px-4` |
|
|||
|
|
| **背景** | 浅灰色 | `bg-slate-50 dark:bg-slate-800/50` |
|
|||
|
|
| **边框** | 底部边框 | `border-b border-slate-200` |
|
|||
|
|
| **标题字体** | 14px 粗体 | `text-sm font-semibold` |
|
|||
|
|
|
|||
|
|
### 5.4 Tab 管理规则
|
|||
|
|
|
|||
|
|
1. **同时只能打开一个 Tab**
|
|||
|
|
2. **切换 Tab**: 点击顶部工具栏不同按钮,抽屉内容切换,原 Tab 自动替换
|
|||
|
|
3. **关闭抽屉**: 点击标题栏关闭按钮 / 点击已激活的顶部按钮 toggle 关闭
|
|||
|
|
|
|||
|
|
### 5.5 面板列表
|
|||
|
|
|
|||
|
|
| 面板名称 | 激活条件 | 主要功能 |
|
|||
|
|
|---------|---------|---------|
|
|||
|
|
| **视图列表** | `activePanel === 'views'` | 显示 3D/2D 视角列表,支持保存视点、搜索 |
|
|||
|
|
| **属性面板** | `activePanel === 'properties'` | 显示选中构件的属性信息(几何、材质等) |
|
|||
|
|
| **过滤面板** | `activePanel === 'filter'` | 按楼层、专业、类别/系统筛选模型 |
|
|||
|
|
| **图钉列表** | `activePanel === 'pins'` | 显示和管理问题图钉,支持状态筛选和搜索 |
|
|||
|
|
|
|||
|
|
### 5.6 参考代码片段
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 文件: ViewerDrawer.tsx
|
|||
|
|
export function ViewerDrawer() {
|
|||
|
|
const { state, closePanel } = useViewerState();
|
|||
|
|
|
|||
|
|
const renderActivePanel = () => {
|
|||
|
|
switch (state.activePanel) {
|
|||
|
|
case 'views':
|
|||
|
|
return <ViewListPanel />;
|
|||
|
|
case 'properties':
|
|||
|
|
return <PropertiesPanel />;
|
|||
|
|
case 'filter':
|
|||
|
|
return <FilterPanel />;
|
|||
|
|
case 'pins':
|
|||
|
|
return <PinsPanel />;
|
|||
|
|
default:
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const isLeftSide = state.activePanel === 'views';
|
|||
|
|
const isRightSide = state.activePanel === 'pins';
|
|||
|
|
const showDrawer = state.drawerOpen && (isLeftSide || isRightSide);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AnimatePresence>
|
|||
|
|
{showDrawer && (
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ x: isLeftSide ? '-100%' : '100%' }}
|
|||
|
|
animate={{ x: 0 }}
|
|||
|
|
exit={{ x: isLeftSide ? '-100%' : '100%' }}
|
|||
|
|
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
|||
|
|
className={clsx(
|
|||
|
|
'fixed top-0 bottom-0 w-[320px] bg-white shadow-2xl z-[800]',
|
|||
|
|
isLeftSide ? 'left-0 border-r' : 'right-0 border-l'
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
{/* 标题栏 */}
|
|||
|
|
<div className="flex items-center justify-between px-4 py-3 border-b">
|
|||
|
|
<h3 className="text-sm font-semibold">{getPanelTitle()}</h3>
|
|||
|
|
<button onClick={closePanel}>
|
|||
|
|
<X size={18} />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 面板内容 */}
|
|||
|
|
<div className="flex-1 overflow-hidden">
|
|||
|
|
{renderActivePanel()}
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
)}
|
|||
|
|
</AnimatePresence>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 状态管理
|
|||
|
|
|
|||
|
|
### 6.1 状态类型定义
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 文件: use-viewer-state.tsx
|
|||
|
|
|
|||
|
|
/** Viewer 抽屉面板类型 */
|
|||
|
|
export type ViewerPanel = 'views' | 'properties' | 'filter' | 'pins' | null;
|
|||
|
|
|
|||
|
|
/** Viewer 工具模式类型 */
|
|||
|
|
export type ViewerMode = 'measure' | 'section' | 'pin' | 'walk' | null;
|
|||
|
|
|
|||
|
|
/** Viewer 状态接口 */
|
|||
|
|
export interface ViewerState {
|
|||
|
|
// 抽屉状态
|
|||
|
|
drawerOpen: boolean; // 抽屉是否打开
|
|||
|
|
activePanel: ViewerPanel; // 当前激活的面板
|
|||
|
|
drawerWidth: number; // 抽屉宽度(可拖拽调整)
|
|||
|
|
|
|||
|
|
// 工具模式
|
|||
|
|
activeMode: ViewerMode; // 当前激活的工具模式
|
|||
|
|
|
|||
|
|
// 全屏状态
|
|||
|
|
isFullscreen: boolean; // 是否全屏
|
|||
|
|
|
|||
|
|
// 选中构件
|
|||
|
|
selectedElementId: string | null; // 选中构件的 ID
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 核心方法
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
interface ViewerContextValue {
|
|||
|
|
state: ViewerState;
|
|||
|
|
|
|||
|
|
// 面板控制方法
|
|||
|
|
openPanel: (panel: ViewerPanel) => void; // 打开指定面板
|
|||
|
|
closePanel: () => void; // 关闭面板
|
|||
|
|
togglePanel: (panel: ViewerPanel) => void; // 切换面板(同一个则关闭)
|
|||
|
|
|
|||
|
|
// 模式控制方法
|
|||
|
|
setMode: (mode: ViewerMode) => void; // 设置工具模式(互斥)
|
|||
|
|
exitMode: () => void; // 退出当前模式
|
|||
|
|
|
|||
|
|
// 全屏控制
|
|||
|
|
toggleFullscreen: () => void; // 切换全屏
|
|||
|
|
|
|||
|
|
// 构件选择
|
|||
|
|
setSelectedElement: (id: string | null) => void; // 设置选中构件
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 使用示例
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useViewerState } from '@/hooks/use-viewer-state';
|
|||
|
|
|
|||
|
|
function MyComponent() {
|
|||
|
|
const { state, togglePanel, setMode } = useViewerState();
|
|||
|
|
|
|||
|
|
// 打开/关闭面板
|
|||
|
|
const handleOpenProperties = () => {
|
|||
|
|
togglePanel('properties');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 启用测量模式
|
|||
|
|
const handleStartMeasure = () => {
|
|||
|
|
setMode('measure');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 检查当前状态
|
|||
|
|
const isPropertiesPanelOpen = state.activePanel === 'properties' && state.drawerOpen;
|
|||
|
|
const isMeasureMode = state.activeMode === 'measure';
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
{/* 组件内容 */}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 模式互斥逻辑
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 设置工具模式时,自动退出其他模式
|
|||
|
|
const setMode = useCallback((mode: ViewerMode) => {
|
|||
|
|
setState(prev => ({ ...prev, activeMode: mode }));
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 按 Esc 键退出当前模式
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|||
|
|
if (e.key === 'Escape' && state.activeMode) {
|
|||
|
|
exitMode();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
window.addEventListener('keydown', handleKeyDown);
|
|||
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|||
|
|
}, [state.activeMode, exitMode]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 视觉规范速查表
|
|||
|
|
|
|||
|
|
### 7.1 尺寸规范
|
|||
|
|
|
|||
|
|
| 元素 | 尺寸 | Tailwind CSS |
|
|||
|
|
|------|------|-------------|
|
|||
|
|
| 顶部工具栏按钮 | 自适应高度 36px | `py-2` |
|
|||
|
|
| 底部工具栏按钮 | 48px × 48px | `w-12 h-12` |
|
|||
|
|
| 顶部工具栏图标 | 18px | `size={18}` |
|
|||
|
|
| 底部工具栏图标 | 18px | `size={18}` |
|
|||
|
|
| 二级菜单图标 | 14px | `size={14}` |
|
|||
|
|
| 抽屉宽度 | 320px | `w-[320px]` |
|
|||
|
|
| 标题栏高度 | 48px | `h-12` |
|
|||
|
|
|
|||
|
|
### 7.2 圆角规范
|
|||
|
|
|
|||
|
|
| 元素 | 圆角 | Tailwind CSS |
|
|||
|
|
|------|------|-------------|
|
|||
|
|
| 顶部工具栏容器 | 6px | `rounded-md` |
|
|||
|
|
| 顶部工具栏按钮 | 6px | `rounded-md` |
|
|||
|
|
| 底部工具栏容器 | 完全圆角 | `rounded-full` |
|
|||
|
|
| 底部工具栏按钮 | 完全圆角 | `rounded-full` |
|
|||
|
|
| 二级菜单容器 | 12px | `rounded-xl` |
|
|||
|
|
| 二级菜单项 | 8px | `rounded-lg` |
|
|||
|
|
|
|||
|
|
### 7.3 阴影规范
|
|||
|
|
|
|||
|
|
| 元素 | 阴影 | Tailwind CSS |
|
|||
|
|
|------|------|-------------|
|
|||
|
|
| 顶部工具栏 | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.04)]` |
|
|||
|
|
| 底部工具栏 | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.06)]` |
|
|||
|
|
| 二级菜单 | 较强阴影 | `shadow-[0_8px_24px_rgba(0,0,0,0.12)]` |
|
|||
|
|
| 抽屉面板 | 较强阴影 | `shadow-2xl` |
|
|||
|
|
|
|||
|
|
### 7.4 颜色规范
|
|||
|
|
|
|||
|
|
| 元素 | 亮色模式 | 暗色模式 |
|
|||
|
|
|------|---------|---------|
|
|||
|
|
| **工具栏背景** | `rgba(255,255,255,0.8)` | `rgba(30,41,59,0.8)` |
|
|||
|
|
| **按钮默认文字** | `rgb(71,85,105)` | `rgba(148,163,184)` |
|
|||
|
|
| **按钮激活背景** | `rgb(59,130,246)` | `rgb(37,99,235)` |
|
|||
|
|
| **按钮激活文字** | `rgb(255,255,255)` | `rgb(255,255,255)` |
|
|||
|
|
| **Hover 背景** | `rgba(241,245,249,0.6)` | `rgba(51,65,85,0.6)` |
|
|||
|
|
| **边框** | `rgba(226,232,240,0.5)` | `rgba(71,85,105,0.5)` |
|
|||
|
|
|
|||
|
|
### 7.5 间距规范
|
|||
|
|
|
|||
|
|
| 元素 | 间距 | Tailwind CSS |
|
|||
|
|
|------|------|-------------|
|
|||
|
|
| 顶部工具栏按钮间距 | 6px | `gap-1.5` |
|
|||
|
|
| 底部工具栏按钮间距 | 10px | `gap-2.5` |
|
|||
|
|
| 二级菜单项间距 | 0px | `gap-0` |
|
|||
|
|
| 二级菜单与按钮距离 | 12px | `mb-3` |
|
|||
|
|
|
|||
|
|
### 7.6 动画规范
|
|||
|
|
|
|||
|
|
| 动画类型 | 时长 | 缓动函数 |
|
|||
|
|
|---------|------|---------|
|
|||
|
|
| 按钮状态切换 | 200ms | `ease-out` |
|
|||
|
|
| 二级菜单出现 | 150ms | `[0.16, 1, 0.3, 1]` (cubic-bezier) |
|
|||
|
|
| 抽屉滑入 | 300ms | `spring` (damping: 30, stiffness: 300) |
|
|||
|
|
| Hover 缩放 | 200ms | `ease-out` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 组件清单
|
|||
|
|
|
|||
|
|
### 8.1 核心组件文件
|
|||
|
|
|
|||
|
|
| 组件名称 | 文件路径 | 说明 |
|
|||
|
|
|---------|---------|------|
|
|||
|
|
| **TopCenterToolbar** | `viewer/TopCenterToolbar.tsx` | 顶部中间工具栏组件 |
|
|||
|
|
| **BottomRightToolbar** | `viewer/BottomRightToolbar.tsx` | 底部右侧工具栏组件 |
|
|||
|
|
| **HoverMenu** | `viewer/HoverMenu.tsx` | Hover 二级菜单组件 |
|
|||
|
|
| **ViewerDrawer** | `viewer/ViewerDrawer.tsx` | 右侧抽屉面板容器 |
|
|||
|
|
| **ViewListPanel** | `viewer/ViewListPanel.tsx` | 视图列表面板 |
|
|||
|
|
| **PropertiesPanel** | `viewer/PropertiesPanel.tsx` | 属性面板 |
|
|||
|
|
| **FilterPanel** | `viewer/FilterPanel.tsx` | 过滤面板 |
|
|||
|
|
| **PinsPanel** | `viewer/PinsPanel.tsx` | 图钉列表面板 |
|
|||
|
|
| **ViewerStates** | `viewer/ViewerStates.tsx` | Viewer 各种状态组件 |
|
|||
|
|
| **EnvironmentSettingsModal** | `viewer/EnvironmentSettingsModal.tsx` | 环境设置弹窗 |
|
|||
|
|
| **ModeIndicator** | `viewer/ModeIndicator.tsx` | 模式提示组件 |
|
|||
|
|
|
|||
|
|
### 8.2 状态管理
|
|||
|
|
|
|||
|
|
| Hook/Provider | 文件路径 | 说明 |
|
|||
|
|
|--------------|---------|------|
|
|||
|
|
| **useViewerState** | `hooks/use-viewer-state.tsx` | Viewer 状态管理 Hook |
|
|||
|
|
| **ViewerProvider** | `hooks/use-viewer-state.tsx` | Viewer 状态 Context Provider |
|
|||
|
|
|
|||
|
|
### 8.3 依赖库
|
|||
|
|
|
|||
|
|
| 库名称 | 版本 | 用途 |
|
|||
|
|
|-------|------|------|
|
|||
|
|
| **React** | ^18.x | UI 框架 |
|
|||
|
|
| **TypeScript** | ^5.x | 类型系统 |
|
|||
|
|
| **Ant Design** | ^5.x | UI 组件库 |
|
|||
|
|
| **Tailwind CSS** | ^3.x | 样式框架 |
|
|||
|
|
| **Framer Motion** | ^11.x | 动画库 |
|
|||
|
|
| **lucide-react** | ^0.x | 图标库 |
|
|||
|
|
| **clsx** | ^2.x | 类名组合工具 |
|
|||
|
|
| **next-intl** | ^3.x | 国际化 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录:完整组件层次结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ViewerContainer
|
|||
|
|
├── TopCenterToolbar (顶部中间工具栏)
|
|||
|
|
│ └── Button × 5 (视图/属性/过滤/图钉/全屏)
|
|||
|
|
│
|
|||
|
|
├── BottomRightToolbar (底部右侧工具栏)
|
|||
|
|
│ ├── HoverMenu × 4 (主视图/测量/剖切/漫游)
|
|||
|
|
│ │ └── MenuItem[] (二级菜单项列表)
|
|||
|
|
│ └── Button (环境设置)
|
|||
|
|
│
|
|||
|
|
├── ViewerDrawer (右侧抽屉)
|
|||
|
|
│ ├── ViewListPanel (视图列表面板)
|
|||
|
|
│ ├── PropertiesPanel (属性面板)
|
|||
|
|
│ ├── FilterPanel (过滤面板)
|
|||
|
|
│ └── PinsPanel (图钉列表面板)
|
|||
|
|
│
|
|||
|
|
├── EnvironmentSettingsModal (环境设置弹窗)
|
|||
|
|
│
|
|||
|
|
├── ModeIndicator (模式提示)
|
|||
|
|
│
|
|||
|
|
└── ViewerStates (各种状态组件)
|
|||
|
|
├── EmptyState (空状态)
|
|||
|
|
├── LoadingState (加载中)
|
|||
|
|
└── ErrorState (错误状态)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档编写完成**
|
|||
|
|
如有任何疑问或需要补充的内容,请联系开发团队。
|