#!/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 ReportFrame(ttk.LabelFrame): """日报显示框架""" def __init__(self, parent): super().__init__(parent, text="日报预览", padding="10") self.parent = parent self._create_widgets() def _create_widgets(self): """创建组件""" # 日期标题 self.date_label = ttk.Label(self, text="--月--日", font=('SimHei', 14, 'bold')) self.date_label.pack(pady=(0, 10)) # 船次信息表格 self.ships_frame = ttk.Frame(self) self.ships_frame.pack(fill=tk.X, pady=5) # 统计信息 self.stats_frame = ttk.Frame(self) self.stats_frame.pack(fill=tk.X, pady=10) # 人员信息 self.personnel_frame = ttk.Frame(self) self.personnel_frame.pack(fill=tk.X, pady=10) def display_report(self, report_text: str, date: str): """显示日报""" # 解析日报内容 lines = report_text.strip().split('\n') # 更新日期 self.date_label.config(text=f"{date[5:7]}月{date[8:10]}日") # 清空旧数据 for widget in self.ships_frame.winfo_children(): widget.destroy() for widget in self.stats_frame.winfo_children(): widget.destroy() for widget in self.personnel_frame.winfo_children(): widget.destroy() # 解析并显示各部分 current_section = None ships_data = [] stats_data = {} for line in lines: line = line.strip() if not line: continue if line.startswith('日期:'): continue elif line.startswith('船名:'): current_section = 'ships' ships_data.append({'name': line.replace('船名:', '').strip(), 'teu': None}) elif line.startswith('作业量:'): if ships_data: ships_data[-1]['teu'] = line.replace('作业量:', '').replace('TEU', '').strip() elif line.startswith('当日实际作业量:'): stats_data['daily_total'] = line.replace('当日实际作业量:', '').replace('TEU', '').strip() elif line.startswith('当月计划作业量:'): stats_data['monthly_plan'] = line.replace('当月计划作业量:', '').replace('TEU', '').replace(' (用天数*300TEU)', '').strip() elif line.startswith('当月实际作业量:'): stats_data['monthly_actual'] = line.replace('当月实际作业量:', '').replace('TEU', '').strip() elif line.startswith('当月完成比例:'): stats_data['completion'] = line.replace('当月完成比例:', '').replace('%', '').strip() elif '白班人员' in line: stats_data['day_shift'] = line.split(':')[1].strip() if ':' in line else '' elif '夜班人员' in line: stats_data['night_shift'] = line.split(':')[1].strip() if ':' in line else '' elif '值班手机' in line: stats_data['phone'] = line.split(':')[1].strip() if ':' in line else '' # 显示船次信息 if ships_data: ttk.Label(self.ships_frame, text="船次信息", font=('', 10, 'bold')).pack(anchor=tk.W) for ship in ships_data: ship_frame = ttk.Frame(self.ships_frame) ship_frame.pack(fill=tk.X, pady=2) ttk.Label(ship_frame, text=f"🚢 {ship['name']}", foreground='#1976D2').pack(side=tk.LEFT) ttk.Label(ship_frame, text=f"{ship['teu']} TEU", font=('', 10, 'bold'), foreground='#388E3C').pack(side=tk.RIGHT) # 显示统计信息 if stats_data: ttk.Label(self.stats_frame, text="统计信息", font=('', 10, 'bold')).pack(anchor=tk.W) stats_grid = ttk.Frame(self.stats_frame) stats_grid.pack(fill=tk.X, pady=5) # 第一行 ttk.Label(stats_grid, text="当日作业:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) ttk.Label(stats_grid, text=f"{stats_data.get('daily_total', '0')} TEU", foreground='#388E3C', font=('', 10, 'bold')).grid(row=0, column=1, sticky=tk.W) ttk.Label(stats_grid, text="月计划:").grid(row=0, column=2, sticky=tk.W, padx=(20, 10)) ttk.Label(stats_grid, text=f"{stats_data.get('monthly_plan', '0')} TEU").grid(row=0, column=3, sticky=tk.W) # 第二行 ttk.Label(stats_grid, text="月实际:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10), pady=(5, 0)) ttk.Label(stats_grid, text=f"{stats_data.get('monthly_actual', '0')} TEU", foreground='#388E3C', font=('', 10, 'bold')).grid(row=1, column=1, sticky=tk.W, pady=(5, 0)) ttk.Label(stats_grid, text="完成率:").grid(row=1, column=2, sticky=tk.W, padx=(20, 10), pady=(5, 0)) completion = float(stats_data.get('completion', 0)) color = '#388E3C' if completion >= 100 else '#FF9800' if completion >= 80 else '#F44336' ttk.Label(stats_grid, text=f"{stats_data.get('completion', '0')}%", foreground=color, font=('', 10, 'bold')).grid(row=1, column=3, sticky=tk.W, pady=(5, 0)) # 显示人员信息 ttk.Label(self.personnel_frame, text="班次人员", font=('', 10, 'bold')).pack(anchor=tk.W) personnel_grid = ttk.Frame(self.personnel_frame) personnel_grid.pack(fill=tk.X, pady=5) ttk.Label(personnel_grid, text="☀️ 白班:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) ttk.Label(personnel_grid, text=stats_data.get('day_shift', '-')).grid(row=0, column=1, sticky=tk.W) ttk.Label(personnel_grid, text="🌙 夜班:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10), pady=(5, 0)) ttk.Label(personnel_grid, text=stats_data.get('night_shift', '-')).grid(row=1, column=1, sticky=tk.W, pady=(5, 0)) ttk.Label(personnel_grid, text="📞 值班:").grid(row=2, column=0, sticky=tk.W, padx=(0, 10), pady=(5, 0)) ttk.Label(personnel_grid, text=stats_data.get('phone', '-'), foreground='#1976D2').grid(row=2, column=1, sticky=tk.W, pady=(5, 0)) class OrbitInGUI: """码头作业日志管理工具 GUI""" def __init__(self, root): self.root = root self.root.title("码头作业日志管理工具 - OrbitIn") self.root.geometry("900x650") 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) # 日报显示框架 self.report_frame = ReportFrame(right_frame) self.report_frame.pack(fill=tk.X, pady=(5, 10)) # 输出文本框 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=10 ) 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("按 Ctrl+Enter 快速获取数据") # 默认显示今日日报 self.generate_today_report() 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 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.log_message("") self.log_message("=" * 40) self.log_message(report) self.log_message("=" * 40) # 在可视化区域显示 self.report_frame.display_report(report, date) 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("添加成功!") # 刷新日报显示 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 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()