什么网站可以做高三英语试题,免费搭建,wordpress创建注册,百度邮箱注册入口一、整体架构全部代码在单文件中#xff0c;2200 余行#xff0c;分为五个层次#xff1a;C:\pythoncode\new\weekly_report_manager.py┌─────────────────────────────────────────────┐ │ MainFrame#x…一、整体架构全部代码在单文件中2200 余行分为五个层次C:\pythoncode\new\weekly_report_manager.py┌─────────────────────────────────────────────┐ │ MainFrame主窗口 │ │ 菜单栏 / SplitterWindow / 状态栏 │ ├───────────────┬─────────────────────────────┤ │ 左TreeCtrl │ 右wx.Notebook │ │ 供应商树 │ 主页 / 统计报表 / 项目详情 │ ├───────────────┴─────────────────────────────┤ │ UI 组件层Dialog Panel │ │ AddVendorDialog / CreateProjectDialog / │ │ ImportFileDialog / DateToWeekDialog / │ │ WeekStatusGrid / ProjectDetailPanel / │ │ StatisticsPanel │ ├─────────────────────────────────────────────┤ │ FileStructureManager数据层 │ │ 文件夹读写 / 状态 JSON 管理 / 统计聚合 │ ├─────────────────────────────────────────────┤ │ 工具函数 配置 │ │ date_to_quarter_week / week_date_range / │ │ Config / 常量定义 │ └─────────────────────────────────────────────┘关键设计选择以文件系统为数据库。没有用 SQLite 或任何数据库每个周次对应一个文件夹文件夹里放周报文件和一个.status.json# 文件夹路径即是主键 运维项目周报/供应商A/项目X/2026年第1季度/第6周/ ├── 周报_2026W6.pdf # 实际文件 └── .status.json # {status: 已收取, note: 2月8日收到, updated: ...}这个选择让「打开文件夹」操作变得零成本数据天然可被文件管理器浏览迁移和备份也只是xcopy一条命令。代价是每次读状态都要做一次文件 IO适合这种数据量不大的场景。二、数据层FileStructureManager这是整个程序的核心类封装了所有文件系统操作。2.1 层次遍历四层结构供应商→项目→季度→周次对应四个get_*方法结构一致def get_vendors(self): for item in sorted(os.listdir(self.base_dir)): if item 归档: # 排除归档文件夹 continue if os.path.isdir(...): vendors.append(item) return vendors每层都跳过归档目录这个约定贯穿整个设计——「归档」是一个特殊名称存在于每一层但永远被遍历跳过。2.2 状态读写的容错逻辑def get_week_status(self, vendor, project, quarter, week): week_path ... if not os.path.exists(week_path): return STATUS_MISSING files [f for f in os.listdir(week_path) if not f.startswith(.)] meta_file os.path.join(week_path, .status.json) if os.path.exists(meta_file): try: with open(meta_file, ...) as f: data json.load(f) return data.get(status, STATUS_MISSING if not files else STATUS_RECEIVED) except: pass return STATUS_RECEIVED if files else STATUS_MISSING # fallback有文件就算已收这里有一个优雅的 fallback 机制即使没有.status.json也能根据文件夹是否有内容来推断状态。这使得程序可以无缝接管已有的手动建好的文件夹不需要初始化操作。2.3 is_past_week只统计「已发生」的周次统计报表只显示当前周及之前的数据核心判断在这里def is_past_week(self, quarter_folder, week): today datetime.date.today() cur_year, cur_quarter, cur_week date_to_quarter_week(today) cur_week_num int(cur_week.replace(第, ).replace(周, )) # ... 解析目标 if tgt_year cur_year: return True if tgt_year cur_year: return False if tgt_q_idx cur_q_idx: return True if tgt_q_idx cur_q_idx: return False return tgt_week_num cur_week_num # 包含当前周注意最后一行是当前周也纳入统计。早期版本用了导致当前周不显示这是一个很典型的 off-by-one 问题。2.4 get_all_past_details全局数据汇总def get_all_past_details(self): details [] for v in self.get_vendors(): for p in self.get_projects(v): for q in self.get_quarters(v, p): for w in self.get_weeks(v, p, q): if self.is_past_week(q, w): status self.get_week_status(v, p, q, w) details.append({vendor: v, project: p, quarter: q, week: w, status: status}) return details四层嵌套循环遍历整个树过滤掉未来周次返回扁平化的明细列表。统计面板的所有 Tab 都基于这个方法的返回值做二次聚合不重复读文件。三、日期计算周四归属法这是整个项目中逻辑密度最高的部分值得单独分析。3.1 问题描述需要将任意日期映射到「哪年哪季度第几周」。直觉上很简单但有一个边界情况2026年Q11月1日是周四。按自然周周一到周日算这一周的周一是2025年12月29日。如果直接用日期的月份判断季度12月29日会被算成Q4导致整个周次编号错误。3.2 解决方案周四归属法def date_to_quarter_week(dt): monday dt - datetime.timedelta(daysdt.weekday()) # 本周一 thursday monday datetime.timedelta(days3) # 本周四 # 用周四所在的月份/年份来判断这周属于哪个季度 month thursday.month year thursday.year quarter_idx (month - 1) // 3 # 季度第1周的起点季度首日所在自然周的周一 quarter_start datetime.date(year, quarter_idx * 3 1, 1) q_monday quarter_start - datetime.timedelta(daysquarter_start.weekday()) week_num (monday - q_monday).days // 7 1 return year, QUARTERS[quarter_idx], f第{max(week_num, 1)}周核心思路一周里的「周四」属于哪个月这一周就属于哪个季度。这和 ISO 8601 年份归属规则精神一致ISO 8601 用周四判断年份归属。验证2026年Q1季度首日1月1日(周四) → 本周周一是12月29日 周四归属12月29日这周的周四是1月1日属于1月 → Q1✓ 第1周范围2025-12-29(周一) ~ 2026-01-04(周日)3.3 逆向函数week_date_rangedef week_date_range(year, quarter_label, week_num): quarter_start datetime.date(year, quarter_idx * 3 1, 1) q_monday quarter_start - datetime.timedelta(daysquarter_start.weekday()) wk_monday q_monday datetime.timedelta(weeksweek_num - 1) wk_sunday wk_monday datetime.timedelta(days6) return wk_monday, wk_sunday给定「年份季度周号」返回该周的具体日期范围周一到周日。两个函数互为逆向DateToWeekDialog同时使用两者来展示「日期→周次」和「周次→日期范围」。四、UI 组件设计4.1 WeekStatusGrid自定义状态格子这是最核心的交互控件用wx.WrapSizer排列一组wx.Button每个按钮代表一周class WeekStatusGrid(wx.Panel): def setup_ui(self): grid wx.WrapSizer(wx.HORIZONTAL) # 自动换行 for week in self.weeks: btn wx.Button(self, labelweek.replace(第,).replace(周,W), size(52, 38)) # 显示为 6W 而非 第6周 btn.Bind(wx.EVT_BUTTON, lambda e, wweek, bbtn: self.on_week_click(e, w, b)) btn.Bind(wx.EVT_RIGHT_DOWN, lambda e, wweek, bbtn: self.on_week_right(e, w, b))左键循环切换状态def on_week_click(self, evt, week, btn): current self.fs_manager.get_week_status(...) next_status { STATUS_MISSING: STATUS_RECEIVED, STATUS_RECEIVED: STATUS_LATE, STATUS_LATE: STATUS_MISSING, }.get(current, STATUS_RECEIVED) self.fs_manager.set_week_status(..., next_status) self._apply_btn_style(btn, next_status)用字典代替if-elif链是状态机的一种简洁写法。三个状态循环未收→已收→迟交→未收。Lambda 闭包陷阱注意lambda e, wweek, bbtn:这种写法。如果写成lambda e: self.on_week_click(e, week, btn)由于 Python 的 late binding所有 lambda 都会捕获循环结束后的最后一个week/btn值。用默认参数wweek可以在定义时捕获当前值是 wxPython 事件绑定中必须掌握的技巧。右键菜单def on_week_right(self, evt, week, btn): menu wx.Menu() for status in [STATUS_RECEIVED, STATUS_MISSING, STATUS_LATE]: item menu.Append(wx.ID_ANY, f标记为: {status}) self.Bind(wx.EVT_MENU, lambda e, wweek, bbtn, sstatus: self.set_status(w, b, s), item) menu.AppendSeparator() # ... 添加备注、打开文件夹 self.PopupMenu(menu)wx.ID_ANY让 wxPython 自动分配菜单项 ID避免手动管理 ID 冲突。4.2 ImportFileDialog拖拽文件导入拖拽支持通过wx.FileDropTarget实现class DropTarget(wx.FileDropTarget): def __init__(self, callback): super().__init__() self.callback callback def OnDropFiles(self, x, y, filenames): self.callback(filenames) return True # 必须返回 True 表示接受拖拽 # 注册到 ListCtrl drop_target DropTarget(self._add_files) self.file_list.SetDropTarget(drop_target)这是一个典型的回调注入模式DropTarget不知道谁是宿主只持有一个 callable由对话框注入具体处理逻辑。文件列表同时支持wx.FileDialog手动选择两种方式共用同一个_add_files方法处理避免逻辑重复。4.3 DateToWeekDialog实时响应的日期选择器class DateToWeekDialog(wx.Dialog): def setup_ui(self): self.dp wx.adv.DatePickerCtrl(panel, stylewx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY) today datetime.date.today() self.dp.SetValue(wx.DateTime(today.day, today.month - 1, today.year)) self.dp.Bind(wx.adv.EVT_DATE_CHANGED, self.on_query) # 日期变化即时查询两个注意点1.import wx.adv是必须的。wxPython 4.x 中DatePickerCtrl、EVT_DATE_CHANGED等控件位于wx.adv子模块需要显式导入。如果只有import wx控件会静默创建失败或行为异常不报任何错误——这是一个很难定位的坑。2.wx.DateTime的月份从 0 开始。today.month - 1是必要的转换wx.DateTime用 0 表示一月而 Python 的datetime.date.month从 1 开始。wx.DateTime 和 Python date 的互转def _wx_date_to_py(self, wx_dt): return datetime.date(wx_dt.GetYear(), wx_dt.GetMonth() 1, wx_dt.GetDay())1把 wx 的 0-based 月份转回 Python 的 1-based。这个函数虽然只有一行但每次使用DatePickerCtrl都会用到值得封装。4.4 StatisticsPanel四 Tab 统计报表class StatisticsPanel(wx.Panel): def setup_ui(self): self.tab wx.Notebook(self) self._build_overview_cards() self._build_tab_week() # Tab1按周次查询 self._build_tab_vendor() # Tab2按供应商查询 self._build_tab_project() # Tab3按项目查询 self._build_tab_matrix() # Tab4交叉汇总表Tab4 交叉汇总表用了wx.grid.Grid而不是用wx.ListCtrl模拟import wx.grid as gridlib grid gridlib.Grid(self.mx_scroll) grid.CreateGrid(len(proj_keys), len(all_weeks)) grid.SetDefaultColSize(62) grid.SetRowLabelSize(160) grid.EnableEditing(False) # 按状态设置格子颜色 for ri, (vendor, project) in enumerate(proj_keys): for ci, week in enumerate(all_weeks): if week not in past_week_set: grid.SetCellBackgroundColour(ri, ci, wx.Colour(220, 220, 220)) elif status STATUS_RECEIVED: grid.SetCellBackgroundColour(ri, ci, wx.Colour(39, 174, 96)) grid.SetCellValue(ri, ci, ✔) # ... grid.SetCellAlignment(ri, ci, wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)wx.grid.Grid支持单元格级别的背景色、字体、对齐方式比wx.ListCtrl的 item attribute 方式更直接尤其适合这种需要全格着色的场景。五、主窗口架构5.1 SplitterWindow Notebook 的组合self.splitter wx.SplitterWindow(self, stylewx.SP_3D | wx.SP_LIVE_UPDATE) # 左深色树形导航 left_panel.SetBackgroundColour(wx.Colour(44, 62, 80)) # 右Notebook 多 Tab self.notebook wx.Notebook(self.right_panel) self.splitter.SplitVertically(left_panel, self.right_panel, 220)wx.SP_LIVE_UPDATE使分割线拖动时实时重绘而不是拖完松手才更新。SetMinimumPaneSize(180)防止左栏被拖到消失。5.2 树形控件的数据绑定wxPython 的TreeCtrl通过SetItemData给每个节点绑定任意 Python 对象v_item self.tree.AppendItem(root, f {vendor}) self.tree.SetItemData(v_item, (vendor, vendor)) p_item self.tree.AppendItem(v_item, f {project}) self.tree.SetItemData(p_item, (project, vendor, project))选中节点时通过GetItemData取回数据判断类型做不同响应def on_tree_select(self, evt): data self.tree.GetItemData(evt.GetItem()) if data is None: return kind data[0] if kind vendor: ... elif kind project: vendor, project data[1], data[2] self.open_project_tab(vendor, project)这比用节点文本做字符串解析要干净得多。5.3 项目 Tab 的懒加载与缓存def open_project_tab(self, vendor, project): key (vendor, project) if key in self.project_panels: # 已有 Tab直接切换 for i in range(self.notebook.GetPageCount()): if self.notebook.GetPage(i) self.project_panels[key]: self.notebook.SetSelection(i) return # 新建 Tab panel ProjectDetailPanel(self.notebook, self.fs_manager, vendor, project) tab_title f {project} if self.notebook.GetPageCount() 9: # 最多9个 Tab超出替换最旧的 self.notebook.DeletePage(2) # 从第3页开始替换前两页固定 self.notebook.AddPage(panel, tab_title, selectTrue) self.project_panels[key] panelself.project_panels字典缓存已打开的面板重复点击同一项目不会重复创建。当 Tab 数量超过 9 时删除最旧的第 2 页之后防止无限增长。六、几个值得关注的细节6.1 wx.ID_CANCEL vs wx.ID_CLOSE# 错误写法在 Windows 上会触发系统级窗口关闭事件 wx.Button(panel, wx.ID_CLOSE, 关闭) # 正确写法 wx.Button(panel, wx.ID_CANCEL, 关闭)wx.ID_CLOSE在 Windows 上绑定了系统行为会导致对话框在某些情况下异常关闭。对话框的「关闭」按钮应该用wx.ID_CANCEL或手动绑定EndModal。6.2 CSV 导出用 utf-8-sigwith open(path, w, encodingutf-8-sig) as f:utf-8-sig会在文件开头写入 BOMByte Order Mark。在 Windows 上用 Excel 直接双击打开 CSV 时BOM 告诉 Excel 这是 UTF-8 编码否则中文会乱码。这是一个在国内 Windows 环境下的必要处理。6.3 wx.LaunchDefaultApplication 打开文件夹def open_folder(self, week): path os.path.join(self.fs_manager.base_dir, ...) if os.path.exists(path): wx.LaunchDefaultApplication(path)wx.LaunchDefaultApplication跨平台在 Windows 上调用 Explorer在 macOS 上调用 Finder在 Linux 上调用默认文件管理器不需要写平台判断。6.4 FlexGridSizer 做表单布局form wx.FlexGridSizer(0, 2, 10, 10) # 行数自动2列行间距10列间距10 form.AddGrowableCol(1) # 第2列值列随窗口宽度拉伸 form.Add(wx.StaticText(..., label供应商名称:), 0, wx.ALIGN_CENTER_VERTICAL) form.Add(self.txt_name, 1, wx.EXPAND)FlexGridSizer比手动嵌套多个BoxSizer清晰得多AddGrowableCol(1)让输入框列自动填充剩余宽度是做对话框表单的标准做法。七、总结模块关键技术点数据层文件系统即数据库.status.json 状态持久化fallback 推断机制日期计算周四归属法解决季度边界问题date↔week 互转函数对WeekStatusGridLambda 闭包 default arg 技巧字典状态机WrapSizer 自动换行文件导入FileDropTarget 回调注入FileDialog 拖拽共用处理逻辑DatePickerCtrlwx.adv 显式导入wx.DateTime 月份 0-based 转换交叉汇总表wx.grid.Grid 单元格着色ScrolledWindow 嵌套主窗口TreeCtrl SetItemData 数据绑定Notebook Tab 懒加载缓存细节ID_CANCEL vs ID_CLOSEutf-8-sig BOMLaunchDefaultApplication 跨平台这个项目的整体结构对于同等规模的 wxPython 应用有一定参考价值用文件系统替代嵌入式数据库降低了部署复杂度状态与文件共存于同一目录保持数据的局部性UI 层通过事件驱动和回调注入保持解耦。