feat: translate history star (#9433)

* feat(types): 为翻译历史记录添加收藏状态字段

* feat(翻译服务): 添加更新翻译历史记录功能

新增updateTranslateHistory方法用于更新翻译历史记录,支持修改原文、译文、语言及收藏状态

* refactor(TranslateService): 简化更新翻译历史记录的参数结构

* fix(TranslateService): 添加删除翻译历史的错误处理

捕获删除翻译历史时的异常并记录日志,避免静默失败

* feat(翻译历史): 添加收藏功能并优化删除操作

- 新增翻译历史项的收藏功能
- 将删除操作从右键菜单移至显式按钮
- 增加删除失败的国际化提示
- 调整列表项高度以适应新功能

* feat(翻译历史): 添加收藏筛选功能

新增显示已收藏翻译历史的功能,用户可以通过点击星标按钮切换筛选状态

* feat(i18n): 添加翻译历史删除失败的错误消息

为翻译历史功能添加删除操作失败时的错误提示消息,支持多语言显示

* fix(翻译历史): 将删除按钮文本改为"删除翻译历史"并添加确认弹窗

修改删除按钮文本使其更明确,并添加确认弹窗防止误操作

* style(TabContainer): 移除多余的空行以保持代码整洁
This commit is contained in:
Phantom 2025-08-25 00:10:41 +08:00 committed by GitHub
parent 56cec26858
commit 8925d7d546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 151 additions and 44 deletions

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Clear History",
"clear_description": "Clear history will delete all translation history, continue?",
"delete": "Delete",
"delete": "Delete translation history",
"empty": "No translation history",
"error": {
"delete": "Deletion failed",
"save": "Failed to save translation history"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "履歴をクリア",
"clear_description": "履歴をクリアすると、すべての翻訳履歴が削除されます。続行しますか?",
"delete": "削除",
"delete": "翻訳履歴を削除する",
"empty": "翻訳履歴がありません",
"error": {
"delete": "削除に失敗しました",
"save": "保存翻訳履歴に失敗しました"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Очистить историю",
"clear_description": "Очистка истории удалит все записи переводов. Продолжить?",
"delete": "Удалить",
"delete": "Удалить историю переводов",
"empty": "История переводов отсутствует",
"error": {
"delete": "Удаление не удалось",
"save": "Не удалось сохранить историю переводов"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "清空历史",
"clear_description": "清空历史将删除所有翻译历史记录,是否继续?",
"delete": "删除",
"delete": "删除翻译历史",
"empty": "暂无翻译历史",
"error": {
"delete": "删除失败",
"save": "保存翻译历史失败"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "清空歷史",
"clear_description": "清空歷史將刪除所有翻譯歷史記錄,是否繼續?",
"delete": "刪除",
"delete": "刪除翻譯歷史",
"empty": "翻譯歷史為空",
"error": {
"delete": "删除失败",
"save": "保存翻譯歷史失敗"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Καθαρισμός ιστορικού",
"clear_description": "Η διαγραφή του ιστορικού θα διαγράψει όλα τα απομνημονεύματα μετάφρασης. Θέλετε να συνεχίσετε;",
"delete": "Διαγραφή",
"delete": "Διαγραφή του ιστορικού μετάφρασης",
"empty": "δεν υπάρχουν απομνημονεύματα μετάφρασης",
"error": {
"delete": "Αποτυχία διαγραφής",
"save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Borrar historial",
"clear_description": "Borrar el historial eliminará todos los registros de traducciones, ¿desea continuar?",
"delete": "Eliminar",
"delete": "Eliminar historial de traducción",
"empty": "Sin historial de traducciones por el momento",
"error": {
"delete": "Eliminación fallida",
"save": "Error al guardar el historial de traducciones"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Effacer l'historique",
"clear_description": "L'effacement de l'historique supprimera toutes les entrées d'historique de traduction, voulez-vous continuer ?",
"delete": "Supprimer",
"delete": "Supprimer l'historique des traductions",
"empty": "Aucun historique de traduction pour le moment",
"error": {
"delete": "Échec de la suppression",
"save": "Échec de la sauvegarde de l'historique des traductions"
},
"search": {

View File

@ -3734,9 +3734,10 @@
"history": {
"clear": "Limpar Histórico",
"clear_description": "Limpar histórico irá deletar todos os registros de tradução. Deseja continuar?",
"delete": "Excluir",
"delete": "Apagar histórico de traduções",
"empty": "Nenhum histórico de tradução disponível",
"error": {
"delete": "Falha ao excluir",
"save": "Falha ao guardar o histórico de traduções"
},
"search": {

View File

@ -1,11 +1,11 @@
import { DeleteOutlined } from '@ant-design/icons'
import { DeleteOutlined, StarFilled, StarOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import db from '@renderer/databases'
import useTranslate from '@renderer/hooks/useTranslate'
import { clearHistory, deleteHistory } from '@renderer/services/TranslateService'
import { clearHistory, deleteHistory, updateTranslateHistory } from '@renderer/services/TranslateService'
import { TranslateHistory, TranslateLanguage } from '@renderer/types'
import { Button, Drawer, Dropdown, Empty, Flex, Input, Popconfirm } from 'antd'
import { Button, Drawer, Empty, Flex, Input, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import { isEmpty } from 'lodash'
@ -28,7 +28,7 @@ type TranslateHistoryProps = {
// const logger = loggerService.withContext('TranslateHistory')
// px
const ITEM_HEIGHT = 140
const ITEM_HEIGHT = 160
const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItemClick, onClose }) => {
const { t } = useTranslation()
@ -36,6 +36,7 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
const [search, setSearch] = useState('')
const [displayedHistory, setDisplayedHistory] = useState<DisplayedTranslateHistoryItem[]>([])
const [showStared, setShowStared] = useState<boolean>(false)
const translateHistory: DisplayedTranslateHistoryItem[] = useMemo(() => {
if (!_translateHistory) return []
@ -57,15 +58,64 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
[search]
)
const starFilter = useMemo(
() => (showStared ? (item: DisplayedTranslateHistoryItem) => !!item.star : () => true),
[showStared]
)
const finalFilter = useCallback(
(item: DisplayedTranslateHistoryItem) => searchFilter(item) && starFilter(item),
[searchFilter, starFilter]
)
const handleStar = useCallback(
(id: string) => {
const origin = translateHistory.find((item) => item.id === id)
if (!origin) {
return
}
updateTranslateHistory(id, { star: !origin.star })
},
[translateHistory]
)
const handleDelete = useCallback(
(id: string) => {
try {
deleteHistory(id)
} catch (e) {
window.message.error(t('translate.history.error.delete'))
}
},
[t]
)
useEffect(() => {
setDisplayedHistory(translateHistory.filter(searchFilter))
}, [searchFilter, translateHistory])
setDisplayedHistory(translateHistory.filter(finalFilter))
}, [finalFilter, translateHistory])
const Title = () => {
return (
<Flex align="center">
{t('translate.history.title')}
<Button
icon={showStared ? <StarFilled /> : <StarOutlined />}
color="yellow"
variant="text"
onClick={(e) => {
e.stopPropagation()
setShowStared(!showStared)
}}
/>
</Flex>
)
}
const deferredHistory = useDeferredValue(displayedHistory)
return (
<Drawer
title={t('translate.history.title')}
title={<Title />}
closeIcon={null}
open={isOpen}
maskClosable
@ -121,38 +171,54 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
<DynamicVirtualList list={deferredHistory} estimateSize={() => ITEM_HEIGHT}>
{(item) => {
return (
<Dropdown
key={item.id}
trigger={['contextMenu']}
menu={{
items: [
{
key: 'delete',
label: t('translate.history.delete'),
icon: <DeleteOutlined />,
danger: true,
onClick: () => deleteHistory(item.id)
}
]
}}>
<HistoryListItemContainer>
<HistoryListItem onClick={() => onHistoryItemClick(item)}>
<Flex justify="space-between" vertical gap={4} style={{ width: '100%' }}>
<Flex align="center" justify="space-between" style={{ flex: 1 }}>
<Flex align="center" gap={6}>
<HistoryListItemLanguage>{item._sourceLanguage.label()} </HistoryListItemLanguage>
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
</Flex>
<HistoryListItemDate>{item.createdAt}</HistoryListItemDate>
<HistoryListItemContainer>
<HistoryListItem onClick={() => onHistoryItemClick(item)}>
<Flex justify="space-between" vertical gap={4} style={{ width: '100%', height: '100%', flex: 1 }}>
<Flex align="center" justify="space-between" style={{ height: 30 }}>
<Flex align="center" gap={6}>
<HistoryListItemLanguage>{item._sourceLanguage.label()} </HistoryListItemLanguage>
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
</Flex>
{/* tool bar */}
<Flex align="center" justify="flex-end">
<Button
icon={item.star ? <StarFilled /> : <StarOutlined />}
color="yellow"
variant="text"
onClick={(e) => {
e.stopPropagation()
handleStar(item.id)
}}
/>
<Popconfirm
title={t('translate.history.delete')}
onConfirm={() => {
handleDelete(item.id)
}}
onPopupClick={(e) => {
e.stopPropagation()
}}>
<Button
icon={<DeleteOutlined />}
danger
type="text"
onClick={(e) => {
e.stopPropagation()
}}
/>
</Popconfirm>
</Flex>
</Flex>
<HistoryListItemTextContainer>
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
<HistoryListItemTitle style={{ color: 'var(--color-text-2)' }}>
{item.targetText}
</HistoryListItemTitle>
</Flex>
</HistoryListItem>
</HistoryListItemContainer>
</Dropdown>
</HistoryListItemTextContainer>
<HistoryListItemDate>{item.createdAt}</HistoryListItemDate>
</Flex>
</HistoryListItem>
</HistoryListItemContainer>
)
}}
</DynamicVirtualList>
@ -237,6 +303,12 @@ const HistoryListItemLanguage = styled.div`
color: var(--color-text-3);
`
const HistoryListItemTextContainer = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`
const IconWrapper = styled.div`
display: flex;
justify-content: center;

View File

@ -215,13 +215,37 @@ export const saveTranslateHistory = async (
await db.translate_history.add(history)
}
/**
*
* @param id - ID
* @param update -
* @returns Promise<void>
*/
export const updateTranslateHistory = async (id: string, update: Omit<Partial<TranslateHistory>, 'id'>) => {
try {
const history: Partial<TranslateHistory> = {
...update,
id
}
await db.translate_history.update(id, history)
} catch (e) {
logger.error('Failed to update translate history', e as Error)
throw e
}
}
/**
*
* @param id - ID
* @returns Promise<void>
*/
export const deleteHistory = async (id: string) => {
db.translate_history.delete(id)
try {
db.translate_history.delete(id)
} catch (e) {
logger.error('Failed to delete translate history', e as Error)
throw e
}
}
/**

View File

@ -678,6 +678,8 @@ export interface TranslateHistory {
sourceLanguage: TranslateLanguageCode
targetLanguage: TranslateLanguageCode
createdAt: string
/** 收藏状态 */
star?: boolean
}
export type CustomTranslateLanguage = {