Compare commits

...

6 Commits

  1. 111
      README.md
  2. 189
      app.py

111
README.md

@ -11,125 +11,46 @@
pyinstaller --onefile --windowed --name TaskManager app.py pyinstaller --onefile --windowed --name TaskManager app.py
``` ```
当前python环境的conda listL ```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 # Name Version Build Channel
altgraph 0.17.5 pypi_0 pypi
anaconda-anon-usage 0.7.4 pyhb46e38b_100
anaconda-auth 0.10.0 py313haa95532_1
anaconda-cli-base 0.6.0 py313haa95532_0
anaconda_powershell_prompt 1.1.0 haa95532_1
anaconda_prompt 1.1.0 haa95532_1
annotated-types 0.6.0 py313haa95532_1
archspec 0.2.5 pyhd3eb1b0_0
babel 2.17.0 pypi_0 pypi babel 2.17.0 pypi_0 pypi
boltons 25.0.0 py313haa95532_0
brotlicffi 1.0.9.2 py313h885b0b7_2
bzip2 1.0.8 h2bbff1b_6 bzip2 1.0.8 h2bbff1b_6
ca-certificates 2025.11.4 haa95532_0 ca-certificates 2025.12.2 haa95532_0
certifi 2025.10.5 py313haa95532_0
cffi 2.0.0 py313h02ab6af_1
charset-normalizer 3.4.4 py313haa95532_0
click 8.1.8 py313haa95532_0
colorama 0.4.6 py313haa95532_0
conda 25.9.1 py313haa95532_0
conda-anaconda-telemetry 0.3.0 pyhd3eb1b0_1
conda-anaconda-tos 0.2.2 py313haa95532_1
conda-content-trust 0.2.0 py313haa95532_1
conda-libmamba-solver 25.4.0 pyhdf14ebd_1
conda-package-handling 2.4.0 py313haa95532_1
conda-package-streaming 0.12.0 py313haa95532_1
cpp-expected 1.1.0 h214f63a_0
cryptography 46.0.3 py313habbc9f9_0
customtkinter 5.2.2 pypi_0 pypi customtkinter 5.2.2 pypi_0 pypi
darkdetect 0.8.0 pypi_0 pypi darkdetect 0.8.0 pypi_0 pypi
distro 1.9.0 py313haa95532_0 expat 2.7.3 h885b0b7_4
expat 2.7.3 h9214b88_0 libexpat 2.7.3 h885b0b7_4
fmt 11.2.0 h58b7f6e_0
frozendict 2.4.6 py313h02ab6af_0
idna 3.11 py313haa95532_0
jaraco.classes 3.4.0 py313haa95532_0
jaraco.context 6.0.0 py313haa95532_0
jaraco.functools 4.1.0 py313haa95532_0
jsonpatch 1.33 py313haa95532_1
jsonpointer 3.0.0 py313haa95532_0
keyring 25.6.0 py313haa95532_0
libarchive 3.8.2 h6c023e8_0
libcurl 8.16.0 h97e0424_0
libffi 3.4.4 hd77b12b_1 libffi 3.4.4 hd77b12b_1
libiconv 1.16 h2bbff1b_3
libmamba 2.3.2 h7d9f7df_0
libmambapy 2.3.2 py313h5078c03_0
libmpdec 4.0.0 h827c3e9_0 libmpdec 4.0.0 h827c3e9_0
libsolv 0.7.30 h23a355e_2
libssh2 1.11.1 h2addb87_0
libxml2 2.13.9 h6201b9f_0
libzlib 1.3.1 h02ab6af_0 libzlib 1.3.1 h02ab6af_0
lz4-c 1.9.4 h2bbff1b_1
markdown-it-py 4.0.0 py313haa95532_0
mdurl 0.1.2 py313haa95532_0
menuinst 2.4.1 py313h885b0b7_1
more-itertools 10.8.0 py313haa95532_0
nlohmann_json 3.11.2 h6c2663c_0
openssl 3.0.18 h543e019_0 openssl 3.0.18 h543e019_0
packaging 25.0 py313haa95532_1 packaging 25.0 pypi_0 pypi
pcre2 10.46 h5740b90_0 pip 25.3 pyhc872135_0
pefile 2024.8.26 pypi_0 pypi
pillow 12.0.0 pypi_0 pypi
pip 25.2 pyhc872135_1
pkce 1.0.3 py313haa95532_0
platformdirs 4.5.0 py313haa95532_0
pluggy 1.5.0 py313haa95532_0
pybind11-abi 5 hd3eb1b0_0
pycosat 0.6.6 py313h827c3e9_2
pycparser 2.23 py313haa95532_0
pydantic 2.12.3 py313haa95532_1
pydantic-core 2.41.4 py313h114bc41_0
pydantic-settings 2.10.1 py313haa95532_0
pygments 2.19.2 py313haa95532_0
pyinstaller 6.17.0 pypi_0 pypi
pyinstaller-hooks-contrib 2025.10 pypi_0 pypi
pyjwt 2.10.1 py313haa95532_0
pysocks 1.7.1 py313haa95532_1
python 3.13.9 h260b955_100_cp313 python 3.13.9 h260b955_100_cp313
python-dotenv 1.1.0 py313haa95532_0 python_abi 3.13 3_cp313
python_abi 3.13 1_cp313
pywin32-ctypes 0.2.2 py313haa95532_0
readchar 4.2.1 py313haa95532_0
reproc 14.2.4 hd77b12b_2
reproc-cpp 14.2.4 hd77b12b_2
requests 2.32.5 py313haa95532_1
rich 14.2.0 py313haa95532_0
ruamel.yaml 0.18.16 py313hb9a58be_0
ruamel.yaml.clib 0.2.14 py313hb9a58be_0
semver 3.0.4 py313haa95532_0
setuptools 80.9.0 py313haa95532_0 setuptools 80.9.0 py313haa95532_0
shellingham 1.5.4 py313haa95532_0
simdjson 3.10.1 h214f63a_0
sqlite 3.51.0 hda9a48d_0 sqlite 3.51.0 hda9a48d_0
tk 8.6.15 hf199647_0 tk 8.6.15 hf199647_0
tkcalendar 1.6.1 pypi_0 pypi tkcalendar 1.6.1 pypi_0 pypi
tksheet 7.5.19 pypi_0 pypi tksheet 7.5.19 pypi_0 pypi
tomli 2.2.1 py313haa95532_0
tqdm 4.67.1 py313h4442805_1
truststore 0.10.1 py313haa95532_1
typer 0.17.4 py313haa95532_0
typing-extensions 4.15.0 py313haa95532_0
typing-inspection 0.4.2 py313haa95532_0
typing_extensions 4.15.0 py313haa95532_0
tzdata 2025b h04d1e81_0 tzdata 2025b h04d1e81_0
ucrt 10.0.22621.0 haa95532_0 ucrt 10.0.22621.0 haa95532_0
urllib3 2.5.0 py313haa95532_0
vc 14.3 h2df5915_10 vc 14.3 h2df5915_10
vc14_runtime 14.44.35208 h4927774_10 vc14_runtime 14.44.35208 h4927774_10
vs2015_runtime 14.44.35208 ha6b5a95_10 vs2015_runtime 14.44.35208 ha6b5a95_10
wheel 0.45.1 py313haa95532_0 wheel 0.45.1 py313haa95532_0
win_inet_pton 1.1.0 py313haa95532_1
xz 5.6.4 h4754444_1 xz 5.6.4 h4754444_1
yaml-cpp 0.8.0 hd77b12b_1
zlib 1.3.1 h02ab6af_0 zlib 1.3.1 h02ab6af_0
zstandard 0.24.0 py313he335c29_0
zstd 1.5.7 h56299aa_0
``` ```
**主要功能:** **主要功能:**

189
app.py

@ -10,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.1" VERSION = "v1.0.6"
DB_PATH = "tasks.db" # 数据库文件路径 DB_PATH = "tasks.db" # 数据库文件路径
TEMPLATES_PATH = "templates.json" # 检索模板文件路径 TEMPLATES_PATH = "templates.json" # 检索模板文件路径
@ -30,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)
@ -48,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"},
@ -72,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) 这里的顺序即列显示顺序
@ -128,8 +130,11 @@ DEADLINE_COLOR = [
(4, "#73bc75"), (4, "#73bc75"),
(10, "#83d6ff"), (10, "#83d6ff"),
(15, "#bdd8e9"), (15, "#bdd8e9"),
(30, "#ffffff"),
] ]
# 保持跟进、搁置超出日期时的颜色
PAUSE_DEADLINE_COLOR = "#fbffa9"
# 默认颜色
DEFAULT_DEADLINE_COLOR = "#ffffff"
# endregion # endregion
@ -407,11 +412,17 @@ def compute_composite_score(task_row: dict) -> float:
except Exception: except Exception:
have_deadline = False have_deadline = False
# 各种因素分数 # 各种因素分数
ddl_score = 1 + math.exp(-(0.3 + days_to_deadline / 3.0)) if have_deadline else 0.8 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 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 * ddl_score score = p_score * t_score * s_score * d_score * ddl_score
return round(float(score), 6) return round(float(score), 6)
@ -670,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()
@ -744,23 +759,28 @@ 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="+")
@ -936,23 +956,23 @@ 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: if ddl and st not in ["已完成", "取消"]:
try: try:
dt_deadline = datetime.fromisoformat(ddl) dt_deadline = datetime.fromisoformat(ddl)
days_to_deadline = (dt_deadline.date() - datetime.now(dt_deadline.tzinfo or timezone.utc).date()).days 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: for (d, c) in DEADLINE_COLOR:
if days_to_deadline <= d: if days_to_deadline <= d:
bg = c bg = c if st not in ["保持跟进", "搁置"] else PAUSE_DEADLINE_COLOR
break
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg) try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception: except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg) try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
except Exception: pass except Exception: pass
break
except Exception: except Exception:
pass pass
else: else:
# 取最后一个颜色 bg = DEFAULT_DEADLINE_COLOR
bg = DEADLINE_COLOR[-1][1]
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg) try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception: except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg) try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
@ -969,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")
@ -1035,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()
@ -1098,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:
@ -1490,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