From 7f4255eb69c9e98cbcb362817fa459b60c58216c Mon Sep 17 00:00:00 2001 From: kever Date: Mon, 16 Feb 2026 14:28:15 +0800 Subject: [PATCH] feat(i18n): integrate i18next for internationalization support and add initial translation setup --- frontend/src/pages/OptimizerPage.tsx | 147 ++++++++++++++++----------- frontend/src/pages/optimizer.css | 20 +++- 2 files changed, 103 insertions(+), 64 deletions(-) diff --git a/frontend/src/pages/OptimizerPage.tsx b/frontend/src/pages/OptimizerPage.tsx index 08a990a..7c95b60 100644 --- a/frontend/src/pages/OptimizerPage.tsx +++ b/frontend/src/pages/OptimizerPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; import { Avatar, @@ -395,19 +395,22 @@ export default function OptimizerPage() { boots: 'All', }); const [weightValues, setWeightValues] = useState>({ - atk: 50, - def: 50, - hp: 50, - spd: 50, - cr: 50, - cd: 50, - acc: 50, - res: 50, + atk: 3, + def: 3, + hp: 3, + spd: 3, + cr: 3, + cd: 3, + acc: 3, + res: 3, }); const [results, setResults] = useState([]); const [selectedResult, setSelectedResult] = useState(null); const [totalCombos, setTotalCombos] = useState(0); const {success, error, info} = useMessage(); + + const leftPanelsRef = useRef(null); + const [leftPanelsHeight, setLeftPanelsHeight] = useState(null); const renderSetBadge = (values: string[]) => (values.length === 0 ? 'All' : '已选'); const [setPickerOpen, setSetPickerOpen] = useState(false); @@ -445,7 +448,7 @@ export default function OptimizerPage() { return option ? option.label : '套装'; }; - const openSetPicker = (target: 'set1' | 'set2' | 'set3', event: React.MouseEvent) => { + const openSetPicker = (target: 'set1' | 'set2' | 'set3', event: React.MouseEvent) => { const modalWidth = 520; const modalHeight = 520; const offset = 0; @@ -480,10 +483,10 @@ export default function OptimizerPage() { const openMainPicker = ( target: 'necklace' | 'ring' | 'boots', - event: React.MouseEvent + event: React.MouseEvent ) => { const modalWidth = 300; - const modalHeight = 320; + const modalHeight = Math.min(520, 88 + getMainStatOptions(target).length * 36); const offset = 0; let left = 0; let top = 0; @@ -523,43 +526,43 @@ export default function OptimizerPage() { const getMainStatOptions = (target: 'necklace' | 'ring' | 'boots'): Array<{label: string; value: MainStatKey | 'All'}> => { if (target === 'necklace') { return [ - {label: 'All', value: 'All'}, - {label: '攻击力', value: 'Attack'}, - {label: '防御力', value: 'Defense'}, - {label: '生命值', value: 'Health'}, - {label: '攻击力%', value: 'AttackPercent'}, - {label: '防御力%', value: 'DefensePercent'}, - {label: '生命值%', value: 'HealthPercent'}, - {label: '暴击率', value: 'CriticalHitChancePercent'}, - {label: '暴击伤害', value: 'CriticalHitDamagePercent'}, + {label: '\u5168\u90e8', value: 'All'}, + {label: '\u653b\u51fb\u529b', value: 'Attack'}, + {label: '\u9632\u5fa1\u529b', value: 'Defense'}, + {label: '\u751f\u547d\u503c', value: 'Health'}, + {label: '\u653b\u51fb\u529b%', value: 'AttackPercent'}, + {label: '\u9632\u5fa1\u529b%', value: 'DefensePercent'}, + {label: '\u751f\u547d\u503c%', value: 'HealthPercent'}, + {label: '\u66b4\u51fb\u7387', value: 'CriticalHitChancePercent'}, + {label: '\u66b4\u51fb\u4f24\u5bb3', value: 'CriticalHitDamagePercent'}, ]; } if (target === 'ring') { return [ - {label: 'All', value: 'All'}, - {label: '攻击力', value: 'Attack'}, - {label: '防御力', value: 'Defense'}, - {label: '生命值', value: 'Health'}, - {label: '攻击力%', value: 'AttackPercent'}, - {label: '防御力%', value: 'DefensePercent'}, - {label: '生命值%', value: 'HealthPercent'}, - {label: '命中', value: 'EffectivenessPercent'}, - {label: '抵抗', value: 'EffectResistancePercent'}, + {label: '\u5168\u90e8', value: 'All'}, + {label: '\u653b\u51fb\u529b', value: 'Attack'}, + {label: '\u9632\u5fa1\u529b', value: 'Defense'}, + {label: '\u751f\u547d\u503c', value: 'Health'}, + {label: '\u653b\u51fb\u529b%', value: 'AttackPercent'}, + {label: '\u9632\u5fa1\u529b%', value: 'DefensePercent'}, + {label: '\u751f\u547d\u503c%', value: 'HealthPercent'}, + {label: '\u6548\u679c\u547d\u4e2d', value: 'EffectivenessPercent'}, + {label: '\u6548\u679c\u6297\u6027', value: 'EffectResistancePercent'}, ]; } return [ - {label: 'All', value: 'All'}, - {label: '攻击力', value: 'Attack'}, - {label: '防御力', value: 'Defense'}, - {label: '生命值', value: 'Health'}, - {label: '攻击力%', value: 'AttackPercent'}, - {label: '防御力%', value: 'DefensePercent'}, - {label: '生命值%', value: 'HealthPercent'}, - {label: '速度', value: 'Speed'}, + {label: '\u5168\u90e8', value: 'All'}, + {label: '\u653b\u51fb\u529b', value: 'Attack'}, + {label: '\u9632\u5fa1\u529b', value: 'Defense'}, + {label: '\u751f\u547d\u503c', value: 'Health'}, + {label: '\u653b\u51fb\u529b%', value: 'AttackPercent'}, + {label: '\u9632\u5fa1\u529b%', value: 'DefensePercent'}, + {label: '\u751f\u547d\u503c%', value: 'HealthPercent'}, + {label: '\u901f\u5ea6', value: 'Speed'}, ]; }; - const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStatKey | 'All') => { +const getMainStatLabel = (target: 'necklace' | 'ring' | 'boots', value: MainStatKey | 'All') => { const option = getMainStatOptions(target).find(item => item.value === value); return option ? option.label : 'All'; }; @@ -596,6 +599,18 @@ export default function OptimizerPage() { loadLatestData(); }, []); + useLayoutEffect(() => { + if (!leftPanelsRef.current) return; + const observer = new ResizeObserver(entries => { + for (const entry of entries) { + const next = Math.round(entry.contentRect.height); + setLeftPanelsHeight(next > 0 ? next : null); + } + }); + observer.observe(leftPanelsRef.current); + return () => observer.disconnect(); + }, []); + useEffect(() => { if (setPickerOpen) { bodyStyleRef.current = { @@ -615,14 +630,14 @@ export default function OptimizerPage() { setSetFilters(emptySetFilters); setMainStatFilters({necklace: 'All', ring: 'All', boots: 'All'}); setWeightValues({ - atk: 50, - def: 50, - hp: 50, - spd: 50, - cr: 50, - cd: 50, - acc: 50, - res: 50, + atk: 3, + def: 3, + hp: 3, + spd: 3, + cr: 3, + cd: 3, + acc: 3, + res: 3, }); setResults([]); setSelectedResult(null); @@ -816,7 +831,7 @@ export default function OptimizerPage() { ref={rightPanelRef} >
-
+
{renderSetBadge(setFilters.set1)} @@ -849,7 +864,6 @@ export default function OptimizerPage() {
-
主能力值
项链
-
-
能力值权重
+
攻击 setWeightValues(prev => ({...prev, atk: value as number}))} @@ -897,7 +912,9 @@ export default function OptimizerPage() { 防御 setWeightValues(prev => ({...prev, def: value as number}))} @@ -908,7 +925,9 @@ export default function OptimizerPage() { 生命 setWeightValues(prev => ({...prev, hp: value as number}))} @@ -919,7 +938,9 @@ export default function OptimizerPage() { 速度 setWeightValues(prev => ({...prev, spd: value as number}))} @@ -930,7 +951,9 @@ export default function OptimizerPage() { 暴击 setWeightValues(prev => ({...prev, cr: value as number}))} @@ -941,7 +964,9 @@ export default function OptimizerPage() { 爆伤 setWeightValues(prev => ({...prev, cd: value as number}))} @@ -952,7 +977,9 @@ export default function OptimizerPage() { 命中 setWeightValues(prev => ({...prev, acc: value as number}))} @@ -963,7 +990,9 @@ export default function OptimizerPage() { 抵抗 setWeightValues(prev => ({...prev, res: value as number}))} diff --git a/frontend/src/pages/optimizer.css b/frontend/src/pages/optimizer.css index c783cdc..ffcd4bd 100644 --- a/frontend/src/pages/optimizer.css +++ b/frontend/src/pages/optimizer.css @@ -66,6 +66,7 @@ color: #c9d7f7; text-align: left; padding: 0 4px; + font-size: 16px; } .optimizer-set-button:disabled { @@ -111,23 +112,23 @@ grid-template-columns: 1fr; gap: 6px; padding: 10px 12px 12px 12px; - max-height: 320px; - overflow: hidden; + max-height: none; + overflow: visible; } .optimizer-main-modal-list .optimizer-set-modal-item { padding: 6px 8px; gap: 6px; - font-size: 12px; + font-size: 16px; } .optimizer-main-modal-list .optimizer-set-modal-label { - font-size: 14px; + font-size: 16px; white-space: nowrap; } .optimizer-main-modal-list .optimizer-set-modal-icon { - font-size: 12px; + font-size: 16px; } .optimizer-set-modal-item { @@ -141,6 +142,11 @@ padding: 8px 10px; color: #c9d7f7; text-align: left; + font-size: 16px; +} + +.optimizer-set-modal-label { + font-size: 16px; } .optimizer-set-modal-item:hover { @@ -230,6 +236,10 @@ align-items: center; gap: 8px; margin-bottom: 6px; + background: rgba(6, 10, 20, 0.55); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 6px; } .optimizer-weight-row:last-child {