群聊单个成员消息导出

This commit is contained in:
xuncha
2026-02-19 16:49:00 +08:00
parent d5f0094025
commit 89783b4d45
6 changed files with 366 additions and 3 deletions

View File

@@ -1143,6 +1143,38 @@
text-align: center;
}
.member-action-row {
width: 100%;
margin-bottom: 16px;
}
.export-member-btn {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: none;
border-radius: 10px;
padding: 10px 14px;
background: var(--bg-tertiary);
color: var(--text-primary);
cursor: pointer;
transition: all 0.15s;
font-size: 13px;
font-weight: 500;
&:hover {
background: var(--bg-hover);
color: var(--primary);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.member-details {
width: 100%;
display: flex;

View File

@@ -46,6 +46,7 @@ function GroupAnalyticsPage() {
const [mediaStats, setMediaStats] = useState<{ typeCounts: Array<{ type: number; name: string; count: number }>; total: number } | null>(null)
const [functionLoading, setFunctionLoading] = useState(false)
const [isExportingMembers, setIsExportingMembers] = useState(false)
const [isExportingMemberMessages, setIsExportingMemberMessages] = useState(false)
// 成员详情弹框
const [selectedMember, setSelectedMember] = useState<GroupMember | null>(null)
@@ -323,6 +324,43 @@ function GroupAnalyticsPage() {
}
}
const handleExportMemberMessages = async (member: GroupMember) => {
if (!selectedGroup || !member || isExportingMemberMessages) return
setIsExportingMemberMessages(true)
try {
const downloadsPath = await window.electronAPI.app.getDownloadsPath()
const memberName = member.displayName || member.username
const baseName = sanitizeFileName(`${selectedGroup.displayName || selectedGroup.username}_${memberName}_消息记录`)
const separator = downloadsPath && downloadsPath.includes('\\') ? '\\' : '/'
const defaultPath = downloadsPath ? `${downloadsPath}${separator}${baseName}.xlsx` : `${baseName}.xlsx`
const saveResult = await window.electronAPI.dialog.saveFile({
title: `导出 ${memberName} 的群聊消息`,
defaultPath,
filters: [
{ name: 'Excel', extensions: ['xlsx'] },
{ name: 'CSV', extensions: ['csv'] }
]
})
if (!saveResult || saveResult.canceled || !saveResult.filePath) return
const result = await window.electronAPI.groupAnalytics.exportGroupMemberMessages(
selectedGroup.username,
member.username,
saveResult.filePath
)
if (result.success) {
alert(`导出成功,共 ${result.count ?? 0} 条消息`)
} else {
alert(`导出失败:${result.error || '未知错误'}`)
}
} catch (e) {
console.error('导出成员消息失败:', e)
alert(`导出失败:${String(e)}`)
} finally {
setIsExportingMemberMessages(false)
}
}
const handleCopy = async (text: string, field: string) => {
try {
await navigator.clipboard.writeText(text)
@@ -351,6 +389,16 @@ function GroupAnalyticsPage() {
<Avatar src={selectedMember.avatarUrl} name={selectedMember.displayName} size={96} />
</div>
<h3 className="member-display-name">{selectedMember.displayName}</h3>
<div className="member-action-row">
<button
className="export-member-btn"
onClick={() => handleExportMemberMessages(selectedMember)}
disabled={isExportingMemberMessages}
>
{isExportingMemberMessages ? <Loader2 size={16} className="spin" /> : <Download size={16} />}
<span>{isExportingMemberMessages ? '导出中...' : '导出该成员全部消息'}</span>
</button>
</div>
<div className="member-details">
<div className="detail-row">
<span className="detail-label">ID</span>

View File

@@ -273,6 +273,17 @@ export interface ElectronAPI {
count?: number
error?: string
}>
exportGroupMemberMessages: (
chatroomId: string,
memberUsername: string,
outputPath: string,
startTime?: number,
endTime?: number
) => Promise<{
success: boolean
count?: number
error?: string
}>
}
annualReport: {
getAvailableYears: () => Promise<{
@@ -431,7 +442,7 @@ export interface ElectronAPI {
success: boolean
error?: string
}>
exportContacts: (outputDir: string, options: { format: 'json' | 'csv' | 'vcf'; exportAvatars: boolean; contactTypes: { friends: boolean; groups: boolean; officials: boolean } }) => Promise<{
exportContacts: (outputDir: string, options: { format: 'json' | 'csv' | 'vcf'; exportAvatars: boolean; contactTypes: { friends: boolean; groups: boolean; officials: boolean }; selectedUsernames?: string[] }) => Promise<{
success: boolean
successCount?: number
error?: string