智学文枢:页面切换与跨页数据预填

页面切换与跨页数据预填(open_*_with 模式)

一句话摘要:用户在习题管理页点"AI分析",跳到分析页时学生和习题必须还在------open_*_with 模式用"主窗口切页 + 页面 preset_selection 预填"两段式契约完成跨页上下文传递,配合 getattr 动态方法派发,让 5 个 AI 页面共享同一套跳转约定。

一、引言

先问第一性问题:跨页协作最怕丢什么?

智学文枢的工作流是线性的:学生 → 习题 → AI分析 → 讲解 → 追问 → 变式 → 报告。用户在习题管理页选中一道题,点击"AI分析",分析页必须知道是哪个学生、哪道题------否则用户到了新页面还得重新选一遍,流程断裂,体验归零。

这个"上下文"(学生 ID + 习题 ID)就是跨页跳转要传递的核心资产。难点在于:

  1. 页面实例是懒加载的(第 24 篇),跳转时目标页面可能还不存在;
  2. 目标页面的 UI 是异步刷新的 (列表加载靠 Store 信号),预填不能简单 setText 了事;
  3. 跳转入口很多:导航点击、习题页快捷菜单、分析页"去讲解"、追问页"去变式"......每个入口都要带同样的上下文。

第一性原理的答案是一条约定 :主窗口负责"切到正确的页面并拿到实例",页面负责"接收 (student_id, question_id) 并完成预填"。前者统一成 open_*_with 方法族,后者统一成 preset_selection 方法。两段式契约,各管一段。

二、核心原理

2.1 open_*_with 的两段式结构

#mermaid-svg-6yQcQeFgI3qdYHFS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6yQcQeFgI3qdYHFS .error-icon{fill:#552222;}#mermaid-svg-6yQcQeFgI3qdYHFS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6yQcQeFgI3qdYHFS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6yQcQeFgI3qdYHFS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6yQcQeFgI3qdYHFS .marker.cross{stroke:#333333;}#mermaid-svg-6yQcQeFgI3qdYHFS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6yQcQeFgI3qdYHFS p{margin:0;}#mermaid-svg-6yQcQeFgI3qdYHFS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster-label text{fill:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster-label span{color:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster-label span p{background-color:transparent;}#mermaid-svg-6yQcQeFgI3qdYHFS .label text,#mermaid-svg-6yQcQeFgI3qdYHFS span{fill:#333;color:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS .node rect,#mermaid-svg-6yQcQeFgI3qdYHFS .node circle,#mermaid-svg-6yQcQeFgI3qdYHFS .node ellipse,#mermaid-svg-6yQcQeFgI3qdYHFS .node polygon,#mermaid-svg-6yQcQeFgI3qdYHFS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6yQcQeFgI3qdYHFS .rough-node .label text,#mermaid-svg-6yQcQeFgI3qdYHFS .node .label text,#mermaid-svg-6yQcQeFgI3qdYHFS .image-shape .label,#mermaid-svg-6yQcQeFgI3qdYHFS .icon-shape .label{text-anchor:middle;}#mermaid-svg-6yQcQeFgI3qdYHFS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6yQcQeFgI3qdYHFS .rough-node .label,#mermaid-svg-6yQcQeFgI3qdYHFS .node .label,#mermaid-svg-6yQcQeFgI3qdYHFS .image-shape .label,#mermaid-svg-6yQcQeFgI3qdYHFS .icon-shape .label{text-align:center;}#mermaid-svg-6yQcQeFgI3qdYHFS .node.clickable{cursor:pointer;}#mermaid-svg-6yQcQeFgI3qdYHFS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6yQcQeFgI3qdYHFS .arrowheadPath{fill:#333333;}#mermaid-svg-6yQcQeFgI3qdYHFS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6yQcQeFgI3qdYHFS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6yQcQeFgI3qdYHFS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6yQcQeFgI3qdYHFS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6yQcQeFgI3qdYHFS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6yQcQeFgI3qdYHFS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster text{fill:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS .cluster span{color:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6yQcQeFgI3qdYHFS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6yQcQeFgI3qdYHFS rect.text{fill:none;stroke-width:0;}#mermaid-svg-6yQcQeFgI3qdYHFS .icon-shape,#mermaid-svg-6yQcQeFgI3qdYHFS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6yQcQeFgI3qdYHFS .icon-shape p,#mermaid-svg-6yQcQeFgI3qdYHFS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6yQcQeFgI3qdYHFS .icon-shape .label rect,#mermaid-svg-6yQcQeFgI3qdYHFS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6yQcQeFgI3qdYHFS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6yQcQeFgI3qdYHFS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6yQcQeFgI3qdYHFS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用
第一步: 切页
懒加载/复用
第二步: 预填
同步
选中
异步刷新
发起方

