d267a1d1fa
feat: 商品记忆库 — 从OCR结果学习,逐步替代OCR识别 - 扩展 product_db.py: schema迁移(specification/source/confidence/usage_count/last_seen) + 学习逻辑(learn_from_product)、置信度系统、批量查询、导入导出、云端同步 - 注入处理管线: processor.py 在提取产品后调用 _apply_memory() 用记忆补全OCR + _is_spec_suspicious() 检测OCR规格质量,处理完后自动学习 - order_service.py 创建共享 ProductDatabase 实例 - dialog_utils.py 新增商品记忆库云端同步条目 - 新建 memory_editor.py: Treeview查看/编辑/搜索/删除/重新导入 - main_window.py 系统设置区新增"商品记忆库"按钮 - build_exe.py 添加 memory_editor 到 hidden_imports @
199 lines
7.1 KiB
Python
199 lines
7.1 KiB
Python
"""商品记忆库查看/编辑对话框"""
|
|
|
|
import os
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog
|
|
|
|
from app.config.settings import ConfigManager
|
|
from app.core.db.product_db import ProductDatabase
|
|
from .ui_widgets import center_window
|
|
|
|
|
|
def _get_product_db():
|
|
cfg = ConfigManager()
|
|
db_path = cfg.get_path('Paths', 'product_db', fallback='data/product_cache.db') if hasattr(cfg, 'get_path') else 'data/product_cache.db'
|
|
tpl_folder = cfg.get('Paths', 'template_folder', fallback='templates')
|
|
item_data = cfg.get('Templates', 'item_data', fallback='商品资料.xlsx')
|
|
tpl_path = os.path.join(tpl_folder, item_data)
|
|
return ProductDatabase(db_path, tpl_path)
|
|
|
|
|
|
def show_memory_editor(root):
|
|
"""显示商品记忆库编辑器"""
|
|
db = _get_product_db()
|
|
|
|
dlg = tk.Toplevel(root)
|
|
dlg.title("商品记忆库")
|
|
dlg.geometry("950x520")
|
|
center_window(dlg)
|
|
|
|
# ── 顶部搜索栏 ──
|
|
top = ttk.Frame(dlg)
|
|
top.pack(fill=tk.X, padx=8, pady=(8, 4))
|
|
|
|
ttk.Label(top, text="搜索:").pack(side=tk.LEFT)
|
|
search_var = tk.StringVar()
|
|
search_entry = ttk.Entry(top, textvariable=search_var, width=30)
|
|
search_entry.pack(side=tk.LEFT, padx=4)
|
|
|
|
# ── 统计标签 ──
|
|
stats_label = ttk.Label(top, text="")
|
|
stats_label.pack(side=tk.RIGHT)
|
|
|
|
# ── Treeview ──
|
|
columns = ("barcode", "name", "specification", "unit", "price", "source", "confidence", "usage_count", "last_seen")
|
|
tree = ttk.Treeview(dlg, columns=columns, show="headings", height=18)
|
|
|
|
headers = {
|
|
"barcode": ("条码", 120),
|
|
"name": ("名称", 180),
|
|
"specification": ("规格", 80),
|
|
"unit": ("单位", 50),
|
|
"price": ("单价", 70),
|
|
"source": ("来源", 80),
|
|
"confidence": ("置信度", 60),
|
|
"usage_count": ("使用次数", 70),
|
|
"last_seen": ("最后使用", 140),
|
|
}
|
|
for col, (text, width) in headers.items():
|
|
tree.heading(col, text=text)
|
|
tree.column(col, width=width, anchor="center")
|
|
|
|
# 置信度颜色标签
|
|
tree.tag_configure("high", foreground="#28a745") # >= 80 绿
|
|
tree.tag_configure("medium", foreground="#ffc107") # 50-79 黄
|
|
tree.tag_configure("low", foreground="#dc3545") # < 50 红
|
|
|
|
scrollbar = ttk.Scrollbar(dlg, orient=tk.VERTICAL, command=tree.yview)
|
|
tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(8, 0), pady=4)
|
|
scrollbar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 8), pady=4)
|
|
|
|
# ── 数据加载 ──
|
|
all_records = []
|
|
|
|
def load_data(filter_text=""):
|
|
nonlocal all_records
|
|
all_records = db.get_all_memories()
|
|
|
|
tree.delete(*tree.get_children())
|
|
|
|
filtered = all_records
|
|
if filter_text:
|
|
ft = filter_text.lower()
|
|
filtered = [r for r in all_records
|
|
if ft in str(r.get('barcode', '')).lower()
|
|
or ft in str(r.get('name', '')).lower()]
|
|
|
|
for r in filtered:
|
|
conf = r.get('confidence', 0) or 0
|
|
tag = "high" if conf >= 80 else ("medium" if conf >= 50 else "low")
|
|
|
|
last_seen = r.get('last_seen', '') or ''
|
|
if last_seen and len(last_seen) > 16:
|
|
last_seen = last_seen[:16]
|
|
|
|
source_display = {
|
|
'template': '模板',
|
|
'ocr': 'OCR',
|
|
'user_confirmed': '手动',
|
|
}.get(r.get('source', ''), r.get('source', ''))
|
|
|
|
tree.insert("", tk.END, values=(
|
|
r.get('barcode', ''),
|
|
r.get('name', ''),
|
|
r.get('specification', ''),
|
|
r.get('unit', ''),
|
|
f"{r.get('price', 0):.2f}" if r.get('price') else '',
|
|
source_display,
|
|
conf,
|
|
r.get('usage_count', 0) or 0,
|
|
last_seen,
|
|
), tags=(tag,))
|
|
|
|
stats_label.config(text=f"共 {len(filtered)} / {len(all_records)} 条")
|
|
|
|
def on_search(*_):
|
|
load_data(search_var.get())
|
|
|
|
search_var.trace_add("write", on_search)
|
|
|
|
# ── 按钮区 ──
|
|
btn_frame = ttk.Frame(dlg)
|
|
btn_frame.pack(fill=tk.X, padx=8, pady=(0, 8))
|
|
|
|
def edit_selected():
|
|
sel = tree.selection()
|
|
if not sel:
|
|
messagebox.showwarning("提示", "请先选择一条记录")
|
|
return
|
|
item = tree.item(sel[0])
|
|
vals = item['values']
|
|
barcode = vals[0]
|
|
|
|
# 弹出编辑对话框
|
|
edit_dlg = tk.Toplevel(dlg)
|
|
edit_dlg.title(f"编辑: {barcode}")
|
|
edit_dlg.geometry("380x260")
|
|
center_window(edit_dlg)
|
|
|
|
fields = [
|
|
("名称", "name", vals[1]),
|
|
("规格", "specification", vals[2]),
|
|
("单位", "unit", vals[3]),
|
|
("单价", "price", vals[4]),
|
|
]
|
|
entries = {}
|
|
for i, (label, key, val) in enumerate(fields):
|
|
ttk.Label(edit_dlg, text=label).grid(row=i, column=0, sticky='w', padx=8, pady=4)
|
|
var = tk.StringVar(value=str(val) if val else '')
|
|
ttk.Entry(edit_dlg, textvariable=var, width=30).grid(row=i, column=1, padx=8, pady=4)
|
|
entries[key] = var
|
|
|
|
def save_edit():
|
|
updates = {}
|
|
for key, var in entries.items():
|
|
v = var.get().strip()
|
|
if key == 'price':
|
|
try:
|
|
updates[key] = float(v) if v else 0
|
|
except ValueError:
|
|
updates[key] = 0
|
|
else:
|
|
updates[key] = v
|
|
db.update_memory(barcode, updates)
|
|
edit_dlg.destroy()
|
|
load_data(search_var.get())
|
|
|
|
ttk.Button(edit_dlg, text="保存", command=save_edit).grid(row=len(fields), column=0, columnspan=2, pady=12)
|
|
|
|
def delete_selected():
|
|
sel = tree.selection()
|
|
if not sel:
|
|
messagebox.showwarning("提示", "请先选择一条记录")
|
|
return
|
|
item = tree.item(sel[0])
|
|
barcode = item['values'][0]
|
|
if messagebox.askyesno("确认删除", f"确定要删除条码 {barcode} 的记忆记录吗?"):
|
|
db.delete_memory(barcode)
|
|
load_data(search_var.get())
|
|
|
|
def reimport_template():
|
|
if messagebox.askyesno("确认", "重新从商品资料导入将重置所有模板商品的置信度为100,确定继续吗?"):
|
|
count = db.reimport()
|
|
messagebox.showinfo("完成", f"已重新导入 {count} 条记录")
|
|
load_data(search_var.get())
|
|
|
|
ttk.Button(btn_frame, text="编辑", command=edit_selected).pack(side=tk.LEFT, padx=4)
|
|
ttk.Button(btn_frame, text="删除", command=delete_selected).pack(side=tk.LEFT, padx=4)
|
|
ttk.Button(btn_frame, text="重新导入模板", command=reimport_template).pack(side=tk.LEFT, padx=4)
|
|
ttk.Button(btn_frame, text="刷新", command=lambda: load_data(search_var.get())).pack(side=tk.LEFT, padx=4)
|
|
ttk.Button(btn_frame, text="关闭", command=dlg.destroy).pack(side=tk.RIGHT, padx=4)
|
|
|
|
# 双击编辑
|
|
tree.bind("<Double-1>", lambda e: edit_selected())
|
|
|
|
# 初始加载
|
|
load_data()
|