zhong (钟鹏群) преди 1 седмица
ревизия
5e104681f8
променени са 2 файла, в които са добавени 639 реда и са изтрити 0 реда
  1. 192 0
      README.md
  2. 447 0
      folder_tree_3d.py

+ 192 - 0
README.md

@@ -0,0 +1,192 @@
+# 3D文件树界面程序
+
+一个交互式的3D文件树可视化工具,允许用户输入文件夹路径并以3D形式浏览文件结构。
+
+## 功能特性
+
+- **文件夹路径输入**:用户可以通过界面输入任意文件夹路径
+- **3D文件树可视化**:将文件夹结构以3D树状图形式展示
+- **交互式浏览**:支持旋转、缩放、平移等3D交互操作
+- **文件类型识别**:不同文件类型使用不同颜色和图标区分
+- **实时更新**:当文件夹内容变化时,树状图自动更新
+
+## 技术栈
+
+- **前端框架**:Vue.js 3 + TypeScript
+- **3D图形库**:Three.js
+- **后端框架**:Django + Django REST Framework (DRF)
+- **数据库**:SQLite (开发) / PostgreSQL (生产)
+- **样式框架**:Element Plus 或 Vuetify
+- **构建工具**:Vite
+
+## 项目结构
+
+```
+IamHere/
+├── backend/                 # Django后端
+│   ├── filetree_api/       # Django应用
+│   │   ├── migrations/
+│   │   ├── models.py       # 数据模型
+│   │   ├── serializers.py  # DRF序列化器
+│   │   ├── views.py        # API视图
+│   │   └── urls.py         # 路由配置
+│   ├── config/             # Django配置
+│   │   ├── settings.py
+│   │   ├── urls.py
+│   │   └── wsgi.py
+│   ├── manage.py
+│   └── requirements.txt
+├── frontend/               # Vue.js前端
+│   ├── src/
+│   │   ├── components/     # Vue组件
+│   │   │   ├── FileTree3D/ # 3D文件树组件
+│   │   │   ├── PathInput/  # 路径输入组件
+│   │   │   └── Controls/   # 控制面板组件
+│   │   ├── utils/
+│   │   │   ├── api.js      # API调用工具
+│   │   │   └── treeBuilder.js # 树状结构构建工具
+│   │   ├── App.vue         # 主应用组件
+│   │   └── main.js         # 入口文件
+│   ├── public/
+│   ├── package.json
+│   └── vite.config.js
+├── docs/                   # 文档
+└── README.md
+```
+
+## 安装与运行
+
+### 前提条件
+
+- Python 3.8 或更高版本
+- Node.js 16 或更高版本
+- pip 和 npm
+
+### 后端安装步骤
+
+1. 进入后端目录
+```bash
+cd backend
+```
+
+2. 创建虚拟环境并激活
+```bash
+python -m venv venv
+# Windows
+venv\Scripts\activate
+# Linux/Mac
+source venv/bin/activate
+```
+
+3. 安装依赖
+```bash
+pip install -r requirements.txt
+```
+
+4. 运行数据库迁移
+```bash
+python manage.py migrate
+```
+
+5. 启动Django开发服务器
+```bash
+python manage.py runserver
+```
+
+### 前端安装步骤
+
+1. 进入前端目录
+```bash
+cd frontend
+```
+
+2. 安装依赖
+```bash
+npm install
+```
+
+3. 启动开发服务器
+```bash
+npm run dev
+```
+
+4. 打开浏览器访问 `http://localhost:5173`
+
+## 使用说明
+
+### 基本操作
+
+1. **输入文件夹路径**:在界面顶部的输入框中输入要浏览的文件夹完整路径
+2. **加载文件树**:点击"加载"按钮或按Enter键生成3D文件树
+3. **3D交互控制**:
+   - 鼠标左键拖动:旋转视角
+   - 鼠标滚轮:缩放视图
+   - 鼠标右键拖动:平移视图
+4. **节点交互**:点击文件节点查看详细信息
+
+### 文件类型标识
+
+- 📁 **文件夹**:蓝色立方体
+- 📄 **文本文件**:绿色文档图标
+- 🖼️ **图片文件**:黄色图片图标
+- 📊 **数据文件**:紫色表格图标
+- ⚙️ **可执行文件**:红色齿轮图标
+- 🔧 **配置文件**:灰色工具图标
+
+## 开发计划
+
+### 第一阶段:基础功能
+- [ ] 创建基本的React应用结构
+- [ ] 实现文件夹路径输入界面
+- [ ] 集成Three.js基础3D场景
+- [ ] 实现简单的文件树数据结构
+
+### 第二阶段:3D可视化
+- [ ] 开发3D文件树渲染组件
+- [ ] 实现交互控制(旋转、缩放、平移)
+- [ ] 添加文件类型图标系统
+- [ ] 优化3D性能和大文件树处理
+
+### 第三阶段:增强功能
+- [ ] 添加文件搜索功能
+- [ ] 实现文件操作(重命名、删除等)
+- [ ] 添加主题切换
+- [ ] 支持多语言界面
+
+## 贡献指南
+
+欢迎提交Issue和Pull Request来改进这个项目!
+
+### 开发环境设置
+
+1. Fork 项目仓库
+2. 克隆你的fork到本地
+3. 创建功能分支:`git checkout -b feature/新功能`
+4. 提交更改:`git commit -am '添加新功能'`
+5. 推送到分支:`git push origin feature/新功能`
+6. 创建Pull Request
+
+### 代码规范
+
+- 使用ES6+语法
+- 遵循项目现有的代码风格
+- 添加适当的注释
+- 确保代码通过ESLint检查
+
+## 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
+
+## 联系方式
+
+如有问题或建议,请通过以下方式联系:
+- 提交GitHub Issue
+- 发送邮件至:your-email@example.com
+
+## 更新日志
+
+### v1.0.0 (计划中)
+- 初始版本发布
+- 基础3D文件树功能
+- 文件夹路径输入界面
+- 基本的交互控制

