refactor: 翻译页UI重构
This commit is contained in:
parent
7f05626a8f
commit
16f87537a2
@ -9,11 +9,10 @@ import { fetchTranslate } from '@renderer/services/ApiService'
|
|||||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import { Assistant, Message } from '@renderer/types'
|
import { Assistant, Message } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { Button, Select, Space } from 'antd'
|
import { Button, Flex, Select, Space } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { debounce } from 'lodash'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -37,76 +36,6 @@ const TranslatePage: FC = () => {
|
|||||||
_result = result
|
_result = result
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
const safetyMarginOfTextarea = (textarea: HTMLTextAreaElement): number => {
|
|
||||||
const defaultSafetyMargin = 30
|
|
||||||
const lineHeight = window.getComputedStyle(textarea).lineHeight
|
|
||||||
if (lineHeight.endsWith('px')) {
|
|
||||||
const safetyMargin = parseInt(lineHeight.slice(0, -2))
|
|
||||||
if (Number.isNaN(safetyMargin)) {
|
|
||||||
return defaultSafetyMargin
|
|
||||||
} else {
|
|
||||||
return safetyMargin + 4
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return defaultSafetyMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTextareaToMaxHeight = (textarea: HTMLTextAreaElement, safetyMargin: number) => {
|
|
||||||
const { top: textareaTop } = textarea.getBoundingClientRect()
|
|
||||||
textarea.style.height = `${window.innerHeight - safetyMargin - textareaTop}px`
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTextareaHeight = useCallback((textarea: HTMLTextAreaElement, contentContainer: HTMLDivElement | null) => {
|
|
||||||
textarea.style.height = 'auto'
|
|
||||||
const unlimitedHeightUpdate = () => {
|
|
||||||
textarea.style.height = `${textarea.scrollHeight}px`
|
|
||||||
}
|
|
||||||
const safetyMargin = safetyMarginOfTextarea(textarea)
|
|
||||||
|
|
||||||
if (contentContainer) {
|
|
||||||
const { bottom: textareaBottom, top: textareaTop } = textarea.getBoundingClientRect()
|
|
||||||
const { bottom: contentContainerBottom } = contentContainer.getBoundingClientRect()
|
|
||||||
if (textareaBottom !== 0 && contentContainerBottom !== 0) {
|
|
||||||
if (contentContainerBottom - textareaTop - textarea.scrollHeight < safetyMargin) {
|
|
||||||
updateTextareaToMaxHeight(textarea, safetyMargin)
|
|
||||||
} else {
|
|
||||||
unlimitedHeightUpdate()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unlimitedHeightUpdate()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unlimitedHeightUpdate()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
updateTextareaHeight(event.target, contentContainerRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize when switching to this page
|
|
||||||
if (textAreaRef?.current?.resizableTextArea?.textArea) {
|
|
||||||
updateTextareaHeight(textAreaRef.current.resizableTextArea.textArea, contentContainerRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
const debounceHandleResize = debounce(
|
|
||||||
() => {
|
|
||||||
if (textAreaRef?.current?.resizableTextArea) {
|
|
||||||
updateTextareaHeight(textAreaRef.current.resizableTextArea.textArea, contentContainerRef.current)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
16,
|
|
||||||
{ maxWait: 16 }
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleResize = () => debounceHandleResize()
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
return () => window.removeEventListener('resize', handleResize)
|
|
||||||
}, [textAreaRef, updateTextareaHeight])
|
|
||||||
|
|
||||||
const onTranslate = async () => {
|
const onTranslate = async () => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
return
|
return
|
||||||
@ -182,12 +111,14 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container id="translate-page">
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer id="content-container" ref={contentContainerRef}>
|
<ContentContainer id="content-container" ref={contentContainerRef}>
|
||||||
<MenuContainer>
|
<InputContainer>
|
||||||
|
<OperationBar>
|
||||||
|
<Flex align="center" gap={20}>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value="any"
|
value="any"
|
||||||
@ -196,7 +127,37 @@ const TranslatePage: FC = () => {
|
|||||||
disabled
|
disabled
|
||||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||||
/>
|
/>
|
||||||
|
<SettingButton />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<TranslateButton
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={onTranslate}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
icon={<SendOutlined />}>
|
||||||
|
{t('translate.button.translate')}
|
||||||
|
</TranslateButton>
|
||||||
|
</OperationBar>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
ref={textAreaRef}
|
||||||
|
variant="borderless"
|
||||||
|
placeholder={t('translate.input.placeholder')}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
spellCheck={false}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
|
||||||
|
<Flex justify="center" align="center">
|
||||||
<SwapOutlined />
|
<SwapOutlined />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<OutputContainer>
|
||||||
|
<OperationBar>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={targetLanguage}
|
value={targetLanguage}
|
||||||
@ -216,125 +177,84 @@ const TranslatePage: FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SettingButton />
|
|
||||||
</MenuContainer>
|
|
||||||
<TranslateInputWrapper>
|
|
||||||
<InputContainer>
|
|
||||||
<Textarea
|
|
||||||
ref={textAreaRef}
|
|
||||||
onInput={handleInput}
|
|
||||||
variant="borderless"
|
|
||||||
placeholder={t('translate.input.placeholder')}
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => setText(e.target.value)}
|
|
||||||
disabled={loading}
|
|
||||||
spellCheck={false}
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
<TranslateButton
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={onTranslate}
|
|
||||||
disabled={!text.trim()}
|
|
||||||
icon={<SendOutlined />}>
|
|
||||||
{t('translate.button.translate')}
|
|
||||||
</TranslateButton>
|
|
||||||
</InputContainer>
|
|
||||||
<OutputContainer>
|
|
||||||
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
|
||||||
<CopyButton
|
<CopyButton
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!result}
|
disabled={!result}
|
||||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
||||||
/>
|
/>
|
||||||
|
</OperationBar>
|
||||||
|
|
||||||
|
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
||||||
</OutputContainer>
|
</OutputContainer>
|
||||||
</TranslateInputWrapper>
|
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div`
|
||||||
display: flex;
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 40px 1fr;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const MenuContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
gap: 20px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TranslateInputWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
min-height: 350px;
|
|
||||||
gap: 20px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const InputContainer = styled.div`
|
const InputContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
border: 1px solid var(--color-border-soft);
|
border: 1px solid var(--color-border-soft);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-right: 2px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const OperationBar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px 8px 10px 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Textarea = styled(TextArea)`
|
const Textarea = styled(TextArea)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
overflow: auto;
|
border-radius: 0;
|
||||||
.ant-input {
|
.ant-input {
|
||||||
resize: none;
|
resize: none;
|
||||||
padding: 15px 20px;
|
padding: 5px 16px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const OutputContainer = styled.div`
|
const OutputContainer = styled.div`
|
||||||
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-right: 2px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const OutputText = styled.div`
|
const OutputText = styled.div`
|
||||||
padding: 5px 10px;
|
min-height: 0;
|
||||||
max-height: calc(100vh - var(--navbar-height) - 120px);
|
flex: 1;
|
||||||
overflow: auto;
|
padding: 5px 16px;
|
||||||
|
overflow-y: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TranslateButton = styled(Button)`
|
const TranslateButton = styled(Button)``
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
z-index: 10;
|
|
||||||
`
|
|
||||||
|
|
||||||
const CopyButton = styled(Button)`
|
const CopyButton = styled(Button)``
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
bottom: 15px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default TranslatePage
|
export default TranslatePage
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user