昨天我們探討了前端 Activities 管理系統的 UI/UX 設計,今天來看看如何用程式碼實現這些設計。這篇文章會專注於「為什麼這樣寫」和「程式碼背後的思考」。
就像蓋房子一樣,我們需要不同的「樓層」來組織程式碼:
src/
├── components/ # UI 組件層 - 像樂高積木
├── services/ # 服務層 - 與後端溝通的橋樑
├── contexts/ # 狀態管理層 - 所有組件會用到的數據與邏輯
└── types/ # 類型定義層 - 資料格式的字典
組件層:負責 UI 顯示,像樂高積木一樣可重複使用
服務層:負責與後端 API 溝通,處理數據的取得和轉換
狀態管理層:負責所有組件共用的數據和邏輯
類型定義層:定義資料格式,讓整個專案有一致的資料結構
想像一下,如果每個人都用不同的格式來描述「活動」,會發生什麼事?有人說活動名稱叫 name
,有人說叫 title
,這樣就會造成混亂。
TypeScript 的類型定義就像「資料字典」,告訴整個專案「活動」應該長什麼樣子:
// 定義活動的資料結構
export interface Activity {
id: string; // 唯一識別碼
name: string; // 活動名稱
totalTime: number; // 總時間(分鐘)
weeklyTime: number; // 本週時間(分鐘)
targetTime: number; // 目標時間(分鐘)
color: string; // 活動顏色
icon: ActivityIconType; // 活動圖示
}
好處:
每個組件都需要「接收什麼資料」和「提供什麼功能」的說明:
// 活動卡片需要什麼?
interface ActivityCardProps {
activity: Activity; // 要顯示的活動資料
onClick?: () => void; // 點擊卡片時要做什麼
onEdit?: (activity: Activity) => void; // 點擊編輯時要做什麼
onDelete?: (activity: Activity) => void; // 點擊刪除時要做什麼
}
這樣設計的好處是:組件只專注於顯示,具體要做什麼由父組件決定。
想像一下,如果每個組件都要自己記住「用戶是否已登入」,會發生什麼事?每個組件都要重複寫相同的邏輯,而且資料可能不同步。
我們用兩種方式管理狀態:
1. 全域狀態(Context):所有組件都需要知道的資料
2. 本地狀態(useState):只有特定組件需要的資料
// 錯誤處理 - 全域狀態
const ErrorContext = createContext();
// 任何組件都可以顯示錯誤
const SomeComponent = () => {
const { addError } = useError();
const handleError = () => {
addError({ message: "發生錯誤了!" });
};
};
// 表單狀態 - 本地狀態
const CreateActivityModal = () => {
const [formData, setFormData] = useState({
name: "",
targetTime: "",
});
};
這樣設計的好處是:資料不會重複,也不會不同步。
如果每個組件都直接呼叫 API,會發生什麼問題?
我們建立一個「API 服務層」,就像一個「翻譯官」,負責:
// 統一的 API 服務
const useApi = () => {
// 自動加上認證標頭
const headers = {
"Content-Type": "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
};
// 統一處理錯誤
const handleError = (error) => {
if (error.status === 401) {
logout(); // 自動登出
}
addError({ message: "API 呼叫失敗" });
};
return { get, post, put, delete };
};
// 專用的活動服務
const useActivities = () => {
const api = useApi();
return {
getActivities: () => api.get("/api/activities"),
createActivity: (data) => api.post("/api/activities", data),
updateActivity: (id, data) => api.put(`/api/activities/${id}`, data),
deleteActivity: (id) => api.delete(`/api/activities/${id}`)
};
};
這樣設計的好處是:組件只需要呼叫 createActivity(data)
,不用管認證、錯誤處理等細節。
設計思路:活動卡片就像「名片」,要能清楚顯示活動資訊,也要提供操作按鈕。
關鍵設計:
const ActivityCard = ({ activity, onClick, onEdit, onDelete }) => {
// 防止按鈕點擊觸發卡片點擊
const handleEdit = (e) => {
e.stopPropagation(); // 關鍵:阻止事件冒泡
onEdit(activity);
};
return (
<div onClick={onClick}>
<h3>{activity.name}</h3>
<button onClick={handleEdit}>編輯</button>
<button onClick={handleDelete}>刪除</button>
</div>
);
};
為什麼要 stopPropagation()
?
如果沒有這個,點擊編輯按鈕時會同時觸發卡片的點擊事件,造成意外跳轉。
設計思路:Modal 就像「彈出式表單」,要能收集用戶輸入,也要處理表單驗證。
關鍵設計:
const CreateActivityModal = ({ isOpen, onSubmit }) => {
const [formData, setFormData] = useState({
name: "",
targetTime: "",
color: PRESET_COLORS[0],
icon: PRESET_ICONS[0],
});
const handleSubmit = (e) => {
e.preventDefault();
// 轉換資料格式
const newActivity = {
...formData,
targetTime: parseInt(formData.targetTime), // 字串轉數字
totalTime: 0, // 新活動預設值
weeklyTime: 0,
};
onSubmit(newActivity);
};
};
為什麼要轉換資料格式?
表單輸入都是字串,但 API 需要數字,所以要手動轉換。
設計思路:編輯 Modal 與新增類似,但需要「預填現有資料」。
關鍵差異:
const EditActivityModal = ({ activity, onSubmit }) => {
const [formData, setFormData] = useState({});
// 當活動資料變更時,更新表單
useEffect(() => {
if (activity) {
setFormData({
name: activity.name,
targetTime: activity.targetTime.toString(),
color: activity.color,
icon: activity.icon,
});
}
}, [activity]);
};
為什麼要用 useEffect
?
因為 activity
是從外部傳入的,當它變更時,我們需要同步更新表單。
設計思路:刪除是危險操作,需要「二次確認」防止誤刪。
關鍵設計:
const DeleteConfirmationModal = ({ activity, onConfirm }) => {
return (
<div>
<h2>確定要刪除以下活動嗎?</h2>
<div>
<span>{activity.icon}</span>
<span>{activity.name}</span>
<span>總時間: {activity.totalTime}</span>
</div>
<button onClick={onConfirm}>確定刪除</button>
</div>
);
};
為什麼要顯示活動資訊?
讓用戶確認刪除的是正確的活動,避免誤刪。
如果每個組件都自己處理錯誤,會發生什麼問題?
我們建立一個「統一錯誤處理機制」:
// 任何地方都可以顯示錯誤
const handleCreateActivity = async (data) => {
try {
await createActivity(data);
} catch (error) {
addError({
type: "error",
title: "創建活動失敗",
message: "無法創建新活動,請稍後再試",
});
}
};
好處:
每個組件只做一件事:
ActivityCard
:只負責顯示活動CreateActivityModal
:只負責新增表單DeleteConfirmationModal
:只負責刪除確認// 通用的 Modal 基礎組件
const BaseModal = ({ isOpen, onClose, title, children }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
<h2>{title}</h2>
{children}
</div>
</div>
);
};
好處:所有 Modal 都有相同的樣式和行為,只需要改變內容。
// 提取表單邏輯
const useForm = (initialValues) => {
const [values, setValues] = useState(initialValues);
const setValue = (key, value) =>
setValues((prev) => ({ ...prev, [key]: value }));
const reset = () => setValues(initialValues);
return { values, setValue, reset };
};
好處:表單邏輯可以在多個組件中重複使用。
今天我們探討了前端 Activities CRUD 功能的程式實作,重點在於:
這些設計原則不僅適用於 Activities 功能,也是整個前端應用的基礎架構。