Compare commits

...

7 Commits

  1. 42
      README.md
  2. 295
      app.py

42
README.md

@ -11,6 +11,48 @@
pyinstaller --onefile --windowed --name TaskManager app.py pyinstaller --onefile --windowed --name TaskManager app.py
``` ```
```bash
# 创建一个新环境
conda create -n taskMgr python=3.13.9
conda activate taskMgr
# -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install tkcalendar==1.6.1 tksheet==7.5.19 customtkinter==5.2.2
```
当前python环境的conda list
```
# Name Version Build Channel
babel 2.17.0 pypi_0 pypi
bzip2 1.0.8 h2bbff1b_6
ca-certificates 2025.12.2 haa95532_0
customtkinter 5.2.2 pypi_0 pypi
darkdetect 0.8.0 pypi_0 pypi
expat 2.7.3 h885b0b7_4
libexpat 2.7.3 h885b0b7_4
libffi 3.4.4 hd77b12b_1
libmpdec 4.0.0 h827c3e9_0
libzlib 1.3.1 h02ab6af_0
openssl 3.0.18 h543e019_0
packaging 25.0 pypi_0 pypi
pip 25.3 pyhc872135_0
python 3.13.9 h260b955_100_cp313
python_abi 3.13 3_cp313
setuptools 80.9.0 py313haa95532_0
sqlite 3.51.0 hda9a48d_0
tk 8.6.15 hf199647_0
tkcalendar 1.6.1 pypi_0 pypi
tksheet 7.5.19 pypi_0 pypi
tzdata 2025b h04d1e81_0
ucrt 10.0.22621.0 haa95532_0
vc 14.3 h2df5915_10
vc14_runtime 14.44.35208 h4927774_10
vs2015_runtime 14.44.35208 ha6b5a95_10
wheel 0.45.1 py313haa95532_0
xz 5.6.4 h4754444_1
zlib 1.3.1 h02ab6af_0
```
**主要功能:** **主要功能:**
1. 添加任务(可设置 类型、优先级、状态、链接、备注等信息) 1. 添加任务(可设置 类型、优先级、状态、链接、备注等信息)
2. 可筛选、排序,并且可以保存为模板快速应用 2. 可筛选、排序,并且可以保存为模板快速应用

295
app.py

