This commit is contained in:
beabigegg
2025-11-12 22:53:17 +08:00
commit da700721fa
130 changed files with 23393 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/toast'
import { apiClient } from '@/services/api'
import type { ExportRule } from '@/types/api'
export default function SettingsPage() {
const { t } = useTranslation()
const { toast } = useToast()
const queryClient = useQueryClient()
const [isCreating, setIsCreating] = useState(false)
const [editingRule, setEditingRule] = useState<ExportRule | null>(null)
const [formData, setFormData] = useState({
rule_name: '',
confidence_threshold: 0.5,
include_metadata: true,
filename_pattern: '{filename}_ocr',
css_template: 'default',
})
// Fetch export rules
const { data: exportRules, isLoading } = useQuery({
queryKey: ['exportRules'],
queryFn: () => apiClient.getExportRules(),
})
// Create rule mutation
const createRuleMutation = useMutation({
mutationFn: (rule: any) => apiClient.createExportRule(rule),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
setIsCreating(false)
resetForm()
toast({
title: t('common.success'),
description: '規則已建立',
variant: 'success',
})
},
onError: (error: any) => {
toast({
title: t('common.error'),
description: error.response?.data?.detail || t('errors.networkError'),
variant: 'destructive',
})
},
})
// Update rule mutation
const updateRuleMutation = useMutation({
mutationFn: ({ ruleId, rule }: { ruleId: number; rule: any }) =>
apiClient.updateExportRule(ruleId, rule),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
setEditingRule(null)
resetForm()
toast({
title: t('common.success'),
description: '規則已更新',
variant: 'success',
})
},
onError: (error: any) => {
toast({
title: t('common.error'),
description: error.response?.data?.detail || t('errors.networkError'),
variant: 'destructive',
})
},
})
// Delete rule mutation
const deleteRuleMutation = useMutation({
mutationFn: (ruleId: number) => apiClient.deleteExportRule(ruleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
toast({
title: t('common.success'),
description: '規則已刪除',
variant: 'success',
})
},
onError: (error: any) => {
toast({
title: t('common.error'),
description: error.response?.data?.detail || t('errors.networkError'),
variant: 'destructive',
})
},
})
const resetForm = () => {
setFormData({
rule_name: '',
confidence_threshold: 0.5,
include_metadata: true,
filename_pattern: '{filename}_ocr',
css_template: 'default',
})
}
const handleCreate = () => {
setIsCreating(true)
setEditingRule(null)
resetForm()
}
const handleEdit = (rule: ExportRule) => {
setEditingRule(rule)
setIsCreating(false)
setFormData({
rule_name: rule.rule_name,
confidence_threshold: rule.config_json.confidence_threshold || 0.5,
include_metadata: rule.config_json.include_metadata || true,
filename_pattern: rule.config_json.filename_pattern || '{filename}_ocr',
css_template: rule.css_template || 'default',
})
}
const handleSave = () => {
const ruleData = {
rule_name: formData.rule_name,
config_json: {
confidence_threshold: formData.confidence_threshold,
include_metadata: formData.include_metadata,
filename_pattern: formData.filename_pattern,
},
css_template: formData.css_template,
}
if (editingRule) {
updateRuleMutation.mutate({ ruleId: editingRule.id, rule: ruleData })
} else {
createRuleMutation.mutate(ruleData)
}
}
const handleCancel = () => {
setIsCreating(false)
setEditingRule(null)
resetForm()
}
const handleDelete = (ruleId: number) => {
if (window.confirm('確定要刪除此規則嗎?')) {
deleteRuleMutation.mutate(ruleId)
}
}
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold text-foreground">{t('settings.title')}</h1>
{!isCreating && !editingRule && (
<Button onClick={handleCreate}>{t('export.rules.newRule')}</Button>
)}
</div>
{/* Create/Edit Form */}
{(isCreating || editingRule) && (
<Card>
<CardHeader>
<CardTitle>
{editingRule ? t('common.edit') + ' ' + t('export.rules.title') : t('export.rules.newRule')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Rule Name */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t('export.rules.ruleName')}
</label>
<input
type="text"
value={formData.rule_name}
onChange={(e) => setFormData((prev) => ({ ...prev, rule_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-200 rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="例如:高信心度匯出"
/>
</div>
{/* Confidence Threshold */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t('export.options.confidenceThreshold')}: {formData.confidence_threshold}
</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={formData.confidence_threshold}
onChange={(e) =>
setFormData((prev) => ({
...prev,
confidence_threshold: Number(e.target.value),
}))
}
className="w-full"
/>
</div>
{/* Include Metadata */}
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="include-metadata-form"
checked={formData.include_metadata}
onChange={(e) =>
setFormData((prev) => ({
...prev,
include_metadata: e.target.checked,
}))
}
className="w-4 h-4 border border-gray-200 rounded"
/>
<label htmlFor="include-metadata-form" className="text-sm font-medium text-foreground">
{t('export.options.includeMetadata')}
</label>
</div>
{/* Filename Pattern */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t('export.options.filenamePattern')}
</label>
<input
type="text"
value={formData.filename_pattern}
onChange={(e) =>
setFormData((prev) => ({
...prev,
filename_pattern: e.target.value,
}))
}
className="w-full px-3 py-2 border border-gray-200 rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="{filename}_ocr"
/>
</div>
{/* CSS Template */}
<div>
<label className="block text-sm font-medium text-foreground mb-2">
{t('export.options.cssTemplate')}
</label>
<select
value={formData.css_template}
onChange={(e) => setFormData((prev) => ({ ...prev, css_template: e.target.value }))}
className="w-full px-3 py-2 border border-gray-200 rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="default"></option>
<option value="academic"></option>
<option value="business"></option>
<option value="report"></option>
</select>
</div>
{/* Actions */}
<div className="flex justify-end gap-3 pt-4">
<Button variant="outline" onClick={handleCancel}>
{t('common.cancel')}
</Button>
<Button
onClick={handleSave}
disabled={!formData.rule_name || createRuleMutation.isPending || updateRuleMutation.isPending}
>
{createRuleMutation.isPending || updateRuleMutation.isPending
? t('common.loading')
: t('common.save')}
</Button>
</div>
</CardContent>
</Card>
)}
{/* Rules List */}
<Card>
<CardHeader>
<CardTitle>{t('export.rules.title')}</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="text-center text-muted-foreground py-8">{t('common.loading')}</p>
) : exportRules && exportRules.length > 0 ? (
<div className="space-y-3">
{exportRules.map((rule) => (
<div
key={rule.id}
className="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
>
<div>
<h3 className="font-medium text-foreground">{rule.rule_name}</h3>
<p className="text-sm text-muted-foreground">
: {rule.config_json.confidence_threshold || 0.5} | CSS :{' '}
{rule.css_template || 'default'}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => handleEdit(rule)}>
{t('common.edit')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(rule.id)}
disabled={deleteRuleMutation.isPending}
>
{t('common.delete')}
</Button>
</div>
</div>
))}
</div>
) : (
<p className="text-center text-muted-foreground py-8"></p>
)}
</CardContent>
</Card>
</div>
)
}