diff --git a/electron/services/bizService.ts b/electron/services/bizService.ts index 110b22c..f7c0eed 100644 --- a/electron/services/bizService.ts +++ b/electron/services/bizService.ts @@ -87,11 +87,14 @@ export class BizService { async listAccounts(account?: string): Promise { try { + // 1. 获取公众号联系人列表 const contactsResult = await chatService.getContacts({ lite: true }) if (!contactsResult.success || !contactsResult.contacts) return [] const officialContacts = contactsResult.contacts.filter(c => c.type === 'official') const usernames = officialContacts.map(c => c.username) + + // 获取头像和昵称等补充信息 const enrichment = await chatService.enrichSessionsContactInfo(usernames) const contactInfoMap = enrichment.success && enrichment.contacts ? enrichment.contacts : {} @@ -100,31 +103,45 @@ export class BizService { const accountWxid = account || myWxid if (!root || !accountWxid) return [] - const dbDir = join(root, accountWxid, 'db_storage', 'message') const bizLatestTime: Record = {} - if (existsSync(dbDir)) { - const bizDbFiles = readdirSync(dbDir).filter(f => f.startsWith('biz_message') && f.endsWith('.db')) - for (const file of bizDbFiles) { - const dbPath = join(dbDir, file) - const name2idRes = await wcdbService.execQuery('message', dbPath, 'SELECT username FROM Name2Id') - if (name2idRes.success && name2idRes.rows) { - for (const row of name2idRes.rows) { - const uname = row.username || row.user_name - if (uname) { - const md5 = createHash('md5').update(uname).digest('hex').toLowerCase() - const tName = `Msg_${md5}` - const timeRes = await wcdbService.execQuery('message', dbPath, `SELECT MAX(create_time) as max_time FROM ${tName}`) - if (timeRes.success && timeRes.rows && timeRes.rows[0]?.max_time) { - const t = parseInt(timeRes.rows[0].max_time) - if (!isNaN(t)) bizLatestTime[uname] = Math.max(bizLatestTime[uname] || 0, t) - } - } + try { + const sessionsRes = await wcdbService.getSessions() + if (sessionsRes.success && sessionsRes.sessions) { + for (const session of sessionsRes.sessions) { + const uname = session.username || session.strUsrName || session.userName || session.id + // 适配日志中发现的字段,注意转为整型数字 + const timeStr = session.last_timestamp || session.sort_timestamp || session.nTime || session.timestamp || '0' + const time = parseInt(timeStr.toString(), 10) + + if (usernames.includes(uname) && time > 0) { + bizLatestTime[uname] = time } } } + } catch (e) { + console.error('获取 Sessions 失败:', e) } + // 3. 格式化时间显示 + const formatBizTime = (ts: number) => { + if (!ts) return '' + const date = new Date(ts * 1000) + const now = new Date() + const isToday = date.toDateString() === now.toDateString() + if (isToday) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) + + const yesterday = new Date(now) + yesterday.setDate(now.getDate() - 1) + if (date.toDateString() === yesterday.toDateString()) return '昨天' + + const isThisYear = date.getFullYear() === now.getFullYear() + if (isThisYear) return `${date.getMonth() + 1}/${date.getDate()}` + + return `${date.getFullYear().toString().slice(-2)}/${date.getMonth() + 1}/${date.getDate()}` + } + + // 4. 组装数据 const result: BizAccount[] = officialContacts.map(contact => { const uname = contact.username const info = contactInfoMap[uname] @@ -135,11 +152,12 @@ export class BizService { avatar: info?.avatarUrl || '', type: 0, last_time: lastTime, - formatted_last_time: lastTime ? new Date(lastTime * 1000).toISOString().split('T')[0] : '' + formatted_last_time: formatBizTime(lastTime) } }) - const contactDbPath = join(root, accountWxid, 'contact.db') + // 5. 补充公众号类型 (订阅号/服务号) + const contactDbPath = join(root, accountWxid, 'db_storage', 'contact', 'contact.db') if (existsSync(contactDbPath)) { const bizInfoRes = await wcdbService.execQuery('contact', contactDbPath, 'SELECT username, type FROM biz_info') if (bizInfoRes.success && bizInfoRes.rows) { @@ -149,14 +167,18 @@ export class BizService { } } + // 6. 排序输出 return result - .filter(acc => !acc.name.includes('朋友圈广告')) - .sort((a, b) => { - if (a.username === 'gh_3dfda90e39d6') return -1 - if (b.username === 'gh_3dfda90e39d6') return 1 - return b.last_time - a.last_time - }) - } catch (e) { return [] } + .filter(acc => !acc.name.includes('广告')) + .sort((a, b) => { + if (a.username === 'gh_3dfda90e39d6') return -1 // 微信支付置顶 + if (b.username === 'gh_3dfda90e39d6') return 1 + return b.last_time - a.last_time // 按最新时间降序排列 + }) + } catch (e) { + console.error('获取账号列表发生错误:', e) + return [] + } } async listMessages(username: string, account?: string, limit: number = 20, offset: number = 0): Promise { diff --git a/src/pages/BizPage.scss b/src/pages/BizPage.scss index 26df02b..a2faddb 100644 --- a/src/pages/BizPage.scss +++ b/src/pages/BizPage.scss @@ -125,7 +125,22 @@ margin: 0 auto; display: flex; flex-direction: column; - gap: 24px; + gap: 16px; // 减小间距,因为有了 time-divider + } + } + + .time-divider { + text-align: center; + margin: 16px 0 8px; + + span { + display: inline-block; + padding: 2px 8px; + background-color: var(--bg-primary); + color: var(--text-tertiary); + font-size: 11px; + border-radius: 4px; + opacity: 0.8; } } diff --git a/src/pages/BizPage.tsx b/src/pages/BizPage.tsx index 457d1b2..6831d54 100644 --- a/src/pages/BizPage.tsx +++ b/src/pages/BizPage.tsx @@ -7,7 +7,7 @@ export interface BizAccount { username: string; name: string; avatar: string; - type: number; + type: string; last_time: number; formatted_last_time: string; } @@ -55,15 +55,24 @@ export const BizAccountList: React.FC<{ fetch().then(_r => { } ); }, [myWxid]); + const filtered = useMemo(() => { - if (!searchKeyword) return accounts; - const q = searchKeyword.toLowerCase(); - return accounts.filter(a => - (a.name && a.name.toLowerCase().includes(q)) || - (a.username && a.username.toLowerCase().includes(q)) - ); + let result = accounts; + if (searchKeyword) { + const q = searchKeyword.toLowerCase(); + result = accounts.filter(a => + (a.name && a.name.toLowerCase().includes(q)) || + (a.username && a.username.toLowerCase().includes(q)) + ); + } + return result.sort((a, b) => { + if (a.username === 'gh_3dfda90e39d6') return -1; // 微信支付置顶 + if (b.username === 'gh_3dfda90e39d6') return 1; + return b.last_time - a.last_time; + }); }, [accounts, searchKeyword]); + if (loading) return
加载中...
; return ( @@ -84,18 +93,18 @@ export const BizAccountList: React.FC<{ {item.name || item.username} {item.formatted_last_time} - {item.username === 'gh_3dfda90e39d6' && ( -
服务号
- )} + {/*{item.username === 'gh_3dfda90e39d6' && (*/} + {/*
微信支付
*/} + {/*)}*/} - {/* 我看了下没有接口获取相关type,如果exec没法用的话确实无能为力,后面再适配吧 */} - {/*
*/} - {/* {item.type === 1 ? '服务号' : item.type === 0 ? '订阅号' : item.type === 2 ? '企业号' : '未知'}*/} - {/*
*/} +
+ {item.type === '0' ? '公众号' : item.type === '1' ? '服务号' : item.type === '2' ? '企业号' : item.type === '3' ? '企业附属' : '未知'} +
@@ -104,7 +113,6 @@ export const BizAccountList: React.FC<{ ); }; -// 2. 公众号消息区域组件 export const BizMessageArea: React.FC<{ account: BizAccount | null; }> = ({ account }) => { @@ -115,6 +123,8 @@ export const BizMessageArea: React.FC<{ const [hasMore, setHasMore] = useState(true); const limit = 20; const messageListRef = useRef(null); + const lastScrollHeightRef = useRef(0); + const isInitialLoadRef = useRef(true); const [myWxid, setMyWxid] = useState(''); @@ -143,6 +153,7 @@ export const BizMessageArea: React.FC<{ setMessages([]); setOffset(0); setHasMore(true); + isInitialLoadRef.current = true; loadMessages(account.username, 0); } }, [account, myWxid]); @@ -151,6 +162,10 @@ export const BizMessageArea: React.FC<{ if (loading || !myWxid) return; setLoading(true); + if (messageListRef.current) { + lastScrollHeightRef.current = messageListRef.current.scrollHeight; + } + try { let res; if (username === 'gh_3dfda90e39d6') { @@ -158,9 +173,15 @@ export const BizMessageArea: React.FC<{ } else { res = await window.electronAPI.biz.listMessages(username, myWxid, limit, currentOffset); } + if (res) { if (res.length < limit) setHasMore(false); - setMessages(prev => currentOffset === 0 ? res : [...prev, ...res]); + + setMessages(prev => { + const combined = currentOffset === 0 ? res : [...res, ...prev]; + const uniqueMessages = Array.from(new Map(combined.map(item => [item.local_id || item.create_time, item])).values()); + return uniqueMessages.sort((a, b) => a.create_time - b.create_time); + }); setOffset(currentOffset + limit); } } catch (err) { @@ -170,9 +191,26 @@ export const BizMessageArea: React.FC<{ } }; + useEffect(() => { + if (!messageListRef.current) return; + + if (isInitialLoadRef.current && messages.length > 0) { + messageListRef.current.scrollTop = messageListRef.current.scrollHeight; + isInitialLoadRef.current = false; + } else if (messages.length > 0 && !isInitialLoadRef.current && !loading) { + + const newScrollHeight = messageListRef.current.scrollHeight; + const heightDiff = newScrollHeight - lastScrollHeightRef.current; + if (heightDiff > 0 && messageListRef.current.scrollTop < 100) { + messageListRef.current.scrollTop += heightDiff; + } + } + }, [messages, loading]); + const handleScroll = (e: React.UIEvent) => { const target = e.currentTarget; - if (target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight < 50) { + // 向上滚动到顶部附近触发加载更多(更旧的消息) + if (target.scrollTop < 50) { if (!loading && hasMore && account) { loadMessages(account.username, offset); } @@ -188,6 +226,30 @@ export const BizMessageArea: React.FC<{ ); } + const formatMessageTime = (timestamp: number) => { + if (!timestamp) return ''; + const date = new Date(timestamp * 1000); + const now = new Date(); + + const isToday = date.toDateString() === now.toDateString(); + if (isToday) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }); + } + + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + if (date.toDateString() === yesterday.toDateString()) { + return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + } + + const isThisYear = date.getFullYear() === now.getFullYear(); + if (isThisYear) { + return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + } + + return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`; + }; + const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMTgwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjE4MCIgZmlsbD0iI2Y1ZjVmNSIvPjwvc3ZnPg=='; return ( @@ -197,6 +259,9 @@ export const BizMessageArea: React.FC<{
+ {hasMore && messages.length > 0 && ( +
{loading ? '加载中...' : '向上滚动加载更多历史消息'}
+ )} {!loading && messages.length === 0 && (
@@ -206,40 +271,50 @@ export const BizMessageArea: React.FC<{

该公众号在当前数据库中没有可显示的聊天历史

)} - {messages.map((msg) => ( -
- {account.username === 'gh_3dfda90e39d6' ? ( -
-
- {msg.merchant_icon ? :
¥
} - {msg.merchant_name || '微信支付'} -
-
{msg.title}
-
{msg.description}
-
{msg.formatted_time}
-
- ) : ( -
-
window.electronAPI.shell.openExternal(msg.url)} className="main-article"> - -

{msg.title}

-
- {msg.des &&
{msg.des}
} - {msg.content_list && msg.content_list.length > 1 && ( -
- {msg.content_list.slice(1).map((item: any, idx: number) => ( -
window.electronAPI.shell.openExternal(item.url)} className="sub-item"> - {item.title} - {item.cover && } -
- ))} + {messages.map((msg, index) => { + const showTime = true; + + return ( +
+ {showTime && ( +
+ {formatMessageTime(msg.create_time)} +
+ )} + + {account.username === 'gh_3dfda90e39d6' ? ( +
+
+ {msg.merchant_icon ? :
¥
} + {msg.merchant_name || '微信支付'}
- )} -
- )} -
- ))} - {loading &&
加载中...
} +
{msg.title}
+
{msg.description}
+ {/*
{msg.formatted_time}
*/} +
+ ) : ( +
+
window.electronAPI.shell.openExternal(msg.url)} className="main-article"> + +

{msg.title}

+
+ {msg.des &&
{msg.des}
} + {msg.content_list && msg.content_list.length > 1 && ( +
+ {msg.content_list.slice(1).map((item: any, idx: number) => ( +
window.electronAPI.shell.openExternal(item.url)} className="sub-item"> + {item.title} + {item.cover && } +
+ ))} +
+ )} +
+ )} +
+ ); + })} + {loading && offset === 0 &&
加载中...
}