訊息系統
:::info 內容來源
本頁內容源自 appfuse-web/lib/messaging/README.md,如有差異以 README 為準。
:::
AppFuse Web 提供全域訊息通知系統,包含 Toast(快顯通知)和 Dialog(對話框)兩種形式。
設計理念
- Toast: 非阻斷式通知,自動消失
- Dialog: 阻斷式對話框,需使用者操作
快速開始
1. 設置元件
在應用程式根元件加入 Toast 和 Dialog 容器:
import { MessageToast, MessageDialog } from '@appfuse/appfuse-web/messaging';
function App() {
return (
<div>
{/* 你的應用程式內容 */}
<Routes />
{/* 非阻塞式通知:顯示成功、資訊訊息 */}
<MessageToast
severities={['success', 'info']}
autoHideDuration={3000}
size={5}
/>
{/* 阻塞式對話框:顯示錯誤、警告、確認訊息 */}
<MessageDialog
severities={['error', 'warning', 'confirmation']}
/>
</div>
);
}
2. 使用 prompt API
import { prompt } from '@appfuse/appfuse-web/messaging';
// ✅ 成功訊息(Toast,自動消失)
prompt.success('Data saved successfully');
// ✅ 資訊訊息(預設無操作按鈕)
prompt.info('Please note this important information');
// ❌ 錯誤對話框(需確認)
prompt.error('Operation failed. Please try again.');
// ⚠️ 警告對話框(顯示 "Yes" / "No" 按鈕)
prompt.warn('This action cannot be undone');
// 🔄 確認對話框(回傳 Promise)
const answer = await prompt.confirm('Delete this item?', {
title: '刪除確認',
actions: ['delete', 'cancel'],
});
if (answer === 'delete') {
// 執行刪除
}
API 參考
prompt.success(content, options?)
發布成功訊息,預設無 actions;預設 MessageToast 對無操作訊息顯示 5 秒,可由應用調整。
prompt.success('Data saved');
prompt.success('File uploaded successfully');
使用時機: 操作成功後的正向回饋
prompt.info(content, options?)
發布資訊訊息,預設 actions: [];Toast/Dialog 的呈現方式由應用決定。
prompt.info('Your session will expire in 5 minutes');
使用時機: 需要使用者注意但非錯誤的資訊
prompt.error(content, options?)
顯示錯誤對話框,需點擊確認。
prompt.error('Failed to save data. Please check your connection.');
使用時機: 操作失敗,需要使用者知曉
prompt.warn(content, options?): Promise<string>
顯示警告對話框,帶有 "Yes" / "No" 按鈕。回傳使用者點擊的按鈕文字。
const action = await prompt.warn('This will overwrite existing data');
if (action === 'Yes') {
// 繼續操作
}
使用時機: 警告使用者潛在風險,但允許繼續
prompt.confirm(content, options?): Promise<string>
顯示確認對話框,回傳使用者點擊的按鈕文字。
// 基本用法(預設 "Yes" / "No" 按鈕)
const action = await prompt.confirm('Delete this item?');
if (action === 'Yes') {
// 執行刪除
}
// 自訂按鈕文字
const action = await prompt.confirm('Discard changes?', {
actions: ['Discard', 'Keep editing'],
});
if (action === 'Discard') {
// 捨棄變更
}
使用時機: 需要使用者明確確認的操作
通用 Options
所有 prompt.* 方法的第二參數支援以下選項:
| 選項 | 型別 | 說明 |
|---|---|---|
title | string | 標題 |
thread | string | null | 訊息線程(用於隔離) |
actions | string[] | 操作按鈕 |
操作與顯示生命週期
prompt 只發布訊息與管理操作結果;Toast/Dialog 與計時由應用配置。
success/info 預設 actions: [];error 維持 ['OK'],warn/confirm 維持 ['Yes', 'No']。
warning 是 warn 的別名。
prompt.info('Item saved', {
actions: ['Undo'],
onAction: (action) => { if (action === 'Undo') undoSave() },
})
// 數值只設定無 action 訊息的時間;有 action 仍持續顯示。
<MessageToast autoHideDuration={5000} />
// 全部持續顯示。
<MessageToast autoHideDuration={null} />
// 應用明確允許可從其他入口操作的訊息自動消失。
<MessageToast autoHideDuration={(message) =>
message.actions?.includes('View') ? 10000 : message.actions?.length ? null : 5000
} />
Toast顯示所有非空 actions(包括單一按鈕);× 只關閉,不等同 action。success/info/error接受onAction,回傳原始操作鍵並最多執行一次;×/逾時/卸載不執行此回呼。- 帶 actions 或回呼的通知不去重,避免相同本文覆蓋不同業務操作;純通知依 severity、thread、title、content、明確指定的 actions 隔離去重。
MessageToast預設無 actions 才計時;從實際呈現開始,排隊不計時。hover、焦點在訊息內、視窗失焦或頁面隱藏時暫停,恢復後使用剩餘時間。MessageDialog對沒有 actions 的訊息提供 OK 關閉按鈕;不修改原始 message。warn/confirm不設固定 30 秒逾時;明確選取回傳原始 action,關閉或容器卸載回傳最後一項,空 actions 時回傳'cancel'。- 呼叫端可傳
signal:abort 時回傳'cancel'並由框架容器移除訊息。無呈現容器接收時也可取消等待;prompt.destroy()會結束尚未完成的等待。 - 自建 UI 需以
resolvePromptAction/resolvePromptDismissal回接操作,透過usePromptQueue處理 abort 與卸載。每則確認應只有一個呈現容器接管。
const controller = new AbortController()
const answer = prompt.confirm('Continue?', { signal: controller.signal })
// 工作流程結束,不再需要使用者回答時:
controller.abort()
// await answer === 'cancel'
訊息類型比較
以下為框架容器的預設分工;Toast 的自動消失僅適用於無 actions 的訊息。
| 類型 | 元件 | 自動消失 | 需操作 | 回傳值 |
|---|---|---|---|---|
success | Toast | ✅ 5秒 | ❌ | void |
info | Toast | ✅ 5秒 | ❌ | void |
error | Dialog | ❌ | ✅ 確認 | void |
warn | Dialog | ❌ | ✅ Yes/No | Promise<string> |
confirm | Dialog | ❌ | ✅ Yes/No | Promise<string> |
:::tip info 的預設行為
info 預設由 MessageToast 處理(因為 MessageToast 的預設 severities 包含 info)。如需以 Dialog 顯示,可將 info 改由 MessageDialog 處理。
:::
進階用法
自動訊息去重
系統會自動去除 3 秒內的重複訊息:
// 快速連點按鈕時,只會顯示一次
button.onClick = () => {
prompt.success('Saved'); // 只顯示一次
};
訊息自動翻譯
訊息內容會自動翻譯(如果有對應的 i18n 資源):
// 傳入英文 key
prompt.success('Data saved');
// 如果 i18n 有定義,會顯示翻譯後的文字
// "資料已儲存"
:::warning 避免雙重翻譯
呼叫 prompt.*() 時應傳入英文 key,不要先呼叫 t() 翻譯。MessageToast 和 MessageDialog 會自動翻譯訊息內容。
// ✅ 正確
prompt.success('Login successful!');
// ❌ 錯誤:會導致雙重翻譯警告
prompt.success(t('Login successful!'));
:::
在服務層使用
// services/userService.ts
import { prompt } from '@appfuse/appfuse-web/messaging';
export async function deleteUser(id: string) {
const action = await prompt.confirm('Delete this user?');
if (action !== 'Yes') return false;
try {
await http.delete(`/users/${id}`);
prompt.success('User deleted');
return true;
} catch (error) {
prompt.error('Failed to delete user');
return false;
}
}
表單提交回饋
function UserForm() {
const onSubmit = async (data: FormData) => {
try {
await userService.create(data);
prompt.success('User created');
navigate('/users');
} catch (error) {
if (error.response?.status === 409) {
prompt.error('Email already exists');
} else {
prompt.error('Failed to create user');
}
}
};
return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
}
離開頁面確認
import { useBlocker } from 'react-router';
function EditForm() {
const { isDirty } = useFormState();
useBlocker(async ({ proceed, reset }) => {
if (!isDirty) {
proceed();
return;
}
const action = await prompt.confirm('Discard unsaved changes?');
if (action === 'Yes') {
proceed();
} else {
reset();
}
});
return <form>...</form>;
}
Thread 隔離
每個訊息可指定 thread,實現多頁面訊息隔離(避免 A 頁面的訊息出現在 B 頁面):
// 發送時指定 thread
prompt.success('Data saved', { thread: '/orders' })
// 接收端只顯示對應 thread 的訊息
<MessageToast currentPath="/orders" />
元件 Props
<MessageToast />
訂閱 prompt service,顯示非阻塞式訊息。
type MessageToastProps = {
severities?: Severity[] // 要顯示的訊息類型 (預設: ['success', 'info'])
autoHideDuration?: number | null | ((message: Message) => number | null)
size?: number // 最多顯示幾個訊息 (預設: 3)
currentPath?: string // Thread 隔離路徑
position?: 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right'
// 顯示位置 (預設: 'top-center')
}
<MessageDialog />
訂閱 prompt service,顯示阻塞式訊息。
type MessageDialogProps = {
severities?: Severity[] // 要顯示的訊息類型 (預設: ['error', 'warning', 'confirmation'])
currentPath?: string // Thread 隔離路徑
}
通用 UI 元件
如果不需要 prompt 服務,可直接使用低階的 Toast / PromptDialog 元件自行管理狀態:
import { Toast, ToastContainer, PromptDialog } from '@appfuse/appfuse-web/components'
<ToastContainer position="top-right">
<Toast id={1} message="自訂訊息" severity="success" onClose={handleClose} />
</ToastContainer>
<PromptDialog
open={isOpen}
title="確認"
content="確定要執行嗎?"
severity="warning"
actions={['確定', '取消']}
onAction={handleAction}
onClose={() => setIsOpen(false)}
/>
樣式客製化
Toast 位置
MessageToast 預設顯示在上方置中,可透過 props 調整:
<MessageToast position="top-center" /> {/* 預設 */}
<MessageToast position="top-right" />
<MessageToast position="bottom-center" />
<MessageToast position="bottom-right" />
Dialog 樣式
Dialog 使用 DaisyUI modal 樣式,會自動跟隨主題:
// 不同訊息類型使用不同顏色
// success: 綠色
// info: 藍色
// warning: 橘色
// error: 紅色
UX 最佳實踐
選擇正確的訊息類型
// ✅ 正確
prompt.success('Saved'); // 操作成功,不需注意
prompt.info('Session expires in 5 min'); // 需要注意的資訊
prompt.error('Network error'); // 操作失敗
prompt.confirm('Delete?'); // 需要確認的操作
// ❌ 錯誤
prompt.error('Data saved'); // 成功訊息不應用 error
prompt.success('Failed to save'); // 失敗訊息不應用 success
訊息內容準則
// ✅ 好的訊息
prompt.success('Order #123 created'); // 具體、簡潔
prompt.error('Unable to connect to server'); // 說明問題
// ❌ 不好的訊息
prompt.success('Success'); // 太籠統
prompt.error('Error'); // 沒有說明問題
prompt.error('An unexpected error occurred in the system while processing your request'); // 太長
避免過度使用
// ✅ 適當使用
async function saveForm(data) {
await api.save(data);
prompt.success('Saved'); // 只在最終結果顯示一次
}
// ❌ 過度使用
async function saveForm(data) {
prompt.info('Validating...'); // 不需要
await validate(data);
prompt.info('Saving...'); // 不需要
await api.save(data);
prompt.success('Saved');
}