From 5a6aee333c737daf8ffdbb0d5441830829836bed Mon Sep 17 00:00:00 2001 From: "qichi.liang" Date: Mon, 29 Dec 2025 02:25:51 +0800 Subject: [PATCH] Add tkinter GUI for OrbitIn --- src/gui.py | 415 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 src/gui.py diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..43c1b27 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +""" +码头作业日志管理工具 - GUI 界面 +""" +import tkinter as tk +from tkinter import ttk, messagebox, scrolledtext +import threading +from datetime import datetime +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("800x600") + 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.Frame(main_frame) + 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) + + # 输出文本框 + self.output_text = scrolledtext.ScrolledText( + right_frame, + wrap=tk.WORD, + font=('Consolas', 10), + bg='#f5f5f5' + ) + self.output_text.pack(fill=tk.BOTH, expand=True, pady=(5, 0)) + + # 绑定快捷键 + self.root.bind('', lambda e: self.fetch_data()) + + # 初始消息 + self.log_message("码头作业日志管理工具 - OrbitIn") + self.log_message("=" * 50) + self.log_message("日期格式: YYYY-MM-DD") + self.log_message("月份格式: YYYY-MM") + self.log_message("") + self.log_message("按 Ctrl+Enter 快速获取数据") + + def log_message(self, message, is_error=False): + """输出日志消息""" + timestamp = datetime.now().strftime('%H:%M:%S') + prefix = "❌" if is_error else "📝" + 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 run_in_thread(self, func, *args, **kwargs): + """在后台线程中运行函数""" + def worker(): + try: + func(*args, **kwargs) + except Exception as e: + self.root.after(0, lambda: self.log_message(str(e), is_error=True)) + self.root.after(0, lambda: self.set_status("错误")) + finally: + self.root.after(0, lambda: self.set_status("就绪")) + + thread = threading.Thread(target=worker, daemon=True) + thread.start() + + 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'])} 艘船") + 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.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.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): + """生成今日日报""" + today = datetime.now().strftime('%Y-%m-%d') + self.date_var.set(today) + 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("添加成功!") + 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 show_stats(self): + """显示数据库统计""" + self.set_status("正在统计...") + self.log_message("数据库统计信息:") + self.log_message("-" * 30) + + try: + db = DailyLogsDatabase() + stats = db.get_stats() + 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 stats['ships']: + self.log_message("") + self.log_message("船次列表:") + for ship in sorted(stats['ships']): + self.log_message(f" - {ship}") + + 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()