mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
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:
parent
56cec26858
commit
8925d7d546
@ -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": {
|
||||
|
||||
@ -3734,9 +3734,10 @@
|
||||
"history": {
|
||||
"clear": "履歴をクリア",
|
||||
"clear_description": "履歴をクリアすると、すべての翻訳履歴が削除されます。続行しますか?",
|
||||
"delete": "削除",
|
||||
"delete": "翻訳履歴を削除する",
|
||||
"empty": "翻訳履歴がありません",
|
||||
"error": {
|
||||
"delete": "削除に失敗しました",
|
||||
"save": "保存翻訳履歴に失敗しました"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -3734,9 +3734,10 @@
|
||||
"history": {
|
||||
"clear": "Очистить историю",
|
||||
"clear_description": "Очистка истории удалит все записи переводов. Продолжить?",
|
||||
"delete": "Удалить",
|
||||
"delete": "Удалить историю переводов",
|
||||
"empty": "История переводов отсутствует",
|
||||
"error": {
|
||||
"delete": "Удаление не удалось",
|
||||
"save": "Не удалось сохранить историю переводов"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -3734,9 +3734,10 @@
|
||||
"history": {
|
||||
"clear": "清空历史",
|
||||
"clear_description": "清空历史将删除所有翻译历史记录,是否继续?",
|
||||
"delete": "删除",
|
||||
"delete": "删除翻译历史",
|
||||
"empty": "暂无翻译历史",
|
||||
"error": {
|
||||
"delete": "删除失败",
|
||||
"save": "保存翻译历史失败"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -3734,9 +3734,10 @@
|
||||
"history": {
|
||||
"clear": "清空歷史",
|
||||
"clear_description": "清空歷史將刪除所有翻譯歷史記錄,是否繼續?",
|
||||
"delete": "刪除",
|
||||
"delete": "刪除翻譯歷史",
|
||||
"empty": "翻譯歷史為空",
|
||||
"error": {
|
||||
"delete": "删除失败",
|
||||
"save": "保存翻譯歷史失敗"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -3734,9 +3734,10 @@
|
||||
"history": {
|
||||
"clear": "Καθαρισμός ιστορικού",
|
||||
"clear_description": "Η διαγραφή του ιστορικού θα διαγράψει όλα τα απομνημονεύματα μετάφρασης. Θέλετε να συνεχίσετε;",
|
||||
"delete": "Διαγραφή",
|
||||
"delete": "Διαγραφή του ιστορικού μετάφρασης",
|
||||
"empty": "δεν υπάρχουν απομνημονεύματα μετάφρασης",
|
||||
"error": {
|
||||
"delete": "Αποτυχία διαγραφής",
|
||||
"save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -678,6 +678,8 @@ export interface TranslateHistory {
|
||||
sourceLanguage: TranslateLanguageCode
|
||||
targetLanguage: TranslateLanguageCode
|
||||
createdAt: string
|
||||
/** 收藏状态 */
|
||||
star?: boolean
|
||||
}
|
||||
|
||||
export type CustomTranslateLanguage = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user