From 7fad75fad07cc34b691249f439e952ece60ae64a Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Mon, 16 Mar 2026 18:19:49 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BE=A4=E6=88=90=E5=91=98=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=94=BE=E5=9C=A8=E6=B6=88=E6=81=AF=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E9=87=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/exportService.ts | 2 + src/pages/GroupAnalyticsPage.scss | 93 +++++- src/pages/GroupAnalyticsPage.tsx | 516 +++++++++++++---------------- 3 files changed, 315 insertions(+), 296 deletions(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index dcf3956..cb62352 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -4453,6 +4453,7 @@ class ExportService { const cleanedMyWxid = conn.cleanedWxid const isGroup = sessionId.includes('@chatroom') + const rawMyWxid = String(this.configService.get('myWxid') || '').trim() const sessionInfo = await this.getContactInfo(sessionId) const myInfo = await this.getContactInfo(cleanedMyWxid) @@ -5650,6 +5651,7 @@ class ExportService { const cleanedMyWxid = conn.cleanedWxid const isGroup = sessionId.includes('@chatroom') + const rawMyWxid = String(this.configService.get('myWxid') || '').trim() const sessionInfo = await this.getContactInfo(sessionId) const myInfo = await this.getContactInfo(cleanedMyWxid) const contactCache = new Map() diff --git a/src/pages/GroupAnalyticsPage.scss b/src/pages/GroupAnalyticsPage.scss index 796a65c..95c71e6 100644 --- a/src/pages/GroupAnalyticsPage.scss +++ b/src/pages/GroupAnalyticsPage.scss @@ -480,6 +480,12 @@ overflow: hidden; } +.detail-drag-region { + height: 16px; + flex-shrink: 0; + -webkit-app-region: drag; +} + .resize-handle { width: 4px; cursor: col-resize; @@ -1177,14 +1183,36 @@ .member-message-toolbar { display: grid; gap: 12px; - grid-template-columns: minmax(240px, 360px) minmax(0, 1fr); - align-items: start; + grid-template-columns: minmax(240px, 360px) minmax(160px, 1fr); + align-items: end; @media (max-width: 900px) { grid-template-columns: 1fr; } } + .member-message-toolbar-actions { + display: flex; + justify-content: flex-end; + align-items: center; + + @media (max-width: 900px) { + justify-content: flex-start; + } + } + + .member-message-select-trigger { + border-radius: 12px; + } + + .member-message-summary-text { + align-self: flex-start; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.2; + } + .member-message-summary-card { min-height: 48px; display: flex; @@ -1573,6 +1601,11 @@ background: rgba(30, 30, 30, 0.95); border: 1px solid rgba(255, 255, 255, 0.1); } + + .member-export-modal { + background: rgba(30, 30, 30, 0.95); + border: 1px solid rgba(255, 255, 255, 0.1); + } } // 成员详情弹框 @@ -1733,3 +1766,59 @@ } } } + +.member-export-modal { + background: rgba(255, 255, 255, 0.97); + border-radius: 20px; + padding: 28px; + width: min(720px, calc(100vw - 32px)); + max-height: min(760px, calc(100vh - 32px)); + overflow-y: auto; + position: relative; + backdrop-filter: blur(20px); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + + .modal-close { + position: absolute; + top: 16px; + right: 16px; + background: var(--bg-tertiary); + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + transition: all 0.15s; + + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + } + } + + .member-export-modal-header { + margin-bottom: 18px; + padding-right: 40px; + + h3 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + } + + p { + margin: 6px 0 0; + font-size: 13px; + color: var(--text-secondary); + } + } + + .member-export-panel { + gap: 18px; + } +} diff --git a/src/pages/GroupAnalyticsPage.tsx b/src/pages/GroupAnalyticsPage.tsx index 93891b8..84aaf89 100644 --- a/src/pages/GroupAnalyticsPage.tsx +++ b/src/pages/GroupAnalyticsPage.tsx @@ -37,7 +37,7 @@ interface GroupMessageRank { messageCount: number } -type AnalysisFunction = 'members' | 'memberMessages' | 'memberExport' | 'ranking' | 'activeHours' | 'mediaStats' +type AnalysisFunction = 'members' | 'memberMessages' | 'ranking' | 'activeHours' | 'mediaStats' type MemberExportFormat = 'chatlab' | 'chatlab-jsonl' | 'json' | 'arkme-json' | 'html' | 'txt' | 'excel' | 'weclone' interface MemberMessageExportOptions { @@ -170,7 +170,6 @@ function GroupAnalyticsPage() { const [memberMessagesHasMore, setMemberMessagesHasMore] = useState(false) const [memberMessagesCursor, setMemberMessagesCursor] = useState(0) const [memberMessagesLoadingMore, setMemberMessagesLoadingMore] = useState(false) - const [selectedExportMemberUsername, setSelectedExportMemberUsername] = useState('') const [selectedMessageMemberUsername, setSelectedMessageMemberUsername] = useState('') const [exportFolder, setExportFolder] = useState('') const [memberExportOptions, setMemberExportOptions] = useState({ @@ -188,14 +187,12 @@ function GroupAnalyticsPage() { // 成员详情弹框 const [selectedMember, setSelectedMember] = useState(null) const [copiedField, setCopiedField] = useState(null) + const [showMemberExportModal, setShowMemberExportModal] = useState(false) const [showMessageMemberSelect, setShowMessageMemberSelect] = useState(false) - const [showMemberSelect, setShowMemberSelect] = useState(false) const [showFormatSelect, setShowFormatSelect] = useState(false) const [showDisplayNameSelect, setShowDisplayNameSelect] = useState(false) const [messageMemberSearchKeyword, setMessageMemberSearchKeyword] = useState('') - const [memberSearchKeyword, setMemberSearchKeyword] = useState('') const messageMemberSelectDropdownRef = useRef(null) - const memberSelectDropdownRef = useRef(null) const formatDropdownRef = useRef(null) const displayNameDropdownRef = useRef(null) @@ -241,10 +238,6 @@ function GroupAnalyticsPage() { { value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示昵称' }, { value: 'nickname', label: '微信昵称', desc: '始终显示微信昵称' } ]), []) - const selectedExportMember = useMemo( - () => members.find(member => member.username === selectedExportMemberUsername) || null, - [members, selectedExportMemberUsername] - ) const selectedMessageMember = useMemo( () => members.find(member => member.username === selectedMessageMemberUsername) || null, [members, selectedMessageMemberUsername] @@ -257,9 +250,6 @@ function GroupAnalyticsPage() { () => displayNameOptions.find(option => option.value === memberExportOptions.displayNamePreference) || displayNameOptions[0], [displayNameOptions, memberExportOptions.displayNamePreference] ) - const filteredMemberOptions = useMemo(() => { - return filterMembersByKeyword(members, memberSearchKeyword) - }, [memberSearchKeyword, members]) const filteredMessageMemberOptions = useMemo(() => { return filterMembersByKeyword(members, messageMemberSearchKeyword) }, [members, messageMemberSearchKeyword]) @@ -353,19 +343,14 @@ function GroupAnalyticsPage() { useEffect(() => { if (members.length === 0) { - setSelectedExportMemberUsername('') setSelectedMessageMemberUsername('') return } - const exportExists = members.some(member => member.username === selectedExportMemberUsername) - if (!exportExists) { - setSelectedExportMemberUsername(members[0].username) - } const messageExists = members.some(member => member.username === selectedMessageMemberUsername) if (!messageExists) { setSelectedMessageMemberUsername(members[0].username) } - }, [members, selectedExportMemberUsername, selectedMessageMemberUsername]) + }, [members, selectedMessageMemberUsername]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -373,9 +358,6 @@ function GroupAnalyticsPage() { if (showMessageMemberSelect && messageMemberSelectDropdownRef.current && !messageMemberSelectDropdownRef.current.contains(target)) { setShowMessageMemberSelect(false) } - if (showMemberSelect && memberSelectDropdownRef.current && !memberSelectDropdownRef.current.contains(target)) { - setShowMemberSelect(false) - } if (showFormatSelect && formatDropdownRef.current && !formatDropdownRef.current.contains(target)) { setShowFormatSelect(false) } @@ -385,7 +367,7 @@ function GroupAnalyticsPage() { } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) - }, [showDisplayNameSelect, showFormatSelect, showMemberSelect, showMessageMemberSelect]) + }, [showDisplayNameSelect, showFormatSelect, showMessageMemberSelect]) useEffect(() => { if (preselectAppliedRef.current) return @@ -422,7 +404,7 @@ function GroupAnalyticsPage() { // 日期范围变化时自动刷新 useEffect(() => { - if (dateRangeReady && selectedGroup && selectedFunction && selectedFunction !== 'members' && selectedFunction !== 'memberExport') { + if (dateRangeReady && selectedGroup && selectedFunction && selectedFunction !== 'members') { setDateRangeReady(false) loadFunctionData(selectedFunction) } @@ -436,6 +418,7 @@ function GroupAnalyticsPage() { setSelectedFunction(null) setMembers([]) resetMemberMessageState() + setShowMemberExportModal(false) setRankings([]) setActiveHours({}) setMediaStats(null) @@ -450,10 +433,8 @@ function GroupAnalyticsPage() { setSelectedGroupId(group.username) setSelectedFunction(null) setSelectedMember(null) + setShowMemberExportModal(false) resetMemberMessageState() - setSelectedExportMemberUsername('') - setMemberSearchKeyword('') - setShowMemberSelect(false) setShowFormatSelect(false) setShowDisplayNameSelect(false) } @@ -580,23 +561,6 @@ function GroupAnalyticsPage() { }) break } - case 'memberExport': { - updateBackgroundTask(taskId, { - detail: '正在读取导出成员列表', - progressText: '成员导出' - }) - const result = await window.electronAPI.groupAnalytics.getGroupMembers(targetGroup.username) - if (isBackgroundTaskCancelRequested(taskId)) { - finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,成员导出列表未继续写入' }) - return - } - if (result.success && result.data) setMembers(result.data) - finishBackgroundTask(taskId, result.success ? 'completed' : 'failed', { - detail: result.success ? `成员导出列表加载完成,共 ${result.data?.length || 0} 人` : (result.error || '读取成员导出列表失败'), - progressText: result.success ? `${result.data?.length || 0} 人` : '失败' - }) - break - } case 'ranking': { updateBackgroundTask(taskId, { detail: '正在计算群消息排行', @@ -731,7 +695,6 @@ function GroupAnalyticsPage() { } const handleDateRangeComplete = () => { - if (selectedFunction === 'memberExport') return setDateRangeReady(true) } @@ -796,6 +759,13 @@ function GroupAnalyticsPage() { await loadFunctionData('memberMessages', selectedGroup, member.username) } + const handleOpenMemberExportModal = () => { + setShowMessageMemberSelect(false) + setShowFormatSelect(false) + setShowDisplayNameSelect(false) + setShowMemberExportModal(true) + } + const handleExportMembers = async () => { if (!selectedGroup || isExportingMembers) return setIsExportingMembers(true) @@ -858,8 +828,8 @@ function GroupAnalyticsPage() { } const handleExportMemberMessages = async () => { - if (!selectedGroup || !selectedExportMemberUsername || !exportFolder || isExportingMemberMessages) return - const member = members.find(item => item.username === selectedExportMemberUsername) + if (!selectedGroup || !selectedMessageMemberUsername || !exportFolder || isExportingMemberMessages) return + const member = members.find(item => item.username === selectedMessageMemberUsername) if (!member) { alert('请先选择成员') return @@ -893,6 +863,7 @@ function GroupAnalyticsPage() { } ) if (result.success && (result.successCount ?? 0) > 0) { + setShowMemberExportModal(false) alert(`导出成功:${member.displayName || member.username}`) } else { alert(`导出失败:${result.error || '未知错误'}`) @@ -1080,11 +1051,6 @@ function GroupAnalyticsPage() { 成员消息查看 按成员筛选并分页查看群聊消息 -
handleFunctionSelect('memberExport')}> - - 成员消息导出 - 按成员筛选并导出群聊记录 -
handleFunctionSelect('ranking')}> 群聊发言排行 @@ -1109,7 +1075,6 @@ function GroupAnalyticsPage() { switch (selectedFunction) { case 'members': return '群成员查看' case 'memberMessages': return '成员消息查看' - case 'memberExport': return '成员消息导出' case 'ranking': return '群聊发言排行' case 'activeHours': return '群聊活跃时段' case 'mediaStats': return '媒体内容统计' @@ -1177,15 +1142,16 @@ function GroupAnalyticsPage() {
暂无群成员数据,请先刷新。
) : ( <> +
已加载 {memberMessages.length} 条消息
+
查看成员
)}
-
- 已加载 {memberMessages.length} 条消息 - - 当前成员:{selectedMessageMember?.displayName || selectedMessageMember?.username || '未选择成员'} - +
+
@@ -1283,234 +1253,6 @@ function GroupAnalyticsPage() { )}
)} - {selectedFunction === 'memberExport' && ( -
- {members.length === 0 ? ( -
暂无群成员数据,请先刷新。
- ) : ( - <> -
-
- 导出成员 - - {showMemberSelect && ( -
-
- - setMemberSearchKeyword(e.target.value)} - placeholder="搜索 wxid / 昵称 / 备注 / 微信号" - /> -
-
- {filteredMemberOptions.length === 0 ? ( -
无匹配成员
- ) : ( - filteredMemberOptions.map(member => ( - - )) - )} -
-
- )} -
-
- 导出格式 - - {showFormatSelect && ( -
- {memberExportFormatOptions.map(option => ( - - ))} -
- )} -
-
- 导出目录 -
- - -
-
-
- -
-
- 媒体导出 - -
-
- 媒体类型 -
- - - - -
-
-
- 附加选项 -
- - -
-
-
- 显示名称规则 - - {showDisplayNameSelect && ( -
- {displayNameOptions.map(option => ( - - ))} -
- )} -
-
- -
- -
- - )} -
- )} {selectedFunction === 'ranking' && (
{rankings.map((item, index) => ( @@ -1572,18 +1314,203 @@ function GroupAnalyticsPage() { const renderDetailPanel = () => { + if (selectedFunction) { + return renderFunctionContent() + } + if (!selectedGroup) { return ( -
- + <> + + ) } - if (!selectedFunction) { - return renderFunctionMenu() - } - return renderFunctionContent() + return ( + <> +
{renderMemberModal()} + {renderMemberExportModal()}
) }