Skip to content

Commit 0428006

Browse files
CEQ151CEQ151
authored andcommitted
feat(ui): polish watermark UI and add i18n
1 parent 860f6b0 commit 0428006

16 files changed

Lines changed: 1064 additions & 380 deletions

src/renderer/src/App.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useClipboard } from './hooks/useClipboard'
1919
import { useSettings } from './contexts/SettingsContext'
2020
import { useBatch } from './hooks/useBatch'
2121
import { cleanAll } from './core/cleaner'
22+
import { useI18n } from './i18n'
2223

2324
const CompareView = lazy(() => import('./components/CompareView'))
2425
const SettingsPage = lazy(() => import('./components/SettingsPage'))
@@ -30,6 +31,7 @@ function App(): React.JSX.Element {
3031
const [cleaned, setCleaned] = useState<string | null>(null)
3132
const [bgImageUrl, setBgImageUrl] = useState<string | null>(null)
3233
const [showBgPanel, setShowBgPanel] = useState(false)
34+
const { t } = useI18n()
3335

3436
const { settings, updateSettings } = useSettings()
3537
const batchHook = useBatch()
@@ -152,7 +154,7 @@ function App(): React.JSX.Element {
152154
className="cosmic-btn flex items-center gap-1.5 text-xs px-3 py-2 rounded-xl cursor-pointer no-drag"
153155
>
154156
<Broom size={13} weight="regular" />
155-
清理
157+
{t('common.clean')}
156158
</motion.button>
157159
)}
158160
</div>
@@ -164,7 +166,7 @@ function App(): React.JSX.Element {
164166
className="flex items-center gap-1.5 text-xs text-zinc-600 hover:text-cyan-300 transition-colors duration-300 cursor-pointer no-drag"
165167
>
166168
<ArrowCounterClockwise size={12} weight="regular" />
167-
重置
169+
{t('common.reset')}
168170
</motion.button>
169171
)}
170172
</div>
@@ -210,11 +212,9 @@ function App(): React.JSX.Element {
210212
</div>
211213
<div className="text-center">
212214
<p className="text-sm font-medium text-white">
213-
粘贴文本,按{' '}
214-
<kbd className="font-mono text-[11px] bg-white/[0.07] border border-white/[0.1] px-1.5 py-0.5 rounded-md text-cyan-300/80">Ctrl+↵</kbd>
215-
{' '}开始扫描
215+
{t('detect.empty', { shortcut: 'Ctrl+Enter' })}
216216
</p>
217-
<p className="text-xs text-zinc-600 mt-2 tracking-wide">检测零宽字符、同形字、BiDi 控制码、Tags 区块等</p>
217+
<p className="text-xs text-zinc-400 mt-2 tracking-wide">{t('detect.emptyHint')}</p>
218218
</div>
219219
</motion.div>
220220
)}
@@ -236,7 +236,7 @@ function App(): React.JSX.Element {
236236
<div className="absolute inset-5 rounded-full bg-cyan-400/10 animate-pulse" />
237237
</div>
238238
<div className="flex flex-col items-center gap-1">
239-
<p className="text-sm text-cyan-300/70 font-medium tracking-wide">正在分析字符</p>
239+
<p className="text-sm text-cyan-300/70 font-medium tracking-wide">{t('detect.analyzing')}</p>
240240
<p className="text-xs text-zinc-600 font-mono">SCANNING UNICODE STREAM...</p>
241241
</div>
242242
</motion.div>
@@ -254,16 +254,16 @@ function App(): React.JSX.Element {
254254
<WarningCircle size={24} weight="regular" className="text-red-400" />
255255
</div>
256256
<div className="flex flex-col items-center gap-1 text-center">
257-
<p className="text-sm text-red-400 font-medium">扫描失败</p>
258-
<p className="text-xs text-zinc-500 max-w-xs">{errorMessage || '未知错误,请重试'}</p>
257+
<p className="text-sm text-red-400 font-medium">{t('detect.error')}</p>
258+
<p className="text-xs text-zinc-500 max-w-xs">{errorMessage || t('detect.unknownError')}</p>
259259
</div>
260260
<motion.button
261261
whileTap={{ scale: 0.97 }}
262262
onClick={handleReset}
263263
className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-cyan-300 transition-colors cursor-pointer no-drag mt-2"
264264
>
265265
<ArrowCounterClockwise size={12} weight="regular" />
266-
重置
266+
{t('common.reset')}
267267
</motion.button>
268268
</motion.div>
269269
)}
@@ -280,10 +280,10 @@ function App(): React.JSX.Element {
280280
<div className="flex items-center justify-between px-5 py-3 border-b border-white/[0.06] flex-shrink-0">
281281
<div className="flex items-center gap-1 bg-white/[0.03] rounded-xl p-0.5 border border-white/[0.06]">
282282
{([
283-
{ id: 'highlight' as const, label: '标注视图' },
283+
{ id: 'highlight' as const, label: t('detect.highlight') },
284284
...(cleaned ? [
285-
{ id: 'cleaned' as const, label: '净化文本' },
286-
{ id: 'compare' as const, label: '对比' },
285+
{ id: 'cleaned' as const, label: t('detect.cleaned') },
286+
{ id: 'compare' as const, label: t('detect.compare') },
287287
] : []),
288288
]).map(({ id, label }) => (
289289
<button
@@ -296,7 +296,9 @@ function App(): React.JSX.Element {
296296
))}
297297
</div>
298298
<div className="flex items-center gap-2">
299-
<span className="text-xs font-mono text-zinc-600 tabular-nums">发现 {result.suspiciousCount}</span>
299+
<span className="text-xs font-mono text-zinc-400 tabular-nums">
300+
{t('detect.found', { count: result.suspiciousCount })}
301+
</span>
300302
<ExportMenu result={result} originalText={text} />
301303
</div>
302304
</div>

src/renderer/src/components/BackgroundSwitcher.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useRef } from 'react'
22
import { motion, AnimatePresence } from 'framer-motion'
33
import { X, ImagesSquare, UploadSimple, Trash } from '@phosphor-icons/react'
4+
import { useI18n } from '../i18n'
45

56
interface BackgroundSwitcherProps {
67
open: boolean
@@ -15,6 +16,7 @@ export default function BackgroundSwitcher({
1516
onSelect,
1617
onClose,
1718
}: BackgroundSwitcherProps) {
19+
const { t } = useI18n()
1820
const fileInputRef = useRef<HTMLInputElement>(null)
1921

2022
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -78,7 +80,7 @@ export default function BackgroundSwitcher({
7880
<ImagesSquare size={15} className="text-cyan-400" weight="duotone" />
7981
</div>
8082
<span className="text-sm font-semibold text-white/90">
81-
背景图片
83+
{t('background.title')}
8284
</span>
8385
</div>
8486
<button
@@ -101,7 +103,7 @@ export default function BackgroundSwitcher({
101103
/>
102104
<div className="absolute inset-0 bg-black/20" />
103105
<div className="absolute bottom-2 left-2 right-2 flex justify-between items-center">
104-
<span className="text-[10px] text-white/60">当前自定义背景</span>
106+
<span className="text-[10px] text-white/60">{t('background.currentCustom')}</span>
105107
</div>
106108
</div>
107109
) : (
@@ -110,7 +112,7 @@ export default function BackgroundSwitcher({
110112
style={{ aspectRatio: '16/9', background: 'rgba(255,255,255,0.03)', border: '1px dashed rgba(255,255,255,0.08)' }}
111113
>
112114
<ImagesSquare size={24} weight="duotone" />
113-
<span className="text-[11px]">当前:动态宇宙背景</span>
115+
<span className="text-[11px]">{t('background.currentCosmic')}</span>
114116
</div>
115117
)}
116118

@@ -133,7 +135,7 @@ export default function BackgroundSwitcher({
133135
"
134136
>
135137
<UploadSimple size={14} weight="bold" />
136-
上传背景图片
138+
{t('background.upload')}
137139
</button>
138140

139141
{/* Remove button (only when custom bg is set) */}
@@ -149,12 +151,12 @@ export default function BackgroundSwitcher({
149151
"
150152
>
151153
<Trash size={13} weight="regular" />
152-
恢复动态背景
154+
{t('background.restore')}
153155
</button>
154156
)}
155157

156158
<p className="text-[10px] text-zinc-700 text-center">
157-
支持 PNG · JPG · WebP · GIF
159+
{t('background.supports')}
158160
</p>
159161
</div>
160162
</div>

src/renderer/src/components/BatchMode.tsx

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import {
1616
} from '@phosphor-icons/react'
1717
import { BatchItem, useBatch } from '../hooks/useBatch'
1818
import StatsPanel from './StatsPanel'
19+
import { useI18n } from '../i18n'
1920

2021
type BatchHook = ReturnType<typeof useBatch>
2122

2223
export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
24+
const { t } = useI18n()
2325
const {
2426
items,
2527
scanning,
@@ -77,20 +79,20 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
7779
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.06] flex-shrink-0">
7880
<div>
7981
<h1 className="text-base text-white leading-none font-display tracking-[0.02em]">
80-
批量检测
82+
{t('batch.title')}
8183
</h1>
82-
<p className="text-xs text-zinc-300 mt-1">同时检测多个文件或多段文本</p>
84+
<p className="text-xs text-zinc-300 mt-1">{t('batch.subtitle')}</p>
8385
</div>
8486

8587
{items.length > 0 && (
8688
<div className="flex items-center gap-2 flex-wrap justify-end">
8789
{doneCount > 0 && (
8890
<div className="flex items-center gap-3 px-3 py-1.5 bg-white/[0.04] rounded-lg border border-white/[0.07]">
8991
<span className="text-xs text-zinc-200">
90-
<span className="text-white font-mono font-semibold">{doneCount}</span>/{items.length} 已扫描
92+
{t('batch.scanned', { done: doneCount, total: items.length })}
9193
</span>
9294
<span className="text-xs text-zinc-200">
93-
平均风险{' '}
95+
{t('batch.avgRisk')}{' '}
9496
<span
9597
className="font-mono font-semibold"
9698
style={{
@@ -108,7 +110,7 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
108110
className="flex items-center gap-1.5 text-xs text-white hover:text-white bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.07] px-3 py-1.5 rounded-lg transition-all cursor-pointer no-drag"
109111
>
110112
<Broom size={13} weight="regular" />
111-
全部清理
113+
{t('batch.cleanAll')}
112114
</button>
113115
)}
114116
{/* Scan selected / scan all */}
@@ -119,7 +121,7 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
119121
className="flex items-center gap-1.5 text-xs text-white bg-[#3b7cd4] hover:bg-[#4d8de0] disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 rounded-lg transition-all cursor-pointer no-drag font-medium"
120122
>
121123
<Play size={13} weight="fill" />
122-
{scanning ? '扫描中…' : `扫描已选 (${selectedCount})`}
124+
{scanning ? t('common.scanning') : t('batch.scanSelected', { count: selectedCount })}
123125
</button>
124126
) : (
125127
<button
@@ -128,15 +130,15 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
128130
className="flex items-center gap-1.5 text-xs text-white bg-[#3b7cd4] hover:bg-[#4d8de0] disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 rounded-lg transition-all cursor-pointer no-drag font-medium"
129131
>
130132
<Play size={13} weight="fill" />
131-
{scanning ? '扫描中…' : '扫描全部'}
133+
{scanning ? t('common.scanning') : t('batch.scanAll')}
132134
</button>
133135
)}
134136
<button
135137
onClick={clearAll}
136138
className="flex items-center gap-1.5 text-xs text-zinc-300 hover:text-red-400 transition-colors cursor-pointer no-drag"
137139
>
138140
<Trash size={13} weight="regular" />
139-
清空
141+
{t('common.clear')}
140142
</button>
141143
</div>
142144
)}
@@ -148,8 +150,8 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
148150
{/* Mode toggle */}
149151
<div className="flex items-center gap-1 bg-white/[0.04] rounded-lg p-0.5">
150152
{([
151-
{ id: 'file' as const, label: '文件', icon: <FileText size={12} /> },
152-
{ id: 'text' as const, label: '文本', icon: <TextT size={12} /> },
153+
{ id: 'file' as const, label: t('batch.file'), icon: <FileText size={12} /> },
154+
{ id: 'text' as const, label: t('batch.text'), icon: <TextT size={12} /> },
153155
]).map(({ id, label, icon }) => (
154156
<button
155157
key={id}
@@ -183,8 +185,8 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
183185
<UploadSimple size={22} weight="regular" className="text-zinc-200" />
184186
</div>
185187
<div className="text-center">
186-
<p className="text-sm text-white font-medium">拖入多个文件</p>
187-
<p className="text-xs text-zinc-300 mt-1">点击或拖放 · .txt .md .docx .pdf</p>
188+
<p className="text-sm text-white font-medium">{t('batch.dropFiles')}</p>
189+
<p className="text-xs text-zinc-300 mt-1">{t('batch.dropHint')}</p>
188190
</div>
189191
<input
190192
ref={fileInputRef}
@@ -201,7 +203,7 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
201203
<textarea
202204
value={segmentText}
203205
onChange={(e) => setSegmentText(e.target.value)}
204-
placeholder={`粘贴多段文本,用\n---\n分隔各段`}
206+
placeholder={t('batch.segmentPlaceholder')}
205207
className="flex-1 bg-white/[0.03] rounded-xl border border-white/[0.07] text-sm text-zinc-200 leading-relaxed font-['Geist_Mono',monospace] resize-none px-4 py-3 focus:outline-none focus:border-[#3b7cd4]/40 placeholder:text-zinc-600"
206208
spellCheck={false}
207209
/>
@@ -210,7 +212,7 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
210212
disabled={!segmentText.trim()}
211213
className="flex items-center justify-center gap-1.5 text-xs text-zinc-200 bg-white/[0.06] hover:bg-white/[0.09] disabled:opacity-40 disabled:cursor-not-allowed border border-white/[0.08] px-3 py-2 rounded-lg transition-all cursor-pointer no-drag"
212214
>
213-
添加文本段落
215+
{t('batch.addSegments')}
214216
</button>
215217
</div>
216218
)}
@@ -223,7 +225,7 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
223225
<div className="w-16 h-16 rounded-2xl border border-white/[0.07] bg-white/[0.02] flex items-center justify-center">
224226
<UploadSimple size={28} weight="regular" className="text-zinc-600" />
225227
</div>
226-
<p className="text-sm text-zinc-200">拖入文件或粘贴文本段落开始批量检测</p>
228+
<p className="text-sm text-zinc-200">{t('batch.empty')}</p>
227229
</div>
228230
) : (
229231
<div className="p-5 flex flex-col gap-2">
@@ -240,10 +242,10 @@ export default function BatchMode({ batchHook }: { batchHook: BatchHook }) {
240242
) : (
241243
<Square size={15} weight="regular" className="text-zinc-600" />
242244
)}
243-
{allSelected ? '取消全选' : '全选'}
245+
{allSelected ? t('batch.unselectAll') : t('batch.selectAll')}
244246
</button>
245247
{selectedCount > 0 && (
246-
<span className="text-xs text-zinc-300">已选 {selectedCount}</span>
248+
<span className="text-xs text-zinc-300">{t('batch.selected', { count: selectedCount })}</span>
247249
)}
248250
</div>
249251

@@ -283,10 +285,18 @@ function BatchItemCard({
283285
onRemove: () => void
284286
onSelect: () => void
285287
}) {
288+
const { t } = useI18n()
286289
const score = item.result?.riskScore ?? 0
287290
const scoreColor = score < 30 ? '#10b981' : score < 65 ? '#f59e0b' : '#ef4444'
288-
const riskLabel = score < 30 ? '低风险' : score < 65 ? '中等' : '高风险'
289-
const actionLabel = score === 0 ? '无异常' : score < 30 ? '可忽略' : score < 65 ? '建议清理' : '建议重写'
291+
const riskLabel = score < 30 ? t('stats.low') : score < 65 ? t('stats.medium') : t('stats.high')
292+
const actionLabel =
293+
score === 0
294+
? t('risk.none')
295+
: score < 30
296+
? t('stats.action.ignore')
297+
: score < 65
298+
? t('stats.action.clean')
299+
: t('stats.action.rewrite')
290300

291301
return (
292302
<motion.div
@@ -340,7 +350,10 @@ function BatchItemCard({
340350
<p className="text-sm text-white truncate font-medium">{item.name}</p>
341351
{item.status === 'done' && item.result && (
342352
<p className="text-xs text-zinc-300 mt-0.5">
343-
{item.result.totalChars.toLocaleString()} 字符 · 发现 {item.result.suspiciousCount}
353+
{t('batch.itemSummary', {
354+
chars: item.result.totalChars.toLocaleString(),
355+
count: item.result.suspiciousCount,
356+
})}
344357
</p>
345358
)}
346359
{item.status === 'error' && (
@@ -381,15 +394,15 @@ function BatchItemCard({
381394
<button
382395
onClick={(e) => { e.stopPropagation(); onClean() }}
383396
className="p-1.5 text-zinc-300 hover:text-zinc-200 hover:bg-white/[0.06] rounded transition-colors cursor-pointer"
384-
title="清理"
397+
title={t('common.clean')}
385398
>
386399
<Broom size={13} weight="regular" />
387400
</button>
388401
)}
389402
<button
390403
onClick={(e) => { e.stopPropagation(); onRemove() }}
391404
className="p-1.5 text-zinc-300 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors cursor-pointer"
392-
title="移除"
405+
title={t('common.remove')}
393406
>
394407
<X size={13} weight="regular" />
395408
</button>
@@ -410,7 +423,7 @@ function BatchItemCard({
410423
<StatsPanel result={item.result} />
411424
{item.cleanedText && (
412425
<div className="mt-4">
413-
<p className="text-xs text-zinc-200 mb-2 uppercase tracking-wider">净化结果</p>
426+
<p className="text-xs text-zinc-200 mb-2 uppercase tracking-wider">{t('batch.cleanResult')}</p>
414427
<textarea
415428
value={item.cleanedText}
416429
readOnly

0 commit comments

Comments
 (0)