import { useState, useEffect, useCallback } from 'react' import { Search, Download, FolderOpen, RefreshCw, Check, Calendar, FileJson, FileText, Table, Loader2, X, ChevronDown, ChevronLeft, ChevronRight, FileSpreadsheet, Database, FileCode, CheckCircle, XCircle, ExternalLink } from 'lucide-react' import * as configService from '../services/config' import './ExportPage.scss' interface ChatSession { username: string displayName?: string avatarUrl?: string summary: string lastTimestamp: number } interface ExportOptions { format: 'chatlab' | 'chatlab-jsonl' | 'json' | 'html' | 'txt' | 'excel' | 'sql' dateRange: { start: Date; end: Date } | null useAllTime: boolean exportAvatars: boolean } interface ExportResult { success: boolean successCount?: number failCount?: number error?: string } function ExportPage() { const [sessions, setSessions] = useState([]) const [filteredSessions, setFilteredSessions] = useState([]) const [selectedSessions, setSelectedSessions] = useState>(new Set()) const [isLoading, setIsLoading] = useState(true) const [searchKeyword, setSearchKeyword] = useState('') const [exportFolder, setExportFolder] = useState('') const [isExporting, setIsExporting] = useState(false) const [exportProgress, setExportProgress] = useState({ current: 0, total: 0, currentName: '' }) const [exportResult, setExportResult] = useState(null) const [showDatePicker, setShowDatePicker] = useState(false) const [calendarDate, setCalendarDate] = useState(new Date()) const [selectingStart, setSelectingStart] = useState(true) const [options, setOptions] = useState({ format: 'chatlab', dateRange: { start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), end: new Date() }, useAllTime: true, exportAvatars: true }) const loadSessions = useCallback(async () => { setIsLoading(true) try { const result = await window.electronAPI.chat.connect() if (!result.success) { console.error('连接失败:', result.error) setIsLoading(false) return } const sessionsResult = await window.electronAPI.chat.getSessions() if (sessionsResult.success && sessionsResult.sessions) { setSessions(sessionsResult.sessions) setFilteredSessions(sessionsResult.sessions) } } catch (e) { console.error('加载会话失败:', e) } finally { setIsLoading(false) } }, []) const loadExportPath = useCallback(async () => { try { const savedPath = await configService.getExportPath() if (savedPath) { setExportFolder(savedPath) } else { const downloadsPath = await window.electronAPI.app.getDownloadsPath() setExportFolder(downloadsPath) } } catch (e) { console.error('加载导出路径失败:', e) } }, []) useEffect(() => { loadSessions() loadExportPath() }, [loadSessions, loadExportPath]) useEffect(() => { if (!searchKeyword.trim()) { setFilteredSessions(sessions) return } const lower = searchKeyword.toLowerCase() setFilteredSessions(sessions.filter(s => s.displayName?.toLowerCase().includes(lower) || s.username.toLowerCase().includes(lower) )) }, [searchKeyword, sessions]) const toggleSession = (username: string) => { const newSet = new Set(selectedSessions) if (newSet.has(username)) { newSet.delete(username) } else { newSet.add(username) } setSelectedSessions(newSet) } const toggleSelectAll = () => { if (selectedSessions.size === filteredSessions.length) { setSelectedSessions(new Set()) } else { setSelectedSessions(new Set(filteredSessions.map(s => s.username))) } } const getAvatarLetter = (name: string) => { if (!name) return '?' return [...name][0] || '?' } const formatDate = (date: Date) => { return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) } const openExportFolder = async () => { if (exportFolder) { await window.electronAPI.shell.openPath(exportFolder) } } const startExport = async () => { if (selectedSessions.size === 0 || !exportFolder) return setIsExporting(true) setExportProgress({ current: 0, total: selectedSessions.size, currentName: '' }) setExportResult(null) try { const sessionList = Array.from(selectedSessions) const exportOptions = { format: options.format, exportAvatars: options.exportAvatars, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), // 将结束日期设置为当天的 23:59:59,以包含当天的所有消息 end: Math.floor(new Date(options.dateRange.end.getFullYear(), options.dateRange.end.getMonth(), options.dateRange.end.getDate(), 23, 59, 59).getTime() / 1000) } : null } if (options.format === 'chatlab' || options.format === 'chatlab-jsonl' || options.format === 'json') { const result = await window.electronAPI.export.exportSessions( sessionList, exportFolder, exportOptions ) setExportResult(result) } else { setExportResult({ success: false, error: `${options.format.toUpperCase()} 格式导出功能开发中...` }) } } catch (e) { console.error('导出失败:', e) setExportResult({ success: false, error: String(e) }) } finally { setIsExporting(false) } } const getDaysInMonth = (date: Date) => { const year = date.getFullYear() const month = date.getMonth() return new Date(year, month + 1, 0).getDate() } const getFirstDayOfMonth = (date: Date) => { const year = date.getFullYear() const month = date.getMonth() return new Date(year, month, 1).getDay() } const generateCalendar = () => { const daysInMonth = getDaysInMonth(calendarDate) const firstDay = getFirstDayOfMonth(calendarDate) const days: (number | null)[] = [] for (let i = 0; i < firstDay; i++) { days.push(null) } for (let i = 1; i <= daysInMonth; i++) { days.push(i) } return days } const handleDateSelect = (day: number) => { const year = calendarDate.getFullYear() const month = calendarDate.getMonth() const selectedDate = new Date(year, month, day) if (selectingStart) { setOptions({ ...options, dateRange: options.dateRange ? { ...options.dateRange, start: selectedDate } : { start: selectedDate, end: new Date() } }) setSelectingStart(false) } else { setOptions({ ...options, dateRange: options.dateRange ? { ...options.dateRange, end: selectedDate } : { start: new Date(), end: selectedDate } }) setSelectingStart(true) } } const formatOptions = [ { value: 'chatlab', label: 'ChatLab', icon: FileCode, desc: '标准格式,支持其他软件导入' }, { value: 'chatlab-jsonl', label: 'ChatLab JSONL', icon: FileCode, desc: '流式格式,适合大量消息' }, { value: 'json', label: 'JSON', icon: FileJson, desc: '详细格式,包含完整消息信息' }, { value: 'html', label: 'HTML', icon: FileText, desc: '网页格式,可直接浏览' }, { value: 'txt', label: 'TXT', icon: Table, desc: '纯文本,通用格式' }, { value: 'excel', label: 'Excel', icon: FileSpreadsheet, desc: '电子表格,适合统计分析' }, { value: 'sql', label: 'PostgreSQL', icon: Database, desc: '数据库脚本,便于导入到数据库' } ] return (

选择会话

setSearchKeyword(e.target.value)} /> {searchKeyword && ( )}
已选 {selectedSessions.size} 个
{isLoading ? (
加载中...
) : filteredSessions.length === 0 ? (
暂无会话
) : (
{filteredSessions.map(session => (
toggleSession(session.username)} >
{selectedSessions.has(session.username) && }
{session.avatarUrl ? ( ) : ( {getAvatarLetter(session.displayName || session.username)} )}
{session.displayName || session.username}
{session.summary || '暂无消息'}
))}
)}

