mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Add configurable TXT export
This commit is contained in:
@@ -72,9 +72,21 @@ export interface ExportOptions {
|
|||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
|
txtColumns?: string[]
|
||||||
sessionLayout?: 'shared' | 'per-session'
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
|
||||||
|
{ id: 'index', label: '序号' },
|
||||||
|
{ id: 'time', label: '时间' },
|
||||||
|
{ id: 'senderRole', label: '发送者身份' },
|
||||||
|
{ id: 'messageType', label: '消息类型' },
|
||||||
|
{ id: 'content', label: '内容' },
|
||||||
|
{ id: 'senderNickname', label: '发送者昵称' },
|
||||||
|
{ id: 'senderWxid', label: '发送者微信ID' },
|
||||||
|
{ id: 'senderRemark', label: '发送者备注' }
|
||||||
|
]
|
||||||
|
|
||||||
interface MediaExportItem {
|
interface MediaExportItem {
|
||||||
relativePath: string
|
relativePath: string
|
||||||
kind: 'image' | 'voice' | 'emoji'
|
kind: 'image' | 'voice' | 'emoji'
|
||||||
@@ -428,6 +440,17 @@ class ExportService {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeTxtColumns(columns?: string[] | null): string[] {
|
||||||
|
const fallback = ['index', 'time', 'senderRole', 'messageType', 'content']
|
||||||
|
const selected = new Set((columns && columns.length > 0 ? columns : fallback).filter(Boolean))
|
||||||
|
const ordered = TXT_COLUMN_DEFINITIONS.map((col) => col.id).filter((id) => selected.has(id))
|
||||||
|
return ordered.length > 0 ? ordered : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeTxtValue(value: string): string {
|
||||||
|
return value.replace(/\r?\n/g, ' ').replace(/\t/g, ' ').trim()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出媒体文件到指定目录
|
* 导出媒体文件到指定目录
|
||||||
*/
|
*/
|
||||||
@@ -1880,6 +1903,197 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出单个会话为 TXT 格式(默认与 Excel 精简列一致)
|
||||||
|
*/
|
||||||
|
async exportSessionToTxt(
|
||||||
|
sessionId: string,
|
||||||
|
outputPath: string,
|
||||||
|
options: ExportOptions,
|
||||||
|
onProgress?: (progress: ExportProgress) => void
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const conn = await this.ensureConnected()
|
||||||
|
if (!conn.success || !conn.cleanedWxid) return { success: false, error: conn.error }
|
||||||
|
|
||||||
|
const cleanedMyWxid = conn.cleanedWxid
|
||||||
|
const isGroup = sessionId.includes('@chatroom')
|
||||||
|
const sessionInfo = await this.getContactInfo(sessionId)
|
||||||
|
const myInfo = await this.getContactInfo(cleanedMyWxid)
|
||||||
|
|
||||||
|
onProgress?.({
|
||||||
|
current: 0,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'preparing'
|
||||||
|
})
|
||||||
|
|
||||||
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
|
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
||||||
|
|
||||||
|
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||||
|
const mediaMessages = exportMediaEnabled
|
||||||
|
? sortedMessages.filter(msg => {
|
||||||
|
const t = msg.localType
|
||||||
|
return (t === 3 && options.exportImages) ||
|
||||||
|
(t === 47 && options.exportEmojis) ||
|
||||||
|
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
|
||||||
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
|
|
||||||
|
if (mediaMessages.length > 0) {
|
||||||
|
onProgress?.({
|
||||||
|
current: 25,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'exporting-media'
|
||||||
|
})
|
||||||
|
|
||||||
|
const MEDIA_CONCURRENCY = 8
|
||||||
|
await parallelLimit(mediaMessages, MEDIA_CONCURRENCY, async (msg) => {
|
||||||
|
const mediaKey = `${msg.localType}_${msg.localId}`
|
||||||
|
if (!mediaCache.has(mediaKey)) {
|
||||||
|
const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
|
exportImages: options.exportImages,
|
||||||
|
exportVoices: options.exportVoices,
|
||||||
|
exportEmojis: options.exportEmojis,
|
||||||
|
exportVoiceAsText: options.exportVoiceAsText
|
||||||
|
})
|
||||||
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const voiceMessages = options.exportVoiceAsText
|
||||||
|
? sortedMessages.filter(msg => msg.localType === 34)
|
||||||
|
: []
|
||||||
|
const voiceTranscriptMap = new Map<number, string>()
|
||||||
|
|
||||||
|
if (voiceMessages.length > 0) {
|
||||||
|
onProgress?.({
|
||||||
|
current: 45,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'exporting-voice'
|
||||||
|
})
|
||||||
|
|
||||||
|
const VOICE_CONCURRENCY = 4
|
||||||
|
await parallelLimit(voiceMessages, VOICE_CONCURRENCY, async (msg) => {
|
||||||
|
const transcript = await this.transcribeVoice(sessionId, String(msg.localId))
|
||||||
|
voiceTranscriptMap.set(msg.localId, transcript)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress?.({
|
||||||
|
current: 60,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'exporting'
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnOrder = this.normalizeTxtColumns(options.txtColumns)
|
||||||
|
const columnLabelMap = new Map(TXT_COLUMN_DEFINITIONS.map((col) => [col.id, col.label]))
|
||||||
|
const lines: string[] = []
|
||||||
|
lines.push(columnOrder.map((id) => columnLabelMap.get(id) || id).join('\t'))
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedMessages.length; i++) {
|
||||||
|
const msg = sortedMessages[i]
|
||||||
|
const mediaKey = `${msg.localType}_${msg.localId}`
|
||||||
|
const mediaItem = mediaCache.get(mediaKey) || null
|
||||||
|
|
||||||
|
let contentValue: string
|
||||||
|
if (mediaItem) {
|
||||||
|
contentValue = mediaItem.relativePath
|
||||||
|
} else if (msg.localType === 34 && options.exportVoiceAsText) {
|
||||||
|
contentValue = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
|
||||||
|
} else {
|
||||||
|
contentValue = this.parseMessageContent(msg.content, msg.localType) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let senderRole: string
|
||||||
|
let senderWxid: string
|
||||||
|
let senderNickname: string
|
||||||
|
let senderRemark = ''
|
||||||
|
|
||||||
|
if (msg.isSend) {
|
||||||
|
senderRole = '我'
|
||||||
|
senderWxid = cleanedMyWxid
|
||||||
|
senderNickname = myInfo.displayName || cleanedMyWxid
|
||||||
|
} else if (isGroup && msg.senderUsername) {
|
||||||
|
senderWxid = msg.senderUsername
|
||||||
|
const contactDetail = await wcdbService.getContact(msg.senderUsername)
|
||||||
|
if (contactDetail.success && contactDetail.contact) {
|
||||||
|
senderNickname = contactDetail.contact.nickName || msg.senderUsername
|
||||||
|
senderRemark = contactDetail.contact.remark || ''
|
||||||
|
senderRole = senderRemark || senderNickname
|
||||||
|
} else {
|
||||||
|
senderNickname = msg.senderUsername
|
||||||
|
senderRole = msg.senderUsername
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
senderWxid = sessionId
|
||||||
|
const contactDetail = await wcdbService.getContact(sessionId)
|
||||||
|
if (contactDetail.success && contactDetail.contact) {
|
||||||
|
senderNickname = contactDetail.contact.nickName || sessionId
|
||||||
|
senderRemark = contactDetail.contact.remark || ''
|
||||||
|
senderRole = senderRemark || senderNickname
|
||||||
|
} else {
|
||||||
|
senderNickname = sessionInfo.displayName || sessionId
|
||||||
|
senderRole = senderNickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const values: Record<string, string> = {
|
||||||
|
index: String(i + 1),
|
||||||
|
time: this.formatTimestamp(msg.createTime),
|
||||||
|
senderRole,
|
||||||
|
senderNickname,
|
||||||
|
senderWxid,
|
||||||
|
senderRemark,
|
||||||
|
messageType: this.getMessageTypeName(msg.localType),
|
||||||
|
content: contentValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = columnOrder
|
||||||
|
.map((id) => this.sanitizeTxtValue(values[id] ?? ''))
|
||||||
|
.join('\t')
|
||||||
|
lines.push(line)
|
||||||
|
|
||||||
|
if ((i + 1) % 200 === 0) {
|
||||||
|
const progress = 60 + Math.floor((i + 1) / sortedMessages.length * 30)
|
||||||
|
onProgress?.({
|
||||||
|
current: progress,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'exporting'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress?.({
|
||||||
|
current: 92,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'writing'
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8')
|
||||||
|
|
||||||
|
onProgress?.({
|
||||||
|
current: 100,
|
||||||
|
total: 100,
|
||||||
|
currentSession: sessionInfo.displayName,
|
||||||
|
phase: 'complete'
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量导出多个会话
|
* 批量导出多个会话
|
||||||
*/
|
*/
|
||||||
@@ -1930,6 +2144,7 @@ class ExportService {
|
|||||||
let ext = '.json'
|
let ext = '.json'
|
||||||
if (options.format === 'chatlab-jsonl') ext = '.jsonl'
|
if (options.format === 'chatlab-jsonl') ext = '.jsonl'
|
||||||
else if (options.format === 'excel') ext = '.xlsx'
|
else if (options.format === 'excel') ext = '.xlsx'
|
||||||
|
else if (options.format === 'txt') ext = '.txt'
|
||||||
const outputPath = path.join(sessionDir, `${safeName}${ext}`)
|
const outputPath = path.join(sessionDir, `${safeName}${ext}`)
|
||||||
|
|
||||||
let result: { success: boolean; error?: string }
|
let result: { success: boolean; error?: string }
|
||||||
@@ -1939,6 +2154,8 @@ class ExportService {
|
|||||||
result = await this.exportSessionToChatLab(sessionId, outputPath, options)
|
result = await this.exportSessionToChatLab(sessionId, outputPath, options)
|
||||||
} else if (options.format === 'excel') {
|
} else if (options.format === 'excel') {
|
||||||
result = await this.exportSessionToExcel(sessionId, outputPath, options)
|
result = await this.exportSessionToExcel(sessionId, outputPath, options)
|
||||||
|
} else if (options.format === 'txt') {
|
||||||
|
result = await this.exportSessionToTxt(sessionId, outputPath, options)
|
||||||
} else {
|
} else {
|
||||||
result = { success: false, error: `不支持的格式: ${options.format}` }
|
result = { success: false, error: `不支持的格式: ${options.format}` }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface ExportOptions {
|
|||||||
exportEmojis: boolean
|
exportEmojis: boolean
|
||||||
exportVoiceAsText: boolean
|
exportVoiceAsText: boolean
|
||||||
excelCompactColumns: boolean
|
excelCompactColumns: boolean
|
||||||
|
txtColumns: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExportResult {
|
interface ExportResult {
|
||||||
@@ -34,6 +35,7 @@ interface ExportResult {
|
|||||||
type SessionLayout = 'shared' | 'per-session'
|
type SessionLayout = 'shared' | 'per-session'
|
||||||
|
|
||||||
function ExportPage() {
|
function ExportPage() {
|
||||||
|
const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content']
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([])
|
const [sessions, setSessions] = useState<ChatSession[]>([])
|
||||||
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([])
|
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([])
|
||||||
const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set())
|
const [selectedSessions, setSelectedSessions] = useState<Set<string>>(new Set())
|
||||||
@@ -61,7 +63,8 @@ function ExportPage() {
|
|||||||
exportVoices: true,
|
exportVoices: true,
|
||||||
exportEmojis: true,
|
exportEmojis: true,
|
||||||
exportVoiceAsText: true,
|
exportVoiceAsText: true,
|
||||||
excelCompactColumns: true
|
excelCompactColumns: true,
|
||||||
|
txtColumns: defaultTxtColumns
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildDateRangeFromPreset = (preset: string) => {
|
const buildDateRangeFromPreset = (preset: string) => {
|
||||||
@@ -125,17 +128,20 @@ function ExportPage() {
|
|||||||
savedRange,
|
savedRange,
|
||||||
savedMedia,
|
savedMedia,
|
||||||
savedVoiceAsText,
|
savedVoiceAsText,
|
||||||
savedExcelCompactColumns
|
savedExcelCompactColumns,
|
||||||
|
savedTxtColumns
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
configService.getExportDefaultFormat(),
|
configService.getExportDefaultFormat(),
|
||||||
configService.getExportDefaultDateRange(),
|
configService.getExportDefaultDateRange(),
|
||||||
configService.getExportDefaultMedia(),
|
configService.getExportDefaultMedia(),
|
||||||
configService.getExportDefaultVoiceAsText(),
|
configService.getExportDefaultVoiceAsText(),
|
||||||
configService.getExportDefaultExcelCompactColumns()
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
|
configService.getExportDefaultTxtColumns()
|
||||||
])
|
])
|
||||||
|
|
||||||
const preset = savedRange || 'today'
|
const preset = savedRange || 'today'
|
||||||
const rangeDefaults = buildDateRangeFromPreset(preset)
|
const rangeDefaults = buildDateRangeFromPreset(preset)
|
||||||
|
const txtColumns = savedTxtColumns && savedTxtColumns.length > 0 ? savedTxtColumns : defaultTxtColumns
|
||||||
|
|
||||||
setOptions((prev) => ({
|
setOptions((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -144,7 +150,8 @@ function ExportPage() {
|
|||||||
dateRange: rangeDefaults.dateRange,
|
dateRange: rangeDefaults.dateRange,
|
||||||
exportMedia: savedMedia ?? false,
|
exportMedia: savedMedia ?? false,
|
||||||
exportVoiceAsText: savedVoiceAsText ?? true,
|
exportVoiceAsText: savedVoiceAsText ?? true,
|
||||||
excelCompactColumns: savedExcelCompactColumns ?? true
|
excelCompactColumns: savedExcelCompactColumns ?? true,
|
||||||
|
txtColumns
|
||||||
}))
|
}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载导出默认设置失败:', e)
|
console.error('加载导出默认设置失败:', e)
|
||||||
@@ -233,6 +240,7 @@ function ExportPage() {
|
|||||||
exportEmojis: options.exportMedia && options.exportEmojis,
|
exportEmojis: options.exportMedia && options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
||||||
excelCompactColumns: options.excelCompactColumns,
|
excelCompactColumns: options.excelCompactColumns,
|
||||||
|
txtColumns: options.txtColumns,
|
||||||
sessionLayout,
|
sessionLayout,
|
||||||
dateRange: options.useAllTime ? null : options.dateRange ? {
|
dateRange: options.useAllTime ? null : options.dateRange ? {
|
||||||
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
start: Math.floor(options.dateRange.start.getTime() / 1000),
|
||||||
@@ -241,7 +249,7 @@ function ExportPage() {
|
|||||||
} : null
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.format === 'chatlab' || options.format === 'chatlab-jsonl' || options.format === 'json' || options.format === 'excel') {
|
if (options.format === 'chatlab' || options.format === 'chatlab-jsonl' || options.format === 'json' || options.format === 'excel' || options.format === 'txt') {
|
||||||
const result = await window.electronAPI.export.exportSessions(
|
const result = await window.electronAPI.export.exportSessions(
|
||||||
sessionList,
|
sessionList,
|
||||||
exportFolder,
|
exportFolder,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ function SettingsPage() {
|
|||||||
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
|
const [exportDefaultMedia, setExportDefaultMedia] = useState(false)
|
||||||
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true)
|
const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true)
|
||||||
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true)
|
||||||
|
const [exportDefaultTxtColumns, setExportDefaultTxtColumns] = useState<string[]>(['index', 'time', 'senderRole', 'messageType', 'content'])
|
||||||
|
|
||||||
const [isLoading, setIsLoadingState] = useState(false)
|
const [isLoading, setIsLoadingState] = useState(false)
|
||||||
const [isTesting, setIsTesting] = useState(false)
|
const [isTesting, setIsTesting] = useState(false)
|
||||||
@@ -141,6 +142,8 @@ function SettingsPage() {
|
|||||||
const savedExportDefaultMedia = await configService.getExportDefaultMedia()
|
const savedExportDefaultMedia = await configService.getExportDefaultMedia()
|
||||||
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
|
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
|
||||||
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
|
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
|
||||||
|
const savedExportDefaultTxtColumns = await configService.getExportDefaultTxtColumns()
|
||||||
|
const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content']
|
||||||
|
|
||||||
if (savedKey) setDecryptKey(savedKey)
|
if (savedKey) setDecryptKey(savedKey)
|
||||||
if (savedPath) setDbPath(savedPath)
|
if (savedPath) setDbPath(savedPath)
|
||||||
@@ -158,6 +161,11 @@ function SettingsPage() {
|
|||||||
setExportDefaultMedia(savedExportDefaultMedia ?? false)
|
setExportDefaultMedia(savedExportDefaultMedia ?? false)
|
||||||
setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true)
|
setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true)
|
||||||
setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true)
|
setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true)
|
||||||
|
setExportDefaultTxtColumns(
|
||||||
|
savedExportDefaultTxtColumns && savedExportDefaultTxtColumns.length > 0
|
||||||
|
? savedExportDefaultTxtColumns
|
||||||
|
: defaultTxtColumns
|
||||||
|
)
|
||||||
|
|
||||||
// 如果语言列表为空,保存默认值
|
// 如果语言列表为空,保存默认值
|
||||||
if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) {
|
if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) {
|
||||||
@@ -166,6 +174,10 @@ function SettingsPage() {
|
|||||||
await configService.setTranscribeLanguages(defaultLanguages)
|
await configService.setTranscribeLanguages(defaultLanguages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!savedExportDefaultTxtColumns || savedExportDefaultTxtColumns.length === 0) {
|
||||||
|
await configService.setExportDefaultTxtColumns(defaultTxtColumns)
|
||||||
|
}
|
||||||
|
|
||||||
if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir)
|
if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载配置失败:', e)
|
console.error('加载配置失败:', e)
|
||||||
@@ -899,6 +911,16 @@ function SettingsPage() {
|
|||||||
{ value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' },
|
{ value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' },
|
||||||
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
|
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
|
||||||
]
|
]
|
||||||
|
const exportTxtColumnOptions = [
|
||||||
|
{ value: 'index', label: '序号' },
|
||||||
|
{ value: 'time', label: '时间' },
|
||||||
|
{ value: 'senderRole', label: '发送者身份' },
|
||||||
|
{ value: 'messageType', label: '消息类型' },
|
||||||
|
{ value: 'content', label: '内容' },
|
||||||
|
{ value: 'senderNickname', label: '发送者昵称' },
|
||||||
|
{ value: 'senderWxid', label: '发送者微信ID' },
|
||||||
|
{ value: 'senderRemark', label: '发送者备注' }
|
||||||
|
]
|
||||||
|
|
||||||
const getOptionLabel = (options: { value: string; label: string }[], value: string) => {
|
const getOptionLabel = (options: { value: string; label: string }[], value: string) => {
|
||||||
return options.find((option) => option.value === value)?.label ?? value
|
return options.find((option) => option.value === value)?.label ?? value
|
||||||
@@ -1074,6 +1096,41 @@ function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>TXT 导出栏目</label>
|
||||||
|
<span className="form-hint">默认与 Excel 精简列一致,可多选调整输出字段</span>
|
||||||
|
<div className="language-checkboxes">
|
||||||
|
{exportTxtColumnOptions.map((column) => {
|
||||||
|
const checked = exportDefaultTxtColumns.includes(column.value)
|
||||||
|
return (
|
||||||
|
<label key={column.value} className="language-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const enabled = e.target.checked
|
||||||
|
const nextColumns = enabled
|
||||||
|
? [...exportDefaultTxtColumns, column.value]
|
||||||
|
: exportDefaultTxtColumns.filter((value) => value !== column.value)
|
||||||
|
if (nextColumns.length === 0) {
|
||||||
|
showMessage('至少选择一个 TXT 导出栏目', false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setExportDefaultTxtColumns(nextColumns)
|
||||||
|
await configService.setExportDefaultTxtColumns(nextColumns)
|
||||||
|
showMessage('已更新 TXT 导出栏目', true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="checkbox-custom">
|
||||||
|
<Check size={14} />
|
||||||
|
<span>{column.label}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1225,4 +1282,3 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsPage
|
export default SettingsPage
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange',
|
EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange',
|
||||||
EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia',
|
EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia',
|
||||||
EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText',
|
EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText',
|
||||||
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns'
|
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
||||||
|
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 获取解密密钥
|
// 获取解密密钥
|
||||||
@@ -306,3 +307,14 @@ export async function getExportDefaultExcelCompactColumns(): Promise<boolean | n
|
|||||||
export async function setExportDefaultExcelCompactColumns(enabled: boolean): Promise<void> {
|
export async function setExportDefaultExcelCompactColumns(enabled: boolean): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled)
|
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取导出默认 TXT 列配置
|
||||||
|
export async function getExportDefaultTxtColumns(): Promise<string[] | null> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS)
|
||||||
|
return Array.isArray(value) ? (value as string[]) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置导出默认 TXT 列配置
|
||||||
|
export async function setExportDefaultTxtColumns(columns: string[]): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns)
|
||||||
|
}
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -351,6 +351,7 @@ export interface ExportOptions {
|
|||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
|
txtColumns?: string[]
|
||||||
sessionLayout?: 'shared' | 'per-session'
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user