feat(settings): 实现数据导出和清除聊天功能
- 添加导出聊天数据功能,支持 JSON 格式下载 - 添加清除所有聊天功能,带确认对话框 - 显示待删除对话和消息数量统计 - 优化 SettingsSection 组件支持暗色主题
This commit is contained in:
parent
749247affa
commit
0b5b67174f
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
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 { Toggle } from '@/components/ui/Toggle';
|
||||||
import { ModelCardSelector } from '@/components/ui/ModelCardSelector';
|
import { ModelCardSelector } from '@/components/ui/ModelCardSelector';
|
||||||
import { FontSizePicker } from '@/components/ui/FontSizePicker';
|
import { FontSizePicker } from '@/components/ui/FontSizePicker';
|
||||||
|
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useSettings, useModels, useTools } from '@/hooks/useSettings';
|
import { useSettings, useModels, useTools } from '@/hooks/useSettings';
|
||||||
|
|
||||||
@ -59,6 +60,12 @@ export default function SettingsPage() {
|
|||||||
const [temperature, setTemperature] = useState('0.7');
|
const [temperature, setTemperature] = useState('0.7');
|
||||||
const [promptSaveStatus, setPromptSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
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(() => {
|
useEffect(() => {
|
||||||
if (settings) {
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
@ -510,9 +581,17 @@ export default function SettingsPage() {
|
|||||||
label="导出数据"
|
label="导出数据"
|
||||||
description="下载所有聊天历史"
|
description="下载所有聊天历史"
|
||||||
>
|
>
|
||||||
<button className="btn-ghost inline-flex items-center gap-2">
|
<button
|
||||||
<Download size={16} />
|
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>
|
</button>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
@ -520,12 +599,49 @@ export default function SettingsPage() {
|
|||||||
label="清除所有聊天"
|
label="清除所有聊天"
|
||||||
description="删除所有对话历史"
|
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} />
|
<Trash2 size={16} />
|
||||||
清除
|
清除
|
||||||
</button>
|
</button>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</SettingsSection>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -543,22 +659,22 @@ function SettingsSection({ title, description, children, variant = 'default' }:
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-white border rounded-md mb-6 overflow-hidden',
|
'bg-[var(--color-bg-primary)] border rounded-md mb-6 overflow-hidden',
|
||||||
variant === 'danger' ? 'border-red-200' : 'border-[var(--color-border)]'
|
variant === 'danger' ? 'border-red-500/20' : 'border-[var(--color-border)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-5 py-4 border-b',
|
'px-5 py-4 border-b',
|
||||||
variant === 'danger'
|
variant === 'danger'
|
||||||
? 'bg-red-50 border-red-200'
|
? 'bg-red-500/10 border-red-500/20'
|
||||||
: 'border-[var(--color-border-light)]'
|
: 'border-[var(--color-border-light)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-base font-semibold',
|
'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}
|
{title}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user