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
```
当前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
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
boltons 25.0.0 py313haa95532_0
brotlicffi 1.0.9.2 py313h885b0b7_2
bzip2 1.0.8 h2bbff1b_6
ca-certificates 2025.11.4 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
ca-certificates 2025.12.2 haa95532_0
customtkinter 5.2.2 pypi_0 pypi
darkdetect 0.8.0 pypi_0 pypi
distro 1.9.0 py313haa95532_0
expat 2.7.3 h9214b88_0
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
expat 2.7.3 h885b0b7_4
libexpat 2.7.3 h885b0b7_4
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
libsolv 0.7.30 h23a355e_2
libssh2 1.11.1 h2addb87_0
libxml2 2.13.9 h6201b9f_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
packaging 25.0 py313haa95532_1
pcre2 10.46 h5740b90_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
packaging 25.0 pypi_0 pypi
pip 25.3 pyhc872135_0
python 3.13.9 h260b955_100_cp313
python-dotenv 1.1.0 py313haa95532_0
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
python_abi 3.13 3_cp313
setuptools 80.9.0 py313haa95532_0
shellingham 1.5.4 py313haa95532_0
simdjson 3.10.1 h214f63a_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
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
ucrt 10.0.22621.0 haa95532_0
urllib3 2.5.0 py313haa95532_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
win_inet_pton 1.1.0 py313haa95532_1
xz 5.6.4 h4754444_1
yaml-cpp 0.8.0 hd77b12b_1
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
from tksheet import Sheet
from tkcalendar import Calendar
import tkinter.font as tkfont
# ----------------------- CONFIG -----------------------
# region 配置
VERSION = "v1.0.1"
VERSION = "v1.0.6"
DB_PATH = "tasks.db" # 数据库文件路径
TEMPLATES_PATH = "templates.json" # 检索模板文件路径
@ -30,7 +31,7 @@ CTK_COLOR_THEME = "blue"
# Options
PRIORITY_OPTIONS = ["SSS", "SS", "S", "A", "B", "C", "D", "E"]
TYPE_OPTIONS = ["Bug", "需求", "其他"]
TYPE_OPTIONS = ["Bug", "需求", "其他", "小问题"]
STATUS_OPTIONS = ["待处理", "进行中", "已完成", "保持跟进", "搁置", "取消"]
# Display sets (icon + background)
@ -48,6 +49,7 @@ TYPE_DISPLAY = {
"Bug": {"icon": "🐞", "bg": "#ed7e7e"},
"需求": {"icon": "", "bg": "#a0e9c4"},
"其他": {"icon": "📝", "bg": "#c1d9fe"},
"小问题": {"icon": "🔔", "bg": "#f6a746"},
}
STATUS_DISPLAY = {
"待处理": {"icon": "", "bg": "#f7e086"},
@ -72,17 +74,17 @@ PROCESSED_DISPLAY = {
# SORT ORDER (Rule A) - used for table-column sorting (integer ranks)
SORT_ORDER = {
"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}
}
# COMPOSITE WEIGHTS (Rule B) - used in composite score calculation
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},
"type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0},
"status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.1, "搁置": 0.1, "取消": 0.0, "已完成": 0.0},
"type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0, "小问题": 1.0},
"status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.15, "搁置": 0.1, "取消": 0.0, "已完成": 0.0},
# age factor multiplier (per day)
"age_factor": 0.1
"age_factor": 0.05
}
# Columns (for sheet) 这里的顺序即列显示顺序
@ -128,8 +130,11 @@ DEADLINE_COLOR = [
(4, "#73bc75"),
(10, "#83d6ff"),
(15, "#bdd8e9"),
(30, "#ffffff"),
]
# 保持跟进、搁置超出日期时的颜色
PAUSE_DEADLINE_COLOR = "#fbffa9"
# 默认颜色
DEFAULT_DEADLINE_COLOR = "#ffffff"
# endregion
@ -407,11 +412,17 @@ def compute_composite_score(task_row: dict) -> float:
except Exception:
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
p_score = COMPOSITE_WEIGHTS["priority"].get(task_row.get("priority"), 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)
# 跟进和搁置状态不考虑截止日期
state = task_row.get("status")
if state in ["保持跟进", "搁置"]:
ddl_score = 1.0
# 计算综合优先级分数
score = p_score * t_score * s_score * d_score * ddl_score
return round(float(score), 6)
@ -670,6 +681,10 @@ class TaskManagerApp(ctk.CTk):
# guard to prevent duplicate toggles from multiple bound handlers
self._last_header_toggle = None
# 调整单元格宽度所需参数
self._prev_col_widths = {}
self._resized_column = None
# build UI
self._build_ui()
self.refresh_table()
@ -744,23 +759,28 @@ class TaskManagerApp(ctk.CTk):
self.sheet.grid(row=0, column=0, sticky="nsew")
table_frame.grid_rowconfigure(0, weight=1); table_frame.grid_columnconfigure(0, weight=1)
# bind double click using extra_bindings if available
# ===============================
# 各种绑定
try:
self.sheet.extra_bindings([("double_click_cell", self._on_double_click_cell)])
except Exception:
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:
self.sheet.extra_bindings([("column_select", self._on_header_click)])
except Exception:
print('[Error] bind _on_header_click failed!!!')
except Exception as e:
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="+")
# also listen to ButtonRelease as some versions use release for headers
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("<ButtonRelease-1>", self._on_root_click, add="+")
@ -936,23 +956,23 @@ class TaskManagerApp(ctk.CTk):
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_STATUS,bg)
except Exception: pass
if ddl:
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
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
break
except Exception:
pass
else:
# 取最后一个颜色
bg = DEADLINE_COLOR[-1][1]
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)
@ -969,10 +989,14 @@ class TaskManagerApp(ctk.CTk):
# try auto row size
try:
# self.sheet.refresh()
self.sheet.set_all_cell_sizes_to_text()
except Exception:
pass
# reset column width cache
self._init_column_width_cache()
def _clear_filters(self):
self.search_var.set("")
self.type_listbox.selection_clear(0, "end")
@ -1035,8 +1059,56 @@ class TaskManagerApp(ctk.CTk):
tid = self.displayed_sids[r]
row = self.db.get_task(tid)
links = json.loads(row.get("links") or "[]")
if not links: messagebox.showinfo("No links", "No links found"); return
for u in links: webbrowser.open(u)
if not links:
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):
r = self._get_first_selected_row_index()
@ -1098,6 +1170,24 @@ class TaskManagerApp(ctk.CTk):
# swallow - no-op
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
def _on_sheet_click_generic(self, event):
try:
@ -1490,6 +1580,59 @@ class TaskManagerApp(ctk.CTk):
ctk.CTkButton(right, text="Open Selected Task", command=open_task).pack(pady=6)
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
def run(self):
self.mainloop()

Loading…
Cancel
Save