feat:重构UI
This commit is contained in:
@@ -1,24 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { ArrowRightLeft, Copy, Sparkles, StopCircle, Check } from './icons'
|
||||
import { Button } from './ui/button'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { Card } from './ui/card'
|
||||
import { cn } from '../lib/utils'
|
||||
import LanguageSelect from './LanguageSelect'
|
||||
import {
|
||||
AUTO_LANGUAGE,
|
||||
COMMON_LANGUAGE_OPTIONS,
|
||||
getLanguageName,
|
||||
LANGUAGE_OPTIONS,
|
||||
} from '@/lib/languages'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'auto', name: '自动检测' },
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'en', name: '英语' },
|
||||
{ code: 'ja', name: '日语' },
|
||||
{ code: 'ko', name: '韩语' },
|
||||
{ code: 'fr', name: '法语' },
|
||||
{ code: 'de', name: '德语' },
|
||||
{ code: 'es', name: '西班牙语' },
|
||||
]
|
||||
|
||||
const STYLES = [
|
||||
{ value: 'literal', name: '直译' },
|
||||
{ value: 'fluent', name: '意译' },
|
||||
{ value: 'casual', name: '口语化' },
|
||||
{ value: 'literal', name: '直译', description: '保留原文结构' },
|
||||
{ value: 'fluent', name: '意译', description: '自然流畅' },
|
||||
{ value: 'casual', name: '口语', description: '轻松对话' },
|
||||
]
|
||||
|
||||
export default function TranslatorForm() {
|
||||
@@ -29,6 +30,7 @@ export default function TranslatorForm() {
|
||||
const [style, setStyle] = useState('literal')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const abortRef = useRef<AbortController | null>(null)
|
||||
|
||||
const handleTranslate = async () => {
|
||||
@@ -50,7 +52,9 @@ export default function TranslatorForm() {
|
||||
body: JSON.stringify({
|
||||
source_text: sourceText,
|
||||
source_lang: sourceLang,
|
||||
source_lang_name: getLanguageName(sourceLang),
|
||||
target_lang: targetLang,
|
||||
target_lang_name: getLanguageName(targetLang),
|
||||
style,
|
||||
}),
|
||||
signal: abortRef.current.signal,
|
||||
@@ -83,8 +87,8 @@ export default function TranslatorForm() {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name !== 'AbortError') {
|
||||
} catch (err: any) {
|
||||
if (err.name !== 'AbortError') {
|
||||
setError(err.message || '翻译失败')
|
||||
}
|
||||
} finally {
|
||||
@@ -101,121 +105,135 @@ export default function TranslatorForm() {
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(translation)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handleSwapLanguages = () => {
|
||||
if (sourceLang === 'auto') return
|
||||
setSourceLang(targetLang)
|
||||
setTargetLang(sourceLang)
|
||||
setSourceText(translation)
|
||||
setTranslation(sourceText)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{/* 源语言选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">源语言</label>
|
||||
<select
|
||||
value={sourceLang}
|
||||
onChange={(e) => setSourceLang(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
{LANGUAGES.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 目标语言选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">目标语言</label>
|
||||
<select
|
||||
value={targetLang}
|
||||
onChange={(e) => setTargetLang(e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
{LANGUAGES.filter((l) => l.code !== 'auto').map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 风格选择 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">翻译风格</label>
|
||||
<div className="flex gap-4">
|
||||
{STYLES.map((s) => (
|
||||
<label key={s.value} className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="style"
|
||||
value={s.value}
|
||||
checked={style === s.value}
|
||||
onChange={(e) => setStyle(e.target.value)}
|
||||
className="mr-1"
|
||||
<div className="w-full max-w-5xl mx-auto space-y-6">
|
||||
{/* Controls Bar */}
|
||||
<Card className="p-4 bg-background/50 backdrop-blur-sm border-muted">
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between items-center">
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<div className="w-[140px]">
|
||||
<LanguageSelect
|
||||
value={sourceLang}
|
||||
onValueChange={setSourceLang}
|
||||
options={LANGUAGE_OPTIONS}
|
||||
commonOptions={[AUTO_LANGUAGE, ...COMMON_LANGUAGE_OPTIONS]}
|
||||
placeholder="源语言"
|
||||
allowAuto
|
||||
autoOption={AUTO_LANGUAGE}
|
||||
/>
|
||||
{s.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入输出区域 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">原文</label>
|
||||
<textarea
|
||||
value={sourceText}
|
||||
onChange={(e) => setSourceText(e.target.value)}
|
||||
placeholder="请输入要翻译的文本..."
|
||||
className="w-full h-48 p-3 border rounded resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">译文</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={translation}
|
||||
readOnly
|
||||
placeholder="翻译结果将显示在这里..."
|
||||
className="w-full h-48 p-3 border rounded resize-none bg-gray-50"
|
||||
/>
|
||||
{translation && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 px-2 py-1 text-xs bg-white border rounded hover:bg-gray-100"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleSwapLanguages}
|
||||
className="shrink-0"
|
||||
disabled={sourceLang === 'auto'}
|
||||
>
|
||||
<ArrowRightLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="w-[140px]">
|
||||
<LanguageSelect
|
||||
value={targetLang}
|
||||
onValueChange={setTargetLang}
|
||||
options={LANGUAGE_OPTIONS}
|
||||
commonOptions={COMMON_LANGUAGE_OPTIONS}
|
||||
placeholder="目标语言"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full md:w-auto overflow-x-auto pb-2 md:pb-0">
|
||||
{STYLES.map((s) => (
|
||||
<Button
|
||||
key={s.value}
|
||||
variant={style === s.value ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setStyle(s.value)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
{s.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
{isLoading ? (
|
||||
<Button onClick={handleStop} variant="destructive" size="sm">
|
||||
<StopCircle className="mr-2 h-4 w-4" />
|
||||
停止
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleTranslate}
|
||||
disabled={!sourceText.trim()}
|
||||
className="bg-primary hover:bg-primary/90 text-white shadow-lg shadow-primary/20"
|
||||
>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
翻译
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 text-red-600 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{/* Main Input/Output Area */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-[500px]">
|
||||
{/* Source Text */}
|
||||
<Card className="relative p-0 overflow-hidden border-muted focus-within:ring-2 focus-within:ring-primary/20 transition-all">
|
||||
<Textarea
|
||||
value={sourceText}
|
||||
onChange={(e) => setSourceText(e.target.value)}
|
||||
placeholder="在此输入要翻译的文本..."
|
||||
className="w-full h-full min-h-[400px] p-6 text-lg border-0 focus-visible:ring-0 resize-none bg-transparent"
|
||||
/>
|
||||
<div className="absolute bottom-4 right-4 text-xs text-muted-foreground">
|
||||
{sourceText.length} 字
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 按钮 */}
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleTranslate}
|
||||
disabled={isLoading || !sourceText.trim()}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? '翻译中...' : '翻译'}
|
||||
</button>
|
||||
{isLoading && (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className="px-6 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
)}
|
||||
{/* Translation Result */}
|
||||
<Card className="relative p-0 overflow-hidden border-muted bg-muted/30">
|
||||
<Textarea
|
||||
value={translation}
|
||||
readOnly
|
||||
placeholder="翻译结果..."
|
||||
className="w-full h-full min-h-[400px] p-6 text-lg border-0 focus-visible:ring-0 resize-none bg-transparent cursor-default"
|
||||
/>
|
||||
|
||||
{translation && (
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="h-8 w-8 shadow-sm"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="absolute bottom-4 left-4 right-14 p-2 bg-red-100 dark:bg-red-900/30 text-red-600 text-sm rounded border border-red-200 dark:border-red-900">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user