习题页/分析页等
MainWindow.open_xxx_with
_switch_page(key)
页面实例
page.preset_selection(sid, qid)
get_student + load_questions
_on_question_changed
Store 信号 → UI 更新

第一步 _switch_page 保证"页面在台上且实例存在";第二步 preset_selection 保证"上下文注入完毕"。两步缺一不可:只切页不预填,用户面对空页面;只预填不切页,用户根本看不见。

2.2 preset_selection 是页面侧的统一接口

所有目标页面实现同名同签名的方法:

python 复制代码
def preset_selection(self, student_id: str, question_id: str): ...

签名统一(学生 ID + 习题 ID)是这套模式能成立的前提------主窗口不需要知道每个页面的内部结构,只要会调用这一个方法。

2.3 上下文依赖 Store 传递,而非参数直传

注意 open_analysis_with(student_id, question_id) 传的是 ID 而不是对象 。页面拿到 ID 后通过 student_store.get_student(id)、question_store.load_questions(id) 自行取数。ID 是"引用",对象是"快照"------传 ID 保证任何时候取到的都是最新数据,也避开了跨线程/跨页传递可变对象的竞态。

三、代码实现

3.1 主窗口:5 个 open_*_with 方法

python 复制代码
def open_analysis_with(self, student_id: str, question_id: str):
    """跳转到AI分析页面,并预填学生与习题选择"""
    self._switch_page("analysis")
    page = self._page_instances["analysis"]
    page.preset_selection(student_id, question_id)

def open_variants_with(self, student_id: str, question_id: str):
    """跳转到变式训练页面,并预填学生与习题选择"""
    self._switch_page("variants")
    page = self._page_instances["variants"]
    page.preset_selection(student_id, question_id)

def open_followup_with(self, student_id: str, question_id: str):
    """跳转到AI追问页面,并预填学生与习题选择"""
    self._switch_page("followup")
    page = self._page_instances["followup"]
    page.preset_selection(student_id, question_id)

def open_teaching_with(self, student_id: str, question_id: str):
    """跳转到AI讲解页面,并预填学生与习题选择"""
    self._switch_page("teaching")
    page = self._page_instances["teaching"]
    page.preset_selection(student_id, question_id)

def open_reports_with(self, student_id: str, question_id: str):
    """跳转到报告导出页面,并预填学生与习题选择(单题报告)"""
    self._switch_page("reports")
    page = self._page_instances["reports"]
    page.preset_selection(student_id, question_id)

源码位置:ui/components/layout/main_window.py L156-L184

五个方法结构完全一致,只是页面 key 不同。_switch_page 内部完成了授权拦截 + 懒加载实例化,所以 _page_instances["analysis"] 此处必然存在------这是"先切页后取实例"的顺序保证。

3.2 页面侧:preset_selection 的标准实现

以 analysis_page.py 为例:

python 复制代码
def preset_selection(self, student_id: str, question_id: str):
    """由外部(如习题管理页)跳转并预填学生与习题选择

    加载该学生习题列表,选中指定习题,刷新习题预览与已有分析结果。
    """
    student = student_store.get_student(student_id)
    if student:
        self._current_student_id = student_id
        self.student_btn.setText(student.name)

    # 同步加载该学生习题列表(触发 _on_questions_loaded 刷新按钮状态)
    question_store.load_questions(student_id)

    # 预填习题并刷新展示
    self._selected_question_id = question_id
    self._on_question_changed()

    q = question_store.get_question(question_id)
    if q:
        text = (q.question_text or "").replace("\n", " ")
        if len(text) > 40:
            text = text[:40] + "..."
        self.question_btn.setText(f"[{q.subject}] {text}")
        self.question_btn.setEnabled(True)
    else:
        self.question_btn.setText("点击选择习题...")

