1.0带页面内容
This commit is contained in:
709
前端页面.py
Normal file
709
前端页面.py
Normal file
@ -0,0 +1,709 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user