import { useEffect, useMemo, useRef, useState } from 'react' import { ChevronDown } from 'lucide-react' import * as configService from '../../services/config' import { ExportDateRangeDialog } from './ExportDateRangeDialog' import { createDefaultExportDateRangeSelection, getExportDateRangeLabel, resolveExportDateRangeConfig, serializeExportDateRangeConfig, type ExportDateRangeSelection } from '../../utils/exportDateRange' import './ExportDefaultsSettingsForm.scss' export interface ExportDefaultsSettingsPatch { format?: string avatars?: boolean dateRange?: ExportDateRangeSelection media?: configService.ExportDefaultMediaConfig voiceAsText?: boolean excelCompactColumns?: boolean concurrency?: number } interface ExportDefaultsSettingsFormProps { onNotify?: (text: string, success: boolean) => void onDefaultsChanged?: (patch: ExportDefaultsSettingsPatch) => void layout?: 'stacked' | 'split' } const exportFormatOptions = [ { value: 'excel', label: 'Excel', desc: '电子表格,适合统计分析' }, { value: 'json', label: 'JSON', desc: '详细格式,包含完整消息信息' }, { value: 'html', label: 'HTML', desc: '网页格式,可直接浏览' }, { value: 'txt', label: 'TXT', desc: '纯文本,通用格式' }, { value: 'arkme-json', label: 'Arkme JSON', desc: '紧凑 JSON,支持 sender 去重与关系统计' }, { value: 'chatlab', label: 'ChatLab', desc: '标准格式,支持其他软件导入' }, { value: 'chatlab-jsonl', label: 'ChatLab JSONL', desc: '流式格式,适合大量消息' }, { value: 'weclone', label: 'WeClone CSV', desc: 'WeClone 兼容字段格式(CSV)' }, { value: 'sql', label: 'PostgreSQL', desc: '数据库脚本,便于导入到数据库' } ] as const const exportExcelColumnOptions = [ { value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' }, { value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' } ] as const const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => { return options.find((option) => option.value === value)?.label ?? value } export function ExportDefaultsSettingsForm({ onNotify, onDefaultsChanged, layout = 'stacked' }: ExportDefaultsSettingsFormProps) { const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false) const [isExportDateRangeDialogOpen, setIsExportDateRangeDialogOpen] = useState(false) const exportExcelColumnsDropdownRef = useRef(null) const [exportDefaultFormat, setExportDefaultFormat] = useState('excel') const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true) const [exportDefaultDateRange, setExportDefaultDateRange] = useState(() => createDefaultExportDateRangeSelection()) const [exportDefaultMedia, setExportDefaultMedia] = useState({ images: true, videos: true, voices: true, emojis: true }) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(false) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2) useEffect(() => { let cancelled = false void (async () => { const [savedFormat, savedAvatars, savedDateRange, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedConcurrency] = await Promise.all([ configService.getExportDefaultFormat(), configService.getExportDefaultAvatars(), configService.getExportDefaultDateRange(), configService.getExportDefaultMedia(), configService.getExportDefaultVoiceAsText(), configService.getExportDefaultExcelCompactColumns(), configService.getExportDefaultConcurrency() ]) if (cancelled) return setExportDefaultFormat(savedFormat || 'excel') setExportDefaultAvatars(savedAvatars ?? true) setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange)) setExportDefaultMedia(savedMedia ?? { images: true, videos: true, voices: true, emojis: true }) setExportDefaultVoiceAsText(savedVoiceAsText ?? false) setExportDefaultExcelCompactColumns(savedExcelCompactColumns ?? true) setExportDefaultConcurrency(savedConcurrency ?? 2) })() return () => { cancelled = true } }, []) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as Node if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) { setShowExportExcelColumnsSelect(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [showExportExcelColumnsSelect]) const exportExcelColumnsValue = exportDefaultExcelCompactColumns ? 'compact' : 'full' const exportDateRangeLabel = useMemo(() => getExportDateRangeLabel(exportDefaultDateRange), [exportDefaultDateRange]) const exportExcelColumnsLabel = useMemo(() => getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue), [exportExcelColumnsValue]) const notify = (text: string, success = true) => { onNotify?.(text, success) } return (
导出多个会话时的最大并发(1~6)
{exportConcurrencyOptions.map((option) => ( ))}
导出页面默认选中的格式
{exportFormatOptions.map((option) => ( ))}
开启后导出的聊天消息对应的文件中会带头像信息。
{exportDefaultAvatars ? '已开启' : '已关闭'}
控制导出页面的默认时间选择
setIsExportDateRangeDialogOpen(false)} onConfirm={async (nextSelection) => { setExportDefaultDateRange(nextSelection) await configService.setExportDefaultDateRange(serializeExportDateRangeConfig(nextSelection)) onDefaultsChanged?.({ dateRange: nextSelection }) notify('已更新默认导出时间范围', true) setIsExportDateRangeDialogOpen(false) }} />
控制图片、视频、语音、表情包的默认导出开关
控制 Excel 导出的列字段
{showExportExcelColumnsSelect && (
{exportExcelColumnOptions.map((option) => ( ))}
)}
导出时默认将语音转写为文字
{exportDefaultVoiceAsText ? '已开启' : '已关闭'}
) }