@ -1,5 +1,6 @@
import os import os
import json import json
import math
import sqlite3 import sqlite3
import webbrowser import webbrowser
from datetime import datetime, date, timezone from datetime import datetime, date, timezone
@ -9,10 +10,11 @@ from tkinter import messagebox, simpledialog
import customtkinter as ctk import customtkinter as ctk
from tksheet import Sheet from tksheet import Sheet
from tkcalendar import Calendar from tkcalendar import Calendar
import tkinter.font as tkfont
# ----------------------- CONFIG ----------------------- # ----------------------- CONFIG -----------------------
# region 配置 # region 配置
VERSION = "v1.0.0" VERSION = "v1.0.6"
DB_PATH = "tasks.db" # 数据库文件路径 DB_PATH = "tasks.db" # 数据库文件路径
TEMPLATES_PATH = "templates.json" # 检索模板文件路径 TEMPLATES_PATH = "templates.json" # 检索模板文件路径
@ -29,7 +31,7 @@ CTK_COLOR_THEME = "blue"
# Options # Options
PRIORITY_OPTIONS = ["SSS", "SS", "S", "A", "B", "C", "D", "E"] PRIORITY_OPTIONS = ["SSS", "SS", "S", "A", "B", "C", "D", "E"]
TYPE_OPTIONS = ["Bug", "需求", "其他"] TYPE_OPTIONS = ["Bug", "需求", "其他", "小问题"]
STATUS_OPTIONS = ["待处理", "进行中", "已完成", "保持跟进", "搁置", "取消"] STATUS_OPTIONS = ["待处理", "进行中", "已完成", "保持跟进", "搁置", "取消"]
# Display sets (icon + background) # Display sets (icon + background)
@ -47,6 +49,7 @@ TYPE_DISPLAY = {
"Bug": {"icon": "🐞", "bg": "#ed7e7e"}, "Bug": {"icon": "🐞", "bg": "#ed7e7e"},
"需求": {"icon": "", "bg": "#a0e9c4"}, "需求": {"icon": "", "bg": "#a0e9c4"},
"其他": {"icon": "📝", "bg": "#c1d9fe"}, "其他": {"icon": "📝", "bg": "#c1d9fe"},
"小问题": {"icon": "🔔", "bg": "#f6a746"},
} }
STATUS_DISPLAY = { STATUS_DISPLAY = {
"待处理": {"icon": "", "bg": "#f7e086"}, "待处理": {"icon": "", "bg": "#f7e086"},
@ -71,17 +74,17 @@ PROCESSED_DISPLAY = {
# SORT ORDER (Rule A) - used for table-column sorting (integer ranks) # SORT ORDER (Rule A) - used for table-column sorting (integer ranks)
SORT_ORDER = { SORT_ORDER = {
"priority": {"SSS": 8, "SS": 7, "S": 6, "A": 5, "B": 4, "C": 3, "D": 2, "E": 1}, "priority": {"SSS": 8, "SS": 7, "S": 6, "A": 5, "B": 4, "C": 3, "D": 2, "E": 1},
"type": {"Bug": 3, "需求": 2, "其他": 1}, "type": {"Bug": 3, "需求": 2, "其他": 1, "小问题" : 1},
"status": {"待处理": 6, "进行中": 5, "保持跟进": 4, "搁置": 3, "取消": 2, "已完成": 1} "status": {"待处理": 6, "进行中": 5, "保持跟进": 4, "搁置": 3, "取消": 2, "已完成": 1}
} }
# COMPOSITE WEIGHTS (Rule B) - used in composite score calculation # COMPOSITE WEIGHTS (Rule B) - used in composite score calculation
COMPOSITE_WEIGHTS = { COMPOSITE_WEIGHTS = {
"priority": {"SSS": 4.0, "SS": 3.0, "S": 2.0, "A": 1.0, "B": 0.8, "C": 0.6, "D": 0.4, "E": 0.2}, "priority": {"SSS": 4.0, "SS": 3.0, "S": 2.0, "A": 1.0, "B": 0.8, "C": 0.6, "D": 0.4, "E": 0.2},
"type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0}, "type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0, "小问题": 1.0},
"status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.1, "搁置": 0.1, "取消": 0.0, "已完成": 0.0}, "status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.15, "搁置": 0.1, "取消": 0.0, "已完成": 0.0},
# age factor multiplier (per day) # age factor multiplier (per day)
"age_factor": 0.1 "age_factor": 0.05
} }
# Columns (for sheet) 这里的顺序即列显示顺序 # Columns (for sheet) 这里的顺序即列显示顺序
@ -91,6 +94,7 @@ COLUMNS = [
("type", "类型"), ("type", "类型"),
("status", "状态"), ("status", "状态"),
("priority", "优先级"), ("priority", "优先级"),
("deadline", "截止日期"),
("composite", "综合优先级"), ("composite", "综合优先级"),
("title", "标题"), ("title", "标题"),
("brief", "简介"), ("brief", "简介"),
@ -115,6 +119,22 @@ COL_START_DATE = COL_INDEX.get("start_date")
COL_LINKS_COUNT = COL_INDEX.get("links_count") COL_LINKS_COUNT = COL_INDEX.get("links_count")
COL_NOTES = COL_INDEX.get("notes") COL_NOTES = COL_INDEX.get("notes")
COL_COMPOSITE = COL_INDEX.get("composite") COL_COMPOSITE = COL_INDEX.get("composite")
COL_DEADLINE = COL_INDEX.get("deadline")
# 截止日期颜色,距离截止日期的时间小于哪个值就是哪个颜色
DEADLINE_COLOR = [
(0, "#fb0000"),
(1, "#ff6f22"),
(2, "#ff904b"),
(3, "#b2ff77"),
(4, "#73bc75"),
(10, "#83d6ff"),
(15, "#bdd8e9"),
]
# 保持跟进、搁置超出日期时的颜色
PAUSE_DEADLINE_COLOR = "#fbffa9"
# 默认颜色
DEFAULT_DEADLINE_COLOR = "#ffffff"
# endregion # endregion
@ -141,6 +161,7 @@ class TaskDB:
type TEXT, type TEXT,
status TEXT, status TEXT,
start_date TEXT, start_date TEXT,
deadline TEXT,
links TEXT, links TEXT,
notes TEXT, notes TEXT,
updated_at TEXT updated_at TEXT
@ -178,6 +199,8 @@ class TaskDB:
adds.append(("status", "TEXT")) adds.append(("status", "TEXT"))
if "start_date" not in cols: if "start_date" not in cols:
adds.append(("start_date", "TEXT")) adds.append(("start_date", "TEXT"))
if "deadline" not in cols:
adds.append(("deadline", "TEXT"))
if "links" not in cols: if "links" not in cols:
adds.append(("links", "TEXT")) adds.append(("links", "TEXT"))
if "notes" not in cols: if "notes" not in cols:
@ -224,8 +247,8 @@ class TaskDB:
now = datetime.now(timezone.utc).isoformat() now = datetime.now(timezone.utc).isoformat()
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute(""" cur.execute("""
INSERT INTO tasks (title, brief, description, priority, type, status, start_date, links, notes, updated_at) INSERT INTO tasks (title, brief, description, priority, type, status, start_date, deadline, links, notes, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", ( """, (
task.get("title", ""), task.get("title", ""),
task.get("brief", ""), task.get("brief", ""),
@ -234,6 +257,7 @@ class TaskDB:
task.get("type"), task.get("type"),
task.get("status"), task.get("status"),
task.get("start_date"), task.get("start_date"),
task.get("deadline"),
json.dumps(task.get("links", []), ensure_ascii=False), json.dumps(task.get("links", []), ensure_ascii=False),
task.get("notes", ""), task.get("notes", ""),
now now
@ -244,7 +268,7 @@ class TaskDB:
def update_task(self, tid: int, task: dict): def update_task(self, tid: int, task: dict):
now = datetime.now(timezone.utc).isoformat() now = datetime.now(timezone.utc).isoformat()
self.conn.execute(""" self.conn.execute("""
UPDATE tasks SET title=?, brief=?, description=?, priority=?, type=?, status=?, start_date=?, links=?, notes=?, updated_at=? UPDATE tasks SET title=?, brief=?, description=?, priority=?, type=?, status=?, start_date=?, deadline=?, links=?, notes=?, updated_at=?
WHERE sid=? WHERE sid=?
""", ( """, (
task.get("title", ""), task.get("title", ""),
@ -254,6 +278,7 @@ class TaskDB:
task.get("type"), task.get("type"),
task.get("status"), task.get("status"),
task.get("start_date"), task.get("start_date"),
task.get("deadline"),
json.dumps(task.get("links", []), ensure_ascii=False), json.dumps(task.get("links", []), ensure_ascii=False),
task.get("notes", ""), task.get("notes", ""),
now, now,
@ -363,24 +388,43 @@ def compute_composite_score(task_row: dict) -> float:
Compute composite priority score using COMPOSITE_WEIGHTS. Compute composite priority score using COMPOSITE_WEIGHTS.
Higher number => more urgent. Higher number => more urgent.
""" """
# 读取数据 # 任务开始以来的天数
start_date = task_row.get("start_date") start_date = task_row.get("start_date")
days = 0 days_from_start = 0
try: try:
if start_date: if start_date:
dt = datetime.fromisoformat(start_date) dt = datetime.fromisoformat(start_date)
# convert to local naive days # convert to local naive days
days = (datetime.now(dt.tzinfo or timezone.utc).date() - dt.date()).days days_from_start = (datetime.now(dt.tzinfo or timezone.utc).date() - dt.date()).days
if days < 0: if days_from_start < 0:
days = 0 days_from_start = 0
except Exception:
days_from_start = 0
# 距离截止日期的时间
deadline = task_row.get("deadline")
have_deadline = False
days_to_deadline = 0
try:
if deadline:
dt_deadline = datetime.fromisoformat(deadline)
days_to_deadline = (dt_deadline.date() - datetime.now(dt_deadline.tzinfo or timezone.utc).date()).days
have_deadline = True
except Exception: except Exception:
days = 0 have_deadline = False
d_score = COMPOSITE_WEIGHTS.get("age_factor", 0.1) * days + 1 # 各种因素分数
ddl_score = 1 + math.exp(-(0.3 + days_to_deadline / 3.0)) if have_deadline else 0.9
d_score = COMPOSITE_WEIGHTS.get("age_factor", 0.1) * days_from_start + 1
p_score = COMPOSITE_WEIGHTS["priority"].get(task_row.get("priority"), 1.0) p_score = COMPOSITE_WEIGHTS["priority"].get(task_row.get("priority"), 1.0)
t_score = COMPOSITE_WEIGHTS["type"].get(task_row.get("type"), 1.0) t_score = COMPOSITE_WEIGHTS["type"].get(task_row.get("type"), 1.0)
s_score = COMPOSITE_WEIGHTS["status"].get(task_row.get("status"), 1.0) s_score = COMPOSITE_WEIGHTS["status"].get(task_row.get("status"), 1.0)
# 跟进和搁置状态不考虑截止日期
state = task_row.get("status")
if state in ["保持跟进", "搁置"]:
ddl_score = 1.0
# 计算综合优先级分数 # 计算综合优先级分数
score = p_score * t_score * s_score * d_score score = p_score * t_score * s_score * d_score * ddl_score
return round(float(score), 6) return round(float(score), 6)
@ -401,6 +445,7 @@ class TaskEditor(ctk.CTkToplevel):
self.title_var = ctk.StringVar() self.title_var = ctk.StringVar()
self.brief_var = ctk.StringVar() self.brief_var = ctk.StringVar()
self.start_var = ctk.StringVar(value=date.today().isoformat()) self.start_var = ctk.StringVar(value=date.today().isoformat())
self.deadline_var = ctk.StringVar(value="")
self.priority_var = ctk.StringVar() self.priority_var = ctk.StringVar()
self.type_var = ctk.StringVar() self.type_var = ctk.StringVar()
self.status_var = ctk.StringVar() self.status_var = ctk.StringVar()
@ -446,33 +491,37 @@ class TaskEditor(ctk.CTkToplevel):
ctk.CTkLabel(frm, text="Start Date (YYYY-MM-DD)").grid(row=8, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0)) ctk.CTkLabel(frm, text="Start Date (YYYY-MM-DD)").grid(row=8, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkEntry(frm, textvariable=self.start_var).grid(row=9, column=0, columnspan=3, sticky="ew", padx=pad) ctk.CTkEntry(frm, textvariable=self.start_var).grid(row=9, column=0, columnspan=3, sticky="ew", padx=pad)
# deadline
ctk.CTkLabel(frm, text="Deadline (YYYY-MM-DD)").grid(row=10, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkEntry(frm, textvariable=self.deadline_var).grid(row=11, column=0, columnspan=3, sticky="ew", padx=pad)
# links # links
ctk.CTkLabel(frm, text="Links (one per line)").grid(row=10, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0)) ctk.CTkLabel(frm, text="Links (one per line)").grid(row=12, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
self.links_text = ctk.CTkTextbox(frm, height=80) self.links_text = ctk.CTkTextbox(frm, height=80)
self.links_text.grid(row=11, column=0, columnspan=3, sticky="nsew", padx=pad) self.links_text.grid(row=13, column=0, columnspan=3, sticky="nsew", padx=pad)
# logs (processed dates) # logs (processed dates)
ctk.CTkLabel(frm, text="Processed Dates (handling records)").grid(row=12, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0)) ctk.CTkLabel(frm, text="Processed Dates (handling records)").grid(row=14, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
logs_frame = ctk.CTkFrame(frm) logs_frame = ctk.CTkFrame(frm)
logs_frame.grid(row=13, column=0, columnspan=3, sticky="ew", padx=pad, pady=(4,0)) logs_frame.grid(row=15, column=0, columnspan=3, sticky="ew", padx=pad, pady=(4,0))
ctk.CTkButton(logs_frame, text="Mark Today", width=120, command=self._mark_today).pack(side="left", padx=6, pady=6) ctk.CTkButton(logs_frame, text="Mark Today", width=120, command=self._mark_today).pack(side="left", padx=6, pady=6)
ctk.CTkButton(logs_frame, text="Add Date...", width=120, command=self._add_date_dialog).pack(side="left", padx=6, pady=6) ctk.CTkButton(logs_frame, text="Add Date...", width=120, command=self._add_date_dialog).pack(side="left", padx=6, pady=6)
ctk.CTkButton(logs_frame, text="Remove Selected", fg_color="#cc4444", hover_color="#aa3333", command=self._remove_selected_date).pack(side="right", padx=6, pady=6) ctk.CTkButton(logs_frame, text="Remove Selected", fg_color="#cc4444", hover_color="#aa3333", command=self._remove_selected_date).pack(side="right", padx=6, pady=6)
# list widget for logs (tk Listbox embedded) # list widget for logs (tk Listbox embedded)
self.logs_listbox = tk.Listbox(frm, height=5) self.logs_listbox = tk.Listbox(frm, height=5)
self.logs_listbox.grid(row=14, column=0, columnspan=3, sticky="nsew", padx=pad, pady=(4,0)) self.logs_listbox.grid(row=16, column=0, columnspan=3, sticky="nsew", padx=pad, pady=(4,0))
# buttons # buttons
btn_row = ctk.CTkFrame(frm) btn_row = ctk.CTkFrame(frm)
btn_row.grid(row=15, column=0, columnspan=3, pady=(12,6)) btn_row.grid(row=17, column=0, columnspan=3, pady=(12,6))
ctk.CTkButton(btn_row, text="Save", command=self._save).pack(side="left", padx=8) ctk.CTkButton(btn_row, text="Save", command=self._save).pack(side="left", padx=8)
ctk.CTkButton(btn_row, text="Cancel", fg_color="#888", hover_color="#666", command=self.destroy).pack(side="left", padx=8) ctk.CTkButton(btn_row, text="Cancel", fg_color="#888", hover_color="#666", command=self.destroy).pack(side="left", padx=8)
# grid weights # grid weights
frm.grid_rowconfigure(5, weight=1) frm.grid_rowconfigure(5, weight=1)
frm.grid_rowconfigure(11, weight=0) frm.grid_rowconfigure(13, weight=0)
frm.grid_rowconfigure(14, weight=0) frm.grid_rowconfigure(16, weight=0)
frm.grid_columnconfigure(0, weight=1) frm.grid_columnconfigure(0, weight=1)
frm.grid_columnconfigure(1, weight=1) frm.grid_columnconfigure(1, weight=1)
frm.grid_columnconfigure(2, weight=1) frm.grid_columnconfigure(2, weight=1)
@ -489,6 +538,7 @@ class TaskEditor(ctk.CTkToplevel):
self.type_cb.set(row.get("type") or "") self.type_cb.set(row.get("type") or "")
self.status_cb.set(row.get("status") or "") self.status_cb.set(row.get("status") or "")
self.start_var.set(row.get("start_date") or "") self.start_var.set(row.get("start_date") or "")
self.deadline_var.set(row.get("deadline") or "")
links = json.loads(row.get("links") or "[]") links = json.loads(row.get("links") or "[]")
self.links_text.delete("1.0", "end"); self.links_text.insert("1.0", "\n".join(links)) self.links_text.delete("1.0", "end"); self.links_text.insert("1.0", "\n".join(links))
# logs # logs
@ -556,6 +606,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": self.type_cb.get() or None, "type": self.type_cb.get() or None,
"status": self.status_cb.get() or None, "status": self.status_cb.get() or None,
"start_date": self.start_var.get().strip() or date.today().isoformat(), "start_date": self.start_var.get().strip() or date.today().isoformat(),
"deadline": self.deadline_var.get().strip() or "",
"links": links, "links": links,
"processed_dates": getattr(self, "_logs", []) "processed_dates": getattr(self, "_logs", [])
} }
@ -575,6 +626,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": data["type"], "type": data["type"],
"status": data["status"], "status": data["status"],
"start_date": data["start_date"], "start_date": data["start_date"],
"deadline": data["deadline"],
"links": data["links"], "links": data["links"],
"notes": data["notes"] "notes": data["notes"]
}) })
@ -595,6 +647,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": data["type"], "type": data["type"],
"status": data["status"], "status": data["status"],
"start_date": data["start_date"], "start_date": data["start_date"],
"deadline": data["deadline"],
"links": data["links"], "links": data["links"],
"notes": data["notes"], "notes": data["notes"],
}) })
@ -628,6 +681,10 @@ class TaskManagerApp(ctk.CTk):
# guard to prevent duplicate toggles from multiple bound handlers # guard to prevent duplicate toggles from multiple bound handlers
self._last_header_toggle = None self._last_header_toggle = None
# 调整单元格宽度所需参数
self._prev_col_widths = {}
self._resized_column = None
# build UI # build UI
self._build_ui() self._build_ui()
self.refresh_table() self.refresh_table()
@ -702,30 +759,35 @@ class TaskManagerApp(ctk.CTk):
self.sheet.grid(row=0, column=0, sticky="nsew") self.sheet.grid(row=0, column=0, sticky="nsew")
table_frame.grid_rowconfigure(0, weight=1); table_frame.grid_columnconfigure(0, weight=1) table_frame.grid_rowconfigure(0, weight=1); table_frame.grid_columnconfigure(0, weight=1)
# bind double click using extra_bindings if available # ===============================
# 各种绑定
try: try:
self.sheet.extra_bindings([("double_click_cell", self._on_double_click_cell)]) self.sheet.extra_bindings([("double_click_cell", self._on_double_click_cell)])
except Exception: except Exception:
self.sheet.bind("<Double-1>", self._on_double_click_generic, add="+") self.sheet.bind("<Double-1>", self._on_double_click_generic, add="+")
# try binding header clicks via extra_bindings if available (try multiple tksheet event names)
try: try:
self.sheet.extra_bindings([("column_select", self._on_header_click)]) self.sheet.extra_bindings([("column_select", self._on_header_click)])
except Exception: except Exception as e:
print('[Error] bind _on_header_click failed!!!') print("[Warn] column_select not supported:", e)
try:
self.sheet.extra_bindings([("column_width_resize", self._on_column_resize)])
except Exception as e:
print("[Warn] column_width_resize not supported:", e)
self.bind_all("<ButtonRelease-1>", self._on_column_resize_end, add="+")
# header/column clicks - generic
# primary binding directly on sheet
self.sheet.bind("<Button-1>", self._on_sheet_click_generic, add="+") self.sheet.bind("<Button-1>", self._on_sheet_click_generic, add="+")
# also listen to ButtonRelease as some versions use release for headers
self.sheet.bind("<ButtonRelease-1>", self._on_sheet_click_generic, add="+") self.sheet.bind("<ButtonRelease-1>", self._on_sheet_click_generic, add="+")
# global binding as fallback to ensure header clicks are caught even if tksheet consumes the event
self.bind_all("<Button-1>", self._on_root_click, add="+") self.bind_all("<Button-1>", self._on_root_click, add="+")
self.bind_all("<ButtonRelease-1>", self._on_root_click, add="+") self.bind_all("<ButtonRelease-1>", self._on_root_click, add="+")
# 指定列居中 # 指定列居中
align_center_cols = [] align_center_cols = []
for i, (key, _) in enumerate(COLUMNS): for i, (key, _) in enumerate(COLUMNS):
if key in ('sid', 'priority', 'type', 'status', 'start_date', 'processed_today', 'last_processed', 'links_count', 'composite'): if key in ('sid', 'priority', 'type', 'status', 'start_date', 'processed_today', 'last_processed', 'links_count', 'composite', 'deadline'):
align_center_cols.append(i) align_center_cols.append(i)
self.sheet.align_columns(columns=align_center_cols, align="center") self.sheet.align_columns(columns=align_center_cols, align="center")
@ -758,6 +820,20 @@ class TaskManagerApp(ctk.CTk):
elif col == "processed_today": elif col == "processed_today":
today_iso = date.today().isoformat() today_iso = date.today().isoformat()
rows.sort(key=lambda r: (today_iso in self.db.get_logs_for_task(r["sid"])), reverse=not self.order_asc) rows.sort(key=lambda r: (today_iso in self.db.get_logs_for_task(r["sid"])), reverse=not self.order_asc)
elif col == "deadline":
# Treat empty/null deadlines as greater than any date so they appear last when sorting ascending
def _deadline_key(r):
d = r.get("deadline")
if not d:
# missing deadline -> mark as 'empty' (1) and None for tie
return (1, None)
try:
dt = datetime.fromisoformat(d)
return (0, dt)
except Exception:
# fallback to string compare (ISO-like strings sort correctly)
return (0, d)
rows.sort(key=_deadline_key, reverse=not self.order_asc)
else: else:
rows.sort(key=lambda r: r.get(col) or "", reverse=not self.order_asc) rows.sort(key=lambda r: r.get(col) or "", reverse=not self.order_asc)
@ -801,6 +877,8 @@ class TaskManagerApp(ctk.CTk):
row.append(r.get("last_processed") or "") row.append(r.get("last_processed") or "")
elif key == "start_date": elif key == "start_date":
row.append(r.get("start_date") or "") row.append(r.get("start_date") or "")
elif key == "deadline":
row.append(r.get("deadline") or "")
elif key == "links_count": elif key == "links_count":
row.append(str(links_count)) row.append(str(links_count))
elif key == "notes": elif key == "notes":
@ -857,6 +935,7 @@ class TaskManagerApp(ctk.CTk):
for r_idx, tid in enumerate(self.displayed_sids): for r_idx, tid in enumerate(self.displayed_sids):
task = self.db.get_task(tid) task = self.db.get_task(tid)
pr = task.get("priority"); ty = task.get("type"); st = task.get("status") pr = task.get("priority"); ty = task.get("type"); st = task.get("status")
ddl = task.get("deadline")
# check if processed today using processed_map # check if processed today using processed_map
processed = processed_map.get(tid, False) processed = processed_map.get(tid, False)
if pr and pr in PRIORITY_DISPLAY: if pr and pr in PRIORITY_DISPLAY:
@ -877,6 +956,27 @@ class TaskManagerApp(ctk.CTk):
except Exception: except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_STATUS,bg) try: self.sheet.set_cell_bg(r_idx,COL_STATUS,bg)
except Exception: pass except Exception: pass
if ddl and st not in ["已完成", "取消"]:
try:
dt_deadline = datetime.fromisoformat(ddl)
days_to_deadline = (dt_deadline.date() - datetime.now(dt_deadline.tzinfo or timezone.utc).date()).days
bg = DEFAULT_DEADLINE_COLOR
for (d, c) in DEADLINE_COLOR:
if days_to_deadline <= d:
bg = c if st not in ["保持跟进", "搁置"] else PAUSE_DEADLINE_COLOR
break
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
except Exception: pass
except Exception:
pass
else:
bg = DEFAULT_DEADLINE_COLOR
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
except Exception: pass
# processed today column: use COL_PROCESSED_TODAY constant # processed today column: use COL_PROCESSED_TODAY constant
if processed: if processed:
bg = PROCESSED_DISPLAY["yes"]["bg"] bg = PROCESSED_DISPLAY["yes"]["bg"]
@ -889,10 +989,14 @@ class TaskManagerApp(ctk.CTk):
# try auto row size # try auto row size
try: try:
# self.sheet.refresh()
self.sheet.set_all_cell_sizes_to_text() self.sheet.set_all_cell_sizes_to_text()
except Exception: except Exception:
pass pass
# reset column width cache
self._init_column_width_cache()
def _clear_filters(self): def _clear_filters(self):
self.search_var.set("") self.search_var.set("")
self.type_listbox.selection_clear(0, "end") self.type_listbox.selection_clear(0, "end")
@ -955,8 +1059,56 @@ class TaskManagerApp(ctk.CTk):
tid = self.displayed_sids[r] tid = self.displayed_sids[r]
row = self.db.get_task(tid) row = self.db.get_task(tid)
links = json.loads(row.get("links") or "[]") links = json.loads(row.get("links") or "[]")
if not links: messagebox.showinfo("No links", "No links found"); return if not links:
for u in links: webbrowser.open(u) messagebox.showinfo("No links", "No links found")
return
# Single link: open immediately (preserve original behaviour)
if len(links) == 1:
try:
webbrowser.open(links[0])
except Exception:
messagebox.showerror("Error", "Failed to open link")
return
# Multiple links: show a small window listing them; user selects one to open
win = ctk.CTkToplevel(self)
win.title("Open Link")
win.geometry("640x320")
# header
try:
ctk.CTkLabel(win, text=f"Links for task {tid}").pack(anchor="w", padx=8, pady=(8,0))
except Exception:
tk.Label(win, text=f"Links for task {tid}").pack(anchor="w", padx=8, pady=(8,0))
lb = tk.Listbox(win, height=10)
lb.pack(fill="both", expand=True, padx=8, pady=8)
for u in links:
lb.insert("end", u)
def _open_selected():
sel = lb.curselection()
if not sel:
messagebox.showinfo("Info", "Select a link")
return
url = lb.get(sel[0])
try:
webbrowser.open(url)
except Exception:
messagebox.showerror("Error", "Failed to open link")
win.destroy()
def _open_all():
for u in links:
try: webbrowser.open(u)
except Exception: pass
win.destroy()
lb.bind("<Double-1>", lambda e: _open_selected())
btn_row = ctk.CTkFrame(win)
btn_row.pack(fill="x", padx=8, pady=8)
ctk.CTkButton(btn_row, text="Open All", command=_open_all).pack(side="left", padx=6)
ctk.CTkButton(btn_row, text="Close", fg_color="#888", hover_color="#666", command=win.destroy).pack(side="right", padx=6)
def mark_selected_processed_today(self): def mark_selected_processed_today(self):
r = self._get_first_selected_row_index() r = self._get_first_selected_row_index()
@ -1018,6 +1170,24 @@ class TaskManagerApp(ctk.CTk):
# swallow - no-op # swallow - no-op
pass pass
def _on_column_resize(self, event):
new_w_list = self.sheet.get_column_widths(0)
for col in range(self.sheet.get_total_columns()):
new_w = new_w_list[col]
old_w = self._prev_col_widths[col]
if new_w != old_w:
self._prev_col_widths[col] = new_w
self._resized_column = col
break
def _on_column_resize_end(self, event):
if self._resized_column is None:
return
col = self._resized_column
self._resized_column = None
self._recalc_column_row_heights(col)
# header click generic for sorting # header click generic for sorting
def _on_sheet_click_generic(self, event): def _on_sheet_click_generic(self, event):
try: try:
@ -1410,6 +1580,59 @@ class TaskManagerApp(ctk.CTk):
ctk.CTkButton(right, text="Open Selected Task", command=open_task).pack(pady=6) ctk.CTkButton(right, text="Open Selected Task", command=open_task).pack(pady=6)
on_select() on_select()
# 调整单元格参数
def _init_column_width_cache(self):
self._prev_col_widths = self.sheet.get_column_widths(0)
def _calc_text_lines(self, text, col_width, tk_font):
if not text:
return 1
lines = 0
for paragraph in str(text).split("\n"):
current_width = 0
for ch in paragraph:
ch_width = tk_font.measure(ch)
if current_width + ch_width > col_width:
lines += 1
current_width = ch_width
else:
current_width += ch_width
lines += 1 # 每个段落至少一行
return max(lines, 1)
def _recalc_column_row_heights(self, col):
font_desc = self.sheet.font()
tk_font = tkfont.Font(font=font_desc)
line_height = tk_font.metrics("linespace")
col_widths = self.sheet.get_column_widths(0)
col_num = self.sheet.get_total_columns()
for row in range(self.sheet.get_total_rows()):
max_height = 0
for col in range(col_num):
text = self.sheet.get_cell_data(row, col)
if not text:
continue
col_width = col_widths[col]
lines = self._calc_text_lines(text, col_width, tk_font)
height = lines * line_height + 6
height = min(max(height, 20), 100000)
if height > max_height:
max_height = height
self.sheet.row_height(row, max_height)
self.sheet.refresh()
# run / refresh # run / refresh
def run(self): def run(self):
self.mainloop() self.mainloop()

Loading…
Cancel
Save