certflow.views.output_view 源代码

# src/certflow/views/output_view.py
"""统一输出视图(OutputView)—— 打印/扫描合一(§11.5 阶段3)。

设计要点(方案A · 配置驱动 + 组合复用):
- 顶部一个「输出目标」下拉,选项由 ``output.targets`` 配置驱动(打印/扫描),
  默认值取 ``output.default_target``;切换仅显隐对应子视图,不重建。
- 打印子页复用既有 :class:`PrintView`(ESC/P 打印 + 完整表单 + 预览),
  扫描子页复用既有 :class:`ScanView`(PIL 渲染),二者逻辑/测试零改动 → 行为零回归。
- 护栏(阶段1 ``run_guard``)、``PrintLog`` 写点(阶段2 ``HistoryBackfillService``)、
  历史 diff 闸均为两子视图共享服务,本视图只负责目标切换与信号收敛。
- 信号统一为 ``output_completed(target, payload)``;``PrintView.navigate_to_scan``
  (内部「生成扫描件」按钮)在本视图内直接切换目标并加载,不再外抛。
"""

from __future__ import annotations

from typing import Any

from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
    QComboBox,
    QHBoxLayout,
    QLabel,
    QStackedWidget,
    QVBoxLayout,
    QWidget,
)

from certflow.config.settings import cfg, get_layout_groups
from certflow.controllers.output_controller import OutputController
from certflow.utils.logger import logger

# 目标 key -> 默认显示标签(配置未提供 label 时回退)
_TARGET_LABELS: dict[str, str] = {
    "print": "打印合格证",
    "scan": "扫描件生成",
}


