Communitygithub.com

mawwki/pyqt6-md3-ui

使用 PyQt6/PySide6 建立、重新設計或改造桌面 GUI 應用程式,使其看起來和操作起來像一個精緻的 Material Design 3 (MD3) 應用程式 (無邊框視窗、Material 視窗按鈕、帶有藥丸指示器的導航抽屜、浮動卡片、帶有波紋效果的色調/填滿按鈕、MD3 開關/滑桿/核取方塊/分段按鈕、淺色/深色主題)。每當使用者要求讓桌面 UI 變得「漂亮/時尚」、「Material/Material Design/MD3 風格」、「現代化介面」、「無邊框視窗」、「像這個應用程式一樣」(參考 assets/app_template.py),或想要動畫、狀態圖層、波紋效果、抽屜導航、切換開關、滑桿、聊天、拖放功能時使用。包含參考應用程式的完整建構規格…

pyqt6-md3-ui 是什麼?

此技能專為使用 PyQt6 或 PySide6 開發桌面 GUI 應用程式的開發者設計。它提供了一套完整的組件、設計標記 (Design Tokens) 和實作模式,用以在 Qt 框架中精確複製 Google Material Design 3 (MD3) 的外觀和感覺。核心方法是創建一個自訂的、無邊框視窗,並利用 Qt 樣式表 (QSS)、自訂繪製 `QPainter` 和屬性動畫 `QPropertyAnimation` 來模擬 MD3 的視覺效果,如狀態圖層、波紋回饋、特定的圓角比例和流暢的過渡動畫,從而避開原生 Qt 樣式在實現這些現代設計元素時的限制。此技能支援 Cursor、Claude Code 和 Windsurf 等平台。它提供了一個參考應用程式 (`assets/app_template.py`) 和一份詳細的規格文件 (`SKILL.md`),其中定義了從色彩主題 (淺色/深色)、排版、陰影到每個自訂 Widget (例如 MD3 風格按鈕、開關、滑桿、核取方塊、分段控制項、導航抽屜和檔案拖放區域) 的確切參數和行為。開發者可以直接複製並改編這個參考檔案,或根據規格從頭開始重建,從而節省大量開發時間並確保 UI 在各平台 (Windows、Linux、macOS) 上的一致性。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/mawwki/pyqt6-ui-skills/tree/HEAD/pyqt6-md3-ui

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

PyQt6 風格的 Material Design 3 — 參考應用程式規格

以下為完整規格,可根據行為逐字節重現 assets/app_template.py。 將代理人模擬為來自工作室的介面設計師,該工作室製作 MD3 桌面應用程式:角色調色盤 (primary / on-primary / containers / surface / outline)、狀態圖層、波紋效果、按形狀比例圓角、動畫僅用於解釋發生的事件 — 僅此而已。

主要規則:Qt 中的「MD3 風格」並非原生 API,而是手工重現的美學: 無邊框視窗 + 自訂 QSS + QPropertyAnimation 以實現流暢度。如此一來,應用程式在 Windows、Linux 和 macOS 上看起來都一樣。原生 Qt 樣式無法提供圓角、半透明、狀態圖層和動畫。

參考實作 — assets/app_template.py (單一檔案)。代理人的任務:要麼複製並改編它,要麼根據此文件從頭開始重建。


1. 快速開始

pip install PyQt6
python app_template.py

為使用者建置:pyinstaller --windowed --icon=app.ico app_template.py。

應用程式啟動方式:

def main() -> None:
    app = QApplication(sys.argv)
    app.setStyle("Fusion")                       # 唯一尊重所有地方 QSS 的樣式
    app.setFont(system_font(14, QFont.Weight.Normal))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

