上传文件至 /
This commit is contained in:
commit
e1f06af4ad
|
@ -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_())
|
|
@ -0,0 +1,617 @@
|
|||
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()
|
||||
# 清理当前项目路径下的旧数据
|
||||
print(f"[DEBUG] 清理数据库中的项目路径: {self.root_dir}")
|
||||
cursor.execute("DELETE FROM photos WHERE project_path LIKE ?", (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))]
|
||||
|
||||
print(f"[DEBUG] 找到 {len(unit_dirs)} 个机组目录: {unit_dirs}")
|
||||
|
||||
if not unit_dirs:
|
||||
self.scan_finished.emit(self.root_dir, False, "未找到任何机组目录")
|
||||
return
|
||||
|
||||
# 按机组编号排序
|
||||
def extract_sort_key(unit_id):
|
||||
numbers = re.findall(r'\d+', unit_id)
|
||||
return [int(num) for num in numbers] if numbers else [0]
|
||||
|
||||
unit_dirs_sorted = sorted(unit_dirs, key=extract_sort_key)
|
||||
|
||||
total_photos = 0
|
||||
for unit_dir in unit_dirs_sorted:
|
||||
unit_path = os.path.join(self.root_dir, unit_dir)
|
||||
unit_id = unit_dir
|
||||
|
||||
print(f"[DEBUG] 正在扫描机组: {unit_id}")
|
||||
|
||||
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:
|
||||
print(f"[DEBUG] 跳过无法解析的目录: {item}")
|
||||
continue
|
||||
|
||||
print(f"[DEBUG] 处理叶片: {blade_num}, 检查员: {inspector or '空'}")
|
||||
|
||||
# 检查是否已经处理过这个目录
|
||||
cursor.execute("""
|
||||
SELECT 1 FROM photos
|
||||
WHERE project_path = ? AND unit_id = ? AND blade_number = ?
|
||||
LIMIT 1
|
||||
""", (self.root_dir, unit_id, blade_num))
|
||||
|
||||
if cursor.fetchone():
|
||||
print(f"[DEBUG] 跳过已处理的目录: {item_path}")
|
||||
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("""
|
||||
SELECT 1 FROM photos
|
||||
WHERE photo_path = ?
|
||||
LIMIT 1
|
||||
""", (file_path,))
|
||||
|
||||
if not cursor.fetchone():
|
||||
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 if inspector else None, # 检查人员为空时存NULL
|
||||
file_path, capture_time, file
|
||||
))
|
||||
total_photos += 1
|
||||
else:
|
||||
print(f"[DEBUG] 跳过已存在的照片: {file_path}")
|
||||
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_blade_info(self, dir_name):
|
||||
"""解析叶片目录名称,返回(叶片号, 检查人员)"""
|
||||
dir_name = dir_name.strip()
|
||||
|
||||
# 匹配格式: 数字-姓名(编号) 或 数字-姓名 或 数字-(编号)
|
||||
match = re.match(r'^(\d+)\s*-\s*([^((]*)(?:\s*[((][^))]+[))])?$', dir_name)
|
||||
if match:
|
||||
blade_num = match.group(1).lstrip('0') or '0'
|
||||
inspector = match.group(2).strip()
|
||||
return (blade_num, inspector if inspector else None)
|
||||
|
||||
# 匹配纯数字情况
|
||||
if re.match(r'^\d+$', dir_name):
|
||||
return (dir_name.lstrip('0') or '0', None)
|
||||
|
||||
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 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,
|
||||
UNIQUE(photo_path) -- 添加唯一约束防止重复
|
||||
)
|
||||
""")
|
||||
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 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)
|
||||
self.refresh_btn = QPushButton("强制刷新")
|
||||
self.refresh_btn.clicked.connect(self.force_refresh)
|
||||
|
||||
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)
|
||||
dir_layout.addWidget(self.refresh_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("所有人员", "")
|
||||
self.inspector_combo.addItem("(空)", None)
|
||||
inspector_layout.addWidget(self.inspector_combo)
|
||||
|
||||
# 拍摄日期查询
|
||||
date_layout = QVBoxLayout()
|
||||
date_layout.addWidget(QLabel("拍摄日期"))
|
||||
self.date_edit = QDateEdit()
|
||||
self.date_edit.setCalendarPopup(True)
|
||||
self.date_edit.setDisplayFormat("yyyy-MM-dd")
|
||||
self.date_edit.setDate(QDate.currentDate())
|
||||
date_layout.addWidget(self.date_edit)
|
||||
|
||||
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 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(root_dir)
|
||||
self.load_blades_to_combo()
|
||||
self.load_inspectors_to_combo()
|
||||
|
||||
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 force_refresh(self):
|
||||
"""强制刷新所有数据"""
|
||||
project_path = self.dir_input.text()
|
||||
if project_path and os.path.exists(project_path):
|
||||
self.load_units_to_combo(project_path)
|
||||
else:
|
||||
self.load_units_to_combo()
|
||||
|
||||
self.load_blades_to_combo()
|
||||
self.load_inspectors_to_combo()
|
||||
self.query_photos()
|
||||
self.statusBar().showMessage("已强制刷新所有数据", 3000)
|
||||
|
||||
def load_units_to_combo(self, project_path=None):
|
||||
"""加载机组编号到下拉框,并按数字从小到大排序"""
|
||||
cursor = self.db_conn.cursor()
|
||||
|
||||
if project_path:
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT unit_id FROM photos
|
||||
WHERE unit_id != '' AND project_path = ?
|
||||
""", (project_path,))
|
||||
else:
|
||||
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 != '' OR inspector IS NULL) AND unit_id = ?
|
||||
ORDER BY CASE WHEN inspector IS NULL THEN 1 ELSE 0 END, inspector
|
||||
""", (unit_id,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT inspector FROM photos
|
||||
WHERE inspector != '' OR inspector IS NULL
|
||||
ORDER BY CASE WHEN inspector IS NULL THEN 1 ELSE 0 END, inspector
|
||||
""")
|
||||
|
||||
inspectors = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
self.inspector_combo.clear()
|
||||
self.inspector_combo.addItem("所有人员", "")
|
||||
self.inspector_combo.addItem("(空)", None)
|
||||
for inspector in inspectors:
|
||||
if inspector is not None:
|
||||
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() # 可能是None、空字符串或具体值
|
||||
|
||||
# 处理检查人员查询条件
|
||||
inspector_condition = ""
|
||||
if inspector == "": # 选择了"所有人员"
|
||||
pass
|
||||
elif inspector is None: # 选择了"(空)"
|
||||
inspector_condition = " AND inspector IS NULL"
|
||||
else: # 选择了具体检查人员
|
||||
inspector_condition = " AND inspector = ?"
|
||||
|
||||
selected_date = self.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) = ?
|
||||
"""
|
||||
params = [selected_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_condition:
|
||||
query += inspector_condition
|
||||
if inspector is not None 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()
|
||||
|
||||
if not photo_path:
|
||||
QMessageBox.warning(self, "警告", "未找到文件路径")
|
||||
return
|
||||
|
||||
photo_path = os.path.normpath(photo_path.replace('/', '\\'))
|
||||
|
||||
if photo_path.startswith('\\\\') and 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
|
||||
|
||||
if os.name == 'nt':
|
||||
if photo_path.startswith('\\\\'):
|
||||
photo_path = r'\\' + photo_path[2:]
|
||||
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 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 Exception:
|
||||
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_())
|
|
@ -0,0 +1,715 @@
|
|||
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 DefectPhotoScanner(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()
|
||||
# 清理当前项目路径下的旧数据
|
||||
print(f"[DEBUG] 清理数据库中的项目路径: {self.root_dir}")
|
||||
cursor.execute("DELETE FROM defect_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
|
||||
|
||||
# 收集所有缺陷照片文件
|
||||
defect_files = [f for f in os.listdir(self.root_dir)
|
||||
if os.path.isfile(os.path.join(self.root_dir, f)) and
|
||||
f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
|
||||
|
||||
print(f"[DEBUG] 找到 {len(defect_files)} 个缺陷照片文件")
|
||||
|
||||
if not defect_files:
|
||||
self.scan_finished.emit(self.root_dir, False, "未找到任何缺陷照片文件")
|
||||
return
|
||||
|
||||
total_photos = 0
|
||||
for file in defect_files:
|
||||
file_path = os.path.join(self.root_dir, file)
|
||||
|
||||
# 解析文件名中的信息
|
||||
defect_info = self.parse_defect_info(file)
|
||||
if not defect_info:
|
||||
print(f"[DEBUG] 跳过无法解析的文件: {file}")
|
||||
continue
|
||||
|
||||
# 提取拍摄时间
|
||||
capture_time = self.parse_capture_time(file)
|
||||
if not capture_time:
|
||||
print(f"[DEBUG] 跳过无法解析时间的文件: {file}")
|
||||
continue
|
||||
|
||||
# 插入数据库
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO defect_photos (
|
||||
project_path, unit_id, blade_number, defect_type,
|
||||
defect_description, axial_length, chord_length,
|
||||
photo_path, capture_time, file_name
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
self.root_dir, defect_info['unit_id'], defect_info['blade_number'],
|
||||
defect_info['defect_type'], defect_info['description'],
|
||||
defect_info['axial_length'], defect_info['chord_length'],
|
||||
file_path, capture_time, file
|
||||
))
|
||||
if cursor.rowcount > 0:
|
||||
total_photos += 1
|
||||
|
||||
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_defect_info(self, filename):
|
||||
"""解析缺陷照片文件名中的信息"""
|
||||
# 初始化默认值
|
||||
unit_id = ""
|
||||
blade_number = ""
|
||||
description = ""
|
||||
defect_type = ""
|
||||
axial_length = 0
|
||||
chord_length = 0
|
||||
|
||||
try:
|
||||
# 规则1:提取机组号(从开头到"机组"前的所有字符)
|
||||
parts = re.split(r'机组|_|#| ', filename, maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
unit_id = parts[0].strip()
|
||||
remaining = parts[1]
|
||||
else:
|
||||
return None # 不符合命名规范
|
||||
|
||||
# 规则2:提取叶片编号(从"机组"后到"叶片"前的数字)
|
||||
parts = re.split(r'叶片|_|#| ', remaining, maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
blade_number_match = re.search(r'\d+', parts[0])
|
||||
if blade_number_match:
|
||||
blade_number = blade_number_match.group()
|
||||
remaining = parts[1]
|
||||
else:
|
||||
blade_number = "" # 可能没有明确的叶片编号
|
||||
|
||||
# 优化点1:去除多余的"叶片"字样
|
||||
remaining = re.sub(r'叶片,?叶片', '叶片', remaining)
|
||||
|
||||
# 预先检查复合缺陷类型
|
||||
compound_defects = ["轴向裂纹", "弦向裂纹", "玻璃钢壳体裸露"]
|
||||
defect_pos = -1
|
||||
defect_to_find = None
|
||||
|
||||
# 查找第一个出现的复合缺陷类型
|
||||
for defect in compound_defects:
|
||||
pos = remaining.find(defect)
|
||||
if pos != -1 and (defect_pos == -1 or pos < defect_pos):
|
||||
defect_pos = pos
|
||||
defect_to_find = defect
|
||||
|
||||
# 规则3:提取缺陷描述
|
||||
if defect_to_find:
|
||||
# 如果有复合缺陷类型,描述到缺陷类型开始处
|
||||
description = remaining[:defect_pos].strip(" ,,。.")
|
||||
defect_type = defect_to_find
|
||||
remaining = remaining[defect_pos + len(defect_to_find):]
|
||||
else:
|
||||
# 否则按原有逻辑处理
|
||||
axial_pos = remaining.find("轴向")
|
||||
chord_pos = remaining.find("弦向")
|
||||
|
||||
# 确定描述结束位置
|
||||
if axial_pos != -1 and chord_pos != -1:
|
||||
desc_end = min(axial_pos, chord_pos)
|
||||
elif axial_pos != -1:
|
||||
desc_end = axial_pos
|
||||
elif chord_pos != -1:
|
||||
desc_end = chord_pos
|
||||
else:
|
||||
desc_end = len(remaining)
|
||||
|
||||
description = remaining[:desc_end].strip(" ,,。.")
|
||||
remaining = remaining[desc_end:]
|
||||
|
||||
# 规则4:从描述中提取缺陷类型(优化分类逻辑)
|
||||
defect_type_map = {
|
||||
"玻璃钢壳体裸露": "涂层损伤",
|
||||
"玻璃钢壳体": "涂层损伤",
|
||||
"玻璃钢": "涂层损伤",
|
||||
"龟裂": "涂层损伤",
|
||||
"涂层龟裂": "涂层损伤",
|
||||
"雷击损伤": "雷击损伤",
|
||||
"涂层损伤": "涂层损伤",
|
||||
"涂层裂纹": "涂层损伤",
|
||||
"弦向裂纹": "裂纹",
|
||||
"轴向裂纹": "裂纹",
|
||||
"裂纹": "裂纹"
|
||||
}
|
||||
|
||||
# 按优先级匹配缺陷类型
|
||||
for keyword, dtype in defect_type_map.items():
|
||||
if keyword in description:
|
||||
defect_type = dtype
|
||||
break
|
||||
|
||||
# 规则5:提取缺陷尺寸
|
||||
# 提取轴向尺寸(支持多种格式)
|
||||
axial_match = re.search(r'轴向[::]?\s*(\d+)mm', remaining)
|
||||
if axial_match:
|
||||
axial_length = int(axial_match.group(1))
|
||||
|
||||
# 提取弦向尺寸(支持多种格式)
|
||||
chord_match = re.search(r'弦向[::]?\s*(\d+)mm', remaining)
|
||||
if chord_match:
|
||||
chord_length = int(chord_match.group(1))
|
||||
|
||||
# 处理复合尺寸(如"弦向(上):80mm,弦向(下):50mm")
|
||||
if "弦向(上)" in remaining and "弦向(下)" in remaining:
|
||||
upper_match = re.search(r'弦向(上)[::]?\s*(\d+)mm', remaining)
|
||||
lower_match = re.search(r'弦向(下)[::]?\s*(\d+)mm', remaining)
|
||||
if upper_match and lower_match:
|
||||
chord_length = f"{upper_match.group(1)}/{lower_match.group(1)}"
|
||||
|
||||
return {
|
||||
'unit_id': unit_id,
|
||||
'blade_number': blade_number,
|
||||
'defect_type': defect_type,
|
||||
'description': description,
|
||||
'axial_length': axial_length,
|
||||
'chord_length': chord_length
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"解析文件名出错: {filename}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
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 DefectPhotoManager(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("风电叶片缺陷照片管理系统")
|
||||
self.setWindowIcon(QIcon("defect.ico"))
|
||||
self.setGeometry(100, 100, 1400, 800)
|
||||
|
||||
# 使用独立的缺陷检查数据库
|
||||
self.db_conn = sqlite3.connect("defect_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 defect_photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_path TEXT NOT NULL,
|
||||
unit_id TEXT NOT NULL,
|
||||
blade_number TEXT NOT NULL,
|
||||
defect_type TEXT NOT NULL,
|
||||
defect_description TEXT NOT NULL,
|
||||
axial_length INTEGER NOT NULL DEFAULT 0,
|
||||
chord_length INTEGER NOT NULL DEFAULT 0,
|
||||
photo_path TEXT NOT NULL UNIQUE,
|
||||
capture_time TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_project_path ON defect_photos(project_path)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_unit_id ON defect_photos(unit_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_blade_number ON defect_photos(blade_number)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_capture_time ON defect_photos(capture_time)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_defect_type ON defect_photos(defect_type)")
|
||||
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)
|
||||
|
||||
# 叶片编号选择框
|
||||
blade_layout = QVBoxLayout()
|
||||
blade_layout.addWidget(QLabel("叶片编号"))
|
||||
self.blade_combo = QComboBox()
|
||||
self.blade_combo.addItem("所有叶片", "")
|
||||
blade_layout.addWidget(self.blade_combo)
|
||||
|
||||
# 缺陷类型选择
|
||||
type_layout = QVBoxLayout()
|
||||
type_layout.addWidget(QLabel("缺陷类型"))
|
||||
self.type_combo = QComboBox()
|
||||
self.type_combo.addItem("所有类型", "")
|
||||
self.type_combo.addItem("涂层损伤", "涂层损伤")
|
||||
self.type_combo.addItem("裂纹", "裂纹")
|
||||
self.type_combo.addItem("雷击损伤", "雷击损伤")
|
||||
self.type_combo.addItem("玻璃钢壳体裸露", "玻璃钢壳体裸露")
|
||||
type_layout.addWidget(self.type_combo)
|
||||
|
||||
# 日期范围查询
|
||||
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(-30))
|
||||
|
||||
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)
|
||||
|
||||
# 尺寸筛选
|
||||
size_group = QGroupBox("缺陷尺寸(mm)")
|
||||
size_layout = QHBoxLayout()
|
||||
|
||||
self.min_size_label = QLabel("最小轴向:")
|
||||
self.min_size_input = QLineEdit()
|
||||
self.min_size_input.setPlaceholderText("0")
|
||||
self.max_size_label = QLabel("最大轴向:")
|
||||
self.max_size_input = QLineEdit()
|
||||
self.max_size_input.setPlaceholderText("不限")
|
||||
|
||||
size_layout.addWidget(self.min_size_label)
|
||||
size_layout.addWidget(self.min_size_input)
|
||||
size_layout.addWidget(self.max_size_label)
|
||||
size_layout.addWidget(self.max_size_input)
|
||||
size_group.setLayout(size_layout)
|
||||
|
||||
query_layout.addLayout(unit_layout)
|
||||
query_layout.addLayout(blade_layout)
|
||||
query_layout.addLayout(type_layout)
|
||||
query_layout.addWidget(date_group)
|
||||
query_layout.addWidget(size_group)
|
||||
|
||||
# 查询按钮
|
||||
self.query_btn = QPushButton("查询")
|
||||
self.query_btn.clicked.connect(self.query_defects)
|
||||
query_layout.addWidget(self.query_btn)
|
||||
|
||||
# 主内容区域 - 使用分割器确保预览区域可见
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# 结果显示区域
|
||||
self.result_table = QTableWidget()
|
||||
self.result_table.setColumnCount(9)
|
||||
headers = ["机组编号", "叶片编号", "缺陷类型", "缺陷描述", "轴向(mm)", "弦向(mm)", "拍摄时间", "文件名", "路径"]
|
||||
self.result_table.setHorizontalHeaderLabels(headers)
|
||||
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.Stretch) # 描述
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # 轴向
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # 弦向
|
||||
header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # 时间
|
||||
header.setSectionResizeMode(7, QHeaderView.ResizeToContents) # 文件名
|
||||
header.setSectionResizeMode(8, 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(400, 400)
|
||||
self.preview_label.setText("照片预览区域")
|
||||
self.preview_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 1px solid gray;
|
||||
background-color: #f0f0f0;
|
||||
qproperty-alignment: AlignCenter;
|
||||
}
|
||||
""")
|
||||
|
||||
# 缺陷信息显示
|
||||
self.defect_info_label = QLabel()
|
||||
self.defect_info_label.setWordWrap(True)
|
||||
self.defect_info_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
preview_layout.addWidget(self.preview_label)
|
||||
preview_layout.addWidget(QLabel("缺陷详情:"))
|
||||
preview_layout.addWidget(self.defect_info_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.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)
|
||||
|
||||
def update_blade_combo(self):
|
||||
"""更新叶片编号下拉框"""
|
||||
unit_id = self.unit_combo.currentData()
|
||||
project_path = self.dir_input.text()
|
||||
|
||||
if not project_path:
|
||||
return
|
||||
|
||||
self.blade_combo.clear()
|
||||
self.blade_combo.addItem("所有叶片", "")
|
||||
|
||||
cursor = self.db_conn.cursor()
|
||||
query = """
|
||||
SELECT DISTINCT blade_number
|
||||
FROM defect_photos
|
||||
WHERE project_path = ?
|
||||
"""
|
||||
params = [project_path]
|
||||
|
||||
if unit_id:
|
||||
query += " AND unit_id = ?"
|
||||
params.append(unit_id)
|
||||
|
||||
query += " ORDER BY blade_number"
|
||||
|
||||
cursor.execute(query, params)
|
||||
blades = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
for blade in blades:
|
||||
self.blade_combo.addItem(blade, blade)
|
||||
|
||||
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 = DefectPhotoScanner(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_defects()
|
||||
|
||||
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 defect_photos
|
||||
WHERE unit_id != '' AND project_path = ?
|
||||
ORDER BY unit_id
|
||||
""", (project_path,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT unit_id FROM defect_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_defects(self):
|
||||
"""查询缺陷数据"""
|
||||
# 获取查询条件
|
||||
unit_id = self.unit_combo.currentData() or None
|
||||
blade_number = self.blade_combo.currentData() or None
|
||||
defect_type = self.type_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()
|
||||
|
||||
# 获取尺寸筛选条件
|
||||
min_size = self.min_size_input.text().strip()
|
||||
max_size = self.max_size_input.text().strip()
|
||||
|
||||
try:
|
||||
min_size = int(min_size) if min_size else 0
|
||||
max_size = int(max_size) if max_size else 99999
|
||||
except ValueError:
|
||||
QMessageBox.warning(self, "输入错误", "尺寸必须为整数")
|
||||
return
|
||||
|
||||
if not project_path:
|
||||
QMessageBox.warning(self, "警告", "请先选择缺陷目录")
|
||||
return
|
||||
|
||||
# 构建查询
|
||||
query = """
|
||||
SELECT
|
||||
unit_id, blade_number, defect_type, defect_description,
|
||||
axial_length, chord_length, capture_time, file_name, photo_path
|
||||
FROM defect_photos
|
||||
WHERE date(capture_time) BETWEEN ? AND ?
|
||||
AND project_path = ?
|
||||
AND axial_length BETWEEN ? AND ?
|
||||
"""
|
||||
params = [start_date, end_date, project_path, min_size, max_size]
|
||||
|
||||
# 添加机组条件
|
||||
if unit_id:
|
||||
query += " AND unit_id = ?"
|
||||
params.append(unit_id)
|
||||
|
||||
# 添加叶片编号条件
|
||||
if blade_number:
|
||||
query += " AND blade_number = ?"
|
||||
params.append(blade_number)
|
||||
|
||||
# 添加缺陷类型条件
|
||||
if defect_type:
|
||||
query += " AND defect_type = ?"
|
||||
params.append(defect_type)
|
||||
|
||||
query += " ORDER BY unit_id, blade_number, 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)
|
||||
|
||||
# 如果有结果,自动选择第一行并更新预览
|
||||
if results:
|
||||
self.result_table.selectRow(0)
|
||||
self.update_preview()
|
||||
else:
|
||||
self.preview_label.setText("无照片可预览")
|
||||
self.preview_label.setPixmap(QPixmap())
|
||||
self.defect_info_label.setText("")
|
||||
|
||||
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, 8).text() # 路径在第8列
|
||||
|
||||
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()
|
||||
|
||||
# 获取照片路径(第8列)
|
||||
photo_path = self.result_table.item(selected_row, 8).text()
|
||||
|
||||
# 获取缺陷信息
|
||||
unit_id = self.result_table.item(selected_row, 0).text()
|
||||
blade_number = self.result_table.item(selected_row, 1).text()
|
||||
defect_type = self.result_table.item(selected_row, 2).text()
|
||||
description = self.result_table.item(selected_row, 3).text()
|
||||
axial_length = self.result_table.item(selected_row, 4).text()
|
||||
chord_length = self.result_table.item(selected_row, 5).text()
|
||||
capture_time = self.result_table.item(selected_row, 6).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())
|
||||
|
||||
# 更新缺陷信息
|
||||
info_text = f"""
|
||||
<b>机组编号:</b> {unit_id}<br>
|
||||
<b>叶片编号:</b> {blade_number}<br>
|
||||
<b>缺陷类型:</b> {defect_type}<br>
|
||||
<b>缺陷描述:</b> {description}<br>
|
||||
<b>缺陷尺寸:</b> 轴向 {axial_length}mm × 弦向 {chord_length}mm<br>
|
||||
<b>拍摄时间:</b> {capture_time}
|
||||
"""
|
||||
self.defect_info_label.setText(info_text)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.db_conn.close()
|
||||
event.accept()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
manager = DefectPhotoManager()
|
||||
sys.exit(app.exec_())
|
|
@ -0,0 +1,354 @@
|
|||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import openai
|
||||
from typing import Dict, Any
|
||||
|
||||
class DirectoryAnalyzerApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("风电叶片目录分析工具")
|
||||
self.root.geometry("800x600")
|
||||
|
||||
# 初始化变量
|
||||
self.dir_path = tk.StringVar()
|
||||
self.original_structure = {}
|
||||
self.ai_structure = {}
|
||||
self.tree_items = {} # 用于存储树节点ID
|
||||
|
||||
# 创建界面组件
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 顶部框架 - 目录选择和扫描
|
||||
top_frame = ttk.Frame(self.root, padding="10")
|
||||
top_frame.pack(fill=tk.X)
|
||||
|
||||
ttk.Label(top_frame, text="目录路径:").pack(side=tk.LEFT)
|
||||
|
||||
self.dir_entry = ttk.Entry(top_frame, textvariable=self.dir_path, width=50)
|
||||
self.dir_entry.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
browse_btn = ttk.Button(top_frame, text="浏览...", command=self.browse_directory)
|
||||
browse_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
scan_btn = ttk.Button(top_frame, text="扫描目录", command=self.scan_directory)
|
||||
scan_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 中间框架 - 目录树显示
|
||||
mid_frame = ttk.Frame(self.root, padding="10")
|
||||
mid_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 创建Treeview和滚动条
|
||||
self.tree = ttk.Treeview(mid_frame, columns=("original", "ai"), show="tree")
|
||||
self.tree.heading("#0", text="原始名称")
|
||||
self.tree.heading("original", text="原始名称")
|
||||
self.tree.heading("ai", text="AI建议名称")
|
||||
|
||||
y_scroll = ttk.Scrollbar(mid_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||
self.tree.configure(yscrollcommand=y_scroll.set)
|
||||
|
||||
x_scroll = ttk.Scrollbar(mid_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
|
||||
self.tree.configure(xscrollcommand=x_scroll.set)
|
||||
|
||||
# 布局
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
y_scroll.grid(row=0, column=1, sticky="ns")
|
||||
x_scroll.grid(row=1, column=0, sticky="ew")
|
||||
|
||||
mid_frame.grid_rowconfigure(0, weight=1)
|
||||
mid_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 底部框架 - 操作按钮
|
||||
bottom_frame = ttk.Frame(self.root, padding="10")
|
||||
bottom_frame.pack(fill=tk.X)
|
||||
|
||||
ai_btn = ttk.Button(bottom_frame, text="AI分析", command=self.ai_analyze)
|
||||
ai_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
apply_btn = ttk.Button(bottom_frame, text="应用更改", command=self.apply_changes)
|
||||
apply_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
edit_btn = ttk.Button(bottom_frame, text="更改叶片名", command=self.edit_selected)
|
||||
edit_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 右键菜单
|
||||
self.context_menu = tk.Menu(self.root, tearoff=0)
|
||||
self.context_menu.add_command(label="编辑名称", command=self.edit_selected)
|
||||
|
||||
# 绑定右键事件
|
||||
self.tree.bind("<Button-3>", self.show_context_menu)
|
||||
|
||||
def browse_directory(self):
|
||||
"""打开目录选择对话框"""
|
||||
dir_path = filedialog.askdirectory()
|
||||
if dir_path:
|
||||
self.dir_path.set(dir_path)
|
||||
|
||||
def scan_directory(self):
|
||||
"""扫描选择的目录"""
|
||||
path = self.dir_path.get()
|
||||
if not path:
|
||||
messagebox.showerror("错误", "请先选择目录")
|
||||
return
|
||||
|
||||
try:
|
||||
self.original_structure = self.scan_dir(path)
|
||||
self.display_directory(self.original_structure)
|
||||
messagebox.showinfo("成功", "目录扫描完成")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"扫描目录时出错: {str(e)}")
|
||||
|
||||
def scan_dir(self, path):
|
||||
"""扫描路径结构,递归查找路径并记录路径结构"""
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
|
||||
dir_structure = {}
|
||||
|
||||
for item in os.listdir(path):
|
||||
item_path = os.path.join(path, item)
|
||||
if os.path.isdir(item_path):
|
||||
# 如果是目录,递归扫描
|
||||
dir_structure[item] = self.scan_dir(item_path)
|
||||
|
||||
return dir_structure
|
||||
|
||||
def display_directory(self, dir_data, parent=""):
|
||||
"""在Treeview中显示目录结构"""
|
||||
# 清空现有树
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
|
||||
# 递归插入节点
|
||||
self._insert_nodes("", dir_data)
|
||||
|
||||
def _insert_nodes(self, parent, data):
|
||||
"""递归插入树节点"""
|
||||
for name, children in data.items():
|
||||
if isinstance(children, dict): # 目录节点
|
||||
node_id = self.tree.insert(parent, "end", text=name, values=(name, ""))
|
||||
self._insert_nodes(node_id, children)
|
||||
else: # 文件节点
|
||||
ai_name = ""
|
||||
if name in self.ai_structure:
|
||||
ai_name = self.ai_structure[name]
|
||||
self.tree.insert(parent, "end", text=name, values=(name, ai_name))
|
||||
|
||||
def ai_analyze(self):
|
||||
"""使用AI分析目录结构"""
|
||||
if not self.original_structure:
|
||||
messagebox.showerror("错误", "请先扫描目录")
|
||||
return
|
||||
|
||||
try:
|
||||
# 提取所有文件名(最底层节点)
|
||||
file_names = self.extract_leaf_folders(self.original_structure)
|
||||
|
||||
# 获取AI回复
|
||||
self.ai_structure = self.get_ai_reply(file_names)
|
||||
|
||||
# 更新显示
|
||||
self.display_directory(self.ai_structure)
|
||||
messagebox.showinfo("成功", "AI分析完成")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"AI分析时出错: {str(e)}")
|
||||
|
||||
def extract_leaf_folders(self, dir_data):
|
||||
"""从目录结构中提取所有最末端的文件夹名"""
|
||||
leaf_folders = []
|
||||
|
||||
def _extract(data):
|
||||
for name, children in data.items():
|
||||
if isinstance(children, dict):
|
||||
if not children: # 如果子节点为空字典,说明这是最末端的文件夹
|
||||
leaf_folders.append(name)
|
||||
else:
|
||||
_extract(children)
|
||||
|
||||
_extract(dir_data)
|
||||
print(f'获取最底节点:{leaf_folders}')
|
||||
return leaf_folders
|
||||
def get_ai_reply(self, file_names: list) -> Dict[str, str]:
|
||||
"""获取AI的返回信息,传入文件名列表,让AI返回标准化结构"""
|
||||
api_config = {
|
||||
"api_key": "sk-zbytvldaexbdauavbkclbhdcgrlahojvwfchtiqhyvlvryle",
|
||||
"base_url": "https://api.siliconflow.cn/v1",
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct",
|
||||
"prompt": """请根据风电叶片目录命名规则,将以下叶片名称转换为标准格式。标准格式为:叶片号-检查人员姓名(编号)。编号内不包括字母,如果没有检查人员姓名,则格式为:叶片号-(编号)。并且标准的需要用花括号括起来
|
||||
示例输入1: "1机组-1号叶片(2008-B10-185) 雷俊超"
|
||||
示例输出1: "1-雷俊超(2008-B10-185)" #此处要加花括号
|
||||
|
||||
示例输入2: "1-3号叶片-(2008-B10-165)"
|
||||
示例输出2: "3-(2008-B10-165)" #此处要加花括号
|
||||
|
||||
示例输入3: "2-韩凌柱(2008-B10-180)"
|
||||
示例输出3: "2-韩凌柱(2008-B10-180)" #此处要加花括号
|
||||
|
||||
示例输入4:"1#(2008-B5-289)(王国辉)" #此处‘#’前面是叶片号
|
||||
示例输出4:"1-王国辉(2008-B5-289)" #此处要加花括号
|
||||
|
||||
示例输入5:"8#3#~(刘晋宝)2008-B10-178" #此处8是机组号,3是叶片号
|
||||
示例输出5:"3-刘晋宝(2008-B10-178)" #此处要加花括号
|
||||
|
||||
示例输入6:"A叶片 韩凌柱110030" #此处离”叶片”最近的字母为A,对应叶片号为1(B对应2,C对应3)
|
||||
示例输出6:1-韩凌柱(110030)" #此处要加花括号
|
||||
|
||||
示例输入7:"105# B 叶片检查(刘晋宝)10136G" #此处离”叶片”最近的字母为B,对应叶片号为2(A对应1,C对应3)
|
||||
示例输出7:"2-刘晋宝(10136G)" #此处要加花括号
|
||||
|
||||
示例输入8:"04F叶片A110033(蒋光明)" #04F表示机组,不必考虑在叶片命名内
|
||||
示例输出8:"1-蒋光明(110033) #此处要加花括号
|
||||
|
||||
示例输入9:"2-雷俊超(内部防雷线有雷击痕迹)" #很显然此括号内是一种说明,而不是人名
|
||||
示例输出9:"2-雷俊超" #此处要加花括号
|
||||
现在请转换以下名称: {input}"""
|
||||
}
|
||||
|
||||
try:
|
||||
# 初始化 OpenAI 客户端
|
||||
client = openai.OpenAI(
|
||||
api_key=api_config["api_key"],
|
||||
base_url=api_config["base_url"]
|
||||
)
|
||||
print(f"Client initialized: {client}")
|
||||
except Exception as e:
|
||||
print(f'初始化client失败:{e}')
|
||||
return {name: "客户端初始化失败" for name in file_names}
|
||||
|
||||
result = {}
|
||||
|
||||
for name in file_names:
|
||||
print(f'发送name:{name}')
|
||||
response = client.chat.completions.create(
|
||||
model=api_config["model"],
|
||||
messages=[
|
||||
{"role": "user", "content": api_config["prompt"].format(input=name)}
|
||||
],
|
||||
stream=False # 明确关闭流式传输
|
||||
)
|
||||
ai_reply = response.choices[0].message.content
|
||||
print(f'收到消息:{ai_reply}')
|
||||
# 使用正则表达式查找花括号内的内容
|
||||
import re
|
||||
match = re.search(r'{(.*?)}', ai_reply)
|
||||
if match:
|
||||
content_inside_braces = match.group(1)
|
||||
result[name] = content_inside_braces
|
||||
else:
|
||||
result[name] = ai_reply # 如果没有花括号,使用原始内容
|
||||
print(f'存入:{result[name]}')
|
||||
|
||||
return result
|
||||
|
||||
def apply_changes(self):
|
||||
"""应用AI建议的更改到实际文件"""
|
||||
# 遍历原始结构并应用更改
|
||||
def find_leaf_directories(directory_dict, current_path=None, result=None):
|
||||
if result is None:
|
||||
result = []
|
||||
if current_path is None:
|
||||
current_path = []
|
||||
elif isinstance(current_path, (str, type(self.dir_path))): # 处理 StringVar 或字符串情况
|
||||
current_path = [current_path]
|
||||
|
||||
for name, subdir in directory_dict.items():
|
||||
new_path = current_path.copy() # 创建列表的副本
|
||||
new_path.append(name) # 添加当前目录名
|
||||
if not subdir: # 这是一个最子文件夹
|
||||
result.append({
|
||||
'path': os.path.join(*new_path), # 使用os.path.join来构建路径
|
||||
'name': name
|
||||
})
|
||||
else: # 继续递归查找
|
||||
find_leaf_directories(subdir, new_path, result)
|
||||
|
||||
return result
|
||||
|
||||
# 确保初始路径是列表形式
|
||||
initial_path = [self.dir_path.get()] if hasattr(self.dir_path, 'get') else [str(self.dir_path)]
|
||||
|
||||
result_to_replace = find_leaf_directories(self.original_structure, initial_path)
|
||||
for result in result_to_replace:
|
||||
original_path = result['path'] # 获取原始路径
|
||||
original_name = result['name'] # 获取原始name
|
||||
|
||||
# 从ai_structure中获取要修改的新name值
|
||||
if original_name in self.ai_structure:
|
||||
new_name = self.ai_structure[original_name]
|
||||
|
||||
# 构造新路径
|
||||
dir_name = os.path.dirname(original_path)
|
||||
new_path = os.path.join(dir_name, new_name)
|
||||
|
||||
try:
|
||||
# 确保目标目录不存在
|
||||
if os.path.exists(new_path):
|
||||
print(f"Error: Target already exists - {new_path}")
|
||||
continue
|
||||
|
||||
# 重命名文件夹
|
||||
os.rename(original_path, new_path)
|
||||
print(f"Renamed: {original_path} -> {new_path}")
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Path not found - {original_path}")
|
||||
except Exception as e:
|
||||
print(f"Error renaming {original_path}: {str(e)}")
|
||||
|
||||
|
||||
def edit_selected(self):
|
||||
"""编辑选中的项目名称"""
|
||||
selected_item = self.tree.selection()
|
||||
if not selected_item:
|
||||
messagebox.showerror("错误", "请先选择一个项目")
|
||||
return
|
||||
|
||||
# 获取当前值
|
||||
item_text = self.tree.item(selected_item, "text")
|
||||
item_values = self.tree.item(selected_item, "values")
|
||||
|
||||
# 创建编辑对话框
|
||||
edit_dialog = tk.Toplevel(self.root)
|
||||
edit_dialog.title("编辑名称")
|
||||
edit_dialog.geometry("400x200")
|
||||
|
||||
ttk.Label(edit_dialog, text="原始名称:").pack(pady=5)
|
||||
original_entry = ttk.Entry(edit_dialog)
|
||||
original_entry.insert(0, item_text)
|
||||
original_entry.config(state="readonly")
|
||||
original_entry.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
ttk.Label(edit_dialog, text="AI建议名称:").pack(pady=5)
|
||||
ai_entry = ttk.Entry(edit_dialog)
|
||||
ai_entry.insert(0, item_values[1] if len(item_values) > 1 else "")
|
||||
ai_entry.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
def save_changes():
|
||||
new_name = ai_entry.get()
|
||||
if new_name:
|
||||
# 更新树显示
|
||||
self.tree.item(selected_item, values=(item_text, new_name))
|
||||
|
||||
# 更新AI结构字典
|
||||
if item_text in self.ai_structure:
|
||||
self.ai_structure[item_text] = new_name
|
||||
|
||||
edit_dialog.destroy()
|
||||
|
||||
save_btn = ttk.Button(edit_dialog, text="保存", command=save_changes)
|
||||
save_btn.pack(pady=10)
|
||||
|
||||
def show_context_menu(self, event):
|
||||
"""显示右键上下文菜单"""
|
||||
item = self.tree.identify_row(event.y)
|
||||
if item:
|
||||
self.tree.selection_set(item)
|
||||
self.context_menu.post(event.x_root, event.y_root)
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = DirectoryAnalyzerApp(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import openai
|
||||
from typing import Dict, Any
|
||||
|
||||
class DirectoryAnalyzerApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("风电叶片目录分析工具")
|
||||
self.root.geometry("800x600")
|
||||
|
||||
# 初始化变量
|
||||
self.dir_path = tk.StringVar()
|
||||
self.original_structure = {}
|
||||
self.ai_structure = {}
|
||||
self.tree_items = {} # 用于存储树节点ID
|
||||
|
||||
# 创建界面组件
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 顶部框架 - 目录选择和扫描
|
||||
top_frame = ttk.Frame(self.root, padding="10")
|
||||
top_frame.pack(fill=tk.X)
|
||||
|
||||
ttk.Label(top_frame, text="目录路径:").pack(side=tk.LEFT)
|
||||
|
||||
self.dir_entry = ttk.Entry(top_frame, textvariable=self.dir_path, width=50)
|
||||
self.dir_entry.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
browse_btn = ttk.Button(top_frame, text="浏览...", command=self.browse_directory)
|
||||
browse_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
scan_btn = ttk.Button(top_frame, text="扫描目录", command=self.scan_directory)
|
||||
scan_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 中间框架 - 目录树显示
|
||||
mid_frame = ttk.Frame(self.root, padding="10")
|
||||
mid_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 创建Treeview和滚动条
|
||||
self.tree = ttk.Treeview(mid_frame, columns=("original", "ai"), show="tree")
|
||||
self.tree.heading("#0", text="原始名称")
|
||||
self.tree.heading("original", text="原始名称")
|
||||
self.tree.heading("ai", text="AI建议名称")
|
||||
|
||||
y_scroll = ttk.Scrollbar(mid_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||||
self.tree.configure(yscrollcommand=y_scroll.set)
|
||||
|
||||
x_scroll = ttk.Scrollbar(mid_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
|
||||
self.tree.configure(xscrollcommand=x_scroll.set)
|
||||
|
||||
# 布局
|
||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||
y_scroll.grid(row=0, column=1, sticky="ns")
|
||||
x_scroll.grid(row=1, column=0, sticky="ew")
|
||||
|
||||
mid_frame.grid_rowconfigure(0, weight=1)
|
||||
mid_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 底部框架 - 操作按钮
|
||||
bottom_frame = ttk.Frame(self.root, padding="10")
|
||||
bottom_frame.pack(fill=tk.X)
|
||||
|
||||
ai_btn = ttk.Button(bottom_frame, text="AI分析", command=self.ai_analyze)
|
||||
ai_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
apply_btn = ttk.Button(bottom_frame, text="应用更改", command=self.apply_changes)
|
||||
apply_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
edit_btn = ttk.Button(bottom_frame, text="更改叶片名", command=self.edit_selected)
|
||||
edit_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 右键菜单
|
||||
self.context_menu = tk.Menu(self.root, tearoff=0)
|
||||
self.context_menu.add_command(label="编辑名称", command=self.edit_selected)
|
||||
|
||||
# 绑定右键事件
|
||||
self.tree.bind("<Button-3>", self.show_context_menu)
|
||||
|
||||
def browse_directory(self):
|
||||
"""打开目录选择对话框"""
|
||||
dir_path = filedialog.askdirectory()
|
||||
if dir_path:
|
||||
self.dir_path.set(dir_path)
|
||||
|
||||
def scan_directory(self):
|
||||
"""扫描选择的目录"""
|
||||
path = self.dir_path.get()
|
||||
if not path:
|
||||
messagebox.showerror("错误", "请先选择目录")
|
||||
return
|
||||
|
||||
try:
|
||||
self.original_structure = self.scan_dir(path)
|
||||
self.display_directory(self.original_structure)
|
||||
messagebox.showinfo("成功", "目录扫描完成")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"扫描目录时出错: {str(e)}")
|
||||
|
||||
def scan_dir(self, path):
|
||||
"""扫描路径结构,递归查找路径并记录路径结构"""
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
|
||||
dir_structure = {}
|
||||
|
||||
for item in os.listdir(path):
|
||||
item_path = os.path.join(path, item)
|
||||
if os.path.isdir(item_path):
|
||||
# 如果是目录,递归扫描
|
||||
dir_structure[item] = self.scan_dir(item_path)
|
||||
|
||||
return dir_structure
|
||||
|
||||
def display_directory(self, dir_data, parent=""):
|
||||
"""在Treeview中显示目录结构"""
|
||||
# 清空现有树
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
|
||||
# 递归插入节点
|
||||
self._insert_nodes("", dir_data)
|
||||
|
||||
def _insert_nodes(self, parent, data):
|
||||
"""递归插入树节点"""
|
||||
for name, children in data.items():
|
||||
if isinstance(children, dict): # 目录节点
|
||||
node_id = self.tree.insert(parent, "end", text=name, values=(name, ""))
|
||||
self._insert_nodes(node_id, children)
|
||||
else: # 文件节点
|
||||
ai_name = ""
|
||||
if name in self.ai_structure:
|
||||
ai_name = self.ai_structure[name]
|
||||
self.tree.insert(parent, "end", text=name, values=(name, ai_name))
|
||||
|
||||
def ai_analyze(self):
|
||||
"""使用AI分析目录结构"""
|
||||
if not self.original_structure:
|
||||
messagebox.showerror("错误", "请先扫描目录")
|
||||
return
|
||||
|
||||
try:
|
||||
# 提取所有文件名(最底层节点)
|
||||
file_names = self.extract_leaf_folders(self.original_structure)
|
||||
|
||||
# 获取AI回复
|
||||
self.ai_structure = self.get_ai_reply(file_names)
|
||||
|
||||
# 更新显示
|
||||
self.display_directory(self.ai_structure)
|
||||
messagebox.showinfo("成功", "AI分析完成")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"AI分析时出错: {str(e)}")
|
||||
|
||||
def extract_leaf_folders(self, dir_data):
|
||||
"""从目录结构中提取所有最末端的文件夹名"""
|
||||
leaf_folders = []
|
||||
|
||||
def _extract(data):
|
||||
for name, children in data.items():
|
||||
if isinstance(children, dict):
|
||||
if not children: # 如果子节点为空字典,说明这是最末端的文件夹
|
||||
leaf_folders.append(name)
|
||||
else:
|
||||
_extract(children)
|
||||
|
||||
_extract(dir_data)
|
||||
print(f'获取最底节点:{leaf_folders}')
|
||||
return leaf_folders
|
||||
def get_ai_reply(self, file_names: list) -> Dict[str, str]:
|
||||
"""获取AI的返回信息,传入文件名列表,让AI返回标准化结构"""
|
||||
api_config = {
|
||||
"api_key": "sk-zbytvldaexbdauavbkclbhdcgrlahojvwfchtiqhyvlvryle",
|
||||
"base_url": "https://api.siliconflow.cn/v1",
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct",
|
||||
"prompt": """请根据风电叶片目录命名规则,将以下叶片名称转换为标准格式。标准格式为:叶片号。只需要看第一个字母/数字即可,并且标准的需要用花括号括起来
|
||||
示例输入1: "A#"
|
||||
示例输出1: "1" #此处要加花括号
|
||||
|
||||
示例输入2: "B#"
|
||||
示例输出2: "2" #此处要加花括号
|
||||
|
||||
示例输入3: "C#"
|
||||
示例输出3: "3" #此处要加花括号
|
||||
|
||||
示例输入1: "1#"
|
||||
示例输出1: "1" #此处要加花括号
|
||||
|
||||
示例输入2: "2#"
|
||||
示例输出2: "2" #此处要加花括号
|
||||
|
||||
示例输入3: "3#"
|
||||
示例输出3: "3" #此处要加花括号
|
||||
现在请转换以下名称: {input}"""
|
||||
}
|
||||
|
||||
try:
|
||||
# 初始化 OpenAI 客户端
|
||||
client = openai.OpenAI(
|
||||
api_key=api_config["api_key"],
|
||||
base_url=api_config["base_url"]
|
||||
)
|
||||
print(f"Client initialized: {client}")
|
||||
except Exception as e:
|
||||
print(f'初始化client失败:{e}')
|
||||
return {name: "客户端初始化失败" for name in file_names}
|
||||
|
||||
result = {}
|
||||
|
||||
for name in file_names:
|
||||
print(f'发送name:{name}')
|
||||
response = client.chat.completions.create(
|
||||
model=api_config["model"],
|
||||
messages=[
|
||||
{"role": "user", "content": api_config["prompt"].format(input=name)}
|
||||
],
|
||||
stream=False # 明确关闭流式传输
|
||||
)
|
||||
ai_reply = response.choices[0].message.content
|
||||
print(f'收到消息:{ai_reply}')
|
||||
# 使用正则表达式查找花括号内的内容
|
||||
import re
|
||||
match = re.search(r'{(.*?)}', ai_reply)
|
||||
if match:
|
||||
content_inside_braces = match.group(1)
|
||||
result[name] = content_inside_braces
|
||||
else:
|
||||
result[name] = ai_reply # 如果没有花括号,使用原始内容
|
||||
print(f'存入:{result[name]}')
|
||||
|
||||
return result
|
||||
|
||||
def apply_changes(self):
|
||||
"""应用AI建议的更改到实际文件"""
|
||||
# 遍历原始结构并应用更改
|
||||
def find_leaf_directories(directory_dict, current_path=None, result=None):
|
||||
if result is None:
|
||||
result = []
|
||||
if current_path is None:
|
||||
current_path = []
|
||||
elif isinstance(current_path, (str, type(self.dir_path))): # 处理 StringVar 或字符串情况
|
||||
current_path = [current_path]
|
||||
|
||||
for name, subdir in directory_dict.items():
|
||||
new_path = current_path.copy() # 创建列表的副本
|
||||
new_path.append(name) # 添加当前目录名
|
||||
if not subdir: # 这是一个最子文件夹
|
||||
result.append({
|
||||
'path': os.path.join(*new_path), # 使用os.path.join来构建路径
|
||||
'name': name
|
||||
})
|
||||
else: # 继续递归查找
|
||||
find_leaf_directories(subdir, new_path, result)
|
||||
|
||||
return result
|
||||
|
||||
# 确保初始路径是列表形式
|
||||
initial_path = [self.dir_path.get()] if hasattr(self.dir_path, 'get') else [str(self.dir_path)]
|
||||
|
||||
result_to_replace = find_leaf_directories(self.original_structure, initial_path)
|
||||
for result in result_to_replace:
|
||||
original_path = result['path'] # 获取原始路径
|
||||
original_name = result['name'] # 获取原始name
|
||||
|
||||
# 从ai_structure中获取要修改的新name值
|
||||
if original_name in self.ai_structure:
|
||||
new_name = self.ai_structure[original_name]
|
||||
|
||||
# 构造新路径
|
||||
dir_name = os.path.dirname(original_path)
|
||||
new_path = os.path.join(dir_name, new_name)
|
||||
|
||||
try:
|
||||
# 确保目标目录不存在
|
||||
if os.path.exists(new_path):
|
||||
print(f"Error: Target already exists - {new_path}")
|
||||
continue
|
||||
|
||||
# 重命名文件夹
|
||||
os.rename(original_path, new_path)
|
||||
print(f"Renamed: {original_path} -> {new_path}")
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Path not found - {original_path}")
|
||||
except Exception as e:
|
||||
print(f"Error renaming {original_path}: {str(e)}")
|
||||
|
||||
|
||||
def edit_selected(self):
|
||||
"""编辑选中的项目名称"""
|
||||
selected_item = self.tree.selection()
|
||||
if not selected_item:
|
||||
messagebox.showerror("错误", "请先选择一个项目")
|
||||
return
|
||||
|
||||
# 获取当前值
|
||||
item_text = self.tree.item(selected_item, "text")
|
||||
item_values = self.tree.item(selected_item, "values")
|
||||
|
||||
# 创建编辑对话框
|
||||
edit_dialog = tk.Toplevel(self.root)
|
||||
edit_dialog.title("编辑名称")
|
||||
edit_dialog.geometry("400x200")
|
||||
|
||||
ttk.Label(edit_dialog, text="原始名称:").pack(pady=5)
|
||||
original_entry = ttk.Entry(edit_dialog)
|
||||
original_entry.insert(0, item_text)
|
||||
original_entry.config(state="readonly")
|
||||
original_entry.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
ttk.Label(edit_dialog, text="AI建议名称:").pack(pady=5)
|
||||
ai_entry = ttk.Entry(edit_dialog)
|
||||
ai_entry.insert(0, item_values[1] if len(item_values) > 1 else "")
|
||||
ai_entry.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
def save_changes():
|
||||
new_name = ai_entry.get()
|
||||
if new_name:
|
||||
# 更新树显示
|
||||
self.tree.item(selected_item, values=(item_text, new_name))
|
||||
|
||||
# 更新AI结构字典
|
||||
if item_text in self.ai_structure:
|
||||
self.ai_structure[item_text] = new_name
|
||||
|
||||
edit_dialog.destroy()
|
||||
|
||||
save_btn = ttk.Button(edit_dialog, text="保存", command=save_changes)
|
||||
save_btn.pack(pady=10)
|
||||
|
||||
def show_context_menu(self, event):
|
||||
"""显示右键上下文菜单"""
|
||||
item = self.tree.identify_row(event.y)
|
||||
if item:
|
||||
self.tree.selection_set(item)
|
||||
self.context_menu.post(event.x_root, event.y_root)
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = DirectoryAnalyzerApp(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue