This commit is contained in:
gr
2026-10-27 16:05:19 +08:00
parent fc73dd1aca
commit 690da33722
4 changed files with 369 additions and 37 deletions
@@ -6,7 +6,7 @@ import hashlib
import json
import struct
import zlib
from collections.abc import Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from copy import deepcopy
from datetime import date, datetime, timedelta
from os import PathLike
@@ -2771,10 +2771,14 @@ class DemoDoctorRepository:
*,
call_record_id: int | None = None,
mime_type: str = "audio/webm",
on_progress: Callable[[int, int], None] | None = None,
) -> dict[str, Any]:
"""Upload then attach a demo local-audio file using the production sequence."""
source_path = Path(path)
size = source_path.stat().st_size if source_path.is_file() else 0
if on_progress is not None:
on_progress(0, size)
is_audio = source_path.suffix.lower() in {
".ogg",
".opus",
@@ -2811,6 +2815,8 @@ class DemoDoctorRepository:
record["local_audio_status_text"] = "已保存"
else:
self.attach_local_call_recording(diagnosis_id, file_url, call_record_id=target_id)
if on_progress is not None:
on_progress(size, size)
return {
"diagnosis_id": diagnosis_id,
"call_record_id": target_id,
@@ -683,6 +683,7 @@ class DoctorRepository(Protocol):
*,
call_record_id: int | None = None,
mime_type: str = "audio/webm",
on_progress: Callable[[int, int], None] | None = None,
) -> dict[str, Any]:
"""Upload a locally captured call recording to a real call record."""
@@ -2512,8 +2513,13 @@ class RemoteDoctorRepository:
*,
call_record_id: int | None = None,
mime_type: str = "audio/webm",
on_progress: Callable[[int, int], None] | None = None,
) -> dict[str, Any]:
"""Chunk-upload one local audio recording and attach it atomically."""
"""Chunk-upload one local audio recording and attach it atomically.
``on_progress(sent_bytes, total_bytes)`` runs on the calling thread after
every confirmed chunk; its failures never interrupt the upload.
"""
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
@@ -2533,6 +2539,13 @@ class RemoteDoctorRepository:
clean_mime = str(mime_type or "audio/webm").strip()[:120] or "audio/webm"
final_result: Mapping[str, Any] | None = None
bound_call_record_id = call_record_id
def report(sent: int) -> None:
if on_progress is not None:
with suppress(Exception):
on_progress(min(sent, file_size), file_size)
report(0)
with recording_path.open("rb") as stream:
for chunk_index in range(chunk_total):
content = stream.read(chunk_size)
@@ -2567,6 +2580,7 @@ class RemoteDoctorRepository:
raise ApiProtocolError(
"uploadCallRecording returned a different call_record_id"
)
report((chunk_index + 1) * chunk_size)
if final_result is None or not bool(final_result.get("completed")):
raise ApiProtocolError("uploadCallRecording did not complete the local audio upload")
@@ -2,13 +2,26 @@
from __future__ import annotations
import mimetypes
import re
import sqlite3
from collections.abc import Mapping, Sequence
from contextlib import suppress
from pathlib import Path
from typing import Any
from PySide6.QtCore import QDate, QDateTime, QEvent, QPoint, QRect, Qt, QTimer, QUrl, Signal
from PySide6.QtCore import (
QDate,
QDateTime,
QEvent,
QObject,
QPoint,
QRect,
Qt,
QTimer,
QUrl,
Signal,
)
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QAbstractItemView,
@@ -22,6 +35,7 @@ from PySide6.QtWidgets import (
QLabel,
QMessageBox,
QPlainTextEdit,
QProgressBar,
QPushButton,
QScrollArea,
QSizePolicy,
@@ -83,6 +97,13 @@ from .issued_prescription_ai import can_open_issued_ai, present_issued_prescript
from .local_audio_queue import LocalAudioQueueDialog
from .prescription_ai import can_open_diagnosis_ai_report, present_diagnosis_ai_report
class _UploadProgressRelay(QObject):
"""把后台线程中的分片上传进度转回界面线程(信号跨线程自动排队)。"""
# 用 object 传字节数:Qt 的 int 是 32 位,超过 2 GB 的视频会溢出。
progressed = Signal(object, object)
_PHONE_PERMISSION = "tcm.diagnosis/phonePlain"
_PATIENT_ORDERS_PERMISSION = "tcm.diagnosis/patientOrders"
_ORDER_DETAIL_PERMISSION = "tcm.prescriptionOrder/detail"
@@ -1512,9 +1533,38 @@ class DiagnosisDialog(QDialog):
)
toolbar.addWidget(self.video_upload_button)
layout.addLayout(toolbar)
layout.addWidget(self._build_video_upload_progress())
layout.addWidget(self._new_table("video", "DiagnosisVideoTable"), 1)
return page
def _build_video_upload_progress(self) -> QFrame:
"""上传本地回放时的进度条:按分片实时更新,医生能看到上传在进行、进行到哪。"""
frame = QFrame()
frame.setObjectName("DiagnosisVideoUploadProgress")
frame.setStyleSheet(
"QFrame#DiagnosisVideoUploadProgress { background-color: #EEF1FA;"
" border: 1px solid #C9D1F3; border-radius: 8px; }"
"QFrame#DiagnosisVideoUploadProgress QLabel { color: #3347B8; background: transparent;"
" font-size: 13px; font-weight: 500; }"
"QFrame#DiagnosisVideoUploadProgress QProgressBar { background-color: #FFFFFF;"
" border: 0; border-radius: 4px; min-height: 8px; max-height: 8px; }"
"QFrame#DiagnosisVideoUploadProgress QProgressBar::chunk { background-color: #4F63D9;"
" border-radius: 4px; }"
)
row = QVBoxLayout(frame)
row.setContentsMargins(12, 8, 12, 10)
row.setSpacing(6)
self.video_upload_status = QLabel()
self.video_upload_status.setWordWrap(True)
row.addWidget(self.video_upload_status)
self.video_upload_bar = QProgressBar()
self.video_upload_bar.setRange(0, 1000)
self.video_upload_bar.setTextVisible(False)
row.addWidget(self.video_upload_bar)
frame.hide()
self.video_upload_frame = frame
return frame
def _build_orders_tab(self) -> QWidget:
page = QWidget()
page.setObjectName("DiagnosisTabOrders")
@@ -3323,6 +3373,15 @@ class DiagnosisDialog(QDialog):
)
if not path:
return
# 明确按视频上报类型;默认的 audio/webm 会让 .webm 视频被服务端当成本机录音。
mime_type = mimetypes.guess_type(path)[0] or ""
if not mime_type.startswith("video/"):
mime_type = "video/mp4"
file_name = Path(path).name
relay = _UploadProgressRelay(self)
relay.progressed.connect(lambda sent, total: self._video_upload_progressed(file_name, sent, total))
self._video_upload_relay = relay
self._video_upload_progressed(file_name, 0, 0)
self._run_media_mutation(
lambda: invoke(
self.repository,
@@ -3330,13 +3389,47 @@ class DiagnosisDialog(QDialog):
path=path,
diagnosis_id=self._diagnosis_id,
call_record_id=call_record_id,
mime_type=mime_type,
on_progress=relay.progressed.emit,
),
"视频回放已追加到指定通话记录。"
if call_record_id is not None
else "视频回放已上传并创建通话记录。",
"video",
on_finished=lambda: self._video_upload_finished(relay),
)
def _video_upload_progressed(self, file_name: str, sent: int, total: int) -> None:
self.video_upload_frame.show()
self.video_upload_button.setEnabled(False)
if total <= 0:
self.video_upload_bar.setRange(0, 0) # 未拿到大小前显示滚动条
self.video_upload_status.setText(f"正在准备上传「{file_name}」…")
self.video_upload_button.setText("上传中…")
return
self.video_upload_bar.setRange(0, 1000)
ratio = min(1.0, max(0.0, sent / total))
self.video_upload_bar.setValue(int(ratio * 1000))
percent = int(ratio * 100)
mb = 1024 * 1024
if sent >= total:
text = f"「{file_name}」已上传完成({total / mb:.1f} MB),正在生成通话记录…"
else:
text = f"正在上传「{file_name}」 {percent}%({sent / mb:.1f} / {total / mb:.1f} MB),请勿关闭窗口"
self.video_upload_status.setText(text)
self.video_upload_button.setText(f"上传中 {percent}%")
self._show_message(text, "info")
def _video_upload_finished(self, relay: QObject) -> None:
if getattr(self, "_video_upload_relay", None) is not relay:
return
self._video_upload_relay = None
self.video_upload_frame.hide()
self.video_upload_bar.setRange(0, 1000)
self.video_upload_bar.setValue(0)
self.video_upload_button.setText("上传本地回放")
self.video_upload_button.setEnabled(True)
def _open_local_audio_queue(self) -> None:
if self._diagnosis_id <= 0 or not self._can_video_upload:
self._show_message("当前账号无本机录音上传权限或接口不可用。", "warning")
@@ -3436,7 +3529,14 @@ class DiagnosisDialog(QDialog):
"chat",
)
def _run_media_mutation(self, operation: Any, success_text: str, reload_key: str) -> None:
def _run_media_mutation(
self,
operation: Any,
success_text: str,
reload_key: str,
*,
on_finished: Any = None,
) -> None:
self._media_generation += 1
generation = self._media_generation
diagnosis_id = self._diagnosis_id
@@ -3447,6 +3547,7 @@ class DiagnosisDialog(QDialog):
success_text, reload_key, diagnosis_id, generation
),
on_error=lambda error: self._mutation_error(error, diagnosis_id, generation, "media"),
on_finished=on_finished,
)
def _media_mutation_success(
@@ -28,6 +28,7 @@ from PySide6.QtCore import (
QDate,
QEvent,
QIODevice,
QModelIndex,
QMarginsF,
QObject,
QPoint,
@@ -51,6 +52,8 @@ from PySide6.QtGui import (
QPainter,
QPdfWriter,
QPen,
QStandardItem,
QStandardItemModel,
QTextDocument,
)
from PySide6.QtPrintSupport import QPrintDialog, QPrinter
@@ -61,6 +64,7 @@ from PySide6.QtWidgets import (
QCalendarWidget,
QCheckBox,
QComboBox,
QCompleter,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
@@ -81,6 +85,9 @@ from PySide6.QtWidgets import (
QSizePolicy,
QSpinBox,
QSplitter,
QStyle,
QStyledItemDelegate,
QStyleOptionViewItem,
QTableWidget,
QTableWidgetItem,
QTabWidget,
@@ -323,7 +330,8 @@ QFrame#PrescriptionDrawerSurface QDoubleSpinBox {
border: 1px solid #EDEDEE;
border-radius: 7px;
padding: 0 11px;
selection-background-color: #EEF1FA; font-size: 14px;
selection-background-color: #C9D1F3;
selection-color: #1A1C1F; font-size: 14px;
}
QFrame#PrescriptionDrawerSurface QComboBox {
min-height: 32px;
@@ -333,7 +341,17 @@ QFrame#PrescriptionDrawerSurface QComboBox {
border-radius: 7px;
padding-left: 11px;
padding-right: 28px;
selection-background-color: #EEF1FA; font-size: 14px;
selection-background-color: #C9D1F3;
selection-color: #1A1C1F; font-size: 14px;
}
QFrame#PrescriptionHerbTableRow QComboBox,
QFrame#PrescriptionHerbTableRow QDoubleSpinBox {
border: 1px solid #D2D2D3;
font-weight: 500;
}
QFrame#PrescriptionHerbTableRow QComboBox:focus,
QFrame#PrescriptionHerbTableRow QDoubleSpinBox:focus {
border: 1px solid #4F63D9;
}
QFrame#PrescriptionDrawerSurface QLineEdit[prescriptionControl="true"],
QFrame#PrescriptionDrawerSurface QComboBox[prescriptionControl="true"],
@@ -901,6 +919,23 @@ def _float(value: Any, default: float = 0.0) -> float:
return default
# 处方表单里的数字框、下拉框不响应鼠标滚轮:滚轮只滚动页面,
# 避免医生滚动表单时把鼠标停在输入框上而误改剂量、药材或剂数。
class NoWheelSpinBox(QSpinBox):
def wheelEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
event.ignore()
class NoWheelDoubleSpinBox(QDoubleSpinBox):
def wheelEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
event.ignore()
class NoWheelComboBox(QComboBox):
def wheelEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
event.ignore()
def _bool(value: Any) -> bool:
if isinstance(value, str):
return value.strip().lower() not in {"", "0", "false", "no", "off"}
@@ -1412,6 +1447,47 @@ class MultiSelectComboBox(QComboBox):
self.lineEdit().setText("、".join(self.values()))
_MEDICINE_NAME_ROLE = Qt.ItemDataRole.UserRole + 1
_MEDICINE_ID_ROLE = Qt.ItemDataRole.UserRole + 2
_MEDICINE_META_ROLE = Qt.ItemDataRole.UserRole + 3
class _MedicineSuggestionDelegate(QStyledItemDelegate):
"""药材候选行:左侧药名,右侧灰色「调配中心 · 单位 · ID」,与管理端下拉一致。"""
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: # noqa: N802
return QSize(super().sizeHint(option, index).width(), 36)
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
opt.text = ""
widget = opt.widget
(widget.style() if widget is not None else QApplication.style()).drawControl(
QStyle.ControlElement.CE_ItemViewItem, opt, painter, widget
)
rect = option.rect.adjusted(14, 0, -14, 0)
name = str(index.data(Qt.ItemDataRole.DisplayRole) or "")
meta = str(index.data(_MEDICINE_META_ROLE) or "")
enabled = bool(index.flags() & Qt.ItemFlag.ItemIsEnabled)
metrics = option.fontMetrics
align = Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
painter.save()
if meta:
name_width = int(rect.width() * 0.42)
painter.setPen(QColor("#1A1C1F"))
painter.drawText(rect.adjusted(0, 0, name_width - rect.width(), 0), align,
metrics.elidedText(name, Qt.TextElideMode.ElideRight, name_width - 12))
painter.setPen(QColor("#8A8B8D"))
meta_rect = rect.adjusted(name_width, 0, 0, 0)
painter.drawText(meta_rect, align,
metrics.elidedText(meta, Qt.TextElideMode.ElideRight, meta_rect.width()))
else:
painter.setPen(QColor("#1A1C1F" if enabled else "#8A8B8D"))
painter.drawText(rect, align, metrics.elidedText(name, Qt.TextElideMode.ElideRight, rect.width()))
painter.restore()
class RemoteMedicineComboBox(QComboBox):
"""Editable remote medicine selector retaining both id and canonical name."""
@@ -1433,6 +1509,10 @@ class RemoteMedicineComboBox(QComboBox):
# but no medicine id. New/changed text must still be selected from the
# remote master-data list rather than submitted as arbitrary input.
self._legacy_name = str(name or "").strip() if self._selected_id is None else ""
# 最近一次有效选择(药材库 id + 规范名);手输文字无法匹配药材库时恢复为它。
self._selected_name = str(name or "").strip() if self._selected_id is not None else ""
self._last_valid: tuple[int | None, str] = (self._selected_id, str(name or "").strip())
self._settle_pending = False
self.setEditable(True)
self.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
self.setMaxVisibleItems(14)
@@ -1440,6 +1520,23 @@ class RemoteMedicineComboBox(QComboBox):
self.lineEdit().setPlaceholderText("搜索药材名称或拼音首字母")
self.lineEdit().textEdited.connect(self._queue_search)
self.currentIndexChanged.connect(self._selection_changed)
# 与管理端 MedicineNameSelect 一致:点击或输入即弹出药材库匹配列表,
# 只能从列表中选;列表不抢输入焦点,可边输入边看结果。
self._suggestions = QStandardItemModel(self)
self._completer = QCompleter(self._suggestions, self)
self._completer.setCompletionMode(QCompleter.CompletionMode.UnfilteredPopupCompletion)
self._completer.setCompletionRole(_MEDICINE_NAME_ROLE)
self._completer.setMaxVisibleItems(14)
self._completer.activated[QModelIndex].connect(self._suggestion_activated)
popup = self._completer.popup()
popup.setItemDelegate(_MedicineSuggestionDelegate(popup))
popup.setStyleSheet(
"QListView { background-color: #FFFFFF; border: 1px solid #E4E7ED; border-radius: 6px;"
" padding: 4px 0; font-size: 14px; outline: 0; }"
"QListView::item:selected, QListView::item:hover { background-color: #F5F7FA; }"
)
self.lineEdit().setCompleter(self._completer)
self.lineEdit().installEventFilter(self)
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(250)
@@ -1451,9 +1548,9 @@ class RemoteMedicineComboBox(QComboBox):
@property
def medicine_id(self) -> int | None:
data = self.currentData()
value = _int(data, 0)
return value or self._selected_id
# 只认从药材库选中的 id;不能回退到旧下拉项的 data,否则改过文字的行
# 仍会带着原药材 id 通过校验,相当于允许直接输入药名。
return self._selected_id
@property
def medicine_name(self) -> str:
@@ -1462,12 +1559,16 @@ class RemoteMedicineComboBox(QComboBox):
@property
def has_valid_selection(self) -> bool:
name = self.medicine_name
return bool(self.medicine_id) or bool(self._legacy_name and name == self._legacy_name)
if self._selected_id is not None:
return not self._selected_name or name == self._selected_name
return bool(self._legacy_name and name == self._legacy_name)
def set_value(self, medicine_id: Any, name: Any) -> None:
self._selected_id = _int(medicine_id, 0) or None
canonical_name = str(name or "").strip()
self._legacy_name = canonical_name if self._selected_id is None else ""
self._selected_name = canonical_name if self._selected_id is not None else ""
self._last_valid = (self._selected_id, canonical_name)
self.blockSignals(True)
self.clear()
if canonical_name:
@@ -1478,20 +1579,117 @@ class RemoteMedicineComboBox(QComboBox):
def showPopup(self) -> None:
if self.count() <= 1:
self._queue_search(self.currentText())
# 只是打开列表浏览,不清除已选药材。
self._timer.stop()
self._search()
self._search(browse=self.has_valid_selection or not self.medicine_name)
super().showPopup()
def _queue_search(self, _text: str) -> None:
self._selected_id = None
self._selected_name = ""
self._legacy_name = ""
self._timer.start()
def _search(self) -> None:
query = self.currentText().strip()
def wheelEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
# 滚轮不切换药材,只滚动页面。
event.ignore()
def eventFilter(self, watched: Any, event: Any) -> bool: # noqa: N802 - Qt virtual
# 鼠标点一下输入框就列出药材库,和管理端下拉一样可以直接挑;
# 不看焦点原因,已获得焦点时再点一次也能打开。
if (
watched is self.lineEdit()
and event.type() == QEvent.Type.MouseButtonPress
and self.isEnabled()
and not self._completer.popup().isVisible()
):
self._timer.stop()
self._search(browse=self.has_valid_selection or not self.medicine_name)
return super().eventFilter(watched, event)
def focusOutEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
super().focusOutEvent(event)
# 打开下拉列表也会让输入框失焦,此时还在挑选,不做校正。
if (
event.reason() == Qt.FocusReason.PopupFocusReason
or self.view().isVisible()
or self._completer.popup().isVisible()
):
return
self._settle_typed_text()
def _suggestion_activated(self, index: QModelIndex) -> None:
medicine_id = _int(index.data(_MEDICINE_ID_ROLE), 0)
name = str(index.data(_MEDICINE_NAME_ROLE) or "").strip()
if not medicine_id or not name:
return
for row in range(self.count()):
data = self.itemData(row)
if isinstance(data, Mapping) and _int(data.get("id"), 0) == medicine_id:
self.setCurrentIndex(row)
self._selection_changed(row)
return
self.set_value(medicine_id, name)
def _show_suggestions(self, rows: list[tuple[int, str, str]], note: str = "") -> None:
self._suggestions.clear()
for medicine_id, name, meta in rows:
item = QStandardItem(name)
item.setData(name, _MEDICINE_NAME_ROLE)
item.setData(medicine_id, _MEDICINE_ID_ROLE)
item.setData(meta, _MEDICINE_META_ROLE)
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
self._suggestions.appendRow(item)
if note:
# 提示行不可选,只告诉医生当前状态(搜索中 / 无匹配)。
item = QStandardItem(note)
item.setData(self.medicine_name, _MEDICINE_NAME_ROLE)
item.setFlags(Qt.ItemFlag.NoItemFlags)
self._suggestions.appendRow(item)
if self.lineEdit().hasFocus() and self.isEnabled():
self._completer.complete()
def _settle_typed_text(self) -> None:
"""离开输入框时,手输文字必须落到药材库中的一味药,否则恢复原值。"""
if self.has_valid_selection:
return
text = self.medicine_name
if text and self._select_exact(text):
return
if not text:
self._restore_last_valid()
return
# 搜索结果可能还没回来:立即按当前文字查一次,结果到达后再定。
self._settle_pending = True
self._timer.stop()
self._search()
def _select_exact(self, text: str) -> bool:
key = "".join(text.split()).casefold()
for row in range(self.count()):
data = self.itemData(row)
if isinstance(data, Mapping) and "".join(str(data.get("name") or "").split()).casefold() == key:
self.setCurrentIndex(row)
self._selection_changed(row)
return True
return False
def _restore_last_valid(self) -> None:
medicine_id, name = self._last_valid
if medicine_id is not None or name:
self.set_value(medicine_id, name)
return
self.blockSignals(True)
self.setEditText("")
self.blockSignals(False)
def _search(self, browse: bool = False) -> None:
# browse:点开下拉时按空关键字列出药材库,与管理端打开下拉的行为一致。
query = "" if browse else self.currentText().strip()
self._generation += 1
generation = self._generation
if self.lineEdit().hasFocus() and self._suggestions.rowCount() == 0:
self._show_suggestions([], "正在搜索药材库…")
run_async(
lambda: _repository_action(
self.repository,
@@ -1510,6 +1708,7 @@ class RemoteMedicineComboBox(QComboBox):
return
rows = page_items(result)
current_text = self.currentText().strip()
suggestions: list[tuple[int, str, str]] = []
self.blockSignals(True)
self.clear()
for row in rows:
@@ -1524,11 +1723,23 @@ class RemoteMedicineComboBox(QComboBox):
if meta:
label = f"{name} {meta}"
self.addItem(label, {"id": medicine_id, "name": name})
suggestions.append((medicine_id, name, meta))
self.setEditText(current_text or query)
self.blockSignals(False)
self._show_suggestions(
suggestions,
"" if suggestions else f"药材库中没有与「{query}」匹配的药材,请换个名称或拼音首字母",
)
if self._settle_pending and not self.hasFocus():
self._settle_pending = False
if not self._select_exact(query):
self._restore_last_valid()
def _search_error(self, error: Exception, generation: int) -> None:
if generation == self._generation:
if self._settle_pending and not self.hasFocus():
self._settle_pending = False
self._restore_last_valid()
self.search_failed.emit(friendly_error(error))
def _selection_changed(self, index: int) -> None:
@@ -1538,9 +1749,9 @@ class RemoteMedicineComboBox(QComboBox):
name = str(data.get("name") or "").strip()
if name:
self._legacy_name = ""
self._selected_name = name
self._last_valid = (self._selected_id, name)
self.setEditText(name)
else:
self._selected_id = _int(data, 0) or self._selected_id
class HerbRowWidget(QFrame):
@@ -1575,7 +1786,7 @@ class HerbRowWidget(QFrame):
if source_locked is None
else bool(source_locked)
)
self.formula_combo = QComboBox(self)
self.formula_combo = NoWheelComboBox(self)
self.formula_combo.addItem("主方", "主方")
self.formula_combo.addItem("辅方", "辅方")
_set_combo_data(
@@ -1591,7 +1802,7 @@ class HerbRowWidget(QFrame):
parent=self,
)
self.medicine.setEnabled(not locked)
self.dosage = QDoubleSpinBox(self)
self.dosage = NoWheelDoubleSpinBox(self)
self.dosage.setRange(0, 1e100)
self.dosage.setDecimals(1)
self.dosage.setSingleStep(0.5)
@@ -2173,7 +2384,7 @@ class PrescriptionTemplateDialog(QDialog):
self.name_edit.setMaxLength(100)
self.name_edit.setPlaceholderText("请输入处方名称")
form.addRow("处方名称", self.name_edit)
self.formula_combo = QComboBox()
self.formula_combo = NoWheelComboBox()
self.formula_combo.addItem("主方", "主方")
self.formula_combo.addItem("辅方", "辅方")
_set_combo_data(
@@ -2293,7 +2504,7 @@ class TemplateImportDialog(QDialog):
self.name_edit.setPlaceholderText("请输入处方名称搜索")
self.name_edit.returnPressed.connect(self.search)
filters.addWidget(self.name_edit, 1)
self.formula_combo = QComboBox()
self.formula_combo = NoWheelComboBox()
self.formula_combo.addItem("全部", "")
self.formula_combo.addItem("主方", "主方")
self.formula_combo.addItem("辅方", "辅方")
@@ -2304,7 +2515,7 @@ class TemplateImportDialog(QDialog):
root.addLayout(filters)
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("导入方式"))
self.mode_combo = QComboBox()
self.mode_combo = NoWheelComboBox()
self.mode_combo.addItem("覆盖现有药材", "replace")
self.mode_combo.addItem("追加到末尾", "append")
mode_layout.addWidget(self.mode_combo)
@@ -2511,7 +2722,7 @@ class PasteHerbsDialog(QDialog):
root.addWidget(self.text_edit, 1)
mode_row = QHBoxLayout()
mode_row.addWidget(QLabel("导入方式"))
self.mode_combo = QComboBox()
self.mode_combo = NoWheelComboBox()
self.mode_combo.addItem("覆盖现有药材", "replace")
self.mode_combo.addItem("追加到末尾", "append")
mode_row.addWidget(self.mode_combo)
@@ -2735,8 +2946,8 @@ class PrescriptionEditorDialog(QDialog):
self.revive_banner = MessageBanner()
self.body_layout.addWidget(self.revive_banner)
self._responsive_grids: list[tuple[QGridLayout, int, list[tuple[QWidget, int]]]] = []
self._form_columns = 3
self._responsive_grids: list[tuple[QGridLayout, int, list[tuple[QWidget, int]]]] = []
self._form_columns = 3
self.patient_section = self._build_patient_section()
self.diagnosis_section = self._build_diagnosis_section()
self.herbs_section = self._build_herbs_section()
@@ -2978,7 +3189,7 @@ class PrescriptionEditorDialog(QDialog):
gender_layout.addWidget(self.gender_female)
gender_layout.addStretch(1)
self.gender = gender_host
self.age = QSpinBox()
self.age = NoWheelSpinBox()
self.age.setRange(0, 150)
self.visit_no = QLineEdit()
self.visit_no.setPlaceholderText("自动生成或手动输入")
@@ -3130,7 +3341,7 @@ class PrescriptionEditorDialog(QDialog):
for value in self.PRESCRIPTION_TYPES:
self.prescription_type.addItem(value, value)
self.prescription_type.currentIndexChanged.connect(self._type_changed)
self.dose_count = QSpinBox()
self.dose_count = NoWheelSpinBox()
self.dose_count.setRange(1, 2_147_483_647)
self.dose_unit = SelectComboBox()
self.dose_unit.addItems(["剂", "丸", "袋", "盒", "瓶", "膏", "贴"])
@@ -3153,9 +3364,9 @@ class PrescriptionEditorDialog(QDialog):
self.dosage_bag_count = LabeledIntCombo(range(1, 6), "袋")
self.need_decoction = BoolRadioRow("代煎", "不代煎")
self.bags_per_dose = LabeledIntCombo(range(1, 10), "包")
self.times_per_day = QSpinBox()
self.times_per_day = NoWheelSpinBox()
self.times_per_day.setRange(1, 6)
self.usage_days = QSpinBox()
self.usage_days = NoWheelSpinBox()
self.usage_days.setRange(1, 365)
self._main_dosage_field = self._field("用量", self.dosage_amount)
self._main_dosage_unit_field = self._field("用量单位", self.dosage_unit)
@@ -3188,9 +3399,9 @@ class PrescriptionEditorDialog(QDialog):
self.aux_dosage_bag_count = LabeledIntCombo(range(1, 6), "袋")
self.aux_need_decoction = BoolRadioRow("代煎", "不代煎")
self.aux_bags_per_dose = LabeledIntCombo(range(1, 10), "包")
self.aux_times_per_day = QSpinBox()
self.aux_times_per_day = NoWheelSpinBox()
self.aux_times_per_day.setRange(1, 6)
self.aux_usage_days = QSpinBox()
self.aux_usage_days = NoWheelSpinBox()
self.aux_usage_days.setRange(1, 365)
self.aux_prescription_name = QLineEdit()
self._aux_dosage_field = self._field("用量", self.aux_dosage_amount)
@@ -3964,7 +4175,7 @@ class PatchPatientDialog(QDialog):
self.patient_name = QLineEdit(str(first_value(prescription, "patient_name", default="")))
self.patient_name.setMaxLength(50)
form.addRow("患者姓名 *", self.patient_name)
self.gender = QComboBox()
self.gender = NoWheelComboBox()
self.gender.addItem("男", 1)
self.gender.addItem("女", 0)
_set_combo_data(self.gender, first_value(prescription, "gender", default=1))
@@ -5311,7 +5522,7 @@ class PrescriptionOrderDialog(QDialog):
def _build_recipient_tab(self) -> QWidget:
tab, form = self._form_tab()
self.diagnosis_id = QSpinBox()
self.diagnosis_id = NoWheelSpinBox()
self.diagnosis_id.setRange(0, 2_000_000_000)
self.diagnosis_id.valueChanged.connect(self._diagnosis_changed)
form.addRow("诊单 ID *", self.diagnosis_id)
@@ -5333,14 +5544,14 @@ class PrescriptionOrderDialog(QDialog):
def _build_service_tab(self) -> QWidget:
tab, form = self._form_tab()
self.ship_mode = QComboBox()
self.ship_mode = NoWheelComboBox()
self.ship_mode.addItem("甘草药房", "gancao")
self.ship_mode.addItem("洛阳药房", "direct")
self.ship_mode.setEnabled(self.can_select_ship_mode)
form.addRow("发货类型", self.ship_mode)
self.is_follow_up = QCheckBox("复诊订单")
form.addRow("是否复诊", self.is_follow_up)
self.medication_days = QSpinBox()
self.medication_days = NoWheelSpinBox()
self.medication_days.setRange(0, 365)
self.medication_days.setSpecialValueText("未填写")
form.addRow("服用天数", self.medication_days)
@@ -5365,7 +5576,7 @@ class PrescriptionOrderDialog(QDialog):
def _build_amount_tab(self) -> QWidget:
tab, form = self._form_tab()
self.fee_type = QComboBox()
self.fee_type = NoWheelComboBox()
self.fee_type.addItem("药品费用", 3)
self.fee_type.addItem("挂号费", 1)
self.fee_type.addItem("问诊费", 2)
@@ -5373,12 +5584,12 @@ class PrescriptionOrderDialog(QDialog):
self.fee_type.addItem("尾款", 5)
self.fee_type.addItem("其他", 6)
form.addRow("费用类别 *", self.fee_type)
self.amount = QDoubleSpinBox()
self.amount = NoWheelDoubleSpinBox()
self.amount.setRange(0, 10_000_000)
self.amount.setDecimals(2)
self.amount.setPrefix("¥ ")
form.addRow("订单金额 *", self.amount)
self.internal_cost = QDoubleSpinBox(tab)
self.internal_cost = NoWheelDoubleSpinBox(tab)
self.internal_cost.setRange(0, 10_000_000)
self.internal_cost.setDecimals(2)
self.internal_cost.setPrefix("¥ ")