2. app_template.py 檔案結構 (順序對可讀性很重要)

  1. 匯入:math, sys, time, dataclasses, pathlib.Path;QtCore (Qt, QEvent, QPropertyAnimation, QEasingCurve, QRect(F), QPointF, QParallelAnimationGroup, QVariantAnimation, QTimer, pyqtProperty, pyqtSignal);QtGui (QColor, QFont, QPainter, QPainterPath, QFontDatabase, QFontMetricsF, QPen, QPixmap, QIcon, QPalette);QtSvg (QSvgRenderer);QtWidgets (所有需要的 Widget)。
  2. 設計標記:Theme (dataclass), LIGHT/DARK, 半徑/持續時間常數, REPLIES, SVG_EYE/SVG_EYE_SLASH, PAGES。
  3. 輔助函式:system_font, rgba, accent_fg, shadow_color, lerp_color, animate_property, draw_checkmark。
  4. 自訂 Widget 按依賴順序:MdButton, MdSwitch, MdSlider, MdCheckBox, MdSegmentButton/MdSegmentedControl, DropGlyph/FileDropArea, WindowButton, SidebarToggle, TopAppBar, NavButton/NavigationDrawer。
  5. build_stylesheet(theme)。
  6. MainWindow + main()。

3. 設計標記 (精確值,MD3 角色)

Theme — frozen dataclass,所有顏色皆為 hex 字串,MD3 角色: primary, on_primary, primary_container, on_primary_container, secondary_container, on_secondary_container, surface, surface_container_low, surface_container, surface_container_high, surface_container_highest, on_surface, on_surface_variant, outline, outline_variant, error, on_error, error_container, on_error_container, shadow_alpha (int 0..255,陰影總是黑色)。

LIGHT = Theme(primary="#6750A4", on_primary="#FFFFFF",
              primary_container="#EADDFF", on_primary_container="#21005D",
              secondary_container="#E8DEF8", on_secondary_container="#1D192B",
              surface="#FEF7FF", surface_container_low="#F7F2FA",
              surface_container="#F3EDF7", surface_container_high="#ECE6F0",
              surface_container_highest="#E6E0E9", on_surface="#1D1B20",
              on_surface_variant="#49454F", outline="#79747E", outline_variant="#CAC4D0",
              error="#B3261E", on_error="#FFFFFF",
              error_container="#F9DEDC", on_error_container="#410E0B", shadow_alpha=45)
DARK  = Theme(primary="#D0BCFF", on_primary="#381E72",
              primary_container="#4F378B", on_primary_container="#EADDFF",
              secondary_container="#4A4458", on_secondary_container="#E8DEF8",
              surface="#141218", surface_container_low="#1D1B20",
              surface_container="#211F26", surface_container_high="#2B2930",
              surface_container_highest="#36343B", on_surface="#E6E0E9",
              on_surface_variant="#CAC4D0", outline="#938F99", outline_variant="#49454F",
              error="#F2B8B5", on_error="#601410",
              error_container="#8C1D18", on_error_container="#F9DEDC", shadow_alpha=120)

「角色 / on-角色」配對是強制的:primary 表面的文字永遠是 on_primary,secondary_container 表面上是 on_secondary_container,primary_container 表面上是 on_primary_container。不得硬編碼「white」/「black」。accent_fg(theme) 保留作為相容性輔助函式,並回傳 theme.on_primary。

def rgba(hex_color: str, alpha_0_255: int) -> str:   # QSS 期望 alpha 值為 0..255,非 0..1
    c = QColor(hex_color)
    return f"rgba({c.red()}, {c.green()}, {c.blue()}, {alpha_0_255})"

def shadow_color(theme: Theme) -> QColor:
    c = QColor(0, 0, 0); c.setAlpha(theme.shadow_alpha); return c

常數:

  • 形狀:RADIUS_WINDOW = 16, RADIUS_CARD = 12, RADIUS_MENU = 8, RADIUS_FIELD = 4, SIDEBAR_WIDTH = 220。按鈕/藥丸狀 — radius = height/2;晶片/分段 — full。
  • EDGE_MARGIN = 12 (與根元件的陰影間距一致),邊緣位遮罩 EDGE_TOP, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT = 1, 2, 4, 8。
  • DUR_FAST = 140 (hover), DUR_PRESS = 90, DUR_RELEASE = 200, DUR_MED = 250 (頁面, drawer, 切換開關, 分段), DUR_RIPPLE = 450; EASE = OutCubic, EASE_PRESS = OutQuad。
  • PAGES = ["概覽", "任務", "輸入", "聊天", "檔案", "設定"]。
  • REPLIES — 5 個簡短機器人回覆。
  • SVG_EYE / SVG_EYE_SLASH — Material Symbols visibility / visibility_off (viewBox 0 0 24 24, fill="currentColor")。

