mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
@@ -77,6 +77,7 @@ export interface ExportOptions {
|
|||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
txtColumns?: string[]
|
txtColumns?: string[]
|
||||||
sessionLayout?: 'shared' | 'per-session'
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
|
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
||||||
}
|
}
|
||||||
|
|
||||||
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
|
const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [
|
||||||
@@ -170,23 +171,162 @@ class ExportService {
|
|||||||
return this.contactCache.get(username)!
|
return this.contactCache.get(username)!
|
||||||
}
|
}
|
||||||
|
|
||||||
const [displayNames, avatarUrls] = await Promise.all([
|
const [nameResult, avatarResult] = await Promise.all([
|
||||||
wcdbService.getDisplayNames([username]),
|
wcdbService.getDisplayNames([username]),
|
||||||
wcdbService.getAvatarUrls([username])
|
wcdbService.getAvatarUrls([username])
|
||||||
])
|
])
|
||||||
|
|
||||||
const displayName = displayNames.success && displayNames.map
|
const displayName = (nameResult.success && nameResult.map ? nameResult.map[username] : null) || username
|
||||||
? (displayNames.map[username] || username)
|
const avatarUrl = avatarResult.success && avatarResult.map ? avatarResult.map[username] : undefined
|
||||||
: username
|
|
||||||
const avatarUrl = avatarUrls.success && avatarUrls.map
|
|
||||||
? avatarUrls.map[username]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const info = { displayName, avatarUrl }
|
const info = { displayName, avatarUrl }
|
||||||
this.contactCache.set(username, info)
|
this.contactCache.set(username, info)
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 ext_buffer 二进制数据,提取群成员的群昵称
|
||||||
|
* ext_buffer 包含类似 protobuf 编码的数据,格式示例:
|
||||||
|
* wxid_xxx<binary>群昵称<binary>wxid_yyy<binary>群昵称...
|
||||||
|
*/
|
||||||
|
private parseGroupNicknamesFromExtBuffer(buffer: Buffer): Map<string, string> {
|
||||||
|
const nicknameMap = new Map<string, string>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将 buffer 转为字符串,允许部分乱码
|
||||||
|
const raw = buffer.toString('utf8')
|
||||||
|
|
||||||
|
// 提取所有 wxid 格式的字符串: wxid_ 或 wxid_后跟字母数字下划线
|
||||||
|
const wxidPattern = /wxid_[a-z0-9_]+/gi
|
||||||
|
const wxids = raw.match(wxidPattern) || []
|
||||||
|
|
||||||
|
// 对每个 wxid,尝试提取其后的群昵称
|
||||||
|
for (const wxid of wxids) {
|
||||||
|
const wxidLower = wxid.toLowerCase()
|
||||||
|
const wxidIndex = raw.toLowerCase().indexOf(wxidLower)
|
||||||
|
|
||||||
|
if (wxidIndex === -1) continue
|
||||||
|
|
||||||
|
// 从 wxid 结束位置开始查找
|
||||||
|
const afterWxid = raw.slice(wxidIndex + wxid.length)
|
||||||
|
|
||||||
|
// 提取紧跟在 wxid 后面的可打印字符(中文、字母、数字等)
|
||||||
|
// 跳过前面的不可打印字符和特定控制字符
|
||||||
|
let nickname = ''
|
||||||
|
let foundStart = false
|
||||||
|
|
||||||
|
for (let i = 0; i < afterWxid.length && i < 100; i++) {
|
||||||
|
const char = afterWxid[i]
|
||||||
|
const code = char.charCodeAt(0)
|
||||||
|
|
||||||
|
// 判断是否为可打印字符(中文、字母、数字、常见符号)
|
||||||
|
const isPrintable = (
|
||||||
|
(code >= 0x4E00 && code <= 0x9FFF) || // 中文
|
||||||
|
(code >= 0x3000 && code <= 0x303F) || // CJK 符号
|
||||||
|
(code >= 0xFF00 && code <= 0xFFEF) || // 全角字符
|
||||||
|
(code >= 0x20 && code <= 0x7E) // ASCII 可打印字符
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isPrintable && code !== 0x01 && code !== 0x18) {
|
||||||
|
foundStart = true
|
||||||
|
nickname += char
|
||||||
|
} else if (foundStart) {
|
||||||
|
// 遇到不可打印字符,停止
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理昵称:去除前后空白和特殊字符
|
||||||
|
nickname = nickname.trim().replace(/[\x00-\x1F\x7F]/g, '')
|
||||||
|
|
||||||
|
// 只保存有效的群昵称(长度 > 0 且 < 50)
|
||||||
|
if (nickname && nickname.length > 0 && nickname.length < 50) {
|
||||||
|
nicknameMap.set(wxidLower, nickname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败时返回空 Map
|
||||||
|
console.error('Failed to parse ext_buffer:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nicknameMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 contact.db 的 chat_room 表获取群成员的群昵称
|
||||||
|
* @param chatroomId 群聊ID (如 "xxxxx@chatroom")
|
||||||
|
* @returns Map<wxid, 群昵称>
|
||||||
|
*/
|
||||||
|
async getGroupNicknamesForRoom(chatroomId: string): Promise<Map<string, string>> {
|
||||||
|
console.log('========== getGroupNicknamesForRoom START ==========', chatroomId)
|
||||||
|
try {
|
||||||
|
// 查询 contact.db 的 chat_room 表
|
||||||
|
// path设为null,因为contact.db已经随handle一起打开了
|
||||||
|
const sql = `SELECT ext_buffer FROM chat_room WHERE username = '${chatroomId.replace(/'/g, "''")}'`
|
||||||
|
console.log('执行SQL查询:', sql)
|
||||||
|
|
||||||
|
const result = await wcdbService.execQuery('contact', null, sql)
|
||||||
|
console.log('execQuery结果:', { success: result.success, rowCount: result.rows?.length, error: result.error })
|
||||||
|
|
||||||
|
if (!result.success || !result.rows || result.rows.length === 0) {
|
||||||
|
console.log('❌ 群昵称查询失败或无数据:', chatroomId, result.error)
|
||||||
|
return new Map<string, string>()
|
||||||
|
}
|
||||||
|
|
||||||
|
let extBuffer = result.rows[0].ext_buffer
|
||||||
|
console.log('ext_buffer原始类型:', typeof extBuffer, 'isBuffer:', Buffer.isBuffer(extBuffer))
|
||||||
|
|
||||||
|
// execQuery返回的二进制数据会被编码为字符串(hex或base64)
|
||||||
|
// 需要转换回Buffer
|
||||||
|
if (typeof extBuffer === 'string') {
|
||||||
|
console.log('🔄 ext_buffer是字符串,尝试转换为Buffer...')
|
||||||
|
|
||||||
|
// 尝试判断是hex还是base64
|
||||||
|
if (this.looksLikeHex(extBuffer)) {
|
||||||
|
console.log('✅ 检测到hex编码,使用hex解码')
|
||||||
|
extBuffer = Buffer.from(extBuffer, 'hex')
|
||||||
|
} else if (this.looksLikeBase64(extBuffer)) {
|
||||||
|
console.log('✅ 检测到base64编码,使用base64解码')
|
||||||
|
extBuffer = Buffer.from(extBuffer, 'base64')
|
||||||
|
} else {
|
||||||
|
// 默认尝试hex
|
||||||
|
console.log('⚠️ 无法判断编码格式,默认尝试hex')
|
||||||
|
try {
|
||||||
|
extBuffer = Buffer.from(extBuffer, 'hex')
|
||||||
|
} catch (e) {
|
||||||
|
console.log('❌ hex解码失败,尝试base64')
|
||||||
|
extBuffer = Buffer.from(extBuffer, 'base64')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✅ 转换后的Buffer长度:', extBuffer.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extBuffer || !Buffer.isBuffer(extBuffer)) {
|
||||||
|
console.log('❌ ext_buffer转换失败,不是Buffer类型:', typeof extBuffer)
|
||||||
|
return new Map<string, string>()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 开始解析ext_buffer, 长度:', extBuffer.length)
|
||||||
|
const nicknamesMap = this.parseGroupNicknamesFromExtBuffer(extBuffer)
|
||||||
|
console.log('✅ 解析完成, 找到', nicknamesMap.size, '个群昵称')
|
||||||
|
|
||||||
|
// 打印前5个群昵称作为示例
|
||||||
|
let count = 0
|
||||||
|
for (const [wxid, nickname] of nicknamesMap.entries()) {
|
||||||
|
if (count++ < 5) {
|
||||||
|
console.log(` - ${wxid}: "${nickname}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nicknamesMap
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ getGroupNicknamesForRoom异常:', e)
|
||||||
|
return new Map<string, string>()
|
||||||
|
} finally {
|
||||||
|
console.log('========== getGroupNicknamesForRoom END ==========')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换微信消息类型到 ChatLab 类型
|
* 转换微信消息类型到 ChatLab 类型
|
||||||
*/
|
*/
|
||||||
@@ -269,6 +409,28 @@ class ExportService {
|
|||||||
return /^[0-9a-fA-F]+$/.test(s)
|
return /^[0-9a-fA-F]+$/.test(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户偏好获取显示名称
|
||||||
|
*/
|
||||||
|
private getPreferredDisplayName(
|
||||||
|
wxid: string,
|
||||||
|
nickname: string,
|
||||||
|
remark: string,
|
||||||
|
groupNickname: string,
|
||||||
|
preference: 'group-nickname' | 'remark' | 'nickname' = 'remark'
|
||||||
|
): string {
|
||||||
|
switch (preference) {
|
||||||
|
case 'group-nickname':
|
||||||
|
return groupNickname || remark || nickname || wxid
|
||||||
|
case 'remark':
|
||||||
|
return remark || nickname || wxid
|
||||||
|
case 'nickname':
|
||||||
|
return nickname || wxid
|
||||||
|
default:
|
||||||
|
return nickname || wxid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private looksLikeBase64(s: string): boolean {
|
private looksLikeBase64(s: string): boolean {
|
||||||
if (s.length % 4 !== 0) return false
|
if (s.length % 4 !== 0) return false
|
||||||
return /^[A-Za-z0-9+/=]+$/.test(s)
|
return /^[A-Za-z0-9+/=]+$/.test(s)
|
||||||
@@ -707,7 +869,7 @@ class ExportService {
|
|||||||
if (localType === 3 && options.exportImages) {
|
if (localType === 3 && options.exportImages) {
|
||||||
const result = await this.exportImage(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
const result = await this.exportImage(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
if (result) {
|
if (result) {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +888,7 @@ class ExportService {
|
|||||||
if (localType === 47 && options.exportEmojis) {
|
if (localType === 47 && options.exportEmojis) {
|
||||||
const result = await this.exportEmoji(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
const result = await this.exportEmoji(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
if (result) {
|
if (result) {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,13 +1091,13 @@ class ExportService {
|
|||||||
// 下载表情
|
// 下载表情
|
||||||
if (emojiUrl) {
|
if (emojiUrl) {
|
||||||
const downloaded = await this.downloadFile(emojiUrl, destPath)
|
const downloaded = await this.downloadFile(emojiUrl, destPath)
|
||||||
if (downloaded) {
|
if (downloaded) {
|
||||||
return {
|
return {
|
||||||
relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName),
|
relativePath: path.posix.join(mediaRelativePrefix, 'emojis', fileName),
|
||||||
kind: 'emoji'
|
kind: 'emoji'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -1016,7 +1178,7 @@ class ExportService {
|
|||||||
*/
|
*/
|
||||||
private extractEmojiUrl(content: string): string | undefined {
|
private extractEmojiUrl(content: string): string | undefined {
|
||||||
if (!content) return undefined
|
if (!content) return undefined
|
||||||
// 参考 echotrace 的正则:cdnurl\s*=\s*['"]([^'"]+)['"]
|
// 参考 echotrace 的正则:cdnurl\s*=\s*['"]([^'"]+)['"]
|
||||||
const attrMatch = /cdnurl\s*=\s*['"]([^'"]+)['"]/i.exec(content)
|
const attrMatch = /cdnurl\s*=\s*['"]([^'"]+)['"]/i.exec(content)
|
||||||
if (attrMatch) {
|
if (attrMatch) {
|
||||||
// 解码 & 等实体
|
// 解码 & 等实体
|
||||||
@@ -1181,14 +1343,14 @@ class ExportService {
|
|||||||
// 图片消息
|
// 图片消息
|
||||||
imageMd5 = this.extractImageMd5(content)
|
imageMd5 = this.extractImageMd5(content)
|
||||||
imageDatName = this.extractImageDatName(content)
|
imageDatName = this.extractImageDatName(content)
|
||||||
} else if (localType === 47 && content) {
|
} else if (localType === 47 && content) {
|
||||||
// 动画表情
|
// 动画表情
|
||||||
emojiCdnUrl = this.extractEmojiUrl(content)
|
emojiCdnUrl = this.extractEmojiUrl(content)
|
||||||
emojiMd5 = this.extractEmojiMd5(content)
|
emojiMd5 = this.extractEmojiMd5(content)
|
||||||
} else if (localType === 43 && content) {
|
} else if (localType === 43 && content) {
|
||||||
// 视频消息
|
// 视频消息
|
||||||
videoMd5 = this.extractVideoMd5(content)
|
videoMd5 = this.extractVideoMd5(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
localId,
|
localId,
|
||||||
@@ -1506,11 +1668,11 @@ class ExportService {
|
|||||||
// ========== 阶段1:并行导出媒体文件 ==========
|
// ========== 阶段1:并行导出媒体文件 ==========
|
||||||
const mediaMessages = exportMediaEnabled
|
const mediaMessages = exportMediaEnabled
|
||||||
? allMessages.filter(msg => {
|
? allMessages.filter(msg => {
|
||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) || // 图片
|
return (t === 3 && options.exportImages) || // 图片
|
||||||
(t === 47 && options.exportEmojis) || // 表情
|
(t === 47 && options.exportEmojis) || // 表情
|
||||||
(t === 34 && options.exportVoices && !options.exportVoiceAsText) // 语音文件(非转文字)
|
(t === 34 && options.exportVoices && !options.exportVoiceAsText) // 语音文件(非转文字)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -1693,11 +1855,11 @@ class ExportService {
|
|||||||
// ========== 阶段1:并行导出媒体文件 ==========
|
// ========== 阶段1:并行导出媒体文件 ==========
|
||||||
const mediaMessages = exportMediaEnabled
|
const mediaMessages = exportMediaEnabled
|
||||||
? collected.rows.filter(msg => {
|
? collected.rows.filter(msg => {
|
||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(t === 47 && options.exportEmojis) ||
|
||||||
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -1747,6 +1909,11 @@ class ExportService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 预加载群昵称(用于名称显示偏好) ==========
|
||||||
|
const groupNicknamesMap = isGroup
|
||||||
|
? await this.getGroupNicknamesForRoom(sessionId)
|
||||||
|
: new Map<string, string>()
|
||||||
|
|
||||||
// ========== 阶段3:构建消息列表 ==========
|
// ========== 阶段3:构建消息列表 ==========
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
current: 55,
|
current: 55,
|
||||||
@@ -1773,6 +1940,24 @@ class ExportService {
|
|||||||
content = this.parseMessageContent(msg.content, msg.localType)
|
content = this.parseMessageContent(msg.content, msg.localType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取发送者信息用于名称显示
|
||||||
|
const senderWxid = msg.senderUsername
|
||||||
|
const contact = await wcdbService.getContact(senderWxid)
|
||||||
|
const senderNickname = contact.success && contact.contact?.nickName
|
||||||
|
? contact.contact.nickName
|
||||||
|
: (senderInfo.displayName || senderWxid)
|
||||||
|
const senderRemark = contact.success && contact.contact?.remark ? contact.contact.remark : ''
|
||||||
|
const senderGroupNickname = groupNicknamesMap.get(senderWxid?.toLowerCase() || '') || ''
|
||||||
|
|
||||||
|
// 使用用户偏好的显示名称
|
||||||
|
const senderDisplayName = this.getPreferredDisplayName(
|
||||||
|
senderWxid,
|
||||||
|
senderNickname,
|
||||||
|
senderRemark,
|
||||||
|
senderGroupNickname,
|
||||||
|
options.displayNamePreference || 'remark'
|
||||||
|
)
|
||||||
|
|
||||||
allMessages.push({
|
allMessages.push({
|
||||||
localId: allMessages.length + 1,
|
localId: allMessages.length + 1,
|
||||||
createTime: msg.createTime,
|
createTime: msg.createTime,
|
||||||
@@ -1782,7 +1967,7 @@ class ExportService {
|
|||||||
content,
|
content,
|
||||||
isSend: msg.isSend ? 1 : 0,
|
isSend: msg.isSend ? 1 : 0,
|
||||||
senderUsername: msg.senderUsername,
|
senderUsername: msg.senderUsername,
|
||||||
senderDisplayName: senderInfo.displayName,
|
senderDisplayName,
|
||||||
source,
|
source,
|
||||||
senderAvatarKey: msg.senderUsername
|
senderAvatarKey: msg.senderUsername
|
||||||
})
|
})
|
||||||
@@ -1799,14 +1984,33 @@ class ExportService {
|
|||||||
|
|
||||||
const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup)
|
const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup)
|
||||||
|
|
||||||
|
// 获取会话的昵称和备注信息
|
||||||
|
const sessionContact = await wcdbService.getContact(sessionId)
|
||||||
|
const sessionNickname = sessionContact.success && sessionContact.contact?.nickName
|
||||||
|
? sessionContact.contact.nickName
|
||||||
|
: sessionInfo.displayName
|
||||||
|
const sessionRemark = sessionContact.success && sessionContact.contact?.remark
|
||||||
|
? sessionContact.contact.remark
|
||||||
|
: ''
|
||||||
|
const sessionGroupNickname = isGroup
|
||||||
|
? (groupNicknamesMap.get(sessionId.toLowerCase()) || '')
|
||||||
|
: ''
|
||||||
|
|
||||||
|
// 使用用户偏好的显示名称
|
||||||
|
const sessionDisplayName = this.getPreferredDisplayName(
|
||||||
|
sessionId,
|
||||||
|
sessionNickname,
|
||||||
|
sessionRemark,
|
||||||
|
sessionGroupNickname,
|
||||||
|
options.displayNamePreference || 'remark'
|
||||||
|
)
|
||||||
|
|
||||||
const detailedExport: any = {
|
const detailedExport: any = {
|
||||||
chatlab,
|
|
||||||
meta,
|
|
||||||
session: {
|
session: {
|
||||||
wxid: sessionId,
|
wxid: sessionId,
|
||||||
nickname: sessionInfo.displayName,
|
nickname: sessionNickname,
|
||||||
remark: sessionInfo.displayName,
|
remark: sessionRemark,
|
||||||
displayName: sessionInfo.displayName,
|
displayName: sessionDisplayName,
|
||||||
type: isGroup ? '群聊' : '私聊',
|
type: isGroup ? '群聊' : '私聊',
|
||||||
lastTimestamp: collected.lastTime,
|
lastTimestamp: collected.lastTime,
|
||||||
messageCount: allMessages.length,
|
messageCount: allMessages.length,
|
||||||
@@ -1886,6 +2090,7 @@ class ExportService {
|
|||||||
|
|
||||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
|
|
||||||
|
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
current: 30,
|
current: 30,
|
||||||
total: 100,
|
total: 100,
|
||||||
@@ -1962,7 +2167,7 @@ class ExportService {
|
|||||||
// 表头行
|
// 表头行
|
||||||
const headers = useCompactColumns
|
const headers = useCompactColumns
|
||||||
? ['序号', '时间', '发送者身份', '消息类型', '内容']
|
? ['序号', '时间', '发送者身份', '消息类型', '内容']
|
||||||
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容']
|
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '群昵称', '发送者身份', '消息类型', '内容']
|
||||||
const headerRow = worksheet.getRow(currentRow)
|
const headerRow = worksheet.getRow(currentRow)
|
||||||
headerRow.height = 22
|
headerRow.height = 22
|
||||||
|
|
||||||
@@ -1990,11 +2195,20 @@ class ExportService {
|
|||||||
worksheet.getColumn(3).width = 18 // 发送者昵称
|
worksheet.getColumn(3).width = 18 // 发送者昵称
|
||||||
worksheet.getColumn(4).width = 25 // 发送者微信ID
|
worksheet.getColumn(4).width = 25 // 发送者微信ID
|
||||||
worksheet.getColumn(5).width = 18 // 发送者备注
|
worksheet.getColumn(5).width = 18 // 发送者备注
|
||||||
worksheet.getColumn(6).width = 15 // 发送者身份
|
worksheet.getColumn(6).width = 18 // 群昵称
|
||||||
worksheet.getColumn(7).width = 12 // 消息类型
|
worksheet.getColumn(7).width = 15 // 发送者身份
|
||||||
worksheet.getColumn(8).width = 50 // 内容
|
worksheet.getColumn(8).width = 12 // 消息类型
|
||||||
|
worksheet.getColumn(9).width = 50 // 内容
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预加载群昵称 (仅群聊且完整列模式)
|
||||||
|
console.log('🔍 预加载群昵称检查: isGroup=', isGroup, 'useCompactColumns=', useCompactColumns, 'sessionId=', sessionId)
|
||||||
|
const groupNicknamesMap = (isGroup && !useCompactColumns)
|
||||||
|
? await this.getGroupNicknamesForRoom(sessionId)
|
||||||
|
: new Map<string, string>()
|
||||||
|
console.log('🔍 群昵称Map大小:', groupNicknamesMap.size)
|
||||||
|
|
||||||
|
|
||||||
// 填充数据
|
// 填充数据
|
||||||
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
||||||
|
|
||||||
@@ -2004,11 +2218,11 @@ class ExportService {
|
|||||||
// ========== 并行预处理:媒体文件 ==========
|
// ========== 并行预处理:媒体文件 ==========
|
||||||
const mediaMessages = exportMediaEnabled
|
const mediaMessages = exportMediaEnabled
|
||||||
? sortedMessages.filter(msg => {
|
? sortedMessages.filter(msg => {
|
||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(t === 47 && options.exportEmojis) ||
|
||||||
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -2074,6 +2288,8 @@ class ExportService {
|
|||||||
let senderWxid: string
|
let senderWxid: string
|
||||||
let senderNickname: string
|
let senderNickname: string
|
||||||
let senderRemark: string = ''
|
let senderRemark: string = ''
|
||||||
|
let senderGroupNickname: string = '' // 群昵称
|
||||||
|
|
||||||
|
|
||||||
if (msg.isSend) {
|
if (msg.isSend) {
|
||||||
// 我发送的消息
|
// 我发送的消息
|
||||||
@@ -2113,6 +2329,12 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取群昵称 (仅群聊且完整列模式)
|
||||||
|
if (isGroup && !useCompactColumns && senderWxid) {
|
||||||
|
senderGroupNickname = groupNicknamesMap.get(senderWxid.toLowerCase()) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const row = worksheet.getRow(currentRow)
|
const row = worksheet.getRow(currentRow)
|
||||||
row.height = 24
|
row.height = 24
|
||||||
|
|
||||||
@@ -2125,7 +2347,7 @@ class ExportService {
|
|||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
if (msg.localType === 3 || msg.localType === 47) {
|
if (msg.localType === 3 || msg.localType === 47) {
|
||||||
}
|
}
|
||||||
|
|
||||||
worksheet.getCell(currentRow, 1).value = i + 1
|
worksheet.getCell(currentRow, 1).value = i + 1
|
||||||
worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime)
|
worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime)
|
||||||
@@ -2137,13 +2359,14 @@ class ExportService {
|
|||||||
worksheet.getCell(currentRow, 3).value = senderNickname
|
worksheet.getCell(currentRow, 3).value = senderNickname
|
||||||
worksheet.getCell(currentRow, 4).value = senderWxid
|
worksheet.getCell(currentRow, 4).value = senderWxid
|
||||||
worksheet.getCell(currentRow, 5).value = senderRemark
|
worksheet.getCell(currentRow, 5).value = senderRemark
|
||||||
worksheet.getCell(currentRow, 6).value = senderRole
|
worksheet.getCell(currentRow, 6).value = senderGroupNickname
|
||||||
worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType)
|
worksheet.getCell(currentRow, 7).value = senderRole
|
||||||
worksheet.getCell(currentRow, 8).value = contentValue
|
worksheet.getCell(currentRow, 8).value = this.getMessageTypeName(msg.localType)
|
||||||
|
worksheet.getCell(currentRow, 9).value = contentValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置每个单元格的样式
|
// 设置每个单元格的样式
|
||||||
const maxColumns = useCompactColumns ? 5 : 8
|
const maxColumns = useCompactColumns ? 5 : 9
|
||||||
for (let col = 1; col <= maxColumns; col++) {
|
for (let col = 1; col <= maxColumns; col++) {
|
||||||
const cell = worksheet.getCell(currentRow, col)
|
const cell = worksheet.getCell(currentRow, col)
|
||||||
cell.font = { name: 'Calibri', size: 11 }
|
cell.font = { name: 'Calibri', size: 11 }
|
||||||
@@ -2225,11 +2448,11 @@ class ExportService {
|
|||||||
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||||
const mediaMessages = exportMediaEnabled
|
const mediaMessages = exportMediaEnabled
|
||||||
? sortedMessages.filter(msg => {
|
? sortedMessages.filter(msg => {
|
||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(t === 47 && options.exportEmojis) ||
|
||||||
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
(t === 34 && options.exportVoices && !options.exportVoiceAsText)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -2399,12 +2622,12 @@ class ExportService {
|
|||||||
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||||
const mediaMessages = exportMediaEnabled
|
const mediaMessages = exportMediaEnabled
|
||||||
? sortedMessages.filter(msg => {
|
? sortedMessages.filter(msg => {
|
||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(t === 47 && options.exportEmojis) ||
|
||||||
(t === 34 && options.exportVoices) ||
|
(t === 34 && options.exportVoices) ||
|
||||||
t === 43
|
t === 43
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const mediaCache = new Map<string, MediaExportItem | null>()
|
const mediaCache = new Map<string, MediaExportItem | null>()
|
||||||
@@ -2733,15 +2956,15 @@ class ExportService {
|
|||||||
fs.mkdirSync(outputDir, { recursive: true })
|
fs.mkdirSync(outputDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportMediaEnabled = options.exportMedia === true &&
|
const exportMediaEnabled = options.exportMedia === true &&
|
||||||
Boolean(options.exportImages || options.exportVoices || options.exportEmojis)
|
Boolean(options.exportImages || options.exportVoices || options.exportEmojis)
|
||||||
const sessionLayout = exportMediaEnabled
|
const sessionLayout = exportMediaEnabled
|
||||||
? (options.sessionLayout ?? 'per-session')
|
? (options.sessionLayout ?? 'per-session')
|
||||||
: 'shared'
|
: 'shared'
|
||||||
|
|
||||||
for (let i = 0; i < sessionIds.length; i++) {
|
for (let i = 0; i < sessionIds.length; i++) {
|
||||||
const sessionId = sessionIds[i]
|
const sessionId = sessionIds[i]
|
||||||
const sessionInfo = await this.getContactInfo(sessionId)
|
const sessionInfo = await this.getContactInfo(sessionId)
|
||||||
|
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
current: i + 1,
|
current: i + 1,
|
||||||
@@ -2750,13 +2973,13 @@ class ExportService {
|
|||||||
phase: 'exporting'
|
phase: 'exporting'
|
||||||
})
|
})
|
||||||
|
|
||||||
const safeName = sessionInfo.displayName.replace(/[<>:"/\\|?*]/g, '_')
|
const safeName = sessionInfo.displayName.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
const useSessionFolder = sessionLayout === 'per-session'
|
const useSessionFolder = sessionLayout === 'per-session'
|
||||||
const sessionDir = useSessionFolder ? path.join(outputDir, safeName) : outputDir
|
const sessionDir = useSessionFolder ? path.join(outputDir, safeName) : outputDir
|
||||||
|
|
||||||
if (useSessionFolder && !fs.existsSync(sessionDir)) {
|
if (useSessionFolder && !fs.existsSync(sessionDir)) {
|
||||||
fs.mkdirSync(sessionDir, { recursive: true })
|
fs.mkdirSync(sessionDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext = '.json'
|
let ext = '.json'
|
||||||
if (options.format === 'chatlab-jsonl') ext = '.jsonl'
|
if (options.format === 'chatlab-jsonl') ext = '.jsonl'
|
||||||
|
|||||||
@@ -396,6 +396,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary));
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
z-index: 20;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option.active .option-desc {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.media-options {
|
.media-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1130,11 +1223,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .slider {
|
input:checked+.slider {
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .slider::before {
|
input:checked+.slider::before {
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } 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 { 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 * as configService from '../services/config'
|
||||||
import './ExportPage.scss'
|
import './ExportPage.scss'
|
||||||
@@ -23,6 +23,7 @@ interface ExportOptions {
|
|||||||
exportVoiceAsText: boolean
|
exportVoiceAsText: boolean
|
||||||
excelCompactColumns: boolean
|
excelCompactColumns: boolean
|
||||||
txtColumns: string[]
|
txtColumns: string[]
|
||||||
|
displayNamePreference: 'group-nickname' | 'remark' | 'nickname'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExportResult {
|
interface ExportResult {
|
||||||
@@ -49,6 +50,8 @@ function ExportPage() {
|
|||||||
const [calendarDate, setCalendarDate] = useState(new Date())
|
const [calendarDate, setCalendarDate] = useState(new Date())
|
||||||
const [selectingStart, setSelectingStart] = useState(true)
|
const [selectingStart, setSelectingStart] = useState(true)
|
||||||
const [showMediaLayoutPrompt, setShowMediaLayoutPrompt] = useState(false)
|
const [showMediaLayoutPrompt, setShowMediaLayoutPrompt] = useState(false)
|
||||||
|
const [showDisplayNameSelect, setShowDisplayNameSelect] = useState(false)
|
||||||
|
const displayNameDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [options, setOptions] = useState<ExportOptions>({
|
const [options, setOptions] = useState<ExportOptions>({
|
||||||
format: 'excel',
|
format: 'excel',
|
||||||
@@ -64,7 +67,8 @@ function ExportPage() {
|
|||||||
exportEmojis: true,
|
exportEmojis: true,
|
||||||
exportVoiceAsText: true,
|
exportVoiceAsText: true,
|
||||||
excelCompactColumns: true,
|
excelCompactColumns: true,
|
||||||
txtColumns: defaultTxtColumns
|
txtColumns: defaultTxtColumns,
|
||||||
|
displayNamePreference: 'remark'
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildDateRangeFromPreset = (preset: string) => {
|
const buildDateRangeFromPreset = (preset: string) => {
|
||||||
@@ -189,6 +193,16 @@ function ExportPage() {
|
|||||||
removeListener?.()
|
removeListener?.()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node
|
||||||
|
if (showDisplayNameSelect && displayNameDropdownRef.current && !displayNameDropdownRef.current.contains(target)) {
|
||||||
|
setShowDisplayNameSelect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [showDisplayNameSelect])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchKeyword.trim()) {
|
if (!searchKeyword.trim()) {
|
||||||
@@ -271,6 +285,7 @@ function ExportPage() {
|
|||||||
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
||||||
excelCompactColumns: options.excelCompactColumns,
|
excelCompactColumns: options.excelCompactColumns,
|
||||||
txtColumns: options.txtColumns,
|
txtColumns: options.txtColumns,
|
||||||
|
displayNamePreference: options.displayNamePreference,
|
||||||
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),
|
||||||
@@ -402,6 +417,25 @@ function ExportPage() {
|
|||||||
{ value: 'excel', label: 'Excel', icon: FileSpreadsheet, desc: '电子表格,适合统计分析' },
|
{ value: 'excel', label: 'Excel', icon: FileSpreadsheet, desc: '电子表格,适合统计分析' },
|
||||||
{ value: 'sql', label: 'PostgreSQL', icon: Database, desc: '数据库脚本,便于导入到数据库' }
|
{ value: 'sql', label: 'PostgreSQL', icon: Database, desc: '数据库脚本,便于导入到数据库' }
|
||||||
]
|
]
|
||||||
|
const displayNameOptions = [
|
||||||
|
{
|
||||||
|
value: 'group-nickname',
|
||||||
|
label: '群昵称优先',
|
||||||
|
desc: '仅群聊有效,私聊显示备注/昵称'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'remark',
|
||||||
|
label: '备注优先',
|
||||||
|
desc: '有备注显示备注,否则显示昵称'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'nickname',
|
||||||
|
label: '微信昵称',
|
||||||
|
desc: '始终显示微信昵称'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const displayNameOption = displayNameOptions.find(option => option.value === options.displayNamePreference)
|
||||||
|
const displayNameLabel = displayNameOption?.label || '备注优先'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="export-page">
|
<div className="export-page">
|
||||||
@@ -516,6 +550,44 @@ function ExportPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 发送者名称显示偏好 */}
|
||||||
|
{(options.format === 'html' || options.format === 'json' || options.format === 'txt') && (
|
||||||
|
<div className="setting-section">
|
||||||
|
<h3>发送者名称显示</h3>
|
||||||
|
<p className="setting-subtitle">选择导出时优先显示的名称</p>
|
||||||
|
<div className="select-field" ref={displayNameDropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`select-trigger ${showDisplayNameSelect ? 'open' : ''}`}
|
||||||
|
onClick={() => setShowDisplayNameSelect(!showDisplayNameSelect)}
|
||||||
|
>
|
||||||
|
<span className="select-value">{displayNameLabel}</span>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
{showDisplayNameSelect && (
|
||||||
|
<div className="select-dropdown">
|
||||||
|
{displayNameOptions.map(option => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
className={`select-option ${options.displayNamePreference === option.value ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setOptions({
|
||||||
|
...options,
|
||||||
|
displayNamePreference: option.value as ExportOptions['displayNamePreference']
|
||||||
|
})
|
||||||
|
setShowDisplayNameSelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="option-label">{option.label}</span>
|
||||||
|
<span className="option-desc">{option.desc}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="setting-section">
|
<div className="setting-section">
|
||||||
<h3>媒体文件</h3>
|
<h3>媒体文件</h3>
|
||||||
<p className="setting-subtitle">导出图片/语音/表情并在记录内写入相对路径</p>
|
<p className="setting-subtitle">导出图片/语音/表情并在记录内写入相对路径</p>
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -354,6 +354,7 @@ export interface ExportOptions {
|
|||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
txtColumns?: string[]
|
txtColumns?: string[]
|
||||||
sessionLayout?: 'shared' | 'per-session'
|
sessionLayout?: 'shared' | 'per-session'
|
||||||
|
displayNamePreference?: 'group-nickname' | 'remark' | 'nickname'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportProgress {
|
export interface ExportProgress {
|
||||||
|
|||||||
Reference in New Issue
Block a user