源码位置:ui/pages/analysis/analysis_page.py L271-L296

四步动作值得逐条拆解:

  1. 写状态 :get_student 同步取学生,更新 _current_student_id 和按钮文字------"数据权威"交给 Store,页面只消费;
  2. 触发展示 :question_store.load_questions(student_id) 会 emit(questions_loaded),页面连接的槽函数随之刷新习题按钮状态------预填借道信号,与手动操作走完全相同的路径;
  3. 选中习题 :设置 _selected_question_id 后调 _on_question_changed(),刷新 LaTeX 预览、习题图片与已有分析结果;
  4. 兜底显示:习题不存在时按钮回退到"点击选择习题...",不抛异常、不白屏。

3.3 发起方:习题页的动态菜单派发

习题管理页把 5 个跳转目标声明成"方法名字符串",用 getattr 动态调用:

python 复制代码
def _build_flow_menu(self, question_id: str) -> QMenu:
    """构建流程快捷跳转菜单:AI分析/讲解/追问/变式/报告(跳转不锁定)"""
    menu = QMenu(self)
    student_id = self._current_student_id or ""
    targets = [
        ("AI分析", "open_analysis_with"),
        ("AI讲解", "open_teaching_with"),
        ("AI追问", "open_followup_with"),
        ("变式训练", "open_variants_with"),
        ("习题报告", "open_reports_with"),
    ]
    for label, method in targets:
        act = menu.addAction(label)
        act.triggered.connect(
            lambda checked=False, mid=student_id, qid=question_id, m=method:
                self._open_flow_page(mid, qid, m)
        )
    return menu

def _open_flow_page(self, student_id: str, question_id: str, method: str):
    """调用主窗口跳转到流程阶段页(进度式,可任意跳转)"""
    if not student_id:
        QMessageBox.information(self.window(), "提示", "请先选择学生")
        return
    main_window = self.window()
    if hasattr(main_window, method):
        getattr(main_window, method)(student_id, question_id)

源码位置:ui/pages/questions/question_list_page.py L586-L612

hasattr + getattr 的组合是防御式动态派发 :方法不存在时静默跳过,避免 AttributeError 崩掉菜单;菜单项与主窗口方法一一对应,新增跳转目标只需在 targets 加一行。
QuestionStore StudentStore AI分析页 MainWindow 习题管理页 用户 QuestionStore StudentStore AI分析页 MainWindow 习题管理页 用户 #mermaid-svg-tEWHjhfMHVSkJ1Q6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .error-icon{fill:#552222;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .marker.cross{stroke:#333333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 p{margin:0;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .sequenceNumber{fill:white;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 #sequencenumber{fill:#333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .messageText{fill:#333;stroke:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .labelText,#mermaid-svg-tEWHjhfMHVSkJ1Q6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .loopText,#mermaid-svg-tEWHjhfMHVSkJ1Q6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .noteText,#mermaid-svg-tEWHjhfMHVSkJ1Q6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actorPopupMenu{position:absolute;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 .actor-man circle,#mermaid-svg-tEWHjhfMHVSkJ1Q6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tEWHjhfMHVSkJ1Q6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选中习题 → 点「AI分析」open_analysis_with(sid, qid)_switch_page("analysis") + 授权拦截preset_selection(sid, qid)get_student(sid)学生对象load_questions(sid)emit questions_loaded_selected_question_id = qid_on_question_changed() 刷新预览页面就绪,上下文已注入

3.4 页面间的二次跳转:串联流程

分析页分析完成后,"去讲解"按钮继续接力:

python 复制代码
def _on_goto_teaching(self):
    """跳转到AI讲解页(携带当前学生与习题)"""
    if not self._current_question:
        QMessageBox.information(self.window(), "提示", "请先选择一道习题")
        return

    main_window = self.window()
    if hasattr(main_window, "open_teaching_with"):
        main_window.open_teaching_with(
            self._current_student_id, self._selected_question_id
        )

源码位置:ui/pages/analysis/analysis_page.py L452-L462

这就是"流程串联":每个页面既是接收方又是发起方,上下文 ID 从习题页一路传递到报告页,中间任何一环的 preset_selection 都只依赖 Store,不依赖上游页面。

四、设计权衡 / 踩坑记录

