專案前端模式
本文檔說明花店管理系統特定的前端設計模式與慣例。
目錄結構
app-web/src/
├── applets/ # 業務功能模組
│ ├── products/ # 商品管理
│ ├── orders/ # 訂單管理
│ ├── customers/ # 客戶管理
│ └── shared/ # 共用組件
├── components/ # 應用程式組件
│ ├── applet-shell/ # Applet 容器
│ ├── layout/ # 版面配置
│ └── common/ # 通用組件
├── config/ # 應用程式配置
├── features/ # Redux slices
├── hooks/ # 自定義 hooks
├── layouts/ # 佈局組件
├── mocks/ # MSW mock handlers
├── nls/ # 國際化資源
├── routes/ # 路由配置
├── services/ # API 服務層
├── store/ # Redux store
├── types/ # TypeScript 類型
└── utils/ # 工具函數
Applet 模式
什麼是 Applet?
Applet 是一個獨立的業務功能模組,包含:
- 路由
- 頁面組件
- 服務層
- 狀態管理(如需要)
Applet 結構
applets/products/
├── index.ts # 匯出點
├── routes.tsx # 路由定義
├── pages/ # 頁面組件
│ ├── ProductListPage.tsx
│ ├── ProductDetailPage.tsx
│ └── ProductFormPage.tsx
├── components/ # Applet 專用組件
│ ├── ProductCard.tsx
│ ├── ProductTable.tsx
│ └── ProductForm.tsx
├── hooks/ # Applet 專用 hooks
│ ├── useProducts.ts
│ └── useProductForm.ts
└── services/ # API 服務
└── productApi.ts
註冊 Applet
// applets/products/index.ts
import { ProductRoutes } from './routes';
export const ProductsApplet = {
id: 'products',
name: '商品管理',
icon: 'package',
routes: ProductRoutes,
permissions: ['PRODUCT_R'],
};
服務層模式
API 服務定義
// services/productApi.ts
import { http } from '@/utils/http';
import type { Product, ProductFilter, CreateProductRequest } from '@/types';
const BASE_URL = '/api/v1/products';
export const productApi = {
findAll: async (filter?: ProductFilter) => {
const response = await http.get<Product[]>(BASE_URL, { params: filter });
return response.data;
},
findById: async (id: string) => {
const response = await http.get<Product>(`${BASE_URL}/${id}`);
return response.data;
},
create: async (data: CreateProductRequest) => {
const response = await http.post<Product>(BASE_URL, data);
return response.data;
},
update: async (id: string, data: Partial<Product>) => {
const response = await http.patch<Product>(`${BASE_URL}/${id}`, data);
return response.data;
},
delete: async (id: string) => {
await http.delete(`${BASE_URL}/${id}`);
},
};
搭配 React Query
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productApi } from '@/services/productApi';
export const productKeys = {
all: ['products'] as const,
lists: () => [...productKeys.all, 'list'] as const,
list: (filter: ProductFilter) => [...productKeys.lists(), filter] as const,
details: () => [...productKeys.all, 'detail'] as const,
detail: (id: string) => [...productKeys.details(), id] as const,
};
export function useProducts(filter?: ProductFilter) {
return useQuery({
queryKey: productKeys.list(filter),
queryFn: () => productApi.findAll(filter),
});
}
export function useProduct(id: string) {
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => productApi.findById(id),
enabled: !!id,
});
}
表單模式
使用 React Hook Form
// components/ProductForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { productSchema, type ProductFormData } from './schema';
export function ProductForm({ defaultValues, onSubmit }: ProductFormProps) {
const form = useForm<ProductFormData>({
resolver: zodResolver(productSchema),
defaultValues,
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>商品名稱</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 其他欄位 */}
<Button type="submit">儲存</Button>
</form>
</Form>
);
}
列表頁面模式
標準列表頁面
// pages/ProductListPage.tsx
export function ProductListPage() {
const [searchParams, setSearchParams] = useSearchParams();
const filter = parseFilterFromSearchParams(searchParams);
const { data, isLoading } = useProducts(filter);
const handleFilterChange = (newFilter: ProductFilter) => {
setSearchParams(filterToSearchParams(newFilter));
};
return (
<AppletShell title="商品管理">
<AppletShell.Toolbar>
<SearchBox
value={filter.search}
onChange={(search) => handleFilterChange({ ...filter, search })}
/>
<Button asChild>
<Link to="new">新增商品</Link>
</Button>
</AppletShell.Toolbar>
<AppletShell.Content>
{isLoading ? (
<TableSkeleton />
) : (
<ProductTable data={data} />
)}
</AppletShell.Content>
<AppletShell.Pagination
page={filter.page}
total={data?.totalPages}
onChange={(page) => handleFilterChange({ ...filter, page })}
/>
</AppletShell>
);
}
錯誤處理模式
全域錯誤邊界
// components/ErrorBoundary.tsx
import { Component, type ReactNode } from 'react';
export class ErrorBoundary extends Component<
{ children: ReactNode; fallback?: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <ErrorPage />;
}
return this.props.children;
}
}
API 錯誤處理
// utils/http.ts
import axios from 'axios';
import { toast } from '@/components/ui/toast';
const http = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
http.interceptors.response.use(
(response) => response,
(error) => {
const message = error.response?.data?.message || '發生錯誤,請稍後再試';
if (error.response?.status === 401) {
// 導向登入頁
window.location.href = '/login';
} else if (error.response?.status === 403) {
toast.error('權限不足');
} else {
toast.error(message);
}
return Promise.reject(error);
}
);
export { http };
權限控制模式
權限檢查 Hook
// hooks/usePermission.ts
import { useAuth } from '@/features/auth';
export function usePermission(permission: string | string[]) {
const { user } = useAuth();
if (!user) return false;
const permissions = Array.isArray(permission) ? permission : [permission];
return permissions.some((p) => user.authorities.includes(p));
}
權限保護組件
// components/PermissionGuard.tsx
export function PermissionGuard({
permission,
fallback = null,
children,
}: PermissionGuardProps) {
const hasPermission = usePermission(permission);
if (!hasPermission) {
return fallback;
}
return children;
}
// 使用
<PermissionGuard permission="PRODUCT_W">
<Button>編輯</Button>
</PermissionGuard>
下一步
- AppFuse Web 應用 - 框架使用指南
- Applet 開發 - 開發新的業務功能模組