排版:system_font(point_size=14, weight) — 鏈 Roboto → Google Sans → Noto Sans → Segoe UI, setStyleStrategy(PreferAntialias)。尺寸:頁面標題 24, 卡片標題 16/Medium,按鈕文字 14/Medium,body 14,次要文字 12,時間 11。 間距來自尺度 4/8/12/16/24/32。

陰影:視窗/面板 — blurRadius 24, offset (0,4);卡片 (elevation 1) — blurRadius 8, offset (0,1);顏色 shadow_color(theme)。

狀態圖層 (覆蓋在容器上,顏色為「on-角色」):hover 0.08, press 0.12 (即 0.08*hover + 0.04*press);滑桿 hover 0.10 / press 0.16;核取方塊 0.06 (unchecked) / 0.10 (checked)。


4. 視窗框架

self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setMinimumSize(760, 480)
self.resize(960, 600)
self.setMouseTracking(True)
QApplication.instance().installEventFilter(self)   # 用於邊緣調整大小

元件樹 (_build_ui):QMainWindow → root (QWidget, margins 12) → panel (QFrame#Panel, drop-shadow) → TopAppBar (56px) + body (QHBoxLayout) → NavigationDrawer (220px) + separator (1px, outline_variant) + QStackedWidget。 面板接收 build_stylesheet(theme) — QSS 向下繼承。額外 _apply_palette(theme) 設定 QPalette 的角色 (Window/Base=surface, Text=on_surface, Highlight=primary, HighlightedText=on_primary, PlaceholderText=on_surface_variant, ToolTipBase=surface_container_high…) 用於原生繪製的部分。

陰影和圓角位於子元件 QFrame#Panel 上,而非視窗本身:視窗是透明的 並預留 12px 的陰影間距。不能將效果應用於 QMainWindow。

拖曳與調整大小

移動視窗 — Qt6 的原生機制 (在 TopAppBar.mousePressEvent 中):

handle = self._window.windowHandle()
if handle is not None:
    handle.startSystemMove()

雙擊 TopAppBar — 最大化/還原。

更改邊緣大小 — 原生 QWindow.startSystemResize。在 MainWindow 中設定了 事件過濾器在 QApplication 上:在 MouseButtonPress(Left) 且位於 ≤ EDGE_MARGIN 的區域時 呼叫 _start_system_resize(edges) 並回傳 True;在 MouseMove (無按鈕按下) 時 更新游標;在 Leave 時重置游標。

_resize_edges_for(pos) → 邊緣位遮罩 (僅當視窗可見且未最大化時)。 _cursor_for_edges:中心 → Arrow;左/右 → SizeHor;上/下 → SizeVer; 上+左 或 下+右 → SizeFDiag;否則 → SizeBDiag。

PyQt6 陷阱: Qt.Edge 不能轉換為 int,也不能與 int 透過 | 混合使用。 邊緣數學使用自訂的位常數 EDGE_*,標誌這樣組合:

flag = Qt.Edge(0)
if edges & EDGE_TOP:    flag |= Qt.Edge.TopEdge
if edges & EDGE_BOTTOM: flag |= Qt.Edge.BottomEdge
if edges & EDGE_LEFT:   flag |= Qt.Edge.LeftEdge
if edges & EDGE_RIGHT:  flag |= Qt.Edge.RightEdge
handle.startSystemResize(flag)

5. 自訂 Widget (精確參數)

MdButton (filled / tonal, state layer + ripple)

  • QPushButton,高度 40,藥丸半徑 20,字型 14/Medium,padding 0 24px, minWidth = text + 48。kind="filled" → bg primary/fg on_primary; kind="tonal" → bg secondary_container/fg on_secondary_container。
  • QSS:background: transparent; color: {fg} — 背景手動繪製,文字透過 super().paintEvent()。
  • Q_PROPERTY hover/press/ripple (float)。State layer 顏色為 fg: alphaF = 0.08*hover + 0.04*press,裁剪至圓角矩形。
  • Ripple:mousePressEvent 記錄點 (_ripple_origin),pressed 啟動 _ripple_anim 0→1 (DUR_RIPPLE);在 paint 中 — 從該點開始的橢圓,半徑為 max_r*ripple (max_r = 到最遠角的距離 + 4),alphaF = 0.18*(1-ripple), 裁剪至藥丸形狀。動畫的參考保留在屬性中。

MdSwitch (MD3 切換開關)

  • QWidget 52×32,僅在點擊時發送訊號 toggled(bool);setChecked 靜默動畫。
  • Q_PROPERTY knobPos (0..1) DUR_MED/EASE,加上 hover/press。
  • Off:軌道 surface_container_highest + 邊框 2px outline;手柄 16px 顏色為 outline,中心 x=16。On:軌道 primary 無邊框;手柄 24px on_primary, 中心 x=36,內部有勾選標記顏色為 primary (draw_checkmark, pen 2px)。Press:手柄 28px。
  • 軌道/手柄透過 lerp_color 根據 knobPos 內插;邊框在 t→1 時消失 (alpha = 255*(1-t));state layer 圓形 40px 圍繞手柄。

MdSlider

  • QWidget 高度 28,valueChanged(int)。軌道 4px radius 2:非活躍 surface_container_highest,活躍 primary。手柄 — 圓形 20px primary + 輕微 陰影 (黑色 alpha 30, offset y+1)。State layer 40px primary alphaF = 0.10*hover + 0.06*press。
  • 從 x 取得值:pad = 10 (手柄半徑),ratio = (x-pad)/(width-2*pad);拖曳時 — 無慣性,否則 fill 動畫 DUR_FAST。

MdCheckBox

  • QWidget 高度 24,字型 14,minWidth = 34 + text。方框 18×18 radius 2 at x=3。
  • Unchecked:邊框 2px on_surface_variant。Checked:填滿 primary + 勾選標記 on_primary (pen 2px RoundCap/RoundJoin),勾選標記「逐步繪製」 (check 0..1,兩條線段透過 drawPath)。
  • State layer 36px:on_surface 0.06 (unchecked) / primary 0.10 (checked) * hover。
  • boxScale (OutBack) 在 press 時壓縮方框;setChecked(checked, animate) 發出 toggled;在 mouseReleaseEvent 中僅當游標在矩形內時才觸發點擊。

MdSegmentedControl / MdSegmentButton

  • 容器高度 40,藥丸半徑 20,邊框 1px outline (在 paintEvent 中繪製)。
  • 段等寬 (addWidget(btn, 1)),高度 38,字型 14/Medium; margins (1,1,1,1), spacing 0。在未選取的相鄰段之間的分隔線 1px outline (在 paintEvent 中繪製,使用整數座標)。
  • 指示器 — QFrame#SegIndicator (bg secondary_container, radius 19) 位於按鈕後方, QPropertyAnimation(geometry) DUR_MED;在 showEvent 中 — lower() + 幾何無動畫。
  • 選取的段:文字 on_secondary_container + 文字左側的勾選標記 (draw_checkmark);未選取:on_surface。文字顏色動畫 (color QColor property)。段的 State layer — 藥丸形狀,顏色為 on-角色。

DropGlyph / FileDropArea

  • Glyph 38×38:向下箭頭 + 托盤,顏色 lerp(on_surface_variant, primary, level), halo primary alpha 0.10*level。
  • FileDropArea(QFrame):minHeight 150,setAcceptDrops(True)。背景 surface_container_low radius 16;虛線 2px outline_variant (dashPattern [4,3.5]); 在 hover/drag 時 — 虛線 primary + overlay primary_container alpha ≈ 60glow+20press。
  • dragEnterEvent(hasUrls) / dragLeaveEvent / dropEvent (僅 isLocalFile()) → dropped(list[str]);點擊 → QFileDialog.getOpenFileNames。所有訊號邏輯保持不變。

WindowButton (視窗按鈕)

  • QPushButton 40×40, radius 20, Q_PROPERTY hover/press, set_maximized(bool)。
  • 字形 QPen 1.8 RoundCap/RoundJoin,整數座標:最小化 — 水平線; 最大化 — 正方形 12×12 (outline),在最大化狀態時 — 兩個重疊的正方形; 關閉 — X 形。
  • 字形顏色 lerp(on_surface_variant, on_surface, hover);關閉按鈕在 hover 時 — 背景 error (圓形 40px) + 字形 on_error。其他按鈕在 hover 時 — state layer on_surface 0.08。
  • 從右到左的順序:最小化, 最大化, 關閉。

SidebarToggle

  • QPushButton 40×40, state layer on_surface 0.08, 字形 lerp(on_surface_variant, on_surface, hover)。
  • 字形:邊框 16×16 radius 2 + 分隔線;開啟的面板 — 左側分隔線 + 左箭頭 < (panel-left-close);摺疊的面板 — 右側分隔線 (panel-left-open)。
  • set_collapsed(bool);同步 MainWindow.toggle_sidebar。

TopAppBar

  • QWidget#TopAppBar 高度 56,在 Panel 上方透明;margins (8,0,8,0), spacing 4。
  • 左側是 SidebarToggle,然後是標題「我的專案」18/Medium on_surface,stretch,右側 是 WindowButton 最小化/最大化/關閉。_max_btn + _sync_max_state 透過 eventFilter 在 WindowStateChange 時處理。startSystemMove + 雙擊最大化。

NavButton / NavigationDrawer

  • NavButton(QPushButton#NavButton) 高度 48,字型 14,可勾選;圖示 20×20 at x=16 使用 QPainter 繪製 (Material Symbols outlined):dashboard (4 個正方形), tasks (圓形+勾選), edit (鉛筆), chat (對話框), folder, tune (3 條線 + 旋鈕)。文字透過 QSS padding-left: 48px。
  • nextCheckState 設定 checked = True;取消勾選僅能透過父容器 (NavigationDrawer) 進行。

NavigationDrawer(QWidget) 220px (SIDEBAR_WIDTH):頂部內邊距 12px,標題「功能表」 16/Medium on_surface_variant,spacing 4。按下 NavButton → 在 NavButton 上設定 checked, 發送 page_changed(int),在 MainWindow 中切換 QStackedWidget。

MdButton 和 NavButton 的 paintEvent 實作會在背景上繪製 state layer (使用 QPainterPath 裁剪) 和/或 ripple (如果正在動畫中),然後 再呼叫 super().paintEvent() 繪製文字/圖示。所有自訂小工具都會重新實作 paintEvent (以及某些情況下的 mousePressEvent/mouseReleaseEvent/enterEvent/leaveEvent); 如果小工具有 Q_PROPERTY,則也會實作 sizeHint/minimumSizeHint。


6. 切換主題、頁面和字型縮放

  • MainWindow.set_light_theme() / set_dark_theme():更新主題,重新建置樣式表, 更新 _apply_palette,為所有一次性的調整發出 theme_changed。透過 Qt.FindChildren 找到的圖示在主題變更時重新繪製 (update());動畫 QPropertyAnimation(themePos, 0→1) 在 lerp_color 中用於背景和所有有 Q_PROPERTY 顏色的 QSS 屬性。
  • MainWindow.actionLightTheme.emit() / actionDarkTheme.emit() → 觸發。
  • 事件過濾器重新繪製所有 children (child.update()),導致 ~16ms 的閃爍。
  • 頁面切換:MainWindow 中的 page_changed(int) → QPropertyAnimation 在 QStackedWidget 的 currentIndex 上 (DUR_MED/EASE)。堆疊中的每個頁面都是 QWidget,帶有自訂內容。
  • 字型縮放:QWidget.setFont(…),其中 … = system_font(size, weight)。 對所有元素使用 eventFilter 惰性調整過於複雜;僅參考 MainWindow.theme 和 MainWindow.fontSize。

7. 頁面內容 (精確複製行為)

概覽 (Dashboard)

  • 1 個排成兩列的卡片。RADIUS_CARD、陰影、邊框 outline_variant。
  • 每個卡片包含:圖示 (20px primary_container / on_primary_container, 藥丸半徑 10) + 標題 on_surface 16 Medium + 副標題 on_surface_variant 14。第三張卡片橫跨整行, 包含「活動」的 QListWidget + scrollbar。

任務 (Tasks)

  • QListWidget 包含 5 個可勾選項目 (核取方塊) + MdButton「新增任務」 (tonal, 文字 on_secondary_container)。每個項目:QWidget + MdCheckBox。

輸入 (Input)

  • QLineEdit (radius 4, bg surface_container_highest, 邊框 outline)
    • MdButton「提交」+ 標籤顯示顯示的文字。準確複製。

聊天 (Chat)

  • 3 個聊天下一步按鈕;QScrollArea + QVBoxLayout 包含氣泡。 用戶氣泡:bg primary_container, radius 12, on_primary_container, 右對齊。機器人氣泡:bg surface_container, radius 12, on_surface, 左對齊。底部輸入區域:QLineEdit + MdButton「傳送」。

檔案 (Files)

  • FileDropArea,其下是一個帶有 QListWidget 的卡片。 選擇的檔案路徑以列表形式顯示。

設定 (Settings)

  • 3 個帶有 MdSwitch、MdSlider、MdSegmentedControl 的區塊。 每個區塊:圖示 + 標籤 + Widget。準確複製。

8. 樣式表 (QSS) 關鍵點

build_stylesheet(theme) 回傳一個大型 f-string,用於 QFrame#Panel 及其內部。 關鍵選擇器:

  • QMainWindow, QWidget:background: transparent;
  • QFrame#Panel:background: {theme.surface}; border-radius: {RADIUS_WINDOW}px;
  • QPushButton#NavButton:text-align: left; padding-left: 48px; border: none; 文字顏色 on_surface,選取時 on_secondary_container。
  • QPushButton 通用:border: none; 以防止 Qt 預設邊框。
  • QLineEdit:radius 4, padding 12px, 邊框 1px outline, bg surface。
  • QScrollBar:寬度 6, radius 3, bg transparent, handle outline_variant。
  • QListWidget:border none, bg transparent, item 內邊距 8px。
  • QFrame#Panel 上的 #shadow 效果:QGraphicsDropShadowEffect 在 Python 中, 由 QSS 的 background 所設定。

9. 硬編碼的陰影和重疊層

  • QGraphicsDropShadowEffect 套用於 QFrame#Panel (blurRadius 24, offset (0,4), color shadow_color(theme))。
  • 效果透過 panel.setGraphicsEffect(effect) 設定,並在主題變更時更新。
  • 動畫期間,效果被隱藏 (effect.setEnabled(False)) 以避免效能問題。
  • 重疊層:QWidget#Overlay (bg on_surface alpha 0.32) 在 drawer 開啟時顯示。 點擊時關閉 drawer。

10. 網路和額外依賴

  • QSvgRenderer:如果存在相關的 SVG 檔案,則用於圖示,但 NavButton 使用 QPainter.drawPath 自訂繪製。預設情況下,所有圖示都是自訂繪製的。
  • pathlib:用於 app_template.py 中的檔案路徑。所有資源都相對於腳本目錄。
  • dataclasses:用於 Theme。

11. 注意事項 (常見陷阱)

  1. QMainWindow 沒有背景:它必須是透明的;陰影位於其子 QFrame#Panel 上。 對 QMainWindow 設定背景會破壞陰影。
  2. Fusion 樣式是強制的:其他樣式 (Windows, macOS) 會忽略邊框半徑和背景。
  3. 所有座標都是整數:QPainter 操作使用 int 座標以避免子像素偽影。
  4. 切換開關/核取方塊的 sizeHint:必須回傳實際尺寸 (52, 24) 以便佈局正確計算。 5.** Q_PROPERTY 名稱必須完全匹配**:hover, press, ripple, knobPos, boxScale, themePos。
  5. 動畫後清理:動畫物件必須保留為屬性 (例如 self._ripple_anim),否則會被垃圾回收。
  6. QEventFilter 中的 MouseMove:必須檢查 event->buttons() == Qt.NoButton 以僅在未按下按鈕時更新游標。
  7. 主題變更:觸發 actionLightTheme.emit() 或 actionDarkTheme.emit() 以重新建置 樣式表。

12. 參考資源

  • assets/app_template.py — 精確的 1:1 實作。
  • Material Design 3 文件:角色、形狀、狀態。
  • PyQt6 文件:QPropertyAnimation, QPainter, QSS。

規格結束。所有值 (顏色、半徑、持續時間) 必須與此文件完全相符。

相關技能