first commit
This commit is contained in:
@@ -0,0 +1,676 @@
|
||||
"""
|
||||
主窗口 UI 组件
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import openpyxl
|
||||
|
||||
# ---- 配色 ----
|
||||
C = {
|
||||
"bg": "#f0f2f5",
|
||||
"white": "#ffffff",
|
||||
"text": "#1a1a2e",
|
||||
"text_muted": "#6b7280",
|
||||
"accent": "#4f46e5",
|
||||
"accent_hover": "#4338ca",
|
||||
"accent_light": "#eef2ff",
|
||||
"border": "#e5e7eb",
|
||||
"success": "#059669",
|
||||
}
|
||||
|
||||
# 列宽
|
||||
COL_W = [220, 1, 220, 1, 220, 1, 200]
|
||||
COL_X = [0]
|
||||
for w in COL_W[:-1]:
|
||||
COL_X.append(COL_X[-1] + w)
|
||||
|
||||
ROW_H = 44
|
||||
CELL_PAD = 8
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 自定义下拉组件
|
||||
# ======================================================================
|
||||
class Dropdown(tk.Frame):
|
||||
"""纯自定义下拉选择器 — 不用任何系统 Combobox"""
|
||||
|
||||
def __init__(self, parent, values=None, default="", on_select=None,
|
||||
font=("Microsoft YaHei", 10), width=180, **kw):
|
||||
super().__init__(parent, bg=C["white"], highlightbackground=C["border"],
|
||||
highlightthickness=1, cursor="hand2", **kw)
|
||||
self.values = values or []
|
||||
self._selected = default or ""
|
||||
self._on_select = on_select
|
||||
self._font = font
|
||||
self._popup = None
|
||||
self._hover = False
|
||||
|
||||
# 文字标签
|
||||
self._label = tk.Label(
|
||||
self, text=self._selected or "请选择",
|
||||
font=font, bg=C["white"], fg=C["text"] if self._selected else C["text_muted"],
|
||||
anchor=tk.W, padx=8,
|
||||
)
|
||||
self._label.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# 箭头
|
||||
self._arrow = tk.Label(
|
||||
self, text="▾", font=("Microsoft YaHei", 8),
|
||||
bg=C["white"], fg="#9ca3af", padx=6,
|
||||
)
|
||||
self._arrow.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 事件绑定
|
||||
for w in (self, self._label, self._arrow):
|
||||
w.bind("<Button-1>", self._toggle)
|
||||
w.bind("<Enter>", self._on_enter)
|
||||
w.bind("<Leave>", self._on_leave)
|
||||
|
||||
def _on_enter(self, _):
|
||||
self._hover = True
|
||||
self.configure(highlightbackground=C["accent"])
|
||||
self._label.configure(bg="#f9fafb")
|
||||
self._arrow.configure(bg="#f9fafb", fg=C["accent"])
|
||||
|
||||
def _on_leave(self, _):
|
||||
self._hover = False
|
||||
if not self._popup:
|
||||
self.configure(highlightbackground=C["border"])
|
||||
self._label.configure(bg=C["white"])
|
||||
self._arrow.configure(bg=C["white"], fg="#9ca3af")
|
||||
|
||||
def _toggle(self, event=None):
|
||||
if self._popup:
|
||||
self._close()
|
||||
else:
|
||||
self._open()
|
||||
|
||||
def _open(self):
|
||||
self.configure(highlightbackground=C["accent"])
|
||||
self._label.configure(bg="#f9fafb")
|
||||
self._arrow.configure(bg="#f9fafb", fg=C["accent"])
|
||||
|
||||
self._popup = tk.Toplevel(self)
|
||||
self._popup.overrideredirect(True)
|
||||
self._popup.configure(bg=C["white"])
|
||||
|
||||
# 计算位置
|
||||
x = self.winfo_rootx()
|
||||
y = self.winfo_rooty() + self.winfo_height() + 1
|
||||
w = self.winfo_width()
|
||||
item_h = 32
|
||||
max_h = min(len(self.values) * item_h, 240)
|
||||
list_h = max(item_h, max_h)
|
||||
|
||||
self._popup.geometry(f"{w}x{list_h}+{x}+{y}")
|
||||
|
||||
# 外边框
|
||||
border = tk.Frame(self._popup, bg=C["border"])
|
||||
border.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 列表
|
||||
self._listbox = tk.Listbox(
|
||||
border, font=self._font,
|
||||
bg=C["white"], fg=C["text"],
|
||||
selectbackground=C["accent_light"],
|
||||
selectforeground=C["accent"],
|
||||
selectborderwidth=0,
|
||||
activestyle="none",
|
||||
highlightthickness=0,
|
||||
borderwidth=0,
|
||||
relief=tk.FLAT,
|
||||
height=len(self.values),
|
||||
)
|
||||
self._listbox.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)
|
||||
|
||||
for i, v in enumerate(self.values):
|
||||
self._listbox.insert(tk.END, f" {v}")
|
||||
if v == self._selected:
|
||||
self._listbox.selection_set(i)
|
||||
self._listbox.see(i)
|
||||
|
||||
self._listbox.bind("<ButtonRelease-1>", self._on_pick)
|
||||
self._listbox.bind("<Return>", self._on_pick)
|
||||
self._listbox.bind("<Escape>", lambda e: self._close())
|
||||
self._listbox.focus_set()
|
||||
|
||||
# 点击外部关闭
|
||||
self._popup.bind("<FocusOut>", lambda e: self.after(100, self._check_focus))
|
||||
|
||||
# 绑定全局点击关闭(鼠标点击 popup 外)
|
||||
self._grab_bind = self.winfo_toplevel().bind(
|
||||
"<Button-1>", lambda e: self._on_global_click(e), add="+"
|
||||
)
|
||||
|
||||
def _on_global_click(self, event):
|
||||
if self._popup:
|
||||
px, py = event.x_root, event.y_root
|
||||
px2 = self._popup.winfo_rootx()
|
||||
py2 = self._popup.winfo_rooty()
|
||||
pw = self._popup.winfo_width()
|
||||
ph = self._popup.winfo_height()
|
||||
if not (px2 <= px <= px2 + pw and py2 <= py <= py2 + ph):
|
||||
self._close()
|
||||
|
||||
def _check_focus(self):
|
||||
if self._popup and not self._popup.focus_displayof():
|
||||
self._close()
|
||||
|
||||
def _close(self, *_):
|
||||
if self._popup:
|
||||
self._popup.destroy()
|
||||
self._popup = None
|
||||
self._listbox = None
|
||||
self.configure(highlightbackground=C["border"])
|
||||
self._label.configure(bg=C["white"])
|
||||
self._arrow.configure(bg=C["white"], fg="#9ca3af")
|
||||
# 移除全局绑定
|
||||
try:
|
||||
self.winfo_toplevel().unbind("<Button-1>", self._grab_bind)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_pick(self, event):
|
||||
sel = self._listbox.curselection()
|
||||
if sel:
|
||||
idx = sel[0]
|
||||
self._selected = self.values[idx]
|
||||
self._label.configure(text=self._selected, fg=C["text"])
|
||||
if self._on_select:
|
||||
self._on_select(self._selected)
|
||||
self._close()
|
||||
|
||||
# ── 公开 API ──
|
||||
def get(self):
|
||||
return self._selected
|
||||
|
||||
def set_values(self, values, default=None):
|
||||
self.values = values
|
||||
if default is not None:
|
||||
self._selected = default
|
||||
self._label.configure(text=default, fg=C["text"] if default else C["text_muted"])
|
||||
# 如果已打开则重建
|
||||
if self._popup:
|
||||
self._close()
|
||||
self._open()
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 主窗口
|
||||
# ======================================================================
|
||||
class MainWindow:
|
||||
"""应用程序主窗口"""
|
||||
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.configure(bg=C["bg"])
|
||||
self._files_data = []
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
# ── 选择卡片 ──
|
||||
card = tk.Frame(self.root, bg=C["white"])
|
||||
card.pack(fill=tk.X, padx=24, pady=(20, 0))
|
||||
|
||||
card_border = tk.Frame(card, bg=C["white"],
|
||||
highlightbackground=C["border"],
|
||||
highlightthickness=1)
|
||||
card_border.pack(fill=tk.X)
|
||||
|
||||
tk.Label(
|
||||
card_border, text="📂 选择 Excel 文件",
|
||||
font=("Microsoft YaHei", 12, "bold"),
|
||||
bg=C["white"], fg=C["text"], anchor=tk.W,
|
||||
).pack(fill=tk.X, padx=20, pady=(16, 10))
|
||||
|
||||
input_row = tk.Frame(card_border, bg=C["white"])
|
||||
input_row.pack(fill=tk.X, padx=20, pady=(0, 10))
|
||||
|
||||
entry_border = tk.Frame(
|
||||
input_row, bg=C["white"],
|
||||
highlightbackground=C["border"], highlightthickness=1,
|
||||
)
|
||||
entry_border.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
|
||||
self._path_entry = tk.Entry(
|
||||
entry_border,
|
||||
font=("Microsoft YaHei", 10),
|
||||
bg="#f9fafb", fg=C["text_muted"],
|
||||
relief=tk.FLAT, borderwidth=0,
|
||||
readonlybackground="#f9fafb",
|
||||
)
|
||||
self._path_entry.pack(fill=tk.X, padx=10, pady=8)
|
||||
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
|
||||
self._path_entry.configure(state="readonly")
|
||||
|
||||
self._browse_btn = tk.Button(
|
||||
input_row, text="浏览...",
|
||||
font=("Microsoft YaHei", 10, "bold"),
|
||||
bg=C["accent"], fg="white", relief=tk.FLAT,
|
||||
activebackground=C["accent_hover"], activeforeground="white",
|
||||
cursor="hand2", padx=20, pady=7, borderwidth=0,
|
||||
command=self._on_select_files,
|
||||
)
|
||||
self._browse_btn.pack(side=tk.LEFT, padx=(8, 0))
|
||||
|
||||
status_bar = tk.Frame(card_border, bg="#f9fafb")
|
||||
status_bar.pack(fill=tk.X)
|
||||
|
||||
self._status_label = tk.Label(
|
||||
status_bar, text=" 未选择任何文件",
|
||||
font=("Microsoft YaHei", 9),
|
||||
bg="#f9fafb", fg=C["text_muted"], anchor=tk.W,
|
||||
)
|
||||
self._status_label.pack(side=tk.LEFT, padx=16, pady=(6, 6))
|
||||
|
||||
# ── 表格区域 ──
|
||||
self._table_area = tk.Frame(self.root, bg=C["bg"])
|
||||
self._table_area.pack(fill=tk.BOTH, expand=True, padx=24, pady=(12, 0))
|
||||
|
||||
# ── 日志面板(底部固定高度,内部独立滚动) ──
|
||||
self._log_frame = tk.Frame(self.root, bg="white", height=130)
|
||||
self._log_frame.pack(fill=tk.X, padx=24, pady=(8, 8))
|
||||
self._log_frame.pack_propagate(False)
|
||||
|
||||
log_header = tk.Frame(self._log_frame, bg="white")
|
||||
log_header.pack(fill=tk.X)
|
||||
tk.Label(log_header, text="📋 处理日志",
|
||||
font=("Microsoft YaHei", 9, "bold"),
|
||||
bg="white", fg=C["text"], anchor=tk.W,
|
||||
).pack(side=tk.LEFT, padx=10, pady=(6, 2))
|
||||
|
||||
log_text_frame = tk.Frame(self._log_frame, bg="white")
|
||||
log_text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 8))
|
||||
|
||||
self._log_text = tk.Text(
|
||||
log_text_frame,
|
||||
font=("Consolas", 9),
|
||||
bg="#f9fafb", fg=C["text"],
|
||||
wrap=tk.WORD, state=tk.DISABLED,
|
||||
relief=tk.FLAT, borderwidth=0,
|
||||
height=6,
|
||||
)
|
||||
log_scrollbar = tk.Scrollbar(log_text_frame, orient=tk.VERTICAL,
|
||||
command=self._log_text.yview)
|
||||
self._log_text.configure(yscrollcommand=log_scrollbar.set)
|
||||
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self._log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
self._show_empty()
|
||||
|
||||
# ==================================================================
|
||||
# 空状态
|
||||
# ==================================================================
|
||||
def _show_empty(self):
|
||||
for w in self._table_area.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
drop = tk.Frame(self._table_area, bg="#fafafe",
|
||||
highlightbackground="#c7d2fe",
|
||||
highlightthickness=2)
|
||||
drop.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
inner = tk.Frame(drop, bg="#fafafe")
|
||||
inner.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
||||
|
||||
tk.Label(inner, text="📂", font=("Segoe UI Emoji", 40),
|
||||
bg="#fafafe", fg="#c7d2fe").pack(pady=(0, 4))
|
||||
tk.Label(inner, text="点击上方「浏览...」按钮选择文件",
|
||||
font=("Microsoft YaHei", 13, "bold"),
|
||||
bg="#fafafe", fg=C["text_muted"]).pack()
|
||||
tk.Label(inner, text="支持 .xlsx、.xls 格式,可同时选择多个文件",
|
||||
font=("Microsoft YaHei", 9),
|
||||
bg="#fafafe", fg="#c0c4cc").pack(pady=(2, 0))
|
||||
|
||||
# ==================================================================
|
||||
# 读取 xlsx 信息(用内置 zipfile,避免 openpyxl 打包问题)
|
||||
# ==================================================================
|
||||
def _read_xlsx_info(self, filepath):
|
||||
import pandas as pd
|
||||
sheet_names = []
|
||||
columns = []
|
||||
for engine in ["openpyxl", "xlrd"]:
|
||||
try:
|
||||
xls = pd.ExcelFile(filepath, engine=engine)
|
||||
sheet_names = xls.sheet_names
|
||||
if sheet_names:
|
||||
df = pd.read_excel(xls, sheet_name=sheet_names[0], nrows=1)
|
||||
columns = [str(c) for c in df.columns.tolist()]
|
||||
xls.close()
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
return sheet_names, columns
|
||||
|
||||
# ==================================================================
|
||||
# 选择文件
|
||||
# ==================================================================
|
||||
def _on_select_files(self):
|
||||
paths = filedialog.askopenfilenames(
|
||||
title="选择 Excel 文件",
|
||||
filetypes=[
|
||||
("Excel 文件", "*.xlsx *.xls"),
|
||||
("Excel 2007+", "*.xlsx"),
|
||||
("Excel 97-2003", "*.xls"),
|
||||
],
|
||||
)
|
||||
if not paths:
|
||||
return
|
||||
|
||||
for p in paths:
|
||||
if any(d["path"] == p for d in self._files_data):
|
||||
continue
|
||||
try:
|
||||
sheet_names, columns = self._read_xlsx_info(p)
|
||||
first_sheet = sheet_names[0] if sheet_names else None
|
||||
self._files_data.append({
|
||||
"path": p,
|
||||
"filename": os.path.basename(p),
|
||||
"sheet_names": sheet_names,
|
||||
"columns": columns,
|
||||
"selected_sheet": first_sheet,
|
||||
"selected_col": columns[0] if columns else None,
|
||||
"parse_mode": "横向排列",
|
||||
})
|
||||
except Exception as e:
|
||||
messagebox.showerror("读取失败", f"无法读取文件:\n{os.path.basename(p)}\n\n{e}")
|
||||
continue
|
||||
|
||||
n = len(self._files_data)
|
||||
if n == 0:
|
||||
self._path_entry.configure(state="normal")
|
||||
self._path_entry.delete(0, tk.END)
|
||||
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
|
||||
self._path_entry.configure(state="readonly", fg=C["text_muted"])
|
||||
self._status_label.configure(text=" 未选择任何文件")
|
||||
elif n == 1:
|
||||
self._path_entry.configure(state="normal")
|
||||
self._path_entry.delete(0, tk.END)
|
||||
self._path_entry.insert(0, self._files_data[0]["path"])
|
||||
self._path_entry.configure(state="readonly", fg=C["text"])
|
||||
self._status_label.configure(
|
||||
text=f" 已选择 1 个文件 | 点击「浏览...」可继续添加",
|
||||
fg=C["success"],
|
||||
)
|
||||
else:
|
||||
self._path_entry.configure(state="normal")
|
||||
self._path_entry.delete(0, tk.END)
|
||||
self._path_entry.insert(0, f"已选择 {n} 个文件")
|
||||
self._path_entry.configure(state="readonly", fg=C["text"])
|
||||
self._status_label.configure(
|
||||
text=f" 已选择 {n} 个文件 | 点击「浏览...」可继续添加",
|
||||
fg=C["success"],
|
||||
)
|
||||
|
||||
if self._files_data:
|
||||
self._build_table()
|
||||
else:
|
||||
self._show_empty()
|
||||
|
||||
# ==================================================================
|
||||
# 构建表格
|
||||
# ==================================================================
|
||||
def _build_table(self):
|
||||
for w in self._table_area.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
self._table_frame = tk.Frame(self._table_area, bg=C["bg"])
|
||||
self._table_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self._draw_header()
|
||||
for fi, fd in enumerate(self._files_data):
|
||||
self._draw_row(fi, fd)
|
||||
|
||||
# ── 确认按钮 ──
|
||||
btn_bar = tk.Frame(self._table_frame, bg=C["bg"])
|
||||
btn_bar.pack(fill=tk.X, pady=(16, 0))
|
||||
|
||||
self._confirm_btn = tk.Button(
|
||||
btn_bar, text="✅ 开始解析",
|
||||
font=("Microsoft YaHei", 12, "bold"),
|
||||
bg=C["accent"], fg="white", relief=tk.FLAT,
|
||||
activebackground=C["accent_hover"], activeforeground="white",
|
||||
cursor="hand2", padx=28, pady=10, borderwidth=0,
|
||||
command=self._on_confirm,
|
||||
)
|
||||
self._confirm_btn.pack(side=tk.RIGHT)
|
||||
|
||||
self._progress_label = tk.Label(
|
||||
btn_bar, text="",
|
||||
font=("Microsoft YaHei", 9),
|
||||
bg=C["bg"], fg=C["text_muted"], anchor=tk.E,
|
||||
)
|
||||
self._progress_label.pack(side=tk.RIGHT, padx=(0, 12))
|
||||
|
||||
def _draw_header(self):
|
||||
hdr = tk.Frame(self._table_frame, bg=C["white"], height=38)
|
||||
hdr.pack(fill=tk.X)
|
||||
hdr.pack_propagate(False)
|
||||
|
||||
headers = ["文件名", "工作表", "解析列", "解析方式"]
|
||||
for i, title in enumerate(headers):
|
||||
col_idx = i * 2
|
||||
x, w = COL_X[col_idx], COL_W[col_idx]
|
||||
tk.Label(
|
||||
hdr, text=title, font=("Microsoft YaHei", 10, "bold"),
|
||||
bg=C["white"], fg=C["text"], anchor=tk.W,
|
||||
).place(x=x + CELL_PAD, y=0, width=w - CELL_PAD * 2, height=38)
|
||||
if i < 3:
|
||||
tk.Frame(hdr, bg=C["border"], width=1).place(
|
||||
x=COL_X[col_idx + 1], y=6, height=26
|
||||
)
|
||||
|
||||
tk.Frame(self._table_frame, bg=C["border"], height=1).pack(fill=tk.X)
|
||||
|
||||
def _draw_row(self, fi: int, fd: dict):
|
||||
row = tk.Frame(self._table_frame, bg=C["white"], height=ROW_H)
|
||||
row.pack(fill=tk.X)
|
||||
row.pack_propagate(False)
|
||||
|
||||
# ── 文件名 ──
|
||||
x, w = COL_X[0], COL_W[0]
|
||||
tk.Label(
|
||||
row, text=f"📄 {fd['filename']}",
|
||||
font=("Microsoft YaHei", 10), bg=C["white"], fg=C["text"], anchor=tk.W,
|
||||
).place(x=x + CELL_PAD, y=0, width=w - CELL_PAD * 2, height=ROW_H)
|
||||
|
||||
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[1], y=6, height=ROW_H - 12)
|
||||
|
||||
# ── 工作表(自定义 Dropdown) ──
|
||||
x, w = COL_X[2], COL_W[2]
|
||||
dd_sheet = Dropdown(
|
||||
row,
|
||||
values=fd["sheet_names"],
|
||||
default=fd["selected_sheet"] or "",
|
||||
on_select=lambda val, i=fi: self._on_sheet_change(i, val),
|
||||
)
|
||||
dd_sheet.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
|
||||
fd["sheet_dd"] = dd_sheet
|
||||
|
||||
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[3], y=6, height=ROW_H - 12)
|
||||
|
||||
# ── 解析列(自定义 Dropdown) ──
|
||||
x, w = COL_X[4], COL_W[4]
|
||||
dd_col = Dropdown(
|
||||
row,
|
||||
values=fd["columns"],
|
||||
default=fd["selected_col"] or "",
|
||||
on_select=lambda val, i=fi: self._on_col_change(i, val),
|
||||
)
|
||||
dd_col.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
|
||||
fd["col_dd"] = dd_col
|
||||
|
||||
tk.Frame(row, bg=C["border"], width=1).place(x=COL_X[5], y=6, height=ROW_H - 12)
|
||||
|
||||
# ── 解析方式(自定义 Dropdown) ──
|
||||
x, w = COL_X[6], COL_W[6]
|
||||
dd_mode = Dropdown(
|
||||
row,
|
||||
values=["横向排列", "纵向排序"],
|
||||
default=fd["parse_mode"],
|
||||
on_select=lambda val, i=fi: self._on_mode_change(i, val),
|
||||
)
|
||||
dd_mode.place(x=x + CELL_PAD, y=6, width=w - CELL_PAD * 2, height=ROW_H - 12)
|
||||
fd["mode_dd"] = dd_mode
|
||||
|
||||
fd["row_frame"] = row
|
||||
tk.Frame(self._table_frame, bg=C["border"], height=1).pack(fill=tk.X)
|
||||
|
||||
# ==================================================================
|
||||
# 切换工作表 → 更新解析列
|
||||
# ==================================================================
|
||||
def _on_sheet_change(self, idx: int, new_sheet: str):
|
||||
fd = self._files_data[idx]
|
||||
fd["selected_sheet"] = new_sheet
|
||||
|
||||
try:
|
||||
import pandas as pd
|
||||
for engine in ["openpyxl", "xlrd"]:
|
||||
try:
|
||||
df = pd.read_excel(fd["path"], sheet_name=new_sheet, nrows=1, engine=engine)
|
||||
columns = [str(c) for c in df.columns.tolist()]
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
fd["columns"] = columns
|
||||
first_col = columns[0] if columns else None
|
||||
fd["selected_col"] = first_col
|
||||
fd["col_dd"].set_values(columns, default=first_col)
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"读取工作表失败: {e}")
|
||||
|
||||
# ==================================================================
|
||||
# 下拉选择回调
|
||||
# ==================================================================
|
||||
def _on_col_change(self, idx: int, val: str):
|
||||
self._files_data[idx]["selected_col"] = val
|
||||
|
||||
def _on_mode_change(self, idx: int, val: str):
|
||||
self._files_data[idx]["parse_mode"] = val
|
||||
|
||||
# ==================================================================
|
||||
# 日志输出
|
||||
# ==================================================================
|
||||
def _log_write(self, text: str):
|
||||
"""向底部日志面板追加一行"""
|
||||
self._log_text.configure(state=tk.NORMAL)
|
||||
self._log_text.insert(tk.END, text + "\n")
|
||||
self._log_text.see(tk.END)
|
||||
self._log_text.configure(state=tk.DISABLED)
|
||||
|
||||
def _log_clear(self):
|
||||
"""清空日志面板"""
|
||||
self._log_text.configure(state=tk.NORMAL)
|
||||
self._log_text.delete("1.0", tk.END)
|
||||
self._log_text.configure(state=tk.DISABLED)
|
||||
|
||||
# ==================================================================
|
||||
# 确认解析
|
||||
# ==================================================================
|
||||
def _on_confirm(self):
|
||||
if not self._files_data:
|
||||
return
|
||||
|
||||
self._confirm_btn.configure(state=tk.DISABLED, text="⏳ 解析中...")
|
||||
self._log_clear()
|
||||
self._log_write("🚀 开始解析...")
|
||||
|
||||
# 捕获 print 输出到日志面板
|
||||
class _LogWriter:
|
||||
def __init__(self, root, log_func):
|
||||
self._root = root
|
||||
self._log = log_func
|
||||
self._buf = ""
|
||||
def write(self, s):
|
||||
self._buf += s
|
||||
if "\n" in self._buf:
|
||||
lines = self._buf.split("\n")
|
||||
self._buf = lines[-1]
|
||||
for line in lines[:-1]:
|
||||
stripped = line.strip()
|
||||
if stripped:
|
||||
self._root.after(0, lambda t=stripped: self._log(t))
|
||||
def flush(self):
|
||||
if self._buf.strip():
|
||||
self._root.after(0, lambda t=self._buf.strip(): self._log(t))
|
||||
self._buf = ""
|
||||
|
||||
def _run():
|
||||
import sys
|
||||
from app import finance_parse as fp
|
||||
total = len(self._files_data)
|
||||
success = 0
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
log_writer = _LogWriter(self.root, self._log_write)
|
||||
sys.stdout = log_writer
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
try:
|
||||
for i, fd in enumerate(self._files_data):
|
||||
fname = fd["filename"]
|
||||
self.root.after(0, lambda n=i+1, fn=fname:
|
||||
self._log_write(f"⏳ [{n}/{total}] 正在处理: {fn}"))
|
||||
try:
|
||||
save_path, amount = fp.parse_finance_data(
|
||||
file_path=fd["path"],
|
||||
target_index=fd["col_dd"].get(),
|
||||
is_horizontal=(fd["mode_dd"].get() == "横向排列"),
|
||||
sheet_name=fd["sheet_dd"].get(),
|
||||
)
|
||||
success += 1
|
||||
fd["result"] = save_path
|
||||
fd["amount"] = amount
|
||||
self.root.after(0, lambda fn=fname, sp=save_path, amt=amount:
|
||||
self._log_write(f"✅ [{success}/{total}] 完成: {fn} 共 {amt} 条"))
|
||||
except Exception as e:
|
||||
fd["error"] = str(e)
|
||||
self.root.after(0, lambda fn=fname, err=str(e):
|
||||
self._log_write(f"❌ [{i+1}/{total}] 失败: {fn} — {err}"))
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
self.root.after(0, lambda: self._on_parse_done(success, total))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _on_parse_done(self, success: int, total: int):
|
||||
self._confirm_btn.configure(state=tk.NORMAL, text="✅ 开始解析")
|
||||
|
||||
# 从列表中移除已成功解析的文件,保留失败的
|
||||
self._files_data = [fd for fd in self._files_data if "error" in fd]
|
||||
|
||||
if success == total:
|
||||
self._log_write(f"🎉 全部 {total} 个文件解析完成!结果已保存到原文件所在目录。")
|
||||
messagebox.showinfo("解析完成",
|
||||
f"全部 {total} 个文件解析完成!\n\n结果已保存到原文件所在目录。")
|
||||
else:
|
||||
failed_count = len(self._files_data)
|
||||
self._log_write(f"⚠️ 完成 {success}/{total},有 {failed_count} 个文件失败。")
|
||||
errors = []
|
||||
for fd in self._files_data:
|
||||
if "error" in fd:
|
||||
errors.append(f"• {fd['filename']}: {fd['error']}")
|
||||
msg = f"完成 {success}/{total}\n\n失败:\n" + "\n".join(errors[:5])
|
||||
messagebox.showwarning("解析完成(部分失败)", msg)
|
||||
|
||||
# 刷新 UI:清除成功项后重建表格或显示空状态
|
||||
if self._files_data:
|
||||
self._build_table()
|
||||
n = len(self._files_data)
|
||||
self._status_label.configure(
|
||||
text=f" 剩余 {n} 个失败文件 | 可调整配置后重新解析",
|
||||
fg="#dc2626",
|
||||
)
|
||||
else:
|
||||
self._show_empty()
|
||||
self._path_entry.configure(state="normal")
|
||||
self._path_entry.delete(0, tk.END)
|
||||
self._path_entry.insert(0, "请选择 Excel 文件(.xlsx / .xls,可多选)")
|
||||
self._path_entry.configure(state="readonly", fg=C["text_muted"])
|
||||
self._status_label.configure(text=" 未选择任何文件")
|
||||
Reference in New Issue
Block a user