导出设置

导出格式

{formatOptions.map(fmt => (
setOptions({ ...options, format: fmt.value as any })} > {fmt.label} {fmt.desc}
))}

时间范围

{!options.useAllTime && options.dateRange && (
setShowDatePicker(true)}> {formatDate(options.dateRange.start)} - {formatDate(options.dateRange.end)}
)}

导出头像

导出位置

{exportFolder || '未设置'}

可在设置页面修改导出目录

{/* 导出进度弹窗 */} {isExporting && (

正在导出

{exportProgress.currentName}

{exportProgress.current} / {exportProgress.total}

)} {/* 导出结果弹窗 */} {exportResult && (
{exportResult.success ? : }

{exportResult.success ? '导出完成' : '导出失败'}

{exportResult.success ? (

成功导出 {exportResult.successCount} 个会话 {exportResult.failCount ? `,${exportResult.failCount} 个失败` : ''}

) : (

{exportResult.error}

)}
{exportResult.success && ( )}
)} {/* 日期选择弹窗 */} {showDatePicker && (
setShowDatePicker(false)}>
e.stopPropagation()}>

选择时间范围

setSelectingStart(true)} > 开始日期 {options.dateRange?.start.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}
setSelectingStart(false)} > 结束日期 {options.dateRange?.end.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })}
{calendarDate.getFullYear()}年{calendarDate.getMonth() + 1}月
{['日', '一', '二', '三', '四', '五', '六'].map(day => (
{day}
))}
{generateCalendar().map((day, index) => { if (day === null) { return
} const currentDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), day) const isStart = options.dateRange?.start.toDateString() === currentDate.toDateString() const isEnd = options.dateRange?.end.toDateString() === currentDate.toDateString() const isInRange = options.dateRange && currentDate >= options.dateRange.start && currentDate <= options.dateRange.end return (
handleDateSelect(day)} > {day}
) })}
)}
) } export default ExportPage