Aller au contenu

card —— 卡片

Ce contenu n’est pas encore disponible dans votre langue.

卡片是「內容容器」,但現代元件庫的卡片常常整張可點選。這時它就不再是一個普通 <div>——它需要 button 語義、鍵盤可聚焦、Enter/Space 觸發。core/card 用 getCardInteractivity 把這個「是不是按鈕」的決策顯式化,讓 UI 層據此掛 tabindex 和 role。

  • 各端實作 LhCard:類別名與互動語義由 core 計算。
  • 資訊聚合展示:封面 + 標題 + 描述 + 操作區。
  • 不該用它:需要表單控制項、需要複雜巢狀互動的場景。卡片裡再放按鈕是允許的(操作區),但整卡可點選 + 卡內又有按鈕會造成巢狀互動的無障礙問題,應避免。
import { getCardClassNames, isCardInteractive, getCardInteractivity } from '@lh-ui/share/core/card'
import type { LhCardState, LhCardShadow, LhCardInteractivity } from '@lh-ui/share/core/card'

isCardInteractive 的判定是 clickable && !loading:載入中的卡片不可互動。這個 && 看起來理所當然,但它解決了一個真實問題——卡片載入時通常顯示骨架,此時點選應當被吞掉而不是觸發還沒準備好的操作。

getCardInteractivity 傳回 'interactive' / 'static' 而不是布林, 是為了讓 UI 層的程式碼讀起來像在處理兩種不同的元素:

  • interactive:渲染時掛 role="button"、tabindex="0"、監聽鍵盤 Enter/Space、掛 @click。
  • static:什麼語義都不掛,就是一個容器。

這個分層是有必要的:直接讓 UI 層寫 v-if="clickable" 會讓 role/tabindex/鍵盤事件三處分支各自演化,容易漏。

函式 簽名 傳回 作用
getCardClassNames (state: LhCardState) => string[] 類別名陣列 shadow 檔位、可點選、載入修飾類
isCardInteractive (state: LhCardState) => boolean 布林 clickable && !loading
getCardInteractivity (state: LhCardState) => LhCardInteractivity 'interactive' / 'static' 語義角色決策

輸出範例:

getCardClassNames({ shadow: 'hover', clickable: true, loading: false })
// ['lh-card', 'lh-card--shadow-hover', 'lh-card--clickable']
getCardInteractivity({ shadow: 'hover', clickable: true, loading: false }) // 'interactive'
getCardInteractivity({ shadow: 'always', clickable: false, loading: false }) // 'static'
export type LhCardShadow = 'always' | 'hover' | 'never'
export type LhCardInteractivity = 'interactive' | 'static'
export interface LhCardState {
shadow: LhCardShadow
clickable: boolean
loading: boolean
}

這是本元件的核心。整卡可點選時必須按 APG button pattern 實作,UI 層的三件事缺一不可:

<div
:class="getCardClassNames({ shadow, clickable, loading })"
:role="interactivity === 'interactive' ? 'button' : undefined"
:tabindex="interactivity === 'interactive' ? 0 : undefined"
@click="interactivity === 'interactive' && emit('click')"
@keydown.enter="interactivity === 'interactive' && emit('click')"
@keydown.space.prevent="interactivity === 'interactive' && emit('click')"
>

注意 Space 鍵的 .prevent:原生 button 會阻止 Space 的預設捲動行為,用 div 模擬時必須手動阻止,否則卡片會一邊捲動一邊觸發。

不要為了省事把可點選卡片渲染成 <button>:button 的預設樣式(inline、文字置中)與卡片版面配置衝突,且 button 內部放區塊級內容在舊瀏覽器有問題。div + role 是 APG 認可的等價實作。

import { getCardClassNames, getCardInteractivity } from '@lh-ui/share/core/card'
getCardClassNames({ shadow: 'never', clickable: false, loading: true })
// ['lh-card', 'lh-card--shadow-never', 'lh-card--loading']
// React
const interactivity = getCardInteractivity({ shadow, clickable, loading })
const interactive = interactivity === 'interactive'
return (
<div
className={getCardClassNames({ shadow, clickable, loading }).join(' ')}
role={interactive ? 'button' : undefined}
tabIndex={interactive ? 0 : undefined}
onClick={interactive ? onClick : undefined}
onKeyDown={interactive ? (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick() }
} : undefined}
>
{loading ? <Skeleton /> : children}
</div>
)
// uniapp:view 支援 hover-class,可用 :hover-class 模擬 hover 陰影;
// 鍵盤事件僅 h5 端有效,非 h5 端只回應 tap
類別名 語義
.lh-card 基礎類:圓角、邊框、背景、過渡
.lh-card--shadow-{always|hover|never} 陰影三檔:常顯 / 懸停顯 / 從不顯
.lh-card--clickable 可點選:hover 抬升 + 指標游標 + active 下壓
.lh-card--loading 載入中:內容區被骨架覆蓋
.lh-card__header / .lh-card__title / .lh-card__extra 頭部:標題 + 右側操作位
.lh-card__cover 封面圖區(通欄圓角銜接)
.lh-card__body 主體內容區
.lh-card__footer 底部操作區
.lh-card__loading-line / --narrow / --wide 骨架佔位行

陰影與圓角走 --lh-shadow-* / --lh-radius-* 令牌,可被 ConfigProvider 覆蓋。

  • 巢狀互動陷阱:整卡 role="button" 時,卡內的按鈕/連結會成為 button 的後代,讀屏軟體會跳過它們(button 內不允許互動內容)。若必須同時存在,放棄整卡可點選,把點選區收窄到某個明確的元素。
  • shadow 無預設值兜底:類型要求必傳,UI 層的 props 預設值通常給 'hover'。
  • 載入中的骨架由 .lh-card__loading-line 在樣式層實作,core 只輸出 --loading 類。
  • clickable 為 true 但沒有綁 click 事件是合法的(只是視覺像可點),這是呼叫方的問題不是 core 的問題。
@lh-ui/share/core/card → getCardClassNames( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue