UX 設計指南
目標讀者: 前端開發者、UI/UX 設計師、AI 助手
本文檔定義參考實作(
src/)應遵循的使用者體驗設計原則。這些原則確保應用程式具有一致、直覺的互動體驗。
目錄
1. 錯誤處理策略
設計原則
優先使用 Inline Alert,無表單時才使用阻塞式對話框
錯誤訊息的顯示位置應該讓使用者能夠:
- 立即理解問題所在
- 方便地修正錯誤
- 不被額外的對話框打斷
錯誤處理決策表
| 情境 | 處理方式 | 原因 |
|---|---|---|
| 表單頁面(Editor) | Inline Alert | 用戶可直接修正後重試 |
| 對話框內有表單 | Dialog 內 Inline Alert | 避免對話框疊對話框 |
| 無表單操作(列表動作) | prompt.error() | 操作已觸發,需確保用戶知悉 |
實作範例
情境一:表單頁面錯誤
function OrderEditor() {
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (data: FormData) => {
try {
setError(null)
await orderService.create(data)
prompt.success(t('Order created successfully'))
navigate('/orders')
} catch (err) {
// 錯誤顯示在表單上方
setError(err instanceof ErrorResponse ? err.message : t('Failed to create order'))
}
}
return (
<form onSubmit={handleSubmit}>
{/* Inline Alert */}
{error && (
<div className="alert alert-error mb-4">
<span>{error}</span>
</div>
)}
{/* 表單內容 */}
</form>
)
}
情境二:對話框內有表單
// 父組件
function CustomerFinder() {
const [deactivateError, setDeactivateError] = useState<string | null>(null)
const handleConfirmDeactivate = async (reason: string) => {
try {
setDeactivateError(null)
await customerService.deactivate(id, reason)
prompt.success(t('Customer deactivated'))
closeDialog()
} catch (err) {
// 錯誤顯示在對話框內,而非 prompt.error()
setDeactivateError(err instanceof ErrorResponse ? err.message : t('Failed'))
}
}
return (
<DeactivateDialog
error={deactivateError}
onConfirm={handleConfirmDeactivate}
onCancel={() => {
setDeactivateError(null)
closeDialog()
}}
/>
)
}
// 對話框組件
function DeactivateDialog({ error, onConfirm, onCancel }) {
return (
<Dialog>
{/* Inline Alert 在對話框內 */}
{error && (
<div className="alert alert-error mb-4">
<span>{error}</span>
</div>
)}
{/* 表單內容 */}
</Dialog>
)
}
情境三:無表單的列表操作
// 列表頁面的快速操作(無對話框承載錯誤)
const handleQuickDelete = async (id: string) => {
try {
await orderService.delete(id)
prompt.success(t('Order deleted'))
} catch (err) {
// 沒有對話框,使用 prompt.error()
prompt.error(err instanceof ErrorResponse ? err.message : t('Delete failed'))
}
}
錯誤清除時機
- 開啟對話框時:清除之前的錯誤
- 重新提交時:清除錯誤後再執行
- 關閉對話框時:清除錯誤
2. 使用者通知
設計原則
使用 prompt API,禁止使用原生 alert/confirm
| 訊息類型 | 方法 | 顯示方式 | 使用時機 |
|---|---|---|---|
| 成功 | prompt.success() | Toast(自動消失) | 操作成功完成 |
| 資訊 | prompt.info() | Toast | 一般資訊提示 |
| 錯誤 | prompt.error() | 阻塞式對話框 | 無表單時的錯誤 |
| 警告 | prompt.warn() | 阻塞式對話框 | 需要用戶確認的警告 |
| 確認 | prompt.confirm() | 阻塞式對話框 | 需要用戶做選擇 |
禁止使用
// ❌ 禁止
alert('Success!')
confirm('Delete this?')
window.alert()
window.confirm()
// ✅ 使用 prompt API
prompt.success('Success!')
await prompt.confirm('Delete this?', { actions: ['delete', 'cancel'] })
訊息內容原則
- 簡潔明確:用最少的文字傳達資訊
- 使用 i18n:所有訊息都要經過翻譯
t('...') - 動態參數:使用模板語法
t('Order ${orderNumber} created', { orderNumber })
3. 確認對話框
設計原則
按鈕觸發的確認優先使用 Button 內建屬性
Button 內建確認(推薦)
// 危險操作(紅色警告)
<Button
warning={t('Are you sure to delete? This cannot be undone.')}
onClick={handleDelete}
>
{t('Delete')}
</Button>
// 一般確認(藍色資訊)
<Button
confirmation={t('Confirm this action?')}
onClick={handleAction}
>
{t('Execute')}
</Button>
優點:
- 程式碼簡潔,無需 async/await
- 確認邏輯與按鈕綁定
warning優先級高於confirmation
prompt.confirm(次選)
僅在非按鈕觸發時使用:
// 選單項目、鍵盤快捷鍵等非按鈕觸發
const handleMenuAction = async () => {
const answer = await prompt.confirm(t('Confirm?'), {
actions: ['yes', 'no']
})
if (answer === 'yes') {
// 執行操作
}
}
4. 對話框內容規範
設計原則
對話框內所有元素應保持一致的視覺語言(comfortable density)
使用透明度區分資訊層次,而非縮小字體大小。這確保:
- 一致性:同一對話框內的元素有統一的視覺風格
- 可讀性:說明文字雖為次要資訊,但用戶仍需清楚理解操作後果
- 層次分明:透過透明度(如
/70)區分主次資訊
字體大小規範
| 元素 | 樣式 | 說明 |
|---|---|---|
| 標題 | text-lg font-semibold text-base-content | 最突出 |
| 說明文字 | text-base text-base-content/70 | 透明度區分次要 |
| 表單標籤 | text-base text-base-content | 與表單元素一致 |
| 表單元素 | comfortable density (text-base) | 主要互動區域 |
| 按鈕 | comfortable density (text-base) | 主要動作 |
| 輔助資訊 | text-base text-base-content/60 | 更次要的提示 |
錯誤範例
// ❌ 錯誤:混用 text-sm 和 text-base
<Description className="text-sm text-base-content/70"> // 說明用 text-sm
{t('Are you sure?')}
</Description>
<RadioGroup>
<Label className="text-sm">...</Label> // 標籤用 text-sm
</RadioGroup>
<Button>{t('Confirm')}</Button> // 按鈕用 text-base (comfortable)
正確範例
// ✅ 正確:統一使用 text-base,透明度區分層次
<Description className="text-base text-base-content/70"> // 透明度 70%
{t('Are you sure?')}
</Description>
<RadioGroup>
<Label className="text-base text-base-content">...</Label> // 完整透明度
</RadioGroup>
<Button>{t('Confirm')}</Button> // comfortable density
完整對話框範例
<Dialog>
{/* Header */}
<div className="flex items-center gap-2 p-4 border-b">
<AlertTriangle className="w-5 h-5 text-warning" />
<DialogTitle className="text-lg font-semibold text-base-content">
{t('Confirm Action')}
</DialogTitle>
</div>
{/* Content */}
<div className="p-4">
{/* 說明文字:text-base + 透明度 */}
<Description className="text-base text-base-content/70 mb-4">
{t('This action cannot be undone.')}
</Description>
{/* 表單元素:text-base */}
<RadioGroup>
<Radio>
<Label className="text-base text-base-content">
{t('Option 1')}
</Label>
</Radio>
</RadioGroup>
{/* 輔助資訊:text-base + 更低透明度 */}
<p className="text-base text-base-content/60 mt-2">
{t('Select an option to continue.')}
</p>
</div>
{/* Actions */}
<div className="flex justify-end gap-2 p-4">
<Button variant="soft">{t('Cancel')}</Button>
<Button color="primary">{t('Confirm')}</Button>
</div>
</Dialog>
5. 列表元素規範
設計原則
列表頁面中的元素應保持一致的視覺語言
所有 Finder(列表頁面)中的 Badge 和識別碼應遵循統一的樣式規範,確保跨模組的視覺一致性。
Badge 規範
所有狀態標籤統一使用 DaisyUI 的 badge-md 尺寸:
// ✅ 正確:使用 badge-md
<span className="badge badge-success badge-md whitespace-nowrap">
{t('Active')}
</span>
// ❌ 錯誤:使用 badge-sm 或自訂 padding
<span className="badge badge-success badge-sm">...</span>
<span className="badge badge-success text-sm px-4 py-2">...</span>
| 屬性 | 值 | 說明 |
|---|---|---|
| 尺寸 | badge-md | DaisyUI 中等尺寸,視覺平衡 |
| 換行 | whitespace-nowrap | 防止文字換行 |
| 顏色 | badge-{color} | 依語意選擇顏色 |
Badge 欄位對齊
在列表(VirtualTable)中,Badge 欄位應設定為置中對齊,使不同長度的 Badge 看起來更整齊:
// ✅ 正確:Badge 欄位置中對齊
{
accessorKey: 'status',
header: t('Status'),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
meta: { align: 'center' }, // Badge 欄位置中
enableSorting: false,
}
// ❌ 錯誤:Badge 欄位左對齊(預設)
{
accessorKey: 'status',
header: t('Status'),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
// 缺少 meta: { align: 'center' }
}
Badge 顏色語意:
| 狀態類型 | Badge Class | 使用場景 |
|---|---|---|
| 成功/啟用 | badge-success | 已完成、啟用、上架 |
| 警告/待處理 | badge-warning | 待確認、低庫存 |
| 錯誤/停用 | badge-error | 已取消、停用、缺貨 |
| 資訊/進行中 | badge-info | 已確認、進行中 |
| 主要 | badge-primary | 設計中 |
| 次要 | badge-secondary | 待出貨、VVIP |
| 強調 | badge-accent | 配送中 |
| 中性 | badge-neutral | 一般、預設 |
啟用/停用狀態規範:
所有模組的啟用/停用狀態統一使用以下配色:
| 狀態 | Badge Class | 說明 |
|---|---|---|
| 啟用 (active) | badge-success | 綠色,表示正常運作 |
| 停用 (inactive) | badge-error | 紅色,表示已停用 |
注意:不使用
badge-ghost作為停用狀態,因為透明樣式與其他狀態視覺差異過大,造成不一致感。
識別碼規範
訂單編號、客戶編號、商品 SKU 等識別碼統一使用等寬字體:
// ✅ 正確:使用 font-mono,無 text-sm
<Link to={id} className="link link-hover whitespace-nowrap font-mono">
{orderNumber}
</Link>
// ❌ 錯誤:額外添加 text-sm 或缺少 font-mono
<Link className="link link-hover whitespace-nowrap font-mono text-sm">...</Link>
<Link className="link link-hover whitespace-nowrap">...</Link>
| 屬性 | 值 | 說明 |
|---|---|---|
| 字體 | font-mono | 等寬字體,適合編號對齊 |
| 連結樣式 | link link-hover | 可點擊的連結樣式 |
| 換行 | whitespace-nowrap | 防止編號換行 |
| 字體大小 | 繼承預設 | 不額外設定 text-sm |
參考實作
| 模組 | Badge 組件 | 識別碼 Cell |
|---|---|---|
| 訂單管理 | OrderStatusBadge | OrderNumberCell |
| 客戶管理 | CustomerTierBadge, CustomerStatusBadge | CustomerNumberCell |
| 商品管理 | ProductCategoryBadge, ProductStatusBadge | ProductSkuCell |
注意:庫存狀態使用
StockStatusIndicator(彩色圓點),而非 Badge。這是因為庫存欄位包含數字+狀態,使用圓點可確保靠右對齊時視覺整齊。
6. 頁面佈局與捲動
設計原則
使用固定高度容器 + 內容區捲動,而非整頁捲動
這確保:
- 固定 Header:Header 永遠可見,不隨內容捲動
- 獨立捲動區域:每個內容區域可以獨立捲動
- 視覺一致性:所有頁面使用相同的佈局模式
佈局架構
應用程式使用三層佈局架構:
┌─────────────────────────────────────────────┐
│ MainLayout (h-screen overflow-hidden) │
│ ┌─────────────────────────────────────────┐ │
│ │ Header (固定高度) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ main (flex-1 overflow-hidden h-full) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ AppletShell (h-full overflow-hidden)│ │ │
│ │ │ ┌──────┐ ┌────────────────────────┐ │ │ │
│ │ │ │aside │ │ main │ │ │ │
│ │ │ │ │ │ (flex-1 overflow-y-auto)│ │ │ │
│ │ │ │捲動 │ │ ↕ 內容區域可以捲動 │ │ │ │
│ │ │ └──────┘ └────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ HomePage (h-full overflow-y-auto) │ │ │
│ │ │ ↕ 獨立頁面可以捲動 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
三層架構說明:
-
MainLayout 層(最外層)
- 使用
h-screen overflow-hidden固定視窗高度 - Header 固定在頂部,不隨內容捲動
- main 區域使用
flex-1 overflow-hidden h-full佔據剩餘空間
- 使用
-
AppletShell 層(Applet 容器)
- 使用
h-full overflow-hidden佔滿父容器高度 - aside(側邊工具列)使用
overflow-y-auto可獨立捲動 - main(內容區域)使用
flex-1 overflow-y-auto可獨立捲動
- 使用
-
內容層(頁面內容)
- Applet 頁面:位於 AppletShell 的 main 區域中
- 獨立頁面(如 HomePage):直接位於 MainLayout 的 main 區域,使用
h-full overflow-y-auto
Applet 頁面佈局
Applet 頁面(Finder、Detail、Editor)位於 AppletShell 的 main 區域中,不需要額外設定高度或 overflow:
// ✅ 正確:Applet 頁面不設定高度
export function OrderFinder() {
return (
<div className="p-4">
{/* 頁面內容 */}
<VirtualTable />
</div>
)
}
AppletShell 結構(參考 src/components/applet-shell/applet-shell.tsx):
<div className="flex h-full overflow-hidden">
{/* 側邊工具列:可獨立捲動 */}
<aside className="overflow-y-auto">
{/* 工具列內容 */}
</aside>
{/* 內容區域:可獨立捲動 */}
<main className="flex-1 overflow-y-auto">
{children} {/* Applet 頁面放這裡 */}
</main>
</div>
獨立頁面佈局
獨立頁面(如 HomePage)直接位於 MainLayout 的 main 區域,必須設定 h-full overflow-y-auto:
// ✅ 正確:獨立頁面使用 h-full overflow-y-auto
export function HomePage() {
return (
<div className="h-full overflow-y-auto bg-base-200">
<div className="p-6 md:p-8">
{/* 頁面內容 */}
</div>
</div>
)
}
為什麼需要 h-full overflow-y-auto?
h-full:佔滿父容器(MainLayout 的 main)高度overflow-y-auto:當內容超出高度時,啟用垂直捲動- 內層
<div className="p-6 md:p-8">:提供內容的 padding
常見錯誤
錯誤 1:使用 min-h-screen
// ❌ 錯誤:min-h-screen 會導致容器無法正確計算高度
export function HomePage() {
return (
<div className="min-h-screen bg-base-200 p-6">
{/* 當內容超出螢幕高度時,容器會繼續向下延伸 */}
{/* 導致 MainLayout 的 main 區域無法啟用捲動 */}
</div>
)
}
問題:min-h-screen 設定最小高度為 100vh,當內容超出時,容器會繼續向下延伸,而非啟用捲動。
解決方案:使用 h-full overflow-y-auto 代替 min-h-screen。
錯誤 2:在 Applet 頁面設定高度
// ❌ 錯誤:Applet 頁面不應設定高度
export function OrderFinder() {
return (
<div className="h-full overflow-y-auto p-4">
{/* AppletShell 已經處理捲動,這裡不需要 */}
</div>
)
}
問題:AppletShell 的 main 區域已經設定 overflow-y-auto,Applet 頁面不需要額外設定。
解決方案:Applet 頁面只設定 padding 和其他樣式,不設定高度或 overflow。
錯誤 3:缺少外層捲動容器
// ❌ 錯誤:獨立頁面缺少捲動容器
export function HomePage() {
return (
<div className="p-6 md:p-8 bg-base-200">
{/* 缺少 h-full overflow-y-auto,內容超出時無法捲動 */}
</div>
)
}
問題:缺少 h-full overflow-y-auto 的外層容器,導致內容超出時無法捲動。
解決方案:
// ✅ 正確:添加外層捲動容器
export function HomePage() {
return (
<div className="h-full overflow-y-auto bg-base-200">
<div className="p-6 md:p-8">
{/* 內容 */}
</div>
</div>
)
}
參考實作
| 佈局類型 | 參考檔案 | 說明 |
|---|---|---|
| MainLayout | src/layouts/main-layout.tsx | 最外層佈局,h-screen overflow-hidden |
| AppletShell | src/components/applet-shell/applet-shell.tsx | Applet 容器,h-full overflow-hidden |
| Applet 頁面 | src/applets/order-applet/order-finder.tsx | 無需設定高度或 overflow |
| 獨立頁面 | src/pages/home-page.tsx | 使用 h-full overflow-y-auto |
7. 載入狀態
設計原則
- 即時回饋:操作開始後立即顯示載入狀態
- 禁用互動:載入期間禁用相關按鈕和輸入
- 視覺一致:使用統一的載入指示器
頁面載入
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<span className="loading loading-spinner loading-lg text-primary"></span>
</div>
)
}
按鈕載入狀態
<Button
disabled={isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-xs"></span>
{t('Processing...')}
</>
) : (
t('Submit')
)}
</Button>
對話框載入
對話框操作期間:
- 所有按鈕設為
disabled - 關閉按鈕(X)設為
disabled - 顯示載入指示器
<Dialog>
<button disabled={isLoading} onClick={onClose}>
<X className="h-4 w-4" />
</button>
<Button disabled={isLoading}>
{isLoading ? (
<>
<span className="loading loading-spinner loading-xs"></span>
{t('Processing...')}
</>
) : (
t('Confirm')
)}
</Button>
</Dialog>
8. 空狀態
設計原則
- 說明情況:清楚告知為什麼沒有資料
- 引導行動:提供下一步操作建議
- 視覺友善:使用圖示增加親和力
列表空狀態
{orders.length === 0 && (
<div className="text-center py-12">
<ShoppingBag className="mx-auto h-12 w-12 text-base-content/30" />
<h3 className="mt-2 text-sm font-semibold text-base-content">
{t('No orders')}
</h3>
<p className="mt-1 text-sm text-base-content/60">
{t('Get started by creating a new order.')}
</p>
<div className="mt-6">
<Button onClick={() => navigate('/orders/new')}>
{t('Create Order')}
</Button>
</div>
</div>
)}
搜尋無結果
{searchResults.length === 0 && searchTerm && (
<div className="text-center py-8">
<Search className="mx-auto h-8 w-8 text-base-content/30" />
<p className="mt-2 text-sm text-base-content/60">
{t('No results found for "${term}"', { term: searchTerm })}
</p>
<p className="text-sm text-base-content/40">
{t('Try adjusting your search or filters.')}
</p>
</div>
)}
工作台空狀態(待辦清空)
{pendingOrders.length === 0 && (
<div className="text-center py-12">
<CheckCircle className="mx-auto h-12 w-12 text-success" />
<h3 className="mt-2 text-lg font-semibold text-base-content">
{t('All done!')}
</h3>
<p className="mt-1 text-sm text-base-content/60">
{t('All orders have been processed.')}
</p>
</div>
)}
9. 表單互動
設計原則
- 即時驗證:欄位失焦時進行驗證
- 清楚標示:必填欄位標示星號
* - 錯誤定位:錯誤訊息顯示在對應欄位下方
- 保護資料:離開前提示未儲存變更
欄位驗證錯誤
<Input
label={t('Email')}
required
error={errors.email?.message}
/>
// 錯誤訊息會顯示在欄位下方
// ⚠ 請輸入有效的電子郵件地址
未儲存變更警告
// 使用 Button 的 warning 屬性
<Button
variant="soft"
warning={hasDraft ? t('Discard unsaved changes?') : undefined}
onClick={() => navigate(-1)}
>
{t('Cancel')}
</Button>
表單提交狀態
<div className="flex gap-2">
<Button
variant="soft"
disabled={isSubmitting}
onClick={handleCancel}
>
{t('Cancel')}
</Button>
<Button
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-xs"></span>
{t('Saving...')}
</>
) : (
<>
<Save className="w-4 h-4" />
{t('Save')}
</>
)}
</Button>
</div>
10. 狀態回饋
設計原則
- 視覺化進度:使用進度條顯示流程階段
- 顏色語意:
- 綠色:已完成
- 藍色:當前階段
- 灰色:未開始
- 紅色:錯誤/取消
- 歷史紀錄:提供可追溯的狀態變更歷史
狀態進度條
[✓] 待確認 → [✓] 已確認 → [●] 設計中 → [ ] 待出貨 → [ ] 配送中 → [ ] 已完成
綠色 綠色 藍色高亮 灰色 灰色 灰色
狀態標籤顏色
| 狀態類型 | DaisyUI Class | 使用場景 |
|---|---|---|
| 待處理 | badge-warning | 待確認、待出貨 |
| 進行中 | badge-info | 設計中、配送中 |
| 已完成 | badge-success | 已完成、已簽收 |
| 已取消 | badge-error | 已取消、配送失敗 |
狀態操作按鈕
根據當前狀態動態顯示可執行的操作:
// 只顯示當前狀態允許的操作
{canConfirm && (
<Button onClick={handleConfirm}>
{t('Confirm Order')}
</Button>
)}
{canCancel && (
<Button
color="error"
warning={t('Are you sure to cancel this order?')}
onClick={handleCancel}
>
{t('Cancel Order')}
</Button>
)}
11. 頁面標題列按鈕
設計原則
頁面標題列右側的操作按鈕應該遵循一致的樣式和排列規則,確保使用者能夠快速識別和操作。
按鈕樣式規範
所有標題列按鈕使用統一樣式:
// 主要操作按鈕(純文字)
<Button
variant="soft"
density="tight"
className="text-base"
color="primary"
>
{t('Save')}
</Button>
// 圖示按鈕(更多選單、關閉)
<Button
variant="soft"
density="tight"
className="text-base"
shape="square"
>
<Icon className="w-4 h-4" />
</Button>
| 屬性 | 值 | 說明 |
|---|---|---|
| variant | soft | 柔和風格,hover 時填滿背景 |
| density | tight | 緊湊高度 (32px),適合標題列 |
| className | text-base | 字體大小與頁面內容一致 (16px) |
按鈕類型:
- 主要操作:純文字,無圖示(簡潔明確)
- 更多選單:
shape="square"+ MoreVertical 圖示 - 關閉按鈕:
shape="square"+ X 圖示
按鈕顏色語意
| 操作類型 | color | 範例 |
|---|---|---|
| 主要操作 | primary | 儲存、編輯、新增 |
| 成功操作 | success | 確認、啟用、建立並確認 |
| 警告操作 | warning | 取消訂單、停用 |
| 危險操作 | error | 刪除 |
| 一般操作 | 無 (default) | 重新整理、關閉 |
按鈕排列順序
從左到右的排列順序:
[主要操作] [次要操作...] [關閉按鈕]
- 主要操作:頁面最重要的操作(儲存、建立等)
- 次要操作:其他可用操作(編輯、取消等)
- 關閉按鈕:永遠在最右側
關閉按鈕
關閉按鈕使用 shape="square" 且只顯示圖示:
<Button
variant="soft"
density="tight"
className="text-base"
shape="square"
title={t('Close')}
onClick={handleClose}
>
<X className="size-4" />
</Button>
按鈕數量控制
當按鈕過多時,使用「更多操作」選單收納次要操作:
| 按鈕數量 | 處理方式 |
|---|---|
| ≤ 3 個 | 全部顯示 |
| > 3 個 | 保留 1-2 個主要操作 + 更多選單 + 關閉按鈕 |
更多選單範例:
<div className="flex">
{/* 主要操作(純文字) */}
<Button variant="soft" density="tight" className="text-base" color="primary">
{t('Save')}
</Button>
{/* 更多操作選單 */}
<Dropdown align="end">
<Dropdown.Trigger>
<Button variant="soft" density="tight" className="text-base" shape="square" title={t('More actions')}>
<MoreVertical className="w-4 h-4" />
</Button>
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Item onClick={handleCancel}>
<XCircle className="w-4 h-4" />
{t('Cancel Order')}
</Dropdown.Item>
<Dropdown.Item color="error" onClick={handleDelete}>
<Trash2 className="w-4 h-4" />
{t('Delete')}
</Dropdown.Item>
</Dropdown.Content>
</Dropdown>
{/* 關閉按鈕 */}
<Button variant="soft" density="tight" className="text-base" shape="square">
<X className="size-4" />
</Button>
</div>
Responsive 設計
桌面版與手機版採用相同的設計模式,只保留一個主要操作按鈕,其他收進選單:
| 螢幕大小 | 顯示方式 |
|---|---|
| 桌面版 (sm+) | [主要操作] [更多選單 ⋮] [關閉] |
| 手機版 | [主要操作] [更多選單 ⋮] [關閉] |
這樣的好處:
- 介面一致,使用者不需要適應不同的操作方式
- 標題列保持簡潔
- 次要操作統一收納在選單中
容器樣式
按鈕容器使用 flex,不需要 gap(soft variant 按鈕之間自然分隔):
<div className="flex items-center justify-between">
<h1>...</h1>
<div className="flex">
{/* 按鈕 */}
</div>
</div>
參考實作
以下是遵循本指南的參考實作:
| 模式 | 參考檔案 |
|---|---|
| 表單錯誤處理 | src/applets/order-applet/order-editor.tsx |
| 對話框內表單錯誤 | src/applets/order-applet/components/cancel-order-dialog.tsx |
| 確認對話框 | src/applets/order-applet/order-detail.tsx |
| 載入狀態 | src/applets/customer-applet/customer-detail.tsx |
| 空狀態 | src/applets/sales-applet/sales-applet.tsx |
| 狀態進度條 | src/applets/order-applet/components/order-status-progress.tsx |
| 頁面標題列按鈕 | src/applets/order-applet/order-detail.tsx |
| 表格操作欄選單 | src/applets/order-applet/order-finder.tsx |
相關文件
最後更新: 2025-12-21