diff --git a/browser_login/auto_fetch_abnormal_report.py b/browser_login/auto_fetch_abnormal_report.py index 202d1bc..2f670e8 100644 --- a/browser_login/auto_fetch_abnormal_report.py +++ b/browser_login/auto_fetch_abnormal_report.py @@ -73,6 +73,22 @@ def fetch_report_data(page): first_day = datetime.date(now.year, now.month, 1).strftime('%Y-%m-%d') last_day = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]).strftime('%Y-%m-%d') + # ==== 断点续传逻辑 ==== + state_file = OUTPUT_DIR / 'abnormal_sync_state.json' + start_page = 1 + if state_file.exists(): + try: + with open(state_file, 'r', encoding='utf-8') as f: + state = json.load(f) + if state.get('month') == f"{now.year}-{now.month}": + saved_page = state.get('current_page', 1) + if saved_page > 1: + start_page = saved_page + print(f"发现上次中断记录,准备从第 {start_page} 页恢复抓取...") + except Exception as e: + print(f"读取状态文件失败: {e}") + # ==================== + print(f"设置下单日期为当月: {first_day} 至 {last_day},并清理发料情况过滤条件...") # 使用注入到全部 iframe 的 JS 强制执行 EasyUI 方法 @@ -121,7 +137,6 @@ def fetch_report_data(page): }} // 4. [提速黑科技]:强行把每页请求的数量从 50 条改为 500 条 - // 找到底部的分页组件并修改它的 pageSize,这样点击查询时就会一次请求 500 条 var paginations = doc.querySelectorAll('.pagination'); for(var i=0; i 1): + try: + with open(state_file, 'w', encoding='utf-8') as f: + json.dump({ + 'month': f"{now.year}-{now.month}", + 'current_page': current_page, + 'total_pages': total_pages + }, f) + except Exception as e: + print(f"保存进度失败: {e}") + pass + else: - print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 数据结构不匹配。") + print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 数据结构不匹配。") except Exception as e: - print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 解析数据包出错: {e}") + print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 解析数据包出错: {e}") pass if not found_data: - print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 第 {current_page} 页等待了超时,没有拦截到匹配的报表数据...") + # 检查是否由于会话超时被系统强制跳转回首页 + if "Home/Index" in target_tab.url or target_tab.url == "https://yunmes.tftykj.cn/": + print("❌ 警告:页面已跳转回首页,可能是会话超时或被强制登出。") + print(f"进度已保存 (停留在第 {current_page} 页),下次启动抓取任务将自动从中断处继续!") + return False + + print(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 第 {current_page} 页等待了超时,没有拦截到匹配的报表数据...") # 再给一次机会等3秒 print("再等待3秒重试...") @@ -226,8 +266,36 @@ def fetch_report_data(page): # 重新让上面解析 continue + # ====== 触发断点续传跳页 ====== + if current_page == 1 and start_page > 1: + print(f"===================================") + print(f"⏭️ 触发断点续传,跳过第 1 页,直接跳转到第 {start_page} 页...") + print(f"===================================") + current_page = start_page + target_tab.run_js(f""" + var iframes = document.querySelectorAll('iframe'); + for(var j=0; j= total_pages: print(f"已到达最后一页 (共 {total_pages} 页),抓取完成!") + try: + if state_file.exists(): + state_file.unlink() # 抓取完毕后清除记录 + except: + pass break print(f"准备抓取下一页 (第 {current_page + 1} 页)...") diff --git a/browser_login/fetch_receipt_details_full.py b/browser_login/fetch_receipt_details_full.py index 6de64c5..96accb7 100644 --- a/browser_login/fetch_receipt_details_full.py +++ b/browser_login/fetch_receipt_details_full.py @@ -40,7 +40,7 @@ def fetch_receipt_details_full(): menus = [ ("第一层: 业务统计报表", 'xpath://*[@id="app"]/div/div[1]/div[1]/div[2]/div/div[1]/div/div[10]/div/p'), ("第二层: 采购业务报表", 'xpath:/html/body/div[7]/div/div[1]/div/div[4]/div/p'), - ("第三层: 收货明细报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[4]/div/p') + ("第三层: 财务收货明细报表", 'xpath:/html/body/div[8]/div/div[1]/div/div[4]/div/p') ] log("INFO", "开始模拟人工点击左侧导航菜单...") diff --git a/web_ui/app.py b/web_ui/app.py index 94838e7..b4de997 100644 --- a/web_ui/app.py +++ b/web_ui/app.py @@ -489,11 +489,34 @@ def sync_abnormal_report(): try: import auto_fetch_abnormal_report page = auto_fetch_abnormal_report.get_page(port=9222) - success = auto_fetch_abnormal_report.navigate_to_report(page) - if success: - auto_fetch_abnormal_report.fetch_report_data(page) + + # 自动重试机制 + while True: + try: + success = auto_fetch_abnormal_report.navigate_to_report(page) + if success: + result = auto_fetch_abnormal_report.fetch_report_data(page) + # 如果返回 False,说明被踢回首页或发生中断,需要重试 + if result is False: + print("♻️ 检测到页面被系统踢回首页或中断,正在为您自动重新导航并恢复抓取...") + import time + time.sleep(3) + continue + else: + break + else: + print("❌ 导航进入报表失败,尝试重新导航...") + import time + time.sleep(3) + continue + except Exception as inner_e: + print(f"⚠️ 抓取循环中发生异常: {inner_e},尝试重新恢复...") + import time + time.sleep(3) + continue + except Exception as e: - print(f"手动发料异常报表抓取失败: {e}") + print(f"❌ 手动发料异常报表抓取严重失败: {e}") finally: release_browser() @@ -1207,15 +1230,17 @@ if __name__ == '__main__': # 启动前开启一个线程去拉起浏览器 threading.Thread(target=open_browser, args=(port,), daemon=True).start() - print(f"🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:{port}") + print("🚀 前端展示后端服务已启动!") + print(f"👉 本机访问地址: http://127.0.0.1:{port}") + print(f"👉 局域网访问地址: http://192.168.7.198:{port} (允许手机/其他电脑访问)") else: # 如果是热加载的主控进程,随便给个默认端口(反正它不干活),并且不打开浏览器 port = 5050 - # 更改为动态端口,避开被占用的端口。修改 host 为 127.0.0.1 避免 Windows 权限拦截 + # 更改为动态端口,避开被占用的端口。修改 host 为 0.0.0.0 允许局域网访问 app.run( debug=not is_frozen, - host='127.0.0.1', + host='0.0.0.0', port=port, threaded=True, use_reloader=False # 彻底关闭 Flask 内置的热加载,避免双进程互相影响 diff --git a/web_ui/templates/abnormal_report.html b/web_ui/templates/abnormal_report.html index 2a280f8..73120e7 100644 --- a/web_ui/templates/abnormal_report.html +++ b/web_ui/templates/abnormal_report.html @@ -42,6 +42,7 @@ + 搜索 @@ -123,7 +124,10 @@ diff --git a/web_ui/templates/bom_compare.html b/web_ui/templates/bom_compare.html index d30c3ae..ced1e6b 100644 --- a/web_ui/templates/bom_compare.html +++ b/web_ui/templates/bom_compare.html @@ -171,6 +171,7 @@

