From 616d8d144f76a28b98849e134f598e171e3e0be2 Mon Sep 17 00:00:00 2001 From: ChengQiqian Date: Fri, 25 Jul 2025 18:08:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tempCodeRunnerFile.py | 548 ++++++++++++++++++++++++++++++++++++++ waibu.py | 599 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1147 insertions(+) create mode 100644 tempCodeRunnerFile.py create mode 100644 waibu.py diff --git a/tempCodeRunnerFile.py b/tempCodeRunnerFile.py new file mode 100644 index 0000000..22b9cb5 --- /dev/null +++ b/tempCodeRunnerFile.py @@ -0,0 +1,548 @@ +import os +import re +import sys +import sqlite3 +import subprocess +from datetime import datetime +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QFileDialog, QTableWidget, + QTableWidgetItem, QComboBox, QMessageBox, QHeaderView, QMenu, QDateEdit) +from PyQt5.QtCore import Qt, QDate, QThread, pyqtSignal +from PyQt5.QtGui import QIcon, QPixmap + + +class DirectoryScanner(QThread): + """使用线程来扫描目录,避免界面冻结""" + scan_finished = pyqtSignal(str, bool, str) # 参数: root_dir, success, message + + def __init__(self, root_dir, db_conn): + super().__init__() + self.root_dir = root_dir + self.db_conn = db_conn + + def run(self): + try: + cursor = self.db_conn.cursor() + cursor.execute("DELETE FROM photos WHERE project_path=?", (self.root_dir,)) + self.db_conn.commit() + + # 先收集所有机组目录 + unit_dirs = [d for d in os.listdir(self.root_dir) if "#" in d] + + # 按机组编号排序 + def extract_number(unit_id): + match = re.search(r'(\d+)', unit_id) + return int(match.group(1)) if match else 0 + + unit_dirs_sorted = sorted(unit_dirs, key=extract_number) + + for unit_dir in unit_dirs_sorted: + unit_path = os.path.join(self.root_dir, unit_dir) + unit_id = unit_dir + + for item in os.listdir(unit_path): + item_path = os.path.join(unit_path, item) + + if not os.path.isdir(item_path): + continue + + blade_num, inspector = self.parse_blade_info(item) + + if not blade_num: + continue + + for file in os.listdir(item_path): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): + file_path = os.path.join(item_path, file) + capture_time = self.parse_capture_time(file) + + if capture_time: + cursor.execute(""" + INSERT INTO photos ( + project_path, unit_id, blade_number, inspector, + photo_path, capture_time, file_name + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + self.root_dir, unit_id, blade_num, inspector, + file_path, capture_time, file + )) + + self.db_conn.commit() + self.scan_finished.emit(self.root_dir, True, "目录扫描完成,数据已更新") + except Exception as e: + self.scan_finished.emit(self.root_dir, False, f"扫描目录时出错: {str(e)}") + + def parse_blade_info(self, dir_name): + """解析叶片目录名称,返回(叶片号, 检查人员)""" + dir_name = dir_name.strip() + + # 情况1: 纯数字 (如 "1", "01", "001") + if re.match(r"^\d+$", dir_name): + return (dir_name.lstrip('0') or '0', None) + + # 情况2: 数字-xxx 格式 (如 "1-张三", "01-李四") + match = re.match(r"^(\d+)[-_]*(.*?)(?:(.+))?$", dir_name) + if match: + blade_num = match.group(1).lstrip('0') or '0' + inspector = match.group(2).strip() or None + return (blade_num, inspector) + + return (None, None) + + def parse_capture_time(self, filename): + match = re.search(r"(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})", filename) + if match: + year, month, day, hour, minute, second = match.groups() + try: + return f"{year}-{month}-{day} {hour}:{minute}:{second}" + except: + return None + return None + + +class PhotoManager(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("风电叶片检查照片管理系统") + self.setWindowIcon(QIcon("wind_turbine.ico")) + self.setGeometry(100, 100, 1200, 800) + + # 数据库连接 + self.db_conn = sqlite3.connect("wind_turbine_photos.db", + check_same_thread=False) # 允许多线程访问 + self.create_db_table() + + # 初始化UI组件 + self.init_ui_components() + + # 加载数据 + self.load_units_to_combo() + self.load_blades_to_combo() + self.load_inspectors_to_combo() + + self.show() + + def init_ui_components(self): + """初始化所有UI组件""" + main_widget = QWidget() + main_layout = QVBoxLayout() + + # 目录选择区域 + dir_layout = QHBoxLayout() + self.dir_label = QLabel("项目目录:") + self.dir_input = QLineEdit() + self.dir_input.setPlaceholderText("请选择风电叶片检查照片根目录") + self.browse_btn = QPushButton("浏览...") + self.browse_btn.clicked.connect(self.browse_directory) + self.scan_btn = QPushButton("扫描目录") + self.scan_btn.clicked.connect(self.start_scan_directory) + + dir_layout.addWidget(self.dir_label) + dir_layout.addWidget(self.dir_input, stretch=1) + dir_layout.addWidget(self.browse_btn) + dir_layout.addWidget(self.scan_btn) + + # 查询条件区域 + query_layout = QHBoxLayout() + + # 机组编号查询 + unit_layout = QVBoxLayout() + unit_layout.addWidget(QLabel("机组编号")) + self.unit_combo = QComboBox() + self.unit_combo.addItem("所有机组", "") + unit_layout.addWidget(self.unit_combo) + + # 叶片号查询 + blade_layout = QVBoxLayout() + blade_layout.addWidget(QLabel("叶片号")) + self.blade_combo = QComboBox() + self.blade_combo.addItem("所有叶片", "") + blade_layout.addWidget(self.blade_combo) + + # 检查人员查询 + inspector_layout = QVBoxLayout() + inspector_layout.addWidget(QLabel("检查人员")) + self.inspector_combo = QComboBox() + self.inspector_combo.addItem("所有人员", "") + inspector_layout.addWidget(self.inspector_combo) + + # 日期范围查询 + date_layout = QVBoxLayout() + date_layout.addWidget(QLabel("拍摄日期范围")) + date_sub_layout = QHBoxLayout() + + self.start_date_edit = QDateEdit() + self.start_date_edit.setDisplayFormat("yyyy-MM-dd") + self.start_date_edit.setDate(QDate.currentDate().addMonths(-1)) + + self.end_date_edit = QDateEdit() + self.end_date_edit.setDisplayFormat("yyyy-MM-dd") + self.end_date_edit.setDate(QDate.currentDate()) + + date_sub_layout.addWidget(self.start_date_edit) + date_sub_layout.addWidget(QLabel("至")) + date_sub_layout.addWidget(self.end_date_edit) + date_layout.addLayout(date_sub_layout) + + query_layout.addLayout(unit_layout) + query_layout.addLayout(blade_layout) + query_layout.addLayout(inspector_layout) + query_layout.addLayout(date_layout) + + # 查询按钮 + self.query_btn = QPushButton("查询") + self.query_btn.clicked.connect(self.query_photos) + query_layout.addWidget(self.query_btn) + + # 主内容区域 + content_layout = QHBoxLayout() + + # 结果显示区域 + self.result_table = QTableWidget() + self.result_table.setColumnCount(6) + self.result_table.setHorizontalHeaderLabels(["机组编号", "叶片号", "检查人员", "拍摄时间", "文件名", "路径"]) + self.result_table.setSelectionBehavior(QTableWidget.SelectRows) + self.result_table.setEditTriggers(QTableWidget.NoEditTriggers) + self.result_table.doubleClicked.connect(self.open_selected_photo) + + # 设置列宽策略 + header = self.result_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.Stretch) + header.setSectionResizeMode(5, QHeaderView.Stretch) + + # 启用行交替颜色 + self.result_table.setAlternatingRowColors(True) + + # 启用右键菜单 + self.result_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.result_table.customContextMenuRequested.connect(self.show_context_menu) + + # 预览区域 + self.preview_label = QLabel() + self.preview_label.setAlignment(Qt.AlignCenter) + self.preview_label.setFixedSize(300, 300) + self.preview_label.setText("照片预览区域") + self.preview_label.setStyleSheet("border: 1px solid gray;") + + # 连接选择变化信号 + self.result_table.selectionModel().selectionChanged.connect(self.update_preview) + + # 添加内容到主布局 + content_layout.addWidget(self.result_table, stretch=3) + content_layout.addWidget(self.preview_label, stretch=1) + + # 状态栏 + self.statusBar().showMessage("就绪") + + # 布局组装 + main_layout.addLayout(dir_layout) + main_layout.addLayout(query_layout) + main_layout.addLayout(content_layout, stretch=1) + + main_widget.setLayout(main_layout) + self.setCentralWidget(main_widget) + + # 在UI组件初始化完成后连接信号 + self.unit_combo.currentIndexChanged.connect(self.update_blades_by_unit) + self.unit_combo.currentIndexChanged.connect(self.update_inspectors_by_unit) + + def create_db_table(self): + cursor = self.db_conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS photos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_path TEXT NOT NULL, + unit_id TEXT NOT NULL, + blade_number TEXT, + inspector TEXT, + photo_path TEXT NOT NULL, + capture_time TEXT NOT NULL, + file_name TEXT NOT NULL + ) + """) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_unit_id ON photos(unit_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_blade_number ON photos(blade_number)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_inspector ON photos(inspector)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_capture_time ON photos(capture_time)") + self.db_conn.commit() + + def browse_directory(self): + dir_path = QFileDialog.getExistingDirectory(self, "选择风电叶片检查照片根目录") + if dir_path: + self.dir_input.setText(dir_path) + + def start_scan_directory(self): + """启动目录扫描线程""" + root_dir = self.dir_input.text() + if not root_dir or not os.path.exists(root_dir): + QMessageBox.warning(self, "警告", "请选择有效的目录路径") + return + + self.scan_btn.setEnabled(False) + self.statusBar().showMessage("正在扫描目录,请稍候...") + + self.scanner = DirectoryScanner(root_dir, self.db_conn) + self.scanner.scan_finished.connect(self.on_scan_finished) + self.scanner.start() + + def on_scan_finished(self, root_dir, success, message): + """扫描完成后的回调""" + self.scan_btn.setEnabled(True) + + if success: + self.load_units_to_combo() + self.load_blades_to_combo() + self.load_inspectors_to_combo() + self.statusBar().showMessage(f"目录扫描完成: {root_dir}", 5000) + QMessageBox.information(self, "完成", message) + else: + QMessageBox.critical(self, "错误", message) + + def load_units_to_combo(self): + """加载机组编号到下拉框,并按数字从小到大排序""" + cursor = self.db_conn.cursor() + cursor.execute("SELECT DISTINCT unit_id FROM photos WHERE unit_id != ''") + units = [row[0] for row in cursor.fetchall()] + + # 提取数字部分进行自然排序 + def natural_sort_key(unit_id): + # 匹配数字部分 + numbers = re.findall(r'\d+', unit_id) + return [int(num) for num in numbers] if numbers else [0] + + units_sorted = sorted(units, key=natural_sort_key) + + self.unit_combo.clear() + self.unit_combo.addItem("所有机组", "") + for unit in units_sorted: + self.unit_combo.addItem(unit, unit) + + def load_blades_to_combo(self, unit_id=None): + """动态加载叶片号到下拉框""" + cursor = self.db_conn.cursor() + + if unit_id: + cursor.execute(""" + SELECT DISTINCT blade_number FROM photos + WHERE blade_number IS NOT NULL AND blade_number != '' AND unit_id = ? + ORDER BY CAST(blade_number AS INTEGER) + """, (unit_id,)) + else: + cursor.execute(""" + SELECT DISTINCT blade_number FROM photos + WHERE blade_number IS NOT NULL AND blade_number != '' + ORDER BY CAST(blade_number AS INTEGER) + """) + + blades = [str(row[0]) for row in cursor.fetchall()] + + self.blade_combo.clear() + self.blade_combo.addItem("所有叶片", "") + for blade in blades: + self.blade_combo.addItem(blade, blade) + + def load_inspectors_to_combo(self, unit_id=None): + cursor = self.db_conn.cursor() + + if unit_id: + cursor.execute(""" + SELECT DISTINCT inspector FROM photos + WHERE inspector != '' AND inspector IS NOT NULL AND unit_id = ? + ORDER BY inspector + """, (unit_id,)) + else: + cursor.execute(""" + SELECT DISTINCT inspector FROM photos + WHERE inspector != '' AND inspector IS NOT NULL + ORDER BY inspector + """) + + inspectors = [row[0] for row in cursor.fetchall()] + + self.inspector_combo.clear() + self.inspector_combo.addItem("所有人员", "") + for inspector in inspectors: + self.inspector_combo.addItem(inspector, inspector) + + def update_blades_by_unit(self): + unit_id = self.unit_combo.currentData() + self.load_blades_to_combo(unit_id) + + def update_inspectors_by_unit(self): + unit_id = self.unit_combo.currentData() + self.load_inspectors_to_combo(unit_id) + + def query_photos(self): + # 获取查询条件 + unit_id = self.unit_combo.currentData() or None + blade_num = self.blade_combo.currentData() + inspector = self.inspector_combo.currentData() or None + start_date = self.start_date_edit.date().toString("yyyy-MM-dd") + end_date = self.end_date_edit.date().toString("yyyy-MM-dd") + + # 构建SQL查询 + query = """ + SELECT unit_id, blade_number, inspector, capture_time, file_name, photo_path + FROM photos + WHERE date(capture_time) BETWEEN ? AND ? + """ + params = [start_date, end_date] + + if unit_id: + query += " AND unit_id = ?" + params.append(unit_id) + + if blade_num: + query += " AND blade_number = ?" + params.append(str(blade_num)) + + if inspector: + query += " AND inspector = ?" + params.append(inspector) + + query += " ORDER BY unit_id, CAST(blade_number AS INTEGER), capture_time" + + # 执行查询 + cursor = self.db_conn.cursor() + try: + cursor.execute(query, params) + results = cursor.fetchall() + + self.result_table.setRowCount(len(results)) + for row_idx, row in enumerate(results): + for col_idx, col in enumerate(row): + item = QTableWidgetItem(str(col) if col is not None else "") + self.result_table.setItem(row_idx, col_idx, item) + + self.statusBar().showMessage(f"找到 {len(results)} 条记录", 5000) + except sqlite3.Error as e: + QMessageBox.critical(self, "数据库错误", f"查询失败: {str(e)}") + + def open_selected_photo(self, index): + """双击打开选中的照片""" + selected_row = index.row() + photo_path = self.result_table.item(selected_row, 5).text() # 第5列是路径 + + if not photo_path: + QMessageBox.warning(self, "警告", "未找到文件路径") + return + + # 标准化路径 - 替换所有斜杠为系统正确的分隔符 + photo_path = os.path.normpath(photo_path.replace('/', '\\')) + + # 检查网络路径是否可用 + if photo_path.startswith('\\\\'): + if not os.path.exists(photo_path): + QMessageBox.warning(self, "网络路径不可访问", + f"无法访问网络路径:\n{photo_path}\n" + f"请检查网络连接或路径权限") + return + + # 检查文件是否存在 + if not os.path.exists(photo_path): + # 尝试从项目目录中查找 + project_dir = self.dir_input.text() + if project_dir: + project_dir = os.path.normpath(project_dir.replace('/', '\\')) + + # 提取文件名 + file_name = os.path.basename(photo_path) + + # 在整个项目目录中搜索文件 + found = False + for root, _, files in os.walk(project_dir): + if file_name in files: + photo_path = os.path.normpath(os.path.join(root, file_name)) + found = True + break + + if not found: + QMessageBox.warning(self, "文件不存在", + f"无法找到文件:\n原始路径: {photo_path}\n" + f"在项目目录中也没有找到同名文件") + return + + try: + # 再次检查路径是否存在 + if not os.path.exists(photo_path): + QMessageBox.warning(self, "错误", f"文件不存在:\n{photo_path}") + return + + # Windows系统 + if os.name == 'nt': + # 对于网络路径,确保使用原始字符串格式 + if photo_path.startswith('\\\\'): + photo_path = r'\\' + photo_path[2:] + os.startfile(photo_path) + # Mac系统 + elif sys.platform == 'darwin': + subprocess.call(['open', photo_path]) + # Linux系统 + else: + subprocess.call(['xdg-open', photo_path]) + + self.statusBar().showMessage(f"正在打开: {os.path.basename(photo_path)}", 3000) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开文件:\n路径: {photo_path}\n错误: {str(e)}") + + def show_context_menu(self, position): + """显示右键菜单""" + menu = QMenu() + open_action = menu.addAction("打开照片") + open_action.triggered.connect(lambda: self.open_selected_photo(self.result_table.currentIndex())) + + show_in_folder_action = menu.addAction("在文件夹中显示") + show_in_folder_action.triggered.connect(self.show_in_folder) + + menu.exec_(self.result_table.viewport().mapToGlobal(position)) + + def show_in_folder(self): + """在文件夹中显示选中的照片""" + selected_row = self.result_table.currentRow() + if selected_row >= 0: + photo_path = self.result_table.item(selected_row, 5).text() + if os.path.exists(photo_path): + folder_path = os.path.dirname(photo_path) + try: + if os.name == 'nt': + os.startfile(folder_path) + elif sys.platform == 'darwin': + subprocess.call(['open', folder_path]) + else: + subprocess.call(['xdg-open', folder_path]) + self.statusBar().showMessage(f"打开文件夹: {folder_path}", 3000) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开文件夹: {str(e)}") + + def update_preview(self): + """更新照片预览""" + selected_rows = self.result_table.selectionModel().selectedRows() + if selected_rows: + photo_path = self.result_table.item(selected_rows[0].row(), 5).text() + if os.path.exists(photo_path): + try: + pixmap = QPixmap(photo_path) + if not pixmap.isNull(): + pixmap = pixmap.scaled(self.preview_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.preview_label.setPixmap(pixmap) + self.preview_label.setText("") + else: + self.preview_label.setText("无法加载预览") + except: + self.preview_label.setText("预览错误") + else: + self.preview_label.setText("文件不存在") + + def closeEvent(self, event): + self.db_conn.close() + event.accept() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + manager = PhotoManager() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/waibu.py b/waibu.py new file mode 100644 index 0000000..015cead --- /dev/null +++ b/waibu.py @@ -0,0 +1,599 @@ +import os +import re +import sys +import sqlite3 +import subprocess +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QFileDialog, QTableWidget, + QTableWidgetItem, QComboBox, QMessageBox, QHeaderView, QMenu, + QDateEdit, QGroupBox, QRadioButton, QButtonGroup, QSplitter) +from PyQt5.QtCore import Qt, QDate, QThread, pyqtSignal +from PyQt5.QtGui import QIcon, QPixmap + + +class ExternalDirectoryScanner(QThread): + """无人机外部检查目录扫描线程""" + scan_finished = pyqtSignal(str, bool, str) # 参数: root_dir, success, message + + def __init__(self, root_dir, db_conn, inspector="无人机检查"): + super().__init__() + self.root_dir = root_dir + self.db_conn = db_conn + self.inspector = inspector + + def run(self): + try: + cursor = self.db_conn.cursor() + # 清理当前项目路径下的旧数据 + print(f"[DEBUG] 清理数据库中的项目路径: {self.root_dir}") + cursor.execute("DELETE FROM external_photos WHERE project_path = ?", (self.root_dir,)) + deleted_rows = cursor.rowcount + print(f"[DEBUG] 已删除 {deleted_rows} 条旧记录") + self.db_conn.commit() + + if not os.path.exists(self.root_dir): + self.scan_finished.emit(self.root_dir, False, f"目录不存在: {self.root_dir}") + return + + # 收集所有机组目录 - 直接使用目录名称作为机组号 + unit_dirs = [d for d in os.listdir(self.root_dir) + if os.path.isdir(os.path.join(self.root_dir, d)) and + d != "缺陷"] # 排除缺陷目录 + + print(f"[DEBUG] 找到 {len(unit_dirs)} 个机组目录: {unit_dirs}") + + if not unit_dirs: + self.scan_finished.emit(self.root_dir, False, "未找到任何机组目录") + return + + total_photos = 0 + for unit_dir in unit_dirs: + unit_path = os.path.join(self.root_dir, unit_dir) + unit_id = unit_dir # 直接使用目录名称作为机组号 + + print(f"[DEBUG] 正在扫描机组: {unit_id}") + + # 首先处理机组目录下的直接图片文件(机组图片) + for file in os.listdir(unit_path): + file_path = os.path.join(unit_path, file) + if os.path.isfile(file_path) and file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): + capture_time = self.parse_capture_time(file) + + if capture_time: + cursor.execute(""" + INSERT OR IGNORE INTO external_photos ( + project_path, unit_id, blade_number, inspector, + photo_path, capture_time, file_name + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + self.root_dir, unit_id, "机组", # blade_number设为"机组"表示机组图片 + self.inspector, + file_path, capture_time, file + )) + if cursor.rowcount > 0: + total_photos += 1 + else: + print(f"[DEBUG] 跳过无法解析时间的文件: {file}") + + # 然后处理叶片目录 + for item in os.listdir(unit_path): + item_path = os.path.join(unit_path, item) + + if not os.path.isdir(item_path): + continue + + # 直接使用目录名称作为叶片编号 + blade_num = item + + print(f"[DEBUG] 处理机组 {unit_id} 的 {blade_num}") + + for file in os.listdir(item_path): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): + file_path = os.path.join(item_path, file) + capture_time = self.parse_capture_time(file) + + if capture_time: + cursor.execute(""" + INSERT OR IGNORE INTO external_photos ( + project_path, unit_id, blade_number, inspector, + photo_path, capture_time, file_name + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + self.root_dir, unit_id, blade_num, + self.inspector, + file_path, capture_time, file + )) + if cursor.rowcount > 0: + total_photos += 1 + else: + print(f"[DEBUG] 跳过无法解析时间的文件: {file}") + + self.db_conn.commit() + print(f"[DEBUG] 扫描完成,共找到 {total_photos} 张照片") + self.scan_finished.emit(self.root_dir, True, f"目录扫描完成,共找到 {total_photos} 张照片") + except Exception as e: + error_msg = f"扫描目录时出错: {str(e)}" + print(f"[ERROR] {error_msg}") + self.scan_finished.emit(self.root_dir, False, error_msg) + + def parse_capture_time(self, filename): + """从文件名解析拍摄时间""" + # 尝试第一种格式:DJI_YYYYMMDDHHMMSS_xxx_Z.JPG + match = re.search(r"DJI_(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", filename) + if match: + year, month, day, hour, minute, second = match.groups() + try: + return f"{year}-{month}-{day} {hour}:{minute}:{second}" + except: + return None + + # 尝试第二种格式:YYYYMMDD_HHMMSS.jpg + match = re.search(r"(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})", filename) + if match: + year, month, day, hour, minute, second = match.groups() + try: + return f"{year}-{month}-{day} {hour}:{minute}:{second}" + except: + return None + + return None + + +class ExternalPhotoManager(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("风电叶片无人机外部检查照片管理系统") + self.setWindowIcon(QIcon("drone.ico")) + self.setGeometry(100, 100, 1200, 800) + + # 使用独立的外部检查数据库 + self.db_conn = sqlite3.connect("external_inspection.db", + check_same_thread=False) + self.create_db_table() + + # 初始化UI组件 + self.init_ui_components() + + # 加载数据 + self.load_units_to_combo() + + self.show() + + def create_db_table(self): + """创建独立的外部检查数据库表结构""" + cursor = self.db_conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS external_photos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_path TEXT NOT NULL, + unit_id TEXT NOT NULL, + blade_number TEXT NOT NULL, + inspector TEXT NOT NULL DEFAULT '无人机检查', + photo_path TEXT NOT NULL UNIQUE, + capture_time TEXT NOT NULL, + file_name TEXT NOT NULL + ) + """) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_ext_project_path ON external_photos(project_path)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_ext_unit_id ON external_photos(unit_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_ext_blade_number ON external_photos(blade_number)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_ext_capture_time ON external_photos(capture_time)") + self.db_conn.commit() + + def init_ui_components(self): + """初始化无人机外部检查专用UI""" + main_widget = QWidget() + main_layout = QVBoxLayout() + + # 目录选择区域 + dir_layout = QHBoxLayout() + self.dir_label = QLabel("项目目录:") + self.dir_input = QLineEdit() + self.dir_input.setPlaceholderText("请选择无人机外部检查照片根目录") + self.browse_btn = QPushButton("浏览...") + self.browse_btn.clicked.connect(self.browse_directory) + self.scan_btn = QPushButton("扫描目录") + self.scan_btn.clicked.connect(self.start_scan_directory) + + dir_layout.addWidget(self.dir_label) + dir_layout.addWidget(self.dir_input, stretch=1) + dir_layout.addWidget(self.browse_btn) + dir_layout.addWidget(self.scan_btn) + + # 查询条件区域 + query_layout = QHBoxLayout() + + # 机组编号查询 + unit_layout = QVBoxLayout() + unit_layout.addWidget(QLabel("机组编号")) + self.unit_combo = QComboBox() + self.unit_combo.addItem("所有机组", "") + unit_layout.addWidget(self.unit_combo) + + # 检查类型选择 + type_layout = QVBoxLayout() + type_layout.addWidget(QLabel("检查类型")) + + self.type_group = QButtonGroup() + self.all_types_radio = QRadioButton("所有类型") + self.blade_radio = QRadioButton("叶片") + self.tower_radio = QRadioButton("塔筒") + self.unit_radio = QRadioButton("机组图片") + + self.all_types_radio.setChecked(True) + self.type_group.addButton(self.all_types_radio) + self.type_group.addButton(self.blade_radio) + self.type_group.addButton(self.tower_radio) + self.type_group.addButton(self.unit_radio) + + type_layout.addWidget(self.all_types_radio) + type_layout.addWidget(self.blade_radio) + type_layout.addWidget(self.tower_radio) + type_layout.addWidget(self.unit_radio) + + # 日期范围查询 + date_group = QGroupBox("日期范围") + date_layout = QHBoxLayout() + + self.start_date_edit = QDateEdit() + self.start_date_edit.setCalendarPopup(True) + self.start_date_edit.setDisplayFormat("yyyy-MM-dd") + self.start_date_edit.setDate(QDate.currentDate().addDays(-7)) + + self.end_date_edit = QDateEdit() + self.end_date_edit.setCalendarPopup(True) + self.end_date_edit.setDisplayFormat("yyyy-MM-dd") + self.end_date_edit.setDate(QDate.currentDate()) + + date_layout.addWidget(self.start_date_edit) + date_layout.addWidget(QLabel("至")) + date_layout.addWidget(self.end_date_edit) + date_group.setLayout(date_layout) + + # 叶片编号选择框 + self.blade_combo = QComboBox() + self.blade_combo.addItem("所有叶片", "") + self.blade_combo.setEnabled(False) + + # 将叶片编号添加到机组布局中 + unit_layout.addWidget(QLabel("叶片编号")) + unit_layout.addWidget(self.blade_combo) + + query_layout.addLayout(unit_layout) + query_layout.addLayout(type_layout) + query_layout.addWidget(date_group) + + # 查询按钮 + self.query_btn = QPushButton("查询") + self.query_btn.clicked.connect(self.query_photos) + query_layout.addWidget(self.query_btn) + + # 主内容区域 - 使用分割器确保预览区域可见 + splitter = QSplitter(Qt.Horizontal) + + # 结果显示区域 + self.result_table = QTableWidget() + self.result_table.setColumnCount(6) # 初始设置为最大列数 + self.result_table.setHorizontalHeaderLabels(["机组编号", "叶片/塔筒编号", "检查类型", "拍摄时间", "文件名", "路径"]) + self.result_table.setSelectionBehavior(QTableWidget.SelectRows) + self.result_table.setEditTriggers(QTableWidget.NoEditTriggers) + self.result_table.doubleClicked.connect(self.open_selected_photo) + + # 设置列宽策略 + header = self.result_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QHeaderView.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.Stretch) + header.setSectionResizeMode(5, QHeaderView.Stretch) + + # 预览区域 + preview_widget = QWidget() + preview_layout = QVBoxLayout() + preview_layout.addWidget(QLabel("照片预览")) + + self.preview_label = QLabel() + self.preview_label.setAlignment(Qt.AlignCenter) + self.preview_label.setMinimumSize(300, 300) + self.preview_label.setText("照片预览区域") + self.preview_label.setStyleSheet(""" + QLabel { + border: 1px solid gray; + background-color: #f0f0f0; + qproperty-alignment: AlignCenter; + } + """) + + preview_layout.addWidget(self.preview_label) + preview_widget.setLayout(preview_layout) + + # 将表格和预览区域添加到分割器 + splitter.addWidget(self.result_table) + splitter.addWidget(preview_widget) + splitter.setStretchFactor(0, 3) # 表格占3份空间 + splitter.setStretchFactor(1, 1) # 预览占1份空间 + + # 连接信号 + self.type_group.buttonClicked.connect(self.update_blade_combo) + self.unit_combo.currentIndexChanged.connect(self.update_blade_combo) + + # 连接选择变化信号 + self.result_table.selectionModel().selectionChanged.connect(self.update_preview) + + # 状态栏 + self.statusBar().showMessage("就绪") + + # 布局组装 + main_layout.addLayout(dir_layout) + main_layout.addLayout(query_layout) + main_layout.addWidget(splitter, stretch=1) # 使用分割器作为主要内容区域 + + main_widget.setLayout(main_layout) + self.setCentralWidget(main_widget) + + def browse_directory(self): + dir_path = QFileDialog.getExistingDirectory(self, "选择无人机外部检查照片根目录") + if dir_path: + self.dir_input.setText(dir_path) + # 自动加载该目录下的机组 + self.load_units_to_combo(dir_path) + + def update_blade_combo(self): + """更新叶片编号下拉框""" + if self.blade_radio.isChecked(): + unit_id = self.unit_combo.currentData() + self.blade_combo.setEnabled(True) + + cursor = self.db_conn.cursor() + query = """ + SELECT DISTINCT blade_number + FROM external_photos + WHERE unit_id = ? AND blade_number NOT IN ('塔筒', '机组') + AND project_path = ? + ORDER BY blade_number + """ + cursor.execute(query, (unit_id, self.dir_input.text())) + blades = [row[0] for row in cursor.fetchall()] + + self.blade_combo.clear() + self.blade_combo.addItem("所有叶片", "") + for blade in blades: + clean_blade = blade.replace("#", "") + self.blade_combo.addItem(clean_blade, blade) + else: + self.blade_combo.setEnabled(False) + + def start_scan_directory(self): + """启动目录扫描线程""" + root_dir = self.dir_input.text() + if not root_dir or not os.path.exists(root_dir): + QMessageBox.warning(self, "警告", "请选择有效的目录路径") + return + + self.scan_btn.setEnabled(False) + self.statusBar().showMessage("正在扫描目录,请稍候...") + + self.scanner = ExternalDirectoryScanner(root_dir, self.db_conn) + self.scanner.scan_finished.connect(self.on_scan_finished) + self.scanner.start() + + def on_scan_finished(self, root_dir, success, message): + """扫描完成后的回调""" + self.scan_btn.setEnabled(True) + + if success: + self.load_units_to_combo(root_dir) + self.query_photos() + + self.statusBar().showMessage(f"目录扫描完成: {root_dir}", 5000) + QMessageBox.information(self, "完成", message) + else: + error_msg = f"扫描目录时出错:\n\n{message}\n\n请检查:\n1. 目录结构是否符合要求\n2. 是否有访问权限" + QMessageBox.critical(self, "扫描错误", error_msg) + self.statusBar().showMessage(f"扫描失败: {message}", 5000) + + def load_units_to_combo(self, project_path=None): + """加载机组编号到下拉框""" + cursor = self.db_conn.cursor() + + # 如果没有传入project_path,尝试使用当前目录输入框的值 + if project_path is None: + project_path = self.dir_input.text() if self.dir_input.text() else None + + if project_path: + cursor.execute(""" + SELECT DISTINCT unit_id FROM external_photos + WHERE unit_id != '' AND project_path = ? + ORDER BY unit_id + """, (project_path,)) + else: + cursor.execute(""" + SELECT DISTINCT unit_id FROM external_photos + WHERE unit_id != '' + ORDER BY unit_id + """) + + units = [row[0] for row in cursor.fetchall()] + + self.unit_combo.clear() + self.unit_combo.addItem("所有机组", "") + for unit in units: + self.unit_combo.addItem(unit, unit) + + def query_photos(self): + """查询照片数据""" + # 获取查询条件 + unit_id = self.unit_combo.currentData() or None + start_date = self.start_date_edit.date().toString("yyyy-MM-dd") + end_date = self.end_date_edit.date().toString("yyyy-MM-dd") + project_path = self.dir_input.text() # 获取当前扫描目录 + + # 根据检查类型设置表格列 + is_blade = self.blade_radio.isChecked() + + # 设置表格列标题 + if is_blade: + # 叶片模式: 显示叶片编号列 + self.result_table.setColumnCount(6) + headers = ["机组编号", "叶片编号", "检查类型", "拍摄时间", "文件名", "路径"] + else: + # 非叶片模式: 隐藏叶片编号列 + self.result_table.setColumnCount(5) + headers = ["机组编号", "检查类型", "拍摄时间", "文件名", "路径"] + + self.result_table.setHorizontalHeaderLabels(headers) + + # 构建基础查询 + query = """ + SELECT + unit_id, + blade_number, + CASE + WHEN blade_number = '塔筒' THEN '塔筒' + WHEN blade_number = '机组' THEN '机组图片' + ELSE '叶片' + END AS inspection_type, + capture_time, + file_name, + photo_path + FROM external_photos + WHERE date(capture_time) BETWEEN ? AND ? + AND project_path = ? + """ + params = [start_date, end_date, project_path] + + # 添加机组条件 + if unit_id: + query += " AND unit_id = ?" + params.append(unit_id) + + # 添加检查类型条件 + if is_blade: + query += " AND blade_number NOT IN ('塔筒', '机组')" + # 添加叶片编号条件 + blade_num = self.blade_combo.currentData() + if blade_num: + query += " AND blade_number = ?" + params.append(blade_num) + elif self.tower_radio.isChecked(): + query += " AND blade_number = '塔筒'" + elif self.unit_radio.isChecked(): + query += " AND blade_number = '机组'" + + query += " ORDER BY unit_id, blade_number, capture_time" + + # 执行查询 + cursor = self.db_conn.cursor() + try: + cursor.execute(query, params) + results = cursor.fetchall() + + # 处理结果显示 + processed_results = [] + for row in results: + unit, blade, typ, time, name, path = row + if is_blade: + # 叶片模式下显示叶片编号(去除#号) + clean_blade = blade.replace("#", "") + processed_results.append((unit, clean_blade, typ, time, name, path)) + else: + # 非叶片模式下不显示叶片编号 + processed_results.append((unit, typ, time, name, path)) + + self.result_table.setRowCount(len(processed_results)) + for row_idx, row in enumerate(processed_results): + for col_idx, col in enumerate(row): + item = QTableWidgetItem(str(col) if col is not None else "") + self.result_table.setItem(row_idx, col_idx, item) + + self.statusBar().showMessage(f"找到 {len(processed_results)} 条记录", 5000) + + # 如果有结果,自动选择第一行并更新预览 + if processed_results: + self.result_table.selectRow(0) + self.update_preview() + else: + self.preview_label.setText("无照片可预览") + self.preview_label.setPixmap(QPixmap()) + + except sqlite3.Error as e: + QMessageBox.critical(self, "数据库错误", f"查询失败: {str(e)}") + + def open_selected_photo(self, index): + """双击打开选中的照片""" + selected_row = index.row() + + # 根据当前显示模式确定路径列索引 + is_blade = self.blade_radio.isChecked() + path_col = 5 if is_blade else 4 + + photo_path = self.result_table.item(selected_row, path_col).text() + + if not photo_path: + QMessageBox.warning(self, "警告", "未找到文件路径") + return + + photo_path = os.path.normpath(photo_path) + + if not os.path.exists(photo_path): + QMessageBox.warning(self, "文件不存在", f"无法找到文件:\n{photo_path}") + return + + try: + if os.name == 'nt': + os.startfile(photo_path) + elif sys.platform == 'darwin': + subprocess.call(['open', photo_path]) + else: + subprocess.call(['xdg-open', photo_path]) + + self.statusBar().showMessage(f"正在打开: {os.path.basename(photo_path)}", 3000) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开文件:\n路径: {photo_path}\n错误: {str(e)}") + + def update_preview(self): + """更新照片预览""" + selected_rows = self.result_table.selectionModel().selectedRows() + if selected_rows: + selected_row = selected_rows[0].row() + + # 根据当前显示模式确定路径列索引 + is_blade = self.blade_radio.isChecked() + path_col = 5 if is_blade else 4 + + photo_path = self.result_table.item(selected_row, path_col).text() + + if photo_path and os.path.exists(photo_path): + try: + pixmap = QPixmap(photo_path) + if not pixmap.isNull(): + # 保持宽高比缩放图片 + scaled_pixmap = pixmap.scaled( + self.preview_label.width(), + self.preview_label.height(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + self.preview_label.setPixmap(scaled_pixmap) + self.preview_label.setText("") + else: + self.preview_label.setText("无法加载预览") + self.preview_label.setPixmap(QPixmap()) + except Exception as e: + print(f"预览错误: {str(e)}") + self.preview_label.setText("预览错误") + self.preview_label.setPixmap(QPixmap()) + else: + self.preview_label.setText("文件不存在") + self.preview_label.setPixmap(QPixmap()) + + def closeEvent(self, event): + self.db_conn.close() + event.accept() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + manager = ExternalPhotoManager() + sys.exit(app.exec_())