Files
orc-order-v2/app/ui/db_viewer.py
T

265 lines
9.9 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""数据库内容查看器模块"""
import os
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from datetime import datetime
from typing import Optional, Dict, List, Any
from app.config.settings import ConfigManager
from app.ui.ui_widgets import center_window
from app.ui.theme import THEMES, get_theme_mode
class DatabaseViewer:
"""通用数据库查看器,支持多表切换"""
def __init__(self, root, config: Optional[ConfigManager] = None):
self.root = root
self.config = config or ConfigManager()
self.db_path = self.config.get_path('Paths', 'product_db', fallback='data/product_cache.db')
# 确保路径是绝对路径
if not os.path.isabs(self.db_path):
app_root = getattr(self.config, 'app_root', os.getcwd())
self.db_path = os.path.join(app_root, self.db_path)
self.dlg = tk.Toplevel(root)
self.dlg.title("数据库内容查看器")
self.dlg.geometry("1000x600")
center_window(self.dlg)
theme = THEMES[get_theme_mode()]
self.dlg.configure(bg=theme["bg"])
# 使用 Notebook 实现多表切换
self.notebook = ttk.Notebook(self.dlg)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 定义要查看的表及其列信息
self.table_configs = {
"order_metadata": {
"title": "订单记录",
"columns": {
"bill_date": ("单据日期", 100),
"supplier": ("供应商", 200),
"total_amount": ("总金额", 80),
"updated_at": ("处理时间", 150),
"source_image": ("原始图片", 250),
"file_hash": ("Hash", 120)
},
"query": "SELECT * FROM order_metadata ORDER BY updated_at DESC"
},
"missing_barcodes": {
"title": "缺失条码",
"columns": {
"barcode": ("条码", 150),
"name": ("商品名称", 200),
"count": ("出现次数", 80),
"last_seen": ("最后发现", 150),
"source_file": ("来源文件", 250)
},
"query": "SELECT * FROM missing_barcodes ORDER BY last_seen DESC"
},
"products": {
"title": "商品记忆库",
"columns": {
"barcode": ("条码", 120),
"name": ("名称", 180),
"specification": ("规格", 80),
"unit": ("单位", 50),
"price": ("单价", 70),
"confidence": ("置信度", 60),
"usage_count": ("使用次数", 70),
"last_seen": ("最后使用", 140)
},
"query": "SELECT * FROM products ORDER BY last_seen DESC"
}
}
self.trees = {}
self._init_tabs()
# 底部按钮
btn_frame = ttk.Frame(self.dlg)
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
ttk.Button(btn_frame, text="刷新当前表", command=self.refresh_current_tab).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="清空记录 (慎用)", command=self.clear_current_table).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="关闭", command=self.dlg.destroy).pack(side=tk.RIGHT, padx=5)
def _init_tabs(self):
"""初始化各个标签页"""
for table_id, config in self.table_configs.items():
frame = ttk.Frame(self.notebook)
self.notebook.add(frame, text=config["title"])
# 搜索栏
search_frame = ttk.Frame(frame)
search_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT)
search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
search_entry.pack(side=tk.LEFT, padx=5)
# Treeview
cols = list(config["columns"].keys())
tree = ttk.Treeview(frame, columns=cols, show="headings")
for col, (text, width) in config["columns"].items():
tree.heading(col, text=text)
tree.column(col, width=width, anchor="center")
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 双击复制单元格内容
tree.bind("<Double-1>", lambda e, tid=table_id: self.copy_cell_value(e, tid))
self.trees[table_id] = {
"tree": tree,
"search_var": search_var,
"config": config
}
# 绑定搜索事件
search_var.trace_add("write", lambda *args, tid=table_id: self.load_table_data(tid))
# 初始加载数据
self.load_table_data(table_id)
def load_table_data(self, table_id):
"""加载指定表的数据"""
if not os.path.exists(self.db_path):
return
info = self.trees[table_id]
tree = info["tree"]
config = info["config"]
search_text = info["search_var"].get().lower()
# 清空现有数据
for item in tree.get_children():
tree.delete(item)
try:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 检查表是否存在
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_id,))
if not cursor.fetchone():
conn.close()
return
cursor.execute(config["query"])
rows = cursor.fetchall()
cols = list(config["columns"].keys())
for row in rows:
vals = [row[c] if c in row.keys() else "" for c in cols]
# 简单搜索过滤
if search_text:
match = False
for val in vals:
if search_text in str(val).lower():
match = True
break
if not match:
continue
# 格式化金额
if "total_amount" in row.keys():
idx = cols.index("total_amount")
try:
vals[idx] = f"{float(vals[idx]):.2f}"
except:
pass
tree.insert("", tk.END, values=vals)
conn.close()
# 自动调整列宽
self.auto_resize_columns(table_id)
except Exception as e:
print(f"加载表 {table_id} 数据失败: {e}")
def auto_resize_columns(self, table_id):
"""根据内容自动调整列宽"""
info = self.trees[table_id]
tree = info["tree"]
config = info["config"]
for col in list(config["columns"].keys()):
# 获取表头宽度
header_text = config["columns"][col][0]
max_w = len(header_text) * 12 + 20
# 获取内容宽度(检查前 20 行)
for item in tree.get_children()[:20]:
val = str(tree.set(item, col))
w = len(val) * 8 + 20
if w > max_w:
max_w = w
# 限制最大宽度
max_w = min(max_w, 400)
tree.column(col, width=max_w)
def copy_cell_value(self, event, table_id):
"""双击复制单元格内容到剪贴板"""
tree = self.trees[table_id]["tree"]
region = tree.identify_region(event.x, event.y)
if region == "cell":
column = tree.identify_column(event.x)
item = tree.identify_row(event.y)
value = tree.set(item, column)
self.root.clipboard_clear()
self.root.clipboard_append(value)
messagebox.showinfo("成功", f"内容已复制到剪贴板:\n{value}")
def refresh_current_tab(self):
"""刷新当前选中的标签页"""
current_tab_idx = self.notebook.index(self.notebook.select())
table_ids = list(self.table_configs.keys())
if current_tab_idx < len(table_ids):
self.load_table_data(table_ids[current_tab_idx])
def clear_current_table(self):
"""清空当前表的记录"""
current_tab_idx = self.notebook.index(self.notebook.select())
table_ids = list(self.table_configs.keys())
if current_tab_idx >= len(table_ids):
return
table_id = table_ids[current_tab_idx]
title = self.table_configs[table_id]["title"]
if not messagebox.askyesno("警告", f"确定要永久清空表 '{title}' 的所有记录吗?此操作不可恢复!"):
return
try:
conn = sqlite3.connect(self.db_path)
conn.execute(f"DELETE FROM {table_id}")
conn.commit()
conn.close()
self.load_table_data(table_id)
messagebox.showinfo("成功", f"表 '{title}' 已清空")
except Exception as e:
messagebox.showerror("错误", f"清空表失败: {e}")
def show_db_viewer(root, config: Optional[ConfigManager] = None):
"""显示数据库查看器"""
DatabaseViewer(root, config=config)