-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
284 additions
and
43 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
import { X, Clock, Star, Search } from 'lucide-react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useHistoryStore } from '../../store/history'; | ||
import useStore from '../../store'; | ||
import HistoryList from './HistoryList'; | ||
|
||
interface HistoryDialogProps { | ||
onClose: () => void; | ||
} | ||
|
||
const HistoryDialog: React.FC<HistoryDialogProps> = ({ onClose }) => { | ||
const { t } = useTranslation(); | ||
const { darkMode } = useStore(); | ||
const [searchQuery, setSearchQuery] = React.useState(''); | ||
const [showFavorites, setShowFavorites] = React.useState(false); | ||
|
||
return ( | ||
<div | ||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" | ||
onClick={(e) => { | ||
if (e.target === e.currentTarget) { | ||
onClose(); | ||
} | ||
}} | ||
> | ||
<div className="w-full max-w-4xl h-[90vh] rounded-2xl bg-white shadow-2xl dark:bg-gray-800 flex flex-col"> | ||
{/* 头部 */} | ||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700"> | ||
<div className="flex items-center space-x-2"> | ||
<Clock className="h-5 w-5 text-gray-400" /> | ||
<h2 className="text-lg font-medium text-gray-900 dark:text-white"> | ||
{t('history.title')} | ||
</h2> | ||
</div> | ||
<button | ||
onClick={onClose} | ||
className="rounded-full p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700" | ||
> | ||
<X className="h-5 w-5" /> | ||
</button> | ||
</div> | ||
|
||
{/* 工具栏 */} | ||
<div className="flex items-center space-x-4 px-6 py-4 border-b border-gray-200 dark:border-gray-700"> | ||
<div className="relative flex-1"> | ||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" /> | ||
<input | ||
type="text" | ||
placeholder={t('history.searchPlaceholder')} | ||
value={searchQuery} | ||
onChange={(e) => setSearchQuery(e.target.value)} | ||
className="w-full rounded-md border-gray-300 pl-10 focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white" | ||
/> | ||
</div> | ||
<button | ||
onClick={() => setShowFavorites(!showFavorites)} | ||
className={`flex items-center space-x-2 px-3 py-2 rounded-md ${ | ||
showFavorites | ||
? 'bg-indigo-50 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400' | ||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700' | ||
}`} | ||
> | ||
<Star className="h-4 w-4" /> | ||
<span>{t('history.favorites')}</span> | ||
</button> | ||
</div> | ||
|
||
{/* 历史记录列表 */} | ||
<div className="flex-1 overflow-y-auto p-6"> | ||
<HistoryList | ||
searchQuery={searchQuery} | ||
showFavorites={showFavorites} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default HistoryDialog; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,111 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Star, ArrowRight, Trash2, Clock } from 'lucide-react'; | ||
import { useHistoryStore } from '../../store/history'; | ||
import useStore from '../../store'; | ||
import { formatDistanceToNow } from 'date-fns'; | ||
import { zhCN, enUS } from 'date-fns/locale'; | ||
|
||
export const HistoryList = () => { | ||
const { history, fetchHistory, toggleFavorite } = useHistoryStore(); | ||
interface HistoryListProps { | ||
searchQuery: string; | ||
showFavorites: boolean; | ||
} | ||
|
||
useEffect(() => { | ||
fetchHistory(); | ||
}, []); | ||
const HistoryList: React.FC<HistoryListProps> = ({ searchQuery, showFavorites }) => { | ||
const { t, i18n } = useTranslation(); | ||
const { history, toggleFavorite, deleteHistory } = useHistoryStore(); | ||
const { setSourceText, setTranslatedText, sourceLang, targetLang, setSourceLang, setTargetLang } = useStore(); | ||
|
||
// 处理收藏切换 | ||
const handleFavoriteToggle = async (id: string, isFavorite: boolean) => { | ||
await toggleFavorite(id, isFavorite); | ||
const dateLocale = i18n.language === 'zh' ? zhCN : enUS; | ||
|
||
const filteredHistory = history | ||
.filter(item => { | ||
const matchesSearch = !searchQuery || | ||
item.source_text.toLowerCase().includes(searchQuery.toLowerCase()) || | ||
item.translated_text.toLowerCase().includes(searchQuery.toLowerCase()); | ||
const matchesFavorite = !showFavorites || item.is_favorite; | ||
return matchesSearch && matchesFavorite; | ||
}) | ||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); | ||
|
||
const handleRestore = (item: typeof history[0]) => { | ||
setSourceText(item.source_text); | ||
setTranslatedText(item.translated_text); | ||
setSourceLang(item.source_lang); | ||
setTargetLang(item.target_lang); | ||
}; | ||
|
||
return ( | ||
// 渲染历史记录列表 | ||
<div className="space-y-4"> | ||
{filteredHistory.map(item => ( | ||
<div | ||
key={item.id} | ||
className="group relative rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800" | ||
> | ||
<div className="mb-2 flex items-center justify-between"> | ||
<div className="flex items-center space-x-2"> | ||
<button | ||
onClick={() => toggleFavorite(item.id, !item.is_favorite)} | ||
className={`rounded-full p-1 transition-colors ${ | ||
item.is_favorite | ||
? 'text-yellow-500 hover:text-yellow-600' | ||
: 'text-gray-400 hover:text-gray-500' | ||
}`} | ||
> | ||
<Star className="h-4 w-4 fill-current" /> | ||
</button> | ||
<span className="text-sm text-gray-500 dark:text-gray-400"> | ||
{formatDistanceToNow(new Date(item.created_at), { | ||
addSuffix: true, | ||
locale: dateLocale | ||
})} | ||
</span> | ||
</div> | ||
<div className="flex items-center space-x-2"> | ||
<button | ||
onClick={() => handleRestore(item)} | ||
className="rounded-md px-2 py-1 text-sm text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-900/50" | ||
> | ||
{t('history.restore')} | ||
</button> | ||
<button | ||
onClick={() => deleteHistory(item.id)} | ||
className="rounded-md p-1 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400" | ||
> | ||
<Trash2 className="h-4 w-4" /> | ||
</button> | ||
</div> | ||
</div> | ||
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> | ||
<div className="space-y-2"> | ||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400"> | ||
{t(`common.${item.source_lang}`)} | ||
</div> | ||
<div className="text-sm text-gray-900 dark:text-white line-clamp-3"> | ||
{item.source_text} | ||
</div> | ||
</div> | ||
<div className="space-y-2"> | ||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400"> | ||
{t(`common.${item.target_lang}`)} | ||
</div> | ||
<div className="text-sm text-gray-900 dark:text-white line-clamp-3"> | ||
{item.translated_text} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
))} | ||
|
||
{filteredHistory.length === 0 && ( | ||
<div className="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"> | ||
<Clock className="h-12 w-12 mb-4" /> | ||
<p>{showFavorites ? t('history.noFavorites') : t('history.noHistory')}</p> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; | ||
}; | ||
|
||
export default HistoryList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,65 @@ | ||
import { supabase } from '../../config/supabase-auth'; | ||
import type { Database } from '../../types/supabase'; | ||
import { withRetry } from '../../utils/supabase-helper'; | ||
|
||
type History = Database['public']['Tables']['translation_history']['Row']; | ||
type HistoryInsert = Database['public']['Tables']['translation_history']['Insert']; | ||
type HistoryUpdate = Database['public']['Tables']['translation_history']['Update']; | ||
|
||
export const historyService = { | ||
// 获取翻译历史 | ||
async getHistory(userId: string, limit = 20) { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.select('*') | ||
.eq('user_id', userId) | ||
.order('created_at', { ascending: false }) | ||
.limit(limit); | ||
|
||
if (error) throw error; | ||
return data; | ||
async getHistory(userId: string, limit = 50) { | ||
return withRetry(async () => { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.select('*') | ||
.eq('user_id', userId) | ||
.order('created_at', { ascending: false }) | ||
.limit(limit); | ||
|
||
if (error) throw error; | ||
return data; | ||
}); | ||
}, | ||
|
||
// 添加翻译记录 | ||
async addHistory(history: HistoryInsert) { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.insert(history) | ||
.select() | ||
.single(); | ||
|
||
if (error) throw error; | ||
return data; | ||
return withRetry(async () => { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.insert(history) | ||
.select() | ||
.single(); | ||
|
||
if (error) throw error; | ||
return data; | ||
}); | ||
}, | ||
|
||
// 更新收藏状态 | ||
async toggleFavorite(id: string, isFavorite: boolean) { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.update({ is_favorite: isFavorite }) | ||
.eq('id', id) | ||
.select() | ||
.single(); | ||
|
||
if (error) throw error; | ||
return data; | ||
return withRetry(async () => { | ||
const { data, error } = await supabase | ||
.from('translation_history') | ||
.update({ is_favorite: isFavorite }) | ||
.eq('id', id) | ||
.select() | ||
.single(); | ||
|
||
if (error) throw error; | ||
return data; | ||
}); | ||
}, | ||
|
||
// 删除历史记录 | ||
async deleteHistory(id: string) { | ||
return withRetry(async () => { | ||
const { error } = await supabase | ||
.from('translation_history') | ||
.delete() | ||
.eq('id', id); | ||
|
||
if (error) throw error; | ||
}); | ||
} | ||
}; |
Oops, something went wrong.