Files
OCR/FRONTEND_CODE_EXAMPLES.md
2025-11-12 23:55:21 +08:00

653 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 添加全局樣式