#!/usr/bin/env python3 """ 码头作业日志管理工具 - GUI 界面 """ import tkinter as tk from tkinter import ttk, messagebox, scrolledtext import threading from datetime import datetime, timedelta import sys import os # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src.confluence import ConfluenceClient from src.extractor import HTMLTextExtractor from src.parser import HandoverLogParser from src.database import DailyLogsDatabase from src.report import DailyReportGenerator class OrbitInGUI: """码头作业日志管理工具 GUI""" def __init__(self, root): self.root = root self.root.title("码头作业日志管理工具 - OrbitIn") self.root.geometry("900x700") self.root.resizable(True, True) # 设置样式 style = ttk.Style() style.theme_use('clam') # 创建主框架 main_frame = ttk.Frame(root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 左侧控制面板 left_frame = ttk.LabelFrame(main_frame, text="操作面板", padding="10") left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) # 右侧主区域 right_frame = ttk.Frame(main_frame) right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # === 左侧控制面板 === # 获取数据按钮 btn_fetch = ttk.Button( left_frame, text="获取并处理数据", command=self.fetch_data, width=20 ) btn_fetch.pack(pady=5) btn_fetch_debug = ttk.Button( left_frame, text="获取 (Debug模式)", command=self.fetch_debug, width=20 ) btn_fetch_debug.pack(pady=5) # 分隔线 ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) # 生成日报 ttk.Label(left_frame, text="生成日报:").pack(anchor=tk.W, pady=(10, 5)) date_frame = ttk.Frame(left_frame) date_frame.pack(fill=tk.X, pady=5) self.date_var = tk.StringVar(value=datetime.now().strftime('%Y-%m-%d')) date_entry = ttk.Entry(date_frame, textvariable=self.date_var, width=12) date_entry.pack(side=tk.LEFT) btn_report = ttk.Button( left_frame, text="生成日报", command=self.generate_report, width=20 ) btn_report.pack(pady=5) btn_report_today = ttk.Button( left_frame, text="今日日报", command=self.generate_today_report, width=20 ) btn_report_today.pack(pady=5) # 分隔线 ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) # 未统计数据 ttk.Label(left_frame, text="添加未统计数据:").pack(anchor=tk.W, pady=(10, 5)) unaccounted_frame = ttk.Frame(left_frame) unaccounted_frame.pack(fill=tk.X, pady=5) ttk.Label(unaccounted_frame, text="月份:").pack(side=tk.LEFT) self.month_var = tk.StringVar(value=datetime.now().strftime('%Y-%m')) month_entry = ttk.Entry(unaccounted_frame, textvariable=self.month_var, width=8) month_entry.pack(side=tk.LEFT, padx=(5, 10)) ttk.Label(unaccounted_frame, text="TEU:").pack(side=tk.LEFT) self.teu_var = tk.StringVar() teu_entry = ttk.Entry(unaccounted_frame, textvariable=self.teu_var, width=8) teu_entry.pack(side=tk.LEFT, padx=(5, 0)) btn_unaccounted = ttk.Button( left_frame, text="添加", command=self.add_unaccounted, width=20 ) btn_unaccounted.pack(pady=5) # 分隔线 ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) # 数据库统计 btn_stats = ttk.Button( left_frame, text="数据库统计", command=self.show_stats, width=20 ) btn_stats.pack(pady=5) # 清空输出按钮 btn_clear = ttk.Button( left_frame, text="清空输出", command=self.clear_output, width=20 ) btn_clear.pack(pady=5) # === 右侧主区域 === # 状态标签 self.status_var = tk.StringVar(value="就绪") status_label = ttk.Label(right_frame, textvariable=self.status_var) status_label.pack(anchor=tk.W) # 日报完整内容(可复制) ttk.Label(right_frame, text="日报内容 (可复制):").pack(anchor=tk.W, pady=(5, 0)) # 完整日报文本框(可编辑和复制) self.report_text = scrolledtext.ScrolledText( right_frame, wrap=tk.WORD, font=('SimHei', 10), bg='white', height=18 ) self.report_text.pack(fill=tk.X, pady=(5, 10)) # 按钮栏 btn_bar = ttk.Frame(right_frame) btn_bar.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_bar, text="复制日报", command=self.copy_report).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(btn_bar, text="复制全部", command=self.copy_all).pack(side=tk.LEFT) # 输出文本框 ttk.Label(right_frame, text="日志输出:").pack(anchor=tk.W) self.output_text = scrolledtext.ScrolledText( right_frame, wrap=tk.WORD, font=('Consolas', 9), bg='#f5f5f5', height=8 ) self.output_text.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) # 绑定快捷键 self.root.bind('', lambda e: self.fetch_data()) self.root.bind('', lambda e: self.copy_report() if self.report_text.focus_get() else None) # 初始消息 self.log_message("码头作业日志管理工具 - OrbitIn") self.log_message("=" * 50) self.log_message("按 Ctrl+Enter 快速获取数据") # 启动时自动获取新数据 self.root.after(100, self.auto_fetch_data) def log_message(self, message, is_error=False): """输出日志消息""" timestamp = datetime.now().strftime('%H:%M:%S') prefix = "[ERROR]" if is_error else "[INFO]" self.output_text.insert(tk.END, f"[{timestamp}] {prefix} {message}\n") self.output_text.see(tk.END) self.root.update() def clear_output(self): """清空输出""" self.output_text.delete(1.0, tk.END) self.log_message("输出已清空") def set_status(self, status): """设置状态""" self.status_var.set(status) self.root.update() def copy_report(self): """复制日报内容""" self.report_text.tag_add(tk.SEL, "1.0", tk.END) self.report_text.event_generate("<>") self.report_text.tag_remove(tk.SEL, "1.0", tk.END) self.log_message("日报已复制到剪贴板") def copy_all(self): """复制完整内容""" content = self.report_text.get("1.0", tk.END).strip() if content: self.root.clipboard_clear() self.root.clipboard_append(content) self.log_message("完整日报已复制到剪贴板") def fetch_data(self): """获取并处理数据""" self.set_status("正在获取数据...") self.log_message("开始获取数据...") try: # 加载配置 from dotenv import load_dotenv load_dotenv() base_url = os.getenv('CONFLUENCE_BASE_URL') token = os.getenv('CONFLUENCE_TOKEN') content_id = os.getenv('CONFLUENCE_CONTENT_ID') if not base_url or not token or not content_id: self.log_message("错误: 未配置 Confluence 信息,请检查 .env 文件", is_error=True) return # 获取 HTML self.log_message("正在从 Confluence 获取 HTML...") client = ConfluenceClient(base_url, token) html = client.get_html(content_id) if not html: self.log_message("错误: 未获取到 HTML 内容", is_error=True) return self.log_message(f"获取成功,共 {len(html)} 字符") # 提取文本 self.log_message("正在提取布局文本...") extractor = HTMLTextExtractor() layout_text = extractor.extract(html) self.log_message(f"提取完成,共 {len(layout_text)} 字符") # 解析数据 self.log_message("正在解析日志数据...") parser = HandoverLogParser() logs = parser.parse(layout_text) self.log_message(f"解析到 {len(logs)} 条记录") # 保存到数据库 if logs: self.log_message("正在保存到数据库...") db = DailyLogsDatabase() count = db.insert_many([log.to_dict() for log in logs]) db.close() self.log_message(f"已保存 {count} 条记录") # 显示统计 db = DailyLogsDatabase() stats = db.get_stats() db.close() self.log_message(f"数据库总计: {stats['total']} 条记录, {len(stats['ships'])} 艘船") # 刷新日报显示 self.generate_today_report() else: self.log_message("未解析到任何记录") self.set_status("完成") except Exception as e: self.log_message(f"错误: {e}", is_error=True) self.set_status("错误") def fetch_debug(self): """Debug模式获取数据""" self.set_status("正在获取 Debug 数据...") self.log_message("使用本地 layout_output.txt 进行 Debug...") try: # 检查本地文件 if os.path.exists('layout_output.txt'): filepath = 'layout_output.txt' elif os.path.exists('debug/layout_output.txt'): filepath = 'debug/layout_output.txt' else: self.log_message("错误: 未找到 layout_output.txt 文件", is_error=True) return self.log_message(f"使用文件: {filepath}") with open(filepath, 'r', encoding='utf-8') as f: text = f.read() self.log_message(f"读取完成,共 {len(text)} 字符") # 解析数据 self.log_message("正在解析日志数据...") parser = HandoverLogParser() logs = parser.parse(text) self.log_message(f"解析到 {len(logs)} 条记录") if logs: self.log_message("正在保存到数据库...") db = DailyLogsDatabase() count = db.insert_many([log.to_dict() for log in logs]) db.close() self.log_message(f"已保存 {count} 条记录") # 刷新日报显示 self.generate_today_report() self.set_status("完成") except Exception as e: self.log_message(f"错误: {e}", is_error=True) self.set_status("错误") def generate_report(self): """生成指定日期的日报""" date = self.date_var.get().strip() if not date: self.log_message("错误: 请输入日期", is_error=True) return try: datetime.strptime(date, '%Y-%m-%d') except ValueError: self.log_message("错误: 日期格式无效,请使用 YYYY-MM-DD", is_error=True) return self.set_status("正在生成日报...") self.log_message(f"生成 {date} 的日报...") try: g = DailyReportGenerator() report = g.generate_report(date) g.close() # 在日报文本框中显示(可复制) self.report_text.delete("1.0", tk.END) self.report_text.insert("1.0", report) # 在日志中显示原始内容 self.log_message("") self.log_message("=" * 40) self.log_message(report) self.log_message("=" * 40) self.set_status("完成") except Exception as e: self.log_message(f"错误: {e}", is_error=True) self.set_status("错误") def generate_today_report(self): """生成昨日日报(因为是第二天汇报)""" yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') self.date_var.set(yesterday) self.generate_report() def add_unaccounted(self): """添加未统计数据""" year_month = self.month_var.get().strip() teu = self.teu_var.get().strip() if not year_month or not teu: self.log_message("错误: 请输入月份和 TEU", is_error=True) return try: teu = int(teu) except ValueError: self.log_message("错误: TEU 必须是数字", is_error=True) return self.set_status("正在添加...") self.log_message(f"添加 {year_month} 月未统计数据: {teu}TEU") try: db = DailyLogsDatabase() result = db.insert_unaccounted(year_month, teu, '') db.close() if result: self.log_message("添加成功!") # 刷新日报显示 self.generate_today_report() else: self.log_message("添加失败!", is_error=True) self.set_status("完成") except Exception as e: self.log_message(f"错误: {e}", is_error=True) self.set_status("错误") def auto_fetch_data(self): """自动获取新数据(GUI启动时调用)""" self.set_status("正在自动获取新数据...") self.log_message("GUI启动,开始自动获取新数据...") try: # 1. 检查飞书配置,如果配置完整则刷新排班信息 from dotenv import load_dotenv load_dotenv() feishu_token = os.getenv('FEISHU_TOKEN') feishu_spreadsheet_token = os.getenv('FEISHU_SPREADSHEET_TOKEN') if feishu_token and feishu_spreadsheet_token: try: self.log_message("正在刷新排班信息...") from src.feishu_v2 import FeishuScheduleManagerV2 feishu_manager = FeishuScheduleManagerV2() # 只刷新未来7天的排班,减少API调用 feishu_manager.refresh_all_schedules(days=7) self.log_message("排班信息刷新完成") except Exception as e: self.log_message(f"刷新排班信息时出错: {e}", is_error=True) self.log_message("将继续处理其他任务...") else: self.log_message("飞书配置不完整,跳过排班信息刷新") # 2. 尝试获取最新的作业数据 self.log_message("正在尝试获取最新作业数据...") base_url = os.getenv('CONFLUENCE_BASE_URL') token = os.getenv('CONFLUENCE_TOKEN') content_id = os.getenv('CONFLUENCE_CONTENT_ID') if base_url and token and content_id: try: # 获取 HTML self.log_message("正在从 Confluence 获取 HTML...") from src.confluence import ConfluenceClient client = ConfluenceClient(base_url, token) html = client.get_html(content_id) if html: self.log_message(f"获取成功,共 {len(html)} 字符") # 提取文本 self.log_message("正在提取布局文本...") from src.extractor import HTMLTextExtractor extractor = HTMLTextExtractor() layout_text = extractor.extract(html) # 解析数据 self.log_message("正在解析日志数据...") from src.parser import HandoverLogParser parser = HandoverLogParser() logs = parser.parse(layout_text) if logs: # 保存到数据库 self.log_message("正在保存到数据库...") db = DailyLogsDatabase() count = db.insert_many([log.to_dict() for log in logs]) db.close() self.log_message(f"已保存 {count} 条新记录") else: self.log_message("未解析到新记录") else: self.log_message("未获取到 HTML 内容,跳过数据获取") except Exception as e: self.log_message(f"获取作业数据时出错: {e}", is_error=True) else: self.log_message("Confluence 配置不完整,跳过数据获取") # 3. 显示今日日报 self.log_message("正在生成今日日报...") self.generate_today_report() self.set_status("就绪") self.log_message("自动获取完成,GUI已就绪") except Exception as e: self.log_message(f"自动获取过程中出现错误: {e}", is_error=True) self.log_message("将继续显示GUI界面...") self.set_status("就绪") # 即使出错也显示今日日报 self.generate_today_report() def show_stats(self): """显示数据库统计""" self.set_status("正在统计...") self.log_message("数据库统计信息:") self.log_message("-" * 30) try: db = DailyLogsDatabase() stats = db.get_stats() # 获取当月船次统计 current_month = datetime.now().strftime('%Y-%m') ships_monthly = db.get_ships_with_monthly_teu(current_month) db.close() self.log_message(f"总记录数: {stats['total']}") self.log_message(f"船次数量: {len(stats['ships'])}") self.log_message(f"日期范围: {stats['date_range']['start']} ~ {stats['date_range']['end']}") if ships_monthly: self.log_message("") self.log_message(f"{current_month}月船次统计:") total_monthly_teu = 0 for ship in ships_monthly: monthly_teu = ship['monthly_teu'] or 0 total_monthly_teu += monthly_teu self.log_message(f" {ship['ship_name']}: {monthly_teu}TEU") self.log_message(f" ---") self.log_message(f" 本月合计: {total_monthly_teu}TEU") self.set_status("完成") except Exception as e: self.log_message(f"错误: {e}", is_error=True) self.set_status("错误") def main(): """主函数""" root = tk.Tk() app = OrbitInGUI(root) root.mainloop() if __name__ == '__main__': main()