1225 lines
52 KiB
Python
1225 lines
52 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""任务配置面板 - 简化版统一界面"""
|
||
|
||
import shutil
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Set, Tuple
|
||
|
||
from PySide6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox,
|
||
QPushButton, QPlainTextEdit, QFrame, QFileDialog, QMessageBox, QScrollArea,
|
||
QSpinBox, QDateTimeEdit, QSizePolicy, QTabWidget, QRadioButton, QButtonGroup
|
||
)
|
||
from PySide6.QtCore import Qt, Signal, QDateTime, QTimer
|
||
from PySide6.QtGui import QFont
|
||
|
||
from ..models.task_model import TaskConfig
|
||
from ..models.task_registry import (
|
||
task_registry, BusinessDomain, DOMAIN_LABELS, TaskDefinition,
|
||
get_fact_ods_task_codes, get_dimension_ods_task_codes,
|
||
)
|
||
from ..utils.cli_builder import CLIBuilder
|
||
from ..utils.app_settings import app_settings
|
||
from .task_selector import TaskSelectorWidget, DwdTableSelectorWidget
|
||
from .pipeline_selector import (
|
||
PipelineSelectorWidget, PIPELINE_OPTIONS, get_pipeline_layers,
|
||
WINDOW_SPLIT_OPTIONS, WINDOW_SPLIT_DAY_OPTIONS
|
||
)
|
||
|
||
|
||
class CollapsibleSection(QWidget):
|
||
"""可折叠区域组件"""
|
||
|
||
def __init__(self, title: str, parent: Optional[QWidget] = None):
|
||
super().__init__(parent)
|
||
self._is_expanded = False
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# 标题按钮
|
||
self._toggle_btn = QPushButton(f"▶ {title}")
|
||
self._toggle_btn.setStyleSheet("""
|
||
QPushButton {
|
||
text-align: left;
|
||
padding: 8px 12px;
|
||
background: #f0f0f0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
""")
|
||
self._toggle_btn.clicked.connect(self._toggle)
|
||
layout.addWidget(self._toggle_btn)
|
||
|
||
# 内容区域
|
||
self._content = QWidget()
|
||
self._content.setVisible(False)
|
||
self._content_layout = QVBoxLayout(self._content)
|
||
self._content_layout.setContentsMargins(12, 8, 12, 8)
|
||
layout.addWidget(self._content)
|
||
|
||
self._title = title
|
||
|
||
def _toggle(self):
|
||
"""切换展开/折叠状态"""
|
||
self._is_expanded = not self._is_expanded
|
||
self._content.setVisible(self._is_expanded)
|
||
icon = "▼" if self._is_expanded else "▶"
|
||
self._toggle_btn.setText(f"{icon} {self._title}")
|
||
|
||
def set_expanded(self, expanded: bool):
|
||
"""设置展开状态"""
|
||
if self._is_expanded != expanded:
|
||
self._toggle()
|
||
|
||
def setExpanded(self, expanded: bool):
|
||
"""Qt 风格别名,保持兼容性"""
|
||
self.set_expanded(expanded)
|
||
|
||
def isExpanded(self) -> bool:
|
||
"""获取当前展开状态"""
|
||
return self._is_expanded
|
||
|
||
def content_layout(self) -> QVBoxLayout:
|
||
"""获取内容布局"""
|
||
return self._content_layout
|
||
|
||
|
||
class TaskPanel(QWidget):
|
||
"""任务配置面板 - 简化版"""
|
||
|
||
ML_IMPORT_TASK_CODE = "DWS_ML_MANUAL_IMPORT"
|
||
ML_TEMPLATE_RELATIVE_PATH = "docs/templates/ml_manual_ledger_template.xlsx"
|
||
|
||
# 信号
|
||
task_started = Signal(str)
|
||
task_finished = Signal(bool, str)
|
||
log_message = Signal(str)
|
||
add_to_queue = Signal(object) # TaskConfig
|
||
create_schedule = Signal(str, list, dict)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.cli_builder = CLIBuilder()
|
||
self._init_ui()
|
||
self._connect_signals()
|
||
self._load_settings()
|
||
|
||
# 定时器:每秒更新时间预览
|
||
self._time_preview_timer = QTimer(self)
|
||
self._time_preview_timer.timeout.connect(self._update_time_preview)
|
||
self._time_preview_timer.start(1000)
|
||
|
||
def _init_ui(self):
|
||
"""初始化界面"""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(16, 16, 16, 16)
|
||
layout.setSpacing(12)
|
||
|
||
# 标题
|
||
title = QLabel("ETL 分组配置")
|
||
title.setProperty("heading", True)
|
||
layout.addWidget(title)
|
||
|
||
# 任务分组标签页
|
||
self.task_tabs = QTabWidget()
|
||
self._init_update_tab()
|
||
self._init_build_tab()
|
||
layout.addWidget(self.task_tabs, 1)
|
||
|
||
# 通用选项
|
||
common_options = self._create_common_options()
|
||
layout.addWidget(common_options)
|
||
|
||
# 底部:CLI 预览和执行按钮
|
||
bottom_widget = self._create_bottom_area()
|
||
layout.addWidget(bottom_widget)
|
||
|
||
def _init_update_tab(self):
|
||
"""初始化数据更新选项卡"""
|
||
self.update_tab = QWidget()
|
||
update_layout = QVBoxLayout(self.update_tab)
|
||
update_layout.setContentsMargins(0, 0, 0, 0)
|
||
update_layout.setSpacing(12)
|
||
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setFrameShape(QFrame.NoFrame)
|
||
|
||
content_widget = QWidget()
|
||
content_layout = QVBoxLayout(content_widget)
|
||
content_layout.setContentsMargins(0, 0, 8, 0)
|
||
content_layout.setSpacing(12)
|
||
|
||
# 1. 管道选择组件
|
||
self.pipeline_selector = PipelineSelectorWidget()
|
||
content_layout.addWidget(self.pipeline_selector)
|
||
|
||
# 2. 高级选项(折叠)
|
||
self.advanced_section = CollapsibleSection("高级选项 - 任务分组与参数")
|
||
self._create_update_advanced_content()
|
||
content_layout.addWidget(self.advanced_section)
|
||
|
||
content_layout.addStretch()
|
||
scroll_area.setWidget(content_widget)
|
||
update_layout.addWidget(scroll_area, 1)
|
||
|
||
self.task_tabs.addTab(self.update_tab, "数据更新")
|
||
|
||
def _init_build_tab(self):
|
||
"""初始化数据建设选项卡"""
|
||
self.build_tab = QWidget()
|
||
build_layout = QVBoxLayout(self.build_tab)
|
||
build_layout.setContentsMargins(0, 0, 0, 0)
|
||
build_layout.setSpacing(12)
|
||
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setFrameShape(QFrame.NoFrame)
|
||
|
||
content_widget = QWidget()
|
||
content_layout = QVBoxLayout(content_widget)
|
||
content_layout.setContentsMargins(0, 0, 8, 0)
|
||
content_layout.setSpacing(12)
|
||
|
||
# 数据建设任务分组
|
||
self.build_task_checks: Dict[str, QCheckBox] = {}
|
||
schema_tasks = task_registry.get_tasks_by_domain(BusinessDomain.SCHEMA)
|
||
quality_tasks = task_registry.get_tasks_by_domain(BusinessDomain.QUALITY)
|
||
other_tasks = task_registry.get_tasks_by_domain(BusinessDomain.OTHER)
|
||
|
||
if schema_tasks:
|
||
content_layout.addWidget(
|
||
self._create_task_group("数据库初始化", schema_tasks, self.build_task_checks)
|
||
)
|
||
if quality_tasks:
|
||
content_layout.addWidget(
|
||
self._create_task_group("质检与校验", quality_tasks, self.build_task_checks)
|
||
)
|
||
if other_tasks:
|
||
content_layout.addWidget(
|
||
self._create_task_group("其他工具", other_tasks, self.build_task_checks)
|
||
)
|
||
|
||
# ML 人工台账导入(数据建设专用)
|
||
ml_import_task = task_registry.get_task(self.ML_IMPORT_TASK_CODE)
|
||
relation_task = task_registry.get_task("DWS_RELATION_INDEX")
|
||
if ml_import_task or relation_task:
|
||
content_layout.addWidget(
|
||
self._create_ml_import_group(ml_import_task, relation_task)
|
||
)
|
||
|
||
# 时间窗口设置(可选)
|
||
self.build_window_group = self._create_build_window_group()
|
||
content_layout.addWidget(self.build_window_group)
|
||
|
||
content_layout.addStretch()
|
||
scroll_area.setWidget(content_widget)
|
||
build_layout.addWidget(scroll_area, 1)
|
||
|
||
self.task_tabs.addTab(self.build_tab, "数据建设")
|
||
|
||
def _create_common_options(self) -> QWidget:
|
||
"""创建通用选项区"""
|
||
group = QGroupBox("通用选项")
|
||
layout = QGridLayout(group)
|
||
|
||
self.dry_run_check = QCheckBox("Dry-run 模式(不提交数据库)")
|
||
layout.addWidget(self.dry_run_check, 0, 0, 1, 2)
|
||
|
||
layout.addWidget(QLabel("JSON 数据目录:"), 1, 0)
|
||
self.ingest_source_edit = QLineEdit()
|
||
self.ingest_source_edit.setPlaceholderText("可选,用于 INGEST_ONLY / MANUAL_INGEST")
|
||
layout.addWidget(self.ingest_source_edit, 1, 1)
|
||
|
||
self.browse_btn = QPushButton("浏览...")
|
||
self.browse_btn.setProperty("secondary", True)
|
||
self.browse_btn.setFixedWidth(80)
|
||
layout.addWidget(self.browse_btn, 1, 2)
|
||
|
||
return group
|
||
|
||
def _create_update_advanced_content(self):
|
||
"""创建数据更新高级选项内容"""
|
||
adv_layout = self.advanced_section.content_layout()
|
||
|
||
# ODS 表选择(当管道包含 ODS 时可用)
|
||
self.ods_group = QGroupBox("ODS 表选择")
|
||
ods_layout = QVBoxLayout(self.ods_group)
|
||
|
||
ods_desc = QLabel("选择要处理的 ODS 表(默认全选)")
|
||
ods_desc.setStyleSheet("color: #666;")
|
||
ods_layout.addWidget(ods_desc)
|
||
|
||
self.ods_task_selector = TaskSelectorWidget(
|
||
show_dimensions=True,
|
||
show_facts=True,
|
||
default_select_facts=True,
|
||
default_select_dimensions=True,
|
||
compact=True,
|
||
max_height=0,
|
||
)
|
||
ods_layout.addWidget(self.ods_task_selector)
|
||
adv_layout.addWidget(self.ods_group)
|
||
|
||
# DWD 表选择(按业务域分组,类似 ODS 选择器)
|
||
self.dwd_tables_group = QGroupBox("DWD 装载表选择")
|
||
dwd_group_layout = QVBoxLayout(self.dwd_tables_group)
|
||
|
||
dwd_desc = QLabel("选择要装载的 DWD 表(默认全选)")
|
||
dwd_desc.setStyleSheet("color: #666;")
|
||
dwd_group_layout.addWidget(dwd_desc)
|
||
|
||
self.dwd_table_selector = DwdTableSelectorWidget()
|
||
dwd_group_layout.addWidget(self.dwd_table_selector)
|
||
|
||
adv_layout.addWidget(self.dwd_tables_group)
|
||
|
||
# DWS 汇总任务选择
|
||
self.dws_task_checks: Dict[str, QCheckBox] = {}
|
||
dws_tasks = task_registry.get_tasks_by_domain(BusinessDomain.DWS)
|
||
self.dws_tasks_group = self._create_task_group(
|
||
"DWS 汇总任务",
|
||
dws_tasks,
|
||
self.dws_task_checks,
|
||
default_checked={"DWS_BUILD_ORDER_SUMMARY"},
|
||
)
|
||
adv_layout.addWidget(self.dws_tasks_group)
|
||
|
||
# 指数任务选择
|
||
self.index_group = QGroupBox("DWS 指数任务")
|
||
index_layout = QVBoxLayout(self.index_group)
|
||
self.index_task_checks: Dict[str, QCheckBox] = {}
|
||
self.index_task_order: List[str] = []
|
||
|
||
index_tasks = [
|
||
task for task in task_registry.get_tasks_by_domain(BusinessDomain.INDEX)
|
||
if task.code not in {"DWS_RECALL_INDEX", self.ML_IMPORT_TASK_CODE}
|
||
]
|
||
default_index_tasks = {"DWS_WINBACK_INDEX", "DWS_NEWCONV_INDEX", "DWS_RELATION_INDEX"}
|
||
self._attach_select_buttons(index_layout, self.index_task_checks)
|
||
for task in index_tasks:
|
||
checkbox = QCheckBox(task.name)
|
||
checkbox.setToolTip(f"{task.code}: {task.description}")
|
||
checkbox.setProperty("task_code", task.code)
|
||
checkbox.setChecked(task.code in default_index_tasks)
|
||
checkbox.stateChanged.connect(self._update_preview)
|
||
checkbox.stateChanged.connect(self._save_settings)
|
||
self.index_task_checks[task.code] = checkbox
|
||
self.index_task_order.append(task.code)
|
||
if task.code == "DWS_WINBACK_INDEX":
|
||
self.index_winback_check = checkbox
|
||
elif task.code == "DWS_NEWCONV_INDEX":
|
||
self.index_newconv_check = checkbox
|
||
elif task.code == "DWS_INTIMACY_INDEX":
|
||
self.index_intimacy_check = checkbox
|
||
elif task.code == "DWS_RELATION_INDEX":
|
||
self.index_relation_check = checkbox
|
||
elif task.code == "DWS_RECALL_INDEX":
|
||
self.index_recall_check = checkbox
|
||
index_layout.addWidget(checkbox)
|
||
|
||
# 指数回溯天数
|
||
index_params = QHBoxLayout()
|
||
index_params.addWidget(QLabel("回溯天数:"))
|
||
self.index_lookback_days = QSpinBox()
|
||
self.index_lookback_days.setRange(7, 180)
|
||
self.index_lookback_days.setValue(60)
|
||
self.index_lookback_days.setSuffix(" 天")
|
||
index_params.addWidget(self.index_lookback_days)
|
||
index_params.addStretch()
|
||
index_layout.addLayout(index_params)
|
||
|
||
adv_layout.addWidget(self.index_group)
|
||
|
||
# 初始化可见性
|
||
self._update_advanced_visibility()
|
||
|
||
def _create_task_group(
|
||
self,
|
||
title: str,
|
||
tasks: List[TaskDefinition],
|
||
checkbox_map: Dict[str, QCheckBox],
|
||
default_checked: Optional[Set[str]] = None,
|
||
) -> QGroupBox:
|
||
"""创建任务复选框分组"""
|
||
group_box = QGroupBox(title)
|
||
group_layout = QVBoxLayout(group_box)
|
||
group_layout.setContentsMargins(8, 4, 8, 4)
|
||
group_layout.setSpacing(4)
|
||
|
||
self._attach_select_buttons(group_layout, checkbox_map)
|
||
for task in tasks:
|
||
checkbox = QCheckBox(task.name)
|
||
checkbox.setToolTip(f"{task.code}: {task.description}")
|
||
checkbox.setProperty("task_code", task.code)
|
||
if default_checked is not None:
|
||
checkbox.setChecked(task.code in default_checked)
|
||
checkbox.stateChanged.connect(self._update_preview)
|
||
checkbox.stateChanged.connect(self._save_settings)
|
||
checkbox_map[task.code] = checkbox
|
||
group_layout.addWidget(checkbox)
|
||
|
||
return group_box
|
||
|
||
def _attach_select_buttons(self, layout: QVBoxLayout, checkbox_map: Dict[str, QCheckBox]):
|
||
"""为任务分组添加全选/全不选按钮"""
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.setSpacing(8)
|
||
|
||
select_all_btn = QPushButton("全选")
|
||
select_all_btn.setProperty("secondary", True)
|
||
select_all_btn.setFixedWidth(60)
|
||
select_all_btn.clicked.connect(lambda: self._set_all_checked(checkbox_map, True))
|
||
|
||
deselect_all_btn = QPushButton("全不选")
|
||
deselect_all_btn.setProperty("secondary", True)
|
||
deselect_all_btn.setFixedWidth(60)
|
||
deselect_all_btn.clicked.connect(lambda: self._set_all_checked(checkbox_map, False))
|
||
|
||
btn_layout.addWidget(select_all_btn)
|
||
btn_layout.addWidget(deselect_all_btn)
|
||
btn_layout.addStretch()
|
||
layout.addLayout(btn_layout)
|
||
|
||
def _create_build_window_group(self) -> QGroupBox:
|
||
"""创建数据建设时间窗口设置"""
|
||
group = QGroupBox("时间窗口(可选)")
|
||
layout = QVBoxLayout(group)
|
||
layout.setSpacing(8)
|
||
|
||
self.build_window_button_group = QButtonGroup(self)
|
||
|
||
lookback_layout = QHBoxLayout()
|
||
self.build_lookback_radio = QRadioButton("回溯:")
|
||
self.build_lookback_radio.setProperty("mode_id", "lookback")
|
||
self.build_lookback_radio.setChecked(True)
|
||
self.build_window_button_group.addButton(self.build_lookback_radio, 0)
|
||
lookback_layout.addWidget(self.build_lookback_radio)
|
||
|
||
self.build_lookback_hours = QSpinBox()
|
||
self.build_lookback_hours.setRange(1, 720)
|
||
self.build_lookback_hours.setValue(24)
|
||
self.build_lookback_hours.setSuffix(" 小时")
|
||
self.build_lookback_hours.setToolTip("回溯时间长度")
|
||
self.build_lookback_hours.setFixedWidth(110)
|
||
lookback_layout.addWidget(self.build_lookback_hours)
|
||
lookback_layout.addStretch()
|
||
layout.addLayout(lookback_layout)
|
||
|
||
custom_layout = QHBoxLayout()
|
||
self.build_custom_radio = QRadioButton("自定义:")
|
||
self.build_custom_radio.setProperty("mode_id", "custom")
|
||
self.build_window_button_group.addButton(self.build_custom_radio, 1)
|
||
custom_layout.addWidget(self.build_custom_radio)
|
||
|
||
self.build_start_datetime = QDateTimeEdit()
|
||
self.build_start_datetime.setCalendarPopup(True)
|
||
self.build_start_datetime.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
||
self.build_start_datetime.setDateTime(QDateTime.currentDateTime().addDays(-1))
|
||
custom_layout.addWidget(self.build_start_datetime)
|
||
|
||
custom_layout.addWidget(QLabel("~"))
|
||
|
||
self.build_end_datetime = QDateTimeEdit()
|
||
self.build_end_datetime.setCalendarPopup(True)
|
||
self.build_end_datetime.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
||
self.build_end_datetime.setDateTime(QDateTime.currentDateTime())
|
||
custom_layout.addWidget(self.build_end_datetime)
|
||
|
||
custom_layout.addStretch()
|
||
layout.addLayout(custom_layout)
|
||
|
||
# 时间窗口切分
|
||
split_layout = QHBoxLayout()
|
||
split_layout.addWidget(QLabel("时间窗口切分:"))
|
||
self.build_split_combo = QComboBox()
|
||
for split_id, split_name in WINDOW_SPLIT_OPTIONS:
|
||
self.build_split_combo.addItem(split_name, split_id)
|
||
default_split_index = self.build_split_combo.findData("day")
|
||
if default_split_index >= 0:
|
||
self.build_split_combo.setCurrentIndex(default_split_index)
|
||
self.build_split_combo.setFixedWidth(110)
|
||
split_layout.addWidget(self.build_split_combo)
|
||
|
||
split_layout.addWidget(QLabel("切分天数:"))
|
||
self.build_split_days_combo = QComboBox()
|
||
for days, label in WINDOW_SPLIT_DAY_OPTIONS:
|
||
self.build_split_days_combo.addItem(label, days)
|
||
default_days_index = self.build_split_days_combo.findData(10)
|
||
if default_days_index >= 0:
|
||
self.build_split_days_combo.setCurrentIndex(default_days_index)
|
||
self.build_split_days_combo.setFixedWidth(90)
|
||
split_layout.addWidget(self.build_split_days_combo)
|
||
|
||
split_layout.addStretch()
|
||
layout.addLayout(split_layout)
|
||
|
||
self._update_build_window_controls()
|
||
return group
|
||
|
||
def _create_ml_import_group(
|
||
self,
|
||
ml_import_task: Optional[TaskDefinition],
|
||
relation_task: Optional[TaskDefinition],
|
||
) -> QGroupBox:
|
||
"""创建 ML 台账导入与关系指数重算区域。"""
|
||
group = QGroupBox("ML人工台账导入")
|
||
layout = QVBoxLayout(group)
|
||
layout.setSpacing(8)
|
||
|
||
desc = QLabel(
|
||
"先导入人工台账,再执行关系指数(RS/OS/MS/ML)。\n"
|
||
"覆盖策略:30天内按日覆盖,超过30天按固定纪元30天批次覆盖。"
|
||
)
|
||
desc.setStyleSheet("color: #666;")
|
||
layout.addWidget(desc)
|
||
|
||
if ml_import_task:
|
||
self.ml_manual_import_check = QCheckBox(ml_import_task.name)
|
||
self.ml_manual_import_check.setToolTip(
|
||
f"{ml_import_task.code}: {ml_import_task.description}"
|
||
)
|
||
self.ml_manual_import_check.setChecked(False)
|
||
self.ml_manual_import_check.stateChanged.connect(self._update_preview)
|
||
self.ml_manual_import_check.stateChanged.connect(self._save_settings)
|
||
self.build_task_checks[ml_import_task.code] = self.ml_manual_import_check
|
||
layout.addWidget(self.ml_manual_import_check)
|
||
else:
|
||
self.ml_manual_import_check = None
|
||
|
||
if relation_task:
|
||
self.build_relation_index_check = QCheckBox(relation_task.name)
|
||
self.build_relation_index_check.setToolTip(
|
||
f"{relation_task.code}: {relation_task.description}"
|
||
)
|
||
self.build_relation_index_check.setChecked(True)
|
||
self.build_relation_index_check.stateChanged.connect(self._update_preview)
|
||
self.build_relation_index_check.stateChanged.connect(self._save_settings)
|
||
self.build_task_checks[relation_task.code] = self.build_relation_index_check
|
||
layout.addWidget(self.build_relation_index_check)
|
||
else:
|
||
self.build_relation_index_check = None
|
||
|
||
file_layout = QHBoxLayout()
|
||
file_layout.addWidget(QLabel("台账文件:"))
|
||
self.ml_manual_file_edit = QLineEdit()
|
||
self.ml_manual_file_edit.setPlaceholderText("请选择 .xlsx 台账文件(订单一行,最多5个助教)")
|
||
self.ml_manual_file_edit.textChanged.connect(self._update_preview)
|
||
self.ml_manual_file_edit.textChanged.connect(self._save_settings)
|
||
file_layout.addWidget(self.ml_manual_file_edit, 1)
|
||
|
||
self.ml_manual_file_btn = QPushButton("选择文件...")
|
||
self.ml_manual_file_btn.setProperty("secondary", True)
|
||
self.ml_manual_file_btn.clicked.connect(self._browse_ml_manual_file)
|
||
file_layout.addWidget(self.ml_manual_file_btn)
|
||
|
||
self.ml_manual_template_btn = QPushButton("下载模板")
|
||
self.ml_manual_template_btn.setProperty("secondary", True)
|
||
self.ml_manual_template_btn.clicked.connect(self._download_ml_template)
|
||
file_layout.addWidget(self.ml_manual_template_btn)
|
||
layout.addLayout(file_layout)
|
||
|
||
return group
|
||
|
||
def _update_build_window_controls(self):
|
||
"""更新数据建设时间窗口控件状态"""
|
||
is_lookback = self.build_lookback_radio.isChecked()
|
||
self.build_lookback_hours.setEnabled(is_lookback)
|
||
self.build_start_datetime.setEnabled(not is_lookback)
|
||
self.build_end_datetime.setEnabled(not is_lookback)
|
||
if hasattr(self, "build_split_days_combo") and hasattr(self, "build_split_combo"):
|
||
self.build_split_days_combo.setEnabled(self.build_split_combo.currentData() == "day")
|
||
|
||
def _get_selected_task_codes(self, checkbox_map: Dict[str, QCheckBox]) -> List[str]:
|
||
"""获取勾选的任务编码"""
|
||
return [code for code, checkbox in checkbox_map.items() if checkbox.isChecked()]
|
||
|
||
def _set_checked_codes(self, checkbox_map: Dict[str, QCheckBox], codes: List[str]):
|
||
"""设置勾选的任务编码"""
|
||
codes_set = set(codes or [])
|
||
for code, checkbox in checkbox_map.items():
|
||
checkbox.blockSignals(True)
|
||
checkbox.setChecked(code in codes_set)
|
||
checkbox.blockSignals(False)
|
||
|
||
def _set_all_checked(self, checkbox_map: Dict[str, QCheckBox], checked: bool):
|
||
"""设置全部勾选/取消"""
|
||
for checkbox in checkbox_map.values():
|
||
checkbox.blockSignals(True)
|
||
checkbox.setChecked(checked)
|
||
checkbox.blockSignals(False)
|
||
self._update_preview()
|
||
self._save_settings()
|
||
|
||
def _get_selected_index_tasks(self) -> List[str]:
|
||
"""获取勾选的指数任务编码"""
|
||
selected = []
|
||
for code in self.index_task_order:
|
||
checkbox = self.index_task_checks.get(code)
|
||
if checkbox and checkbox.isChecked():
|
||
selected.append(code)
|
||
return selected
|
||
|
||
def _get_build_window_strings(self) -> Tuple[str, str]:
|
||
"""获取数据建设窗口字符串"""
|
||
if self.build_lookback_radio.isChecked():
|
||
now = datetime.now()
|
||
start_time = now - timedelta(hours=self.build_lookback_hours.value())
|
||
return (
|
||
start_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||
now.strftime("%Y-%m-%d %H:%M:%S"),
|
||
)
|
||
|
||
start_str = self.build_start_datetime.dateTime().toString("yyyy-MM-dd HH:mm:ss")
|
||
end_str = self.build_end_datetime.dateTime().toString("yyyy-MM-dd HH:mm:ss")
|
||
return start_str, end_str
|
||
|
||
def _get_build_window_split(self) -> Tuple[str, Optional[int]]:
|
||
"""获取数据建设时间窗口切分配置"""
|
||
split_unit = "none"
|
||
split_days: Optional[int] = None
|
||
if hasattr(self, "build_split_combo"):
|
||
split_unit = self.build_split_combo.currentData()
|
||
if split_unit == "day" and hasattr(self, "build_split_days_combo"):
|
||
split_days = int(self.build_split_days_combo.currentData())
|
||
return split_unit, split_days
|
||
|
||
def _is_update_tab(self) -> bool:
|
||
"""是否数据更新选项卡"""
|
||
return self.task_tabs.currentIndex() == 0
|
||
|
||
def _is_build_tab(self) -> bool:
|
||
"""是否数据建设选项卡"""
|
||
return self.task_tabs.currentIndex() == 1
|
||
|
||
def _create_bottom_area(self) -> QWidget:
|
||
"""创建底部区域"""
|
||
widget = QWidget()
|
||
layout = QVBoxLayout(widget)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(8)
|
||
|
||
# 时间窗口预览
|
||
self.time_preview_label = QLabel()
|
||
self.time_preview_label.setStyleSheet("color: #666; font-size: 12px;")
|
||
layout.addWidget(self.time_preview_label)
|
||
self._update_time_preview()
|
||
|
||
# CLI 预览
|
||
preview_group = QGroupBox("命令行预览(可编辑)")
|
||
preview_layout = QVBoxLayout(preview_group)
|
||
|
||
self.cli_preview = QPlainTextEdit()
|
||
self.cli_preview.setMaximumHeight(100)
|
||
self.cli_preview.setFont(QFont("Consolas", 10))
|
||
self.cli_preview.setPlaceholderText("命令将在这里显示...")
|
||
preview_layout.addWidget(self.cli_preview)
|
||
|
||
layout.addWidget(preview_group)
|
||
|
||
# 执行按钮
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.addStretch()
|
||
|
||
self.run_btn = QPushButton("单次执行")
|
||
self.run_btn.setFixedWidth(120)
|
||
btn_layout.addWidget(self.run_btn)
|
||
|
||
self.schedule_btn = QPushButton("创建调度")
|
||
self.schedule_btn.setProperty("secondary", True)
|
||
self.schedule_btn.setFixedWidth(100)
|
||
btn_layout.addWidget(self.schedule_btn)
|
||
|
||
self.stop_btn = QPushButton("停止")
|
||
self.stop_btn.setProperty("danger", True)
|
||
self.stop_btn.setEnabled(False)
|
||
self.stop_btn.setFixedWidth(80)
|
||
btn_layout.addWidget(self.stop_btn)
|
||
|
||
layout.addLayout(btn_layout)
|
||
|
||
return widget
|
||
|
||
def _connect_signals(self):
|
||
"""连接信号"""
|
||
# 选项卡变化
|
||
self.task_tabs.currentChanged.connect(self._on_tab_changed)
|
||
|
||
# 管道选择变化
|
||
self.pipeline_selector.pipeline_changed.connect(self._on_pipeline_changed)
|
||
self.pipeline_selector.config_changed.connect(self._update_preview)
|
||
self.pipeline_selector.config_changed.connect(self._update_time_preview)
|
||
|
||
# 高级选项变化
|
||
self.ods_task_selector.selection_changed.connect(self._update_preview)
|
||
self.dwd_table_selector.selection_changed.connect(self._update_preview)
|
||
self.dwd_table_selector.selection_changed.connect(self._save_settings)
|
||
self.index_lookback_days.valueChanged.connect(self._update_preview)
|
||
self.dry_run_check.stateChanged.connect(self._update_preview)
|
||
self.ingest_source_edit.textChanged.connect(self._update_preview)
|
||
self.build_lookback_hours.valueChanged.connect(self._update_preview)
|
||
self.build_start_datetime.dateTimeChanged.connect(self._update_preview)
|
||
self.build_end_datetime.dateTimeChanged.connect(self._update_preview)
|
||
if hasattr(self, "build_split_combo"):
|
||
self.build_split_combo.currentIndexChanged.connect(self._on_build_window_split_changed)
|
||
if hasattr(self, "build_split_days_combo"):
|
||
self.build_split_days_combo.currentIndexChanged.connect(self._on_build_window_split_changed)
|
||
self.build_lookback_hours.valueChanged.connect(self._update_time_preview)
|
||
self.build_start_datetime.dateTimeChanged.connect(self._update_time_preview)
|
||
self.build_end_datetime.dateTimeChanged.connect(self._update_time_preview)
|
||
|
||
# 浏览目录
|
||
self.browse_btn.clicked.connect(self._browse_source_dir)
|
||
|
||
# 执行按钮
|
||
self.run_btn.clicked.connect(self._run_task)
|
||
self.schedule_btn.clicked.connect(self._create_schedule)
|
||
self.stop_btn.clicked.connect(self._stop_task)
|
||
|
||
# 保存设置
|
||
self.pipeline_selector.config_changed.connect(self._save_settings)
|
||
self.ods_task_selector.selection_changed.connect(self._save_settings)
|
||
self.index_lookback_days.valueChanged.connect(self._save_settings)
|
||
self.dry_run_check.stateChanged.connect(self._save_settings)
|
||
self.ingest_source_edit.textChanged.connect(self._save_settings)
|
||
self.build_lookback_hours.valueChanged.connect(self._save_settings)
|
||
self.build_start_datetime.dateTimeChanged.connect(self._save_settings)
|
||
self.build_end_datetime.dateTimeChanged.connect(self._save_settings)
|
||
if hasattr(self, "build_split_combo"):
|
||
self.build_split_combo.currentIndexChanged.connect(self._save_settings)
|
||
if hasattr(self, "build_split_days_combo"):
|
||
self.build_split_days_combo.currentIndexChanged.connect(self._save_settings)
|
||
|
||
self.build_lookback_radio.toggled.connect(self._on_build_window_mode_changed)
|
||
self.build_custom_radio.toggled.connect(self._on_build_window_mode_changed)
|
||
|
||
def _on_pipeline_changed(self, pipeline_id: str):
|
||
"""管道选择变化"""
|
||
self._update_advanced_visibility()
|
||
self._update_preview()
|
||
|
||
def _on_tab_changed(self, index: int):
|
||
"""选项卡切换"""
|
||
self._update_time_preview()
|
||
self._update_preview()
|
||
self._save_settings()
|
||
|
||
def _on_build_window_mode_changed(self):
|
||
"""数据建设窗口模式变化"""
|
||
self._update_build_window_controls()
|
||
self._update_time_preview()
|
||
self._update_preview()
|
||
self._save_settings()
|
||
|
||
def _on_build_window_split_changed(self):
|
||
"""数据建设窗口切分变化"""
|
||
self._update_build_window_controls()
|
||
self._update_preview()
|
||
|
||
def _update_advanced_visibility(self):
|
||
"""更新高级选项的可见性"""
|
||
layers = self.pipeline_selector.get_pipeline_layers()
|
||
|
||
self.ods_group.setVisible("ODS" in layers)
|
||
self.dwd_tables_group.setVisible("DWD" in layers)
|
||
self.dws_tasks_group.setVisible("DWS" in layers)
|
||
self.index_group.setVisible("INDEX" in layers)
|
||
|
||
def _update_time_preview(self):
|
||
"""更新时间窗口预览"""
|
||
if self._is_update_tab():
|
||
config = self.pipeline_selector.get_config()
|
||
mode = config.get("window_mode", "lookback")
|
||
|
||
if mode == "lookback":
|
||
now = datetime.now()
|
||
hours = config.get("lookback_hours", 24)
|
||
overlap = config.get("overlap_seconds", 600)
|
||
start_time = now - timedelta(hours=hours, seconds=overlap)
|
||
|
||
start_str = start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||
end_str = now.strftime("%Y-%m-%d %H:%M:%S")
|
||
self.time_preview_label.setText(f"时间窗口: {start_str} ~ {end_str}")
|
||
else:
|
||
start_str = config.get("window_start", "")
|
||
end_str = config.get("window_end", "")
|
||
self.time_preview_label.setText(f"时间窗口: {start_str} ~ {end_str}")
|
||
return
|
||
|
||
# 数据建设窗口预览
|
||
start_str, end_str = self._get_build_window_strings()
|
||
self.time_preview_label.setText(f"时间窗口: {start_str} ~ {end_str}")
|
||
|
||
def _browse_source_dir(self):
|
||
"""浏览数据源目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择 JSON 数据目录")
|
||
if dir_path:
|
||
self.ingest_source_edit.setText(dir_path)
|
||
|
||
def _browse_ml_manual_file(self):
|
||
"""选择 ML 人工台账文件。"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self,
|
||
"选择 ML 人工台账文件",
|
||
self.ml_manual_file_edit.text().strip() or "",
|
||
"Excel 文件 (*.xlsx)",
|
||
)
|
||
if file_path:
|
||
self.ml_manual_file_edit.setText(file_path)
|
||
|
||
def _resolve_project_root(self) -> Path:
|
||
"""解析 ETL 项目根目录。"""
|
||
candidates: List[Path] = []
|
||
if app_settings.etl_project_path:
|
||
candidates.append(Path(app_settings.etl_project_path))
|
||
candidates.extend(
|
||
[
|
||
Path.cwd() / "etl_billiards",
|
||
Path(__file__).resolve().parents[2],
|
||
]
|
||
)
|
||
for path in candidates:
|
||
if path and (path / "cli" / "main.py").exists():
|
||
return path
|
||
return Path.cwd()
|
||
|
||
def _download_ml_template(self):
|
||
"""下载(复制)ML 台账模板到用户指定位置。"""
|
||
project_root = self._resolve_project_root()
|
||
template_path = project_root / self.ML_TEMPLATE_RELATIVE_PATH
|
||
if not template_path.exists():
|
||
QMessageBox.warning(
|
||
self,
|
||
"提示",
|
||
f"未找到模板文件:{template_path}\n请先执行模板生成脚本。",
|
||
)
|
||
return
|
||
|
||
save_path, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"保存台账模板",
|
||
str(Path.home() / "ml_manual_ledger_template.xlsx"),
|
||
"Excel 文件 (*.xlsx)",
|
||
)
|
||
if not save_path:
|
||
return
|
||
|
||
try:
|
||
shutil.copyfile(template_path, save_path)
|
||
QMessageBox.information(self, "成功", f"模板已保存到:\n{save_path}")
|
||
except Exception as exc: # noqa: BLE001
|
||
QMessageBox.critical(self, "失败", f"模板保存失败:{exc}")
|
||
|
||
def _get_config(self) -> TaskConfig:
|
||
"""获取当前配置"""
|
||
if self._is_build_tab():
|
||
return self._get_build_config()
|
||
|
||
return self._get_update_config()
|
||
|
||
def _get_update_config(self) -> TaskConfig:
|
||
"""获取数据更新配置"""
|
||
pipeline_config = self.pipeline_selector.get_config()
|
||
layers = self.pipeline_selector.get_pipeline_layers()
|
||
|
||
# 收集任务列表
|
||
tasks: List[str] = []
|
||
|
||
if "ODS" in layers:
|
||
tasks.extend(self.ods_task_selector.get_selected_codes())
|
||
|
||
if "DWD" in layers:
|
||
tasks.append("DWD_LOAD_FROM_ODS")
|
||
|
||
if "DWS" in layers:
|
||
tasks.extend(self._get_selected_task_codes(self.dws_task_checks))
|
||
|
||
selected_index_tasks = []
|
||
if "INDEX" in layers:
|
||
selected_index_tasks = self._get_selected_index_tasks()
|
||
tasks.extend(selected_index_tasks)
|
||
|
||
# 构建时间窗口
|
||
window_mode = pipeline_config.get("window_mode", "lookback")
|
||
if window_mode == "lookback":
|
||
now = datetime.now()
|
||
hours = pipeline_config.get("lookback_hours", 24)
|
||
overlap = pipeline_config.get("overlap_seconds", 600)
|
||
start_time = now - timedelta(hours=hours, seconds=overlap)
|
||
window_start = start_time.strftime("%Y-%m-%d %H:%M:%S")
|
||
window_end = now.strftime("%Y-%m-%d %H:%M:%S")
|
||
else:
|
||
window_start = pipeline_config.get("window_start")
|
||
window_end = pipeline_config.get("window_end")
|
||
|
||
# 构建环境变量
|
||
env_vars = {}
|
||
split_unit = pipeline_config.get("window_split", "day")
|
||
split_days = pipeline_config.get("window_split_days") or 10
|
||
if split_unit:
|
||
env_vars["WINDOW_SPLIT_UNIT"] = split_unit
|
||
if split_unit == "day":
|
||
env_vars["WINDOW_SPLIT_DAYS"] = str(split_days)
|
||
|
||
if selected_index_tasks:
|
||
env_vars["INDEX_LOOKBACK_DAYS"] = str(self.index_lookback_days.value())
|
||
|
||
# DWD 表过滤(仅当未全选时传递,避免不必要的过滤)
|
||
if "DWD" in layers:
|
||
selected_dwd_tables = self.dwd_table_selector.get_selected_codes()
|
||
if not self.dwd_table_selector.is_all_selected():
|
||
env_vars["DWD_ONLY_TABLES"] = ",".join(selected_dwd_tables)
|
||
|
||
# 处理模式
|
||
processing_mode = pipeline_config.get("processing_mode", "increment_only")
|
||
if processing_mode == "increment_verify":
|
||
env_vars["ENABLE_POST_VERIFICATION"] = "1"
|
||
|
||
# 校验附加选项(通过环境变量覆盖配置)
|
||
skip_ods = pipeline_config.get("skip_ods_when_fetch_before_verify")
|
||
if skip_ods is not None:
|
||
env_vars["VERIFY_SKIP_ODS_ON_FETCH"] = "true" if skip_ods else "false"
|
||
use_local_json = pipeline_config.get("ods_use_local_json")
|
||
if use_local_json is not None:
|
||
env_vars["VERIFY_ODS_LOCAL_JSON"] = "true" if use_local_json else "false"
|
||
|
||
config = TaskConfig(
|
||
tasks=tasks,
|
||
pipeline_flow="FULL",
|
||
dry_run=self.dry_run_check.isChecked(),
|
||
window_start=window_start,
|
||
window_end=window_end,
|
||
window_split=split_unit,
|
||
window_split_days=split_days if split_unit == "day" else None,
|
||
ingest_source=self.ingest_source_edit.text().strip() or None,
|
||
env_vars=env_vars,
|
||
pipeline=pipeline_config.get("pipeline", "api_ods_dwd"),
|
||
processing_mode=processing_mode,
|
||
fetch_before_verify=pipeline_config.get("fetch_before_verify", False),
|
||
window_mode=window_mode,
|
||
lookback_hours=pipeline_config.get("lookback_hours", 24),
|
||
overlap_seconds=pipeline_config.get("overlap_seconds", 600),
|
||
)
|
||
|
||
return config
|
||
|
||
def _get_build_config(self) -> TaskConfig:
|
||
"""获取数据建设配置"""
|
||
tasks = self._get_selected_task_codes(self.build_task_checks)
|
||
window_start, window_end = self._get_build_window_strings()
|
||
env_vars: Dict[str, str] = {}
|
||
split_unit, split_days = self._get_build_window_split()
|
||
if split_unit:
|
||
env_vars["WINDOW_SPLIT_UNIT"] = split_unit
|
||
if split_unit == "day" and split_days:
|
||
env_vars["WINDOW_SPLIT_DAYS"] = str(split_days)
|
||
|
||
# ML 台账导入任务通过环境变量传递 Excel 路径
|
||
if self.ML_IMPORT_TASK_CODE in tasks:
|
||
ledger_file = self.ml_manual_file_edit.text().strip() if hasattr(self, "ml_manual_file_edit") else ""
|
||
if ledger_file:
|
||
env_vars["ML_MANUAL_LEDGER_FILE"] = ledger_file
|
||
|
||
config = TaskConfig(
|
||
tasks=tasks,
|
||
pipeline_flow="FULL",
|
||
dry_run=self.dry_run_check.isChecked(),
|
||
window_start=window_start,
|
||
window_end=window_end,
|
||
window_split=split_unit,
|
||
window_split_days=split_days if split_unit == "day" else None,
|
||
ingest_source=self.ingest_source_edit.text().strip() or None,
|
||
env_vars=env_vars,
|
||
pipeline="legacy",
|
||
)
|
||
|
||
return config
|
||
|
||
def _update_preview(self):
|
||
"""更新命令行预览"""
|
||
config = self._get_config()
|
||
cmd_str = self.cli_builder.build_command_string(config)
|
||
|
||
# 添加环境变量注释
|
||
if config.env_vars:
|
||
env_preview = "\n".join(f"# {k}={v}" for k, v in config.env_vars.items())
|
||
cmd_str = f"{cmd_str}\n\n# 环境变量:\n{env_preview}"
|
||
|
||
self.cli_preview.setPlainText(cmd_str)
|
||
|
||
def _run_task(self):
|
||
"""执行任务 - 通过任务管理器执行,以便在任务队列中显示"""
|
||
config = self._get_config()
|
||
|
||
if not config.tasks:
|
||
QMessageBox.warning(self, "提示", "当前配置没有可执行的任务")
|
||
return
|
||
|
||
# ML 台账导入前置校验:必须选择有效文件
|
||
if self._is_build_tab() and self.ML_IMPORT_TASK_CODE in config.tasks:
|
||
ledger_file = config.env_vars.get("ML_MANUAL_LEDGER_FILE", "").strip()
|
||
if not ledger_file:
|
||
QMessageBox.warning(self, "提示", "已勾选“ML人工台账导入”,请先选择台账文件")
|
||
return
|
||
if not Path(ledger_file).exists():
|
||
QMessageBox.warning(self, "提示", f"台账文件不存在:\n{ledger_file}")
|
||
return
|
||
|
||
# 获取管道名称
|
||
pipeline_name = "数据建设"
|
||
if self._is_update_tab():
|
||
pipeline_name = ""
|
||
for pid, name, _ in PIPELINE_OPTIONS:
|
||
if pid == config.pipeline:
|
||
pipeline_name = name
|
||
break
|
||
pipeline_name = pipeline_name or config.pipeline
|
||
|
||
# 通过 add_to_queue 信号将任务添加到任务管理器的队列中
|
||
# 主窗口会自动切换到任务管理面板并开始执行
|
||
self.add_to_queue.emit(config)
|
||
|
||
self.log_message.emit(f"[GUI] 任务已添加到队列: {pipeline_name} ({len(config.tasks)}个任务)")
|
||
|
||
def _create_schedule(self):
|
||
"""创建调度任务"""
|
||
config = self._get_config()
|
||
|
||
if not config.tasks:
|
||
QMessageBox.warning(self, "提示", "当前配置没有可执行的任务")
|
||
return
|
||
|
||
if self._is_update_tab():
|
||
pipeline_config = self.pipeline_selector.get_config()
|
||
pipeline_name = ""
|
||
for pid, name, _ in PIPELINE_OPTIONS:
|
||
if pid == config.pipeline:
|
||
pipeline_name = name
|
||
break
|
||
pipeline_name = pipeline_name or config.pipeline
|
||
|
||
task_config = {
|
||
"pipeline": config.pipeline,
|
||
"processing_mode": config.processing_mode,
|
||
"window_mode": config.window_mode,
|
||
"lookback_hours": config.lookback_hours,
|
||
"overlap_seconds": config.overlap_seconds,
|
||
"window_split": config.window_split,
|
||
"window_split_days": config.window_split_days,
|
||
"skip_ods_when_fetch_before_verify": pipeline_config.get("skip_ods_when_fetch_before_verify"),
|
||
"ods_use_local_json": pipeline_config.get("ods_use_local_json"),
|
||
"pipeline_flow": "FULL",
|
||
}
|
||
|
||
schedule_name = f"调度: {pipeline_name}"
|
||
schedule_desc = f"管道: {pipeline_name}"
|
||
else:
|
||
lookback_hours = self.build_lookback_hours.value()
|
||
if self.build_custom_radio.isChecked():
|
||
start_sec = self.build_start_datetime.dateTime().toSecsSinceEpoch()
|
||
end_sec = self.build_end_datetime.dateTime().toSecsSinceEpoch()
|
||
if end_sec > start_sec:
|
||
lookback_hours = max(1, int((end_sec - start_sec) // 3600))
|
||
split_unit, split_days = self._get_build_window_split()
|
||
task_config = {
|
||
"pipeline_flow": "FULL",
|
||
"lookback_hours": lookback_hours,
|
||
"window_split": split_unit,
|
||
"window_split_days": split_days,
|
||
}
|
||
schedule_name = "调度: 数据建设"
|
||
schedule_desc = "类型: 数据建设"
|
||
|
||
self.create_schedule.emit(schedule_name, config.tasks, task_config)
|
||
|
||
self.log_message.emit(f"[GUI] 创建调度任务: {schedule_name}")
|
||
QMessageBox.information(
|
||
self, "提示",
|
||
f"已创建调度任务\n\n{schedule_desc}\n任务数: {len(config.tasks)}"
|
||
)
|
||
|
||
def _stop_task(self):
|
||
"""停止任务 - 已委托给任务管理器,此方法保留兼容性"""
|
||
self.log_message.emit("[GUI] 请在「任务管理」页面停止任务")
|
||
QMessageBox.information(self, "提示", "请切换到「任务管理」页面停止任务")
|
||
|
||
def is_running(self) -> bool:
|
||
"""是否正在执行任务 - 现在任务由任务管理器执行"""
|
||
# 任务已委托给任务管理器,此处总是返回 False
|
||
# 主窗口会通过任务管理器检查任务状态
|
||
return False
|
||
|
||
# ==================== 设置持久化 ====================
|
||
|
||
def _load_settings(self):
|
||
"""从持久化存储加载设置"""
|
||
try:
|
||
# 加载管道配置
|
||
if hasattr(app_settings, 'unified_pipeline'):
|
||
self.pipeline_selector.set_pipeline_id(app_settings.unified_pipeline)
|
||
if hasattr(app_settings, 'unified_processing_mode'):
|
||
self.pipeline_selector.set_processing_mode(app_settings.unified_processing_mode)
|
||
if hasattr(app_settings, 'unified_fetch_before_verify'):
|
||
self.pipeline_selector.set_fetch_before_verify(app_settings.unified_fetch_before_verify)
|
||
if hasattr(app_settings, 'unified_skip_ods_when_fetch_before_verify'):
|
||
self.pipeline_selector.set_skip_ods_when_fetch_before_verify(
|
||
app_settings.unified_skip_ods_when_fetch_before_verify
|
||
)
|
||
if hasattr(app_settings, 'unified_ods_use_local_json'):
|
||
self.pipeline_selector.set_ods_use_local_json(
|
||
app_settings.unified_ods_use_local_json
|
||
)
|
||
if hasattr(app_settings, 'unified_window_mode'):
|
||
self.pipeline_selector.set_window_mode(app_settings.unified_window_mode)
|
||
if hasattr(app_settings, 'unified_lookback_hours'):
|
||
self.pipeline_selector.set_lookback_hours(app_settings.unified_lookback_hours)
|
||
if hasattr(app_settings, 'unified_overlap_seconds'):
|
||
self.pipeline_selector.set_overlap_seconds(app_settings.unified_overlap_seconds)
|
||
if hasattr(app_settings, 'unified_window_split'):
|
||
self.pipeline_selector.set_window_split(app_settings.unified_window_split)
|
||
if hasattr(app_settings, 'unified_window_split_days'):
|
||
self.pipeline_selector.set_window_split_days(app_settings.unified_window_split_days)
|
||
|
||
# 加载 ODS 任务选择
|
||
if hasattr(app_settings, 'unified_ods_tasks'):
|
||
saved_tasks = app_settings.unified_ods_tasks
|
||
if saved_tasks:
|
||
self.ods_task_selector.set_selected_codes(saved_tasks)
|
||
|
||
# 加载 DWD 表选择
|
||
if hasattr(app_settings, 'unified_dwd_tasks'):
|
||
saved_dwd = app_settings.unified_dwd_tasks
|
||
if saved_dwd:
|
||
self.dwd_table_selector.set_selected_codes(saved_dwd)
|
||
|
||
# 加载 DWS 任务选择
|
||
if hasattr(app_settings, 'unified_dws_tasks'):
|
||
saved_dws = app_settings.unified_dws_tasks
|
||
if saved_dws:
|
||
self._set_checked_codes(self.dws_task_checks, saved_dws)
|
||
|
||
# 加载指数设置
|
||
if hasattr(app_settings, 'index_winback_check'):
|
||
self.index_winback_check.setChecked(app_settings.index_winback_check)
|
||
elif hasattr(app_settings, 'index_recall_check'):
|
||
# 兼容旧设置
|
||
self.index_winback_check.setChecked(app_settings.index_recall_check)
|
||
if hasattr(app_settings, 'index_newconv_check'):
|
||
self.index_newconv_check.setChecked(app_settings.index_newconv_check)
|
||
if hasattr(app_settings, 'index_intimacy_check'):
|
||
self.index_intimacy_check.setChecked(app_settings.index_intimacy_check)
|
||
if hasattr(app_settings, 'index_relation_check') and hasattr(self, 'index_relation_check'):
|
||
self.index_relation_check.setChecked(app_settings.index_relation_check)
|
||
if hasattr(app_settings, 'index_recall_check') and hasattr(self, 'index_recall_check'):
|
||
self.index_recall_check.setChecked(app_settings.index_recall_check)
|
||
if hasattr(app_settings, 'index_lookback_days'):
|
||
self.index_lookback_days.setValue(app_settings.index_lookback_days)
|
||
|
||
# 加载数据建设任务选择
|
||
if hasattr(app_settings, 'build_tasks'):
|
||
build_tasks = app_settings.build_tasks
|
||
if build_tasks:
|
||
self._set_checked_codes(self.build_task_checks, build_tasks)
|
||
if hasattr(app_settings, 'ml_manual_file_path') and hasattr(self, "ml_manual_file_edit"):
|
||
self.ml_manual_file_edit.setText(app_settings.ml_manual_file_path)
|
||
|
||
# 加载数据建设窗口配置
|
||
if hasattr(app_settings, 'build_window_mode'):
|
||
if app_settings.build_window_mode == "custom":
|
||
self.build_custom_radio.setChecked(True)
|
||
else:
|
||
self.build_lookback_radio.setChecked(True)
|
||
if hasattr(app_settings, 'build_lookback_hours'):
|
||
self.build_lookback_hours.setValue(app_settings.build_lookback_hours)
|
||
if hasattr(app_settings, 'build_window_start'):
|
||
dt = QDateTime.fromString(app_settings.build_window_start, "yyyy-MM-dd HH:mm:ss")
|
||
if dt.isValid():
|
||
self.build_start_datetime.setDateTime(dt)
|
||
if hasattr(app_settings, 'build_window_end'):
|
||
dt = QDateTime.fromString(app_settings.build_window_end, "yyyy-MM-dd HH:mm:ss")
|
||
if dt.isValid():
|
||
self.build_end_datetime.setDateTime(dt)
|
||
if hasattr(app_settings, 'build_window_split') and hasattr(self, "build_split_combo"):
|
||
index = self.build_split_combo.findData(app_settings.build_window_split)
|
||
if index >= 0:
|
||
self.build_split_combo.setCurrentIndex(index)
|
||
if hasattr(app_settings, 'build_window_split_days') and hasattr(self, "build_split_days_combo"):
|
||
index = self.build_split_days_combo.findData(app_settings.build_window_split_days)
|
||
if index >= 0:
|
||
self.build_split_days_combo.setCurrentIndex(index)
|
||
|
||
# 恢复选项卡
|
||
if hasattr(app_settings, 'task_panel_tab'):
|
||
tab_idx = app_settings.task_panel_tab
|
||
if 0 <= tab_idx < self.task_tabs.count():
|
||
self.task_tabs.setCurrentIndex(tab_idx)
|
||
|
||
# 更新可见性
|
||
self._update_advanced_visibility()
|
||
self._update_preview()
|
||
self._update_time_preview()
|
||
|
||
except Exception as e:
|
||
self.log_message.emit(f"[GUI] 加载设置失败: {e}")
|
||
|
||
def _save_settings(self):
|
||
"""保存设置到持久化存储"""
|
||
try:
|
||
config = self.pipeline_selector.get_config()
|
||
|
||
app_settings.unified_pipeline = config.get("pipeline", "api_ods_dwd")
|
||
app_settings.unified_processing_mode = config.get("processing_mode", "increment_only")
|
||
app_settings.unified_fetch_before_verify = config.get("fetch_before_verify", False)
|
||
app_settings.unified_skip_ods_when_fetch_before_verify = config.get(
|
||
"skip_ods_when_fetch_before_verify", True
|
||
)
|
||
app_settings.unified_ods_use_local_json = config.get(
|
||
"ods_use_local_json", True
|
||
)
|
||
app_settings.unified_window_mode = config.get("window_mode", "lookback")
|
||
app_settings.unified_lookback_hours = config.get("lookback_hours", 24)
|
||
app_settings.unified_overlap_seconds = config.get("overlap_seconds", 600)
|
||
app_settings.unified_window_split = config.get("window_split", "day")
|
||
app_settings.unified_window_split_days = config.get("window_split_days") or 10
|
||
app_settings.unified_ods_tasks = self.ods_task_selector.get_selected_codes()
|
||
app_settings.unified_dwd_tasks = self.dwd_table_selector.get_selected_codes()
|
||
app_settings.unified_dws_tasks = self._get_selected_task_codes(self.dws_task_checks)
|
||
app_settings.index_winback_check = self.index_winback_check.isChecked()
|
||
app_settings.index_newconv_check = self.index_newconv_check.isChecked()
|
||
app_settings.index_intimacy_check = self.index_intimacy_check.isChecked()
|
||
if hasattr(self, 'index_relation_check'):
|
||
app_settings.index_relation_check = self.index_relation_check.isChecked()
|
||
if hasattr(self, 'index_recall_check'):
|
||
app_settings.index_recall_check = self.index_recall_check.isChecked()
|
||
app_settings.index_lookback_days = self.index_lookback_days.value()
|
||
|
||
app_settings.build_tasks = self._get_selected_task_codes(self.build_task_checks)
|
||
app_settings.build_window_mode = "custom" if self.build_custom_radio.isChecked() else "lookback"
|
||
app_settings.build_lookback_hours = self.build_lookback_hours.value()
|
||
app_settings.build_window_start = self.build_start_datetime.dateTime().toString("yyyy-MM-dd HH:mm:ss")
|
||
app_settings.build_window_end = self.build_end_datetime.dateTime().toString("yyyy-MM-dd HH:mm:ss")
|
||
if hasattr(self, "build_split_combo"):
|
||
app_settings.build_window_split = self.build_split_combo.currentData()
|
||
if hasattr(self, "build_split_days_combo"):
|
||
app_settings.build_window_split_days = int(self.build_split_days_combo.currentData()) or 10
|
||
if hasattr(self, "ml_manual_file_edit"):
|
||
app_settings.ml_manual_file_path = self.ml_manual_file_edit.text().strip()
|
||
app_settings.task_panel_tab = self.task_tabs.currentIndex()
|
||
|
||
except Exception as e:
|
||
self.log_message.emit(f"[GUI] 保存设置失败: {e}")
|
||
|
||
# ==================== 兼容性方法 ====================
|
||
|
||
def refresh_tasks(self):
|
||
"""刷新任务列表(兼容性方法)"""
|
||
pass
|