Files
Orbitin/src/gui.py
qichi.liang dc2a55bbf4 feat: 添加飞书表格模块支持排班人员信息获取
- 新增 src/feishu_v2.py: 飞书表格API客户端,支持数据库存储和2026年全年排班表
- 新增 src/schedule_database.py: 排班信息数据库模块,用于缓存排班数据
- 新增 docs/feishu_data_flow.md: 飞书数据流文档
- 新增 plans/feishu_scheduling_plan.md: 飞书排班表模块设计文档
- 更新 src/report.py: 使用新的飞书模块获取排班人员信息
- 更新 src/gui.py: 启动时自动获取新数据,添加auto_fetch_data方法
- 更新 .env.example: 添加飞书配置示例
- 更新 AGENTS.md: 更新项目文档
- 更新 main.py: 集成飞书模块

功能特性:
1. 支持从飞书表格获取排班人员信息
2. 支持2025年月度表格和2026年全年排班表
3. 使用SQLite数据库缓存,减少API调用
4. 自动检测表格更新
5. GUI启动时自动获取最新数据
6. 日报中正确显示次日班次人员信息
2025-12-31 00:03:34 +08:00

550 lines
20 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, 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('<Control-Return>', lambda e: self.fetch_data())
self.root.bind('<Control-c>', 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("<<Copy>>")
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()