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

14 KiB
Raw Blame History

Tool_OCR 前端代碼示例和最佳實踐

1. Tailwind CSS 樣式使用示例

佈局組件 (Layout.tsx 提取)

// 導航欄樣式示例
<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)

// 默認按鈕
<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 提取)

<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 提取)

<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 提取)

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)

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)

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)

// 登錄
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 清除

文件上傳示例

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)
  }
}

導出功能示例

// 導出 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. 國際化使用示例

在組件中使用翻譯

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)

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)

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)

// 認證
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. 組件結構

// 導入順序
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. 錯誤處理

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. 類名合併

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 配置

// 主入口 (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 文件

VITE_API_BASE_URL=http://localhost:12010

使用環境變數

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