""" 共享 UI 组件库 """ from PyQt5.QtWidgets import ( QWidget, QLabel, QFrame, QPushButton, QVBoxLayout, QHBoxLayout, QGridLayout, QDialog, QFormLayout, QLineEdit ) from PyQt5.QtCore import Qt, pyqtSignal, QTimer # ───────────────────────────────────────────────────────────────────────────── # 1. InlineValidator # ───────────────────────────────────────────────────────────────────────────── class InlineValidator(QWidget): """在输入框下方实时显示验证结果的提示组件""" def __init__(self, parent=None): super().__init__(parent) self._is_valid = None # None=未验证, True=有效, False=无效 layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._label = QLabel() self._label.setWordWrap(True) self._label.setStyleSheet("font-size: 12px;") layout.addWidget(self._label) self.setVisible(False) # ── 公开接口 ────────────────────────────────────────── def set_valid(self, message: str = "✓") -> None: """显示绿色通过提示""" self._is_valid = True self._label.setText(message) self._label.setStyleSheet("font-size: 12px; color: #27ae60;") self.setVisible(True) def set_invalid(self, message: str) -> None: """显示红色错误提示""" self._is_valid = False self._label.setText(message) self._label.setStyleSheet("font-size: 12px; color: #e74c3c;") self.setVisible(True) def clear(self) -> None: """隐藏提示(初始/重置状态)""" self._is_valid = None self._label.setText("") self.setVisible(False) @property def is_valid(self): """当前是否处于有效状态(None=未验证,True=有效,False=无效)""" return self._is_valid # ───────────────────────────────────────────────────────────────────────────── # 2. NotificationToast # ───────────────────────────────────────────────────────────────────────────── class NotificationToast(QFrame): """屏幕右下角弹出的非阻塞消息通知浮层""" clicked = pyqtSignal(str, object) # chat_type, target_id def __init__(self, parent: QWidget): super().__init__(parent) self._timer = QTimer(self) self._timer.setSingleShot(True) self._timer.timeout.connect(self.dismiss) self._chat_type = None self._target_id = None self.setFixedWidth(280) self.setFrameShape(QFrame.StyledPanel) self.setCursor(Qt.PointingHandCursor) self.setStyleSheet( "NotificationToast {" " background: white;" " border-radius: 10px;" " border: 1px solid #e0e0e0;" "}" ) layout = QVBoxLayout(self) layout.setContentsMargins(14, 10, 14, 10) layout.setSpacing(4) self._sender_lbl = QLabel() self._sender_lbl.setStyleSheet("font-weight: bold; font-size: 13px; color: #222;") self._preview_lbl = QLabel() self._preview_lbl.setStyleSheet("font-size: 12px; color: #666;") self._preview_lbl.setWordWrap(True) layout.addWidget(self._sender_lbl) layout.addWidget(self._preview_lbl) self.setVisible(False) def show_message( self, sender: str, preview: str, chat_type: str, target_id, auto_dismiss_ms: int = 4000, ) -> None: self._chat_type = chat_type self._target_id = target_id self._sender_lbl.setText(sender) truncated = preview[:30] + ("…" if len(preview) > 30 else "") self._preview_lbl.setText(truncated) self._reposition() self.setVisible(True) self.raise_() self._timer.stop() self._timer.start(auto_dismiss_ms) def dismiss(self) -> None: self._timer.stop() self.setVisible(False) def _reposition(self) -> None: """绝对定位于父窗口右下角""" parent = self.parentWidget() if parent is None: return margin = 16 x = parent.width() - self.width() - margin y = parent.height() - self.sizeHint().height() - margin self.move(x, y) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.clicked.emit(self._chat_type, self._target_id) self.dismiss() super().mousePressEvent(event) # ───────────────────────────────────────────────────────────────────────────── # 3. EmojiPicker # ───────────────────────────────────────────────────────────────────────────── _EMOJIS = [ "😀", "😂", "😍", "😎", "😭", "😡", "😱", "🤔", "👍", "👎", "👏", "🙏", "🤝", "💪", "🎉", "🔥", "❤️", "💔", "💯", "✨", "🌟", "⭐", "🎵", "🎶", "🍕", "🍔", "🍦", "☕", "🍺", "🎂", "🌈", "🌙", "😊", "😇", "🥰", "😏", "😴", "🤣", "😅", "😬", "🙄", "😤", "🤯", "🥳", "🤩", "😋", "🤗", "🤫", ] class EmojiPicker(QFrame): """简单 emoji 选择面板""" emoji_selected = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.setFrameShape(QFrame.StyledPanel) self.setStyleSheet( "EmojiPicker {" " background: white;" " border-radius: 8px;" " border: 1px solid #d0d7de;" "}" ) grid = QGridLayout(self) grid.setContentsMargins(8, 8, 8, 8) grid.setSpacing(2) cols = 8 for idx, emoji in enumerate(_EMOJIS): btn = QPushButton(emoji) btn.setFixedSize(32, 32) btn.setFlat(True) btn.setCursor(Qt.PointingHandCursor) btn.setStyleSheet( "QPushButton { font-size: 18px; border: none; background: transparent; }" "QPushButton:hover { background: #f0f0f0; border-radius: 4px; }" ) btn.clicked.connect(lambda checked, e=emoji: self._on_emoji_clicked(e)) grid.addWidget(btn, idx // cols, idx % cols) self.setVisible(False) def _on_emoji_clicked(self, emoji: str) -> None: self.emoji_selected.emit(emoji) self.setVisible(False) # ───────────────────────────────────────────────────────────────────────────── # 5. SettingsDialog # ───────────────────────────────────────────────────────────────────────────── class SettingsDialog(QDialog): """用户设置对话框:修改昵称、用户名、密码、通知设置""" profile_update_requested = pyqtSignal(str) change_username_requested = pyqtSignal(str) change_nickname_requested = pyqtSignal(str) change_password_requested = pyqtSignal(str, str) notify_toggled = pyqtSignal(bool) def __init__(self, username: str, nickname: str, parent=None): super().__init__(parent) self.username = username self.setWindowTitle("个人设置") self.setMinimumWidth(440) self.setMaximumHeight(680) self.setModal(True) self._build_ui(username, nickname) self._apply_style() def _build_ui(self, username: str, nickname: str) -> None: root = QVBoxLayout(self) root.setContentsMargins(20, 16, 20, 16) root.setSpacing(12) root.setAlignment(Qt.AlignTop) from PyQt5.QtWidgets import QScrollArea scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}") content = QWidget() cl = QVBoxLayout(content) cl.setSpacing(12) title = QLabel("个人设置") title.setStyleSheet("font-size: 18px; font-weight: bold; color: #1a1a2e;") cl.addWidget(title) section1 = QLabel("基本资料") section1.setStyleSheet("font-size: 12px; font-weight: bold; color: #e94560; margin-top: 2px;") cl.addWidget(section1) form = QFormLayout() form.setSpacing(10) form.setLabelAlignment(Qt.AlignRight) self._username_input = QLineEdit(username) self._username_input.setPlaceholderText("修改用户名(4-20位字母/数字/下划线)") form.addRow("用户名:", self._username_input) self._nickname_input = QLineEdit(nickname) self._nickname_input.setPlaceholderText("修改昵称") form.addRow("昵称:", self._nickname_input) cl.addLayout(form) section2 = QLabel("修改密码(留空则不修改)") section2.setStyleSheet("font-size: 12px; font-weight: bold; color: #e94560; margin-top: 2px;") cl.addWidget(section2) pw_form = QFormLayout() pw_form.setSpacing(10) pw_form.setLabelAlignment(Qt.AlignRight) self._old_password_input = QLineEdit() self._old_password_input.setEchoMode(QLineEdit.Password) self._old_password_input.setPlaceholderText("输入原密码") pw_form.addRow("原密码:", self._old_password_input) self._new_password_input = QLineEdit() self._new_password_input.setEchoMode(QLineEdit.Password) self._new_password_input.setPlaceholderText("输入新密码(6-20位)") pw_form.addRow("新密码:", self._new_password_input) self._confirm_password_input = QLineEdit() self._confirm_password_input.setEchoMode(QLineEdit.Password) self._confirm_password_input.setPlaceholderText("确认新密码") pw_form.addRow("确认密码:", self._confirm_password_input) cl.addLayout(pw_form) from PyQt5.QtWidgets import QCheckBox section3 = QLabel("通知设置") section3.setStyleSheet("font-size: 12px; font-weight: bold; color: #e94560; margin-top: 2px;") cl.addWidget(section3) self._notify_check = QCheckBox("启用桌面通知") self._notify_check.setChecked(True) self._notify_check.setStyleSheet("font-size: 13px; color: #555;") self._notify_check.toggled.connect(self.notify_toggled.emit) cl.addWidget(self._notify_check) cl.addStretch() scroll.setWidget(content) root.addWidget(scroll) self._status_label = QLabel("") self._status_label.setWordWrap(True) self._status_label.setVisible(False) root.addWidget(self._status_label) btn_row = QHBoxLayout() btn_row.addStretch() self._cancel_btn = QPushButton("取消") self._cancel_btn.setCursor(Qt.PointingHandCursor) self._cancel_btn.clicked.connect(self.reject) self._save_btn = QPushButton("保存修改") self._save_btn.setCursor(Qt.PointingHandCursor) self._save_btn.setDefault(True) self._save_btn.clicked.connect(self._on_save) btn_row.addWidget(self._cancel_btn) btn_row.addWidget(self._save_btn) root.addLayout(btn_row) def _apply_style(self) -> None: self.setStyleSheet(""" QDialog { background: #fafbfc; } QLineEdit { padding: 8px 14px; border: 1.5px solid #e0e0e0; border-radius: 8px; font-size: 13px; color: #333; background: white; } QLineEdit:focus { border: 1.5px solid #e94560; background: #fff5f5; } QLabel { color: #444; } QPushButton { padding: 8px 22px; border-radius: 8px; font-size: 13px; } QPushButton#saveBtn { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #e94560,stop:1 #c23152); color: white; border: none; font-weight: bold; } QPushButton#saveBtn:hover { background: #c23152; } QPushButton#cancelBtn { background: #f0f0f0; color: #555; border: 1px solid #ddd; } QPushButton#cancelBtn:hover { background: #e0e0e0; } """) self._save_btn.setObjectName("saveBtn") self._cancel_btn.setObjectName("cancelBtn") def _on_save(self) -> None: new_username = self._username_input.text().strip() new_nickname = self._nickname_input.text().strip() old_password = self._old_password_input.text() new_password = self._new_password_input.text() confirm_password = self._confirm_password_input.text() changed = False if new_nickname and new_nickname != self.username: self.change_nickname_requested.emit(new_nickname) changed = True if new_username and new_username != self.username: if len(new_username) < 4 or len(new_username) > 20: self._show_status("用户名需4-20位", "error") return if not all(c.isalnum() or c == '_' for c in new_username): self._show_status("用户名只能包含字母、数字和下划线", "error") return self.change_username_requested.emit(new_username) self.username = new_username changed = True if old_password or new_password or confirm_password: if not old_password: self._show_status("请输入原密码", "error") return if not new_password: self._show_status("请输入新密码", "error") return if len(new_password) < 6 or len(new_password) > 20: self._show_status("新密码需6-20位", "error") return if new_password != confirm_password: self._show_status("两次密码不一致", "error") return self.change_password_requested.emit(old_password, new_password) changed = True if changed: self._show_status("设置已保存", "success") else: self._show_status("没有需要修改的内容", "info") def _show_status(self, message, kind): self._status_label.setText(message) if kind == "error": self._status_label.setStyleSheet( "font-size: 12px; padding: 6px 12px; border-radius: 6px; background: #ffeaea; color: #c0392b;") elif kind == "success": self._status_label.setStyleSheet( "font-size: 12px; padding: 6px 12px; border-radius: 6px; background: #eafaf1; color: #27ae60;") else: self._status_label.setStyleSheet( "font-size: 12px; padding: 6px 12px; border-radius: 6px; background: #eaf2f8; color: #2980b9;") self._status_label.setVisible(True)