From 6d929c322b70bd3eb37b43e2caf2a7dc4ed80ea4 Mon Sep 17 00:00:00 2001 From: icarus Date: Sun, 6 Jul 2025 21:03:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(translate):=20=E6=94=AF=E6=8C=81=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=89=A7=E8=A1=8C=E7=BF=BB=E8=AF=91=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增translate store模块管理翻译状态 - 实现useTranslate hook封装翻译逻辑 - 重构TranslatePage组件使用新的翻译逻辑 --- src/renderer/src/hooks/useTranslate.ts | 70 +++++++++++++++++++ .../src/pages/translate/TranslatePage.tsx | 68 +++++------------- src/renderer/src/store/index.ts | 4 +- src/renderer/src/store/translate.ts | 34 +++++++++ 4 files changed, 126 insertions(+), 50 deletions(-) create mode 100644 src/renderer/src/hooks/useTranslate.ts create mode 100644 src/renderer/src/store/translate.ts diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts new file mode 100644 index 0000000000..b414f77be3 --- /dev/null +++ b/src/renderer/src/hooks/useTranslate.ts @@ -0,0 +1,70 @@ +import db from '@renderer/databases' +import { fetchTranslate } from '@renderer/services/ApiService' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import { + setTranslatedContent as _setTranslatedContent, + setTranslating as _setTranslating +} from '@renderer/store/translate' +import { Assistant, TranslateHistory } from '@renderer/types' +import { uuid } from '@renderer/utils' + +export default function useTranslate() { + const translatedContent = useAppSelector((state) => state.translate.translatedContent) + const translating = useAppSelector((state) => state.translate.translating) + + const dispatch = useAppDispatch() + + const setTranslatedContent = (content: string) => { + dispatch(_setTranslatedContent(content)) + } + + const setTranslating = (translating: boolean) => { + dispatch(_setTranslating(translating)) + } + + const translate = async ( + text: string, + assistant: Assistant, + actualSourceLanguage: string, + actualTargetLanguage: string + ) => { + setTranslating(true) + await fetchTranslate({ + content: text, + assistant, + onResponse: (text) => { + setTranslatedContent(text) + } + }) + const translatedContent = store.getState().translate.translatedContent + await saveTranslateHistory(text, translatedContent, actualSourceLanguage, actualTargetLanguage) + + setTranslating(false) + } + + const saveTranslateHistory = async ( + sourceText: string, + targetText: string, + sourceLanguage: string, + targetLanguage: string + ) => { + const history: TranslateHistory = { + id: uuid(), + sourceText, + targetText, + sourceLanguage, + targetLanguage, + createdAt: new Date().toISOString() + } + await db.translate_history.add(history) + } + + return { + translatedContent, + translating, + setTranslatedContent, + setTranslating, + translate, + saveTranslateHistory + } +} diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 8d4e7116b6..be486145da 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -10,13 +10,13 @@ import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useProviders } from '@renderer/hooks/useProvider' import { useSettings } from '@renderer/hooks/useSettings' -import { fetchTranslate } from '@renderer/services/ApiService' +import useTranslate from '@renderer/hooks/useTranslate' import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { useAppDispatch } from '@renderer/store' import { setTranslateModelPrompt } from '@renderer/store/settings' import type { Model, TranslateHistory } from '@renderer/types' -import { runAsyncFunction, uuid } from '@renderer/utils' +import { runAsyncFunction } from '@renderer/utils' import { createInputScrollHandler, createOutputScrollHandler, @@ -34,7 +34,6 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' let _text = '' -let _result = '' let _targetLanguage = 'english' const TranslateSettings: FC<{ @@ -277,10 +276,8 @@ const TranslatePage: FC = () => { const { shikiMarkdownIt } = useCodeStyle() const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) const [text, setText] = useState(_text) - const [result, setResult] = useState(_result) const [renderedMarkdown, setRenderedMarkdown] = useState('') const { translateModel, setTranslateModel } = useDefaultModel() - const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false) const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false) @@ -299,9 +296,9 @@ const TranslatePage: FC = () => { const allModels = useMemo(() => providers.map((p) => p.models).flat(), [providers]) const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), []) + const { translatedContent, translating, translate, setTranslatedContent, setTranslating } = useTranslate() _text = text - _result = result _targetLanguage = targetLanguage const selectOptions = useMemo( @@ -326,23 +323,6 @@ const TranslatePage: FC = () => { db.settings.put({ id: 'translate:model', value: model.id }) } - const saveTranslateHistory = async ( - sourceText: string, - targetText: string, - sourceLanguage: string, - targetLanguage: string - ) => { - const history: TranslateHistory = { - id: uuid(), - sourceText, - targetText, - sourceLanguage, - targetLanguage, - createdAt: new Date().toISOString() - } - await db.translate_history.add(history) - } - const deleteHistory = async (id: string) => { db.translate_history.delete(id) } @@ -361,7 +341,7 @@ const TranslatePage: FC = () => { return } - setLoading(true) + setTranslating(true) try { // 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测 let actualSourceLanguage: string @@ -385,7 +365,7 @@ const TranslatePage: FC = () => { content: errorMessage, key: 'translate-message' }) - setLoading(false) + setTranslating(false) return } @@ -395,25 +375,15 @@ const TranslatePage: FC = () => { } const assistant = getDefaultTranslateAssistant(actualTargetLanguage, text) - let translatedText = '' - await fetchTranslate({ - content: text, - assistant, - onResponse: (text) => { - translatedText = text.replace(/^\s*\n+/g, '') - setResult(translatedText) - } - }) - await saveTranslateHistory(text, translatedText, actualSourceLanguage, actualTargetLanguage) - setLoading(false) + await translate(text, assistant, actualSourceLanguage, actualTargetLanguage) } catch (error) { console.error('Translation error:', error) window.message.error({ content: String(error), key: 'translate-message' }) - setLoading(false) + setTranslating(false) return } } @@ -424,26 +394,26 @@ const TranslatePage: FC = () => { } const onCopy = () => { - navigator.clipboard.writeText(result) + navigator.clipboard.writeText(translatedContent) setCopied(true) setTimeout(() => setCopied(false), 2000) } const onHistoryItemClick = (history: TranslateHistory) => { setText(history.sourceText) - setResult(history.targetText) + setTranslatedContent(history.targetText) setTargetLanguage(history.targetLanguage) } useEffect(() => { - isEmpty(text) && setResult('') - }, [text]) + isEmpty(text) && setTranslatedContent('') + }, [setTranslatedContent, text]) // Render markdown content when result or enableMarkdown changes useEffect(() => { - if (enableMarkdown && result) { + if (enableMarkdown && translatedContent) { let isMounted = true - shikiMarkdownIt(result).then((rendered) => { + shikiMarkdownIt(translatedContent).then((rendered) => { if (isMounted) { setRenderedMarkdown(rendered) } @@ -455,7 +425,7 @@ const TranslatePage: FC = () => { setRenderedMarkdown('') return undefined } - }, [result, enableMarkdown, shikiMarkdownIt]) + }, [enableMarkdown, shikiMarkdownIt, translatedContent]) useEffect(() => { runAsyncFunction(async () => { @@ -650,7 +620,7 @@ const TranslatePage: FC = () => { }> }> @@ -667,7 +637,7 @@ const TranslatePage: FC = () => { onChange={(e) => setText(e.target.value)} onKeyDown={onKeyDown} onScroll={handleInputScroll} - disabled={loading} + disabled={translating} spellCheck={false} allowClear /> @@ -680,18 +650,18 @@ const TranslatePage: FC = () => { : } /> - {!result ? ( + {!translatedContent ? ( t('translate.output.placeholder') ) : enableMarkdown ? (
) : ( -
{result}
+
{translatedContent}
)} diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index f0c0cb2680..f78a5cadd2 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -24,6 +24,7 @@ import runtime from './runtime' import selectionStore from './selectionStore' import settings from './settings' import shortcuts from './shortcuts' +import translate from './translate' import websearch from './websearch' const rootReducer = combineReducers({ @@ -47,7 +48,8 @@ const rootReducer = combineReducers({ preprocess, messages: newMessagesReducer, messageBlocks: messageBlocksReducer, - inputTools: inputToolsReducer + inputTools: inputToolsReducer, + translate }) const persistedReducer = persistReducer( diff --git a/src/renderer/src/store/translate.ts b/src/renderer/src/store/translate.ts new file mode 100644 index 0000000000..d03e701c55 --- /dev/null +++ b/src/renderer/src/store/translate.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface TranslateState { + translating: boolean + translatedContent: string +} + +const initialState: TranslateState = { + translating: false, + translatedContent: '' +} + +const translateSlice = createSlice({ + name: 'translate', + initialState, + reducers: { + setTranslating: (state, action: PayloadAction) => { + return { + ...state, + translating: action.payload + } + }, + setTranslatedContent: (state, action: PayloadAction) => { + return { + ...state, + translatedContent: action.payload + } + } + } +}) + +export const { setTranslating, setTranslatedContent } = translateSlice.actions + +export default translateSlice.reducer