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())