📊 BOM 成本期间对比分析表

+ 返回主控台 返回雷达图 返回收货明细
@@ -435,7 +436,8 @@ this.searchParents(); }, methods: { - goToReceipts() { window.location.href = '/'; }, + goBack() { window.location.href = '/'; }, + goToReceipts() { window.location.href = '/receipts'; }, goToBomTree() { window.location.href = '/bom'; }, getStatusText(status) { diff --git a/web_ui/templates/bom_tree.html b/web_ui/templates/bom_tree.html index 7742db6..ed72d13 100644 --- a/web_ui/templates/bom_tree.html +++ b/web_ui/templates/bom_tree.html @@ -122,7 +122,8 @@

🕸️ ERP 动态 BOM 成本核算雷达图

- 切换至 BOM 期间成本对比 + 返回主控台 + 切换至 BOM 期间成本对比 返回收货明细
@@ -233,9 +234,12 @@ }); }, methods: { - goToReceipts() { + goBack() { window.location.href = '/'; }, + goToReceipts() { + window.location.href = '/receipts'; + }, goToCompare() { window.location.href = '/compare'; }, diff --git a/web_ui/templates/global_log.html b/web_ui/templates/global_log.html index 40f43b3..3752fc7 100644 --- a/web_ui/templates/global_log.html +++ b/web_ui/templates/global_log.html @@ -15,8 +15,7 @@
暂无后台任务输出...
-
- {{ log }} +
@@ -62,6 +61,17 @@ } }, methods: { + getLogStyle(log) { + if (!log) return ''; + if (log.includes('✅') || log.includes('大功告成')) return 'color: #67C23A; font-weight: bold;'; // 绿色 + if (log.includes('❌') || log.includes('ERR') || log.includes('失败') || log.includes('异常')) return 'color: #F56C6C; font-weight: bold;'; // 红色 + if (log.includes('⏭️') || log.includes('断点续传') || log.includes('中断记录')) return 'color: #E040FB; font-weight: bold;'; // 紫色 + if (log.includes('正在收集并解析') || log.includes('准备抓取下一页') || log.includes('等待重试') || log.includes('警告')) return 'color: #E6A23C;'; // 橙黄色 + if (log.includes('===================================')) return 'color: #409EFF; font-weight: bold;'; // 蓝色 + if (log.includes('命中目标 URL') || log.includes('POST 请求')) return 'color: #909399; font-style: italic;'; // 灰色斜体 + if (log.includes('开始') || log.includes('完成')) return 'color: #FFF; font-weight: bold;'; // 白色高亮 + return 'color: #a9b7c6;'; // 默认灰白色 + }, fetchLogs() { // 如果弹窗没打开,也可以不刷新日志以节省性能,或者一直拉取保持最新 if (!this.logDialogVisible) return; diff --git a/web_ui/templates/home.html b/web_ui/templates/home.html index 5bf28c9..b0c33c5 100644 --- a/web_ui/templates/home.html +++ b/web_ui/templates/home.html @@ -171,8 +171,8 @@ diff --git a/web_ui/templates/index.html b/web_ui/templates/index.html index dac3820..564fda8 100644 --- a/web_ui/templates/index.html +++ b/web_ui/templates/index.html @@ -23,6 +23,9 @@ background-color: #f5f7fa; } .header { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #ebeef5; @@ -46,8 +49,11 @@
-

📦 ERP 数据展示看板 - 收货明细报表

- 切换至 BOM 成本雷达图 +

📦 ERP 数据展示看板 - 财务收货明细报表

+
+ 返回主控台 + 切换至 BOM 成本雷达图 +
@@ -132,6 +138,9 @@ this.fetchData(); }, methods: { + goBack() { + window.location.href = '/'; + }, goToBomTree() { window.location.href = '/bom'; }, diff --git a/web_ui/templates/reconciliation.html b/web_ui/templates/reconciliation.html index 06408b9..165298a 100644 --- a/web_ui/templates/reconciliation.html +++ b/web_ui/templates/reconciliation.html @@ -244,6 +244,16 @@ } }, mounted() { + // 默认初始化为当月的第一天和最后一天 + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth() + 1; + const pad = (n) => n.toString().padStart(2, '0'); + const lastDayDate = new Date(y, m, 0); + const firstDay = `${y}-${pad(m)}-01`; + const lastDay = `${y}-${pad(m)}-${pad(lastDayDate.getDate())}`; + this.dateRangeSelect = [firstDay, lastDay]; + // 初始化加载数据 this.loadSummary(); this.loadUnmatchedData();