update FRONTEND documentation

This commit is contained in:
beabigegg
2025-11-12 23:55:21 +08:00
parent 21bc2f92f1
commit fed112656f
5 changed files with 1748 additions and 1 deletions

652
FRONTEND_CODE_EXAMPLES.md Normal file
View File

@@ -0,0 +1,652 @@
# Tool_OCR 前端代碼示例和最佳實踐
## 1. Tailwind CSS 樣式使用示例
### 佈局組件 (Layout.tsx 提取)
```typescript
// 導航欄樣式示例
<header className="border-b bg-card">
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">{t('app.title')}</h1>
<button
className="px-4 py-2 text-sm font-medium text-foreground hover:text-primary transition-colors"
>
{t('nav.logout')}
</button>
</div>
</header>
{/* 導航選項卡 */}
<nav className="border-b bg-card">
<div className="container mx-auto px-4">
<ul className="flex space-x-1">
{navLinks.map((link) => (
<NavLink
to={link.to}
className={({ isActive }) =>
`block px-4 py-3 text-sm font-medium transition-colors ${
isActive
? 'text-primary border-b-2 border-primary'
: 'text-muted-foreground hover:text-foreground'
}`
}
>
{link.label}
</NavLink>
))}
</ul>
</div>
</nav>
```
### 按鈕變體示例 (button.tsx)
```typescript
// 默認按鈕
<Button>Save Changes</Button>
// 危險操作
<Button variant="destructive">Delete</Button>
// 輪廓樣式
<Button variant="outline">Cancel</Button>
// 幽靈按鈕
<Button variant="ghost">Remove</Button>
// 鏈接樣式
<Button variant="link">Learn More</Button>
// 不同尺寸
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">+</Button>
```
### 響應式卡片示例 (UploadPage.tsx 提取)
```typescript
<div className="max-w-4xl mx-auto space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
{t('upload.selectedFiles')} ({selectedFiles.length})
</CardTitle>
<Button variant="outline" size="sm" onClick={handleClearAll}>
{t('upload.clearAll')}
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{selectedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-muted rounded-md"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(file.size)}
</p>
</div>
<Button variant="ghost" size="sm" onClick={() => handleRemoveFile(index)}>
{t('upload.removeFile')}
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
```
### 文件上傳拖放區域 (FileUpload.tsx 提取)
```typescript
<Card
{...getRootProps()}
className={cn(
'border-2 border-dashed transition-colors cursor-pointer hover:border-primary/50',
{
'border-primary bg-primary/5': isDragActive && !isDragReject,
'border-destructive bg-destructive/5': isDragReject,
'opacity-50 cursor-not-allowed': disabled,
}
)}
>
<div className="p-12 text-center">
<input {...getInputProps()} />
<div className="mb-4">
<svg className="mx-auto h-12 w-12 text-muted-foreground" />
</div>
<div className="space-y-2">
{isDragActive ? (
<p className="text-lg font-medium text-primary">
{isDragReject ? t('upload.invalidFiles') : t('upload.dropFilesHere')}
</p>
) : (
<>
<p className="text-lg font-medium text-foreground">
{t('upload.dragAndDrop')}
</p>
<p className="text-sm text-muted-foreground">
{t('upload.supportedFormats')}
</p>
</>
)}
</div>
</div>
</Card>
```
---
## 2. React Query 使用示例
### 批次狀態查詢 (ResultsPage.tsx 提取)
```typescript
import { useQuery } from '@tanstack/react-query'
export default function ResultsPage() {
const { batchId } = useUploadStore()
const [selectedFileId, setSelectedFileId] = useState<number | null>(null)
// 獲取批次狀態
const { data: batchStatus, isLoading } = useQuery({
queryKey: ['batchStatus', batchId],
queryFn: () => apiClient.getBatchStatus(batchId!),
enabled: !!batchId, // 只在有 batchId 時查詢
})
// 獲取 OCR 結果
const { data: ocrResult, isLoading: isLoadingResult } = useQuery({
queryKey: ['ocrResult', selectedFileId],
queryFn: () => apiClient.getOCRResult(selectedFileId!.toString()),
enabled: !!selectedFileId,
})
if (!batchId) {
return <div>Please upload files first</div>
}
return (
<div>
{isLoading ? (
<p>Loading batch status...</p>
) : (
<ResultsTable data={batchStatus?.files} />
)}
</div>
)
}
```
---
## 3. Zustand 狀態管理示例
### 認證存儲 (authStore.ts)
```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { User } from '@/types/api'
interface AuthState {
user: User | null
isAuthenticated: boolean
setUser: (user: User | null) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
setUser: (user) =>
set({
user,
isAuthenticated: user !== null,
}),
logout: () =>
set({
user: null,
isAuthenticated: false,
}),
}),
{
name: 'auth-storage', // localStorage 鍵名
}
)
)
// 使用示例
function LoginPage() {
const setUser = useAuthStore((state) => state.setUser)
const handleLogin = async (username: string, password: string) => {
const response = await apiClient.login({ username, password })
setUser({ id: 1, username })
}
}
```
### 上傳狀態存儲 (uploadStore.ts)
```typescript
export const useUploadStore = create<UploadState>()(
persist(
(set) => ({
batchId: null,
files: [],
uploadProgress: 0,
setBatchId: (id) => {
set({ batchId: id })
},
setFiles: (files) => set({ files }),
setUploadProgress: (progress) => set({ uploadProgress: progress }),
updateFileStatus: (fileId, status) =>
set((state) => ({
files: state.files.map((file) =>
file.id === fileId ? { ...file, status } : file
),
})),
clearUpload: () =>
set({
batchId: null,
files: [],
uploadProgress: 0,
}),
}),
{
name: 'tool-ocr-upload-store',
// 只持久化 batchId 和 files不持久化進度
partialize: (state) => ({
batchId: state.batchId,
files: state.files,
}),
}
)
)
// 使用示例
function UploadPage() {
const { setBatchId, setFiles } = useUploadStore()
const handleUploadSuccess = (data: UploadResponse) => {
setBatchId(data.batch_id)
setFiles(data.files)
}
}
```
---
## 4. API 客戶端使用示例
### API 認證流程 (api.ts)
```typescript
// 登錄
const response = await apiClient.login({
username: 'admin',
password: 'password'
})
// Token 自動保存到 localStorage
// 後續請求自動附帶 token
const status = await apiClient.getBatchStatus(123)
// 請求頭自動包含: Authorization: Bearer <token>
// 登出
apiClient.logout()
// Token 自動從 localStorage 清除
```
### 文件上傳示例
```typescript
const handleUpload = async (files: File[]) => {
try {
const response = await apiClient.uploadFiles(files)
// response: { batch_id: 1, files: [...] }
setBatchId(response.batch_id)
setFiles(response.files)
} catch (error) {
// 自動處理 401 錯誤並重定向到登錄頁
showError(error.response?.data?.detail)
}
}
```
### 導出功能示例
```typescript
// 導出 PDF
const handleDownloadPDF = async (fileId: number) => {
const blob = await apiClient.exportPDF(fileId)
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ocr-result-${fileId}.pdf`
a.click()
window.URL.revokeObjectURL(url)
}
// 導出規則管理
const rules = await apiClient.getExportRules()
const newRule = await apiClient.createExportRule({
rule_name: 'My Rule',
config_json: { /* ... */ }
})
await apiClient.updateExportRule(rule.id, { rule_name: 'Updated' })
await apiClient.deleteExportRule(rule.id)
```
---
## 5. 國際化使用示例
### 在組件中使用翻譯
```typescript
import { useTranslation } from 'react-i18next'
export default function UploadPage() {
const { t } = useTranslation()
return (
<div>
<h1>{t('upload.title')}</h1>
<p>{t('upload.dragAndDrop')}</p>
{/* 帶插值的翻譯 */}
<p>{t('upload.fileCount', { count: 5 })}</p>
{/* 會渲染: "已選擇 5 個檔案" */}
<button>{t('upload.uploadButton')}</button>
</div>
)
}
```
### i18n 初始化 (i18n/index.ts)
```typescript
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import zhTW from './locales/zh-TW.json'
i18n.use(initReactI18next).init({
resources: {
'zh-TW': {
translation: zhTW,
},
},
lng: 'zh-TW',
fallbackLng: 'zh-TW',
interpolation: {
escapeValue: false,
},
})
export default i18n
```
---
## 6. 路由和保護示例
### 受保護的路由 (App.tsx)
```typescript
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <>{children}</>
}
function App() {
return (
<Routes>
{/* 公開路由 */}
<Route path="/login" element={<LoginPage />} />
{/* 受保護的路由 */}
<Route
path="/"
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/upload" replace />} />
<Route path="upload" element={<UploadPage />} />
<Route path="processing" element={<ProcessingPage />} />
<Route path="results" element={<ResultsPage />} />
<Route path="export" element={<ExportPage />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
{/* 全部匹配 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
}
```
---
## 7. 類型定義示例
### API 類型 (types/api.ts)
```typescript
// 認證
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
access_token: string
token_type: string
}
// 文件上傳
export interface UploadResponse {
batch_id: number
files: FileInfo[]
}
export interface FileInfo {
id: number
filename: string
file_size: number
format: string
status: 'pending' | 'processing' | 'completed' | 'failed'
}
// OCR 結果
export interface OCRResult {
file_id: number
filename: string
status: string
markdown_content: string
json_data: OCRJsonData
confidence: number
processing_time: number
}
export interface TextBlock {
text: string
confidence: number
bbox: [number, number, number, number]
position: number
}
// 導出規則
export interface ExportRule {
id: number
rule_name: string
config_json: Record<string, any>
css_template?: string
created_at: string
}
```
---
## 8. 最佳實踐
### 1. 組件結構
```typescript
// 導入順序
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
// 內部導入
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { useAuthStore } from '@/store/authStore'
import { apiClient } from '@/services/api'
// 組件定義
export default function MyPage() {
// Hooks
const { t } = useTranslation()
const navigate = useNavigate()
const [state, setState] = useState(null)
// 查詢
const { data, isLoading } = useQuery({
queryKey: ['key'],
queryFn: () => apiClient.getData(),
})
// 狀態更新
const handleClick = () => {
// ...
}
// 渲染
return (
<div className="space-y-4">
{/* JSX */}
</div>
)
}
```
### 2. 錯誤處理
```typescript
const { toast } = useToast()
const handleAction = async () => {
try {
const result = await apiClient.doSomething()
toast({
title: t('success.title'),
description: t('success.message'),
variant: 'success',
})
} catch (error: any) {
const errorMessage = error.response?.data?.detail || t('errors.generic')
toast({
title: t('error.title'),
description: errorMessage,
variant: 'destructive',
})
}
}
```
### 3. 類名合併
```typescript
import { cn } from '@/lib/utils'
// 條件類名
const buttonClass = cn(
'base-classes',
{
'conditional-classes': isActive,
'other-classes': isDisabled,
},
customClassName
)
// 動態樣式
const cardClass = cn(
'p-4 rounded-lg',
variant === 'outlined' && 'border border-input',
variant === 'elevated' && 'shadow-lg'
)
```
### 4. React Query 配置
```typescript
// 主入口 (main.tsx)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 5, // 5 分鐘
},
},
})
// 使用示例
const { data, isLoading, error } = useQuery({
queryKey: ['items', id],
queryFn: () => apiClient.getItem(id),
enabled: !!id, // 條件查詢
staleTime: 1000 * 60 * 10, // 10 分鐘不新鮮
})
```
---
## 9. 環境變數
### .env 文件
```env
VITE_API_BASE_URL=http://localhost:12010
```
### 使用環境變數
```typescript
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:12010'
```
---
## 10. 常見開發流程
### 創建新頁面
1.`/src/pages` 創建 `NewPage.tsx`
2.`App.tsx` 添加路由
3. 使用 Zustand 管理狀態
4. 使用 React Query 獲取數據
5. 使用 Tailwind CSS 樣式
6. 使用 i18next 添加文字
### 添加新 API 接口
1.`types/api.ts` 定義類型
2.`services/api.ts` 添加方法
3. 在需要的地方使用 `apiClient.method()`
4. 使用 React Query 或直接調用
### 修改樣式
1. 優先使用 Tailwind CSS 工具類
2. 使用 cn() 合併類名
3. 修改 `tailwind.config.js` 自定義主題
4.`index.css` 添加全局樣式