4.1 权衡:为什么传 ID 而不是传对象

传 Student 对象看似省事,但对象可能携带旧数据(_students 缓存更新后旧引用还在),且跨页传递可变对象容易在未知位置被修改。传 ID、到站取数,让数据永远来自 Store 的权威缓存,是这条链路不出脏数据的根本保证。

4.2 权衡:getattr 动态派发 vs 硬编码 5 个 if

习题页菜单有 5 个目标,若每个都写 if method == "open_analysis_with": main_window.open_analysis_with(...),代码重复且新增目标要改两处。"方法名即接口名"的约定让菜单声明与主窗口 API 一一对应,一处数据、一处逻辑。

4.3 坑:preset_selection 里是"同步加载 + 信号刷新"的混合

load_questions 是同步方法(直接查库 + emit),而列表页的刷新槽函数是异步触发的。这意味着 preset_selection 执行完,按钮文字是自己手动 setText 的,而习题按钮的可用性依赖 questions_loaded 槽------两套路径。代码用"先设 _selected_question_id 再调 _on_question_changed"的顺序保证:即使列表还没刷新完,预览和按钮文字也已经就位。

4.4 坑:跳转前必须校验业务前置条件

习题页 _open_flow_page 先校验 student_id;分析页 _on_goto_teaching 先校验 _current_question;习题页对组合题题干还有专门拦截("组合题题干仅作上下文,请选择其下的具体子题进行分析")。预填模式只负责"带得对",不负责"能不带"------业务守卫留在发起方,收口在调用链起点。

4.5 权衡:为什么不直接用 QStackedWidget 的 currentChanged 自动带参

QStackedWidget.currentChanged 只携带新页面的下标/指针,没有"学生 ID、习题 ID"这些业务参数,页面自己还得记上次选了什么------切页时上下文会丢失。open_*_with 把参数显式化,跳转即预填 ,语义清晰、时序可控,代价是多写 5 个外观相似的方法------用统一命名(open_*_with)+ 统一签名换来了可维护性。

五、小结

  • open_*_with 是两段式契约:主窗口 _switch_page 保证"页面在台",页面 preset_selection(student_id, question_id) 保证"上下文注入";
  • 上下文以 ID 传递,到站后通过 Store 权威缓存取数,杜绝脏对象跨页流动;
  • 发起方用 hasattr + getattr 动态派发,菜单声明与主窗口 API 一一对应,新增目标只改一行;
  • 页面既接收又接力(分析页 → 讲解页 → 追问页 → 变式 → 报告),上下文沿工作流一路传递;
  • 业务前置校验留在调用链起点,预填模式专注"把对的上下文带到对的页面"。

至此,第 3 部分「Store 层与 UI 数据流」的 9 篇文章全部完成:从 17 行的 BaseStore 地基,到 7 个 Store 的三种形态(直连仓库 / 代理服务 / 异步线程),再到主窗口、侧边栏、主题、标题栏四块 UI 骨架,最后以 open_*_with 跨页模式收官。

相关推荐
2601_962885723 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python
打工仔折腾 AI3 小时前
在 Ubuntu ARM64 部署 Q1 V3.0 MuJoCo 模型:加载、单步推理与执行器响应解析
linux·运维·人工智能·python·深度学习·ubuntu·性能优化
yyuuuzz3 小时前
跨境站点日志时区混乱的排查记录
运维·服务器·网络·云计算·aws·1024程序员节
西安同步电子3 小时前
GNSS信号模拟器在车规导航ISO26262证中的测试方案
1024程序员节
信誓旦旦的程序猿3 小时前
【Python量化实战宝典 #21】北向资金跟随手册:聪明钱择时信号(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
严谨的麻辣烫4 小时前
静态 ISP 代理请求超时怎么排查?Python 与 curl 实战
python·http·requests·1024程序员节·网络排障
我不会插花弄玉5 小时前
13. 用户管理【由浅入深-MySQL】
1024程序员节
KeepRobots5 小时前
11-01 温哥华不回拨:用 60 行 Python 查你的工单系统会不会差一小时(KeepRobots 工程笔记)
python
xiao77mwq5 小时前
你以为 OCR 慢?实测 81.3 s 里全是故障代价:如何把「定格态」消掉,让耗时从 6.5 s 到 13 倍差距显出原形
python