上传文件至 /

This commit is contained in:
ChengQiqian 2025-07-25 18:08:17 +08:00
commit e1f06af4ad
5 changed files with 2579 additions and 0 deletions

548
neibu.py Normal file
View File

@ -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_())

617
neibub_final.py Normal file
View File

@ -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_())

715
quexian.py Normal file
View File

@ -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_())

354
structure_file.py Normal file
View File

@ -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对应2C对应3)
示例输出6:1-韩凌柱110030" #此处要加花括号
示例输入7:"105# B 叶片检查刘晋宝10136G" #此处离”叶片”最近的字母为B对应叶片号为2(A对应1C对应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()

345
structure_file2.py Normal file
View File

@ -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()