更新
This commit is contained in:
@@ -8,7 +8,7 @@ from html import escape
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QDateTime, QModelIndex, QRectF, QSize, Qt
|
||||
from PySide6.QtCore import QDateTime, QEvent, QModelIndex, QRectF, QSize, Qt, QTimer
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
@@ -33,7 +33,8 @@ from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QListWidgetItem,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
@@ -48,7 +49,16 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .. import icons, motion
|
||||
from .. import icons, motion
|
||||
from ..dialogs.issued_prescription_ai import (
|
||||
PrescriptionAiStatisticsDialog,
|
||||
agreement_text,
|
||||
batch_running,
|
||||
can_open_issued_ai,
|
||||
present_issued_prescription_ai,
|
||||
state_text,
|
||||
status_tooltip,
|
||||
)
|
||||
from ..dialogs.prescription import (
|
||||
AuditPrescriptionDialog,
|
||||
DiagnosisDetailDialog,
|
||||
@@ -877,7 +887,7 @@ class _PrescriptionInfoDelegate(QStyledItemDelegate):
|
||||
painter.setPen(QColor(foreground))
|
||||
painter.drawText(pill, Qt.AlignmentFlag.AlignCenter, label)
|
||||
elif column != 2:
|
||||
lines = text.rsplit(" · ", 2) if column == 5 else text.rsplit(" ", 1) if column == 10 else [text]
|
||||
lines = text.rsplit(" · ", 2) if column == 5 else text.rsplit(" ", 1) if column == 10 else text.splitlines() if column == 12 else [text]
|
||||
if len(lines) > 1:
|
||||
primary, secondary = lines[0], " · ".join(lines[1:]) if column == 5 else lines[1]
|
||||
if column == 5:
|
||||
@@ -979,7 +989,18 @@ class PrescriptionsPage(QWidget):
|
||||
self._detail_target = 0
|
||||
self._diagnosis_detail_generation = 0
|
||||
self._diagnosis_detail_target = 0
|
||||
self._mutation_pending = False
|
||||
self._mutation_pending = False
|
||||
self._ai_statuses: dict[int, dict[str, Any]] = {}
|
||||
self._ai_enabled = False
|
||||
self._ai_request_token = 0
|
||||
self._ai_pending = False
|
||||
self._ai_timer = QTimer(self)
|
||||
self._ai_timer.setInterval(5000)
|
||||
self._ai_timer.timeout.connect(self._load_ai_statuses)
|
||||
self._ai_scroll_timer = QTimer(self)
|
||||
self._ai_scroll_timer.setSingleShot(True)
|
||||
self._ai_scroll_timer.setInterval(180)
|
||||
self._ai_scroll_timer.timeout.connect(self._load_ai_statuses)
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(28, 16, 26, 8)
|
||||
@@ -1010,7 +1031,11 @@ class PrescriptionsPage(QWidget):
|
||||
self.orders_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.orders_button.setVisible(has_permission(permissions, "tcm.prescriptionOrder/lists"))
|
||||
self.orders_button.clicked.connect(lambda: self._open_orders())
|
||||
header.add_action(self.orders_button)
|
||||
header.add_action(self.orders_button)
|
||||
self.ai_statistics_button = QPushButton("AI 一致度统计", header)
|
||||
self.ai_statistics_button.setVisible(has_permission(permissions, "tcm.prescriptionAi/statistics", default=False) and callable(getattr(repository, "prescription_ai_statistics", None)))
|
||||
self.ai_statistics_button.clicked.connect(self._open_ai_statistics)
|
||||
header.add_action(self.ai_statistics_button)
|
||||
self.add_button = QPushButton("新增处方", header)
|
||||
self.add_button.setObjectName("PrescriptionAddButton")
|
||||
self.add_button.setMinimumWidth(122)
|
||||
@@ -1156,7 +1181,10 @@ class PrescriptionsPage(QWidget):
|
||||
toolbar.addWidget(self.count_badge)
|
||||
toolbar.addStretch(1)
|
||||
self.view_button = self._action_button("查看", "cf.prescription/read", self._view_selected)
|
||||
toolbar.addWidget(self.view_button)
|
||||
toolbar.addWidget(self.view_button)
|
||||
self.ai_report_button = self._action_button("AI 报告", "tcm.prescriptionAi/reports", self._open_ai_report)
|
||||
self.ai_report_button.setVisible(can_open_issued_ai(self.permissions) and callable(getattr(self.repository, "list_prescription_ai_reports", None)))
|
||||
toolbar.addWidget(self.ai_report_button)
|
||||
self.patch_button = self._action_button(
|
||||
"修改患者", "tcm.prescription/patchPatient", self._patch_selected
|
||||
)
|
||||
@@ -1181,9 +1209,17 @@ class PrescriptionsPage(QWidget):
|
||||
refresh.setIcon(_blue_prescription_icon("refresh", "#5D6B80"))
|
||||
refresh.setIconSize(QSize(15, 15))
|
||||
refresh.clicked.connect(self.refresh)
|
||||
toolbar.addWidget(refresh)
|
||||
layout.addWidget(toolbar_scroll)
|
||||
self.stack = QStackedWidget()
|
||||
toolbar.addWidget(refresh)
|
||||
layout.addWidget(toolbar_scroll)
|
||||
self.ai_status_notice = QLabel("AI 分析:正在检查服务状态。", card)
|
||||
self.ai_status_notice.setObjectName("PrescriptionAiStatusNotice")
|
||||
self.ai_status_notice.setProperty("role", "muted")
|
||||
self.ai_status_notice.setTextFormat(Qt.TextFormat.PlainText)
|
||||
self.ai_status_notice.setWordWrap(True)
|
||||
self.ai_status_notice.setMargin(12)
|
||||
self.ai_status_notice.setAccessibleName("AI 分析状态")
|
||||
layout.addWidget(self.ai_status_notice)
|
||||
self.stack = QStackedWidget()
|
||||
table_host = QWidget()
|
||||
table_layout = QVBoxLayout(table_host)
|
||||
table_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -1200,7 +1236,9 @@ class PrescriptionsPage(QWidget):
|
||||
TableColumn("void_status", "作废", 72, _void_cell),
|
||||
TableColumn("doctor_name", "医生信息", 180, _doctor_cell),
|
||||
TableColumn("assistant_name", "医助", 125),
|
||||
TableColumn("create_time", "创建时间", 180, _create_time_cell),
|
||||
TableColumn("create_time", "创建时间", 180, _create_time_cell),
|
||||
TableColumn("__ai_status__", "AI 分析", 144, lambda _value, _row: "—"),
|
||||
TableColumn("__ai_agreement__", "与 AI 一致度", 130, lambda _value, _row: "—"),
|
||||
]
|
||||
)
|
||||
self.table.setObjectName("PrescriptionTable")
|
||||
@@ -1213,8 +1251,13 @@ class PrescriptionsPage(QWidget):
|
||||
self.table.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
self.table.horizontalHeader().moveSection(2, 10)
|
||||
for column, width in enumerate((42, 192, 144, 108, 94, 144, 108, 80, 137, 83, 128)):
|
||||
self.table.setColumnWidth(column, width)
|
||||
for column, width in enumerate((42, 192, 144, 108, 94, 144, 108, 80, 137, 83, 128)):
|
||||
self.table.setColumnWidth(column, width)
|
||||
self.table.setColumnHidden(11, True)
|
||||
self.table.setColumnHidden(12, True)
|
||||
self.table.horizontalHeaderItem(12).setToolTip("分别显示千问、OpenAI 的药味与剂量一致度;点击查看逐味贡献。")
|
||||
self.table.horizontalHeader().viewport().installEventFilter(self)
|
||||
self.table.verticalScrollBar().valueChanged.connect(lambda _value: self._schedule_ai_statuses())
|
||||
self.table.setWordWrap(False)
|
||||
# Rows are not uniform: the number column's delegate grows a row that
|
||||
# carries an order warning so the warning text stays readable without a
|
||||
@@ -1227,7 +1270,10 @@ class PrescriptionsPage(QWidget):
|
||||
self.table.horizontalHeaderItem(0).setIcon(_blue_prescription_icon("checkbox", "#8A97A9", 14))
|
||||
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.table.itemSelectionChanged.connect(self._selection_changed)
|
||||
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
|
||||
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
|
||||
self.table.itemClicked.connect(lambda item: self._open_ai_report() if item.column() in {11, 12} else None)
|
||||
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table.customContextMenuRequested.connect(self._open_ai_context_menu)
|
||||
table_layout.addWidget(self.table, 1)
|
||||
self.pager = InfiniteList(self._page_size)
|
||||
self.pager.bind(self.table)
|
||||
@@ -1271,9 +1317,22 @@ class PrescriptionsPage(QWidget):
|
||||
self.filter_grid.setColumnStretch(column, stretch)
|
||||
self.filter_card.setFixedHeight(200 if compact else 144)
|
||||
|
||||
def _number_column_resized(self, column: int, _old_size: int, _new_size: int) -> None:
|
||||
def _number_column_resized(self, column: int, _old_size: int, _new_size: int) -> None:
|
||||
if column == 1:
|
||||
self.table.resizeRowsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def eventFilter(self, watched: Any, event: Any) -> bool:
|
||||
if hasattr(self, "table") and watched is self.table.horizontalHeader().viewport() and event.type() in {QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseButtonDblClick}:
|
||||
header = self.table.horizontalHeader()
|
||||
position = event.position().toPoint()
|
||||
column = header.logicalIndexAt(position)
|
||||
if column in {11, 12} and event.button() == Qt.MouseButton.LeftButton:
|
||||
relative = position.x() - header.sectionViewportPosition(column)
|
||||
# Keep native column resizing, but never rank mixed-model display
|
||||
# text or treat unavailable values as numeric zero through sorting.
|
||||
if 5 < relative < header.sectionSize(column) - 5:
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def _action_button(
|
||||
self,
|
||||
@@ -1354,7 +1413,11 @@ class PrescriptionsPage(QWidget):
|
||||
self.doctor_filter.clear()
|
||||
self._search()
|
||||
|
||||
def refresh(self) -> None:
|
||||
def refresh(self) -> None:
|
||||
self._ai_timer.stop()
|
||||
self._ai_request_token += 1
|
||||
self._ai_pending = False
|
||||
self._ai_statuses.clear()
|
||||
self._loading = True
|
||||
self._refresh_pending = False
|
||||
self._generation += 1
|
||||
@@ -1392,8 +1455,9 @@ class PrescriptionsPage(QWidget):
|
||||
if generation != self._generation:
|
||||
return
|
||||
rows = page_items(result)
|
||||
self.table.set_rows(rows)
|
||||
self._decorate_rows()
|
||||
self.table.set_rows(rows)
|
||||
self._decorate_rows()
|
||||
self._render_ai_statuses()
|
||||
total = page_total(result, len(rows))
|
||||
self.pager.update_state(requested_page, total)
|
||||
self.count_badge.setText(f"共 {total} 条")
|
||||
@@ -1415,7 +1479,8 @@ class PrescriptionsPage(QWidget):
|
||||
self.banner.clear()
|
||||
if rows and self.table.currentRow() < 0:
|
||||
self.table.selectRow(0)
|
||||
self._selection_changed()
|
||||
self._selection_changed()
|
||||
self._load_ai_statuses()
|
||||
|
||||
def _decorate_rows(self) -> None:
|
||||
"""Apply the reference table's tags, checkbox, avatar, and row actions."""
|
||||
@@ -1489,7 +1554,7 @@ class PrescriptionsPage(QWidget):
|
||||
actions.setContentsMargins(3, 0, 3, 0)
|
||||
actions.setSpacing(3)
|
||||
actions.addStretch(1)
|
||||
if has_permission(self.permissions, "cf.prescription/read"):
|
||||
if has_permission(self.permissions, "cf.prescription/read"):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
"eye",
|
||||
@@ -1499,7 +1564,15 @@ class PrescriptionsPage(QWidget):
|
||||
),
|
||||
actions_host,
|
||||
)
|
||||
)
|
||||
)
|
||||
if self._ai_enabled and can_open_issued_ai(self.permissions):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
"eye", "AI 报告",
|
||||
lambda _checked=False, target=row: self._run_row_action(target, self._open_ai_report),
|
||||
actions_host, label="AI",
|
||||
)
|
||||
)
|
||||
if has_permission(self.permissions, "cf.prescription/edit"):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
@@ -1531,7 +1604,162 @@ class PrescriptionsPage(QWidget):
|
||||
actions.addStretch(1)
|
||||
self.table.setCellWidget(row_index, 2, actions_host)
|
||||
self._sync_row_mutation_actions()
|
||||
self.table.resizeRowsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def _visible_ai_ids(self) -> list[int]:
|
||||
if not self.isVisible() or not self.table.isVisible():
|
||||
return []
|
||||
ids = []
|
||||
viewport = self.table.viewport().rect()
|
||||
for index in range(self.table.rowCount()):
|
||||
item = self.table.item(index, 0)
|
||||
# Column 0 may be horizontally off-screen; use row geometry only.
|
||||
top = self.table.rowViewportPosition(index)
|
||||
if item is None or top + self.table.rowHeight(index) <= 0 or top >= viewport.height():
|
||||
continue
|
||||
row = item.data(Qt.ItemDataRole.UserRole)
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
if value > 0:
|
||||
ids.append(value)
|
||||
return list(dict.fromkeys(ids))[:100]
|
||||
|
||||
def _schedule_ai_statuses(self) -> None:
|
||||
if self.isVisible():
|
||||
self._ai_scroll_timer.start()
|
||||
|
||||
def _load_ai_statuses(self) -> None:
|
||||
method = getattr(self.repository, "list_prescription_ai_statuses", None)
|
||||
if not has_permission(self.permissions, "tcm.prescriptionAi/statuses", default=False):
|
||||
self.ai_status_notice.setText("AI 分析:当前账号没有查看分析状态的权限,请联系管理员授权。")
|
||||
return
|
||||
if not callable(method):
|
||||
self.ai_status_notice.setText("AI 分析:当前数据模式暂不支持此功能。")
|
||||
return
|
||||
if self._ai_pending:
|
||||
return
|
||||
ids = self._visible_ai_ids()
|
||||
ids = [value for value in ids if value not in self._ai_statuses or batch_running(self._ai_statuses[value])]
|
||||
if not ids:
|
||||
self._ai_timer.stop()
|
||||
if self.table.rowCount() == 0:
|
||||
self.ai_status_notice.setText("AI 分析:当前列表没有处方,暂无可查看的分析结果。")
|
||||
return
|
||||
self._ai_pending = True
|
||||
self._ai_request_token += 1
|
||||
token, generation = self._ai_request_token, self._generation
|
||||
run_async(
|
||||
lambda: method(ids),
|
||||
on_success=lambda result: self._ai_statuses_ready(result, ids, token, generation),
|
||||
on_error=lambda error: self._ai_statuses_error(error, token, generation),
|
||||
on_finished=lambda: self._ai_statuses_finished(token),
|
||||
)
|
||||
|
||||
def _ai_statuses_finished(self, token: int) -> None:
|
||||
if token == self._ai_request_token:
|
||||
self._ai_pending = False
|
||||
|
||||
def _set_ai_columns(self, enabled: bool) -> None:
|
||||
changed = enabled != self._ai_enabled
|
||||
self._ai_enabled = enabled
|
||||
for column in (11, 12):
|
||||
self.table.setColumnHidden(column, not enabled)
|
||||
if enabled:
|
||||
header = self.table.horizontalHeader()
|
||||
header.moveSection(header.visualIndex(11), header.visualIndex(4) + 1)
|
||||
header.moveSection(header.visualIndex(12), header.visualIndex(11) + 1)
|
||||
self.table.setColumnWidth(11, 144)
|
||||
self.table.setColumnWidth(12, 130)
|
||||
self.table.setColumnWidth(2, 178)
|
||||
if changed:
|
||||
self._decorate_rows()
|
||||
|
||||
def _ai_statuses_ready(self, result: Any, ids: list[int], token: int, generation: int) -> None:
|
||||
if token != self._ai_request_token or generation != self._generation or not self.isVisible():
|
||||
return
|
||||
data = _row_mapping(result)
|
||||
self._set_ai_columns(data.get("enabled") is True)
|
||||
if not self._ai_enabled:
|
||||
self._ai_timer.stop()
|
||||
notice = "AI 分析未启用:暂不生成报告或一致度,请联系管理员启用。"
|
||||
if can_open_issued_ai(self.permissions):
|
||||
notice += "已有报告仍可从“AI 报告”查看。"
|
||||
self.ai_status_notice.setText(notice)
|
||||
self.ai_status_notice.setToolTip("")
|
||||
self.ai_report_button.setToolTip("自动分析未启用;仍可查看已保存的历史报告。")
|
||||
return
|
||||
self.ai_status_notice.setText("AI 分析已启用:保存手工处方后自动生成两份报告,并显示药味与剂量一致度。")
|
||||
self.ai_status_notice.setToolTip("")
|
||||
self.ai_report_button.setToolTip("查看两份 AI 报告、候选处方和逐味对照。")
|
||||
for value in ids:
|
||||
self._ai_statuses[value] = {}
|
||||
for batch in data.get("items") or []:
|
||||
value = _int(get_value(batch, "prescription_id"), 0)
|
||||
if value in ids:
|
||||
self._ai_statuses[value] = _row_mapping(batch)
|
||||
self._render_ai_statuses()
|
||||
if any(batch_running(self._ai_statuses.get(value)) for value in self._visible_ai_ids()):
|
||||
self._ai_timer.start()
|
||||
else:
|
||||
self._ai_timer.stop()
|
||||
|
||||
def _render_ai_statuses(self) -> None:
|
||||
sorting = self.table.isSortingEnabled()
|
||||
self.table.setSortingEnabled(False)
|
||||
changed = False
|
||||
try:
|
||||
for index in range(self.table.rowCount()):
|
||||
row = self.table.item(index, 0).data(Qt.ItemDataRole.UserRole)
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
batch = self._ai_statuses.get(value)
|
||||
if batch is None:
|
||||
continue
|
||||
state = state_text(batch) if batch else ("尚未开方" if _truthy(get_value(row, "is_system_auto")) else "尚无分析记录")
|
||||
for column, text in ((11, state), (12, agreement_text(batch))):
|
||||
item = self.table.item(index, column)
|
||||
if item.text() != text:
|
||||
item.setText(text)
|
||||
changed = True
|
||||
item.setToolTip(status_tooltip(batch))
|
||||
item.setData(Qt.ItemDataRole.AccessibleTextRole, text)
|
||||
if changed:
|
||||
self.table.resizeRowsToContents()
|
||||
finally:
|
||||
self.table.setSortingEnabled(sorting)
|
||||
|
||||
def _ai_statuses_error(self, error: Exception, token: int, generation: int) -> None:
|
||||
if token == self._ai_request_token and generation == self._generation:
|
||||
self._ai_timer.stop()
|
||||
self._set_ai_columns(False)
|
||||
self.ai_status_notice.setText("AI 分析暂不可用:请稍后刷新;持续无法使用时,请联系管理员检查服务。")
|
||||
self.ai_status_notice.setToolTip(friendly_error(error))
|
||||
self.ai_report_button.setToolTip("AI 状态暂不可用:" + friendly_error(error))
|
||||
|
||||
def _open_ai_report(self) -> None:
|
||||
row = self._selected()
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
if value > 0:
|
||||
present_issued_prescription_ai(self.repository, self.permissions, self, prescription_id=value)
|
||||
|
||||
def _open_ai_context_menu(self, position: Any) -> None:
|
||||
if not can_open_issued_ai(self.permissions) or not callable(getattr(self.repository, "list_prescription_ai_reports", None)):
|
||||
return
|
||||
item = self.table.itemAt(position)
|
||||
if item is None:
|
||||
return
|
||||
self.table.selectRow(item.row())
|
||||
row = self._selected()
|
||||
menu = QMenu(self.table)
|
||||
action = menu.addAction("AI 报告 / 逐味对照")
|
||||
action.triggered.connect(lambda: self._run_row_action(row, self._open_ai_report))
|
||||
self._ai_context_menu = menu
|
||||
menu.popup(self.table.viewport().mapToGlobal(position))
|
||||
|
||||
def _open_ai_statistics(self) -> None:
|
||||
if not has_permission(self.permissions, "tcm.prescriptionAi/statistics", default=False):
|
||||
return
|
||||
dialog = PrescriptionAiStatisticsDialog(self.repository, self.permissions, self)
|
||||
dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
||||
dialog.show()
|
||||
|
||||
def _run_row_action(self, row: Any, callback: Callable[[], None]) -> None:
|
||||
target_id = _int(first_value(row, "id", "prescription_id", default=None), 0)
|
||||
@@ -1566,7 +1794,8 @@ class PrescriptionsPage(QWidget):
|
||||
def _selection_changed(self) -> None:
|
||||
row = self.table.current_data()
|
||||
active = not self._mutation_pending
|
||||
self.view_button.setEnabled(active and row is not None)
|
||||
self.view_button.setEnabled(active and row is not None)
|
||||
self.ai_report_button.setEnabled(row is not None)
|
||||
self.patch_button.setEnabled(active and can_patch_patient(row))
|
||||
self.create_order_button.setEnabled(active and can_create_order(row))
|
||||
self.edit_button.setEnabled(active and can_edit_or_delete(row))
|
||||
@@ -1935,10 +2164,19 @@ class PrescriptionsPage(QWidget):
|
||||
self.banner.show_message(message, "danger")
|
||||
show_toast(self, message, "danger", 5000)
|
||||
|
||||
def showEvent(self, event: Any) -> None:
|
||||
def showEvent(self, event: Any) -> None:
|
||||
super().showEvent(event)
|
||||
if self.table.rowCount() == 0 and not self._loading:
|
||||
self.refresh()
|
||||
if self.table.rowCount() == 0 and not self._loading:
|
||||
self.refresh()
|
||||
elif not self._loading:
|
||||
self._load_ai_statuses()
|
||||
|
||||
def hideEvent(self, event: Any) -> None:
|
||||
self._ai_timer.stop()
|
||||
self._ai_scroll_timer.stop()
|
||||
self._ai_request_token += 1
|
||||
self._ai_pending = False
|
||||
super().hideEvent(event)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
Reference in New Issue
Block a user