update FRONTEND documentation
This commit is contained in:
652
FRONTEND_CODE_EXAMPLES.md
Normal file
652
FRONTEND_CODE_EXAMPLES.md
Normal 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` 添加全局樣式
|
||||
|
||||
Reference in New Issue
Block a user