mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
feat(preproc): 新增适用于重复性定时任务的预处理脚本以及可视化编排对话框
This commit is contained in:
@@ -18,6 +18,7 @@ from PySide6.QtCore import (
|
||||
from base.MsgBase import MsgBase
|
||||
from operators.AutoLib import AutoLib
|
||||
from utils.JSONReader import JSONReader
|
||||
from utils.PreprocEngine import PreprocEngine
|
||||
|
||||
|
||||
class AutoLibWorker(MsgBase, QThread):
|
||||
@@ -76,25 +77,28 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}",
|
||||
no_log=True
|
||||
)
|
||||
self.__run_config = JSONReader(self.__config_paths["run"]).data()
|
||||
self._run_config = JSONReader(self.__config_paths["run"]).data()
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}",
|
||||
no_log=True
|
||||
)
|
||||
self.__user_config = JSONReader(self.__config_paths["user"]).data()
|
||||
if self.__run_config is None or self.__user_config is None:
|
||||
self._user_config = JSONReader(self.__config_paths["user"]).data()
|
||||
if self._run_config is None or self._user_config is None:
|
||||
self._showTrace(
|
||||
"配置文件加载失败, 请检查配置文件是否正确",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
return False
|
||||
if not self.__user_config.get("groups"):
|
||||
if not self._user_config.get("groups"):
|
||||
self._showTrace(
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return False
|
||||
self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO)
|
||||
self._showLog(
|
||||
f"配置文件加载成功, 任务组数量: {len(self._user_config.get('groups', []))}",
|
||||
self.TraceLevel.INFO
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -115,9 +119,9 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
auto_lib = AutoLib(
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self.__run_config
|
||||
self._run_config
|
||||
)
|
||||
groups = self.__user_config.get("groups")
|
||||
groups = self._user_config.get("groups")
|
||||
for group in groups:
|
||||
if not group["enabled"]:
|
||||
self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True)
|
||||
@@ -162,7 +166,76 @@ class TimerTaskWorker(AutoLibWorker):
|
||||
):
|
||||
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 开始运行")
|
||||
super().run()
|
||||
if not self.checkTimeAvailable() or not self.checkConfigPaths():
|
||||
self._showTrace("定时任务跳过执行 (时间或配置文件检查未通过)")
|
||||
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||
return
|
||||
try:
|
||||
if not self.loadConfigs():
|
||||
raise Exception("配置文件加载失败")
|
||||
self._applyRepeatPreproc()
|
||||
auto_lib = AutoLib(
|
||||
self._input_queue,
|
||||
self._output_queue,
|
||||
self._run_config
|
||||
)
|
||||
groups = self._user_config.get("groups")
|
||||
for group in groups:
|
||||
if not group["enabled"]:
|
||||
self._showTrace(
|
||||
f"任务组 {group['name']} 已跳过",
|
||||
no_log=True
|
||||
)
|
||||
continue
|
||||
self._showTrace(
|
||||
f"正在运行任务组 {group['name']}",
|
||||
no_log=True
|
||||
)
|
||||
auto_lib.run(
|
||||
{"users": group.get("users", [])}
|
||||
)
|
||||
auto_lib.close()
|
||||
except Exception as e:
|
||||
self._showTrace(
|
||||
f"定时任务 {self.__timer_task['name']} 运行时发生异常: {e}",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
|
||||
return
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行结束")
|
||||
self.timerTaskWorkerIsFinished.emit(False, self.__timer_task)
|
||||
|
||||
|
||||
def _applyRepeatPreproc(
|
||||
self
|
||||
):
|
||||
|
||||
preproc_script = self.__timer_task.get("repeat_preproc", "")
|
||||
if not preproc_script or not preproc_script.strip():
|
||||
return
|
||||
self._showTrace(
|
||||
f"检测到重复定时任务预处理脚本, 开始执行...",
|
||||
no_log=True
|
||||
)
|
||||
groups = self._user_config.get("groups", [])
|
||||
affected_count = 0
|
||||
for group in groups:
|
||||
if not group.get("enabled", False):
|
||||
continue
|
||||
for user in group.get("users", []):
|
||||
try:
|
||||
PreprocEngine.execute(preproc_script, user)
|
||||
affected_count += 1
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预处理脚本执行错误 (用户 {user['username']}): {e}",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
self._showLog(
|
||||
f"预处理脚本执行完毕, "
|
||||
f"影响 {affected_count} 个用户",
|
||||
self.TraceLevel.INFO
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def onTimerTaskFinishedWithError(
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
|
||||
from PySide6.QtGui import (
|
||||
QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QIcon
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
|
||||
QDialogButtonBox, QPushButton, QLabel, QApplication, QStyle
|
||||
)
|
||||
|
||||
|
||||
class ALScriptHighlighter(QSyntaxHighlighter):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
self._rules = []
|
||||
|
||||
keywordFmt = QTextCharFormat()
|
||||
keywordFmt.setForeground(QColor("#316BFF"))
|
||||
keywordFmt.setFontWeight(QFont.Weight.Bold)
|
||||
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
|
||||
"SET", "PASS", "THEN", ".TRUE.", ".FALSE."]:
|
||||
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
|
||||
self._rules.append((pattern, keywordFmt))
|
||||
|
||||
opFmt = QTextCharFormat()
|
||||
opFmt.setForeground(QColor("#9C27B0"))
|
||||
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
|
||||
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
|
||||
self._rules.append((op, opFmt))
|
||||
|
||||
varFmt = QTextCharFormat()
|
||||
varFmt.setForeground(QColor("#E65100"))
|
||||
for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
|
||||
"RESERVE_DATE", "USERNAME", "USER_ENABLE",
|
||||
"PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
|
||||
self._rules.append((r"\b" + var + r"\b", varFmt))
|
||||
|
||||
funcFmt = QTextCharFormat()
|
||||
funcFmt.setForeground(QColor("#2E7D32"))
|
||||
self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
|
||||
self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
|
||||
|
||||
strFmt = QTextCharFormat()
|
||||
strFmt.setForeground(QColor("#388E3C"))
|
||||
self._rules.append((r"'[^']*'", strFmt))
|
||||
|
||||
numFmt = QTextCharFormat()
|
||||
numFmt.setForeground(QColor("#D32F2F"))
|
||||
self._rules.append((r"\b\d+\b", numFmt))
|
||||
|
||||
commentFmt = QTextCharFormat()
|
||||
commentFmt.setForeground(QColor("#999999"))
|
||||
commentFmt.setFontItalic(True)
|
||||
self._rules.append((r"//[^\n]*", commentFmt))
|
||||
|
||||
def highlightBlock(
|
||||
self,
|
||||
text
|
||||
):
|
||||
|
||||
import re
|
||||
for pattern, fmt in self._rules:
|
||||
for match in re.finditer(pattern, text, re.IGNORECASE):
|
||||
start = match.start()
|
||||
length = match.end() - match.start()
|
||||
self.setFormat(start, length, fmt)
|
||||
|
||||
|
||||
class ALScriptPreviewDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
script: str = ""
|
||||
):
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.__fontSize = 13
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
|
||||
self._textEdit.setPlainText(script)
|
||||
self._highlighter = ALScriptHighlighter(
|
||||
self._textEdit.document()
|
||||
)
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowTitle("预处理脚本预览 - AutoLibrary")
|
||||
self.setMinimumSize(520, 360)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
toolbarLayout = QHBoxLayout()
|
||||
self._zoomInBtn = QPushButton("+")
|
||||
self._zoomInBtn.setFixedSize(30, 25)
|
||||
self._zoomOutBtn = QPushButton("-")
|
||||
self._zoomOutBtn.setFixedSize(30, 25)
|
||||
self._zoomResetBtn = QPushButton(
|
||||
QApplication.style().standardIcon(
|
||||
QStyle.StandardPixmap.SP_BrowserReload
|
||||
), ""
|
||||
)
|
||||
self._zoomResetBtn.setFixedSize(30, 25)
|
||||
self._zoomResetBtn.setToolTip("重置缩放")
|
||||
self._zoomLabel = QLabel(f"{self.__fontSize}px")
|
||||
self._zoomLabel.setFixedHeight(25)
|
||||
toolbarLayout.addWidget(self._zoomInBtn)
|
||||
toolbarLayout.addWidget(self._zoomOutBtn)
|
||||
toolbarLayout.addWidget(self._zoomResetBtn)
|
||||
toolbarLayout.addWidget(self._zoomLabel)
|
||||
toolbarLayout.addStretch()
|
||||
self._copyBtn = QPushButton(
|
||||
QApplication.style().standardIcon(
|
||||
QStyle.StandardPixmap.SP_FileDialogDetailedView
|
||||
), ""
|
||||
)
|
||||
self._copyBtn.setFixedSize(30, 25)
|
||||
self._copyBtn.setToolTip("复制脚本")
|
||||
toolbarLayout.addWidget(self._copyBtn)
|
||||
layout.addLayout(toolbarLayout)
|
||||
|
||||
self._textEdit = QPlainTextEdit(self)
|
||||
self._textEdit.setReadOnly(True)
|
||||
self._textEdit.setLineWrapMode(
|
||||
QPlainTextEdit.LineWrapMode.NoWrap
|
||||
)
|
||||
self._textEdit.setStyleSheet(
|
||||
"QPlainTextEdit {"
|
||||
" font-family: 'Courier New', 'Consolas', monospace;"
|
||||
" font-size: 13px;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._textEdit)
|
||||
|
||||
self._btnBox = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Close
|
||||
)
|
||||
self._btnBox.button(
|
||||
QDialogButtonBox.StandardButton.Close
|
||||
).setText("关闭")
|
||||
layout.addWidget(self._btnBox)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self._btnBox.rejected.connect(self.reject)
|
||||
self._zoomInBtn.clicked.connect(self._onZoomIn)
|
||||
self._zoomOutBtn.clicked.connect(self._onZoomOut)
|
||||
self._zoomResetBtn.clicked.connect(self._onZoomReset)
|
||||
self._copyBtn.clicked.connect(self._onCopy)
|
||||
|
||||
|
||||
def _onZoomIn(
|
||||
self
|
||||
):
|
||||
|
||||
self.__fontSize = min(self.__fontSize + 2, 40)
|
||||
self._updateFontSize()
|
||||
|
||||
|
||||
def _onZoomOut(
|
||||
self
|
||||
):
|
||||
|
||||
self.__fontSize = max(self.__fontSize - 2, 8)
|
||||
self._updateFontSize()
|
||||
|
||||
|
||||
def _onZoomReset(
|
||||
self
|
||||
):
|
||||
|
||||
self.__fontSize = 13
|
||||
self._updateFontSize()
|
||||
|
||||
|
||||
def _onCopy(
|
||||
self
|
||||
):
|
||||
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(self._textEdit.toPlainText())
|
||||
original = self._copyBtn.text()
|
||||
self._copyBtn.setText("已复制")
|
||||
self._copyBtn.setEnabled(False)
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(2000, lambda: (
|
||||
self._copyBtn.setText(original),
|
||||
self._copyBtn.setEnabled(True)
|
||||
))
|
||||
|
||||
|
||||
def _updateFontSize(
|
||||
self
|
||||
):
|
||||
|
||||
font = self._textEdit.font()
|
||||
font.setPointSize(self.__fontSize)
|
||||
self._textEdit.setFont(font)
|
||||
self._textEdit.setStyleSheet(
|
||||
"QPlainTextEdit {"
|
||||
" font-family: 'Courier New', 'Consolas', monospace;"
|
||||
f" font-size: {self.__fontSize}px;"
|
||||
"}"
|
||||
)
|
||||
self._zoomLabel.setText(f"{self.__fontSize}px")
|
||||
@@ -0,0 +1,864 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
|
||||
from PySide6.QtCore import QTime, QDate
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QComboBox, QPushButton, QScrollArea, QTimeEdit,
|
||||
QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox,
|
||||
QStackedWidget, QFrame, QDialogButtonBox,
|
||||
QGroupBox, QSizePolicy
|
||||
)
|
||||
|
||||
from utils.PreprocEngine import PreprocEngine
|
||||
|
||||
|
||||
VARIABLE_META = PreprocEngine.VARIABLE_META
|
||||
|
||||
_VAR_COMBO_ITEMS = [
|
||||
(display, varname, vartype)
|
||||
for display, (varname, vartype) in VARIABLE_META.items()
|
||||
]
|
||||
|
||||
_VAR_COMBO_ITEMS_SET = [
|
||||
(display, varname, vartype)
|
||||
for display, (varname, vartype) in VARIABLE_META.items()
|
||||
if not varname.startswith("CURRENT_")
|
||||
]
|
||||
|
||||
OP_ITEMS = [
|
||||
("等于", ".EQ."),
|
||||
("不等于", ".NEQ."),
|
||||
("大于", ".BGT."),
|
||||
("小于", ".BLT."),
|
||||
("大于等于", ".BGE."),
|
||||
("小于等于", ".BLE."),
|
||||
]
|
||||
|
||||
|
||||
def _makeVarCombo(
|
||||
) -> QComboBox:
|
||||
|
||||
cb = QComboBox()
|
||||
for display, varname, vartype in _VAR_COMBO_ITEMS:
|
||||
cb.addItem(display, (varname, vartype))
|
||||
cb.setMinimumWidth(120)
|
||||
cb.setFixedHeight(25)
|
||||
return cb
|
||||
|
||||
|
||||
def _makeSetVarCombo(
|
||||
) -> QComboBox:
|
||||
|
||||
cb = QComboBox()
|
||||
for display, varname, vartype in _VAR_COMBO_ITEMS_SET:
|
||||
cb.addItem(display, (varname, vartype))
|
||||
cb.setMinimumWidth(120)
|
||||
cb.setFixedHeight(25)
|
||||
return cb
|
||||
|
||||
|
||||
def _makeOpCombo(
|
||||
) -> QComboBox:
|
||||
|
||||
cb = QComboBox()
|
||||
for display, op in OP_ITEMS:
|
||||
cb.addItem(display, op)
|
||||
cb.setMinimumWidth(80)
|
||||
cb.setFixedHeight(25)
|
||||
return cb
|
||||
|
||||
|
||||
def _makeValueWidget(
|
||||
data_type: str
|
||||
) -> QWidget:
|
||||
|
||||
if data_type == "Time":
|
||||
w = QTimeEdit()
|
||||
w.setDisplayFormat("HH:mm")
|
||||
w.setMinimumWidth(100)
|
||||
w.setFixedHeight(25)
|
||||
elif data_type == "Date":
|
||||
w = QDateEdit()
|
||||
w.setDisplayFormat("yyyy-MM-dd")
|
||||
w.setCalendarPopup(True)
|
||||
w.setMinimumWidth(130)
|
||||
w.setFixedHeight(25)
|
||||
elif data_type == "Integer":
|
||||
w = QSpinBox()
|
||||
w.setMinimum(-999999)
|
||||
w.setMaximum(999999)
|
||||
w.setMinimumWidth(100)
|
||||
w.setFixedHeight(25)
|
||||
elif data_type == "Float":
|
||||
w = QDoubleSpinBox()
|
||||
w.setMinimum(-999999)
|
||||
w.setMaximum(999999)
|
||||
w.setDecimals(2)
|
||||
w.setMinimumWidth(100)
|
||||
w.setFixedHeight(25)
|
||||
elif data_type == "Boolean":
|
||||
w = QComboBox()
|
||||
w.addItem(".TRUE.", ".TRUE.")
|
||||
w.addItem(".FALSE.", ".FALSE.")
|
||||
w.setMinimumWidth(100)
|
||||
w.setFixedHeight(25)
|
||||
else:
|
||||
w = QLineEdit()
|
||||
w.setPlaceholderText("输入值")
|
||||
w.setMinimumWidth(120)
|
||||
w.setFixedHeight(25)
|
||||
return w
|
||||
|
||||
|
||||
def _makeActionValueWidget(
|
||||
data_type: str
|
||||
) -> QWidget:
|
||||
|
||||
if data_type == "Date":
|
||||
w = QComboBox()
|
||||
w.addItem("今天", "today")
|
||||
w.addItem("明天", "tomorrow")
|
||||
w.setFixedHeight(25)
|
||||
w.setMinimumWidth(100)
|
||||
w._is_date_action = True
|
||||
return w
|
||||
|
||||
if data_type == "Time":
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(2)
|
||||
modeCombo = QComboBox()
|
||||
modeCombo.addItem("固定时间", "fixed")
|
||||
modeCombo.addItem("相对当前", "relative")
|
||||
modeCombo.setFixedHeight(25)
|
||||
stack = QStackedWidget()
|
||||
timeEdit = QTimeEdit()
|
||||
timeEdit.setDisplayFormat("HH:mm")
|
||||
timeEdit.setFixedHeight(25)
|
||||
spinBox = QSpinBox()
|
||||
spinBox.setRange(0, 23)
|
||||
spinBox.setSuffix("小时")
|
||||
spinBox.setFixedHeight(25)
|
||||
stack.addWidget(timeEdit)
|
||||
stack.addWidget(spinBox)
|
||||
modeCombo.currentIndexChanged.connect(
|
||||
lambda i: stack.setCurrentIndex(i)
|
||||
)
|
||||
layout.addWidget(modeCombo)
|
||||
layout.addWidget(stack)
|
||||
container._modeCombo = modeCombo
|
||||
container._timeEdit = timeEdit
|
||||
container._spinBox = spinBox
|
||||
container._isActionTime = True
|
||||
return container
|
||||
|
||||
return _makeValueWidget(data_type)
|
||||
|
||||
|
||||
def _getValueFromWidget(
|
||||
w: QWidget
|
||||
) -> str:
|
||||
|
||||
if getattr(w, '_isActionTime', False):
|
||||
if w._modeCombo.currentData() == "fixed":
|
||||
return w._timeEdit.time().toString("HH:mm")
|
||||
else:
|
||||
return f"+{w._spinBox.value()}"
|
||||
if isinstance(w, QTimeEdit):
|
||||
return w.time().toString("HH:mm")
|
||||
if isinstance(w, QDateEdit):
|
||||
return w.date().toString("yyyy-MM-dd")
|
||||
if isinstance(w, QComboBox):
|
||||
return w.currentText()
|
||||
if isinstance(w, QSpinBox):
|
||||
return str(w.value())
|
||||
if isinstance(w, QDoubleSpinBox):
|
||||
return str(w.value())
|
||||
if isinstance(w, QLineEdit):
|
||||
return w.text()
|
||||
return ""
|
||||
|
||||
|
||||
def _encodeValueStr(
|
||||
raw_value: str,
|
||||
data_type: str
|
||||
) -> str:
|
||||
|
||||
if data_type == "Time":
|
||||
if raw_value.startswith("+"):
|
||||
return raw_value
|
||||
return f"TIME({raw_value})"
|
||||
elif data_type == "Date":
|
||||
if raw_value == "今天":
|
||||
return "CURRENT_DATE"
|
||||
elif raw_value == "明天":
|
||||
return "CURRENT_DATE + 1"
|
||||
return f"DATE({raw_value})"
|
||||
elif data_type == "Boolean":
|
||||
return raw_value
|
||||
elif data_type == "String":
|
||||
escaped = raw_value.replace("'", "''")
|
||||
return f"'{escaped}'"
|
||||
else:
|
||||
return raw_value
|
||||
|
||||
|
||||
def _setWidgetValue(
|
||||
w: QWidget,
|
||||
vartype: str,
|
||||
expr: str
|
||||
):
|
||||
|
||||
import re
|
||||
s = expr.strip()
|
||||
|
||||
if getattr(w, '_isActionTime', False):
|
||||
timeMatch = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
|
||||
if timeMatch:
|
||||
w._modeCombo.setCurrentIndex(0)
|
||||
parts = timeMatch.group(1).split(":")
|
||||
w._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
|
||||
return
|
||||
relMatch = re.match(r"^\+(\d+)$", s)
|
||||
if relMatch:
|
||||
w._modeCombo.setCurrentIndex(1)
|
||||
w._spinBox.setValue(int(relMatch.group(1)))
|
||||
return
|
||||
return
|
||||
if getattr(w, '_is_date_action', False) and isinstance(w, QComboBox):
|
||||
if s.upper() in ("CURRENT_DATE", "TODAY"):
|
||||
w.setCurrentIndex(0)
|
||||
elif s.upper() in ("CURRENT_DATE + 1", "TOMORROW"):
|
||||
w.setCurrentIndex(1)
|
||||
else:
|
||||
dateMatch = re.match(
|
||||
r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE
|
||||
)
|
||||
if dateMatch:
|
||||
from datetime import datetime, timedelta
|
||||
dateStr = dateMatch.group(1)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
tomorrow = (
|
||||
datetime.now() + timedelta(days=1)
|
||||
).strftime("%Y-%m-%d")
|
||||
if dateStr == today:
|
||||
w.setCurrentIndex(0)
|
||||
elif dateStr == tomorrow:
|
||||
w.setCurrentIndex(1)
|
||||
return
|
||||
if vartype == "Time":
|
||||
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
|
||||
if m and isinstance(w, QTimeEdit):
|
||||
parts = m.group(1).split(":")
|
||||
w.setTime(QTime(int(parts[0]), int(parts[1])))
|
||||
elif vartype == "Date":
|
||||
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE)
|
||||
if m and isinstance(w, QDateEdit):
|
||||
parts = m.group(1).split("-")
|
||||
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
|
||||
elif vartype == "Boolean" and isinstance(w, QComboBox):
|
||||
for i in range(w.count()):
|
||||
if w.itemData(i) == s.upper():
|
||||
w.setCurrentIndex(i)
|
||||
break
|
||||
elif vartype == "Integer" and isinstance(w, QSpinBox):
|
||||
try:
|
||||
w.setValue(int(s))
|
||||
except ValueError:
|
||||
pass
|
||||
elif vartype == "Float" and isinstance(w, QDoubleSpinBox):
|
||||
try:
|
||||
w.setValue(float(s))
|
||||
except ValueError:
|
||||
pass
|
||||
elif isinstance(w, QLineEdit):
|
||||
inner = s
|
||||
if (inner.startswith("'") and inner.endswith("'")) or \
|
||||
(inner.startswith('"') and inner.endswith('"')):
|
||||
inner = inner[1:-1].replace("''", "'")
|
||||
w.setText(inner)
|
||||
|
||||
|
||||
class ActionStepFrame(QFrame):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self._onTargetChanged(0)
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
self.setFixedHeight(35)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(2, 2, 2, 2)
|
||||
layout.setSpacing(4)
|
||||
|
||||
self.targetCombo = _makeSetVarCombo()
|
||||
self.valueWidgetStack = QStackedWidget()
|
||||
self.valueWidgetStack.setFixedHeight(25)
|
||||
self._initValueStack()
|
||||
|
||||
setLabel = QLabel("设置")
|
||||
setLabel.setFixedHeight(25)
|
||||
layout.addWidget(setLabel)
|
||||
layout.addWidget(self.targetCombo)
|
||||
toLabel = QLabel("为")
|
||||
toLabel.setFixedHeight(25)
|
||||
layout.addWidget(toLabel)
|
||||
layout.addWidget(self.valueWidgetStack)
|
||||
|
||||
self.deleteBtn = QPushButton("×")
|
||||
self.deleteBtn.setFixedSize(24, 25)
|
||||
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
|
||||
layout.addWidget(self.deleteBtn)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.targetCombo.currentIndexChanged.connect(self._onTargetChanged)
|
||||
|
||||
def _initValueStack(
|
||||
self
|
||||
):
|
||||
|
||||
self._valueWidgets = {}
|
||||
for _, _, vartype in _VAR_COMBO_ITEMS:
|
||||
if vartype not in self._valueWidgets:
|
||||
w = _makeActionValueWidget(vartype)
|
||||
self._valueWidgets[vartype] = w
|
||||
self.valueWidgetStack.addWidget(w)
|
||||
self.valueWidgetStack.setCurrentWidget(
|
||||
self._valueWidgets.get("String", self.valueWidgetStack.widget(0))
|
||||
)
|
||||
|
||||
def _onTargetChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
if idx < 0:
|
||||
return
|
||||
data = self.targetCombo.itemData(idx)
|
||||
if data:
|
||||
_, vartype = data
|
||||
w = self._valueWidgets.get(vartype)
|
||||
if w:
|
||||
self.valueWidgetStack.setCurrentWidget(w)
|
||||
|
||||
def getTarget(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
data = self.targetCombo.currentData()
|
||||
return data[0] if data else ""
|
||||
|
||||
def getTargetType(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
data = self.targetCombo.currentData()
|
||||
return data[1] if data else "String"
|
||||
|
||||
def getValueRaw(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
currentType = self.getTargetType()
|
||||
w = self._valueWidgets.get(currentType)
|
||||
if w:
|
||||
return _getValueFromWidget(w)
|
||||
return ""
|
||||
|
||||
def toScriptLine(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
target = self.getTarget()
|
||||
if not target:
|
||||
return ""
|
||||
rawVal = self.getValueRaw()
|
||||
targetType = self.getTargetType()
|
||||
encoded = _encodeValueStr(rawVal, targetType)
|
||||
if targetType == "Time" and rawVal.startswith("+"):
|
||||
hours = rawVal[1:]
|
||||
return f" {target} .ADD. {hours}"
|
||||
return f" SET {target} = {encoded}"
|
||||
|
||||
def loadFromScript(
|
||||
self,
|
||||
targetVar: str,
|
||||
valueExpr: str
|
||||
):
|
||||
|
||||
for idx in range(self.targetCombo.count()):
|
||||
data = self.targetCombo.itemData(idx)
|
||||
if data and data[0] == targetVar:
|
||||
self.targetCombo.setCurrentIndex(idx)
|
||||
break
|
||||
self._setValueFromExpr(valueExpr)
|
||||
|
||||
def _setValueFromExpr(
|
||||
self,
|
||||
expr: str
|
||||
):
|
||||
|
||||
targetType = self.getTargetType()
|
||||
w = self._valueWidgets.get(targetType)
|
||||
if not w:
|
||||
return
|
||||
_setWidgetValue(w, targetType, expr)
|
||||
|
||||
|
||||
class ConditionalBlock(QGroupBox):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
blockIndex: int,
|
||||
parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.blockIndex = blockIndex
|
||||
self._actionWidgets = []
|
||||
|
||||
self.setupUi()
|
||||
self.connectSignals()
|
||||
self._onOperandChanged(0)
|
||||
|
||||
|
||||
def setupUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setStyleSheet(
|
||||
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
|
||||
"margin-top: 8px; padding-top: 8px; }"
|
||||
)
|
||||
self.setSizePolicy(
|
||||
QSizePolicy.Policy.Preferred,
|
||||
QSizePolicy.Policy.Fixed
|
||||
)
|
||||
|
||||
mainLayout = QVBoxLayout(self)
|
||||
mainLayout.setSpacing(4)
|
||||
mainLayout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
headerLayout = QHBoxLayout()
|
||||
self.typeCombo = QComboBox()
|
||||
self.typeCombo.addItem("IF", "IF")
|
||||
self.typeCombo.addItem("ELSE IF", "ELSE IF")
|
||||
self.typeCombo.addItem("ELSE", "ELSE")
|
||||
if self.blockIndex == 0:
|
||||
self.typeCombo.setEnabled(False)
|
||||
typeLabel = QLabel("类型:")
|
||||
typeLabel.setFixedHeight(25)
|
||||
headerLayout.addWidget(typeLabel)
|
||||
headerLayout.addWidget(self.typeCombo)
|
||||
headerLayout.addStretch()
|
||||
self.deleteBlockBtn = QPushButton("删除此块")
|
||||
self.deleteBlockBtn.setStyleSheet("color: red;")
|
||||
self.deleteBlockBtn.setFixedHeight(25)
|
||||
headerLayout.addWidget(self.deleteBlockBtn)
|
||||
mainLayout.addLayout(headerLayout)
|
||||
|
||||
self.conditionWidget = QWidget()
|
||||
self.conditionWidget.setFixedHeight(60)
|
||||
condLayout = QHBoxLayout(self.conditionWidget)
|
||||
condLayout.setContentsMargins(0, 0, 0, 0)
|
||||
ifLabel = QLabel("如果")
|
||||
ifLabel.setFixedHeight(25)
|
||||
condLayout.addWidget(ifLabel)
|
||||
self.operandCombo = _makeVarCombo()
|
||||
condLayout.addWidget(self.operandCombo)
|
||||
self.opCombo = _makeOpCombo()
|
||||
condLayout.addWidget(self.opCombo)
|
||||
|
||||
self.condValueStack = QStackedWidget()
|
||||
self.condValueStack.setFixedHeight(25)
|
||||
self._condValueWidgets = {}
|
||||
for vartype in ["Time", "Date", "String", "Integer", "Float", "Boolean"]:
|
||||
w = _makeValueWidget(vartype)
|
||||
self._condValueWidgets[vartype] = w
|
||||
self.condValueStack.addWidget(w)
|
||||
self.condValueStack.setCurrentWidget(self._condValueWidgets.get("String"))
|
||||
condLayout.addWidget(self.condValueStack)
|
||||
mainLayout.addWidget(self.conditionWidget)
|
||||
|
||||
self.actionLabel = QLabel("执行步骤:")
|
||||
self.actionLabel.setFixedHeight(25)
|
||||
mainLayout.addWidget(self.actionLabel)
|
||||
|
||||
self.actionsLayout = QVBoxLayout()
|
||||
self.actionsLayout.setSpacing(2)
|
||||
mainLayout.addLayout(self.actionsLayout)
|
||||
|
||||
self.addActionBtn = QPushButton("+ 添加执行步骤")
|
||||
self.addActionBtn.setFixedHeight(25)
|
||||
mainLayout.addWidget(self.addActionBtn)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.operandCombo.currentIndexChanged.connect(self._onOperandChanged)
|
||||
self.typeCombo.currentIndexChanged.connect(self._onTypeChanged)
|
||||
self.addActionBtn.clicked.connect(self._addActionStep)
|
||||
|
||||
def _onOperandChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
if idx < 0:
|
||||
return
|
||||
data = self.operandCombo.itemData(idx)
|
||||
if data:
|
||||
_, vartype = data
|
||||
w = self._condValueWidgets.get(vartype)
|
||||
if w:
|
||||
self.condValueStack.setCurrentWidget(w)
|
||||
|
||||
def _onTypeChanged(
|
||||
self,
|
||||
idx
|
||||
):
|
||||
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
|
||||
self.conditionWidget.setVisible(isCond)
|
||||
self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:")
|
||||
|
||||
def _addActionStep(
|
||||
self
|
||||
):
|
||||
|
||||
step = ActionStepFrame(self)
|
||||
step.deleteBtn.clicked.connect(lambda: self._removeActionStep(step))
|
||||
self._actionWidgets.append(step)
|
||||
self.actionsLayout.addWidget(step)
|
||||
|
||||
def _removeActionStep(
|
||||
self,
|
||||
step: ActionStepFrame
|
||||
):
|
||||
|
||||
if step in self._actionWidgets:
|
||||
self._actionWidgets.remove(step)
|
||||
self.actionsLayout.removeWidget(step)
|
||||
step.hide()
|
||||
step.deleteLater()
|
||||
|
||||
def getBlockType(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.typeCombo.currentData()
|
||||
|
||||
def toScriptLines(
|
||||
self
|
||||
) -> list:
|
||||
|
||||
blockType = self.getBlockType()
|
||||
lines = []
|
||||
|
||||
if blockType in ("IF", "ELSE IF"):
|
||||
operand = self.operandCombo.currentData()
|
||||
operandName = operand[0] if operand else ""
|
||||
operandType = operand[1] if operand else "String"
|
||||
opSym = self.opCombo.currentData()
|
||||
rawVal = _getValueFromWidget(
|
||||
self._condValueWidgets.get(operandType, QLineEdit())
|
||||
)
|
||||
encodedVal = _encodeValueStr(rawVal, operandType)
|
||||
if blockType == "IF":
|
||||
lines.append(f"IF({operandName} {opSym} {encodedVal}) THEN")
|
||||
else:
|
||||
lines.append(f"ELSE IF({operandName} {opSym} {encodedVal}) THEN")
|
||||
else:
|
||||
lines.append("ELSE")
|
||||
for step in self._actionWidgets:
|
||||
scriptLine = step.toScriptLine()
|
||||
if scriptLine:
|
||||
lines.append(scriptLine)
|
||||
|
||||
return lines
|
||||
|
||||
def getConditionSummary(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
bt = self.getBlockType()
|
||||
if bt == "ELSE":
|
||||
return "ELSE"
|
||||
operandData = self.operandCombo.currentData()
|
||||
if not operandData:
|
||||
return bt
|
||||
operandDisplay = self.operandCombo.currentText()
|
||||
opDisplay = self.opCombo.currentText()
|
||||
rawVal = self.getConditionRawValuePreview()
|
||||
return f"{bt} ({operandDisplay} {opDisplay} {rawVal})"
|
||||
|
||||
def getConditionRawValuePreview(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
data = self.operandCombo.currentData()
|
||||
if not data:
|
||||
return ""
|
||||
_, vartype = data
|
||||
w = self._condValueWidgets.get(vartype)
|
||||
if w:
|
||||
return _getValueFromWidget(w)
|
||||
return ""
|
||||
|
||||
def countActionSteps(
|
||||
self
|
||||
) -> int:
|
||||
|
||||
return len(self._actionWidgets)
|
||||
|
||||
|
||||
class ALPreprocOrchDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
existingScript: str = ""
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._blocks: list[ConditionalBlock] = []
|
||||
|
||||
self.modifyUi()
|
||||
self.connectSignals()
|
||||
|
||||
if existingScript and existingScript.strip():
|
||||
self._loadFromScript(existingScript)
|
||||
else:
|
||||
self._addBlock()
|
||||
self._scrollLayout.addStretch()
|
||||
|
||||
|
||||
def modifyUi(
|
||||
self
|
||||
):
|
||||
|
||||
self.setWindowTitle("预处理指令编排 - AutoLibrary")
|
||||
self.setMinimumSize(420, 400)
|
||||
self.setModal(True)
|
||||
mainLayout = QVBoxLayout(self)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scrollContent = QWidget()
|
||||
self._scrollLayout = QVBoxLayout(scrollContent)
|
||||
self._scrollLayout.setSpacing(5)
|
||||
scroll.setWidget(scrollContent)
|
||||
mainLayout.addWidget(scroll)
|
||||
addBlockLayout = QHBoxLayout()
|
||||
self.addBlockBtn = QPushButton("+ 添加判断块")
|
||||
self.addBlockBtn.setFixedHeight(25)
|
||||
addBlockLayout.addStretch()
|
||||
addBlockLayout.addWidget(self.addBlockBtn)
|
||||
addBlockLayout.addStretch()
|
||||
mainLayout.addLayout(addBlockLayout)
|
||||
self.btnBox = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok |
|
||||
QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
|
||||
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
|
||||
mainLayout.addWidget(self.btnBox)
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
):
|
||||
|
||||
self.btnBox.accepted.connect(self.accept)
|
||||
self.btnBox.rejected.connect(self.reject)
|
||||
self.addBlockBtn.clicked.connect(self._addBlock)
|
||||
|
||||
def _addBlock(
|
||||
self
|
||||
):
|
||||
|
||||
block = ConditionalBlock(len(self._blocks), self)
|
||||
block.deleteBlockBtn.clicked.connect(lambda: self._removeBlock(block))
|
||||
self._blocks.append(block)
|
||||
block._addActionStep()
|
||||
if self._scrollLayout.count() > 0:
|
||||
lastItem = self._scrollLayout.itemAt(
|
||||
self._scrollLayout.count() - 1
|
||||
)
|
||||
if lastItem and lastItem.spacerItem():
|
||||
self._scrollLayout.insertWidget(
|
||||
self._scrollLayout.count() - 1, block
|
||||
)
|
||||
return
|
||||
self._scrollLayout.addWidget(block)
|
||||
|
||||
def _removeBlock(
|
||||
self,
|
||||
block: ConditionalBlock
|
||||
):
|
||||
|
||||
if block in self._blocks:
|
||||
self._blocks.remove(block)
|
||||
self._scrollLayout.removeWidget(block)
|
||||
block.hide()
|
||||
block.deleteLater()
|
||||
|
||||
def getScript(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
parts = []
|
||||
for i, block in enumerate(self._blocks):
|
||||
blockType = block.getBlockType()
|
||||
if blockType == "IF" and i > 0:
|
||||
parts.append("ENDIF")
|
||||
lines = block.toScriptLines()
|
||||
parts.extend(lines)
|
||||
if self._blocks and self._blocks[0].getBlockType() == "IF":
|
||||
parts.append("ENDIF")
|
||||
return "\n".join(parts)
|
||||
|
||||
def getScriptPreview(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
s = self.getScript()
|
||||
if len(s) > 10:
|
||||
return s[:7] + "..."
|
||||
return s
|
||||
|
||||
def _loadFromScript(
|
||||
self,
|
||||
script: str
|
||||
):
|
||||
|
||||
import re
|
||||
lines = [l.strip() for l in script.split("\n") if l.strip()]
|
||||
if not lines:
|
||||
self._addBlock()
|
||||
return
|
||||
|
||||
currentBlock = None
|
||||
currentBlockType = None
|
||||
actionsBuffer = []
|
||||
|
||||
def flushBlock():
|
||||
nonlocal currentBlock, currentBlockType, actionsBuffer
|
||||
if currentBlock is None:
|
||||
return
|
||||
typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
|
||||
idx = typeIdxMap.get(currentBlockType, 0)
|
||||
currentBlock.typeCombo.setCurrentIndex(idx)
|
||||
currentBlock._onTypeChanged(idx)
|
||||
for oldStep in list(currentBlock._actionWidgets):
|
||||
currentBlock._removeActionStep(oldStep)
|
||||
for target, valueExpr in actionsBuffer:
|
||||
currentBlock._addActionStep()
|
||||
step = currentBlock._actionWidgets[-1]
|
||||
step.loadFromScript(target, valueExpr)
|
||||
self._blocks.clear()
|
||||
while self._scrollLayout.count():
|
||||
item = self._scrollLayout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
for line in lines:
|
||||
upper = line.upper()
|
||||
ifMatch = re.match(r"^IF\((.+)\)\s*THEN\s*$", upper)
|
||||
if ifMatch:
|
||||
flushBlock()
|
||||
currentBlockType = "IF"
|
||||
actionsBuffer = []
|
||||
self._addBlock()
|
||||
currentBlock = self._blocks[-1]
|
||||
self._parseConditionToBlock(currentBlock, ifMatch.group(1))
|
||||
continue
|
||||
elifIfMatch = re.match(r"^ELSE\s+IF\((.+)\)\s*THEN\s*$", upper)
|
||||
if elifIfMatch:
|
||||
flushBlock()
|
||||
currentBlockType = "ELSE IF"
|
||||
actionsBuffer = []
|
||||
self._addBlock()
|
||||
currentBlock = self._blocks[-1]
|
||||
self._parseConditionToBlock(currentBlock, elifIfMatch.group(1))
|
||||
continue
|
||||
if upper == "ELSE":
|
||||
flushBlock()
|
||||
currentBlockType = "ELSE"
|
||||
actionsBuffer = []
|
||||
self._addBlock()
|
||||
currentBlock = self._blocks[-1]
|
||||
currentBlock.conditionWidget.setVisible(False)
|
||||
continue
|
||||
setMatch = re.match(r"^SET\s+(\w+)\s*=\s*(.+)$", line, re.IGNORECASE)
|
||||
if setMatch:
|
||||
target = setMatch.group(1).strip()
|
||||
valueExpr = setMatch.group(2).strip()
|
||||
actionsBuffer.append((target, valueExpr))
|
||||
continue
|
||||
addMatch = re.match(r"^(\w+)\s+\.ADD\.\s+(\d+)$", line, re.IGNORECASE)
|
||||
if addMatch:
|
||||
target = addMatch.group(1).strip()
|
||||
hours = addMatch.group(2).strip()
|
||||
actionsBuffer.append((target, f"+{hours}"))
|
||||
continue
|
||||
if upper in ("ENDIF", "END IF"):
|
||||
flushBlock()
|
||||
currentBlock = None
|
||||
currentBlockType = None
|
||||
actionsBuffer = []
|
||||
continue
|
||||
flushBlock()
|
||||
if not self._blocks:
|
||||
self._addBlock()
|
||||
|
||||
def _parseConditionToBlock(
|
||||
self,
|
||||
block: ConditionalBlock,
|
||||
condStr: str
|
||||
):
|
||||
|
||||
condStr = condStr.strip()
|
||||
for _, opSym in OP_ITEMS:
|
||||
idx = condStr.upper().find(opSym)
|
||||
if idx >= 0:
|
||||
leftPart = condStr[:idx].strip()
|
||||
rightPart = condStr[idx + len(opSym):].strip()
|
||||
for ci in range(block.operandCombo.count()):
|
||||
data = block.operandCombo.itemData(ci)
|
||||
if data and data[0] == leftPart:
|
||||
block.operandCombo.setCurrentIndex(ci)
|
||||
break
|
||||
for oi in range(block.opCombo.count()):
|
||||
if block.opCombo.itemData(oi) == opSym:
|
||||
block.opCombo.setCurrentIndex(oi)
|
||||
break
|
||||
opData = block.operandCombo.currentData()
|
||||
vartype = opData[1] if opData else "String"
|
||||
w = block._condValueWidgets.get(vartype)
|
||||
if w:
|
||||
_setWidgetValue(w, vartype, rightPart)
|
||||
return
|
||||
@@ -13,9 +13,10 @@ from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PySide6.QtCore import Slot, QDateTime
|
||||
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QGridLayout, QDateTimeEdit
|
||||
from PySide6.QtWidgets import QLabel, QDialog, QWidget, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout, QDateTimeEdit, QGroupBox, QPushButton
|
||||
|
||||
from gui.resources.ui.Ui_ALTimerTaskAddDialog import Ui_ALTimerTaskAddDialog
|
||||
from gui.ALPreprocOrchDialog import ALPreprocOrchDialog
|
||||
from utils.TimerUtils import TimerUtils
|
||||
|
||||
|
||||
@@ -86,6 +87,33 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
self.TimerConfigLayout.addWidget(self.RelativeTimerWidget)
|
||||
self.RelativeTimerWidget.setVisible(False)
|
||||
|
||||
self.PreprocGroupBox = QGroupBox("预处理脚本")
|
||||
self.PreprocLayout = QVBoxLayout(self.PreprocGroupBox)
|
||||
self.PreprocLayout.setContentsMargins(3, 3, 3, 3)
|
||||
self.PreprocLayout.setSpacing(3)
|
||||
|
||||
preproc_btn_layout = QHBoxLayout()
|
||||
self.PreprocSetButton = QPushButton("设置预处理指令")
|
||||
self.PreprocSetButton.setMinimumHeight(25)
|
||||
self.PreprocSetButton.setFixedWidth(130)
|
||||
preproc_btn_layout.addWidget(self.PreprocSetButton)
|
||||
self.PreprocPreviewButton = QPushButton("预览")
|
||||
self.PreprocPreviewButton.setMinimumHeight(25)
|
||||
self.PreprocPreviewButton.setFixedWidth(60)
|
||||
self.PreprocPreviewButton.setEnabled(False)
|
||||
preproc_btn_layout.addWidget(self.PreprocPreviewButton)
|
||||
preproc_btn_layout.addStretch()
|
||||
self.PreprocStatusLabel = QLabel("未设置")
|
||||
self.PreprocStatusLabel.setStyleSheet("color: #969696;")
|
||||
self.PreprocStatusLabel.setFixedHeight(25)
|
||||
preproc_btn_layout.addWidget(self.PreprocStatusLabel)
|
||||
self.PreprocLayout.addLayout(preproc_btn_layout)
|
||||
self.ALAddTimerTaskLayout.insertWidget(
|
||||
self.ALAddTimerTaskLayout.indexOf(self.TaskConfigGroupBox) + 1,
|
||||
self.PreprocGroupBox
|
||||
)
|
||||
self.__repeat_preproc_script = ""
|
||||
|
||||
|
||||
def connectSignals(
|
||||
self
|
||||
@@ -95,6 +123,35 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
self.ConfirmButton.clicked.connect(self.accept)
|
||||
self.TimerTypeComboBox.currentIndexChanged.connect(self.onTimerTypeComboBoxIndexChanged)
|
||||
self.RepeatCheckBox.toggled.connect(self.onRepeatCheckBoxToggled)
|
||||
self.PreprocSetButton.clicked.connect(self._onSetPreproc)
|
||||
self.PreprocPreviewButton.clicked.connect(self._onPreviewPreproc)
|
||||
|
||||
|
||||
@Slot()
|
||||
def _onSetPreproc(self):
|
||||
dlg = ALPreprocOrchDialog(self, existingScript=self.__repeat_preproc_script)
|
||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||
script = dlg.getScript()
|
||||
self.__repeat_preproc_script = script
|
||||
if script:
|
||||
self.PreprocStatusLabel.setText("已设置")
|
||||
self.PreprocStatusLabel.setStyleSheet("color: #4CAF50;")
|
||||
self.PreprocPreviewButton.setEnabled(True)
|
||||
else:
|
||||
self.PreprocStatusLabel.setText("未设置")
|
||||
self.PreprocStatusLabel.setStyleSheet("color: #969696;")
|
||||
self.PreprocPreviewButton.setEnabled(False)
|
||||
dlg.deleteLater()
|
||||
|
||||
|
||||
@Slot()
|
||||
def _onPreviewPreproc(self):
|
||||
if not self.__repeat_preproc_script:
|
||||
return
|
||||
from gui.ALPreProcPrevDialog import ALScriptPreviewDialog
|
||||
dlg = ALScriptPreviewDialog(self, self.__repeat_preproc_script)
|
||||
dlg.exec()
|
||||
dlg.deleteLater()
|
||||
|
||||
|
||||
def getTimerTask(
|
||||
@@ -129,6 +186,7 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
"status": ALTimerTaskStatus.PENDING,
|
||||
"executed": False,
|
||||
"repeat": self.RepeatCheckBox.isChecked(),
|
||||
"repeat_preproc": self.__repeat_preproc_script,
|
||||
}
|
||||
if task_data["repeat"]:
|
||||
task_data["history"] = [] # repeat history
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- ALSeatMapTable: Seat map table class.
|
||||
- ALSeatMapSelectDialog: Seat map select dialog class.
|
||||
- ALTimerTaskAddDialog: Timer task add dialog class.
|
||||
- ALPreprocOrchDialog: Preprocessing script orchestration dialog class.
|
||||
- ALTimerTaskHistoryDialog: Timer task history dialog class.
|
||||
- ALTimerTaskManageWidget: Timer task manage widget class.
|
||||
- ALUserTreeWidget: User tree widget class.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>350</width>
|
||||
<height>400</height>
|
||||
<height>460</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class PreprocEngine:
|
||||
|
||||
COMPARE_OPS = {
|
||||
".EQ.": lambda a, b: a == b,
|
||||
".NEQ.": lambda a, b: a != b,
|
||||
".BGT.": lambda a, b: a > b,
|
||||
".BLT.": lambda a, b: a < b,
|
||||
".BGE.": lambda a, b: a >= b,
|
||||
".BLE.": lambda a, b: a <= b,
|
||||
}
|
||||
|
||||
VARIABLE_META = {
|
||||
"预约开始时间": ("RESERVE_BEGIN_TIME", "Time"),
|
||||
"预约结束时间": ("RESERVE_END_TIME", "Time"),
|
||||
"预约日期": ("RESERVE_DATE", "Date"),
|
||||
"用户名": ("USERNAME", "String"),
|
||||
"用户启用": ("USER_ENABLE", "Boolean"),
|
||||
"当前时间": ("CURRENT_TIME", "Time"),
|
||||
"当前日期": ("CURRENT_DATE", "Date"),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def execute(
|
||||
script_text: str,
|
||||
user_data: dict
|
||||
):
|
||||
|
||||
if not script_text or not script_text.strip():
|
||||
return
|
||||
lines = [l.strip() for l in script_text.split("\n") if l.strip()]
|
||||
if not lines:
|
||||
return
|
||||
|
||||
if_stack = []
|
||||
|
||||
for line in lines:
|
||||
upper_line = line.upper().strip()
|
||||
if upper_line.startswith("IF("):
|
||||
cond_end = _findConditionEnd(upper_line)
|
||||
if cond_end < 0:
|
||||
raise ValueError("语法错误: IF 缺少右括号")
|
||||
condition_str = line[3:cond_end].strip()
|
||||
matched = PreprocEngine._evaluateCondition(
|
||||
condition_str, user_data
|
||||
)
|
||||
if_stack.append([matched, matched])
|
||||
elif upper_line.startswith("ELSE IF("):
|
||||
if not if_stack:
|
||||
raise ValueError("语法错误: ELSE IF 前缺少 IF")
|
||||
cond_end = _findConditionEnd(upper_line)
|
||||
if cond_end < 0:
|
||||
raise ValueError("语法错误: ELSE IF 缺少右括号")
|
||||
condition_str = line[8:cond_end].strip()
|
||||
_, has_matched = if_stack[-1]
|
||||
if not has_matched:
|
||||
matched = PreprocEngine._evaluateCondition(
|
||||
condition_str, user_data
|
||||
)
|
||||
if_stack[-1] = [matched, matched]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line == "ELSE":
|
||||
if not if_stack:
|
||||
raise ValueError("语法错误: ELSE 前缺少 IF")
|
||||
_, has_matched = if_stack[-1]
|
||||
if not has_matched:
|
||||
if_stack[-1] = [True, True]
|
||||
else:
|
||||
if_stack[-1][0] = False
|
||||
elif upper_line in ("ENDIF", "END IF"):
|
||||
if not if_stack:
|
||||
raise ValueError("语法错误: ENDIF/END IF 前缺少 IF")
|
||||
if_stack.pop()
|
||||
elif upper_line.startswith("SET "):
|
||||
should_execute = (
|
||||
all(ctx[0] for ctx in if_stack) if if_stack else True
|
||||
)
|
||||
if should_execute:
|
||||
PreprocEngine._executeSet(line, user_data)
|
||||
elif upper_line == "PASS":
|
||||
continue
|
||||
else:
|
||||
should_execute = (
|
||||
all(ctx[0] for ctx in if_stack) if if_stack else True
|
||||
)
|
||||
if should_execute:
|
||||
PreprocEngine._executeOperation(line, user_data)
|
||||
if if_stack:
|
||||
raise ValueError("语法错误: IF 与 ENDIF/END IF 不匹配")
|
||||
|
||||
@staticmethod
|
||||
def _resolveValue(
|
||||
value_str: str,
|
||||
user_data: dict
|
||||
) -> str:
|
||||
|
||||
s = value_str.strip()
|
||||
time_match = re.match(r"^TIME\((\d{1,2}):(\d{2})\)$", s, re.IGNORECASE)
|
||||
if time_match:
|
||||
h, m = time_match.group(1), time_match.group(2)
|
||||
return f"{int(h):02d}:{int(m):02d}"
|
||||
date_match = re.match(r"^DATE\((\d{4})-(\d{2})-(\d{2})\)$", s, re.IGNORECASE)
|
||||
if date_match:
|
||||
y, mo, d = date_match.group(1), date_match.group(2), date_match.group(3)
|
||||
return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}"
|
||||
if s.upper() == ".TRUE.":
|
||||
return "True"
|
||||
if s.upper() == ".FALSE.":
|
||||
return "False"
|
||||
if s.startswith("'") and s.endswith("'"):
|
||||
inner = s[1:-1].replace("''", "'")
|
||||
return inner
|
||||
if s.startswith('"') and s.endswith('"'):
|
||||
return s[1:-1]
|
||||
relDate = re.match(r"^CURRENT_DATE\s*\+\s*(\d+)$", s, re.IGNORECASE)
|
||||
if relDate:
|
||||
days = int(relDate.group(1))
|
||||
return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
relTime = re.match(r"^CURRENT_TIME\s*\+\s*(\d+)$", s, re.IGNORECASE)
|
||||
if relTime:
|
||||
hours = int(relTime.group(1))
|
||||
return (datetime.now() + timedelta(hours=hours)).strftime("%H:%M")
|
||||
try:
|
||||
float(s)
|
||||
return s
|
||||
except ValueError:
|
||||
pass
|
||||
resolved = PreprocEngine._resolveField(s, user_data)
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _resolveField(
|
||||
field_name: str,
|
||||
user_data: dict
|
||||
) -> str:
|
||||
|
||||
upper_name = field_name.upper().strip()
|
||||
if upper_name == "CURRENT_DATE":
|
||||
return datetime.now().strftime("%Y-%m-%d")
|
||||
elif upper_name == "CURRENT_TIME":
|
||||
return datetime.now().strftime("%H:%M")
|
||||
elif upper_name == "USERNAME":
|
||||
return user_data.get("username", "")
|
||||
elif upper_name == "USER_ENABLE":
|
||||
return str(user_data.get("enabled", "False"))
|
||||
elif upper_name == "RESERVE_DATE":
|
||||
return user_data.get("reserve_info", {}).get("date", "")
|
||||
elif upper_name == "RESERVE_BEGIN_TIME":
|
||||
return (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("begin_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
elif upper_name == "RESERVE_END_TIME":
|
||||
return (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("end_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _setField(
|
||||
field_name: str,
|
||||
value: str,
|
||||
user_data: dict
|
||||
):
|
||||
upper_name = field_name.upper().strip()
|
||||
if upper_name == "RESERVE_DATE":
|
||||
user_data.setdefault("reserve_info", {})["date"] = value
|
||||
elif upper_name == "RESERVE_BEGIN_TIME":
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("begin_time", {})["time"] = value
|
||||
elif upper_name == "RESERVE_END_TIME":
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("end_time", {})["time"] = value
|
||||
elif upper_name == "USERNAME":
|
||||
user_data["username"] = value
|
||||
elif upper_name == "USER_ENABLE":
|
||||
user_data["enabled"] = value.upper() == "TRUE"
|
||||
|
||||
@staticmethod
|
||||
def _evaluateCondition(
|
||||
condition_str: str,
|
||||
user_data: dict
|
||||
) -> bool:
|
||||
|
||||
for op, cmp_func in PreprocEngine.COMPARE_OPS.items():
|
||||
if op not in condition_str.upper():
|
||||
continue
|
||||
idx = condition_str.upper().find(op)
|
||||
parts = [condition_str[:idx], condition_str[idx + len(op):]]
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
field_name = parts[0].strip()
|
||||
value_str = parts[1].strip()
|
||||
left_val = PreprocEngine._resolveField(field_name, user_data)
|
||||
right_val = PreprocEngine._resolveValue(value_str, user_data)
|
||||
return cmp_func(left_val, right_val)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _executeSet(
|
||||
line: str,
|
||||
user_data: dict
|
||||
):
|
||||
rest = line[3:].strip()
|
||||
eq_idx = rest.find("=")
|
||||
if eq_idx < 0:
|
||||
return
|
||||
field_name = rest[:eq_idx].strip()
|
||||
value_str = rest[eq_idx + 1:].strip()
|
||||
if not field_name:
|
||||
return
|
||||
resolved = PreprocEngine._resolveValue(value_str, user_data)
|
||||
PreprocEngine._setField(field_name, resolved, user_data)
|
||||
|
||||
@staticmethod
|
||||
def _executeOperation(
|
||||
line: str,
|
||||
user_data: dict
|
||||
):
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
return
|
||||
field_name = parts[0].upper().strip()
|
||||
op = parts[1].upper().strip()
|
||||
raw_value = parts[2].strip()
|
||||
try:
|
||||
num_value = float(raw_value) if "." in raw_value else int(raw_value)
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if field_name == "RESERVE_DATE":
|
||||
date_str = user_data.get("reserve_info", {}).get("date", "")
|
||||
if not date_str:
|
||||
return
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if op == ".ADD.":
|
||||
date_obj += timedelta(days=num_value)
|
||||
elif op == ".SUB.":
|
||||
date_obj -= timedelta(days=num_value)
|
||||
else:
|
||||
return
|
||||
user_data.setdefault("reserve_info", {})["date"] = \
|
||||
date_obj.strftime("%Y-%m-%d")
|
||||
elif field_name == "RESERVE_BEGIN_TIME":
|
||||
time_str = (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("begin_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
if not time_str:
|
||||
return
|
||||
try:
|
||||
time_obj = datetime.strptime(time_str, "%H:%M")
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if op == ".ADD.":
|
||||
time_obj += timedelta(hours=num_value)
|
||||
elif op == ".SUB.":
|
||||
time_obj -= timedelta(hours=num_value)
|
||||
else:
|
||||
return
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("begin_time", {})["time"] = \
|
||||
time_obj.strftime("%H:%M")
|
||||
elif field_name == "RESERVE_END_TIME":
|
||||
time_str = (
|
||||
user_data
|
||||
.get("reserve_info", {})
|
||||
.get("end_time", {})
|
||||
.get("time", "")
|
||||
)
|
||||
if not time_str:
|
||||
return
|
||||
try:
|
||||
time_obj = datetime.strptime(time_str, "%H:%M")
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
if op == ".ADD.":
|
||||
time_obj += timedelta(hours=num_value)
|
||||
elif op == ".SUB.":
|
||||
time_obj -= timedelta(hours=num_value)
|
||||
else:
|
||||
return
|
||||
ri = user_data.setdefault("reserve_info", {})
|
||||
ri.setdefault("end_time", {})["time"] = \
|
||||
time_obj.strftime("%H:%M")
|
||||
|
||||
|
||||
def _findConditionEnd(
|
||||
upper_line: str
|
||||
) -> int:
|
||||
|
||||
line = upper_line.rstrip()
|
||||
if line.endswith(" THEN"):
|
||||
line = line[:-5].rstrip()
|
||||
paren_depth = 0
|
||||
start_found = False
|
||||
for i, ch in enumerate(line):
|
||||
if ch == "(":
|
||||
paren_depth += 1
|
||||
start_found = True
|
||||
elif ch == ")":
|
||||
paren_depth -= 1
|
||||
if start_found and paren_depth == 0:
|
||||
return i
|
||||
return -1
|
||||
Reference in New Issue
Block a user