Files
Orbitin/src/gui.py

416 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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('<Control-Return>', 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()