Skip to content

Commit

Permalink
历史记录
Browse files Browse the repository at this point in the history
  • Loading branch information
cottman99 committed Nov 23, 2024
1 parent 0d5cd46 commit 62cb172
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 43 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.39.3",
"axios": "^1.6.7",
"date-fns": "^4.1.0",
"i18next": "^23.16.5",
"i18next-browser-languagedetector": "^8.0.0",
"katex": "^0.16.9",
Expand Down
13 changes: 11 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n/config';
import { Github, Settings, BookOpen, Save, Download, Upload, Clipboard } from 'lucide-react';
import { Github, Settings, BookOpen, Save, Download, Upload, Clipboard, Clock } from 'lucide-react';
import { ToastContainer } from 'react-toastify';
import useStore from './store/index';
import AuthPage from './components/auth/AuthPage';
Expand All @@ -16,9 +16,10 @@ import 'react-toastify/dist/ReactToastify.css';
import useAuthStore from './store/auth';
import { useSettingsStore } from './store/settings';
import { useTemplateStore } from './store/templates';
import HistoryDialog from './components/history/HistoryDialog';

function App() {
const { darkMode, openSettings, uploadFile, downloadFile, saveFile, copyToClipboard, isSettingsOpen } = useStore();
const { darkMode, openSettings, uploadFile, downloadFile, saveFile, copyToClipboard, isSettingsOpen, isHistoryOpen, openHistory, closeHistory } = useStore();
const { checkAuth, isAuthenticated } = useAuthStore();
const { fetchSettings } = useSettingsStore();
const { fetchTemplates } = useTemplateStore();
Expand Down Expand Up @@ -92,6 +93,13 @@ function App() {
>
<Settings className="h-5 w-5" />
</button>
<button
onClick={openHistory}
className="p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
title={i18n.t('common.history')}
>
<Clock className="h-5 w-5" />
</button>
<a
href="https://github.com"
target="_blank"
Expand All @@ -115,6 +123,7 @@ function App() {
</div>
</main>
{isSettingsOpen && <SettingsDialog />}
{isHistoryOpen && <HistoryDialog onClose={closeHistory} />}
</div>
</ProtectedRoute>
}
Expand Down
81 changes: 81 additions & 0 deletions src/components/history/HistoryDialog.tsx
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;
113 changes: 103 additions & 10 deletions src/components/history/HistoryList.tsx
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;
73 changes: 46 additions & 27 deletions src/services/supabase/history.ts
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;
});
}
};
Loading

0 comments on commit 62cb172

Please sign in to comment.