feat(settings): 实现数据导出和清除聊天功能

- 添加导出聊天数据功能,支持 JSON 格式下载
- 添加清除所有聊天功能,带确认对话框
- 显示待删除对话和消息数量统计
- 优化 SettingsSection 组件支持暗色主题
This commit is contained in:
gaoziman 2025-12-19 15:57:50 +08:00
parent 749247affa
commit 0b5b67174f

View File

@ -2,10 +2,11 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft, Download, Check, Loader2, Eye, EyeOff, RotateCcw, Moon, Sun, Sparkles, Trash2 } from 'lucide-react';
import { ArrowLeft, Download, Check, Loader2, Eye, EyeOff, RotateCcw, Moon, Sun, Sparkles, Trash2, AlertTriangle } from 'lucide-react';
import { Toggle } from '@/components/ui/Toggle';
import { ModelCardSelector } from '@/components/ui/ModelCardSelector';
import { FontSizePicker } from '@/components/ui/FontSizePicker';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { cn } from '@/lib/utils';
import { useSettings, useModels, useTools } from '@/hooks/useSettings';
@ -59,6 +60,12 @@ export default function SettingsPage() {
const [temperature, setTemperature] = useState('0.7');
const [promptSaveStatus, setPromptSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
// 导出和清除状态
const [exportLoading, setExportLoading] = useState(false);
const [clearDialogOpen, setClearDialogOpen] = useState(false);
const [clearLoading, setClearLoading] = useState(false);
const [chatStats, setChatStats] = useState<{ conversationCount: number; messageCount: number } | null>(null);
// 当设置加载完成后,更新本地状态
useEffect(() => {
if (settings) {
@ -198,6 +205,70 @@ export default function SettingsPage() {
}
};
// 导出聊天数据
const handleExportData = async () => {
setExportLoading(true);
try {
const response = await fetch('/api/conversations/export');
if (!response.ok) {
throw new Error('Export failed');
}
const data = await response.json();
// 创建 Blob 并下载
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const date = new Date().toISOString().split('T')[0];
a.download = `cchcode-chat-export-${date}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to export data:', error);
alert('导出失败,请稍后重试');
} finally {
setExportLoading(false);
}
};
// 打开清除对话框(获取统计信息)
const handleOpenClearDialog = async () => {
try {
const response = await fetch('/api/conversations/all');
if (response.ok) {
const stats = await response.json();
setChatStats(stats);
}
} catch (error) {
console.error('Failed to get stats:', error);
}
setClearDialogOpen(true);
};
// 清除所有聊天
const handleClearAllChats = async () => {
setClearLoading(true);
try {
const response = await fetch('/api/conversations/all', {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Delete failed');
}
setClearDialogOpen(false);
// 跳转到首页
window.location.href = '/';
} catch (error) {
console.error('Failed to clear chats:', error);
alert('清除失败,请稍后重试');
} finally {
setClearLoading(false);
}
};
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center">
@ -510,9 +581,17 @@ export default function SettingsPage() {
label="导出数据"
description="下载所有聊天历史"
>
<button className="btn-ghost inline-flex items-center gap-2">
<Download size={16} />
<button
onClick={handleExportData}
disabled={exportLoading}
className="btn-ghost inline-flex items-center gap-2"
>
{exportLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Download size={16} />
)}
{exportLoading ? '导出中...' : '导出'}
</button>
</SettingsItem>
@ -520,12 +599,49 @@ export default function SettingsPage() {
label="清除所有聊天"
description="删除所有对话历史"
>
<button className="btn-ghost text-red-600 hover:text-red-700 inline-flex items-center gap-2">
<button
onClick={handleOpenClearDialog}
className="btn-ghost text-red-600 hover:text-red-700 inline-flex items-center gap-2"
>
<Trash2 size={16} />
</button>
</SettingsItem>
</SettingsSection>
{/* 清除确认对话框 */}
<ConfirmDialog
isOpen={clearDialogOpen}
onClose={() => setClearDialogOpen(false)}
onConfirm={handleClearAllChats}
title="确认清除所有聊天记录?"
variant="danger"
confirmText="确认清除"
cancelText="取消"
loading={clearLoading}
>
<div className="space-y-3">
<p className="text-sm text-[var(--color-text-secondary)]">
</p>
<ul className="text-sm text-[var(--color-text-primary)] space-y-1 ml-4">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-red-500" />
<span><strong>{chatStats?.conversationCount || 0}</strong> </span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-red-500" />
<span><strong>{chatStats?.messageCount || 0}</strong> </span>
</li>
</ul>
<div className="flex items-start gap-2 p-3 rounded-md bg-red-500/10 border border-red-500/20">
<AlertTriangle className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
<p className="text-xs text-red-500">
</p>
</div>
</div>
</ConfirmDialog>
</main>
</div>
);
@ -543,22 +659,22 @@ function SettingsSection({ title, description, children, variant = 'default' }:
return (
<section
className={cn(
'bg-white border rounded-md mb-6 overflow-hidden',
variant === 'danger' ? 'border-red-200' : 'border-[var(--color-border)]'
'bg-[var(--color-bg-primary)] border rounded-md mb-6 overflow-hidden',
variant === 'danger' ? 'border-red-500/20' : 'border-[var(--color-border)]'
)}
>
<div
className={cn(
'px-5 py-4 border-b',
variant === 'danger'
? 'bg-red-50 border-red-200'
? 'bg-red-500/10 border-red-500/20'
: 'border-[var(--color-border-light)]'
)}
>
<h2
className={cn(
'text-base font-semibold',
variant === 'danger' ? 'text-red-600' : 'text-[var(--color-text-primary)]'
variant === 'danger' ? 'text-red-500' : 'text-[var(--color-text-primary)]'
)}
>
{title}