AppFuse Web 應用
本文檔說明如何在花店管理系統中使用 AppFuse Web 框架。
框架基礎
AppFuse Web 是本專案使用的前端框架。關於框架的基本概念與完整文檔,請參閱:
- AppFuse Web 快速開始 - 框架入門
- AppFuse Web 元件庫 - 所有可用元件
- Storybook - 互動式元件展示
在花店系統中的應用
專案結構
app-web/src/
├── applets/ # 業務功能模組(Applet)
│ ├── product-applet/ # 商品管理 Applet
│ ├── order-applet/ # 訂單管理 Applet
│ ├── customer-applet/ # 客戶管理 Applet
│ ├── dashboard-applet/ # 儀表板 Applet
│ ├── delivery-applet/ # 配送管理 Applet
│ ├── design-applet/ # 設計管理 Applet
│ ├── sales-applet/ # 銷售管理 Applet
│ └── shared/ # Applet 共用組件
├── components/ # 專案共用組件
│ ├── applet-shell/ # Applet 容器組件
│ └── ... # 其他共用組件
├── conf/ # 應用程式配置
│ └── config.ts # 主配置(MSW、baseURL 等)
├── config/ # 功能配置
│ └── applet-registry.ts # Applet 註冊表
├── features/ # Redux slices
│ ├── iam/ # 身份驗證與授權狀態
│ ├── product/ # 商品狀態(含草稿管理)
│ └── ... # 其他功能狀態
├── services/ # API 服務層(依業務領域分類)
│ ├── api-client.ts # API 客戶端配置
│ ├── core/ # 核心服務(環境、參考資料、多語系)
│ ├── iam/ # 身份驗證服務
│ ├── sales/ # 銷售領域服務(商品、訂單)
│ ├── crm/ # CRM 領域服務(客戶)
│ └── dashboard/ # 儀表板服務
├── mocks/ # MSW mock handlers
│ ├── handlers/ # Mock API handlers
│ ├── data/ # Mock 資料與測試帳號
│ ├── middleware/ # Mock 中介軟體(認證檢查)
│ └── utils/ # Mock 工具
├── nls/ # 多語系資源(TypeScript)
│ ├── message/ # 訊息翻譯
│ ├── term/ # 術語翻譯
│ └── article/ # 文章翻譯
├── layouts/ # 佈局組件
├── pages/ # 獨立頁面(登入、錯誤等)
├── routes/ # 路由配置
│ └── index.tsx # 主路由配置
└── store/ # Redux store 配置
使用框架元件
AppFuse Web 提供豐富的 UI 元件,在花店系統中廣泛使用:
資料表格
import { DataTable } from '@appfuse/appfuse-web/components';
function ProductList() {
return (
<DataTable
data={products}
columns={[
{ key: 'name', header: '商品名稱' },
{ key: 'basePrice', header: '價格', render: (p) => `$${p.basePrice}` },
{ key: 'stock', header: '庫存' },
]}
onRowClick={(product) => navigate(`/products/${product.id}`)}
/>
);
}
查看完整範例:app-web/src/applets/product-applet/product-finder.tsx
表單元件
import { TextField, Select, Button } from '@appfuse/appfuse-web/components';
function ProductForm() {
return (
<form>
<TextField
label="商品名稱"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<Select
label="分類"
options={categories}
value={category}
onChange={setCategory}
/>
<Button type="submit">儲存</Button>
</form>
);
}
查看完整範例:app-web/src/applets/product-applet/product-form.tsx
AppletShell
所有 Applet 都使用 <AppletShell> 提供標準佈局與導航:
import { useMemo } from 'react';
import { Routes, Route } from 'react-router';
import { List, Plus } from 'lucide-react';
import { useTranslation } from '@appfuse/appfuse-web/utils';
import { AppletShell, type AppletAction } from '@/components/applet-shell';
import { ProductFinder } from './product-finder';
import { ProductEditor } from './product-editor';
import { ProductDetail } from './product-detail';
const BASE_PATH = '/products';
export function ProductApplet() {
const { t } = useTranslation();
// 定義 Applet 操作選單(使用 useMemo 確保語言切換時重新計算)
const actions: AppletAction[] = useMemo(() => [
{ path: BASE_PATH, icon: List, label: t('Products') },
{ path: `${BASE_PATH}/new`, icon: Plus, label: t('Add Product') },
], [t]);
return (
<AppletShell
basePath={BASE_PATH}
actions={actions}
extraButtons={
<>
{/* 側邊欄額外按鈕(如草稿、常用項目) */}
<DraftProductButtons />
<PinnedProductButtons />
</>
}
>
{/* Applet 內部路由 */}
<Routes>
<Route index element={<ProductFinder />} />
<Route path="new" element={<ProductEditor />} />
<Route path=":id" element={<ProductDetail />} />
<Route path=":id/edit" element={<ProductEditor />} />
</Routes>
</AppletShell>
);
}
查看完整範例:app-web/src/applets/product-applet/product-applet.tsx
API 整合
使用框架提供的 HTTP 客戶端進行 API 調用:
// services/api-client.ts - API 客戶端配置
import { createHttpClient } from '@appfuse/appfuse-web/utils';
import { store } from '@/store';
import { selectAuthorization } from '@/features/iam/me-slice';
/**
* 應用層 API 客戶端
* - 自動附加 Access Token
* - 401 時自動刷新 Token 並重試
* - 支援檔案上傳(binary-separate 模式)
*/
export const apiClient = createHttpClient({
baseURL: '/',
fileUpload: {
strategy: 'binary',
endpoint: '/api/v1/staging/files/prepare',
},
});
// 請求攔截器:自動附加 Access Token
apiClient.interceptors.request.use((config) => {
const state = store.getState();
const authorization = selectAuthorization(state);
if (authorization?.access_token) {
config.headers.Authorization = `Bearer ${authorization.access_token}`;
}
return config;
});
// 響應攔截器:處理 Token 刷新與錯誤
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 401 Unauthorized - 嘗試刷新 Token
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// ... Token 刷新邏輯
return apiClient(originalRequest);
}
throw error;
}
);
// services/sales/product-service.ts - 商品服務
import { apiClient } from '@/services/api-client';
import type { PagedResult } from '@appfuse/appfuse-web';
import type {
Product,
CreateProductRequest,
UpdateProductRequest,
UpdateProductStatusRequest,
} from '@/mocks/types';
class ProductService {
/**
* 查詢商品列表(支援搜尋、篩選、分頁)
*/
async query(params: ProductQueryParams = {}): Promise<PagedResult<Product>> {
const response = await apiClient.get<Product[]>('/api/v1/products', { params });
// 從 Header 讀取分頁資訊
const totalElements = Number(response.headers['x-total-count'] ?? '0');
const page = response.headers['x-page'] ? Number(response.headers['x-page']) : undefined;
const size = response.headers['x-per-page'] ? Number(response.headers['x-per-page']) : undefined;
return {
content: response.data,
totalElements,
page,
size,
};
}
/**
* 獲取商品詳情
*/
async get(id: string): Promise<Product> {
const response = await apiClient.get<Product>(`/api/v1/products/${id}`);
return response.data;
}
/**
* 創建商品
*/
async create(data: CreateProductRequest): Promise<Product> {
const response = await apiClient.post<Product>('/api/v1/products', data);
return response.data;
}
/**
* 更新商品(部分更新)
*/
async update(id: string, data: UpdateProductRequest): Promise<Product> {
const response = await apiClient.patch<Product>(`/api/v1/products/${id}`, data);
return response.data;
}
/**
* 更新商品狀態(上架/下架)
*/
async updateStatus(id: string, data: UpdateProductStatusRequest): Promise<Product> {
const response = await apiClient.patch<Product>(`/api/v1/products/${id}/status`, data);
return response.data;
}
/**
* 刪除商品
*/
async delete(id: string): Promise<void> {
await apiClient.delete(`/api/v1/products/${id}`);
}
/**
* 上傳商品圖片
*/
async uploadImage(file: File): Promise<{ url: string; id: string }> {
const formData = new FormData();
formData.append('image', file);
const response = await apiClient.post('/api/v1/products/upload-image', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
}
}
export const productService = new ProductService();
查看完整範例:app-web/src/services/api-client.ts、app-web/src/services/sales/product-service.ts
狀態管理
使用 Redux Toolkit 管理全域狀態:
// features/auth/authSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: null,
isAuthenticated: false,
},
reducers: {
setCredentials: (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
},
logout: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
},
},
});
使用狀態:
import { useSelector, useDispatch } from 'react-redux';
import { selectAuth } from '@/features/auth/authSlice';
function UserProfile() {
const { user } = useSelector(selectAuth);
return <div>{user?.name}</div>;
}
Mock API(開發階段)
使用 MSW (Mock Service Worker) 提供 Mock API:
// mocks/handlers/products.ts
import { http, HttpResponse } from 'msw';
import { checkAuth } from '@/mocks/middleware/auth-middleware';
import { db, tenantQuery } from '@/mocks/data/db';
import type { Product, CreateProductRequest } from '@/mocks/types';
const API_BASE = '*';
export const productHandlers = [
/**
* GET /api/v1/products
* 獲取商品列表(支援搜尋、篩選、分頁)
*/
http.get(`${API_BASE}/api/v1/products`, ({ request }) => {
// 檢查認證與權限
const authCheck = checkAuth(request);
if (!authCheck.success) {
return authCheck.error!;
}
const { tenantId } = authCheck;
// 解析查詢參數
const url = new URL(request.url);
const searchQuery = url.searchParams.get('search') || '';
const page = parseInt(url.searchParams.get('page') || '1', 10);
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
// 1. 獲取租戶的商品
let products = tenantQuery.getByTenant(db.products, tenantId!);
// 2. 搜尋邏輯
if (searchQuery) {
products = products.filter((product) =>
product.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// 3. 分頁
const start = (page - 1) * limit;
const end = start + limit;
const pagedProducts = products.slice(start, end);
// 4. 回傳結果與分頁資訊
return HttpResponse.json(pagedProducts, {
headers: {
'X-Total-Count': String(products.length),
'X-Page': String(page),
'X-Per-Page': String(limit),
},
});
}),
/**
* POST /api/v1/products
* 創建商品
*/
http.post(`${API_BASE}/api/v1/products`, async ({ request }) => {
const authCheck = checkAuth(request);
if (!authCheck.success) {
return authCheck.error!;
}
const data = await request.json() as CreateProductRequest;
const newProduct: Product = {
...data,
id: generateId(),
sku: generateProductSku(data.category),
tenantId: authCheck.tenantId!,
createdAt: new Date(),
updatedAt: new Date(),
};
db.products.push(newProduct);
return HttpResponse.json(newProduct, { status: 201 });
}),
];
查看完整範例:app-web/src/mocks/handlers/products.ts
詳細說明:Mock → 真實 API 切換
路由配置
使用 React Router 配置路由:
// routes/index.tsx
import { Routes, Route, Navigate } from 'react-router';
import { ProductApplet } from '@/applets/product-applet';
import { OrderApplet } from '@/applets/order-applet';
import { CustomerApplet } from '@/applets/customer-applet';
import { DashboardApplet } from '@/applets/dashboard-applet';
import { ProtectedRoute } from './protected-route';
export function AppRoutes() {
return (
<Routes>
{/* 預設重導向至儀表板 */}
<Route index element={<Navigate to="/dashboard" replace />} />
{/* 儀表板 Applet */}
<Route path="/dashboard/*" element={
<ProtectedRoute>
<DashboardApplet />
</ProtectedRoute>
} />
{/* 商品管理 Applet - 處理所有 /products/* 路由 */}
<Route path="/products/*" element={
<ProtectedRoute>
<ProductApplet />
</ProtectedRoute>
} />
{/* 訂單管理 Applet - 處理所有 /orders/* 路由 */}
<Route path="/orders/*" element={
<ProtectedRoute>
<OrderApplet />
</ProtectedRoute>
} />
{/* 客戶管理 Applet - 處理所有 /customers/* 路由 */}
<Route path="/customers/*" element={
<ProtectedRoute>
<CustomerApplet />
</ProtectedRoute>
} />
</Routes>
);
}
注意:Applet 內部使用 <Routes> 處理子路由,因此在主路由使用 /* 匹配所有子路徑。
國際化
使用框架提供的多語系功能:
// 使用框架的 useTranslation hook
import { useTranslation } from '@appfuse/appfuse-web/utils';
// 或從根路徑導入
import { useTranslation } from '@appfuse/appfuse-web';
function ProductList() {
const { t } = useTranslation();
return (
<div>
<h1>{t('Products')}</h1>
{products.length === 0 && <p>{t('No products found')}</p>}
</div>
);
}
多語系檔案結構:
app-web/src/nls/
├── message/ # 訊息翻譯
│ └── zh-TW.ts # 繁體中文訊息
├── term/ # 術語翻譯
│ └── zh-TW.ts # 繁體中文術語
├── article/ # 文章翻譯
└── types.ts # 類型定義
翻譯檔案範例(nls/message/zh-TW.ts):
export default {
'Products': '商品管理',
'Add Product': '新增商品',
'No products found': '目前沒有商品',
'Save': '儲存',
'Cancel': '取消',
// ...
};
專案特定慣例
檔案命名
實際專案使用的命名慣例:
- Applet 組件:
kebab-case.tsx(如product-applet.tsx、product-finder.tsx) - 一般組件:
PascalCase.tsx或kebab-case.tsx(依團隊慣例) - Service:
kebab-case-service.ts(如product-service.ts、customer-service.ts) - 類型定義:
types.ts(放在模組目錄下) - Hooks:
use*.ts(如useProduct.ts、useAuth.ts) - Mock handlers:
kebab-case.ts(如products.ts、orders.ts)
Import 順序
// 1. React 相關
import { useState, useEffect, useMemo } from 'react';
// 2. 第三方套件
import { useNavigate } from 'react-router';
import { User } from 'lucide-react';
// 3. AppFuse Web 框架
import { Button } from '@appfuse/appfuse-web/components';
import { useTranslation } from '@appfuse/appfuse-web/utils';
// 或從根路徑導入常用功能
import { useTranslation, environ } from '@appfuse/appfuse-web';
// 4. 專案內部(使用 @/ alias)
import { productService } from '@/services/sales/product-service';
import { useAppSelector } from '@/store';
import type { Product } from '@/mocks/types';
組件結構
// 1. Imports
import { ... } from '...';
// 2. Types
interface Props {
productId: string;
}
// 3. Component
export function ProductDetail({ productId }: Props) {
// 4. Hooks
const [product, setProduct] = useState<Product | null>(null);
// 5. Effects
useEffect(() => {
loadProduct();
}, [productId]);
// 6. Handlers
const handleEdit = () => {
// ...
};
// 7. Render
return (
<div>
{/* ... */}
</div>
);
}
學習資源
框架文檔
專案範例
實際參考實作檔案:
Applet 範例:
app-web/src/applets/product-applet/- 商品管理 Applet 完整範例product-applet.tsx- Applet 容器與路由配置product-finder.tsx- 商品列表與搜尋頁面product-editor.tsx- 商品編輯器(創建/編輯)product-detail.tsx- 商品詳情頁面product-form.tsx- 商品表單組件components/- Applet 專用組件
app-web/src/applets/order-applet/- 訂單管理 Applet 範例app-web/src/applets/customer-applet/- 客戶管理 Applet 範例app-web/src/applets/dashboard-applet/- 儀表板 Applet 範例
Service 範例:
app-web/src/services/api-client.ts- API 客戶端配置與 Token 處理app-web/src/services/sales/product-service.ts- 商品服務app-web/src/services/sales/product-queries.ts- 商品 React Query hooksapp-web/src/services/sales/order-service.ts- 訂單服務app-web/src/services/crm/customer-service.ts- 客戶服務
Mock 範例:
app-web/src/mocks/handlers/products.ts- 商品 Mock APIapp-web/src/mocks/handlers/orders.ts- 訂單 Mock APIapp-web/src/mocks/middleware/auth-middleware.ts- 認證中介軟體app-web/src/mocks/data/test-accounts.ts- 測試帳號資料
配置範例:
app-web/src/conf/config.ts- 應用程式主配置(MSW、baseURL)app-web/src/config/applet-registry.ts- Applet 註冊表app-web/src/routes/index.tsx- 主路由配置
組件範例:
app-web/src/components/applet-shell/- AppletShell 組件實作app-web/src/applets/shared/- Applet 間共用組件