You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

602 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import tkinter as tk
import customtkinter as ctk
from tkinter import messagebox
import tkinter.scrolledtext as scrolledtext
from datetime import datetime
from base_manager import BaseManager
"""
日程管理模块
该模块提供了日程管理的UI界面包括显示日程列表、查看日程详情、创建新日程和删除日程等功能。
使用tkinter和customtkinter库实现现代化的用户界面。
"""
class ScheduleManager(BaseManager):
"""日程管理类
负责日程管理界面的创建和交互逻辑,包括:
1. 显示用户日程列表
2. 查看日程详细信息
3. 创建新日程
4. 删除日程
属性:
app: 主应用程序对象
root: 主窗口对象
auth: 用户认证对象
current_session_id: 当前会话ID
schedule_listbox: 日程列表框控件
schedule_detail_text: 日程详情文本框控件
"""
def __init__(self, app):
"""初始化日程管理类
Args:
app: 主应用程序对象包含root、auth等核心组件
"""
self.app = app
self.root = app.root # 主窗口
self.auth = app.auth # 用户认证对象
super().__init__(app, app.root) # 调用基类初始化
self.logger = getattr(app, 'logger_manager', None) # 日志记录对象(使用正确的属性名)
self.current_session_id = app.current_session_id # 当前会话ID
self.schedule_listbox = None # 日程列表框
self.schedule_detail_text = None # 日程详情文本框
self._original_schedules = [] # 原始日程数据
self._current_schedules = [] # 当前显示的日程数据
def update_session_id(self, session_id):
"""更新当前会话ID
更新应用程序和当前类实例的会话ID确保操作使用正确的用户上下文。
Args:
session_id (str): 新的会话ID
"""
self.app.current_session_id = session_id # 更新应用程序的会话ID
self.current_session_id = session_id # 更新当前实例的会话ID
if session_id and self.logger:
# 获取用户名
username = session_id
if self.auth:
valid, username = self.auth.validate_session(session_id)
if not valid:
username = session_id.split("_")[0] if "_" in session_id else session_id
self.logger.log("info", f"更新日程管理器会话: {session_id}", username)
def show_schedules(self, parent_frame=None):
"""显示我的日程
在指定的父框架中显示日程管理界面,包括:
1. 左侧日程列表
2. 右侧日程详情
3. 创建、标记完成和删除日程按钮
如果用户未登录,显示提示信息。
如果用户没有日程数据,添加示例数据。
Args:
parent_frame (CTkFrame, optional): 要显示日程的父框架。如果为None则创建新窗口兼容旧代码
"""
try:
# 调用基类的show方法保存聊天历史记录
super().show(parent_frame)
# 决定使用哪个框架作为父容器
if parent_frame:
# 清空父框架中的所有组件
for widget in parent_frame.winfo_children():
widget.destroy()
container = parent_frame
else:
# 兼容旧代码:创建新窗口
container = ctk.CTkToplevel(self.root)
container.title("我的日程")
container.geometry("800x600")
container.configure(fg_color="#1e1e1e")
# 检查用户是否登录
current_user_info = self.app.auth.get_current_user(self.app.current_session_id)
if not current_user_info:
if not parent_frame: # 只有在新窗口模式下才需要销毁
container.destroy()
return
# 验证会话有效性
valid, username = self.app.auth.validate_session(self.app.current_session_id)
if not valid:
if not parent_frame: # 只有在新窗口模式下才需要销毁
container.destroy()
return
# 更新当前会话ID
self.update_session_id(self.app.current_session_id)
# 加载用户日程
schedules = self.app.auth.load_schedules(self.app.current_session_id)
# 如果没有日程数据,添加一些示例数据
if not schedules:
# 创建示例日程
example_schedules = [
{
"id": "example-1",
"title": "项目会议",
"description": "讨论项目进展和下一步计划",
"start_time": "2025-12-15T14:00:00",
"end_time": "2025-12-15T15:30:00",
"location": "会议室A",
"reminder": None,
"created_at": datetime.now().isoformat()
},
{
"id": "example-2",
"title": "健身时间",
"description": "每周健身计划",
"start_time": "2025-12-16T18:00:00",
"end_time": "2025-12-16T19:30:00",
"location": "健身房",
"reminder": None,
"created_at": datetime.now().isoformat()
}
]
# 保存示例日程
self.app.auth.save_schedules(self.app.current_session_id, example_schedules)
schedules = example_schedules
# 按开始时间排序日程
schedules.sort(key=lambda x: x["start_time"])
# 创建左侧日程列表区域
left_frame = ctk.CTkFrame(container, width=300, corner_radius=0)
left_frame.pack(side="left", fill="y", padx=(0, 10), pady=10)
# 日程列表标题
list_title = ctk.CTkLabel(left_frame, text="我的日程", font=ctk.CTkFont(size=18, weight="bold"))
list_title.pack(pady=10)
# 添加搜索框
self.search_var = tk.StringVar()
search_frame = ctk.CTkFrame(left_frame)
search_frame.pack(fill="x", padx=10, pady=5)
search_label = ctk.CTkLabel(search_frame, text="搜索:", font=ctk.CTkFont(size=12))
search_label.pack(side="left", padx=(0, 5))
search_entry = ctk.CTkEntry(search_frame, textvariable=self.search_var, placeholder_text="输入关键字")
search_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
search_button = ctk.CTkButton(search_frame, text="搜索", width=60, command=self._on_search)
search_button.pack(side="left")
# 添加搜索快捷键
search_entry.bind('<Return>', lambda event: self._on_search())
# 创建日程列表
self.schedule_listbox = tk.Listbox(left_frame, selectmode=tk.SINGLE, bg="#1e1e1e", fg="#ffffff",
selectbackground="#3a3a3a", selectforeground="#ffffff",
font=ctk.CTkFont(size=27))
self.schedule_listbox.pack(fill="both", expand=True, padx=10, pady=(0, 10))
# 保存原始日程数据
self._original_schedules = schedules.copy()
# 添加日程到列表
for i, schedule in enumerate(schedules):
# 优化时间显示格式
try:
start_dt = datetime.fromisoformat(schedule['start_time'])
display_time = start_dt.strftime('%Y-%m-%d %H:%M')
except ValueError:
display_time = schedule['start_time']
display_text = f"{i+1}. {schedule['title']} - {display_time}"
self.schedule_listbox.insert(tk.END, display_text)
# 创建右侧日程详情区域
right_frame = ctk.CTkFrame(container, corner_radius=0)
right_frame.pack(side="right", fill="both", expand=True, padx=(0, 10), pady=10)
# 日程详情标题
detail_title = ctk.CTkLabel(right_frame, text="日程详情", font=ctk.CTkFont(size=18, weight="bold"))
detail_title.pack(pady=10)
except Exception as e:
# 记录错误日志
username = self._get_username()
if self.logger:
self.logger.log("error", f"显示日程界面失败: {e}", username)
messagebox.showerror("错误", f"显示日程界面失败: {str(e)}")
# 创建日程详情滚动框架(用于气泡式显示)
self.schedule_detail_frame = ctk.CTkScrollableFrame(right_frame, fg_color="#252525",
scrollbar_button_color="#333333",
scrollbar_button_hover_color="#444444")
self.schedule_detail_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
# 绑定列表选择事件
self.schedule_listbox.bind("<<ListboxSelect>>", lambda e: self._on_schedule_select(e, schedules))
# 保存当前显示的日程列表
self._current_schedules = schedules
# 创建按钮框架
button_frame = ctk.CTkFrame(right_frame)
button_frame.pack(pady=10, padx=10, fill="x")
# 创建日程按钮
create_button = ctk.CTkButton(button_frame, text="创建新日程",
command=lambda: self._create_new_schedule(container, parent_frame),
fg_color="#009688", hover_color="#00796b")
create_button.pack(side="left", padx=(0, 5), fill="x", expand=True)
# 编辑日程按钮
edit_button = ctk.CTkButton(button_frame, text="编辑日程",
command=lambda: self._edit_schedule(schedules, container, parent_frame),
fg_color="#ff9800", hover_color="#f57c00")
edit_button.pack(side="left", padx=5, fill="x", expand=True)
# 删除日程按钮
delete_button = ctk.CTkButton(button_frame, text="删除选中日程",
command=lambda: self._delete_schedule(schedules, container, parent_frame),
fg_color="#d13438", hover_color="#b32d30")
delete_button.pack(side="right", padx=(5, 0), fill="x", expand=True)
def _get_username(self):
"""获取当前会话的用户名
Returns:
str: 用户名或会话ID
"""
if not self.current_session_id:
return "unknown"
username = self.current_session_id
if self.auth:
try:
valid, username = self.auth.validate_session(self.current_session_id)
if not valid:
username = self.current_session_id.split("_")[0] if "_" in self.current_session_id else self.current_session_id
except Exception as e:
username = self.current_session_id.split("_")[0] if "_" in self.current_session_id else self.current_session_id
return username
def _create_new_schedule(self, parent_window, content_parent=None):
"""创建新日程的弹窗
创建一个模态窗口,用于输入新日程的详细信息
Args:
parent_window: 父窗口对象
content_parent: 内容父框架,用于在创建成功后刷新日程列表
"""
self._show_schedule_dialog(parent_window, content_parent=content_parent)
def _edit_schedule(self, schedules, parent_window, content_parent=None):
"""编辑日程的弹窗
创建一个模态窗口,用于编辑选中日程的详细信息
Args:
schedules: 日程列表数据
parent_window: 父窗口对象
content_parent: 内容父框架,用于在编辑成功后刷新日程列表
"""
# 获取选中项的索引
selection = self.schedule_listbox.curselection()
if selection:
index = selection[0]
schedule = schedules[index]
self._show_schedule_dialog(parent_window, schedule, content_parent=content_parent)
else:
messagebox.showinfo("提示", "请先选择一个日程")
def _show_schedule_dialog(self, parent_window, schedule=None, content_parent=None):
"""显示日程对话框(创建或编辑)
Args:
parent_window: 父窗口对象
schedule: 可选参数要编辑的日程数据。如果为None则创建新日程。
content_parent: 内容父框架,用于在操作成功后刷新日程列表
"""
is_edit = schedule is not None
# 创建新窗口
dialog_window = ctk.CTkToplevel(parent_window)
dialog_window.title("编辑日程" if is_edit else "创建新日程")
dialog_window.geometry("500x500")
dialog_window.configure(fg_color="#1e1e1e")
dialog_window.transient(parent_window) # 设置为父窗口的子窗口
dialog_window.grab_set() # 模态窗口,阻止操作其他窗口
# 标题标签和输入框
title_label = ctk.CTkLabel(dialog_window, text="标题:", font=ctk.CTkFont(size=14))
title_label.pack(pady=(20, 5), padx=20, anchor="w")
title_entry = ctk.CTkEntry(dialog_window, width=400, height=40, font=ctk.CTkFont(size=14))
title_entry.pack(pady=(0, 15), padx=20)
# 描述标签和文本框
desc_label = ctk.CTkLabel(dialog_window, text="描述:", font=ctk.CTkFont(size=14))
desc_label.pack(pady=(10, 5), padx=20, anchor="w")
desc_text = scrolledtext.ScrolledText(dialog_window, width=50, height=10,
bg="#252525", fg="#ffffff",
font=ctk.CTkFont(size=14), wrap="word")
desc_text.pack(pady=(0, 15), padx=20)
# 开始时间标签和输入框
start_label = ctk.CTkLabel(dialog_window, text="开始时间 (YYYY-MM-DDTHH:MM:SS):", font=ctk.CTkFont(size=14))
start_label.pack(pady=(10, 5), padx=20, anchor="w")
start_entry = ctk.CTkEntry(dialog_window, width=400, height=40, font=ctk.CTkFont(size=14))
start_entry.pack(pady=(0, 15), padx=20)
# 结束时间标签和输入框
end_label = ctk.CTkLabel(dialog_window, text="结束时间 (YYYY-MM-DDTHH:MM:SS):", font=ctk.CTkFont(size=14))
end_label.pack(pady=(10, 5), padx=20, anchor="w")
end_entry = ctk.CTkEntry(dialog_window, width=400, height=40, font=ctk.CTkFont(size=14))
end_entry.pack(pady=(0, 15), padx=20)
# 地点标签和输入框
location_label = ctk.CTkLabel(dialog_window, text="地点:", font=ctk.CTkFont(size=14))
location_label.pack(pady=(10, 5), padx=20, anchor="w")
location_entry = ctk.CTkEntry(dialog_window, width=400, height=40, font=ctk.CTkFont(size=14))
location_entry.pack(pady=(0, 25), padx=20)
# 如果是编辑模式,填充现有数据
if is_edit:
title_entry.insert(0, schedule['title'])
desc_text.insert(1.0, schedule['description'])
start_entry.insert(0, schedule['start_time'])
end_entry.insert(0, schedule['end_time'])
location_entry.insert(0, schedule['location'])
else:
# 创建模式,使用当前时间作为默认值
start_entry.insert(0, datetime.now().isoformat())
end_entry.insert(0, datetime.now().isoformat())
# 保存按钮的回调函数
def save_schedule():
# 获取输入数据
title = title_entry.get()
description = desc_text.get(1.0, tk.END).strip()
start_time = start_entry.get()
end_time = end_entry.get()
location = location_entry.get()
# 验证必填字段
if not title:
messagebox.showerror("错误", "标题不能为空")
return
if not start_time:
messagebox.showerror("错误", "开始时间不能为空")
return
if not end_time:
messagebox.showerror("错误", "结束时间不能为空")
return
try:
# 验证时间格式
datetime.fromisoformat(start_time)
datetime.fromisoformat(end_time)
except ValueError:
messagebox.showerror("错误", "时间格式不正确请使用YYYY-MM-DDTHH:MM:SS格式")
return
# 创建日程数据
schedule_data = {
"title": title,
"description": description,
"start_time": start_time,
"end_time": end_time,
"location": location
}
# 保存日程
if is_edit:
success = self.auth.update_schedule(self.current_session_id, schedule['id'], schedule_data)
action = "更新"
else:
success, new_schedule = self.auth.create_schedule(self.current_session_id, schedule_data)
action = "创建"
if success:
messagebox.showinfo("成功", f"日程{action}成功")
dialog_window.destroy()
# 重新加载日程列表
if content_parent:
self.show_schedules(content_parent)
else:
parent_window.destroy()
self.show_schedules()
else:
messagebox.showerror("错误", f"日程{action}失败")
# 创建保存按钮
save_button = ctk.CTkButton(dialog_window, text="保存日程",
command=save_schedule, fg_color="#009688", hover_color="#00796b")
save_button.pack(pady=10, padx=20, fill="x")
def _on_search(self):
"""
搜索按钮点击事件处理
根据用户输入的关键字搜索日程,并更新日程列表。
"""
# 获取搜索关键字
keyword = self.search_var.get().strip()
# 如果没有关键字,显示所有日程
if not keyword:
search_results = self._original_schedules
else:
# 搜索日程
search_results = []
for schedule in self._original_schedules:
if (keyword.lower() in schedule['title'].lower() or
keyword.lower() in schedule.get('description', '').lower() or
keyword.lower() in schedule.get('location', '').lower()):
search_results.append(schedule)
# 清空列表
self.schedule_listbox.delete(0, tk.END)
# 添加搜索结果到列表
for i, schedule in enumerate(search_results):
# 优化时间显示格式
try:
start_dt = datetime.fromisoformat(schedule['start_time'])
display_time = start_dt.strftime('%Y-%m-%d %H:%M')
except ValueError:
display_time = schedule['start_time']
display_text = f"{i+1}. {schedule['title']} - {display_time}"
self.schedule_listbox.insert(tk.END, display_text)
# 更新当前显示的日程列表
self._current_schedules = search_results
# 清空详情区域
for widget in self.schedule_detail_frame.winfo_children():
widget.destroy()
def _on_schedule_select(self, event, schedules):
"""
日程列表选择事件处理
当日程列表中的项被选中时,在右侧详情区域以气泡样式显示该日程的详细信息。
Args:
event: 事件对象
schedules: 日程列表数据(仅用于向后兼容)
"""
# 获取选中项的索引
selection = self.schedule_listbox.curselection()
if selection:
index = selection[0]
# 获取当前显示的日程列表
current_schedules = getattr(self, '_current_schedules', schedules)
# 获取选中的日程
schedule = current_schedules[index]
# 清空详情区域
for widget in self.schedule_detail_frame.winfo_children():
widget.destroy()
# 优化时间显示格式
try:
start_dt = datetime.fromisoformat(schedule['start_time'])
formatted_start = start_dt.strftime('%Y-%m-%d %H:%M')
except ValueError:
formatted_start = schedule['start_time']
try:
if schedule.get('end_time'):
end_dt = datetime.fromisoformat(schedule['end_time'])
formatted_end = end_dt.strftime('%Y-%m-%d %H:%M')
else:
formatted_end = schedule.get('end_time', '')
except ValueError:
formatted_end = schedule.get('end_time', '')
try:
created_dt = datetime.fromisoformat(schedule['created_at'])
formatted_created = created_dt.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
formatted_created = schedule['created_at']
# 定义日程参数列表
schedule_params = [
("标题", schedule['title']),
("描述", schedule['description']),
("开始时间", formatted_start),
("结束时间", formatted_end),
("地点", schedule['location']),
("创建时间", formatted_created)
]
# 显示日程参数为气泡对
for param_name, param_value in schedule_params:
# 只显示有值的参数
if param_value:
# 创建参数名称气泡(左对齐)
name_frame = ctk.CTkFrame(self.schedule_detail_frame, fg_color="#333333", corner_radius=10)
name_frame.pack(fill="x", padx=10, pady=(5, 0), anchor="w")
name_label = ctk.CTkLabel(name_frame, text=param_name,
font=ctk.CTkFont(size=14, weight="bold"),
text_color="#ffffff")
name_label.pack(padx=10, pady=5)
# 创建参数值气泡(右对齐)
value_frame = ctk.CTkFrame(self.schedule_detail_frame, fg_color="#424242", corner_radius=10)
value_frame.pack(fill="x", padx=10, pady=(0, 10), anchor="e")
value_label = ctk.CTkLabel(value_frame, text=param_value,
font=ctk.CTkFont(size=14),
text_color="#ffffff",
wraplength=300, justify="left")
value_label.pack(padx=10, pady=5)
def _delete_schedule(self, schedules, window, content_parent=None):
"""删除选中的日程
删除用户选中的日程,并重新加载日程列表。
Args:
schedules: 日程列表数据
window: 父窗口对象
content_parent: 内容父框架,用于在删除后刷新日程列表
"""
try:
# 获取选中项的索引
selection = self.schedule_listbox.curselection()
if not selection:
messagebox.showinfo("提示", "请先选择一个要删除的日程")
return
index = selection[0]
schedule = schedules[index]
schedule_id = schedule['id']
# 确认删除
if messagebox.askyesno("确认", f"确定要删除日程 '{schedule['title']}' 吗?"):
# 调用认证对象的删除日程方法
success = self.auth.delete_schedule(self.app.current_session_id, schedule_id)
if success:
# 记录日志
username = self._get_username()
if self.logger:
self.logger.log("info", f"删除日程成功: {schedule['title']}", username)
messagebox.showinfo("成功", "日程删除成功")
# 重新加载日程列表
if content_parent:
self.show_schedules(content_parent)
else:
window.destroy()
self.show_schedules()
else:
# 记录日志
username = self._get_username()
if self.logger:
self.logger.log("error", f"删除日程失败: {schedule['title']}", username)
messagebox.showerror("错误", "日程删除失败")
except Exception as e:
# 记录错误日志
username = self._get_username()
if self.logger:
self.logger.log("error", f"删除日程时发生错误: {e}", username)
messagebox.showerror("错误", f"删除日程时发生错误: {str(e)}")