# -*- coding: utf-8 -*- import ftplib import inspect import os import shutil from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QSize, QUrl, Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QTreeWidget, QTreeWidgetItem, QMessageBox, \ QInputDialog, QLineEdit, QWidget, QFileDialog from .FtpUitl import FtpOper from .FtpConfig import * # from .ModelWebView import ModelWebView current_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) # 获取当前路径 class Ui_FTPDockWidget(object): def setupUi(self, FTPDockWidget): self.ftpOper = FtpOper() try: self.ftpOper.connect(ftpHost, ftpPort, ftpUsername, ftpPassword, timeout=30, passive=True) except Exception as e: QMessageBox.critical(None, 'FTP连接失败', f'无法连接到FTP服务器: {str(e)}') self.fileItem = None self.ftpItem = None self.createTempDir() FTPDockWidget.setObjectName("FTPDockWidget") FTPDockWidget.resize(422, 303) font = QtGui.QFont() font.setFamily("微软雅黑") FTPDockWidget.setFont(font) self.dockWidgetContents = QtWidgets.QWidget() self.dockWidgetContents.setObjectName("dockWidgetContents") # 添加FTP管理界面 self.mainLayout = QVBoxLayout(self.dockWidgetContents) self.layoutH = QHBoxLayout() # 按钮布局 self.layoutV = QVBoxLayout() # 树结构的布局 # 功能按钮区域 self.uploadFileButton = QPushButton() self.uploadFileButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.uploadFileButton) self.uploadFileButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件上传.png"))) self.uploadFileButton.setIconSize(QSize(20, 20)) self.uploadFileButton.setFixedSize(27, 27) self.uploadFileButton.setToolTip("上传文件") self.uploadFileButton.clicked.connect(self.actionUploadHandler) self.layoutH.addWidget(self.uploadFileButton) self.uploadFolderButton = QPushButton() self.uploadFolderButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.uploadFolderButton) self.uploadFolderButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件夹上传.png"))) self.uploadFolderButton.setIconSize(QSize(20, 20)) self.uploadFolderButton.setFixedSize(27, 27) self.uploadFolderButton.setToolTip("上传文件夹") self.uploadFolderButton.clicked.connect(self.actionUploadDirHandler) self.layoutH.addWidget(self.uploadFolderButton) self.downloadButton = QPushButton() self.downloadButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.downloadButton) self.downloadButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件下载.png"))) self.downloadButton.setIconSize(QSize(20, 20)) self.downloadButton.setFixedSize(27, 27) self.downloadButton.setToolTip("下载") self.downloadButton.clicked.connect(self.actionDownloadHandler) self.layoutH.addWidget(self.downloadButton) self.createDirButton = QPushButton() self.createDirButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.createDirButton) self.createDirButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "新建文件夹.png"))) self.createDirButton.setIconSize(QSize(20, 20)) self.createDirButton.setFixedSize(27, 27) self.createDirButton.setToolTip("新建目录") self.createDirButton.clicked.connect(self.actionCreateHandler) self.layoutH.addWidget(self.createDirButton) self.renameButton = QPushButton() self.renameButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.renameButton) self.renameButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件重命名.png"))) self.renameButton.setIconSize(QSize(20, 20)) self.renameButton.setFixedSize(27, 27) self.renameButton.setToolTip("重命名") self.renameButton.clicked.connect(self.actionRenameHandler) self.layoutH.addWidget(self.renameButton) self.deleteButton = QPushButton() self.deleteButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.deleteButton) self.deleteButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件删除.png"))) self.deleteButton.setIconSize(QSize(20, 20)) self.deleteButton.setFixedSize(27, 27) self.deleteButton.setToolTip("删除") self.deleteButton.clicked.connect(self.actionDeleteHandler) self.layoutH.addWidget(self.deleteButton) self.modelViewButton = QPushButton() self.modelViewButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.modelViewButton) self.modelViewButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "模型查看.png"))) self.modelViewButton.setIconSize(QSize(20, 20)) self.modelViewButton.setFixedSize(27, 27) self.modelViewButton.setToolTip("查看模型") self.modelViewButton.clicked.connect(self.action3DHandler) self.layoutH.addWidget(self.modelViewButton) self.openFileButton = QPushButton() self.openFileButton.setCursor(Qt.PointingHandCursor) self.setBtnStyleSheet(self.openFileButton) self.openFileButton.setIcon(QIcon(os.path.join(current_directory + "\\icon", "文件查看.png"))) self.openFileButton.setIconSize(QSize(20, 20)) self.openFileButton.setFixedSize(27, 27) self.openFileButton.setToolTip("查看文件") self.openFileButton.clicked.connect(self.actionFileOpenHandler) self.layoutH.addWidget(self.openFileButton) # 添加动态布局 元素自动左对齐 self.layoutH.addStretch() # 目录树结构区 self.treeWidget = QTreeWidget() # QTreeWidget组件定义 self.treeWidget.headerItem().setText(0, "列表") # FTP服务器节点树 self.treeWidget.headerItem().setText(1, "类型") # 节点对应的类型:文件,文件夹 self.treeWidget.headerItem().setText(2, "路径") # 当前节点在FTP服务器中的路径,方便文件操作时获取服务器中路径,为隐藏列 self.treeWidget.setColumnWidth(0, 375) # 给第1列设置列宽 self.rootItem = QTreeWidgetItem() self.rootItem.setText(0, "文件结构树") # 给根节点增加文本 self.treeWidget.addTopLevelItem(self.rootItem) # 增加根节点 self.layoutV.addWidget(self.treeWidget) # 将组件添加到布局中 self.treeWidget.setColumnHidden(1, True) # 隐藏路径列 self.treeWidget.setColumnHidden(2, True) # 隐藏路径列 self.mainLayout.addLayout(self.layoutH) self.mainLayout.addLayout(self.layoutV) # 为窗体添加布局 FTPDockWidget.setWidget(self.dockWidgetContents) self.retranslateUi(FTPDockWidget) QtCore.QMetaObject.connectSlotsByName(FTPDockWidget) # 遍历构建树 try: self.traverseFTPDirectory(self.ftpOper.ensure_connected(), '/', self.rootItem) self.rootItem.setExpanded(True) except Exception as e: QMessageBox.warning(None, 'FTP错误', f'读取目录失败: {str(e)}') def retranslateUi(self, SearchDockWidget): _translate = QtCore.QCoreApplication.translate SearchDockWidget.setWindowTitle(_translate("SearchDockWidget", "文件服务器")) # 设置按钮样式 def setBtnStyleSheet(self, btn): btn.setStyleSheet(""" QPushButton { border: 2px solid transparent; /* 默认边框透明 */ padding: 10px; background-color: #f0f0f0; } QPushButton:hover { background-color: #f6f6f6; border: 1px solid #958585; /* 鼠标悬浮时显示蓝色边框 */ } """) def actionUploadHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() if self.currentItem.text(1) == '文件': # 如果选中文件节点则获取此节点的父节点 self.ftp_path = self.currentItem.parent().text(2) self.ftpItem = self.currentItem.parent() elif self.currentItem.text(1) == '文件夹': self.ftp_path = self.currentItem.text(2) self.ftpItem = self.currentItem file_paths, _ = QFileDialog.getOpenFileNames(None, '选择文件', '', 'All Files (*)') if len(file_paths) > 0: for file_path in file_paths: try: self.file_name = os.path.basename(file_path) self.ftpOper.uploadfile(file_path, self.ftp_path) self.fileItem = QTreeWidgetItem() self.fileItem.setText(0, self.file_name) self.fileItem.setText(1, '文件') self.fileItem.setText(2, self.ftp_path + '/' + self.file_name) # 已有同名节点不再增加此名称的节点 havesame = self.findTreeSameNode(self.ftpItem, self.file_name) if not havesame: self.ftpItem.addChild(self.fileItem) except Exception as e: QMessageBox.warning(None, '上传失败', f'上传文件失败: {self.file_name}\n{str(e)}') else: QMessageBox.information(self, "提示信息", "请选择树节点") # 文件上传时查找目录树此节点的子节点中是否已有此名称 def findTreeSameNode(self, thisitem, filename): for i in range(thisitem.childCount()): child = thisitem.child(i) if child.text(0) == filename: return True return False # 文件夹上传操作 def actionUploadDirHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() if self.currentItem.text(1) == '文件': # 如果选中文件节点则获取此节点的父节点 self.ftp_path = self.currentItem.parent().text(2) self.ftpItem = self.currentItem.parent() else: self.ftp_path = self.currentItem.text(2) self.ftpItem = self.currentItem dir_path = QFileDialog.getExistingDirectory(None, "选择文件夹", "") if dir_path: try: self.ftpOper.uploaddir(dir_path, self.ftp_path) dirname = os.path.split(dir_path)[-1] # 文件夹名称 rootchildItem = QTreeWidgetItem() rootchildItem.setText(0, dirname) rootchildItem.setText(1, '文件夹') rootchildItem.setText(2, self.ftp_path + '/' + dirname) self.ftpItem.addChild(rootchildItem) self.traverseLocalDirectory(dir_path, rootchildItem, self.ftp_path + '/' + dirname) except Exception as e: QMessageBox.warning(None, '上传失败', f'上传文件夹失败: {str(e)}') else: QMessageBox.information(self, "提示信息", "请选择树节点") # 遍历本地目录,构建可视化树结构 def traverseLocalDirectory(self, dirpath, item, ftppath): for file in os.listdir(dirpath): src = os.path.join(dirpath, file) childItem = QTreeWidgetItem() childItem.setText(0, file) if os.path.isfile(src): childItem.setText(1, '文件') childItem.setText(2, ftppath + '/' + file) item.addChild(childItem) elif os.path.isdir(src): childItem.setText(1, '文件夹') childItem.setText(2, ftppath + '/' + file) item.addChild(childItem) # 递归调用自身 self.traverseLocalDirectory(src, childItem, ftppath + '/' + file) # 文件下载操作 def actionDownloadHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() dir_path = QFileDialog.getExistingDirectory(None, "选择文件夹", "") if dir_path: try: if self.currentItem.text(1) == '文件': localfilepath = dir_path + '/' + self.currentItem.text(0) reomtefilepath = self.currentItem.text(2) self.ftpOper.downloadfile(localfilepath, reomtefilepath) else: localdirpath = dir_path + '/' + self.currentItem.text(0) remotedirpath = self.currentItem.text(2) self.ftpOper.downloaddir(localdirpath, remotedirpath) except Exception as e: QMessageBox.warning(None, '下载失败', f'下载失败: {str(e)}') else: QMessageBox.information(self, "提示信息", "请选择树节点") # 文件删除操作 def actionDeleteHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() questiontext = '是否要删除所选中的:【' + self.currentItem.text(0) + '】?' question = QMessageBox.question(None, '删除确认', questiontext) if question == QMessageBox.Yes: try: reomtefilepath = self.currentItem.text(2) if self.currentItem.text(1) == '文件': remotepath, remotefile_name = os.path.split(reomtefilepath) self.ftpOper.deletfile(remotefile_name, remotepath) # 删除FTP服务端文件 self.currentItem.parent().removeChild(self.currentItem) # 删除目录树对应的节点 else: self.ftpOper.deletedir(reomtefilepath) self.currentItem.parent().removeChild(self.currentItem) # 删除目录树对应的节点 except Exception as e: QMessageBox.warning(None, '删除失败', f'删除失败: {str(e)}') else: QMessageBox.information(self, "提示信息", "请选择树节点") # 创建文件夹操作 def actionCreateHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() dirname, ok = QInputDialog.getText(QWidget(), '新建文件夹', '请输入文件夹名称:') if ok: if self.currentItem.text(1): if self.currentItem.text(1) == '文件': # 如果选中文件节点则获取此节点的父节点 self.dirpath = self.currentItem.parent().text(2) self.ftpItem = self.currentItem.parent() else: self.dirpath = self.currentItem.text(2) self.ftpItem = self.currentItem else: self.dirpath = '' self.ftpItem = self.currentItem self.newdirpath = self.dirpath + '/' + dirname self.rusulttext = self.ftpOper.makedir(dirname, self.dirpath, self.newdirpath) if self.rusulttext == '文件夹创建成功' or self.rusulttext == '文件夹已存在': self.fileItem = QTreeWidgetItem() self.fileItem.setText(0, dirname) self.fileItem.setText(1, '文件夹') self.fileItem.setText(2, self.newdirpath) self.ftpItem.addChild(self.fileItem) else: QMessageBox.information(self, "提示信息", "请选择树节点") # 重命名操作 def actionRenameHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() extension = None oldname = None oldpath = None if self.currentItem.text(1) == '文件': oldname, oldextension = os.path.splitext(self.currentItem.text(0)) elif self.currentItem.text(1) == '文件夹': oldname = self.currentItem.text(0) newname, ok = QInputDialog.getText(QWidget(), '重命名', '请输入新名称', QLineEdit.Normal, oldname) if ok: try: if self.currentItem.text(1) == '文件': oldpath = self.currentItem.text(2) pathparent, name1 = os.path.split(oldpath) name2, extension = os.path.splitext(name1) newpath = pathparent + '/' + newname + extension else: oldpath = self.currentItem.text(2) if not (self.currentItem.parent().text(1)): # 根目录 newpath = '/' + newname else: pathparent, name1 = os.path.split(oldpath) newpath = pathparent + '/' + newname result = self.ftpOper.rename(oldpath, newpath) if result: if extension: self.currentItem.setText(0, newname + extension) else: self.currentItem.setText(0, newname) self.currentItem.setText(2, newpath) except Exception as e: QMessageBox.warning(None, '重命名失败', f'重命名失败: {str(e)}') else: QMessageBox.information(self, "提示信息", "请选择树节点") # 查看三维模型操作 def action3DHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() if (self.currentItem.text(1) == '文件'): try: rel_path = self.currentItem.text(2) modelUrl = (modelViewerUri + modelBaseUri + rel_path).replace('\\', '/') print(f'打开模型URL: {modelUrl}') try: # 优先使用内置WebView from .ModelWebView import ModelWebView # 延迟导入,避免环境缺少QtWebEngine时报错 self.modelWebView = ModelWebView(weburi=modelUrl, title='数管系统三维模型浏览') self.modelWebView.show() except Exception as webview_err: print(f'ModelWebView不可用,回退到系统浏览器: {webview_err}') # 回退到系统默认浏览器 QtGui.QDesktopServices.openUrl(QUrl(modelUrl)) QMessageBox.information(None, '提示', '已在默认浏览器中打开模型') except Exception as e: QMessageBox.warning(None, '打开失败', f'无法打开模型: {str(e)}') else: QMessageBox.information(self, "提示信息", "请选择文件类型树节点") else: QMessageBox.information(self, "提示信息", "请选择树节点") # 打开文件操作 def actionFileOpenHandler(self): if self.treeWidget.currentItem(): self.currentItem = self.treeWidget.currentItem() if (self.currentItem.text(1) == '文件'): localdir = os.path.abspath(os.curdir) localtempdir = localdir + '/temp' if os.path.exists(localtempdir): if self.currentItem.text(1) == '文件': localfilepath = localtempdir + '/' + self.currentItem.text(0) reomtefilepath = self.currentItem.text(2) if os.path.exists(localfilepath): try: os.startfile(localfilepath) # 打开本地文件 except Exception as e: print(e) else: self.ftpOper.downloadfile(localfilepath, reomtefilepath) # 下载到本地临时文件夹 try: os.startfile(localfilepath) # 打开本地文件 except Exception as e: print(e) else: QMessageBox.information(self, "提示信息", "请选择文件类型树节点") else: QMessageBox.information(self, "提示信息", "请选择树节点") # 创建本地文件临时文件夹 def createTempDir(self): localdir = os.path.abspath(os.curdir) localtempdir = localdir + '/temp' if not (os.path.exists(localtempdir)): # 如不存在,创建本地临时文件夹 os.makedirs(localtempdir) else: # 如存在,删除后重新创建本地临时文件夹 shutil.rmtree(localtempdir) os.mkdir(localtempdir) # 遍历FTP服务端目录,构建可视化树结构 def traverseFTPDirectory(self, ftp, path, item): try: ftp.cwd(path) file_list = ftp.nlst() except Exception as e: raise e for file in file_list: try: ftp.cwd(path + '/' + file) # 尝试切换到子目录 # 添加子节点 childItem = QTreeWidgetItem() childItem.setText(0, file) childItem.setText(1, '文件夹') childItem.setText(2, (path + '/' + file).replace('//', '/')) item.addChild(childItem) # 递归调用自身 self.traverseFTPDirectory(ftp, (path + '/' + file).replace('//', '/'), childItem) ftp.cwd('..') # 返回到上一级目录 except ftplib.error_perm: # 添加子节点 childItem = QTreeWidgetItem() childItem.setText(0, file) childItem.setText(1, '文件') childItem.setText(2, (path + '/' + file).replace('//', '/')) item.addChild(childItem)