import
tkinter as tk
from
tkinter
import
ttk, messagebox, filedialog
import
json
from
datetime
import
datetime, timedelta
from
tkcalendar
import
DateEntry
from
tkinter
import
font as tkfont
import
threading
import
time
import
sys
import
os
class
AppConfig:
COLORS
=
{
"bg"
:
"#ffffff"
, # 纯白背景
"fg"
:
"#333333"
, # 深灰文字
"accent"
:
"#4a90e2"
, # 清新蓝色主题
"success"
:
"#2ecc71"
, # 薄荷绿
"warning"
:
"#f1c40f"
, # 明黄色
"error"
:
"#e74c3c"
, # 柔和红
"light_bg"
:
"#f5f6fa"
, # 超浅灰背景
"border"
:
"#e1e4e8"
, # 浅灰边框
"listbox_bg"
:
"#ffffff"
, # 列表框背景
"listbox_fg"
:
"#333333"
, # 列表框文字
"hover"
:
"#f8f9fa"
, # 悬停背景色
"disabled"
:
"#dcdde1"
, # 禁用状态色
"progress_normal"
:
"#4a90e2"
, # 正常进度颜色
"progress_warning"
:
"#f1c40f"
, # 警告进度颜色(
75
%
以上)
"progress_danger"
:
"#e74c3c"
, # 危险进度颜色(
90
%
以上)
"progress_bg"
:
"#f5f6fa"
# 进度条背景色
}
DEFAULT_CONFIG
=
{
"window_size"
:
"1280x720"
,
"transparency"
:
1.0
}
CATEGORIES
=
[
"默认"
,
"工作"
,
"个人"
,
"购物"
,
"学习"
]
PRIORITIES
=
[
"低"
,
"普通"
,
"高"
]
PRIORITY_ICONS
=
{
"高"
: (
"🔴"
, COLORS[
"error"
]),
"普通"
: (
"🔵"
, COLORS[
"warning"
]),
"低"
: (
"🟢"
, COLORS[
"success"
])
}
SUPPORTED_ATTACHMENTS
=
[
(
"所有文件"
,
"*.*"
),
(
"文本文件"
,
"*.txt"
),
(
"图片文件"
,
"*.png *.jpg *.jpeg *.gif"
),
(
"文档文件"
,
"*.pdf *.doc *.docx"
),
]
ATTACHMENT_DIR
=
"attachments"
PROGRESS_COLORS
=
{
"normal"
:
"progress_normal"
,
"warning"
:
"progress_warning"
,
"danger"
:
"progress_danger"
}
class
FileManager:
@staticmethod
def
get_resource_path(relative_path):
try
:
if
getattr
(sys,
'frozen'
,
False
):
base_path
=
os.path.abspath(os.path.dirname(sys.executable))
else
:
base_path
=
os.path.abspath(
'.'
)
return
os.path.abspath(os.path.normpath(os.path.join(base_path, relative_path)))
except
Exception as e:
print
(f
"获取路径失败: {e},文件路径: {relative_path}"
,
file
=
sys.stderr)
return
os.path.abspath(os.path.normpath(os.path.join(os.path.abspath(
"."
), relative_path)))
@classmethod
def
load_json(
cls
, filename):
try
:
path
=
cls
.get_resource_path(filename)
os.makedirs(os.path.dirname(path), exist_ok
=
True
)
if
os.path.exists(path):
with
open
(path,
"r"
, encoding
=
"utf-8"
) as f:
try
:
return
json.load(f)
except
json.JSONDecodeError as e:
print
(f
"JSON解析错误: {e},文件路径: {path}"
,
file
=
sys.stderr)
return
None
else
:
if
filename
=
=
"tasks.json"
:
cls
.save_json([], filename)
return
[]
elif
filename
=
=
"config.json"
:
default_config
=
AppConfig.DEFAULT_CONFIG.copy()
cls
.save_json(default_config, filename)
return
default_config
except
Exception as e:
print
(f
"加载{filename}失败: {e},文件路径: {path}"
,
file
=
sys.stderr)
if
filename
=
=
"config.json"
:
return
AppConfig.DEFAULT_CONFIG.copy()
return
None
@classmethod
def
save_json(
cls
, data, filename):
path
=
cls
.get_resource_path(filename)
backup_path
=
f
"{path}.bak"
temp_path
=
f
"{path}.tmp"
try
:
with
open
(temp_path,
"w"
, encoding
=
"utf-8"
) as f:
json.dump(data, f, ensure_ascii
=
False
, indent
=
2
)
if
os.path.exists(path):
try
:
import
shutil
shutil.copy2(path, backup_path)
except
Exception as e:
print
(f
"创建备份文件失败: {e}"
)
try
:
os.replace(temp_path, path)
if
os.path.exists(backup_path):
os.remove(backup_path)
return
True
except
Exception as e:
if
os.path.exists(backup_path):
os.replace(backup_path, path)
print
(f
"保存{filename}失败: {e}"
)
return
False
except
Exception as e:
print
(f
"保存{filename}失败: {e}"
)
return
False
finally
:
if
os.path.exists(temp_path):
try
:
os.remove(temp_path)
except
:
pass
class
TaskModel:
def
__init__(
self
):
self
.tasks
=
[]
self
.filtered_indices
=
[]
self
.load_tasks()
def
load_tasks(
self
):
tasks
=
FileManager.load_json(
"tasks.json"
)
if
tasks:
for
task
in
tasks:
self
._init_task_fields(task)
self
.tasks
=
tasks
else
:
self
.tasks
=
[]
def
save_tasks(
self
):
return
FileManager.save_json(
self
.tasks,
"tasks.json"
)
def
add_task(
self
, task_data):
task
=
{
"task"
: task_data[
"task"
],
"priority"
: task_data[
"priority"
],
"due_date"
: task_data[
"due_date"
],
"category"
: task_data[
"category"
],
"completed"
: task_data.get(
"completed"
,
False
),
"reminded"
:
False
,
"created_at"
: task_data[
"created_at"
],
"attachments"
: task_data.get(
"attachments"
, []),
"links"
: task_data.get(
"links"
, [])
}
self
.tasks.append(task)
self
.save_tasks()
def
_init_task_fields(
self
, task):
task.setdefault(
"reminded"
,
False
)
task.setdefault(
"priority"
,
"普通"
)
task.setdefault(
"category"
,
"默认"
)
task.setdefault(
"due_date"
, datetime.now().strftime(
"%Y-%m-%d"
))
task.setdefault(
"attachments"
, [])
task.setdefault(
"links"
, [])
class
TodoApp:
def
__init__(
self
, root):
self
.root
=
root
self
.model
=
TaskModel()
self
.config
=
self
._load_config()
self
.selected_task
=
None
self
.selected_frame
=
None
import
threading
self
.task_lock
=
threading.Lock()
import
atexit
atexit.register(
self
.model.save_tasks)
self
.root.protocol(
"WM_DELETE_WINDOW"
,
self
._on_closing)
self
._init_ui()
self
._setup_bindings()
self
._start_reminder_thread()
self
._check_urgent_tasks()
def
_init_ui(
self
):
self
.root.title(
"✨ 待办事项清单"
)
self
._setup_window()
self
._setup_styles()
self
._create_main_frame()
self
._create_menu()
self
.task_tree.tag_configure(
"priority_高"
, background
=
AppConfig.COLORS[
"error"
])
self
.task_tree.tag_configure(
"priority_普通"
, background
=
AppConfig.COLORS[
"warning"
])
self
.task_tree.tag_configure(
"priority_低"
, background
=
AppConfig.COLORS[
"success"
])
self
.task_tree.tag_configure(
"priority_高"
, background
=
AppConfig.COLORS[
"error"
], foreground
=
"white"
)
self
.task_tree.tag_configure(
"priority_普通"
, background
=
AppConfig.COLORS[
"warning"
], foreground
=
"black"
)
self
.task_tree.tag_configure(
"priority_低"
, background
=
AppConfig.COLORS[
"success"
], foreground
=
"white"
)
self
.update_task_list()
self
.update_stats()
def
_setup_window(
self
):
self
.root.geometry(
self
.config.get(
"window_size"
,
"1280x720"
))
self
.root.minsize(
512
,
288
)
self
.root.attributes(
"-alpha"
,
self
.config.get(
"transparency"
,
1.0
))
self
.root.grid_rowconfigure(
0
, weight
=
1
)
self
.root.grid_columnconfigure(
0
, weight
=
1
)
def
_setup_styles(
self
):
style
=
ttk.Style()
colors
=
AppConfig.COLORS
style.configure(
"."
,
font
=
(
'Microsoft YaHei UI'
,
10
),
background
=
colors[
"bg"
])
style.configure(
"Treeview"
,
rowheight
=
30
,
foreground
=
colors[
"fg"
])
style.configure(
"Title.TLabel"
,
font
=
(
'Microsoft YaHei UI'
,
24
,
'bold'
),
foreground
=
colors[
"fg"
],
background
=
colors[
"bg"
],
padding
=
(
0
,
10
))
style.configure(
"TButton"
,
padding
=
(
10
,
5
),
font
=
(
'Microsoft YaHei UI'
,
10
),
background
=
colors[
"light_bg"
],
foreground
=
colors[
"fg"
])
style.
map
(
"TButton"
,
background
=
[(
"active"
, colors[
"hover"
]),
(
"pressed"
, colors[
"accent"
])],
foreground
=
[(
"active"
, colors[
"fg"
]),
(
"pressed"
, colors[
"fg"
])])
style.configure(
"Accent.TButton"
,
padding
=
(
20
,
10
),
font
=
(
'Microsoft YaHei UI'
,
10
,
'bold'
),
background
=
colors[
"accent"
],
foreground
=
colors[
"fg"
])
style.
map
(
"Accent.TButton"
,
background
=
[(
"active"
, colors[
"hover"
]),
(
"pressed"
, colors[
"accent"
]),
(
"disabled"
, colors[
"disabled"
])],
foreground
=
[(
"disabled"
, colors[
"fg"
]),
(
"active"
, colors[
"fg"
]),
(
"pressed"
, colors[
"fg"
])])
style.configure(
"TEntry"
,
fieldbackground
=
colors[
"light_bg"
],
foreground
=
colors[
"fg"
],
borderwidth
=
0
,
relief
=
"flat"
,
padding
=
(
10
,
8
))
style.configure(
"TCombobox"
,
background
=
colors[
"light_bg"
],
fieldbackground
=
colors[
"light_bg"
],
foreground
=
colors[
"fg"
],
arrowcolor
=
colors[
"fg"
],
padding
=
(
5
,
5
))
style.configure(
"TLabelframe"
,
background
=
colors[
"bg"
],
borderwidth
=
1
,
relief
=
"solid"
)
style.configure(
"TLabelframe.Label"
,
background
=
colors[
"bg"
],
foreground
=
colors[
"fg"
],
font
=
(
'Microsoft YaHei UI'
,
9
))
style.configure(
"Link.TButton"
,
background
=
colors[
"bg"
],
foreground
=
colors[
"accent"
],
borderwidth
=
0
,
font
=
(
'Microsoft YaHei UI'
,
9
,
'underline'
))
style.
map
(
"Link.TButton"
,
background
=
[(
"active"
, colors[
"bg"
]),
(
"pressed"
, colors[
"bg"
])],
foreground
=
[(
"active"
, colors[
"error"
])])
style.configure(
"Normal.Horizontal.TProgressbar"
,
troughcolor
=
colors[
"progress_bg"
],
background
=
colors[
"progress_normal"
],
borderwidth
=
0
,
thickness
=
6
)
style.configure(
"Warning.Horizontal.TProgressbar"
,
troughcolor
=
colors[
"progress_bg"
],
background
=
colors[
"progress_warning"
],
borderwidth
=
0
,
thickness
=
6
)
style.configure(
"Danger.Horizontal.TProgressbar"
,
troughcolor
=
colors[
"progress_bg"
],
background
=
colors[
"progress_danger"
],
borderwidth
=
0
,
thickness
=
6
)
style.configure(
"Progress.TLabel"
,
font
=
(
'Microsoft YaHei UI'
,
9
),
background
=
colors[
"bg"
],
foreground
=
colors[
"fg"
])
style.configure(
"Selected.TFrame"
,
background
=
colors[
"accent"
],
relief
=
"solid"
,
borderwidth
=
1
)
style.theme_use(
"clam"
)
def
_create_main_frame(
self
):
self
.main_frame
=
ttk.Frame(
self
.root, padding
=
"20"
)
self
.main_frame.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
)
self
.main_frame.grid_columnconfigure(
0
, weight
=
3
)
self
.main_frame.grid_columnconfigure(
1
, weight
=
1
)
self
.main_frame.grid_rowconfigure(
0
, weight
=
1
)
self
._create_left_panel()
self
._create_right_panel()
def
_create_left_panel(
self
):
left_panel
=
ttk.Frame(
self
.main_frame)
left_panel.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
, padx
=
(
0
,
20
))
left_panel.grid_columnconfigure(
0
, weight
=
1
)
left_panel.grid_columnconfigure(
0
, weight
=
1
)
left_panel.grid_rowconfigure(
0
, weight
=
0
)
left_panel.grid_rowconfigure(
1
, weight
=
0
)
left_panel.grid_rowconfigure(
2
, weight
=
1
)
ttk.Label(left_panel, text
=
"✨ 我的待办清单(重构版)"
, style
=
"Title.TLabel"
).grid(
row
=
0
, column
=
0
, sticky
=
"w"
, pady
=
(
0
,
20
))
input_frame
=
ttk.LabelFrame(left_panel, text
=
"新建事项"
, padding
=
10
)
input_frame.grid(row
=
1
, column
=
0
, sticky
=
"ew"
)
input_frame.grid_columnconfigure(
0
, weight
=
1
)
self
.task_var
=
tk.StringVar()
self
.task_entry
=
ttk.Entry(
input_frame,
textvariable
=
self
.task_var,
font
=
(
'Microsoft YaHei UI'
,
11
)
)
self
.task_entry.grid(row
=
0
, column
=
0
, sticky
=
"ew"
, pady
=
(
0
,
10
))
self
.entry_menu
=
tk.Menu(
self
.task_entry, tearoff
=
0
)
self
.entry_menu.add_command(label
=
"剪切"
, command
=
lambda
:
self
.entry_menu_action(
'cut'
))
self
.entry_menu.add_command(label
=
"复制"
, command
=
lambda
:
self
.entry_menu_action(
'copy'
))
self
.entry_menu.add_command(label
=
"粘贴"
, command
=
lambda
:
self
.entry_menu_action(
'paste'
))
self
.entry_menu.add_separator()
self
.entry_menu.add_command(label
=
"全选"
, command
=
lambda
:
self
.entry_menu_action(
'select_all'
))
self
.task_entry.bind(
"<Button-3>"
,
self
.show_entry_menu)
attrs_frame
=
ttk.Frame(input_frame)
attrs_frame.grid(row
=
1
, column
=
0
, sticky
=
"ew"
)
attrs_frame.grid_columnconfigure(
3
, weight
=
1
)
priority_frame
=
ttk.Frame(attrs_frame)
priority_frame.grid(row
=
0
, column
=
0
, padx
=
(
0
,
15
))
ttk.Label(priority_frame, text
=
"优先级"
).grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.priority_var
=
tk.StringVar(value
=
"普通"
)
self
.priority_combo
=
ttk.Combobox(
priority_frame,
textvariable
=
self
.priority_var,
values
=
[
"低"
,
"普通"
,
"高"
],
width
=
6
,
state
=
"readonly"
)
self
.priority_combo.grid(row
=
0
, column
=
1
)
date_frame
=
ttk.Frame(attrs_frame)
date_frame.grid(row
=
0
, column
=
1
, padx
=
(
0
,
15
))
ttk.Label(date_frame, text
=
"截止日期"
).grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.due_date
=
DateEntry(
date_frame,
width
=
10
,
background
=
AppConfig.COLORS[
"accent"
],
foreground
=
"white"
,
borderwidth
=
0
)
self
.due_date.grid(row
=
0
, column
=
1
)
category_frame
=
ttk.Frame(attrs_frame)
category_frame.grid(row
=
0
, column
=
2
)
ttk.Label(category_frame, text
=
"分类"
).grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.category_var
=
tk.StringVar(value
=
"默认"
)
self
.category_combo
=
ttk.Combobox(
category_frame,
textvariable
=
self
.category_var,
values
=
[
"默认"
,
"工作"
,
"个人"
,
"购物"
,
"学习"
],
width
=
8
,
state
=
"readonly"
)
self
.category_combo.grid(row
=
0
, column
=
1
)
buttons_frame
=
ttk.Frame(attrs_frame)
buttons_frame.grid(row
=
0
, column
=
3
, padx
=
(
10
,
0
))
self
.attachment_button
=
ttk.Button(
buttons_frame,
text
=
"📎 附件"
,
command
=
self
.add_attachment,
width
=
8
)
self
.attachment_button.grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.link_button
=
ttk.Button(
buttons_frame,
text
=
"🔗 链接"
,
command
=
self
.add_link,
width
=
8
)
self
.link_button.grid(row
=
0
, column
=
1
)
preview_frame
=
ttk.Frame(input_frame)
preview_frame.grid(row
=
3
, column
=
0
, sticky
=
"ew"
, pady
=
(
10
,
0
))
self
.current_attachments
=
[]
self
.current_links
=
[]
self
.attachment_preview_frame
=
ttk.Frame(preview_frame)
self
.attachment_preview_frame.grid(row
=
0
, column
=
0
, sticky
=
"w"
)
self
.link_preview_frame
=
ttk.Frame(preview_frame)
self
.link_preview_frame.grid(row
=
1
, column
=
0
, sticky
=
"w"
)
add_button
=
ttk.Button(
input_frame,
text
=
"➕ 添加事项"
,
command
=
self
.add_task,
style
=
"Accent.TButton"
)
add_button.grid(row
=
2
, column
=
0
, sticky
=
"ew"
, pady
=
(
10
,
0
))
self
.task_entry.bind(
'<Return>'
,
lambda
e:
self
.add_task())
list_frame
=
ttk.Frame(left_panel)
list_frame.grid(row
=
2
, column
=
0
, sticky
=
"nsew"
, pady
=
10
)
list_frame.grid_columnconfigure(
0
, weight
=
1
)
list_frame.grid_rowconfigure(
0
, weight
=
1
)
self
.tasks_container
=
ttk.Frame(list_frame)
self
.tasks_container.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
)
self
.tasks_container.grid_columnconfigure(
0
, weight
=
1
)
self
.tasks_container.grid_rowconfigure(
0
, weight
=
1
)
self
.task_tree
=
ttk.Treeview(
self
.tasks_container, columns
=
(
"status"
,
"priority"
,
"task"
,
"due_date"
,
"category"
,
"task_data"
), show
=
"headings"
)
self
.task_tree.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
)
self
.task_tree.heading(
"status"
, text
=
"状态"
, command
=
lambda
:
self
._sort_by_column(
"status"
))
self
.task_tree.heading(
"priority"
, text
=
"优先级"
, command
=
lambda
:
self
._sort_by_column(
"priority"
))
self
.task_tree.heading(
"task"
, text
=
"事项"
, command
=
lambda
:
self
._sort_by_column(
"task"
))
self
.task_tree.heading(
"due_date"
, text
=
"剩余时间"
, command
=
lambda
:
self
._sort_by_column(
"due_date"
))
self
.task_tree.heading(
"category"
, text
=
"分类"
, command
=
lambda
:
self
._sort_by_column(
"category"
))
self
.task_tree.heading(
"task_data"
, text
=
"")
self
.task_tree.column(
"status"
, width
=
0
, minwidth
=
60
, stretch
=
True
)
self
.task_tree.column(
"priority"
, width
=
0
, minwidth
=
60
, stretch
=
True
)
self
.task_tree.column(
"task"
, width
=
300
, stretch
=
False
)
self
.task_tree.column(
"due_date"
, width
=
0
, minwidth
=
80
, stretch
=
True
)
self
.task_tree.column(
"category"
, width
=
0
, minwidth
=
60
, stretch
=
True
)
self
.task_tree.column(
"task_data"
, width
=
0
, stretch
=
False
)
style
=
ttk.Style()
style.configure(
"Treeview"
, rowheight
=
25
)
style.configure(
"Treeview"
, wraplength
=
190
)
scrollbar
=
ttk.Scrollbar(list_frame, orient
=
"vertical"
, command
=
self
.task_tree.yview)
scrollbar.grid(row
=
0
, column
=
1
, sticky
=
"ns"
)
self
.task_tree.configure(yscrollcommand
=
scrollbar.
set
)
def
_on_canvas_configure(
self
, event):
self
.canvas.itemconfig(
1
, width
=
event.width)
def
_create_right_panel(
self
):
right_panel
=
ttk.Frame(
self
.main_frame)
right_panel.grid(row
=
0
, column
=
1
, sticky
=
"nsew"
)
right_panel.grid_columnconfigure(
0
, weight
=
1
)
for
i
in
range
(
6
):
right_panel.grid_rowconfigure(i, weight
=
1
)
self
.detail_frame
=
ttk.LabelFrame(right_panel, text
=
"✍️ 事项详情"
, padding
=
10
)
self
.detail_frame.grid(row
=
0
, column
=
0
, sticky
=
"nsew"
, pady
=
(
0
,
10
))
self
.detail_frame.grid_columnconfigure(
1
, weight
=
1
)
self
.detail_labels
=
{}
fields
=
[
(
"事项内容"
,
"task"
),
(
"创建时间"
,
"created_at"
),
(
"截止日期"
,
"due_date"
),
(
"优先级"
,
"priority"
),
(
"分类"
,
"category"
),
(
"状态"
,
"completed"
),
(
"附件"
,
"attachments"
),
(
"链接"
,
"links"
)
]
for
i, (label, _)
in
enumerate
(fields):
ttk.Label(
self
.detail_frame, text
=
f
"{label}:"
).grid(row
=
i, column
=
0
, sticky
=
"w"
, pady
=
2
)
self
.detail_labels[label]
=
ttk.Label(
self
.detail_frame, text
=
"")
self
.detail_labels[label].grid(row
=
i, column
=
1
, sticky
=
"w"
, pady
=
2
, padx
=
5
)
self
.stats_frame
=
ttk.LabelFrame(right_panel, text
=
"📊 统计信息"
, padding
=
10
)
self
.stats_frame.grid(row
=
1
, column
=
0
, sticky
=
"nsew"
, pady
=
(
0
,
10
))
self
.stats_frame.grid_columnconfigure(
0
, weight
=
1
)
self
.stats_labels
=
{}
stats
=
[
(
"总事项"
,
"🗂️"
),
(
"已完成"
,
"✅"
),
(
"未完成"
,
"⏳"
),
(
"今日截止"
,
"📅"
)
]
for
i, (label, icon)
in
enumerate
(stats):
frame
=
ttk.Frame(
self
.stats_frame)
frame.grid(row
=
i, column
=
0
, sticky
=
"ew"
, pady
=
2
)
frame.grid_columnconfigure(
1
, weight
=
1
)
ttk.Label(frame, text
=
f
"{icon} {label}:"
).grid(row
=
0
, column
=
0
, sticky
=
"w"
)
self
.stats_labels[label]
=
ttk.Label(frame, text
=
"0"
)
self
.stats_labels[label].grid(row
=
0
, column
=
1
, sticky
=
"e"
)
filter_frame
=
ttk.LabelFrame(right_panel, text
=
"🔍 筛选"
, padding
=
10
)
filter_frame.grid(row
=
2
, column
=
0
, sticky
=
"nsew"
, pady
=
(
0
,
10
))
filter_frame.grid_columnconfigure(
0
, weight
=
1
)
category_filter_frame
=
ttk.Frame(filter_frame)
category_filter_frame.grid(row
=
0
, column
=
0
, sticky
=
"ew"
, pady
=
(
0
,
5
))
category_filter_frame.grid_columnconfigure(
1
, weight
=
1
)
ttk.Label(category_filter_frame, text
=
"分类:"
).grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.filter_category_var
=
tk.StringVar(value
=
"全部"
)
self
.filter_category
=
ttk.Combobox(
category_filter_frame,
textvariable
=
self
.filter_category_var,
values
=
[
"全部"
,
"默认"
,
"工作"
,
"个人"
,
"购物"
,
"学习"
],
width
=
8
,
state
=
"readonly"
)
self
.filter_category.grid(row
=
0
, column
=
1
, sticky
=
"ew"
)
self
.filter_category.bind(
'<<ComboboxSelected>>'
,
lambda
e:
self
.update_task_list())
status_filter_frame
=
ttk.Frame(filter_frame)
status_filter_frame.grid(row
=
1
, column
=
0
, sticky
=
"ew"
)
status_filter_frame.grid_columnconfigure(
1
, weight
=
1
)
ttk.Label(status_filter_frame, text
=
"状态:"
).grid(row
=
0
, column
=
0
, padx
=
(
0
,
5
))
self
.filter_status_var
=
tk.StringVar(value
=
"全部"
)
self
.filter_status
=
ttk.Combobox(
status_filter_frame,
textvariable
=
self
.filter_status_var,
values
=
[
"全部"
,
"已完成"
,
"未完成"
],
width
=
8
,
state
=
"readonly"
)
self
.filter_status.grid(row
=
0
, column
=
1
, sticky
=
"ew"
)
self
.filter_status.bind(
'<<ComboboxSelected>>'
,
lambda
e:
self
.update_task_list())
buttons_frame
=
ttk.LabelFrame(right_panel, text
=
"操作"
, padding
=
10
)
buttons_frame.grid(row
=
3
, column
=
0
, sticky
=
"nsew"
, pady
=
(
0
,
10
))
buttons_frame.grid_columnconfigure(
0
, weight
=
1
)
self
.mark_button
=
ttk.Button(
buttons_frame,
text
=
"✓ 标记完成"
,
command
=
self
.mark_complete,
style
=
"Accent.TButton"
)
self
.mark_button.grid(row
=
0
, column
=
0
, sticky
=
"ew"
, pady
=
2
)
ttk.Button(
buttons_frame,
text
=
"🗑️ 删除事项"
,
command
=
self
.delete_task,
style
=
"Accent.TButton"
).grid(row
=
1
, column
=
0
, sticky
=
"ew"
, pady
=
2
)
ttk.Button(
buttons_frame,
text
=
"📋 导出事项"
,
command
=
self
.export_tasks,
style
=
"Accent.TButton"
).grid(row
=
2
, column
=
0
, sticky
=
"ew"
, pady
=
2
)
sort_frame
=
ttk.LabelFrame(right_panel, text
=
"排序方式"
, padding
=
10
)
sort_frame.grid(row
=
4
, column
=
0
, sticky
=
"nsew"
, pady
=
(
0
,
10
))
sort_frame.grid_columnconfigure(
0
, weight
=
1
)
self
.sort_var
=
tk.StringVar(value
=
"优先级"
)
self
.sort_combo
=
ttk.Combobox(
sort_frame,
textvariable
=
self
.sort_var,
values
=
[
"优先级"
,
"创建时间"
,
"截止日期"
,
"分类"
],
state
=
"readonly"
)
self
.sort_combo.grid(row
=
0
, column
=
0
, sticky
=
"ew"
)
self
.sort_combo.bind(
'<<ComboboxSelected>>'
,
lambda
e:
self
.update_task_list())
self
.task_tree.bind(
'<<TreeviewSelect>>'
,
self
._on_tree_select)
self
.task_tree.bind(
'<Up>'
,
self
._on_up_key)
self
.task_tree.bind(
'<Down>'
,
self
._on_down_key)
def
_setup_bindings(
self
):
self
.root.bind(
"<Configure>"
,
self
._on_window_configure)
self
.root.protocol(
"WM_DELETE_WINDOW"
,
self
._on_closing)
self
.task_tree.bind(
'<space>'
,
self
._on_tree_space)
self
.task_tree.bind(
'<Delete>'
,
self
.delete_task)
self
.task_tree.bind(
'<Up>'
,
self
.select_previous_task)
self
.task_tree.bind(
'<Down>'
,
self
.select_next_task)
self
.task_tree.bind(
'<Double-1>'
,
self
._on_tree_double_click)
self
.context_menu
=
tk.Menu(
self
.root, tearoff
=
0
)
self
.context_menu.add_command(label
=
"✓ 标记完成"
, command
=
self
.mark_complete)
self
.context_menu.add_command(label
=
"🗑️ 删除事项"
, command
=
self
.delete_task)
self
.context_menu.add_separator()
self
.context_menu.add_command(label
=
"📋 复制内容"
, command
=
self
.copy_task_content)
self
.task_tree.bind(
'<Button-3>'
,
self
._show_context_menu)
def
_on_tree_space(
self
, event
=
None
):
selected_items
=
self
.task_tree.selection()
if
selected_items:
try
:
item_id
=
selected_items[
0
]
if
not
self
.task_tree.exists(item_id):
print
(f
"Item {item_id} not found."
)
return
values
=
self
.task_tree.item(item_id,
'values'
)
if
values
and
len
(values) >
0
:
task_data
=
json.loads(values[
-
1
])
for
task
in
self
.model.tasks:
if
task
=
=
task_data:
self
.toggle_task_status(
None
, task)
break
except
(json.JSONDecodeError, IndexError) as e:
print
(f
"Error processing task: {e}"
)
def
_on_tree_double_click(
self
, event):
item
=
self
.task_tree.identify(
'item'
, event.x, event.y)
if
item:
if
not
self
.task_tree.exists(item):
print
(f
"Item {item} not found."
)
return
item_index
=
self
.task_tree.index(item)
if
0
<
=
item_index <
len
(
self
.model.filtered_indices):
try
:
task
=
self
.model.tasks[
self
.model.filtered_indices[item_index]]
self
.toggle_task_status(
None
, task)
self
.task_tree.update_idletasks()
if
self
.task_tree.exists(item):
self
.task_tree.selection_set(item)
except
IndexError as e:
print
(f
"Error processing task: {e}"
)
def
_show_context_menu(
self
, event):
item
=
self
.task_tree.identify(
'item'
, event.x, event.y)
if
item:
self
.task_tree.selection_set(item)
item_index
=
self
.task_tree.index(item)
if
0
<
=
item_index <
len
(
self
.model.filtered_indices):
self
.selected_task
=
self
.model.tasks[
self
.model.filtered_indices[item_index]]
button_text
=
"✓ 标记未完成"
if
self
.selected_task[
"completed"
]
else
"✓ 标记完成"
self
.context_menu.entryconfig(
1
, label
=
button_text)
self
.context_menu.tk_popup(event.x_root, event.y_root)
def
toggle_task_status(
self
, item
=
None
, task
=
None
):
target_task
=
None
if
task:
target_task
=
task
elif
item:
if
not
self
.task_tree.exists(item):
print
(f
"Item {item} not found."
)
return
values
=
self
.task_tree.item(item,
'values'
)
if
values:
task_data
=
json.loads(values[
-
1
])
for
t
in
self
.model.tasks:
if
t
=
=
task_data:
target_task
=
t
break
if
target_task:
target_task[
'completed'
]
=
not
target_task[
'completed'
]
self
.selected_task
=
target_task
self
.model.save_tasks()
self
.update_task_list()
self
.update_stats()
self
.show_task_details()
self
.task_tree.update_idletasks()
for
item_id
in
self
.task_tree.get_children():
if
not
self
.task_tree.exists(item_id):
continue
values
=
self
.task_tree.item(item_id,
'values'
)
if
values
and
json.loads(values[
-
1
])
=
=
target_task:
self
.task_tree.selection_set(item_id)
self
.task_tree.see(item_id)
break
def
_safe_update_ui(
self
):
try
:
self
.update_task_list()
self
.update_stats()
except
Exception as e:
print
(f
"UI更新出错: {e}"
)
def
_start_reminder_thread(
self
):
self
.task_lock
=
threading.Lock()
self
.running
=
True
def
update_progress():
while
self
.running:
try
:
with
self
.task_lock:
if
self
.root
and
self
.root.winfo_exists():
self
.root.after_idle(
self
._safe_update_ui)
time.sleep(
120
)
except
Exception as e:
print
(f
"更新进度出错: {e}"
)
time.sleep(
180
)
self
.reminder_thread
=
threading.Thread(
target
=
update_progress,
daemon
=
True
)
self
.reminder_thread.start()
def
_load_config(
self
):
config
=
FileManager.load_json(
"config.json"
)
if
config
is
None
:
config
=
AppConfig.DEFAULT_CONFIG.copy()
FileManager.save_json(config,
"config.json"
)
else
:
for
key, value
in
AppConfig.DEFAULT_CONFIG.items():
config.setdefault(key, value)
return
config
def
_save_config(
self
):
FileManager.save_json(
self
.config,
"config.json"
)
def
_on_window_configure(
self
, event):
if
event.widget
=
=
self
.root:
size
=
f
"{self.root.winfo_width()}x{self.root.winfo_height()}"
self
.config[
"window_size"
]
=
size
self
._save_config()
def
_on_closing(
self
):
try
:
if
self
.root
and
self
.root.winfo_exists():
self
.root.withdraw()
self
.root.update()
self
.running
=
False
def
save_data():
try
:
self
.model.save_tasks()
except
Exception as e:
print
(f
"保存事项数据失败: {e}"
,
file
=
sys.stderr)
save_thread
=
threading.Thread(target
=
save_data)
save_thread.start()
save_thread.join(timeout
=
0.5
)
for
widget
in
[
self
.context_menu,
self
.entry_menu,
self
.task_tree]:
try
:
if
hasattr
(
self
, widget.__str__())
and
widget:
widget.destroy()
except
Exception:
pass
if
hasattr
(
self
,
'reminder_thread'
)
and
self
.reminder_thread.is_alive():
self
.reminder_thread.join(timeout
=
0.2
)
if
self
.root
and
self
.root.winfo_exists():
self
.root.destroy()
except
Exception as e:
print
(f
"程序退出时清理资源失败: {e}"
,
file
=
sys.stderr)
if
self
.root
and
self
.root.winfo_exists():
self
.root.destroy()
def
_check_reminders(
self
):
while
True
:
try
:
today
=
datetime.now().strftime(
"%Y-%m-%d"
)
for
task
in
self
.model.tasks:
if
(
not
task[
"completed"
]
and
task[
"due_date"
]
=
=
today
and
not
task.get(
"reminded"
,
False
)):
self
.root.after(
0
,
lambda
t
=
task: messagebox.showwarning(
"事项提醒"
,
f
"事项「{t['task']}」将在今天截止!"
))
task[
"reminded"
]
=
True
self
.model.save_tasks()
due_date
=
datetime.strptime(task[
"due_date"
],
"%Y-%m-%d"
)
remaining_time
=
due_date
-
datetime.now()
if
(
not
task[
"completed"
]
and
remaining_time.days
=
=
0
and
remaining_time.seconds >
0
and
not
task.get(
"reminded_1day"
,
False
)):
self
.root.after(
0
,
lambda
t
=
task: messagebox.showwarning(
"事项提醒"
,
f
"事项「{t['task']}」将在1天内截止!"
))
task[
"reminded_1day"
]
=
True
self
.model.save_tasks()
time.sleep(
300
)
except
Exception as e:
print
(f
"提醒检查出错: {e}"
)
time.sleep(
60
)
def
add_task(
self
):
task
=
self
.task_var.get().strip()
if
task:
current_time
=
datetime.now().strftime(
"%Y-%m-%d %H:%M"
)
self
.model.add_task({
"task"
: task,
"completed"
:
False
,
"created_at"
: current_time,
"priority"
:
self
.priority_var.get(),
"due_date"
:
self
.due_date.get_date().strftime(
"%Y-%m-%d"
),
"category"
:
self
.category_var.get(),
"attachments"
:
self
.current_attachments.copy(),
"links"
:
self
.current_links.copy()
})
self
.task_var.
set
("")
self
.priority_var.
set
(
"普通"
)
self
.current_attachments
=
[]
self
.current_links
=
[]
self
._update_attachment_preview()
self
._update_link_preview()
self
.update_task_list()
self
.update_stats()
else
:
messagebox.showwarning(
"警告"
,
"请输入事项内容!"
)
def
mark_complete(
self
):
if
not
self
.selected_task:
messagebox.showwarning(
"提示"
,
"请先选择要标记的事项!"
)
return
for
task
in
self
.model.tasks:
if
task
=
=
self
.selected_task:
task[
"completed"
]
=
not
task[
"completed"
]
self
.selected_task
=
task
break
self
.model.save_tasks()
self
.update_task_list()
self
.update_stats()
self
.show_task_details()
def
delete_task(
self
, event
=
None
):
if
not
self
.selected_task:
messagebox.showinfo(
"提示"
,
"请先选择要删除的事项!"
)
return
if
messagebox.askyesno(
"确认删除"
, f
"确定要删除事项「{self.selected_task['task']}」吗?"
):
self
.model.tasks.remove(
self
.selected_task)
self
.model.save_tasks()
self
.selected_task
=
None
self
.selected_frame
=
None
self
.update_task_list()
self
.update_stats()
self
.show_task_details()
def
_calculate_remaining_time(
self
, task):
if
task[
"completed"
]:
return
"已完成"
try
:
due_time
=
datetime.strptime(task[
"due_date"
],
"%Y-%m-%d"
)
due_time
=
due_time.replace(hour
=
23
, minute
=
59
, second
=
59
)
current_time
=
datetime.now()
time_diff
=
due_time
-
current_time
if
time_diff.total_seconds() <
0
:
return
"已超时"
days
=
time_diff.days
hours
=
time_diff.seconds
/
/
3600
if
days >
0
:
if
hours >
0
:
return
f
"{days}天{hours}小时"
return
f
"{days}天"
elif
hours >
0
:
return
f
"{hours}小时"
else
:
return
"不足1小时"
except
Exception as e:
print
(f
"计算剩余时间出错: {e}"
,
file
=
sys.stderr)
return
"未知"
def
update_task_list(
self
):
selected_task
=
self
.selected_task
for
item
in
self
.task_tree.get_children():
self
.task_tree.delete(item)
self
.task_tree.column(
"priority"
, width
=
70
, anchor
=
"center"
)
self
.task_tree.tag_configure(
"priority_高"
, background
=
AppConfig.COLORS[
"error"
], foreground
=
"white"
)
self
.task_tree.tag_configure(
"priority_普通"
, background
=
AppConfig.COLORS[
"warning"
], foreground
=
"black"
)
self
.task_tree.tag_configure(
"priority_低"
, background
=
AppConfig.COLORS[
"success"
], foreground
=
"white"
)
self
.task_tree.tag_configure(
"evenrow"
, background
=
"#f8f9fa"
)
self
.task_tree.tag_configure(
"oddrow"
, background
=
"#ffffff"
)
self
.task_tree.tag_configure(
"completed"
, foreground
=
"#666666"
, background
=
"#e0e0e0"
, font
=
(
"Microsoft YaHei UI"
,
10
,
"overstrike"
))
self
.task_tree.tag_configure(
"selected"
, background
=
AppConfig.COLORS[
"accent"
], foreground
=
"white"
)
selected_item_id
=
None
category_filter
=
self
.filter_category_var.get()
status_filter
=
self
.filter_status_var.get()
sort_by
=
self
.sort_var.get()
self
.model.filtered_indices
=
[]
completed_tasks
=
[]
incomplete_tasks
=
[]
for
i, task
in
enumerate
(
self
.model.tasks):
if
category_filter !
=
"全部"
and
task[
"category"
] !
=
category_filter:
continue
if
status_filter
=
=
"已完成"
and
not
task[
"completed"
]:
continue
elif
status_filter
=
=
"未完成"
and
task[
"completed"
]:
continue
if
task[
"completed"
]:
completed_tasks.append((i, task))
else
:
incomplete_tasks.append((i, task))
if
sort_by
=
=
"优先级"
:
priority_order
=
{
"高"
:
0
,
"普通"
:
1
,
"低"
:
2
}
incomplete_tasks.sort(key
=
lambda
x: priority_order[x[
1
][
"priority"
]])
completed_tasks.sort(key
=
lambda
x: priority_order[x[
1
][
"priority"
]])
elif
sort_by
=
=
"创建时间"
:
incomplete_tasks.sort(key
=
lambda
x: x[
1
][
"created_at"
], reverse
=
True
)
completed_tasks.sort(key
=
lambda
x: x[
1
][
"created_at"
], reverse
=
True
)
elif
sort_by
=
=
"截止日期"
:
incomplete_tasks.sort(key
=
lambda
x: x[
1
][
"due_date"
])
completed_tasks.sort(key
=
lambda
x: x[
1
][
"due_date"
])
elif
sort_by
=
=
"分类"
:
incomplete_tasks.sort(key
=
lambda
x: x[
1
][
"category"
])
completed_tasks.sort(key
=
lambda
x: x[
1
][
"category"
])
filtered_tasks
=
incomplete_tasks
+
completed_tasks
self
.model.filtered_indices
=
[idx
for
idx, _
in
filtered_tasks]
for
i, (idx, task)
in
enumerate
(filtered_tasks):
tags
=
[]
if
task[
"completed"
]:
tags.append(
"completed"
)
else
:
if
i
%
2
:
tags.append(
"evenrow"
)
else
:
tags.append(
"oddrow"
)
tags.append(f
"priority_{task['priority']}"
)
priority
=
task[
"priority"
]
status
=
"✓"
if
task[
"completed"
]
else
"⚪"
item_id
=
self
.task_tree.insert("
", "
end", values
=
(
status,
priority,
task[
"task"
],
self
._calculate_remaining_time(task),
task[
"category"
],
json.dumps(task)
), tags
=
tags)
if
selected_task
and
task
=
=
selected_task:
selected_item_id
=
item_id
self
.selected_task
=
task
if
selected_item_id:
self
.task_tree.selection_set(selected_item_id)
self
.task_tree.see(selected_item_id)
self
.show_task_details()
def
_calculate_task_progress(
self
, task):
if
task.get(
"completed"
,
False
):
return
100
,
"已完成"
try
:
created_time
=
datetime.strptime(task[
"created_at"
],
"%Y-%m-%d %H:%M"
)
due_time
=
datetime.strptime(task[
"due_date"
],
"%Y-%m-%d"
)
due_time
=
due_time.replace(hour
=
23
, minute
=
59
, second
=
59
)
current_time
=
datetime.now()
if
current_time > due_time:
return
100
,
"超时"
total_time
=
(due_time
-
created_time).total_seconds()
used_time
=
(current_time
-
created_time).total_seconds()
progress
=
min
(
100
,
int
((used_time
/
total_time)
*
100
))
if
progress >
=
90
:
return
progress,
"即将到期"
elif
progress >
=
75
:
return
progress,
"进行中"
else
:
return
progress,
"正常"
except
Exception as e:
print
(f
"计算事项进度出错: {e},事项: {task['task']}"
,
file
=
sys.stderr)
return
0
,
"未知"
def
update_stats(
self
):
today
=
datetime.now().strftime(
"%Y-%m-%d"
)
total
=
len
(
self
.model.tasks)
completed
=
sum
(
1
for
task
in
self
.model.tasks
if
task[
"completed"
])
due_today
=
sum
(
1
for
task
in
self
.model.tasks
if
task[
"due_date"
]
=
=
today)
self
.stats_labels[
"总事项"
].config(text
=
str
(total))
self
.stats_labels[
"已完成"
].config(text
=
str
(completed))
self
.stats_labels[
"未完成"
].config(text
=
str
(total
-
completed))
self
.stats_labels[
"今日截止"
].config(text
=
str
(due_today))
def
export_tasks(
self
):
filename
=
f
"事项清单_{datetime.now().strftime('%Y%m%d_%H%M')}.txt"
with
open
(filename,
"w"
, encoding
=
"utf-8"
) as f:
f.write(
"=== 待办事项清单 ===\n\n"
)
for
task
in
self
.model.tasks:
status
=
"✓"
if
task[
"completed"
]
else
"⬜"
f.write(f
"{status} {task['task']}\n"
)
f.write(f
" 优先级: {task['priority']}\n"
)
f.write(f
" 分类: {task['category']}\n"
)
f.write(f
" 创建时间: {task['created_at']}\n"
)
f.write(f
" 截止日期: {task['due_date']}\n"
)
f.write(
"\n"
)
messagebox.showinfo(
"成功"
, f
"事项已导出到 {filename}"
)
def
show_task_details(
self
, event
=
None
):
selected_items
=
self
.task_tree.selection()
if
not
selected_items:
self
.mark_button.config(text
=
"✓ 标记完成"
)
for
label
in
self
.detail_labels.values():
label.config(text
=
"")
self
.selected_task
=
None
return
values
=
self
.task_tree.item(selected_items[
0
],
'values'
)
if
values:
task_data
=
json.loads(values[
-
1
])
self
.selected_task
=
task_data
button_text
=
"✓ 标记未完成"
if
self
.selected_task[
"completed"
]
else
"✓ 标记完成"
self
.mark_button.config(text
=
button_text)
task_content
=
self
.selected_task[
"task"
]
line_count
=
len
(task_content.split(
'\n'
))
content_height
=
max
(
3
,
min
(
10
, line_count))
if
not
hasattr
(
self
,
'task_content_text'
):
self
.task_content_text
=
tk.Text(
self
.detail_frame,
wrap
=
tk.WORD,
width
=
40
,
height
=
content_height,
font
=
(
'Microsoft YaHei UI'
,
10
),
background
=
AppConfig.COLORS[
"light_bg"
],
foreground
=
AppConfig.COLORS[
"fg"
]
)
self
.task_content_text.grid(row
=
0
, column
=
1
, sticky
=
"nsew"
, pady
=
2
)
task_scrollbar
=
ttk.Scrollbar(
self
.detail_frame, orient
=
"vertical"
, command
=
self
.task_content_text.yview)
task_scrollbar.grid(row
=
0
, column
=
2
, sticky
=
"ns"
)
self
.task_content_text.configure(yscrollcommand
=
task_scrollbar.
set
)
else
:
self
.task_content_text.configure(height
=
content_height)
self
.task_content_text.config(state
=
'normal'
)
self
.task_content_text.delete(
'1.0'
, tk.END)
self
.task_content_text.insert(
'1.0'
,
self
.selected_task[
"task"
])
self
.task_content_text.config(state
=
'disabled'
)
self
.detail_labels[
"创建时间"
].config(text
=
self
.selected_task[
"created_at"
])
self
.detail_labels[
"截止日期"
].config(text
=
self
.selected_task[
"due_date"
])
self
.detail_labels[
"优先级"
].config(text
=
self
.selected_task[
"priority"
])
self
.detail_labels[
"分类"
].config(text
=
self
.selected_task[
"category"
])
self
.detail_labels[
"状态"
].config(
text
=
"已完成"
if
self
.selected_task[
"completed"
]
else
"未完成"
)
attachments
=
self
.selected_task.get(
"attachments"
, [])
links
=
self
.selected_task.get(
"links"
, [])
if
"attachment_frame"
in
self
.__dict__:
self
.attachment_frame.destroy()
self
.attachment_frame
=
ttk.Frame(
self
.detail_frame)
self
.attachment_frame.grid(row
=
6
, column
=
1
, sticky
=
"w"
, pady
=
2
)
if
attachments:
for
i, attachment
in
enumerate
(attachments):
btn
=
ttk.Button(
self
.attachment_frame,
text
=
f
"📎 {attachment['name']}"
,
command
=
lambda
a
=
attachment[
'path'
]:
self
.open_attachment(a),
style
=
"Link.TButton"
)
btn.grid(row
=
0
, column
=
i, padx
=
(
0
,
5
))
else
:
ttk.Label(
self
.attachment_frame, text
=
"无"
).grid(row
=
0
, column
=
0
)
if
"link_frame"
in
self
.__dict__:
self
.link_frame.destroy()
self
.link_frame
=
ttk.Frame(
self
.detail_frame)
self
.link_frame.grid(row
=
7
, column
=
1
, sticky
=
"w"
, pady
=
2
)
if
links:
for
i, link
in
enumerate
(links):
btn
=
ttk.Button(
self
.link_frame,
text
=
f
"🔗 {link['title']}"
,
command
=
lambda
l
=
link:
self
.open_link(l[
'url'
]),
style
=
"Link.TButton"
)
btn.grid(row
=
0
, column
=
i, padx
=
(
0
,
5
))
else
:
ttk.Label(
self
.link_frame, text
=
"无"
).grid(row
=
0
, column
=
0
)
def
show_help(
self
):
help_text
=
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"使用说明"
)
dialog.geometry(
"480x600"
)
dialog.transient(
self
.root)
dialog.grab_set()
text
=
tk.Text(dialog,
wrap
=
tk.WORD,
padx
=
20
,
pady
=
20
,
font
=
(
'Microsoft YaHei UI'
,
10
),
background
=
AppConfig.COLORS[
"light_bg"
],
foreground
=
AppConfig.COLORS[
"fg"
])
text.pack(fill
=
tk.BOTH, expand
=
True
)
text.insert(
'1.0'
, help_text)
text.config(state
=
'disabled'
)
ttk.Button(dialog,
text
=
"确定"
,
command
=
dialog.destroy,
style
=
"Accent.TButton"
).pack(pady
=
10
)
def
_create_menu(
self
):
menubar
=
tk.Menu(
self
.root)
self
.root.config(menu
=
menubar)
view_menu
=
tk.Menu(menubar, tearoff
=
0
)
view_menu.add_command(label
=
"透明度调节"
, command
=
self
.show_transparency_dialog)
menubar.add_cascade(label
=
"视图"
, menu
=
view_menu)
help_menu
=
tk.Menu(menubar, tearoff
=
0
)
help_menu.add_command(label
=
"使用说明"
, command
=
self
.show_help)
help_menu.add_separator()
help_menu.add_command(label
=
"关于"
, command
=
self
.show_about)
menubar.add_cascade(label
=
"帮助"
, menu
=
help_menu)
def
show_transparency_dialog(
self
):
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"透明度调节"
)
dialog.transient(
self
.root)
dialog.grab_set()
transparency_label
=
ttk.Label(dialog, text
=
"透明度:"
)
transparency_label.grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
5
)
transparency_scale
=
ttk.Scale(
dialog,
from_
=
0.1
,
to
=
1.0
,
value
=
self
.config.get(
"transparency"
,
1.0
),
command
=
self
.set_transparency
)
transparency_scale.grid(row
=
0
, column
=
1
, padx
=
5
, pady
=
5
)
def
set_transparency(
self
, value):
try
:
value
=
float
(value)
self
.root.attributes(
"-alpha"
, value)
self
.config[
"transparency"
]
=
value
except
ValueError:
pass
def
show_about(
self
):
about_text
=
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"关于"
)
dialog.geometry(
"400x450"
)
dialog.transient(
self
.root)
dialog.grab_set()
text
=
tk.Text(dialog,
wrap
=
tk.WORD,
padx
=
20
,
pady
=
20
,
font
=
(
'Microsoft YaHei UI'
,
10
),
background
=
AppConfig.COLORS[
"light_bg"
],
foreground
=
AppConfig.COLORS[
"fg"
])
text.pack(fill
=
tk.BOTH, expand
=
True
)
text.insert(
'1.0'
, about_text)
text.config(state
=
'disabled'
)
ttk.Button(dialog,
text
=
"确定"
,
command
=
dialog.destroy,
style
=
"Accent.TButton"
).pack(pady
=
10
)
def
_show_context_menu(
self
, event):
item
=
self
.task_tree.identify(
'item'
, event.x, event.y)
if
item:
self
.task_tree.selection_set(item)
values
=
self
.task_tree.item(item)[
'values'
]
if
values:
try
:
task_data
=
json.loads(values[
5
])
self
.selected_task
=
task_data
self
.context_menu.entryconfig(
0
,
label
=
"⭕ 取消完成"
if
task_data[
"completed"
]
else
"✓ 标记完成"
)
self
.context_menu.tk_popup(event.x_root, event.y_root)
except
(json.JSONDecodeError, IndexError) as e:
print
(f
"解析事项数据出错: {e}"
)
finally
:
self
.context_menu.grab_release()
def
copy_task_content(
self
):
if
not
self
.selected_task:
messagebox.showinfo(
"提示"
,
"请先选择要复制的事项!"
)
return
self
.root.clipboard_clear()
self
.root.clipboard_append(
self
.selected_task[
"task"
])
messagebox.showinfo(
"提示"
,
"已复制事项内容到剪贴板"
)
def
show_entry_menu(
self
, event):
try
:
has_selection
=
self
.task_entry.selection_present()
try
:
clipboard
=
bool
(
self
.root.clipboard_get())
except
:
clipboard
=
False
self
.entry_menu.entryconfig(
"剪切"
, state
=
"normal"
if
has_selection
else
"disabled"
)
self
.entry_menu.entryconfig(
"复制"
, state
=
"normal"
if
has_selection
else
"disabled"
)
self
.entry_menu.entryconfig(
"粘贴"
, state
=
"normal"
if
clipboard
else
"disabled"
)
self
.entry_menu.post(event.x_root, event.y_root)
except
Exception as e:
print
(f
"显示输入框菜单出错: {e}"
)
def
entry_menu_action(
self
, action):
try
:
if
action
=
=
'cut'
:
self
.root.clipboard_clear()
self
.root.clipboard_append(
self
.task_entry.selection_get())
self
.task_entry.delete(
"sel.first"
,
"sel.last"
)
elif
action
=
=
'copy'
:
self
.root.clipboard_clear()
self
.root.clipboard_append(
self
.task_entry.selection_get())
elif
action
=
=
'paste'
:
try
:
self
.task_entry.delete(
"sel.first"
,
"sel.last"
)
except
:
pass
self
.task_entry.insert(
"insert"
,
self
.root.clipboard_get())
elif
action
=
=
'select_all'
:
self
.task_entry.select_range(
0
, tk.END)
self
.task_entry.icursor(tk.END)
except
Exception as e:
print
(f
"输入框菜单操作出错: {e}"
)
def
toggle_task_status(
self
, event
=
None
, task
=
None
):
if
task:
with
self
.task_lock:
task[
'completed'
]
=
not
task[
'completed'
]
self
.model.save_tasks()
self
.selected_task
=
task
self
.update_task_list()
self
.show_task_details()
def
add_attachment(
self
):
attachment_dir
=
FileManager.get_resource_path(AppConfig.ATTACHMENT_DIR)
os.makedirs(attachment_dir, exist_ok
=
True
)
file_path
=
filedialog.askopenfilename(
title
=
"选择附件"
,
filetypes
=
AppConfig.SUPPORTED_ATTACHMENTS
)
if
file_path:
try
:
file_name
=
os.path.basename(file_path)
timestamp
=
datetime.now().strftime(
"%Y%m%d_%H%M%S"
)
new_file_name
=
f
"{timestamp}_{file_name}"
new_file_path
=
os.path.join(attachment_dir, new_file_name)
import
shutil
shutil.copy2(file_path, new_file_path)
self
.current_attachments.append({
"name"
: file_name,
"path"
: new_file_name
})
self
._update_attachment_preview()
except
Exception as e:
messagebox.showerror(
"错误"
, f
"添加附件失败:{str(e)}"
)
def
add_link(
self
):
dialog
=
tk.Toplevel(
self
.root)
dialog.title(
"添加链接"
)
dialog.transient(
self
.root)
dialog.grab_set()
ttk.Label(dialog, text
=
"链接标题:"
).grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
5
)
title_var
=
tk.StringVar()
title_entry
=
ttk.Entry(dialog, textvariable
=
title_var)
title_entry.grid(row
=
0
, column
=
1
, padx
=
5
, pady
=
5
)
ttk.Label(dialog, text
=
"链接地址:"
).grid(row
=
1
, column
=
0
, padx
=
5
, pady
=
5
)
url_var
=
tk.StringVar()
url_entry
=
ttk.Entry(dialog, textvariable
=
url_var)
url_entry.grid(row
=
1
, column
=
1
, padx
=
5
, pady
=
5
)
def
save_link():
title
=
title_var.get().strip()
url
=
url_var.get().strip()
if
title
and
url:
self
.current_links.append({
"title"
: title,
"url"
: url})
self
._update_link_preview()
dialog.destroy()
else
:
messagebox.showwarning(
"警告"
,
"请输入链接标题和地址!"
)
ttk.Button(dialog, text
=
"确定"
, command
=
save_link).grid(
row
=
2
, column
=
0
, columnspan
=
2
, pady
=
10
)
def
_update_attachment_preview(
self
):
if
hasattr
(
self
,
'attachment_preview_frame'
):
self
.attachment_preview_frame.destroy()
self
.attachment_preview_frame
=
ttk.Frame(
self
.main_frame)
self
.attachment_preview_frame.grid(row
=
3
, column
=
0
, sticky
=
"w"
, pady
=
(
5
,
0
))
if
self
.current_attachments:
ttk.Label(
self
.attachment_preview_frame,
text
=
"📎 已添加附件:"
,
font
=
(
'Microsoft YaHei UI'
,
9
,
'bold'
)).grid(
row
=
0
, column
=
0
, sticky
=
"w"
, pady
=
(
0
,
5
))
for
i, filename
in
enumerate
(
self
.current_attachments):
frame
=
ttk.Frame(
self
.attachment_preview_frame)
frame.grid(row
=
i
+
1
, column
=
0
, sticky
=
"w"
, pady
=
2
)
ttk.Button(frame,
text
=
filename,
style
=
"Link.TButton"
,
command
=
lambda
f
=
filename:
self
.open_attachment(f)).grid(
row
=
0
, column
=
0
, padx
=
(
20
,
5
))
ttk.Button(frame,
text
=
"❌"
,
width
=
3
,
style
=
"Link.TButton"
,
command
=
lambda
f
=
filename:
self
._remove_attachment(f)).grid(
row
=
0
, column
=
1
)
def
_update_link_preview(
self
):
if
hasattr
(
self
,
'link_preview_frame'
):
self
.link_preview_frame.destroy()
self
.link_preview_frame
=
ttk.Frame(
self
.main_frame)
self
.link_preview_frame.grid(row
=
4
, column
=
0
, sticky
=
"w"
, pady
=
(
5
,
0
))
if
self
.current_links:
ttk.Label(
self
.link_preview_frame,
text
=
"🔗 已添加链接:"
,
font
=
(
'Microsoft YaHei UI'
,
9
,
'bold'
)).grid(
row
=
0
, column
=
0
, sticky
=
"w"
, pady
=
(
0
,
5
))
for
i, link
in
enumerate
(
self
.current_links):
frame
=
ttk.Frame(
self
.link_preview_frame)
frame.grid(row
=
i
+
1
, column
=
0
, sticky
=
"w"
, pady
=
2
)
ttk.Button(frame,
text
=
f
"{link['title']}"
,
style
=
"Link.TButton"
,
command
=
lambda
l
=
link:
self
.open_link(l[
'url'
])).grid(
row
=
0
, column
=
0
, padx
=
(
20
,
5
))
ttk.Label(frame,
text
=
f
"({link['url']})"
,
font
=
(
'Microsoft YaHei UI'
,
8
),
foreground
=
AppConfig.COLORS[
"fg"
]).grid(
row
=
0
, column
=
1
, padx
=
(
0
,
5
))
ttk.Button(frame,
text
=
"❌"
,
width
=
3
,
style
=
"Link.TButton"
,
command
=
lambda
l
=
link:
self
._remove_link(l)).grid(
row
=
0
, column
=
2
)
def
_remove_attachment(
self
, filename):
self
.current_attachments.remove(filename)
self
._update_attachment_preview()
def
_remove_link(
self
, link):
self
.current_links.remove(link)
self
._update_link_preview()
def
open_attachment(
self
, attachment):
import
os
import
platform
import
subprocess
try
:
if
isinstance
(attachment,
dict
):
filename
=
attachment[
'path'
]
else
:
filename
=
attachment
attachment_path
=
os.path.join(
FileManager.get_resource_path(AppConfig.ATTACHMENT_DIR),
filename
)
if
not
os.path.exists(attachment_path):
messagebox.showerror(
"错误"
, f
"附件文件 {filename} 不存在!"
)
return
if
platform.system()
=
=
'Windows'
:
os.startfile(attachment_path)
elif
platform.system()
=
=
'Darwin'
:
subprocess.run([
'open'
, attachment_path])
else
:
subprocess.run([
'xdg-open'
, attachment_path])
except
Exception as e:
messagebox.showerror(
"错误"
, f
"打开附件失败: {e}"
)
def
open_link(
self
, url):
import
webbrowser
try
:
webbrowser.
open
(url)
except
Exception as e:
messagebox.showerror(
"错误"
, f
"打开链接失败: {e}"
)
def
_on_tree_select(
self
, event):
selection
=
self
.task_tree.selection()
if
selection:
try
:
item_id
=
selection[
0
]
values
=
self
.task_tree.item(item_id)[
'values'
]
if
values
and
len
(values) >
=
6
:
task_data
=
json.loads(values[
5
])
self
.selected_task
=
task_data
self
.show_task_details()
self
.task_tree.focus_set()
else
:
self
.selected_task
=
None
except
(json.JSONDecodeError, IndexError):
self
.selected_task
=
None
else
:
self
.selected_task
=
None
def
_on_up_key(
self
, event):
selection
=
self
.task_tree.selection()
if
selection:
current_index
=
self
.task_tree.index(selection[
0
])
if
current_index >
0
:
prev_item
=
self
.task_tree.get_children()[current_index
-
1
]
self
.task_tree.selection_set(prev_item)
self
.task_tree.see(prev_item)
elif
self
.task_tree.get_children():
last_item
=
self
.task_tree.get_children()[
-
1
]
self
.task_tree.selection_set(last_item)
self
.task_tree.see(last_item)
def
_on_down_key(
self
, event):
selection
=
self
.task_tree.selection()
if
selection:
current_index
=
self
.task_tree.index(selection[
0
])
children
=
self
.task_tree.get_children()
if
current_index <
len
(children)
-
1
:
next_item
=
children[current_index
+
1
]
self
.task_tree.selection_set(next_item)
self
.task_tree.see(next_item)
elif
self
.task_tree.get_children():
first_item
=
self
.task_tree.get_children()[
0
]
self
.task_tree.selection_set(first_item)
self
.task_tree.see(first_item)
def
_on_tree_double_click(
self
, event):
item
=
self
.task_tree.identify(
'item'
, event.x, event.y)
if
item:
self
.task_tree.selection_set(item)
values
=
self
.task_tree.item(item,
'values'
)
if
values
and
len
(values) >
0
:
try
:
task_data
=
json.loads(values[
-
1
])
for
task
in
self
.model.tasks:
if
task
=
=
task_data:
self
.toggle_task_status(
None
, task)
break
except
(json.JSONDecodeError, IndexError):
pass
def
_on_tree_space(
self
, event
=
None
):
selected_items
=
self
.task_tree.selection()
if
not
selected_items:
return
try
:
item_id
=
selected_items[
0
]
values
=
self
.task_tree.item(item_id,
'values'
)
if
not
values:
return
task_data
=
json.loads(values[
-
1
])
for
task
in
self
.model.tasks:
if
task
=
=
task_data:
self
.toggle_task_status(
None
, task)
break
except
(json.JSONDecodeError, IndexError, ValueError) as e:
print
(f
"处理事项状态切换时出错: {e}"
)
pass
def
_sort_by_column(
self
, column):
tasks
=
[]
for
item
in
self
.task_tree.get_children():
values
=
self
.task_tree.item(item,
'values'
)
task_data
=
json.loads(values[
-
1
])
tasks.append((values, task_data))
completed_tasks
=
[t
for
t
in
tasks
if
t[
1
][
'completed'
]]
incomplete_tasks
=
[t
for
t
in
tasks
if
not
t[
1
][
'completed'
]]
def
get_sort_key(item):
values, task_data
=
item
if
column
=
=
'status'
:
return
task_data[
'completed'
]
elif
column
=
=
'priority'
:
priority_order
=
{
'高'
:
0
,
'普通'
:
1
,
'低'
:
2
}
return
priority_order[task_data[
'priority'
]]
elif
column
=
=
'task'
:
return
task_data[
'task'
].lower()
elif
column
=
=
'due_date'
:
return
task_data[
'due_date'
]
elif
column
=
=
'category'
:
return
task_data[
'category'
]
return
''
incomplete_tasks.sort(key
=
get_sort_key)
for
item
in
self
.task_tree.get_children():
self
.task_tree.delete(item)
for
values, task_data
in
incomplete_tasks:
tags
=
[
'priority_'
+
task_data[
'priority'
]]
self
.task_tree.insert('
', '
end', values
=
values, tags
=
tags)
for
values, task_data
in
completed_tasks:
self
.task_tree.insert('
', '
end
', values=values, tags=['
completed'])
self
.show_task_details()
def
_on_tree_right_click(
self
, event):
item_id
=
self
.task_tree.identify_row(event.y)
if
item_id:
if
not
self
.task_tree.exists(item_id):
print
(f
"Item {item_id} not found."
)
return
self
.task_tree.selection_set(item_id)
item_index
=
self
.task_tree.index(item_id)
if
0
<
=
item_index <
len
(
self
.model.filtered_indices):
try
:
task
=
self
.model.tasks[
self
.model.filtered_indices[item_index]]
self
._show_context_menu(event, task,
None
)
except
IndexError as e:
print
(f
"Error processing task: {e}"
)
def
_apply_row_style(
self
):
self
.task_tree.tag_configure(
"oddrow"
, background
=
"transparent"
)
self
.task_tree.tag_configure(
"evenrow"
, background
=
"transparent"
)
style
=
ttk.Style()
style.configure(
"Treeview"
, rowheight
=
36
, borderwidth
=
1
, relief
=
"solid"
)
def
select_previous_task(
self
, event
=
None
):
if
not
self
.model.filtered_indices:
return
if
not
self
.selected_task:
last_index
=
self
.model.filtered_indices[
-
1
]
last_task
=
self
.model.tasks[last_index]
children
=
self
.tasks_container.winfo_children()
if
children
and
len
(children) >
0
:
last_frame
=
children[
-
1
]
self
.selected_task
=
last_task
self
.show_task_details()
return
current_index
=
self
.model.tasks.index(
self
.selected_task)
try
:
current_position
=
self
.model.filtered_indices.index(current_index)
if
current_position >
0
:
prev_index
=
self
.model.filtered_indices[current_position
-
1
]
prev_task
=
self
.model.tasks[prev_index]
children
=
self
.tasks_container.winfo_children()
if
children
and
current_position
-
1
>
=
0
and
current_position
-
1
<
len
(children):
prev_frame
=
children[current_position
-
1
]
self
.selected_task
=
prev_task
self
.show_task_details()
except
ValueError:
pass
def
select_next_task(
self
, event
=
None
):
if
not
self
.model.filtered_indices:
return
if
not
self
.selected_task:
first_index
=
self
.model.filtered_indices[
0
]
first_task
=
self
.model.tasks[first_index]
children
=
self
.tasks_container.winfo_children()
if
children
and
len
(children) >
0
:
first_frame
=
children[
0
]
self
.selected_task
=
first_task
self
.show_task_details()
return
current_index
=
self
.model.tasks.index(
self
.selected_task)
try
:
current_position
=
self
.model.filtered_indices.index(current_index)
if
current_position <
len
(
self
.model.filtered_indices)
-
1
:
next_index
=
self
.model.filtered_indices[current_position
+
1
]
next_task
=
self
.model.tasks[next_index]
children
=
self
.tasks_container.winfo_children()
if
children
and
current_position
+
1
<
len
(children):
next_frame
=
children[current_position
+
1
]
self
.selected_task
=
next_task
self
.show_task_details()
except
ValueError:
pass
def
_check_urgent_tasks(
self
):
try
:
for
task
in
self
.model.tasks:
if
not
task[
"completed"
]:
due_date
=
datetime.strptime(task[
"due_date"
],
"%Y-%m-%d"
)
due_date
=
due_date.replace(hour
=
23
, minute
=
59
, second
=
59
)
remaining_time
=
due_date
-
datetime.now()
if
remaining_time.days
=
=
0
and
remaining_time.seconds >
0
:
messagebox.showwarning(
"事项提醒"
,
f
"事项「{task['task']}」将在1天内截止!"
)
except
Exception as e:
print
(f
"检查紧急事项出错: {e}"
,
file
=
sys.stderr)
def
_create_progress_frame(
self
, parent, progress, style):
frame
=
ttk.Frame(parent)
progress_bar
=
ttk.Progressbar(
frame,
style
=
style,
length
=
100
,
mode
=
'determinate'
,
value
=
progress
)
progress_bar.pack(side
=
'left'
, padx
=
(
0
,
5
), fill
=
'x'
, expand
=
True
)
progress_label
=
ttk.Label(
frame,
text
=
f
"{int(progress)}%"
,
style
=
"Progress.TLabel"
)
progress_label.pack(side
=
'right'
)
return
frame
def
main():
root
=
tk.Tk()
app
=
TodoApp(root)
root.mainloop()
if
__name__
=
=
"__main__"
:
main()