+ 447 - 0
folder_tree_3d.py

@@ -0,0 +1,447 @@
+import tkinter as tk
+from tkinter import filedialog, ttk
+import os
+import math
+from tkinter import Canvas
+
+class FolderTree3D:
+    def __init__(self, root):
+        self.root = root
+        self.root.title("文件夹3D树状图")
+        self.root.geometry("1200x800")
+        
+        # 创建主框架
+        main_frame = ttk.Frame(root)
+        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
+        
+        # 输入区域
+        input_frame = ttk.Frame(main_frame)
+        input_frame.pack(fill=tk.X, pady=(0, 10))
+        
+        ttk.Label(input_frame, text="文件夹路径:").pack(side=tk.LEFT, padx=(0, 5))
+        self.path_var = tk.StringVar()
+        self.path_entry = ttk.Entry(input_frame, textvariable=self.path_var, width=80)
+        self.path_entry.pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True)
+        
+        browse_btn = ttk.Button(input_frame, text="浏览", command=self.browse_folder)
+        browse_btn.pack(side=tk.LEFT, padx=(0, 5))
+        
+        show_btn = ttk.Button(input_frame, text="显示3D树状图", command=self.show_3d_tree)
+        show_btn.pack(side=tk.LEFT)
+        
+        # 缩放控制
+        zoom_frame = ttk.Frame(main_frame)
+        zoom_frame.pack(fill=tk.X, pady=(0, 5))
+        
+        ttk.Label(zoom_frame, text="缩放:").pack(side=tk.LEFT, padx=(0, 5))
+        self.zoom_var = tk.DoubleVar(value=1.0)
+        self.zoom_scale = ttk.Scale(zoom_frame, from_=0.1, to=2.0, variable=self.zoom_var, command=self.on_zoom)
+        self.zoom_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
+        
+        self.zoom_label = ttk.Label(zoom_frame, text="100%")
+        self.zoom_label.pack(side=tk.LEFT)
+        
+        # 3D显示区域
+        self.canvas = Canvas(main_frame, bg="white")
+        self.canvas.pack(fill=tk.BOTH, expand=True)
+        
+        # 缩放和平移支持
+        self.scale = 1.0
+        self.offset_x = 0
+        self.offset_y = 0
+        self.last_x = 0
+        self.last_y = 0
+        self.dragging = False
+        
+        # 绑定鼠标事件
+        self.canvas.bind("<ButtonPress-1>", self.on_press)
+        self.canvas.bind("<B1-Motion>", self.on_drag)
+        self.canvas.bind("<ButtonRelease-1>", self.on_release)
+        self.canvas.bind("<MouseWheel>", self.on_mouse_wheel)
+        
+        # 文件夹数据
+        self.folder_data = {}
+        self.max_depth = 0
+        self.tree_data = []  # 存储所有节点数据,用于缩放重绘
+        
+    def browse_folder(self):
+        folder_path = filedialog.askdirectory()
+        if folder_path:
+            self.path_var.set(folder_path)
+            
+    def scan_folder(self, folder_path, depth=0):
+        if depth > self.max_depth:
+            self.max_depth = depth
+            
+        try:
+            items = os.listdir(folder_path)
+            folders = []
+            files = []
+            
+            for item in items:
+                item_path = os.path.join(folder_path, item)
+                if os.path.isdir(item_path):
+                    folders.append(item)
+                    self.scan_folder(item_path, depth + 1)
+                else:
+                    files.append(item)
+                    
+            self.folder_data[folder_path] = {"folders": folders, "files": files, "depth": depth}
+            
+        except Exception as e:
+            print(f"扫描文件夹出错: {e}")
+            
+    def draw_3d_tree(self):
+        self.canvas.delete("all")
+        self.tree_data = []
+        
+        if not self.folder_data:
+            return
+            
+        # 收集树状数据
+        self.collect_tree_data(self.path_var.get(), 0, self.canvas.winfo_width()//2, 100)
+        
+        # 绘制树状图
+        self.draw_tree_from_data()
+        
+    def collect_tree_data(self, folder_path, depth=0, root_x=0, root_y=0):
+        if folder_path not in self.folder_data:
+            return
+            
+        data = self.folder_data[folder_path]
+        folders = data["folders"]
+        files = data["files"]
+        
+        # 计算当前节点位置
+        node = {
+            "type": "folder",
+            "path": folder_path,
+            "name": os.path.basename(folder_path),
+            "x": root_x,
+            "y": root_y,
+            "depth": depth,
+            "children": []
+        }
+        
+        self.tree_data.append(node)
+        
+        # 收集所有层级的节点和父节点关系
+        all_nodes = []
+        self.collect_all_nodes(folder_path, depth, all_nodes)
+        
+        # 按层级分组
+        level_nodes = {}
+        for node_data in all_nodes:
+            level = node_data["depth"]
+            if level not in level_nodes:
+                level_nodes[level] = []
+            level_nodes[level].append(node_data)
+        
+        # 重新计算所有节点位置,使用扇区布局
+        self.calculate_sector_positions(root_x, root_y, level_nodes)
+        
+    def collect_all_nodes(self, folder_path, depth, all_nodes):
+        if folder_path not in self.folder_data:
+            return
+            
+        data = self.folder_data[folder_path]
+        folders = data["folders"]
+        files = data["files"]
+        
+        # 添加当前文件夹的子节点
+        for folder in folders:
+            folder_path_full = os.path.join(folder_path, folder)
+            node_data = {
+                "type": "folder",
+                "name": folder,
+                "path": folder_path_full,
+                "depth": depth + 1,
+                "parent": folder_path
+            }
+            all_nodes.append(node_data)
+            self.collect_all_nodes(folder_path_full, depth + 1, all_nodes)
+            
+        for file in files:
+            node_data = {
+                "type": "file",
+                "name": file,
+                "path": os.path.join(folder_path, file),
+                "depth": depth + 1,
+                "parent": folder_path
+            }
+            all_nodes.append(node_data)
+            
+    def calculate_sector_positions(self, root_x, root_y, level_nodes):
+        # 清空原有数据,重新绘制
+        self.tree_data = [{
+            "type": "folder",
+            "name": os.path.basename(self.path_var.get()),
+            "path": self.path_var.get(),
+            "x": root_x,
+            "y": root_y,
+            "depth": 0
+        }]
+        
+        # 先计算所有节点的位置
+        all_node_data = []
+        
+        # 为每个层级计算位置
+        for level in sorted(level_nodes.keys()):
+            nodes = level_nodes[level]
+            total_nodes = len(nodes)
+            if total_nodes == 0:
+                continue
+                
+            radius = level * 100
+            
+            # 按父节点分组
+            parent_groups = {}
+            for node_data in nodes:
+                parent = node_data["parent"]
+                if parent not in parent_groups:
+                    parent_groups[parent] = []
+                parent_groups[parent].append(node_data)
+            
+            # 为每个父节点的子节点计算位置
+            for parent_path, group_nodes in parent_groups.items():
+                parent_node = next((n for n in self.tree_data if n["path"] == parent_path), None)
+                if not parent_node:
+                    continue
+                    
+                # 计算当前层级的扇区范围
+                # 父节点所在环的文件夹个数
+                parent_level = parent_node["depth"]
+                parent_level_nodes = []
+                if parent_level in level_nodes:
+                    parent_level_nodes = [n for n in level_nodes[parent_level] if n["type"] == "folder"]
+                else:
+                    parent_level_nodes = [parent_node] if parent_node["type"] == "folder" else []
+                    
+                total_parent_folders = len(parent_level_nodes)
+                if total_parent_folders == 0:
+                    total_parent_folders = 1
+                    
+                # 每个文件夹分配360度/total_parent_folders的扇区
+                sector_angle = 2 * math.pi / total_parent_folders
+                
+                # 找到父节点在同级中的位置
+                parent_index = 0
+                if parent_level in level_nodes and parent_node["type"] == "folder":
+                    for i, n in enumerate(parent_level_nodes):
+                        if n["path"] == parent_path:
+                            parent_index = i
+                            break
+                
+                # 计算父节点的扇区
+                start_angle = parent_index * sector_angle
+                end_angle = (parent_index + 1) * sector_angle
+                sector_width = end_angle - start_angle
+                
+                # 在扇区内均匀分布子节点
+                angle_step = sector_width / len(group_nodes)
+                
+                for i, node_data in enumerate(group_nodes):
+                    angle = start_angle + (i + 0.5) * angle_step
+                    # 文件夹在大圆上,文件在小圆上
+                    if node_data["type"] == "folder":
+                        current_radius = radius
+                    else:
+                        current_radius = radius - 20
+                    
+                    x = root_x + current_radius * math.cos(angle)
+                    y = root_y + current_radius * math.sin(angle)
+                    
+                    # 保存扇区信息
+                    node_data["sector_start"] = start_angle + i * angle_step
+                    node_data["sector_end"] = start_angle + (i + 1) * angle_step
+                    node_data["x"] = x
+                    node_data["y"] = y
+                    node_data["angle"] = angle
+                    
+                    all_node_data.append(node_data)
+                    
+                    self.tree_data.append({
+                        "type": node_data["type"],
+                        "name": node_data["name"],
+                        "path": node_data["path"],
+                        "x": x,
+                        "y": y,
+                        "depth": level,
+                        "parent_path": node_data["parent"],
+                        "sector_start": node_data["sector_start"],
+                        "sector_end": node_data["sector_end"],
+                        "angle": angle
+                    })
+                    
+                    # 保存扇区信息
+                    node_data["sector_start"] = start_angle + i * angle_step
+                    node_data["sector_end"] = start_angle + (i + 1) * angle_step
+        
+        # 调整父节点位置,使其与子节点中间位置对齐
+        # 从最深层级开始向上调整
+        max_level = max(level_nodes.keys()) if level_nodes else 0
+        
+        for level in range(max_level, 0, -1):
+            if level not in level_nodes:
+                continue
+                
+            # 找到当前层级的文件夹
+            current_folders = [n for n in level_nodes[level] if n["type"] == "folder"]
+            
+            for folder in current_folders:
+                # 找到该文件夹的所有子节点
+                child_nodes = [n for n in all_node_data if n["parent"] == folder["path"]]
+                if not child_nodes:
+                    continue
+                    
+                # 计算子节点的平均角度
+                total_angle = sum(n["angle"] for n in child_nodes)
+                avg_angle = total_angle / len(child_nodes)
+                
+                # 找到父节点
+                parent_node = next((n for n in self.tree_data if n["path"] == folder["path"]), None)
+                if parent_node:
+                    # 调整父节点位置
+                    radius = level * 100
+                    x = root_x + radius * math.cos(avg_angle)
+                    y = root_y + radius * math.sin(avg_angle)
+                    parent_node["x"] = x
+                    parent_node["y"] = y
+                    parent_node["angle"] = avg_angle
+                
+    def draw_node(self, x, y, folder_path, depth, width, level_spacing):
+        # 改为使用收集的数据进行绘制
+        pass
+        
+    def draw_tree_from_data(self):
+        self.canvas.delete("all")
+        
+        for node in self.tree_data:
+            # 应用缩放和平移
+            x = node["x"] * self.scale + self.offset_x
+            y = node["y"] * self.scale + self.offset_y
+            
+            if node["type"] == "folder":
+                node_radius = min(15, 15 * self.scale)  # 缩小文件夹节点
+                self.canvas.create_oval(x - node_radius, y - node_radius, x + node_radius, y + node_radius, fill="#4CAF50", outline="#2E7D32")
+                font_size = min(8, int(8 * self.scale))  # 缩小字体
+                self.canvas.create_text(x, y, text=node["name"][:8] + "..." if len(node["name"]) > 8 else node["name"], fill="white", font=("Arial", font_size, "bold"))
+            else:
+                file_width = min(20, 20 * self.scale)  # 缩小文件节点
+                file_height = min(15, 15 * self.scale)
+                self.canvas.create_rectangle(x - file_width//2, y - file_height//2, x + file_width//2, y + file_height//2, fill="#2196F3", outline="#1565C0")
+                font_size = min(6, int(6 * self.scale))  # 缩小字体
+                self.canvas.create_text(x, y, text=node["name"][:6] + "..." if len(node["name"]) > 6 else node["name"], fill="white", font=("Arial", font_size))
+                
+        # 绘制连接线
+        for node in self.tree_data:
+            if "parent_path" in node:
+                # 找到父节点
+                parent_node = next((n for n in self.tree_data if n["path"] == node["parent_path"]), None)
+                if parent_node:
+                    x1 = parent_node["x"] * self.scale + self.offset_x
+                    y1 = parent_node["y"] * self.scale + self.offset_y
+                    x2 = node["x"] * self.scale + self.offset_x
+                    y2 = node["y"] * self.scale + self.offset_y
+                    self.canvas.create_line(x1, y1, x2, y2, fill="#666666", width=1)
+        
+        # 在文件所在圆上标记根节点与文件节点连线和圆的交点
+        root_node = self.tree_data[0]
+        root_x = root_node["x"] * self.scale + self.offset_x
+        root_y = root_node["y"] * self.scale + self.offset_y
+        
+        for node in self.tree_data:
+            if node["type"] == "file":
+                # 计算根节点与文件节点连线和文件所在圆的交点
+                file_x = node["x"] * self.scale + self.offset_x
+                file_y = node["y"] * self.scale + self.offset_y
+                
+                # 计算文件所在圆的半径
+                file_depth = node["depth"]
+                file_radius = (file_depth * 100 - 20) * self.scale
+                
+                # 计算交点
+                dx = file_x - root_x
+                dy = file_y - root_y
+                distance = math.sqrt(dx * dx + dy * dy)
+                if distance == 0:
+                    continue
+                
+                # 计算交点坐标
+                intersection_x = root_x + (dx / distance) * file_radius
+                intersection_y = root_y + (dy / distance) * file_radius
+                
+                # 标记交点
+                self.canvas.create_oval(intersection_x - 3, intersection_y - 3, intersection_x + 3, intersection_y + 3, fill="#FF0000", outline="#FF0000", width=1)
+        # 绘制同心圆
+        max_level = max(node["depth"] for node in self.tree_data)
+        # Get root node position after scale and offset
+        root_node = self.tree_data[0]
+        root_x = root_node["x"] * self.scale + self.offset_x
+        root_y = root_node["y"] * self.scale + self.offset_y
+        for level in range(1, max_level + 1):
+            radius = level * 100 * self.scale
+            self.canvas.create_oval(root_x - radius, root_y - radius, root_x + radius, root_y + radius, outline="#333333", width=1)
+            # 在内侧20px处添加小同心圆
+            inner_radius = radius - 20 * self.scale
+            if inner_radius > 0:
+                self.canvas.create_oval(root_x - inner_radius, root_y - inner_radius, root_x + inner_radius, root_y + inner_radius, outline="#666666", width=1)
+                
+    def draw_3d_line(self, x1, y1, x2, y2):
+        # 绘制连接线,使用更细的线条
+        self.canvas.create_line(x1, y1, x2, y2, fill="#757575", width=1)
+        self.canvas.create_line(x1 + 1, y1 + 1, x2 + 1, y2 + 1, fill="#BDBDBD", width=0.5)
+        
+    def on_zoom(self, value):
+        self.scale = float(value)
+        self.zoom_label.config(text=f"{int(self.scale * 100)}%")
+        if self.tree_data:
+            self.draw_tree_from_data()
+            
+    def on_press(self, event):
+        self.last_x = event.x
+        self.last_y = event.y
+        self.dragging = True
+        
+    def on_drag(self, event):
+        if self.dragging:
+            dx = event.x - self.last_x
+            dy = event.y - self.last_y
+            self.offset_x += dx
+            self.offset_y += dy
+            self.last_x = event.x
+            self.last_y = event.y
+            if self.tree_data:
+                self.draw_tree_from_data()
+                
+    def on_release(self, event):
+        self.dragging = False
+        
+    def on_mouse_wheel(self, event):
+        # 鼠标滚轮缩放
+        delta = event.delta
+        if delta > 0:
+            self.scale = min(2.0, self.scale * 1.1)
+        else:
+            self.scale = max(0.1, self.scale * 0.9)
+            
+        self.zoom_var.set(self.scale)
+        self.zoom_label.config(text=f"{int(self.scale * 100)}%")
+        if self.tree_data:
+            self.draw_tree_from_data()
+        
+    def show_3d_tree(self):
+        folder_path = self.path_var.get()
+        if not folder_path or not os.path.isdir(folder_path):
+            tk.messagebox.showerror("错误", "请选择有效的文件夹路径")
+            return
+            
+        self.folder_data = {}
+        self.max_depth = 0
+        self.scan_folder(folder_path)
+        self.draw_3d_tree()
+
+if __name__ == "__main__":
+    root = tk.Tk()
+    app = FolderTree3D(root)
+    root.mainloop()