|
|
|
@ -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.4" |
|
|
|
VERSION = "v1.0.5" |
|
|
|
|
|
|
|
DB_PATH = "tasks.db" # 数据库文件路径 |
|
|
|
TEMPLATES_PATH = "templates.json" # 检索模板文件路径 |
|
|
|
@ -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,14 +74,14 @@ 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}, |
|
|
|
"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.05 |
|
|
|
@ -679,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() |
|
|
|
@ -753,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="+") |
|
|
|
|
|
|
|
@ -978,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") |
|
|
|
@ -1107,6 +1122,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: |
|
|
|
@ -1499,6 +1532,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() |
|
|
|
|