diff --git a/electron/services/bizService.ts b/electron/services/bizService.ts index b42c20f..110b22c 100644 --- a/electron/services/bizService.ts +++ b/electron/services/bizService.ts @@ -23,7 +23,6 @@ export interface BizMessage { url: string cover: string content_list: any[] - raw?: any // 调试用 } export interface BizPayRecord { @@ -69,7 +68,7 @@ export class BizService { } if (itemStruct.title) contentList.push(itemStruct) } - } catch (e) {} + } catch (e) { } return contentList } @@ -86,51 +85,6 @@ export class BizService { } catch (e) { return null } } - /** - * 核心:获取公众号消息,支持从 biz_message*.db 自动定位 - */ - private async getBizRawMessages(username: string, account: string, limit: number, offset: number): Promise { - console.log(`[BizService] getBizRawMessages: ${username}, offset=${offset}, limit=${limit}`) - - // 1. 首先尝试直接用 chatService.getMessages (如果 Native 层支持路由) - const chatRes = await chatService.getMessages(username, offset, limit) - if (chatRes.success && chatRes.messages && chatRes.messages.length > 0) { - console.log(`[BizService] chatService found ${chatRes.messages.length} messages for ${username}`) - return chatRes.messages - } - - // 2. 如果 chatService 没找到,手动扫描 biz_message*.db (类似 Python 逻辑) - console.log(`[BizService] chatService empty, manual scanning biz_message*.db...`) - const root = this.configService.get('dbPath') - const accountWxid = account || this.configService.get('myWxid') - if (!root || !accountWxid) return [] - - const dbDir = join(root, accountWxid, 'db_storage', 'message') - if (!existsSync(dbDir)) return [] - - const md5Id = createHash('md5').update(username).digest('hex').toLowerCase() - const tableName = `Msg_${md5Id}` - const bizDbFiles = readdirSync(dbDir).filter(f => f.startsWith('biz_message') && f.endsWith('.db')) - - for (const file of bizDbFiles) { - const dbPath = join(dbDir, file) - // 检查表是否存在 - const checkRes = await wcdbService.execQuery('message', dbPath, `SELECT name FROM sqlite_master WHERE type='table' AND lower(name)='${tableName}'`) - if (checkRes.success && checkRes.rows && checkRes.rows.length > 0) { - console.log(`[BizService] Found table ${tableName} in ${file}`) - // 分页查询原始行 - const sql = `SELECT * FROM ${tableName} ORDER BY create_time DESC LIMIT ${limit} OFFSET ${offset}` - const queryRes = await wcdbService.execQuery('message', dbPath, sql) - if (queryRes.success && queryRes.rows) { - // *** 复用 chatService 的解析逻辑 *** - return chatService.mapRowsToMessagesForApi(queryRes.rows) - } - } - } - - return [] - } - async listAccounts(account?: string): Promise { try { const contactsResult = await chatService.getContacts({ lite: true }) @@ -179,7 +133,7 @@ export class BizService { username: uname, name: info?.displayName || contact.displayName || uname, avatar: info?.avatarUrl || '', - type: 0, + type: 0, last_time: lastTime, formatted_last_time: lastTime ? new Date(lastTime * 1000).toISOString().split('T')[0] : '' } @@ -194,23 +148,24 @@ export class BizService { for (const acc of result) if (typeMap[acc.username] !== undefined) acc.type = typeMap[acc.username] } } -// 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 - }) + + 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 [] } } async listMessages(username: string, account?: string, limit: number = 20, offset: number = 0): Promise { - console.log(`[BizService] listMessages: ${username}, limit=${limit}, offset=${offset}`) try { - const rawMessages = await this.getBizRawMessages(username, account || '', limit, offset) - - const bizMessages: BizMessage[] = rawMessages.map(msg => { + // 仅保留核心路径:利用 chatService 的自动路由能力 + const res = await chatService.getMessages(username, offset, limit) + if (!res.success || !res.messages) return [] + + return res.messages.map(msg => { const bizMsg: BizMessage = { local_id: msg.localId, create_time: msg.createTime, @@ -229,19 +184,17 @@ return result } return bizMsg }) - return bizMessages - } catch (e) { - console.error(`[BizService] listMessages error:`, e) - return [] - } + } catch (e) { return [] } } async listPayRecords(account?: string, limit: number = 20, offset: number = 0): Promise { const username = 'gh_3dfda90e39d6' try { - const rawMessages = await this.getBizRawMessages(username, account || '', limit, offset) + const res = await chatService.getMessages(username, offset, limit) + if (!res.success || !res.messages) return [] + const records: BizPayRecord[] = [] - for (const msg of rawMessages) { + for (const msg of res.messages) { if (!msg.rawContent) continue const parsedData = this.parsePayXml(msg.rawContent) if (parsedData) { diff --git a/src/pages/BizPage.scss b/src/pages/BizPage.scss index 5aee2b7..26df02b 100644 --- a/src/pages/BizPage.scss +++ b/src/pages/BizPage.scss @@ -1,13 +1,13 @@ .biz-account-list { flex: 1; overflow-y: auto; - background-color: var(--bg-primary); + background-color: var(--bg-secondary); // 对齐会话列表背景 .biz-loading { padding: 20px; text-align: center; font-size: 12px; - color: var(--text-muted); + color: var(--text-tertiary); } .biz-account-item { @@ -16,25 +16,31 @@ gap: 12px; padding: 12px 16px; cursor: pointer; - transition: background-color 0.2s; - border-bottom: 1px solid var(--border-subtle); + transition: all 0.2s; + border-bottom: 1px solid var(--border-color); &:hover { - background-color: var(--bg-tertiary); + background-color: var(--bg-hover); } &.active { - background-color: var(--bg-active) !important; + background-color: var(--primary-light) !important; + border-left: 3px solid var(--primary); + padding-left: 13px; // 补偿 border-left } &.pay-account { - background-color: var(--bg-soft); + background-color: var(--bg-primary); + &.active { + background-color: var(--primary-light) !important; + border-left: 3px solid var(--primary); + } } .biz-avatar { - width: 40px; - height: 40px; - border-radius: 4px; + width: 48px; + height: 48px; + border-radius: 8px; // 对齐会话列表头像圆角 object-fit: cover; flex-shrink: 0; background-color: var(--bg-tertiary); @@ -55,7 +61,7 @@ .biz-name { font-size: 14px; font-weight: 500; - color: var(--text-main); + color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -63,7 +69,7 @@ .biz-time { font-size: 11px; - color: var(--text-muted); + color: var(--text-tertiary); flex-shrink: 0; } } @@ -75,10 +81,10 @@ width: fit-content; margin-top: 2px; - &.type-service { color: #03C160; background: rgba(3, 193, 96, 0.1); } - &.type-sub { color: #108ee9; background: rgba(16, 142, 233, 0.1); } + &.type-service { color: #07c160; background: rgba(7, 193, 96, 0.1); } + &.type-sub { color: var(--primary); background: var(--primary-light); } &.type-enterprise { color: #f5222d; background: rgba(245, 34, 45, 0.1); } - &.type-unknown { color: #8c8c8c; background: rgba(140, 140, 140, 0.1); } + &.type-unknown { color: var(--text-tertiary); background: var(--bg-tertiary); } } } } @@ -88,21 +94,21 @@ height: 100%; display: flex; flex-direction: column; - background-color: var(--bg-tertiary); + background-color: var(--bg-secondary); // 对齐聊天页背景 .main-header { height: 56px; padding: 0 20px; display: flex; align-items: center; - border-bottom: 1px solid var(--border-dim); - background-color: var(--bg-primary); + border-bottom: 1px solid var(--border-color); + background-color: var(--card-bg); flex-shrink: 0; h2 { font-size: 16px; font-weight: 600; - color: var(--text-main); + color: var(--text-primary); } } @@ -110,6 +116,8 @@ flex: 1; overflow-y: auto; padding: 24px 16px; + background: var(--chat-pattern); + background-color: var(--bg-tertiary); // 对齐聊天背景色 .messages-wrapper { width: 100%; @@ -121,16 +129,57 @@ } } + // 占位状态:对齐 Chat 页面风格 + .biz-no-record-container { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; + background: var(--bg-tertiary); + + .no-record-icon { + width: 64px; + height: 64px; + border-radius: 50%; + background-color: var(--bg-secondary); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + color: var(--text-tertiary); + opacity: 0.5; + + svg { width: 32px; height: 32px; } + } + + h3 { + font-size: 16px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 8px; + } + + p { + font-size: 13px; + color: var(--text-secondary); + max-width: 280px; + line-height: 1.5; + } + } + .biz-loading-more { text-align: center; padding: 20px; font-size: 12px; - color: var(--text-muted); + color: var(--text-tertiary); } .pay-card { - background-color: var(--bg-primary); - border: 1px solid var(--border-dim); + background-color: var(--card-bg); + border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); @@ -140,7 +189,7 @@ align-items: center; gap: 8px; font-size: 13px; - color: var(--text-muted); + color: var(--text-tertiary); margin-bottom: 20px; .pay-icon { @@ -148,14 +197,17 @@ height: 24px; border-radius: 50%; object-fit: cover; - - &.placeholder { - background-color: #03C160; - color: white; - display: flex; - align-items: center; - justify-content: center; - } + } + .pay-icon-placeholder { + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #07c160; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; } } @@ -163,30 +215,30 @@ text-align: center; font-size: 22px; font-weight: 500; - color: var(--text-main); + color: var(--text-primary); margin-bottom: 24px; } .pay-desc { font-size: 13px; line-height: 1.6; - color: var(--text-muted); + color: var(--text-secondary); white-space: pre-wrap; } .pay-footer { margin-top: 16px; padding-top: 12px; - border-top: 1px solid var(--border-subtle); + border-top: 1px solid var(--border-color); font-size: 12px; - color: var(--text-muted); + color: var(--text-tertiary); text-align: right; } } .article-card { - background-color: var(--bg-primary); - border: 1px solid var(--border-dim); + background-color: var(--card-bg); + border: 1px solid var(--border-color); border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); @@ -226,8 +278,8 @@ .article-digest { padding: 12px 16px; font-size: 14px; - color: var(--text-muted); - border-bottom: 1px solid var(--border-subtle); + color: var(--text-secondary); + border-bottom: 1px solid var(--border-color); } .sub-articles { @@ -236,15 +288,15 @@ align-items: center; justify-content: space-between; padding: 16px; - border-top: 1px solid var(--border-subtle); + border-top: 1px solid var(--border-color); cursor: pointer; - &:hover { background-color: var(--bg-secondary); } + &:hover { background-color: var(--bg-hover); } .sub-title { flex: 1; font-size: 15px; - color: var(--text-main); + color: var(--text-primary); padding-right: 12px; display: -webkit-box; -webkit-line-clamp: 2; @@ -258,7 +310,7 @@ border-radius: 4px; object-fit: cover; flex-shrink: 0; - border: 1px solid var(--border-subtle); + border: 1px solid var(--border-color); } } } @@ -273,6 +325,7 @@ justify-content: center; text-align: center; height: 100%; + background: var(--bg-tertiary); // 对齐 Chat 页面空白背景 .empty-icon { width: 80px; @@ -288,6 +341,5 @@ svg { width: 40px; height: 40px; } } - p { color: var(--text-muted); font-size: 14px; } + p { color: var(--text-tertiary); font-size: 14px; } } - diff --git a/src/pages/BizPage.tsx b/src/pages/BizPage.tsx index 355f86c..9b54ef3 100644 --- a/src/pages/BizPage.tsx +++ b/src/pages/BizPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useThemeStore } from '../stores/themeStore'; -import { Newspaper } from 'lucide-react'; +import { Newspaper, MessageSquareOff } from 'lucide-react'; import './BizPage.scss'; export interface BizAccount { @@ -33,7 +33,7 @@ export const BizAccountList: React.FC<{ console.error("获取 myWxid 失败:", e); } }; - initWxid(); + initWxid().then(_r => { }); }, []); useEffect(() => { @@ -52,7 +52,7 @@ export const BizAccountList: React.FC<{ setLoading(false); } }; - fetch(); + fetch().then(_r => { } ); }, [myWxid]); const filtered = useMemo(() => { @@ -75,7 +75,7 @@ export const BizAccountList: React.FC<{ className={`biz-account-item ${selectedUsername === item.username ? 'active' : ''} ${item.username === 'gh_3dfda90e39d6' ? 'pay-account' : ''}`} > @@ -89,7 +89,7 @@ export const BizAccountList: React.FC<{ item.type === 0 ? 'type-sub' : item.type === 2 ? 'type-enterprise' : 'type-unknown' }`}> - {item.type === 0 ? '服务号' : item.type === 1 ? '订阅号' : item.type === 2 ? '企业号' : '未知'} + {item.type === 1 ? '服务号' : item.type === 0 ? '订阅号' : item.type === 2 ? '企业号' : '未知'} @@ -98,7 +98,7 @@ export const BizAccountList: React.FC<{ ); }; -// 2. 公众号消息区域组件 (展示在右侧消息区) +// 2. 公众号消息区域组件 export const BizMessageArea: React.FC<{ account: BizAccount | null; }> = ({ account }) => { @@ -110,7 +110,6 @@ export const BizMessageArea: React.FC<{ const limit = 20; const messageListRef = useRef(null); - // ======== 修改开始:独立从底层获取 myWxid ======== const [myWxid, setMyWxid] = useState(''); useEffect(() => { @@ -120,13 +119,10 @@ export const BizMessageArea: React.FC<{ if (wxid) { setMyWxid(wxid as string); } - } catch (e) { - console.error("获取 myWxid 失败:", e); - } + } catch (e) { } }; initWxid(); }, []); - // ======== 修改结束 ======== const isDark = useMemo(() => { if (themeMode === 'dark') return true; @@ -136,8 +132,6 @@ export const BizMessageArea: React.FC<{ return false; }, [themeMode]); - // ======== 补充修改:添加 myWxid 依赖 ======== - // 必须加上 myWxid 作为依赖项,否则第一次点击左侧账号时,如果 wxid 还没异步拿回来,就不会触发加载 useEffect(() => { if (account && myWxid) { setMessages([]); @@ -146,19 +140,16 @@ export const BizMessageArea: React.FC<{ loadMessages(account.username, 0); } }, [account, myWxid]); - // ======== 补充修改结束 ======== const loadMessages = async (username: string, currentOffset: number) => { - if (loading || !myWxid) return; // 没账号直接 return + if (loading || !myWxid) return; setLoading(true); try { let res; if (username === 'gh_3dfda90e39d6') { - // 传入 myWxid res = await window.electronAPI.biz.listPayRecords(myWxid, limit, currentOffset); } else { - // 传入 myWxid,替换掉 undefined res = await window.electronAPI.biz.listMessages(username, myWxid, limit, currentOffset); } if (res) { @@ -201,15 +192,20 @@ export const BizMessageArea: React.FC<{
{!loading && messages.length === 0 && ( -
-

暂无本地记录

+
+
+ +
+

暂无本地记录

+

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

)} - {messages.map((msg) => (
+ {messages.map((msg) => ( +
{account.username === 'gh_3dfda90e39d6' ? (
- {msg.merchant_icon ? :
¥
} + {msg.merchant_icon ? :
¥
} {msg.merchant_name || '微信支付'}
{msg.title}
@@ -223,9 +219,9 @@ export const BizMessageArea: React.FC<{

{msg.title}

{msg.des &&
{msg.des}
} - {msg.content_list && msg.content_list.length > 0 && ( + {msg.content_list && msg.content_list.length > 1 && (
- {msg.content_list.map((item: any, idx: number) => ( + {msg.content_list.slice(1).map((item: any, idx: number) => (
window.electronAPI.shell.openExternal(item.url)} className="sub-item"> {item.title} {item.cover && } @@ -244,7 +240,6 @@ export const BizMessageArea: React.FC<{ ); }; -// 保持 BizPage 作为入口 (如果需要独立页面) const BizPage: React.FC = () => { const [selectedAccount, setSelectedAccount] = useState(null); return ( @@ -257,4 +252,4 @@ const BizPage: React.FC = () => { ); } -export default BizPage; \ No newline at end of file +export default BizPage; diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 69781d3..79f415e 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -4487,6 +4487,32 @@ font-weight: 500; } } + +// 公众号入口样式 +.session-item.biz-entry { + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background: var(--hover-bg, rgba(0,0,0,0.05)); + } + + .biz-entry-avatar { + width: 48px; + height: 48px; + border-radius: 8px; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #07c160; + } + + .session-name { + font-weight: 500; + } +} // 消息信息弹窗 .message-info-overlay { position: fixed; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index d98398d..a2d1fed 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -986,6 +986,7 @@ const SessionItem = React.memo(function SessionItem({ ) const isFoldEntry = session.username.toLowerCase().includes('placeholder_foldgroup') + const isBizEntry = session.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID // 折叠入口:专属名称和图标 if (isFoldEntry) { @@ -1010,6 +1011,29 @@ const SessionItem = React.memo(function SessionItem({ ) } + // 公众号入口:专属名称和图标 + if (isBizEntry) { + return ( +
onSelect(session)} + > +
+ +
+
+
+ 订阅号/服务号 + {timeText} +
+
+ {session.summary || '查看公众号历史消息'} +
+
+
+ ) + } + // 根据匹配字段显示不同的 summary const summaryContent = useMemo(() => { if (session.matchedField === 'wxid') { @@ -4965,29 +4989,25 @@ function ChatPage(props: ChatPageProps) { return true }) - // 注入“订阅号/服务号”虚拟项 const bizEntry: ChatSession = { username: OFFICIAL_ACCOUNTS_VIRTUAL_ID, - displayName: '订阅号/服务号', + displayName: '公众号', summary: '查看公众号历史消息', type: 0, - sortTimestamp: 9999999999, // 确保在前面,或者您可以根据需要调整排序 + sortTimestamp: 9999999999, // 放到最前面? 目前还没有严格的对时间进行排序, 后面可以改一下 lastTimestamp: 0, lastMsgType: 0, unreadCount: 0, isMuted: false, isFolded: false } - - // 检查是否已经存在(防止重复注入) + if (!visible.some(s => s.username === OFFICIAL_ACCOUNTS_VIRTUAL_ID)) { - // 插入到首位或者折叠项之后 visible.unshift(bizEntry) } - // 如果有折叠的群聊,但列表中没有入口,则插入入口 if (hasFoldedGroups && !visible.some(s => s.username.toLowerCase().includes('placeholder_foldgroup'))) { - // 找到最新的折叠消息 + const latestFolded = foldedGroups.reduce((latest, current) => { const latestTime = latest.sortTimestamp || latest.lastTimestamp const currentTime = current.sortTimestamp || current.lastTimestamp @@ -6239,7 +6259,7 @@ function ChatPage(props: ChatPageProps) {