e4d62df7e3
- 智能供应商识别(蓉城易购/烟草/杨碧月/通用) - 百度 OCR 表格识别集成 - 规则引擎(列映射/数据清洗/单位转换/规格推断) - 条码映射管理与云端同步(Gitea REST API) - 云端同步支持:条码映射、供应商配置、商品资料、采购模板 - 拖拽一键处理(图片→OCR→Excel→合并) - 191 个单元测试 - 移除无用的模板管理功能 - 清理 IDE 产物目录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
208 lines
9.9 KiB
Python
208 lines
9.9 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""系统设置对话框模块"""
|
|
|
|
import os
|
|
import tkinter as tk
|
|
from tkinter import messagebox, filedialog, ttk
|
|
|
|
from app.config.settings import ConfigManager
|
|
|
|
from .user_settings import load_user_settings, save_user_settings
|
|
from .ui_widgets import center_window
|
|
from app.core.utils.dialog_utils import show_cloud_sync_dialog
|
|
|
|
# 模块级状态
|
|
_PROCESSOR_SERVICE = None
|
|
|
|
|
|
def show_config_dialog(root, cfg: ConfigManager):
|
|
global _PROCESSOR_SERVICE
|
|
|
|
settings = load_user_settings()
|
|
dlg = tk.Toplevel(root)
|
|
dlg.title("系统设置")
|
|
dlg.geometry("560x680")
|
|
center_window(dlg)
|
|
|
|
content = ttk.Frame(dlg)
|
|
content.pack(fill=tk.BOTH, expand=True, padx=16, pady=16)
|
|
for i in range(2):
|
|
content.columnconfigure(i, weight=1)
|
|
|
|
# 当前值
|
|
log_level_val = tk.StringVar(value=settings.get('log_level', 'INFO'))
|
|
max_workers_val = tk.StringVar(value=str(settings.get('concurrency_max_workers', cfg.getint('Performance', 'max_workers', 4))))
|
|
batch_size_val = tk.StringVar(value=str(settings.get('concurrency_batch_size', cfg.getint('Performance', 'batch_size', 5))))
|
|
template_path_val = tk.StringVar(value=settings.get('template_path', os.path.join(cfg.get('Paths', 'template_folder', 'templates'), cfg.get('Templates', 'purchase_order', '银豹-采购单模板.xls'))))
|
|
input_dir_val = tk.StringVar(value=settings.get('input_folder', cfg.get('Paths', 'input_folder', 'data/input')))
|
|
output_dir_val = tk.StringVar(value=settings.get('output_folder', cfg.get('Paths', 'output_folder', 'data/output')))
|
|
result_dir_val = tk.StringVar(value=settings.get('result_folder', 'data/result'))
|
|
|
|
def add_row(row, label_text, widget):
|
|
ttk.Label(content, text=label_text).grid(row=row, column=0, sticky='w', padx=4, pady=6)
|
|
widget.grid(row=row, column=1, sticky='ew', padx=4, pady=6)
|
|
|
|
# 日志级别
|
|
lvl = ttk.Combobox(content, textvariable=log_level_val, values=['DEBUG', 'INFO', 'WARNING', 'ERROR'], state='readonly')
|
|
add_row(0, "日志级别", lvl)
|
|
|
|
# 并发参数
|
|
maxw_entry = ttk.Entry(content, textvariable=max_workers_val)
|
|
add_row(1, "最大并发(max_workers)", maxw_entry)
|
|
batch_entry = ttk.Entry(content, textvariable=batch_size_val)
|
|
add_row(2, "批次大小(batch_size)", batch_entry)
|
|
|
|
# 模板路径
|
|
tpl_frame = ttk.Frame(content)
|
|
tpl_entry = ttk.Entry(tpl_frame, textvariable=template_path_val)
|
|
tpl_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
def _select_template():
|
|
p = filedialog.askopenfilename(title="选择模板文件", filetypes=[("Excel模板", "*.xls *.xlsx"), ("所有文件", "*.*")])
|
|
if p:
|
|
try:
|
|
template_path_val.set(os.path.relpath(p, os.getcwd()))
|
|
except Exception:
|
|
template_path_val.set(p)
|
|
|
|
ttk.Button(tpl_frame, text="选择", command=_select_template).pack(side=tk.LEFT, padx=6)
|
|
add_row(3, "采购单模板文件", tpl_frame)
|
|
|
|
# 目录
|
|
def dir_row(row_idx, label, var):
|
|
f = ttk.Frame(content)
|
|
e = ttk.Entry(f, textvariable=var)
|
|
e.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
def _select_dir():
|
|
d = filedialog.askdirectory(title=f"选择{label}")
|
|
if d:
|
|
try:
|
|
var.set(os.path.relpath(d, os.getcwd()))
|
|
except Exception:
|
|
var.set(d)
|
|
|
|
ttk.Button(f, text="选择", command=_select_dir).pack(side=tk.LEFT, padx=6)
|
|
add_row(row_idx, label, f)
|
|
|
|
dir_row(4, "输入目录", input_dir_val)
|
|
dir_row(5, "输出目录", output_dir_val)
|
|
dir_row(6, "结果目录", result_dir_val)
|
|
|
|
api_key_val = tk.StringVar(value=settings.get('api_key', cfg.get('API', 'api_key', '')))
|
|
secret_key_val = tk.StringVar(value=settings.get('secret_key', cfg.get('API', 'secret_key', '')))
|
|
timeout_val = tk.StringVar(value=str(settings.get('timeout', cfg.getint('API', 'timeout', 30))))
|
|
max_retries_val = tk.StringVar(value=str(settings.get('max_retries', cfg.getint('API', 'max_retries', 3))))
|
|
retry_delay_val = tk.StringVar(value=str(settings.get('retry_delay', cfg.getint('API', 'retry_delay', 2))))
|
|
api_url_val = tk.StringVar(value=settings.get('api_url', cfg.get('API', 'api_url', '')))
|
|
|
|
api_key_entry = ttk.Entry(content, textvariable=api_key_val)
|
|
add_row(7, "API Key", api_key_entry)
|
|
secret_key_entry = ttk.Entry(content, textvariable=secret_key_val)
|
|
secret_key_entry.configure(show='*')
|
|
add_row(8, "Secret Key", secret_key_entry)
|
|
add_row(9, "Timeout", ttk.Entry(content, textvariable=timeout_val))
|
|
add_row(10, "Max Retries", ttk.Entry(content, textvariable=max_retries_val))
|
|
add_row(11, "Retry Delay", ttk.Entry(content, textvariable=retry_delay_val))
|
|
add_row(12, "API URL", ttk.Entry(content, textvariable=api_url_val))
|
|
|
|
# ---- Gitea 云端同步配置 ----
|
|
ttk.Separator(content).grid(row=13, column=0, columnspan=2, sticky='ew', pady=8)
|
|
ttk.Label(content, text="云端同步 (Gitea)", font=("Arial", 10, "bold")).grid(row=14, column=0, sticky='w', padx=4, pady=4)
|
|
|
|
gitea_url_val = tk.StringVar(value=cfg.get('Gitea', 'base_url', fallback='https://gitea.94kan.cn'))
|
|
gitea_owner_val = tk.StringVar(value=cfg.get('Gitea', 'owner', fallback='houhuan'))
|
|
gitea_repo_val = tk.StringVar(value=cfg.get('Gitea', 'repo', fallback='yixuan-sync-data'))
|
|
gitea_token_val = tk.StringVar(value=cfg.get('Gitea', 'token', fallback=''))
|
|
|
|
add_row(15, "Gitea 地址", ttk.Entry(content, textvariable=gitea_url_val))
|
|
add_row(16, "仓库所有者", ttk.Entry(content, textvariable=gitea_owner_val))
|
|
add_row(17, "仓库名称", ttk.Entry(content, textvariable=gitea_repo_val))
|
|
gitea_token_entry = ttk.Entry(content, textvariable=gitea_token_val, show='*')
|
|
add_row(18, "Access Token", gitea_token_entry)
|
|
|
|
# 操作按钮
|
|
btns = ttk.Frame(content)
|
|
btns.grid(row=19, column=0, columnspan=2, sticky='ew', pady=10)
|
|
btns.columnconfigure(0, weight=1)
|
|
|
|
def save_settings():
|
|
try:
|
|
s = load_user_settings()
|
|
s['log_level'] = log_level_val.get()
|
|
s['concurrency_max_workers'] = int(max_workers_val.get() or '4')
|
|
s['concurrency_batch_size'] = int(batch_size_val.get() or '5')
|
|
tp = template_path_val.get()
|
|
inp = input_dir_val.get()
|
|
outp = output_dir_val.get()
|
|
resp = result_dir_val.get()
|
|
try:
|
|
if tp:
|
|
tp = os.path.relpath(tp, os.getcwd()) if os.path.isabs(tp) else tp
|
|
if inp:
|
|
inp = os.path.relpath(inp, os.getcwd()) if os.path.isabs(inp) else inp
|
|
if outp:
|
|
outp = os.path.relpath(outp, os.getcwd()) if os.path.isabs(outp) else outp
|
|
if resp:
|
|
resp = os.path.relpath(resp, os.getcwd()) if os.path.isabs(resp) else resp
|
|
except Exception:
|
|
pass
|
|
s['template_path'] = tp
|
|
s['input_folder'] = inp
|
|
s['output_folder'] = outp
|
|
s['result_folder'] = resp
|
|
save_user_settings(s)
|
|
try:
|
|
from app.core.utils.log_utils import set_log_level
|
|
set_log_level(s['log_level'])
|
|
except Exception:
|
|
pass
|
|
try:
|
|
tpl_path = s['template_path']
|
|
tpl_dir = os.path.dirname(tpl_path)
|
|
tpl_name = os.path.basename(tpl_path)
|
|
cfg.update('Paths', 'template_folder', tpl_dir)
|
|
cfg.update('Templates', 'purchase_order', tpl_name)
|
|
try:
|
|
cfg.update('Paths', 'template_file', os.path.join(tpl_dir, tpl_name))
|
|
except Exception:
|
|
pass
|
|
cfg.update('Paths', 'input_folder', s['input_folder'])
|
|
cfg.update('Paths', 'output_folder', s['output_folder'])
|
|
cfg.update('Performance', 'max_workers', s['concurrency_max_workers'])
|
|
cfg.update('Performance', 'batch_size', s['concurrency_batch_size'])
|
|
cfg.update('API', 'api_key', api_key_val.get())
|
|
cfg.update('API', 'secret_key', secret_key_val.get())
|
|
cfg.update('API', 'timeout', timeout_val.get())
|
|
cfg.update('API', 'max_retries', max_retries_val.get())
|
|
cfg.update('API', 'retry_delay', retry_delay_val.get())
|
|
cfg.update('API', 'api_url', api_url_val.get())
|
|
cfg.update('Gitea', 'base_url', gitea_url_val.get())
|
|
cfg.update('Gitea', 'owner', gitea_owner_val.get())
|
|
cfg.update('Gitea', 'repo', gitea_repo_val.get())
|
|
cfg.update('Gitea', 'token', gitea_token_val.get())
|
|
cfg.save_config()
|
|
except Exception:
|
|
pass
|
|
messagebox.showinfo("设置已保存", "系统设置已更新并保存")
|
|
dlg.destroy()
|
|
except Exception as e:
|
|
messagebox.showerror("保存失败", str(e))
|
|
|
|
def reload_suppliers():
|
|
global _PROCESSOR_SERVICE
|
|
try:
|
|
from app.services.processor_service import ProcessorService
|
|
if _PROCESSOR_SERVICE is None:
|
|
_PROCESSOR_SERVICE = ProcessorService(ConfigManager())
|
|
_PROCESSOR_SERVICE.reload_processors()
|
|
messagebox.showinfo("已重新加载", "供应商处理器已重新加载并应用最新配置")
|
|
except Exception as e:
|
|
messagebox.showerror("重新加载失败", str(e))
|
|
|
|
ttk.Button(btns, text="重新加载供应商配置", command=reload_suppliers).grid(row=0, column=0, sticky='w')
|
|
ttk.Button(btns, text="云端同步", command=lambda: show_cloud_sync_dialog(dlg)).grid(row=0, column=1, sticky='w', padx=6)
|
|
ttk.Button(btns, text="取消", command=dlg.destroy).grid(row=0, column=2, sticky='e')
|
|
ttk.Button(btns, text="保存", command=save_settings).grid(row=0, column=3, sticky='e', padx=6)
|