249 lines
7.9 KiB
Python
249 lines
7.9 KiB
Python
|
|
"""
|
||
|
|
Excel预览工具
|
||
|
|
- 读取.xlsx/.xls文件
|
||
|
|
- QTableWidget显示表格数据
|
||
|
|
- 多Sheet标签页切换
|
||
|
|
"""
|
||
|
|
import logging
|
||
|
|
from pathlib import Path
|
||
|
|
from PySide6.QtWidgets import (
|
||
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||
|
|
QFrame, QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem,
|
||
|
|
QTabWidget, QHeaderView, QAbstractItemView
|
||
|
|
)
|
||
|
|
from PySide6.QtCore import Qt, QThread, Signal
|
||
|
|
from PySide6.QtGui import QFont, QColor
|
||
|
|
|
||
|
|
from ui.workspace import BaseWorkspace, UploadArea
|
||
|
|
|
||
|
|
try:
|
||
|
|
import pandas as pd
|
||
|
|
HAS_PANDAS = True
|
||
|
|
except ImportError:
|
||
|
|
HAS_PANDAS = False
|
||
|
|
logging.warning("pandas未安装, Excel预览功能不可用")
|
||
|
|
|
||
|
|
try:
|
||
|
|
import openpyxl
|
||
|
|
HAS_OPENPYXL = True
|
||
|
|
except ImportError:
|
||
|
|
HAS_OPENPYXL = False
|
||
|
|
logging.warning("openpyxl未安装, Excel预览功能不可用")
|
||
|
|
|
||
|
|
|
||
|
|
class ExcelLoadWorker(QThread):
|
||
|
|
"""Excel加载线程"""
|
||
|
|
finished = Signal(dict) # {sheet_name: DataFrame}
|
||
|
|
error = Signal(str)
|
||
|
|
|
||
|
|
def __init__(self, file_path: str):
|
||
|
|
super().__init__()
|
||
|
|
self.file_path = file_path
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
try:
|
||
|
|
# 使用pandas读取所有sheet
|
||
|
|
excel_file = pd.ExcelFile(self.file_path)
|
||
|
|
sheets_data = {}
|
||
|
|
|
||
|
|
for sheet_name in excel_file.sheet_names:
|
||
|
|
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
||
|
|
sheets_data[sheet_name] = df
|
||
|
|
|
||
|
|
self.finished.emit(sheets_data)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logging.error(f"加载Excel失败: {e}")
|
||
|
|
self.error.emit(str(e))
|
||
|
|
|
||
|
|
|
||
|
|
class SheetTable(QTableWidget):
|
||
|
|
"""Sheet表格组件"""
|
||
|
|
|
||
|
|
def __init__(self, df: 'pd.DataFrame', parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.setup_table(df)
|
||
|
|
|
||
|
|
def setup_table(self, df: 'pd.DataFrame'):
|
||
|
|
"""设置表格数据"""
|
||
|
|
# 设置行列数
|
||
|
|
self.setRowCount(len(df))
|
||
|
|
self.setColumnCount(len(df.columns))
|
||
|
|
|
||
|
|
# 设置表头
|
||
|
|
headers = [str(col) for col in df.columns]
|
||
|
|
self.setHorizontalHeaderLabels(headers)
|
||
|
|
|
||
|
|
# 填充数据
|
||
|
|
for row_idx, (_, row) in enumerate(df.iterrows()):
|
||
|
|
for col_idx, value in enumerate(row):
|
||
|
|
item = QTableWidgetItem(str(value) if pd.notna(value) else "")
|
||
|
|
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) # 只读
|
||
|
|
self.setItem(row_idx, col_idx, item)
|
||
|
|
|
||
|
|
# 设置样式
|
||
|
|
self.setAlternatingRowColors(True)
|
||
|
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||
|
|
self.horizontalHeader().setStretchLastSection(True)
|
||
|
|
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||
|
|
self.verticalHeader().setDefaultSectionSize(35)
|
||
|
|
|
||
|
|
# 自动调整列宽
|
||
|
|
self.resizeColumnsToContents()
|
||
|
|
|
||
|
|
# 限制最大列宽
|
||
|
|
for col in range(self.columnCount()):
|
||
|
|
if self.columnWidth(col) > 300:
|
||
|
|
self.setColumnWidth(col, 300)
|
||
|
|
|
||
|
|
|
||
|
|
class ExcelPreviewPage(BaseWorkspace):
|
||
|
|
"""Excel预览页面"""
|
||
|
|
|
||
|
|
def __init__(self, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
self.excel_path = None
|
||
|
|
self.sheets_data = {}
|
||
|
|
self.setup_preview_ui()
|
||
|
|
|
||
|
|
def setup_preview_ui(self):
|
||
|
|
"""设置预览UI"""
|
||
|
|
self.history_btn.hide()
|
||
|
|
|
||
|
|
# 上传区域
|
||
|
|
self.upload_area = UploadArea("Excel文件 (*.xlsx *.xls)")
|
||
|
|
self.upload_area.files_dropped.connect(self.on_file_added)
|
||
|
|
self.content_layout.addWidget(self.upload_area)
|
||
|
|
|
||
|
|
# 预览区域
|
||
|
|
self.preview_frame = QFrame()
|
||
|
|
self.preview_frame.setObjectName("card")
|
||
|
|
self.preview_frame.setVisible(False)
|
||
|
|
preview_layout = QVBoxLayout(self.preview_frame)
|
||
|
|
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
|
preview_layout.setSpacing(0)
|
||
|
|
|
||
|
|
# 工具栏
|
||
|
|
toolbar = QWidget()
|
||
|
|
toolbar.setStyleSheet("background: rgba(15, 23, 42, 0.5); border-bottom: 1px solid #334155;")
|
||
|
|
toolbar_layout = QHBoxLayout(toolbar)
|
||
|
|
toolbar_layout.setContentsMargins(16, 12, 16, 12)
|
||
|
|
|
||
|
|
# 文件信息
|
||
|
|
self.file_label = QLabel("")
|
||
|
|
self.file_label.setStyleSheet("color: white; font-weight: 500;")
|
||
|
|
toolbar_layout.addWidget(self.file_label)
|
||
|
|
|
||
|
|
toolbar_layout.addStretch()
|
||
|
|
|
||
|
|
# 统计信息
|
||
|
|
self.stats_label = QLabel("")
|
||
|
|
self.stats_label.setStyleSheet("color: #64748b; font-size: 12px;")
|
||
|
|
toolbar_layout.addWidget(self.stats_label)
|
||
|
|
|
||
|
|
# 重新选择按钮
|
||
|
|
change_btn = QPushButton("📂 更换文件")
|
||
|
|
change_btn.setObjectName("secondary_btn")
|
||
|
|
change_btn.clicked.connect(self.change_file)
|
||
|
|
toolbar_layout.addWidget(change_btn)
|
||
|
|
|
||
|
|
preview_layout.addWidget(toolbar)
|
||
|
|
|
||
|
|
# Sheet标签页
|
||
|
|
self.tab_widget = QTabWidget()
|
||
|
|
self.tab_widget.setStyleSheet("""
|
||
|
|
QTabWidget::pane {
|
||
|
|
border: none;
|
||
|
|
background: transparent;
|
||
|
|
}
|
||
|
|
QTabBar::tab {
|
||
|
|
background: #1e293b;
|
||
|
|
color: #94a3b8;
|
||
|
|
padding: 10px 20px;
|
||
|
|
border: none;
|
||
|
|
border-bottom: 2px solid transparent;
|
||
|
|
}
|
||
|
|
QTabBar::tab:hover {
|
||
|
|
color: #e2e8f0;
|
||
|
|
}
|
||
|
|
QTabBar::tab:selected {
|
||
|
|
color: #fbbf24;
|
||
|
|
border-bottom: 2px solid #fbbf24;
|
||
|
|
}
|
||
|
|
""")
|
||
|
|
preview_layout.addWidget(self.tab_widget, 1)
|
||
|
|
|
||
|
|
self.content_layout.addWidget(self.preview_frame, 1)
|
||
|
|
|
||
|
|
def on_file_added(self, files: list):
|
||
|
|
"""文件添加"""
|
||
|
|
if not files:
|
||
|
|
return
|
||
|
|
|
||
|
|
excel_file = None
|
||
|
|
for f in files:
|
||
|
|
if f.lower().endswith(('.xlsx', '.xls')):
|
||
|
|
excel_file = f
|
||
|
|
break
|
||
|
|
|
||
|
|
if not excel_file:
|
||
|
|
QMessageBox.warning(self, "提示", "请选择Excel文件")
|
||
|
|
return
|
||
|
|
|
||
|
|
self.load_excel(excel_file)
|
||
|
|
|
||
|
|
def load_excel(self, file_path: str):
|
||
|
|
"""加载Excel文件"""
|
||
|
|
if not HAS_PANDAS or not HAS_OPENPYXL:
|
||
|
|
QMessageBox.critical(self, "错误", "pandas或openpyxl未安装,无法预览Excel文件")
|
||
|
|
return
|
||
|
|
|
||
|
|
self.excel_path = file_path
|
||
|
|
self.file_label.setText(f"📊 {Path(file_path).name}")
|
||
|
|
|
||
|
|
# 清空现有标签页
|
||
|
|
self.tab_widget.clear()
|
||
|
|
self.sheets_data.clear()
|
||
|
|
|
||
|
|
# 启动加载线程
|
||
|
|
self.worker = ExcelLoadWorker(file_path)
|
||
|
|
self.worker.finished.connect(self.on_load_finished)
|
||
|
|
self.worker.error.connect(self.on_load_error)
|
||
|
|
self.worker.start()
|
||
|
|
|
||
|
|
logging.info(f"开始加载Excel: {file_path}")
|
||
|
|
|
||
|
|
def on_load_finished(self, sheets_data: dict):
|
||
|
|
"""加载完成"""
|
||
|
|
self.sheets_data = sheets_data
|
||
|
|
self.preview_frame.setVisible(True)
|
||
|
|
self.upload_area.setVisible(False)
|
||
|
|
|
||
|
|
total_rows = 0
|
||
|
|
total_cols = 0
|
||
|
|
|
||
|
|
# 创建标签页
|
||
|
|
for sheet_name, df in sheets_data.items():
|
||
|
|
table = SheetTable(df)
|
||
|
|
self.tab_widget.addTab(table, f"📋 {sheet_name}")
|
||
|
|
total_rows += len(df)
|
||
|
|
total_cols = max(total_cols, len(df.columns))
|
||
|
|
|
||
|
|
self.stats_label.setText(
|
||
|
|
f"{len(sheets_data)} 个工作表 | 共 {total_rows} 行 | {total_cols} 列"
|
||
|
|
)
|
||
|
|
|
||
|
|
logging.info(f"Excel加载完成: {len(sheets_data)} 个工作表")
|
||
|
|
|
||
|
|
def on_load_error(self, error: str):
|
||
|
|
"""加载错误"""
|
||
|
|
QMessageBox.critical(self, "错误", f"加载Excel失败:\n{error}")
|
||
|
|
logging.error(f"加载Excel失败: {error}")
|
||
|
|
|
||
|
|
def change_file(self):
|
||
|
|
"""更换文件"""
|
||
|
|
self.preview_frame.setVisible(False)
|
||
|
|
self.upload_area.setVisible(True)
|
||
|
|
self.upload_area.open_file_dialog()
|
||
|
|
|