Files
Contract-document-crawling-…/前端页面.py
2026-01-18 11:31:40 +08:00

709 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import os
import time
import threading
import re
import urllib.parse
import webbrowser
from datetime import datetime
import tkinter as tk
from tkinter import filedialog, messagebox
import requests
import pandas as pd
from lxml import html
# ================= 1. 导入 UI 库 =================
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
# 兼容导入
try:
from ttkbootstrap.widgets import ScrolledText, Tableview, ToastNotification
except ImportError:
from ttkbootstrap.scrolled import ScrolledText
from ttkbootstrap.tableview import Tableview
from ttkbootstrap.toast import ToastNotification
# ================= 2. 后端核心逻辑 (保持功能完整) =================
class CRMCrawler:
def __init__(self, log_callback, data_callback):
self.log = log_callback
self.on_data = data_callback
self.stop_flag = False
self.session = requests.Session()
self.base_url = "http://111.198.24.44:88/index.php"
self.http_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json, text/javascript, */*; q=0.01"
}
def login(self, username, password):
self.log(f"🔑 正在登录... 用户: {username}")
login_payload = {
"module": "Users", "action": "Authenticate", "return_module": "Users",
"return_action": "Login", "user_name": username, "user_password": password, "login_theme": "newskin"
}
try:
self.session.get(self.base_url, headers=self.http_headers)
self.session.post(self.base_url, data=login_payload, headers=self.http_headers)
if 'PHPSESSID' in self.session.cookies:
self.log("✅ 登录成功!")
return True
else:
self.log("❌ 登录失败:请检查账号密码")
return False
except Exception as e:
self.log(f"❌ 网络错误: {str(e)}")
return False
def fetch_detail(self, record_id):
try:
url = f"{self.base_url}?module=SalesOrder&action=DetailView&record={record_id}"
resp = self.session.get(url, headers=self.http_headers, timeout=10)
if resp.status_code == 200:
tree = html.fromstring(resp.content)
target = tree.xpath("/html/body/div[1]/div/div[2]/div[2]/form/div[1]/div[1]/div[2]")
if target:
import copy
el = copy.deepcopy(target[0])
for bad in el.xpath('.//script | .//style'): bad.drop_tree()
for br in el.xpath('.//br'): br.tail = "\n" + (br.tail if br.tail else "")
return "\n".join([line.strip() for line in el.text_content().splitlines() if line.strip()])
except Exception:
pass
return ""
def parse_data(self, text, cid):
if not text: return None
data = {
"系统ID": cid,
"合同编号": "", "内贸合同号": "", "外贸合同号": "",
"签署公司": "", "收款情况": "", "签订日期": "", "销售员": "",
"最终用户单位": "", "最终用户信息联系人": "", "最终用户信息电话": "", "最终用户信息邮箱": "",
"最终用户所在地": "",
"买方单位": "", "买方信息联系人": "", "买方信息电话": "", "买方信息邮箱": "",
"厂家": "", "厂家型号": "", "合同标的": "", "数量": "", "单位": "台/套",
"折扣率(%)": "", "合同额": "", "合同总额": "",
"外购付款方式": "", "最晚发货期": "", "已收款": "", "未收款": "", "收款日期": "",
"IS_ASD": False, "_temp_second_code": ""
}
lines = [line.strip() for line in text.split('\n') if line.strip()]
key_map = {
"收款账户": "签署公司", "收款状态": "收款情况", "签约日期": "签订日期",
"负责人": "销售员", "客户名称": "最终用户单位", "联系人姓名": "最终用户信息联系人",
"合同总额": "合同总额", "最新收款日期": "收款日期", "最晚发货期": "最晚发货期",
"付款比例及期限": "外购付款方式", "地址": "最终用户所在地", "厂家": "厂家"
}
for i, line in enumerate(lines):
if line == "合同订单编号" and i + 1 < len(lines):
parts = lines[i + 1].strip().split()
if len(parts) >= 1: data["合同编号"] = parts[0]
if len(parts) >= 2: data["_temp_second_code"] = parts[1]
elif line in key_map and i + 1 < len(lines):
target = key_map[line]
if not data[target]: data[target] = lines[i + 1]
elif "合同标的" in line and "品名/型号" in line and i + 1 < len(lines):
parts = lines[i + 1].split('/')
if len(parts) >= 1: data["合同标的"] = parts[0]
if len(parts) >= 2: data["厂家型号"] = parts[1]
if len(parts) >= 3: data["数量"] = parts[2]
if len(parts) >= 5: data["合同额"] = parts[4]
buyer_match = re.search(r"(?:买方|The Buyer)[:]\s*(.*?)(?:\n|$)", text)
if buyer_match and len(buyer_match.group(1)) > 1: data["买方单位"] = buyer_match.group(1).strip()
buyer_ct = re.search(r"联系人Contact person[:]\s*(.*?)(?:\n|$)", text)
if buyer_ct: data["买方信息联系人"] = buyer_ct.group(1).strip()
buyer_tel = re.search(r"电话\(Tel\)[:]\s*(.*?)(?:\s+|$|传真)", text)
if buyer_tel: data["买方信息电话"] = buyer_tel.group(1).strip()
try:
total = float(data["合同总额"]) if data["合同总额"] else 0
if "已收" in data["收款情况"]:
data["已收款"] = str(total);
data["未收款"] = "0"
elif "" in data["收款情况"]:
data["已收款"] = "0";
data["未收款"] = str(total)
except:
pass
factory_val = data.get("厂家", "")
if factory_val and "ASD" in factory_val.upper():
data["IS_ASD"] = True
else:
data["IS_ASD"] = False
c_no = data.get("合同编号", "").strip().upper()
sec_code = data.pop("_temp_second_code", "")
if c_no.startswith('W'):
data["外贸合同号"] = sec_code
elif c_no.startswith('N'):
data["内贸合同号"] = sec_code
else:
data["内贸合同号"] = sec_code
if not c_no: return None
return data
def extract_time(self, text):
matches = re.findall(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})", text)
if matches:
dt_objects = [datetime.strptime(m, "%Y-%m-%d %H:%M:%S") for m in matches]
return max(dt_objects)
return None
def run_task(self, mode, **kwargs):
crmids = []
if mode == 'search':
query = kwargs.get('query')
self.log(f"🔍 正在搜索: {query}")
url = f"{self.base_url}?module=Home&action=UnifiedSearch&selectedmodule=undefined&query_string={urllib.parse.quote(query)}"
resp = self.session.get(url, headers=self.http_headers)
tree = html.fromstring(resp.content)
links = tree.xpath('//a[contains(@onclick, "record=")]')
for link in links:
onclick = link.get('onclick', '')
match = re.search(r"record=(\d+)", onclick)
if match: crmids.append(match.group(1))
crmids = list(set(crmids))
elif mode == 'full':
self.log("🚀 开始全量爬取 (演示限制前5页)")
crmids = self._get_list_ids(limit_pages=5)
elif mode == 'date':
s_date = kwargs.get('start')
e_date = kwargs.get('end')
self.log(f"📅 时间筛选: {s_date} ~ {e_date}")
self._process_date_range(s_date, e_date)
return
self.log(f" 共找到 {len(crmids)} 条记录,开始解析详情...")
for i, cid in enumerate(crmids):
if self.stop_flag: break
self._process_single_id(cid)
self.log(f" 进度: {i + 1}/{len(crmids)}")
def _get_list_ids(self, limit_pages=3):
ids = []
for p in range(1, limit_pages + 1):
if self.stop_flag: break
try:
ts = int(time.time() * 1000)
url = f"{self.base_url}?module=SalesOrder&action=SalesOrderAjax&file=ListViewData&start={p}&actionId={ts}"
resp = self.session.get(url, headers=self.http_headers)
entries = resp.json().get('data', [])
if not entries: break
for item in entries:
if isinstance(item, dict):
ids.append(item.get('crmid') or item.get('id'))
except:
break
return list(set(ids))
def _process_date_range(self, s_str, e_str):
try:
t_start = datetime.strptime(s_str, "%Y-%m-%d")
t_end = datetime.strptime(e_str, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
except:
self.log("❌ 日期格式错误")
return
page = 1
while not self.stop_flag:
ts = int(time.time() * 1000)
url = f"{self.base_url}?module=SalesOrder&action=SalesOrderAjax&file=ListViewData&sorder=DESC&order_by=modifiedtime&start={page}&pagesize=50&actionId={ts}&isFilter=true&search%5Bviewscope%5D=all_to_me&search%5Bviewname%5D=476"
try:
resp = self.session.get(url, headers=self.http_headers)
data = resp.json()
entries = data.get('data', []) or data.get('entries', [])
if not entries: break
page_ids = [x.get('crmid') or x.get('id') for x in entries if isinstance(x, dict)]
self.log(f" 🔎 正在检查第 {page} 页 ({len(page_ids)} 条)...")
valid_cnt = 0
for cid in page_ids:
if self.stop_flag: break
text = self.fetch_detail(cid)
r_time = self.extract_time(text)
if r_time:
if r_time > t_end: continue
if r_time < t_start:
self.log(f" 🛑 遇到旧数据 ({r_time}),停止爬取")
self.stop_flag = True
break
parsed = self.parse_data(text, cid)
if parsed:
self.on_data(parsed)
valid_cnt += 1
if valid_cnt > 0: self.log(f" ✅ 第 {page} 页入库 {valid_cnt}")
page += 1
if self.stop_flag: break
except Exception as e:
self.log(f"❌ 错误: {e}")
break
def _process_single_id(self, cid):
text = self.fetch_detail(cid)
parsed = self.parse_data(text, cid)
if parsed:
self.on_data(parsed)
# ================= 3. 界面显示类 (重大升级) =================
class CRMGUI(ttk.Window):
def __init__(self):
super().__init__(themename="cosmo") # 使用 cosmo 主题
self.title("CRM 智能数据助手 Pro Max")
self.geometry("1280x850")
self.crawler = CRMCrawler(self.log_msg, self.add_record_to_table)
self.is_running = False
# 内存数据结构:{'ASD': {'Domestic':[], 'Foreign':[], 'Other':[]}, 'NON_ASD': {...}}
self.stored_data = {
'ASD': {'Domestic': [], 'Foreign': [], 'Other': []},
'NON_ASD': {'Domestic': [], 'Foreign': [], 'Other': []}
}
# 引用字典,方便后续操作
self.treeviews = {}
# 基础字段
self.base_cols = [
"合同编号", "签署公司", "收款情况", "签订日期", "销售员", "厂家",
"最终用户单位", "最终用户信息联系人", "最终用户信息电话", "买方单位",
"厂家型号", "合同标的", "数量", "合同额", "合同总额",
"最晚发货期", "已收款", "未收款", "收款日期"
]
# 定制表头
self.cols_domestic = ["内贸合同号"] + self.base_cols + ["系统ID"]
self.cols_foreign = ["外贸合同号"] + self.base_cols + ["系统ID"]
self.cols_other = self.base_cols + ["系统ID"]
self.create_widgets()
def create_widgets(self):
# --- 1. 顶部:控制区 ---
control_frame = ttk.Frame(self, padding=10, bootstyle="light")
control_frame.pack(fill=X)
# 登录
login_grp = ttk.Labelframe(control_frame, text="身份验证", padding=10)
login_grp.pack(side=LEFT, padx=5, fill=Y)
ttk.Label(login_grp, text="用户:").pack(side=LEFT)
self.user_ent = ttk.Entry(login_grp, width=10);
self.user_ent.insert(0, "TEST");
self.user_ent.pack(side=LEFT, padx=5)
ttk.Label(login_grp, text="密码:").pack(side=LEFT)
self.pass_ent = ttk.Entry(login_grp, width=10, show="*");
self.pass_ent.insert(0, "***");
self.pass_ent.pack(side=LEFT, padx=5)
# 模式
mode_grp = ttk.Labelframe(control_frame, text="任务类型", padding=10)
mode_grp.pack(side=LEFT, padx=10, fill=Y, expand=True)
self.nb_mode = ttk.Notebook(mode_grp, bootstyle="primary")
self.nb_mode.pack(fill=BOTH, expand=True)
f_date = ttk.Frame(self.nb_mode, padding=10)
self.nb_mode.add(f_date, text="📅 按时间范围")
self.ent_start = ttk.Entry(f_date, width=12);
self.ent_start.insert(0, "2026-01-14");
self.ent_start.pack(side=LEFT, padx=5)
ttk.Label(f_date, text="").pack(side=LEFT)
self.ent_end = ttk.Entry(f_date, width=12);
self.ent_end.insert(0, "2026-01-15");
self.ent_end.pack(side=LEFT, padx=5)
f_search = ttk.Frame(self.nb_mode, padding=10)
self.nb_mode.add(f_search, text="🔍 关键词搜索")
self.ent_query = ttk.Entry(f_search, width=25);
self.ent_query.pack(fill=X)
f_full = ttk.Frame(self.nb_mode, padding=10)
self.nb_mode.add(f_full, text="🚀 全量")
ttk.Label(f_full, text="数据量大,慎用").pack()
self.nb_mode.select(f_date)
# 运行按钮
btn_grp = ttk.Frame(control_frame, padding=10)
btn_grp.pack(side=RIGHT, fill=Y)
self.btn_run = ttk.Button(btn_grp, text="▶ 开始", bootstyle="success", command=self.start_thread, width=10)
self.btn_run.pack(side=TOP, pady=2)
self.btn_stop = ttk.Button(btn_grp, text="⏹ 停止", bootstyle="danger", command=self.stop_task, state=DISABLED,
width=10)
self.btn_stop.pack(side=TOP, pady=2)
# --- 2. 核心展示区 (解决颜色问题) ---
# 使用“切换按钮”代替顶层Tab实现 [选中=蓝色实心] [未选中=白色空心]
toggle_frame = ttk.Frame(self, padding=(10, 5))
toggle_frame.pack(fill=X)
self.curr_view = tk.StringVar(value="ASD") # 默认 ASD
self.btn_view_asd = ttk.Button(toggle_frame, text="ASD 产品列表", command=lambda: self.switch_view("ASD"),
width=20)
self.btn_view_asd.pack(side=LEFT, padx=5)
self.btn_view_non = ttk.Button(toggle_frame, text="非 ASD 产品列表",
command=lambda: self.switch_view("NON_ASD"), width=20)
self.btn_view_non.pack(side=LEFT, padx=5)
# 容器 Frame
self.container = ttk.Frame(self)
self.container.pack(fill=BOTH, expand=True, padx=10)
# 创建两个大 Frame分别装 ASD 和 NON_ASD 的内容
self.frame_asd = ttk.Frame(self.container)
self.frame_non = ttk.Frame(self.container)
# 初始化内部结构 (内贸/外贸/其他 分离)
self._init_inner_tabs(self.frame_asd, "ASD")
self._init_inner_tabs(self.frame_non, "NON_ASD")
# 默认显示 ASD
self.switch_view("ASD")
# --- 3. 底部区 ---
bottom_frame = ttk.Frame(self, padding=5)
bottom_frame.pack(fill=X, padx=10, pady=5)
log_frame = ttk.Labelframe(bottom_frame, text="系统日志", padding=5)
log_frame.pack(side=LEFT, fill=BOTH, expand=True)
self.txt_log = ScrolledText(log_frame, height=5)
self.txt_log.text.configure(state=DISABLED)
self.txt_log.pack(fill=BOTH, expand=True)
export_frame = ttk.Frame(bottom_frame, padding=10)
export_frame.pack(side=RIGHT, fill=Y)
ttk.Button(export_frame, text="📂 导出 Excel", bootstyle="primary", command=self.export_data).pack(fill=X,
pady=10)
def _init_inner_tabs(self, parent_frame, prefix):
"""在父Frame中创建 内贸/外贸/其他 的Tab结构"""
nb = ttk.Notebook(parent_frame, bootstyle="info")
nb.pack(fill=BOTH, expand=True)
# 内贸 Tab
f_dom = ttk.Frame(nb);
nb.add(f_dom, text="内贸 (Domestic)")
self._create_treeview(f_dom, self.cols_domestic, f"{prefix}_Domestic")
# 外贸 Tab
f_for = ttk.Frame(nb);
nb.add(f_for, text="外贸 (Foreign)")
self._create_treeview(f_for, self.cols_foreign, f"{prefix}_Foreign")
# 其他 Tab
f_oth = ttk.Frame(nb);
nb.add(f_oth, text="其他 (Other)")
self._create_treeview(f_oth, self.cols_other, f"{prefix}_Other")
def _create_treeview(self, parent, cols, key):
"""创建表格并注册到 self.treeviews"""
# 滚动条
sy = ttk.Scrollbar(parent, orient=VERTICAL)
sx = ttk.Scrollbar(parent, orient=HORIZONTAL)
tv = ttk.Treeview(parent, columns=cols, show="headings", selectmode="browse",
yscrollcommand=sy.set, xscrollcommand=sx.set)
sy.config(command=tv.yview);
sy.pack(side=RIGHT, fill=Y)
sx.config(command=tv.xview);
sx.pack(side=BOTTOM, fill=X)
tv.pack(side=LEFT, fill=BOTH, expand=True)
for c in cols:
tv.heading(c, text=c)
w = 100
if c in ["合同标的", "最终用户单位", "签署公司", "买方单位"]:
w = 200
elif c == "系统ID":
w = 0
tv.column(c, width=w, minwidth=50)
# 绑定双击
tv.bind("<Double-1>", lambda e: self.on_double_click(e, tv, key))
# 绑定右键菜单
tv.bind("<Button-3>", lambda e: self.on_right_click(e, tv, key))
self.treeviews[key] = tv
return tv
def switch_view(self, view_name):
"""切换 ASD / NON_ASD 视图,并处理按钮颜色反转"""
self.curr_view.set(view_name)
if view_name == "ASD":
self.frame_non.pack_forget()
self.frame_asd.pack(fill=BOTH, expand=True)
# ASD选中ASD实心(primary)NON空心(outline)
self.btn_view_asd.configure(bootstyle="primary")
self.btn_view_non.configure(bootstyle="secondary-outline")
else:
self.frame_asd.pack_forget()
self.frame_non.pack(fill=BOTH, expand=True)
# NON选中ASD空心NON实心
self.btn_view_asd.configure(bootstyle="secondary-outline")
self.btn_view_non.configure(bootstyle="primary")
# --- 逻辑控制 ---
def start_thread(self):
if self.is_running: return
# 清空所有数据和表格
self.stored_data = {
'ASD': {'Domestic': [], 'Foreign': [], 'Other': []},
'NON_ASD': {'Domestic': [], 'Foreign': [], 'Other': []}
}
for tv in self.treeviews.values():
for item in tv.get_children(): tv.delete(item)
self.is_running = True
self.crawler.stop_flag = False
self.btn_run.config(state=DISABLED)
self.btn_stop.config(state=NORMAL)
t = threading.Thread(target=self._worker)
t.daemon = True
t.start()
def stop_task(self):
self.crawler.stop_flag = True
self.log_msg("🛑 正在停止...")
def _worker(self):
user = self.user_ent.get()
pwd = self.pass_ent.get()
if not self.crawler.login(user, pwd):
self._reset_ui();
return
curr_idx = self.nb_mode.index(self.nb_mode.select())
mode = "date"
kwargs = {}
if curr_idx == 0:
mode = "date";
kwargs = {'start': self.ent_start.get(), 'end': self.ent_end.get()}
elif curr_idx == 1:
mode = "search";
kwargs = {'query': self.ent_query.get()}
elif curr_idx == 2:
mode = "full"
try:
self.crawler.run_task(mode, **kwargs)
self.log_msg("🎉 完成!")
except Exception as e:
self.log_msg(f"❌ 错误: {e}")
finally:
self._reset_ui()
def _reset_ui(self):
self.is_running = False
self.after(0, lambda: self.btn_run.config(state=NORMAL))
self.after(0, lambda: self.btn_stop.config(state=DISABLED))
def log_msg(self, msg):
self.after(0, lambda: self._append_log(msg))
def _append_log(self, msg):
self.txt_log.text.configure(state=NORMAL)
self.txt_log.text.insert(END, f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
self.txt_log.text.see(END)
self.txt_log.text.configure(state=DISABLED)
# --- 数据分发逻辑 (核心) ---
def add_record_to_table(self, record):
def _update():
# 1. 确定大类
main_key = 'ASD' if record['IS_ASD'] else 'NON_ASD'
# 2. 确定子类 (内贸/外贸/其他)
c_no = str(record.get("合同编号", "")).strip().upper()
sub_key = "Other"
if c_no.startswith('N'):
sub_key = "Domestic"
elif c_no.startswith('W'):
sub_key = "Foreign"
# 3. 存入内存
self.stored_data[main_key][sub_key].append(record)
# 4. 插入对应表格
# 组合 Key 找到对应的 Treeview
tv_key = f"{main_key}_{sub_key}"
tv = self.treeviews.get(tv_key)
if tv:
# 获取该表格对应的列
# 注意columns 是 tuple需要转 list
cols = list(tv['columns'])
vals = [record.get(c, "") for c in cols]
# iid 设为列表索引,方便查找
idx = len(self.stored_data[main_key][sub_key]) - 1
tv.insert("", END, iid=idx, values=vals)
self.after(0, _update)
# --- 编辑与跳转逻辑 ---
def on_right_click(self, event, tv, key):
"""右键菜单"""
item_id = tv.identify_row(event.y)
if not item_id: return
tv.selection_set(item_id)
# 解析 key (例如 "ASD_Domestic")
parts = key.split('_')
main_key = parts[0]
if len(parts) > 2: main_key = f"{parts[0]}_{parts[1]}" # 防止 NON_ASD 这种
sub_key = parts[-1]
record = self.stored_data[main_key][sub_key][int(item_id)]
crm_id = record.get("系统ID", "")
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="🌐 在浏览器查看", command=lambda: self.open_browser(crm_id))
menu.add_command(label="📝 编辑详情", command=lambda: self.show_detail_popup(record, tv, item_id))
menu.post(event.x_root, event.y_root)
def on_double_click(self, event, tv, key):
item_id = tv.selection()
if not item_id: return
idx = int(item_id[0])
parts = key.split('_')
main_key = parts[0]
if len(parts) > 2: main_key = f"{parts[0]}_{parts[1]}"
sub_key = parts[-1]
record = self.stored_data[main_key][sub_key][idx]
self.show_detail_popup(record, tv, item_id)
def open_browser(self, crm_id):
if crm_id:
url = f"http://111.198.24.44:88/index.php?module=SalesOrder&action=DetailView&record={crm_id}"
webbrowser.open(url)
self.log_msg(f"🌐 跳转: {crm_id}")
def show_detail_popup(self, record, tv, item_id):
top = ttk.Toplevel(self)
top.title(f"订单详情: {record.get('合同编号')}")
top.geometry("600x700")
# 滚动容器
canvas = tk.Canvas(top)
sb = ttk.Scrollbar(top, orient="vertical", command=canvas.yview)
f_scroll = ttk.Frame(canvas)
f_scroll.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=f_scroll, anchor="nw")
canvas.configure(yscrollcommand=sb.set)
canvas.pack(side="left", fill="both", expand=True)
sb.pack(side="right", fill="y")
# 滚轮支持
canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(int(-1 * (e.delta / 120)), "units"))
# 按钮
crm_id = record.get("系统ID", "")
ttk.Button(f_scroll, text="🌐 浏览器查看原始网页", bootstyle="info-outline",
command=lambda: self.open_browser(crm_id)).grid(row=0, column=0, columnspan=2, pady=10)
# 字段编辑
entries = {}
row = 1
# 显示该表格对应的所有列
cols = list(tv['columns'])
for field in cols:
if field == "系统ID": continue
ttk.Label(f_scroll, text=field + ":").grid(row=row, column=0, sticky=E, padx=5, pady=5)
ent = ttk.Entry(f_scroll, width=40)
ent.insert(0, str(record.get(field, "")))
ent.grid(row=row, column=1, padx=5, pady=5)
entries[field] = ent
row += 1
def save():
for k, e in entries.items(): record[k] = e.get()
new_vals = [record.get(c, "") for c in cols]
tv.item(item_id, values=new_vals)
top.destroy()
ToastNotification("保存成功", "本地数据已更新", 1500).show_toast()
ttk.Button(f_scroll, text="💾 保存修改", bootstyle="success", command=save).grid(row=row, column=0, columnspan=2,
pady=20)
# --- 导出 ---
def export_data(self):
folder = filedialog.askdirectory()
if not folder: return
self.log_msg(f"💾 正在导出...")
ts = time.strftime("%Y%m%d_%H%M%S")
# 完整的字段映射,用于导出时的列顺序(比界面显示的更多更全)
export_cols = [
"合同编号", "签署公司", "收款情况", "签订日期", "销售员", "厂家",
"最终用户单位", "最终用户信息联系人", "最终用户信息电话", "最终用户信息邮箱", "最终用户所在地",
"买方单位", "买方信息联系人", "买方信息电话", "买方信息邮箱",
"厂家型号", "合同标的", "数量", "单位", "折扣率(%)", "合同额", "合同总额",
"外购付款方式", "最晚发货期", "已收款", "未收款", "收款日期"
]
for main_key, prefix in [('ASD', 'ASD_产品表'), ('NON_ASD', 'Non_ASD_产品表')]:
data_map = self.stored_data[main_key]
# data_map 结构: {'Domestic': [records], 'Foreign': [], ...}
# 检查是否为空
total = sum(len(v) for v in data_map.values())
if total == 0: continue
path = os.path.join(folder, f"{prefix}_{ts}.xlsx")
try:
with pd.ExcelWriter(path, engine='openpyxl') as writer:
# 内贸 Sheet
if data_map['Domestic']:
df = pd.DataFrame(data_map['Domestic'])
# 插入内贸号
cols = export_cols[:2] + ["内贸合同号"] + export_cols[2:]
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='内贸', index=False)
# 外贸 Sheet
if data_map['Foreign']:
df = pd.DataFrame(data_map['Foreign'])
# 插入外贸号
cols = export_cols[:2] + ["外贸合同号"] + export_cols[2:]
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='外贸', index=False)
# 其他 Sheet
if data_map['Other']:
df = pd.DataFrame(data_map['Other'])
cols = export_cols[:2] + ["内贸合同号"] + export_cols[2:] # 默认用内贸结构
df = df.reindex(columns=cols)
df.to_excel(writer, sheet_name='其他', index=False)
self.log_msg(f" ✅ 导出成功: {os.path.basename(path)}")
except Exception as e:
self.log_msg(f" ❌ 导出失败: {e}")
Messagebox.show_info("导出完成", "任务结束")
if __name__ == "__main__":
app = CRMGUI()
app.mainloop()