290 lines
15 KiB
Python
290 lines
15 KiB
Python
class ReviewAssistantPicker(QDialog):
|
||
recipientsLoaded = Signal(object)
|
||
recipientValidated = Signal(object)
|
||
|
||
def __init__(self, account, parent=None, *, current_recipient_id=""):
|
||
super().__init__(parent)
|
||
self.account = str(account)
|
||
self.current_recipient_id = str(current_recipient_id or "")
|
||
self.recipient = None
|
||
self._serial = 0
|
||
self._offset = 0
|
||
self._closed = False
|
||
self._validating = False
|
||
self._validation_id = ""
|
||
self._filter = "sendable"
|
||
self._has_more = False
|
||
self.setWindowTitle("选择医疗助理私人微信")
|
||
self.resize(740, 640)
|
||
self.setMinimumSize(620, 560)
|
||
layout = QVBoxLayout(self)
|
||
layout.setSpacing(9)
|
||
note = QLabel("点击联系人查看详情,再确认设为医疗助理。找不到时,先刷新「微信消息库」,再刷新此列表。")
|
||
note.setWordWrap(True)
|
||
layout.addWidget(note)
|
||
self.configured = QLabel()
|
||
self.configured.setWordWrap(True)
|
||
self.configured.setTextFormat(Qt.TextFormat.PlainText)
|
||
self.configured.setText("当前已设助理:" + self.current_recipient_id if self.current_recipient_id else "当前尚未设置医疗助理")
|
||
layout.addWidget(self.configured)
|
||
search = QHBoxLayout()
|
||
self.query = QLineEdit()
|
||
self.query.setPlaceholderText("搜索备注、昵称或联系编号")
|
||
self.query.setMaxLength(100)
|
||
search.addWidget(self.query, 1)
|
||
self.refresh = QPushButton("刷新列表")
|
||
self.refresh.setToolTip("仅重读本机已缓存的联系人。请先在微信消息库刷新。")
|
||
search.addWidget(self.refresh)
|
||
layout.addLayout(search)
|
||
filters = QHBoxLayout()
|
||
self.filter_buttons = {}
|
||
for key, title in (("sendable", "可发送"), ("directory", "通讯录未关联"), ("all", "全部")):
|
||
button = QPushButton(title)
|
||
button.setCheckable(True)
|
||
button.setChecked(key == self._filter)
|
||
button.setStyleSheet("QPushButton:checked { background: #E7F0FF; color: #1557C0; border: 1px solid #578CDE; font-weight: 600; }")
|
||
button.clicked.connect(lambda _checked=False, value=key: self._set_filter(value))
|
||
self.filter_buttons[key] = button
|
||
filters.addWidget(button)
|
||
filters.addStretch(1)
|
||
self.order = QComboBox()
|
||
self.order.addItem("最近联系优先", "recent")
|
||
self.order.addItem("按名称排序", "name")
|
||
self.order.setAccessibleName("联系人排序")
|
||
filters.addWidget(self.order)
|
||
layout.addLayout(filters)
|
||
self.contacts = QListWidget()
|
||
self.contacts.setObjectName("ReviewAssistantContacts")
|
||
self.contacts.setWordWrap(False)
|
||
self.contacts.setTextElideMode(Qt.TextElideMode.ElideRight)
|
||
self.contacts.setSpacing(3)
|
||
self.contacts.setMinimumHeight(140)
|
||
self.contacts.setStyleSheet("""
|
||
QListWidget#ReviewAssistantContacts { background: #FFFFFF; border: 1px solid #D8E2F0; border-radius: 8px; padding: 4px; }
|
||
QListWidget#ReviewAssistantContacts::item { color: #263954; padding: 7px 10px; border: 1px solid transparent; border-radius: 6px; }
|
||
QListWidget#ReviewAssistantContacts::item:hover { background: #F0F5FF; }
|
||
QListWidget#ReviewAssistantContacts::item:selected,
|
||
QListWidget#ReviewAssistantContacts::item:selected:active,
|
||
QListWidget#ReviewAssistantContacts::item:selected:!active { color: #FFFFFF; background: #2563CB; border: 1px solid #1550B1; }
|
||
""")
|
||
layout.addWidget(self.contacts, 1)
|
||
self.status = QLabel("正在只读加载联系人…")
|
||
self.status.setWordWrap(True)
|
||
layout.addWidget(self.status)
|
||
details = QFrame()
|
||
details.setObjectName("ReviewAssistantDetails")
|
||
details.setStyleSheet("QFrame#ReviewAssistantDetails { background: #F3F7FE; border: 1px solid #D8E2F0; border-radius: 8px; }")
|
||
detail_layout = QVBoxLayout(details)
|
||
detail_layout.setSpacing(3)
|
||
self.details_name = QLabel()
|
||
self.details_name.setStyleSheet("font-weight: 600; color: #174EA8;")
|
||
self.details_identity = QLabel()
|
||
self.details_last_time = QLabel()
|
||
self.details_reason = QLabel()
|
||
for label in (self.details_name, self.details_identity, self.details_last_time, self.details_reason):
|
||
label.setWordWrap(True)
|
||
label.setTextFormat(Qt.TextFormat.PlainText)
|
||
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||
detail_layout.addWidget(label)
|
||
layout.addWidget(details)
|
||
buttons = QHBoxLayout()
|
||
self.previous = QPushButton("上一页")
|
||
self.next = QPushButton("下一页")
|
||
self.choose = QPushButton("设为医疗助理")
|
||
self.choose.setStyleSheet("QPushButton:enabled { background: #2563CB; color: #FFFFFF; border: 1px solid #1550B1; border-radius: 5px; padding: 7px 14px; }")
|
||
cancel = QPushButton("取消")
|
||
for widget in (self.previous, self.next, self.choose, cancel):
|
||
buttons.addWidget(widget)
|
||
layout.addLayout(buttons)
|
||
self._search_timer = QTimer(self)
|
||
self._search_timer.setSingleShot(True)
|
||
self._search_timer.setInterval(250)
|
||
self._search_timer.timeout.connect(lambda: self._load(reset=True))
|
||
self.query.textChanged.connect(self._query_changed)
|
||
self.order.currentIndexChanged.connect(lambda _index: self._load(reset=True))
|
||
self.contacts.currentItemChanged.connect(self._selection_changed)
|
||
self.refresh.clicked.connect(lambda: self._load(reset=True))
|
||
self.previous.clicked.connect(lambda: self._page(-50))
|
||
self.next.clicked.connect(lambda: self._page(50))
|
||
self.choose.clicked.connect(self._choose)
|
||
cancel.clicked.connect(self.reject)
|
||
self.finished.connect(self._finished)
|
||
self.recipientsLoaded.connect(self._loaded)
|
||
self.recipientValidated.connect(self._validated)
|
||
self._selection_changed(None)
|
||
QTimer.singleShot(0, self, self._load)
|
||
|
||
def _finished(self, _result):
|
||
self._closed = True
|
||
self._search_timer.stop()
|
||
|
||
@staticmethod
|
||
def _display_name(row):
|
||
if row.get("nameKnown") is False:
|
||
return str(row.get("displayName") or ("未读取昵称 · 尾号" + str(row.get("id") or "")[-4:]))
|
||
return str(row.get("displayName") or row.get("name") or "未读取昵称")
|
||
|
||
def _item_text(self, row, *, selected=False):
|
||
state = "可设为助理" if row.get("selectable", True) else "通讯录 · 尚未关联可发送会话"
|
||
if str(row.get("id") or "") == self.current_recipient_id:
|
||
state = "已设为助理 · " + state
|
||
return ("✓ " if selected else "") + self._display_name(row) + "\n" + state
|
||
|
||
def _selection_changed(self, item, old=None):
|
||
if old is not None:
|
||
old_row = old.data(Qt.ItemDataRole.UserRole)
|
||
if old_row:
|
||
old.setText(self._item_text(old_row))
|
||
row = item.data(Qt.ItemDataRole.UserRole) if item is not None else None
|
||
can_choose = bool(row) and row.get("selectable", True) and not self._validating
|
||
self.choose.setEnabled(bool(can_choose))
|
||
if not row:
|
||
self.details_name.setText("尚未选择联系人")
|
||
self.details_identity.setText("点击上方联系人,在这里核对完整编号。")
|
||
self.details_last_time.setText("")
|
||
self.details_reason.setText("")
|
||
return
|
||
item.setText(self._item_text(row, selected=True))
|
||
name = self._display_name(row)
|
||
self.details_name.setText("当前查看:" + name)
|
||
identity = str(row.get("identityLabel") or row.get("id") or "—")
|
||
conversation_id = str(row.get("id") or "")
|
||
self.details_identity.setText("联系编号:" + identity + ("\n会话标识:" + conversation_id if row.get("selectable", True) and conversation_id not in identity else ""))
|
||
self.details_last_time.setText("最近联系:" + str(row.get("lastTime") or "暂无已关联的会话记录"))
|
||
if row.get("selectable", True):
|
||
self.details_reason.setText("此联系人已设为医疗助理。" if conversation_id == self.current_recipient_id else "已关联可发送的私人微信单聊。核对后,点击「设为医疗助理」。")
|
||
else:
|
||
self.details_reason.setText("暂不能设为助理:" + str(row.get("unavailableReason") or "尚无可发送的私人微信会话,请建立单聊并刷新消息库。"))
|
||
|
||
def _query_changed(self, _text):
|
||
if self._closed or self._validating:
|
||
return
|
||
self._serial += 1
|
||
self.contacts.clear()
|
||
self._selection_changed(None)
|
||
self.previous.setEnabled(False)
|
||
self.next.setEnabled(False)
|
||
self.status.setText("正在等待搜索…")
|
||
self._search_timer.start()
|
||
|
||
def _set_filter(self, value):
|
||
if self._closed or self._validating:
|
||
return
|
||
self._filter = value
|
||
for key, button in self.filter_buttons.items():
|
||
button.setChecked(key == value)
|
||
self._load(reset=True)
|
||
|
||
def _page(self, delta):
|
||
if self._closed or self._validating:
|
||
return
|
||
self._offset = max(0, self._offset + delta)
|
||
self._load()
|
||
|
||
def _load(self, *, reset=False):
|
||
if self._closed or self._validating:
|
||
return
|
||
self._search_timer.stop()
|
||
if reset:
|
||
self._offset = 0
|
||
self._serial += 1
|
||
serial, query, offset = self._serial, self.query.text(), self._offset
|
||
status, order = self._filter, str(self.order.currentData() or "recent")
|
||
self.choose.setEnabled(False)
|
||
self.previous.setEnabled(False)
|
||
self.next.setEnabled(False)
|
||
self.refresh.setEnabled(False)
|
||
self.contacts.clear()
|
||
self._selection_changed(None)
|
||
self.status.setText("正在只读加载联系人…")
|
||
def work():
|
||
try:
|
||
from review_assistant_contacts import list_recipients
|
||
result = list_recipients(self.account, query, 50, offset, status=status, order=order)
|
||
except Exception as exc:
|
||
result = {"ready": False, "reason": str(exc), "items": []}
|
||
result["_serial"] = serial
|
||
try:
|
||
self.recipientsLoaded.emit(result)
|
||
except RuntimeError:
|
||
pass
|
||
threading.Thread(target=work, name="review-assistant-contacts", daemon=True).start()
|
||
|
||
def _loaded(self, result):
|
||
if self._closed or result.get("_serial") != self._serial:
|
||
return
|
||
self.refresh.setEnabled(True)
|
||
if not result.get("ready") or str(result.get("account_id")) != self.account:
|
||
self.status.setText(str(result.get("reason") or "当前企微账号已变化,请重新选择"))
|
||
return
|
||
for row in result.get("items", []):
|
||
item = QListWidgetItem(self._item_text(row))
|
||
item.setData(Qt.ItemDataRole.UserRole, dict(row))
|
||
item.setToolTip(self._display_name(row))
|
||
self.contacts.addItem(item)
|
||
if str(row.get("id") or "") == self.current_recipient_id:
|
||
self.configured.setText("当前已设助理:" + self._display_name(row) + " · " + self.current_recipient_id)
|
||
self._has_more = bool(result.get("has_more"))
|
||
self.previous.setEnabled(self._offset > 0)
|
||
self.next.setEnabled(self._has_more)
|
||
total = int(result.get("total") or 0)
|
||
counts = {
|
||
"sendable": int(result.get("all_selectable_total", result.get("selectable_total", total)) or 0),
|
||
"directory": int(result.get("all_contact_total", result.get("contact_total", 0)) or 0),
|
||
"all": int(result.get("all_total", total) or 0),
|
||
}
|
||
for key, title in (("sendable", "可发送"), ("directory", "通讯录未关联"), ("all", "全部")):
|
||
self.filter_buttons[key].setText(f"{title} ({counts[key]})")
|
||
if total:
|
||
self.status.setText(f"共 {total} 位 · 第 {self._offset + 1}–{self._offset + self.contacts.count()} 位;点击联系人查看详情。")
|
||
elif self._filter == "sendable" and counts["directory"]:
|
||
self.status.setText("没有可发送的匹配联系人。可切换「通讯录未关联」查看,再建立单聊并刷新消息库。")
|
||
else:
|
||
self.status.setText("没有匹配联系人。可尝试「全部」、搜索备注或编号,或刷新微信消息库。")
|
||
|
||
def _choose(self):
|
||
item = self.contacts.currentItem()
|
||
if item is None or self._closed or self._validating:
|
||
return
|
||
row = item.data(Qt.ItemDataRole.UserRole)
|
||
if not row or not row.get("selectable", True):
|
||
self.choose.setEnabled(False)
|
||
return
|
||
self._validating = True
|
||
self._validation_id = str(row.get("id") or "")
|
||
self.choose.setEnabled(False)
|
||
for widget in (self.query, self.contacts, self.order, self.previous, self.next, self.refresh, *self.filter_buttons.values()):
|
||
widget.setEnabled(False)
|
||
self.status.setText("正在复核当前企微账号和所选联系人…")
|
||
self._search_timer.stop()
|
||
self._serial += 1
|
||
serial, recipient_id = self._serial, self._validation_id
|
||
def work():
|
||
try:
|
||
from review_assistant_contacts import validate_recipient
|
||
result = validate_recipient(self.account, recipient_id)
|
||
except Exception as exc:
|
||
result = {"ok": False, "reason": str(exc)}
|
||
result["_serial"] = serial
|
||
try:
|
||
self.recipientValidated.emit(result)
|
||
except RuntimeError:
|
||
pass
|
||
threading.Thread(target=work, name="review-assistant-recipient-check", daemon=True).start()
|
||
|
||
def _validated(self, result):
|
||
if self._closed or result.get("_serial", self._serial) != self._serial:
|
||
return
|
||
self._validating = False
|
||
recipient = result.get("recipient") or {}
|
||
if result.get("ok") and result.get("account_id") == self.account and str(recipient.get("id") or "") == self._validation_id:
|
||
self.recipient = recipient
|
||
self.accept()
|
||
else:
|
||
self.status.setText(str(result.get("reason") or "联系人核验失败,请重新读取当前账号"))
|
||
for widget in (self.query, self.contacts, self.order, self.refresh, *self.filter_buttons.values()):
|
||
widget.setEnabled(True)
|
||
self.previous.setEnabled(self._offset > 0)
|
||
self.next.setEnabled(self._has_more)
|
||
self._selection_changed(self.contacts.currentItem())
|