mirror of
https://devops.liangqichi.top/qichi.liang/Orbitin.git
synced 2026-02-10 07:41:29 +08:00
- 新增 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. 日报中正确显示次日班次人员信息
550 lines
20 KiB
Python
550 lines
20 KiB
Python
#!/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()
|