677 lines
36 KiB
Python
677 lines
36 KiB
Python
import tkinter as tk
|
||
from tkinter import ttk, messagebox
|
||
import json
|
||
from datetime import datetime, timedelta
|
||
import calendar
|
||
import uuid
|
||
|
||
from utils import center_toplevel_window
|
||
|
||
class SchedulerApp(tk.Toplevel):
|
||
def __init__(self, master, devices_data, scheduled_tasks, save_config_callback, log_message_callback):
|
||
super().__init__(master)
|
||
self.master = master # master在这里是MainControlApp的实例
|
||
self.title("定时任务管理")
|
||
self.geometry("800x600")
|
||
|
||
self.devices = devices_data
|
||
self.scheduled_tasks = scheduled_tasks
|
||
self.save_config_callback = save_config_callback
|
||
self.log_message_callback = log_message_callback
|
||
|
||
self.create_widgets()
|
||
self._update_schedule_display() # Initial display of tasks
|
||
|
||
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
|
||
center_toplevel_window(self, master)
|
||
# --- 修正点结束 ---
|
||
|
||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||
|
||
def create_widgets(self):
|
||
# Frame for buttons
|
||
button_frame = ttk.Frame(self)
|
||
button_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
|
||
|
||
ttk.Button(button_frame, text="添加任务", command=self.add_task).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(button_frame, text="保存并关闭", command=self.on_closing).pack(side=tk.RIGHT, padx=5)
|
||
|
||
# Frame for task list
|
||
task_list_frame = ttk.Frame(self)
|
||
task_list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||
|
||
self.task_tree = ttk.Treeview(task_list_frame, columns=("Name", "Type", "Trigger", "Actions", "Enabled", "Last Run", "Next Run"), show="headings")
|
||
self.task_tree.heading("Name", text="任务名称")
|
||
self.task_tree.heading("Type", text="类型")
|
||
self.task_tree.heading("Trigger", text="触发条件")
|
||
self.task_tree.heading("Actions", text="执行指令")
|
||
self.task_tree.heading("Enabled", text="启用")
|
||
self.task_tree.heading("Last Run", text="上次运行")
|
||
self.task_tree.heading("Next Run", text="下次运行")
|
||
|
||
self.task_tree.column("Name", width=120, anchor=tk.W)
|
||
self.task_tree.column("Type", width=80, anchor=tk.CENTER)
|
||
self.task_tree.column("Trigger", width=150, anchor=tk.W)
|
||
self.task_tree.column("Actions", width=150, anchor=tk.W)
|
||
self.task_tree.column("Enabled", width=50, anchor=tk.CENTER)
|
||
self.task_tree.column("Last Run", width=120, anchor=tk.CENTER)
|
||
self.task_tree.column("Next Run", width=120, anchor=tk.CENTER)
|
||
|
||
self.task_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
scrollbar = ttk.Scrollbar(task_list_frame, orient="vertical", command=self.task_tree.yview)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.task_tree.config(yscrollcommand=scrollbar.set)
|
||
|
||
self.task_tree.bind("<Double-1>", self.edit_selected_task)
|
||
self.task_tree.bind("<<TreeviewSelect>>", self.on_task_select)
|
||
|
||
# Context menu for tasks
|
||
self.context_menu = tk.Menu(self, tearoff=0)
|
||
self.context_menu.add_command(label="编辑任务", command=self.edit_selected_task)
|
||
self.context_menu.add_command(label="删除任务", command=self.delete_selected_task)
|
||
self.context_menu.add_command(label="启用/禁用任务", command=self.toggle_task_enabled)
|
||
self.task_tree.bind("<Button-3>", self.show_context_menu)
|
||
|
||
def on_task_select(self, event):
|
||
# This function is called when a task is selected, useful if you want to
|
||
# enable/disable buttons based on selection.
|
||
pass
|
||
|
||
def show_context_menu(self, event):
|
||
try:
|
||
self.task_tree.selection_set(self.task_tree.identify_row(event.y))
|
||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
||
finally:
|
||
self.context_menu.grab_release()
|
||
|
||
def _update_schedule_display(self):
|
||
"""刷新任务列表显示"""
|
||
for i in self.task_tree.get_children():
|
||
self.task_tree.delete(i)
|
||
|
||
for task in self.scheduled_tasks:
|
||
task_name = task.get("name", "无名称")
|
||
# 调度类型显示为中文
|
||
schedule_type_display = {
|
||
"once": "一次性",
|
||
"daily": "每天",
|
||
"weekly": "每周",
|
||
"monthly": "每月"
|
||
}.get(task["schedule_type"], "未知")
|
||
|
||
trigger_str = self._get_trigger_string(task)
|
||
actions_str = self._get_actions_string(task)
|
||
enabled_str = "是" if task["enabled"] else "否"
|
||
last_run_str = task["last_run_time"].strftime("%Y-%m-%d %H:%M:%S") if task["last_run_time"] else "N/A"
|
||
next_run_str = task["next_run_time"].strftime("%Y-%m-%d %H:%M:%S") if task["next_run_time"] else "N/A"
|
||
|
||
self.task_tree.insert("", tk.END, iid=task["id"],
|
||
values=(task_name, schedule_type_display, trigger_str, actions_str, enabled_str, last_run_str, next_run_str))
|
||
|
||
def _get_trigger_string(self, task):
|
||
trigger = task["trigger"]
|
||
if task["schedule_type"] == "once":
|
||
return f"{trigger['year']}-{trigger['month']:02d}-{trigger['day']:02d} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
|
||
elif task["schedule_type"] == "daily":
|
||
return f"每天 {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
|
||
elif task["schedule_type"] == "weekly":
|
||
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||
days = ", ".join([day_names[d] for d in sorted(trigger["days_of_week"])])
|
||
return f"{days} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
|
||
elif task["schedule_type"] == "monthly":
|
||
if "month_day" in trigger:
|
||
return f"每月 {trigger['month_day']} 号 {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
|
||
elif "week_of_month" in trigger:
|
||
week_of_month_map = {1: "第一个", 2: "第二个", 3: "第三个", 4: "第四个", -1: "最后一个"}
|
||
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||
return f"每月 {week_of_month_map.get(trigger['week_of_month'], '')}{day_names[trigger['day_of_week']]} {trigger['hour']:02d}:{trigger['minute']:02d}:{trigger['second']:02d}"
|
||
return "N/A"
|
||
|
||
def _get_actions_string(self, task):
|
||
if not task["actions"]:
|
||
return "无指令"
|
||
|
||
action_names = []
|
||
for action in task["actions"]:
|
||
device = next((d for d in self.devices if d["id"] == action["device_id"]), None)
|
||
if device and 0 <= action["command_index"] < len(device["commands"]):
|
||
cmd_name = device["commands"][action["command_index"]]["name"]
|
||
action_names.append(f"{device['name']}:{cmd_name}")
|
||
else:
|
||
action_names.append("未知设备/指令")
|
||
|
||
return ", ".join(action_names)
|
||
|
||
|
||
def add_task(self):
|
||
"""打开一个新窗口以添加或编辑任务"""
|
||
self.TaskEditor(self, None) # self是SchedulerApp实例,作为TaskEditor的master_app
|
||
|
||
def edit_selected_task(self, event=None):
|
||
"""编辑选中的任务"""
|
||
selected_item = self.task_tree.focus()
|
||
if selected_item:
|
||
task_id = selected_item
|
||
task = next((t for t in self.scheduled_tasks if t["id"] == task_id), None)
|
||
if task:
|
||
self.TaskEditor(self, task)
|
||
else:
|
||
messagebox.showerror("错误", "未找到选中的任务。")
|
||
else:
|
||
messagebox.showwarning("提示", "请选择一个任务进行编辑。")
|
||
|
||
def delete_selected_task(self):
|
||
"""删除选中的任务"""
|
||
selected_item = self.task_tree.focus()
|
||
if selected_item:
|
||
if messagebox.askyesno("确认删除", "确定要删除选中的任务吗?"):
|
||
task_id = selected_item
|
||
self.scheduled_tasks[:] = [t for t in self.scheduled_tasks if t["id"] != task_id]
|
||
self.save_config_callback()
|
||
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
|
||
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
|
||
self._update_schedule_display()
|
||
self.log_message_callback(f"定时任务 (ID: {task_id[:8]}...) 已删除。", "blue")
|
||
else:
|
||
messagebox.showwarning("提示", "请选择一个任务进行删除。")
|
||
|
||
def toggle_task_enabled(self):
|
||
"""切换选中任务的启用/禁用状态"""
|
||
selected_item = self.task_tree.focus()
|
||
if selected_item:
|
||
task_id = selected_item
|
||
task = next((t for t in self.scheduled_tasks if t["id"] == task_id), None)
|
||
if task:
|
||
task["enabled"] = not task["enabled"]
|
||
self.save_config_callback()
|
||
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
|
||
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
|
||
self._update_schedule_display()
|
||
self.log_message_callback(f"定时任务 '{task.get('name', task_id[:8])}' (ID: {task_id[:8]}...) 已更新。", "blue")
|
||
else:
|
||
messagebox.showwarning("提示", "请选择一个任务进行操作。")
|
||
|
||
def on_closing(self):
|
||
"""关闭窗口时保存配置"""
|
||
self.save_config_callback()
|
||
self.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
|
||
self.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
|
||
self.destroy()
|
||
|
||
class TaskEditor(tk.Toplevel):
|
||
def __init__(self, master_app, task_data=None):
|
||
super().__init__(master_app)
|
||
self.master_app = master_app # master_app在这里是SchedulerApp的实例
|
||
self.title("编辑定时任务" if task_data else "添加定时任务")
|
||
self.geometry("500x700")
|
||
|
||
self.task_data = task_data if task_data else self._create_new_task_template()
|
||
self.original_task_id = self.task_data["id"] # Store original ID for updates
|
||
|
||
# 定义调度类型的中英文映射
|
||
self.schedule_type_map = {
|
||
"once": "一次性",
|
||
"daily": "每天",
|
||
"weekly": "每周",
|
||
"monthly": "每月"
|
||
}
|
||
self.schedule_type_reverse_map = {v: k for k, v in self.schedule_type_map.items()}
|
||
|
||
self.create_widgets()
|
||
self._load_task_data()
|
||
|
||
# --- 修正点:在所有部件创建并加载数据后,再居中窗口 ---
|
||
center_toplevel_window(self, master_app)
|
||
# --- 修正点结束 ---
|
||
|
||
self.transient(master_app) # Make this window a transient window of the master
|
||
self.grab_set() # Grab all events until this window is destroyed
|
||
self.wait_window(self) # Wait until this window is destroyed
|
||
|
||
def _create_new_task_template(self):
|
||
now = datetime.now()
|
||
return {
|
||
"id": str(uuid.uuid4()),
|
||
"name": "新任务",
|
||
"schedule_type": "daily", # 保持英文
|
||
"trigger": {"hour": now.hour, "minute": now.minute + 1, "second": 0}, # Default to 1 min from now
|
||
"actions": [],
|
||
"enabled": True,
|
||
"last_run_time": None,
|
||
"next_run_time": None
|
||
}
|
||
|
||
def create_widgets(self):
|
||
main_frame = ttk.Frame(self, padding="10")
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Task Name
|
||
ttk.Label(main_frame, text="任务名称:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||
self.name_entry = ttk.Entry(main_frame)
|
||
self.name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2)
|
||
|
||
# Schedule Type
|
||
ttk.Label(main_frame, text="调度类型:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||
|
||
# 内部变量,存储英文键 (用于JSON)
|
||
self.schedule_type_internal_var = tk.StringVar(self)
|
||
# 显示变量,存储中文文本 (用于OptionMenu)
|
||
self.schedule_type_display_var = tk.StringVar(self)
|
||
|
||
# 初始化显示变量,默认显示“每天”
|
||
self.schedule_type_display_var.set(self.schedule_type_map.get(self.task_data["schedule_type"], "每天"))
|
||
|
||
self.schedule_type_optionmenu = ttk.OptionMenu(main_frame, self.schedule_type_display_var,
|
||
self.schedule_type_display_var.get(), # 默认显示值
|
||
*self.schedule_type_map.values(), # 选项为中文显示文本
|
||
command=self._on_schedule_type_change)
|
||
self.schedule_type_optionmenu.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=2)
|
||
|
||
# Trigger Frame
|
||
self.trigger_frame = ttk.LabelFrame(main_frame, text="触发条件", padding="5")
|
||
self.trigger_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||
# 初始创建触发条件部件,此时 schedule_type_internal_var 尚未完全加载,但在 _load_task_data 会再次调用
|
||
self._create_trigger_widgets(self.trigger_frame)
|
||
|
||
# Actions Frame
|
||
self.actions_frame = ttk.LabelFrame(main_frame, text="执行指令", padding="5")
|
||
self.actions_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||
self._create_actions_widgets(self.actions_frame)
|
||
|
||
# Enabled Checkbutton
|
||
self.enabled_var = tk.BooleanVar(self)
|
||
self.enabled_checkbutton = ttk.Checkbutton(main_frame, text="启用任务", variable=self.enabled_var)
|
||
self.enabled_checkbutton.grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=5)
|
||
|
||
# Save/Cancel Buttons
|
||
button_frame = ttk.Frame(main_frame)
|
||
button_frame.grid(row=5, column=0, columnspan=2, sticky=tk.E, pady=10)
|
||
ttk.Button(button_frame, text="保存", command=self._save_task).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(button_frame, text="取消", command=self.destroy).pack(side=tk.LEFT, padx=5)
|
||
|
||
main_frame.columnconfigure(1, weight=1)
|
||
|
||
def _create_trigger_widgets(self, parent_frame):
|
||
# Clear existing trigger widgets
|
||
for widget in parent_frame.winfo_children():
|
||
widget.destroy()
|
||
|
||
# Time inputs (common to all)
|
||
ttk.Label(parent_frame, text="时间 (HH:MM:SS):").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||
self.hour_var = tk.StringVar(self)
|
||
self.minute_var = tk.StringVar(self)
|
||
self.second_var = tk.StringVar(self)
|
||
self.hour_spinbox = ttk.Spinbox(parent_frame, from_=0, to=23, wrap=True, width=3, format="%02.0f", textvariable=self.hour_var)
|
||
self.minute_spinbox = ttk.Spinbox(parent_frame, from_=0, to=59, wrap=True, width=3, format="%02.0f", textvariable=self.minute_var)
|
||
self.second_spinbox = ttk.Spinbox(parent_frame, from_=0, to=59, wrap=True, width=3, format="%02.0f", textvariable=self.second_var)
|
||
self.hour_spinbox.grid(row=0, column=1, sticky=tk.W, padx=(0,2))
|
||
ttk.Label(parent_frame, text=":").grid(row=0, column=2, sticky=tk.W)
|
||
self.minute_spinbox.grid(row=0, column=3, sticky=tk.W, padx=(0,2))
|
||
ttk.Label(parent_frame, text=":").grid(row=0, column=4, sticky=tk.W)
|
||
self.second_spinbox.grid(row=0, column=5, sticky=tk.W)
|
||
|
||
# 使用内部变量获取调度类型
|
||
schedule_type = self.schedule_type_internal_var.get()
|
||
if schedule_type == "once":
|
||
ttk.Label(parent_frame, text="日期 (YYYY-MM-DD):").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||
self.year_var = tk.StringVar(self)
|
||
self.month_var = tk.StringVar(self)
|
||
self.day_var = tk.StringVar(self)
|
||
|
||
# --- 新增:如果当前是新建任务且调度类型为“一次性”,则自动填充为当前日期 ---
|
||
# 检查 self.task_data["trigger"] 中是否已包含日期信息,如果未包含,则说明是新选择“一次性”类型
|
||
if "year" not in self.task_data["trigger"]:
|
||
now = datetime.now()
|
||
self.year_var.set(str(now.year))
|
||
self.month_var.set(f"{now.month:02d}")
|
||
self.day_var.set(f"{now.day:02d}")
|
||
# --- 新增结束 ---
|
||
|
||
self.year_entry = ttk.Entry(parent_frame, width=5, textvariable=self.year_var)
|
||
self.month_entry = ttk.Entry(parent_frame, width=3, textvariable=self.month_var)
|
||
self.day_entry = ttk.Entry(parent_frame, width=3, textvariable=self.day_var)
|
||
self.year_entry.grid(row=1, column=1, sticky=tk.W, padx=(0,2))
|
||
ttk.Label(parent_frame, text="-").grid(row=1, column=2, sticky=tk.W)
|
||
self.month_entry.grid(row=1, column=3, sticky=tk.W, padx=(0,2))
|
||
ttk.Label(parent_frame, text="-").grid(row=1, column=4, sticky=tk.W)
|
||
self.day_entry.grid(row=1, column=5, sticky=tk.W)
|
||
elif schedule_type == "weekly":
|
||
ttk.Label(parent_frame, text="选择星期:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
||
self.days_of_week_vars = []
|
||
day_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||
for i, day_name in enumerate(day_names):
|
||
var = tk.BooleanVar(self)
|
||
cb = ttk.Checkbutton(parent_frame, text=day_name, variable=var)
|
||
cb.grid(row=1 + (i // 4), column=1 + (i % 4), sticky=tk.W, padx=2)
|
||
self.days_of_week_vars.append((i, var)) # Store (day_index, BooleanVar)
|
||
elif schedule_type == "monthly":
|
||
self.monthly_type_var = tk.StringVar(self)
|
||
self.monthly_type_var.set("month_day") # Default to specific day of month
|
||
|
||
ttk.Radiobutton(parent_frame, text="每月指定日期", variable=self.monthly_type_var, value="month_day", command=lambda: self._on_monthly_type_change(parent_frame)).grid(row=1, column=0, columnspan=3, sticky=tk.W)
|
||
ttk.Radiobutton(parent_frame, text="每月第N个星期X", variable=self.monthly_type_var, value="week_of_month", command=lambda: self._on_monthly_type_change(parent_frame)).grid(row=2, column=0, columnspan=3, sticky=tk.W)
|
||
|
||
self.monthly_specific_day_frame = ttk.Frame(parent_frame)
|
||
self.monthly_specific_day_frame.grid(row=1, column=3, columnspan=3, sticky=(tk.W, tk.E))
|
||
ttk.Label(self.monthly_specific_day_frame, text="日期:").pack(side=tk.LEFT)
|
||
self.month_day_var = tk.StringVar(self)
|
||
self.month_day_spinbox = ttk.Spinbox(self.monthly_specific_day_frame, from_=1, to=31, wrap=True, width=3, textvariable=self.month_day_var)
|
||
self.month_day_spinbox.pack(side=tk.LEFT, padx=2)
|
||
|
||
self.monthly_week_day_frame = ttk.Frame(parent_frame)
|
||
self.monthly_week_day_frame.grid(row=2, column=3, columnspan=3, sticky=(tk.W, tk.E))
|
||
ttk.Label(self.monthly_week_day_frame, text="第").pack(side=tk.LEFT)
|
||
self.week_of_month_var = tk.StringVar(self)
|
||
self.week_of_month_optionmenu = ttk.OptionMenu(self.monthly_week_day_frame, self.week_of_month_var, "第一个", "第一个", "第二个", "第三个", "第四个", "最后一个")
|
||
self.week_of_month_optionmenu.pack(side=tk.LEFT, padx=2)
|
||
ttk.Label(self.monthly_week_day_frame, text="个").pack(side=tk.LEFT)
|
||
self.day_of_week_var = tk.StringVar(self)
|
||
self.day_of_week_optionmenu = ttk.OptionMenu(self.monthly_week_day_frame, self.day_of_week_var, "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日")
|
||
self.day_of_week_optionmenu.pack(side=tk.LEFT, padx=2)
|
||
|
||
self._on_monthly_type_change(parent_frame) # Initialize visibility
|
||
|
||
def _on_schedule_type_change(self, selected_display_type):
|
||
# 将选中的中文显示文本映射回英文内部类型
|
||
internal_type = self.schedule_type_reverse_map.get(selected_display_type, "daily")
|
||
self.schedule_type_internal_var.set(internal_type) # 更新内部变量
|
||
|
||
# 根据内部类型重新创建触发条件部件
|
||
self._create_trigger_widgets(self.trigger_frame)
|
||
self._load_trigger_data() # 重新加载触发条件数据以填充新创建的部件
|
||
|
||
def _on_monthly_type_change(self, parent_frame):
|
||
if self.monthly_type_var.get() == "month_day":
|
||
self.monthly_specific_day_frame.grid(row=1, column=3, columnspan=3, sticky=(tk.W, tk.E))
|
||
self.monthly_week_day_frame.grid_forget()
|
||
else:
|
||
self.monthly_specific_day_frame.grid_forget()
|
||
self.monthly_week_day_frame.grid(row=2, column=3, columnspan=3, sticky=(tk.W, tk.E))
|
||
|
||
def _create_actions_widgets(self, parent_frame):
|
||
# Clear existing action widgets
|
||
for widget in parent_frame.winfo_children():
|
||
widget.destroy()
|
||
|
||
self.action_entries = [] # List to hold (device_dropdown, command_dropdown, delay_entry) for each action
|
||
|
||
# Add Action Button
|
||
ttk.Button(parent_frame, text="添加指令", command=self._add_action_row).pack(pady=5)
|
||
|
||
# Scrollable frame for actions
|
||
self.action_canvas = tk.Canvas(parent_frame)
|
||
self.action_scrollbar = ttk.Scrollbar(parent_frame, orient="vertical", command=self.action_canvas.yview)
|
||
self.action_scrollable_frame = ttk.Frame(self.action_canvas)
|
||
|
||
self.action_scrollable_frame.bind(
|
||
"<Configure>",
|
||
lambda e: self.action_canvas.configure(
|
||
scrollregion=self.action_canvas.bbox("all")
|
||
)
|
||
)
|
||
|
||
self.action_canvas.create_window((0, 0), window=self.action_scrollable_frame, anchor="nw")
|
||
self.action_canvas.configure(yscrollcommand=self.action_scrollbar.set)
|
||
|
||
self.action_canvas.pack(side="left", fill="both", expand=True)
|
||
self.action_scrollbar.pack(side="right", fill="y")
|
||
|
||
# Initial action rows if any
|
||
if self.task_data["actions"]:
|
||
for action in self.task_data["actions"]:
|
||
self._add_action_row(action)
|
||
|
||
def _add_action_row(self, action_data=None):
|
||
row_frame = ttk.Frame(self.action_scrollable_frame, padding=5, relief=tk.RIDGE, borderwidth=1)
|
||
row_frame.pack(fill=tk.X, pady=2)
|
||
|
||
# Device selection
|
||
ttk.Label(row_frame, text="设备:").pack(side=tk.LEFT)
|
||
device_names = [d["name"] for d in self.master_app.devices]
|
||
device_var = tk.StringVar(self)
|
||
|
||
# --- 修正点1: 确保 lambda 捕获正确的 command_dropdown 和 command_var ---
|
||
# 先创建 command_dropdown 和 command_var,再创建 device_dropdown
|
||
command_var = tk.StringVar(self)
|
||
command_dropdown = ttk.OptionMenu(row_frame, command_var, "无指令") # Placeholder
|
||
|
||
device_dropdown = ttk.OptionMenu(row_frame, device_var, device_names[0] if device_names else "无设备", *device_names,
|
||
command=lambda selected_device_name, dv=device_var, cdd=command_dropdown, cv=command_var: self._on_device_select(selected_device_name, dv, cdd, cv))
|
||
device_dropdown.pack(side=tk.LEFT, padx=5)
|
||
|
||
# Command selection (will be updated based on device)
|
||
ttk.Label(row_frame, text="指令:").pack(side=tk.LEFT)
|
||
command_dropdown.pack(side=tk.LEFT, padx=5) # 修正点2: 放置 command_dropdown
|
||
|
||
# Delay input
|
||
ttk.Label(row_frame, text="延迟(ms):").pack(side=tk.LEFT)
|
||
delay_var = tk.StringVar(self)
|
||
delay_entry = ttk.Entry(row_frame, width=5, textvariable=delay_var)
|
||
delay_entry.pack(side=tk.LEFT, padx=5)
|
||
delay_var.set("0") # Default delay
|
||
|
||
# Delete button
|
||
delete_btn = ttk.Button(row_frame, text="X", command=lambda: self._delete_action_row(row_frame))
|
||
delete_btn.pack(side=tk.RIGHT)
|
||
|
||
self.action_entries.append((row_frame, device_var, device_dropdown, command_var, command_dropdown, delay_var))
|
||
|
||
# Initialize dropdowns if action_data is provided (editing existing task)
|
||
if action_data:
|
||
device = next((d for d in self.master_app.devices if d["id"] == action_data["device_id"]), None)
|
||
if device:
|
||
device_var.set(device["name"])
|
||
self._on_device_select(device["name"], device_var, command_dropdown, command_var)
|
||
|
||
if 0 <= action_data["command_index"] < len(device["commands"]):
|
||
command_var.set(device["commands"][action_data["command_index"]]["name"])
|
||
delay_var.set(str(action_data.get("delay_ms", 0)))
|
||
else: # For new rows, set default device if available
|
||
if device_names:
|
||
device_var.set(device_names[0])
|
||
self._on_device_select(device_names[0], device_var, command_dropdown, command_var)
|
||
|
||
self.action_scrollable_frame.update_idletasks()
|
||
self.action_canvas.config(scrollregion=self.action_canvas.bbox("all"))
|
||
|
||
def _delete_action_row(self, row_frame):
|
||
for i, (frame, _, _, _, _, _) in enumerate(self.action_entries):
|
||
if frame == row_frame:
|
||
self.action_entries.pop(i)
|
||
row_frame.destroy()
|
||
break
|
||
self.action_scrollable_frame.update_idletasks()
|
||
self.action_canvas.config(scrollregion=self.action_canvas.bbox("all"))
|
||
|
||
|
||
def _on_device_select(self, selected_device_name, device_var, command_dropdown, command_var):
|
||
device = next((d for d in self.master_app.devices if d["name"] == selected_device_name), None)
|
||
if device:
|
||
commands = device.get("commands", [])
|
||
command_names = [cmd["name"] for cmd in commands]
|
||
|
||
# Update the command dropdown
|
||
menu = command_dropdown["menu"]
|
||
menu.delete(0, "end")
|
||
if command_names:
|
||
for cmd_name in command_names:
|
||
menu.add_command(label=cmd_name, command=tk._setit(command_var, cmd_name))
|
||
command_var.set(command_names[0]) # Set default to first command
|
||
else:
|
||
menu.add_command(label="无指令", command=tk._setit(command_var, "无指令"))
|
||
command_var.set("无指令")
|
||
else:
|
||
menu = command_dropdown["menu"]
|
||
menu.delete(0, "end")
|
||
menu.add_command(label="无指令", command=tk._setit(command_var, "无指令"))
|
||
command_var.set("无指令")
|
||
|
||
def _load_task_data(self):
|
||
self.name_entry.delete(0, tk.END)
|
||
self.name_entry.insert(0, self.task_data["name"])
|
||
|
||
# 设置内部变量 (英文)
|
||
self.schedule_type_internal_var.set(self.task_data["schedule_type"])
|
||
# 设置显示变量 (中文)
|
||
self.schedule_type_display_var.set(self.schedule_type_map.get(self.schedule_type_internal_var.get(), "每天"))
|
||
|
||
# 触发更新逻辑,这会调用 _create_trigger_widgets 和 _load_trigger_data
|
||
self._on_schedule_type_change(self.schedule_type_display_var.get())
|
||
|
||
self.enabled_var.set(self.task_data["enabled"])
|
||
|
||
def _load_trigger_data(self):
|
||
trigger = self.task_data["trigger"]
|
||
self.hour_var.set(f"{trigger['hour']:02d}")
|
||
self.minute_var.set(f"{trigger['minute']:02d}")
|
||
self.second_var.set(f"{trigger['second']:02d}")
|
||
|
||
# 使用内部变量获取调度类型
|
||
if self.schedule_type_internal_var.get() == "once":
|
||
# 只有当 task_data["trigger"] 中有日期信息时才加载,否则保持 _create_trigger_widgets 中设置的当前日期
|
||
if "year" in trigger:
|
||
self.year_var.set(str(trigger["year"]))
|
||
self.month_var.set(f"{trigger['month']:02d}")
|
||
self.day_var.set(f"{trigger['day']:02d}")
|
||
elif self.schedule_type_internal_var.get() == "weekly":
|
||
if hasattr(self, 'days_of_week_vars'):
|
||
for i, var in self.days_of_week_vars:
|
||
var.set(i in trigger["days_of_week"])
|
||
elif self.schedule_type_internal_var.get() == "monthly":
|
||
if "month_day" in trigger:
|
||
self.monthly_type_var.set("month_day")
|
||
self.month_day_var.set(str(trigger["month_day"]))
|
||
elif "week_of_month" in trigger:
|
||
self.monthly_type_var.set("week_of_month")
|
||
week_of_month_map = {1: "第一个", 2: "第二个", 3: "第三个", 4: "第四个", -1: "最后一个"}
|
||
day_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
||
self.week_of_month_var.set(week_of_month_map.get(trigger["week_of_month"], "第一个"))
|
||
self.day_of_week_var.set(day_names[trigger["day_of_week"]])
|
||
self._on_monthly_type_change(self.trigger_frame) # Update visibility after loading
|
||
|
||
def _save_task(self):
|
||
try:
|
||
# Basic validation
|
||
name = self.name_entry.get().strip()
|
||
if not name:
|
||
messagebox.showerror("错误", "任务名称不能为空。")
|
||
return
|
||
|
||
hour = int(self.hour_var.get())
|
||
minute = int(self.minute_var.get())
|
||
second = int(self.second_var.get())
|
||
|
||
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
|
||
messagebox.showerror("错误", "时间格式不正确。小时(0-23), 分钟(0-59), 秒(0-59)。")
|
||
return
|
||
|
||
new_trigger = {"hour": hour, "minute": minute, "second": second}
|
||
# 从内部变量获取调度类型 (英文)
|
||
schedule_type = self.schedule_type_internal_var.get()
|
||
|
||
if schedule_type == "once":
|
||
year = int(self.year_var.get())
|
||
month = int(self.month_var.get())
|
||
day = int(self.day_var.get())
|
||
|
||
try:
|
||
datetime(year, month, day) # Validate date
|
||
except ValueError:
|
||
messagebox.showerror("错误", "一次性任务日期无效。")
|
||
return
|
||
new_trigger.update({"year": year, "month": month, "day": day})
|
||
elif schedule_type == "weekly":
|
||
selected_days = [idx for idx, var in self.days_of_week_vars if var.get()]
|
||
if not selected_days:
|
||
messagebox.showerror("错误", "每周任务至少需要选择一天。")
|
||
return
|
||
new_trigger["days_of_week"] = selected_days
|
||
elif schedule_type == "monthly":
|
||
if self.monthly_type_var.get() == "month_day":
|
||
month_day = int(self.month_day_var.get())
|
||
if not (1 <= month_day <= 31):
|
||
messagebox.showerror("错误", "每月指定日期必须在1-31之间。")
|
||
return
|
||
new_trigger["month_day"] = month_day
|
||
else: # week_of_month
|
||
week_of_month_map_rev = {"第一个": 1, "第二个": 2, "第三个": 3, "第四个": 4, "最后一个": -1}
|
||
day_names_rev = {"星期一": 0, "星期二": 1, "星期三": 2, "星期四": 3, "星期五": 4, "星期六": 5, "星期日": 6}
|
||
|
||
week_of_month = week_of_month_map_rev.get(self.week_of_month_var.get())
|
||
day_of_week = day_names_rev.get(self.day_of_week_var.get())
|
||
if week_of_month is None or day_of_week is None:
|
||
messagebox.showerror("错误", "每月第N个星期X任务的设置无效。")
|
||
return
|
||
new_trigger["week_of_month"] = week_of_month
|
||
new_trigger["day_of_week"] = day_of_week
|
||
|
||
new_actions = []
|
||
for _, device_var, _, command_var, _, delay_var in self.action_entries:
|
||
selected_device_name = device_var.get()
|
||
selected_command_name = command_var.get()
|
||
delay = int(delay_var.get())
|
||
|
||
if selected_device_name == "无设备" or selected_command_name == "无指令":
|
||
messagebox.showerror("错误", "指令列表中有未选择设备或指令的项。")
|
||
return
|
||
if delay < 0:
|
||
messagebox.showerror("错误", "指令延迟不能为负数。")
|
||
return
|
||
|
||
device = next((d for d in self.master_app.devices if d["name"] == selected_device_name), None)
|
||
if not device:
|
||
messagebox.showerror("错误", f"设备 '{selected_device_name}' 未找到。")
|
||
return
|
||
|
||
command_index = -1
|
||
for i, cmd in enumerate(device["commands"]):
|
||
if cmd["name"] == selected_command_name:
|
||
command_index = i
|
||
break
|
||
|
||
if command_index == -1:
|
||
messagebox.showerror("错误", f"指令 '{selected_command_name}' 在设备 '{selected_device_name}' 中未找到。")
|
||
return
|
||
|
||
new_actions.append({
|
||
"device_id": device["id"],
|
||
"command_index": command_index,
|
||
"delay_ms": delay
|
||
})
|
||
|
||
if not new_actions:
|
||
messagebox.showerror("错误", "请至少添加一个执行指令。")
|
||
return
|
||
|
||
# Update or add task
|
||
self.task_data.update({
|
||
"name": name,
|
||
"schedule_type": schedule_type, # 保存英文键
|
||
"trigger": new_trigger,
|
||
"actions": new_actions,
|
||
"enabled": self.enabled_var.get()
|
||
})
|
||
|
||
# Clear last_run_time and next_run_time for re-calculation
|
||
self.task_data["last_run_time"] = None
|
||
self.task_data["next_run_time"] = None
|
||
|
||
# If it's a new task, add it to the list
|
||
if self.original_task_id not in [t["id"] for t in self.master_app.scheduled_tasks]:
|
||
self.master_app.scheduled_tasks.append(self.task_data)
|
||
|
||
self.master_app.save_config_callback()
|
||
# 修正此处:通过 master_app.master 访问 MainControlApp 的方法
|
||
self.master_app.master._recalculate_all_next_run_times() # 重新计算所有任务的下次运行时间
|
||
self.master_app.master.refresh_scheduler_display() # 立即刷新主界面的调度器状态
|
||
self.master_app._update_schedule_display() # Refresh the list in SchedulerApp
|
||
self.master_app.log_message_callback(f"定时任务 '{name}' (ID: {self.task_data['id'][:8]}...) 已保存。", "blue")
|
||
self.destroy()
|
||
|
||
except ValueError as e:
|
||
messagebox.showerror("输入错误", f"请检查输入值是否正确: {e}")
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"保存任务时发生未知错误: {e}")
|
||
|