mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-03 23:15:59 +00:00
时间排序
This commit is contained in:
@@ -92,6 +92,7 @@ export class BizService {
|
|||||||
|
|
||||||
const officialContacts = contactsResult.contacts.filter(c => c.type === 'official')
|
const officialContacts = contactsResult.contacts.filter(c => c.type === 'official')
|
||||||
const usernames = officialContacts.map(c => c.username)
|
const usernames = officialContacts.map(c => c.username)
|
||||||
|
|
||||||
const enrichment = await chatService.enrichSessionsContactInfo(usernames)
|
const enrichment = await chatService.enrichSessionsContactInfo(usernames)
|
||||||
const contactInfoMap = enrichment.success && enrichment.contacts ? enrichment.contacts : {}
|
const contactInfoMap = enrichment.success && enrichment.contacts ? enrichment.contacts : {}
|
||||||
|
|
||||||
@@ -100,29 +101,44 @@ export class BizService {
|
|||||||
const accountWxid = account || myWxid
|
const accountWxid = account || myWxid
|
||||||
if (!root || !accountWxid) return []
|
if (!root || !accountWxid) return []
|
||||||
|
|
||||||
const dbDir = join(root, accountWxid, 'db_storage', 'message')
|
|
||||||
const bizLatestTime: Record<string, number> = {}
|
const bizLatestTime: Record<string, number> = {}
|
||||||
|
|
||||||
if (existsSync(dbDir)) {
|
// 暴力解决
|
||||||
const bizDbFiles = readdirSync(dbDir).filter(f => f.startsWith('biz_message') && f.endsWith('.db'))
|
const timePromises = usernames.map(async (uname) => {
|
||||||
for (const file of bizDbFiles) {
|
try {
|
||||||
const dbPath = join(dbDir, file)
|
// limit 设置为 1,只要最新的一条
|
||||||
const name2idRes = await wcdbService.execQuery('message', dbPath, 'SELECT username FROM Name2Id')
|
const res = await chatService.getMessages(uname, 0, 1)
|
||||||
if (name2idRes.success && name2idRes.rows) {
|
if (res.success && res.messages && res.messages.length > 0) {
|
||||||
for (const row of name2idRes.rows) {
|
return { uname, time: res.messages[0].createTime }
|
||||||
const uname = row.username || row.user_name
|
}
|
||||||
if (uname) {
|
} catch (e) {
|
||||||
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}`)
|
return { uname, time: 0 }
|
||||||
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)
|
const timeResults = await Promise.all(timePromises)
|
||||||
}
|
for (const r of timeResults) {
|
||||||
}
|
if (r.time > 0) {
|
||||||
}
|
bizLatestTime[r.uname] = r.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: BizAccount[] = officialContacts.map(contact => {
|
const result: BizAccount[] = officialContacts.map(contact => {
|
||||||
@@ -135,20 +151,14 @@ export class BizService {
|
|||||||
avatar: info?.avatarUrl || '',
|
avatar: info?.avatarUrl || '',
|
||||||
type: 0,
|
type: 0,
|
||||||
last_time: lastTime,
|
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, 'db_storage', 'contact', 'contact.db') // ????
|
const contactDbPath = join(root, accountWxid, 'db_storage', 'contact', 'contact.db')
|
||||||
// console.log(`contactDbPath: ${contactDbPath}`)
|
|
||||||
|
|
||||||
if (existsSync(contactDbPath)) {
|
if (existsSync(contactDbPath)) {
|
||||||
// console.log('ok11')
|
|
||||||
|
|
||||||
const bizInfoRes = await wcdbService.execQuery('contact', contactDbPath, 'SELECT username, type FROM biz_info')
|
const bizInfoRes = await wcdbService.execQuery('contact', contactDbPath, 'SELECT username, type FROM biz_info')
|
||||||
|
|
||||||
// console.log(JSON.stringify(bizInfoRes, null, 2))
|
|
||||||
|
|
||||||
if (bizInfoRes.success && bizInfoRes.rows) {
|
if (bizInfoRes.success && bizInfoRes.rows) {
|
||||||
const typeMap: Record<string, number> = {}
|
const typeMap: Record<string, number> = {}
|
||||||
for (const r of bizInfoRes.rows) typeMap[r.username] = r.type
|
for (const r of bizInfoRes.rows) typeMap[r.username] = r.type
|
||||||
@@ -159,11 +169,16 @@ export class BizService {
|
|||||||
return result
|
return result
|
||||||
.filter(acc => !acc.name.includes('广告'))
|
.filter(acc => !acc.name.includes('广告'))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
// 微信支付强制置顶
|
||||||
if (a.username === 'gh_3dfda90e39d6') return -1
|
if (a.username === 'gh_3dfda90e39d6') return -1
|
||||||
if (b.username === 'gh_3dfda90e39d6') return 1
|
if (b.username === 'gh_3dfda90e39d6') return 1
|
||||||
|
// 后端直接排好序输出
|
||||||
return b.last_time - a.last_time
|
return b.last_time - a.last_time
|
||||||
})
|
})
|
||||||
} catch (e) { return [] }
|
} catch (e) {
|
||||||
|
console.error('获取账号列表失败:', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listMessages(username: string, account?: string, limit: number = 20, offset: number = 0): Promise<BizMessage[]> {
|
async listMessages(username: string, account?: string, limit: number = 20, offset: number = 0): Promise<BizMessage[]> {
|
||||||
|
|||||||
@@ -125,7 +125,22 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,24 @@ export const BizAccountList: React.FC<{
|
|||||||
fetch().then(_r => { } );
|
fetch().then(_r => { } );
|
||||||
}, [myWxid]);
|
}, [myWxid]);
|
||||||
|
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!searchKeyword) return accounts;
|
let result = accounts;
|
||||||
|
if (searchKeyword) {
|
||||||
const q = searchKeyword.toLowerCase();
|
const q = searchKeyword.toLowerCase();
|
||||||
return accounts.filter(a =>
|
result = accounts.filter(a =>
|
||||||
(a.name && a.name.toLowerCase().includes(q)) ||
|
(a.name && a.name.toLowerCase().includes(q)) ||
|
||||||
(a.username && a.username.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]);
|
}, [accounts, searchKeyword]);
|
||||||
|
|
||||||
|
|
||||||
if (loading) return <div className="biz-loading">加载中...</div>;
|
if (loading) return <div className="biz-loading">加载中...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,9 +100,10 @@ export const BizAccountList: React.FC<{
|
|||||||
<div className={`biz-badge ${
|
<div className={`biz-badge ${
|
||||||
item.type === '1' ? 'type-service' :
|
item.type === '1' ? 'type-service' :
|
||||||
item.type === '0' ? 'type-sub' :
|
item.type === '0' ? 'type-sub' :
|
||||||
item.type === '2' ? 'type-enterprise' : 'type-unknown'
|
item.type === '2' ? 'type-enterprise' :
|
||||||
|
item.type === '3' ? 'type-enterprise' : 'type-unknown'
|
||||||
}`}>
|
}`}>
|
||||||
{item.type === '0' ? '公众号' : item.type === '1' ? '服务号' : item.type === '2' ? '企业号' : '未知'}
|
{item.type === '0' ? '公众号' : item.type === '1' ? '服务号' : item.type === '2' ? '企业号' : item.type === '3' ? '企业附属' : '未知'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +113,6 @@ export const BizAccountList: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 公众号消息区域组件
|
|
||||||
export const BizMessageArea: React.FC<{
|
export const BizMessageArea: React.FC<{
|
||||||
account: BizAccount | null;
|
account: BizAccount | null;
|
||||||
}> = ({ account }) => {
|
}> = ({ account }) => {
|
||||||
@@ -114,6 +123,8 @@ export const BizMessageArea: React.FC<{
|
|||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
const messageListRef = useRef<HTMLDivElement>(null);
|
const messageListRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastScrollHeightRef = useRef<number>(0);
|
||||||
|
const isInitialLoadRef = useRef<boolean>(true);
|
||||||
|
|
||||||
const [myWxid, setMyWxid] = useState<string>('');
|
const [myWxid, setMyWxid] = useState<string>('');
|
||||||
|
|
||||||
@@ -142,6 +153,7 @@ export const BizMessageArea: React.FC<{
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
isInitialLoadRef.current = true;
|
||||||
loadMessages(account.username, 0);
|
loadMessages(account.username, 0);
|
||||||
}
|
}
|
||||||
}, [account, myWxid]);
|
}, [account, myWxid]);
|
||||||
@@ -150,6 +162,10 @@ export const BizMessageArea: React.FC<{
|
|||||||
if (loading || !myWxid) return;
|
if (loading || !myWxid) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
if (messageListRef.current) {
|
||||||
|
lastScrollHeightRef.current = messageListRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
if (username === 'gh_3dfda90e39d6') {
|
if (username === 'gh_3dfda90e39d6') {
|
||||||
@@ -157,9 +173,15 @@ export const BizMessageArea: React.FC<{
|
|||||||
} else {
|
} else {
|
||||||
res = await window.electronAPI.biz.listMessages(username, myWxid, limit, currentOffset);
|
res = await window.electronAPI.biz.listMessages(username, myWxid, limit, currentOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
if (res.length < limit) setHasMore(false);
|
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);
|
setOffset(currentOffset + limit);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -169,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<HTMLDivElement>) => {
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
if (target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight < 50) {
|
// 向上滚动到顶部附近触发加载更多(更旧的消息)
|
||||||
|
if (target.scrollTop < 50) {
|
||||||
if (!loading && hasMore && account) {
|
if (!loading && hasMore && account) {
|
||||||
loadMessages(account.username, offset);
|
loadMessages(account.username, offset);
|
||||||
}
|
}
|
||||||
@@ -187,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==';
|
const defaultImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMTgwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjE4MCIgZmlsbD0iI2Y1ZjVmNSIvPjwvc3ZnPg==';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -196,6 +259,9 @@ export const BizMessageArea: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className="message-container" onScroll={handleScroll} ref={messageListRef}>
|
<div className="message-container" onScroll={handleScroll} ref={messageListRef}>
|
||||||
<div className="messages-wrapper">
|
<div className="messages-wrapper">
|
||||||
|
{hasMore && messages.length > 0 && (
|
||||||
|
<div className="biz-loading-more">{loading ? '加载中...' : '向上滚动加载更多历史消息'}</div>
|
||||||
|
)}
|
||||||
{!loading && messages.length === 0 && (
|
{!loading && messages.length === 0 && (
|
||||||
<div className="biz-no-record-container">
|
<div className="biz-no-record-container">
|
||||||
<div className="no-record-icon">
|
<div className="no-record-icon">
|
||||||
@@ -205,8 +271,17 @@ export const BizMessageArea: React.FC<{
|
|||||||
<p>该公众号在当前数据库中没有可显示的聊天历史</p>
|
<p>该公众号在当前数据库中没有可显示的聊天历史</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.map((msg) => (
|
{messages.map((msg, index) => {
|
||||||
<div key={msg.local_id}>
|
const showTime = true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={msg.local_id || index}>
|
||||||
|
{showTime && (
|
||||||
|
<div className="time-divider">
|
||||||
|
<span>{formatMessageTime(msg.create_time)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{account.username === 'gh_3dfda90e39d6' ? (
|
{account.username === 'gh_3dfda90e39d6' ? (
|
||||||
<div className="pay-card">
|
<div className="pay-card">
|
||||||
<div className="pay-header">
|
<div className="pay-header">
|
||||||
@@ -215,7 +290,7 @@ export const BizMessageArea: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className="pay-title">{msg.title}</div>
|
<div className="pay-title">{msg.title}</div>
|
||||||
<div className="pay-desc">{msg.description}</div>
|
<div className="pay-desc">{msg.description}</div>
|
||||||
<div className="pay-footer">{msg.formatted_time}</div>
|
{/* <div className="pay-footer">{msg.formatted_time}</div> */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="article-card">
|
<div className="article-card">
|
||||||
@@ -237,8 +312,9 @@ export const BizMessageArea: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
{loading && <div className="biz-loading-more">加载中...</div>}
|
})}
|
||||||
|
{loading && offset === 0 && <div className="biz-loading-more">加载中...</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user