[文档] class OutputView(QWidget): """打印/扫描合一的统一输出视图(单 tab)""" # 统一完成信号:(target, payload) —— target ∈ {"print","scan"} output_completed = Signal(str, list) def __init__( self, session: Any, config: dict = None, db_manager: Any = None, parent: Any = None, ) -> None: """初始化统一输出视图。 Args: session: SQLAlchemy 会话对象 config: 配置字典 db_manager: 数据库管理器实例(可选) parent: 父控件(可选) """ super().__init__(parent) self.session = session self.config = config or {} self.db_manager = db_manager self._targets: list[tuple[str, str]] = [] # [(key, label), ...] self._build_targets() # §11.8 阶段4:统一输出控制器(聚合 PrinterController/ScanController + 显式 dispatch) self.output_controller = OutputController(self.session) # 复刻 PrintView 懒加载的打印机选择逻辑(集中到统一控制器,避免重复) real = self.output_controller.current_printer if real: self.output_controller.printer_controller.set_printer(real) logger.info(f"[OutputView] 系统默认打印机: {real}") self._init_ui() self._connect_signals() # ============================================================ # 配置驱动:读取 output.targets 决定下拉选项 # ============================================================ def _build_targets(self) -> None: """从 ``output.targets`` 构造目标列表;``default_target`` 置顶。""" targets = cfg("output.targets", {}) or {} default = cfg("output.default_target", "print") keys = list(targets.keys()) if default in keys: keys.remove(default) keys.insert(0, default) for key in keys: label = targets.get(key, {}).get("run", "") # run 非展示用,仅占位 # 优先用内置默认标签,其次取 targets 内可选 label,再回退 key label = _TARGET_LABELS.get(key, label) or key self._targets.append((key, label)) # ============================================================ # UI 构建 # ============================================================ def _init_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # 顶部输出目标选择栏 bar = QHBoxLayout() bar.addWidget(QLabel("输出目标:")) self.target_combo = QComboBox() for key, label in self._targets: self.target_combo.addItem(label, key) self.target_combo.currentIndexChanged.connect(self._on_target_changed) bar.addWidget(self.target_combo) # 常用布局(D):一键套用 target+template+layout 命名组合, # 选项来自 output.layout_groups(enabled 组),加载即写回下方子视图下拉。 bar.addSpacing(16) bar.addWidget(QLabel("常用布局:")) self.layout_group_combo = QComboBox() self._layout_groups = self._load_layout_groups() self.layout_group_combo.addItem("— 选择布局 —") for _g in self._layout_groups: self.layout_group_combo.addItem(_g.get("name", "")) self.layout_group_combo.currentIndexChanged.connect(self._on_layout_group_changed) bar.addWidget(self.layout_group_combo) bar.addStretch() layout.addLayout(bar) # 子视图堆叠:打印 / 扫描 self.stack = QStackedWidget() from certflow.views.print_view import PrintView from certflow.views.scan_view import ScanView self.print_view = PrintView( self.session, self.config, db_manager=self.db_manager, parent=self, printer_controller=self.output_controller.printer_controller, ) self.stack.addWidget(self.print_view) self.scan_view = ScanView( self.session, db_manager=self.db_manager, parent=self, scan_controller=self.output_controller.scan_controller, ) self.stack.addWidget(self.scan_view) layout.addWidget(self.stack) # 默认目标 default = cfg("output.default_target", "print") self.set_target(default) def _on_target_changed(self, index: int) -> None: key = self.target_combo.itemData(index) self._show_target(key) # ============================================================ # 常用布局(D):一键套用 target+template+layout 组合 # ============================================================ def _load_layout_groups(self) -> list[dict]: """读取启用的常用布局组(output.layout_groups,enabled=true)。""" return get_layout_groups(enabled_only=True) def _on_layout_group_changed(self, index: int) -> None: """常用布局下拉变化 → 套用对应组到子视图下拉。 组 = {target, template(模板族), layout(拼版 label, 仅扫描)};套用后把下拉 回置到占位项,便于重复选择同一布局。 """ if index <= 0: return group = self._layout_groups[index - 1] target = group.get("target") if target: self.set_target(target) family = group.get("template") if family: if target == "scan": self.scan_view.set_template_by_family(family) else: self.print_view._restore_template_by_family(family) layout = group.get("layout") if target == "scan" and layout: self.scan_view.apply_layout_preset(layout) # 套用完成,回置占位项(不触发本次逻辑,index<=0 直接 return) self.layout_group_combo.setCurrentIndex(0) def _show_target(self, key: str) -> None: if key == "scan": self.stack.setCurrentWidget(self.scan_view) else: self.stack.setCurrentWidget(self.print_view) # ============================================================ # 对外接口(供 main_window 调用;委托给对应子视图) # ============================================================
[文档] def set_target(self, key: str) -> None: """切换输出目标(打印/扫描)并更新下拉。 Args: key: 目标 key("print" / "scan"),未知 key 回退打印。 """ if key not in {k for k, _ in self._targets}: key = self._targets[0][0] if self._targets else "print" idx = self.target_combo.findData(key) if idx >= 0: self.target_combo.setCurrentIndex(idx) else: self._show_target(key)
[文档] def current_target(self) -> str: """当前输出目标 key。""" idx = self.target_combo.currentIndex() return self.target_combo.itemData(idx) or "print"
[文档] def run_output(self, target: str, action: str = "print", **kwargs: Any) -> Any: """统一调度入口:委托 OutputController.run(§11.8 阶段4 显式 dispatch 表)。 Args: target: "print" / "scan" action: 目标内动作键(print→print_certificate;scan→generate/load) **kwargs: 透传给具体控制器方法 """ return self.output_controller.run(target, action, **kwargs)
[文档] def load_selected_sale_plan_ids(self, sale_plan_ids: list[int]) -> None: """加载销售计划(委托打印子视图)。""" self.set_target("print") self.print_view.load_selected_sale_plan_ids(sale_plan_ids)
[文档] def load_certificates(self, certificate_ids: list[int]) -> None: """加载合格证(委托扫描子视图)。""" self.set_target("scan") self.scan_view.load_certificates(certificate_ids)
# ============================================================ # 信号收敛 # ============================================================ def _connect_signals(self) -> None: # 打印完成 → 统一 output_completed("print", ids) self.print_view.print_completed.connect( lambda ids: self.output_completed.emit("print", ids) ) # 内部「生成扫描件」按钮 → 直接切换目标并加载,不外抛 self.print_view.navigate_to_scan.connect(self._on_navigate_to_scan) # 扫描完成 → 统一 output_completed("scan", ids) self.scan_view.scan_completed.connect(lambda ids: self.output_completed.emit("scan", ids)) def _on_navigate_to_scan(self, certificate_ids: list[int]) -> None: """PrintView 内部「生成扫描件」→ 切换到扫描目标并加载对应合格证。""" self.set_target("scan") if certificate_ids: self.scan_view.load_certificates(certificate_ids)