11<template >
2- <div class =" biz-page h-screen flex overflow-hidden" style =" background-color : var (--app-shell-bg )" >
2+ <div class =" biz-page h-full min-h-0 flex overflow-hidden" style =" background-color : var (--app-shell-bg )" >
33
44 <div :class =" ['w-[300px] lg:w-[320px] border-r flex flex-col flex-shrink-0 z-10', isDark ? 'bg-[#1e1e1e] border-[#333]' : 'bg-white border-gray-200']" >
55 <div class =" p-3 border-b" :class =" isDark ? 'border-[#333]' : 'border-gray-200'" style =" background-color : var (--app-surface-muted )" >
1717 <div v-if =" loadingAccounts" class =" flex justify-center py-4" >
1818 <span class =" text-sm" :class =" isDark ? 'text-gray-500' : 'text-gray-400'" >加载中...</span >
1919 </div >
20- <div v-else >
20+ <div v-else class = " pb-4 " >
2121 <div
2222 v-for =" item in filteredAccounts"
2323 :key =" item.username"
2424 @click =" selectAccount(item)"
2525 class =" flex items-center gap-3 px-4 py-3 cursor-pointer transition-colors border-b"
2626 :class =" [
2727 isDark ? 'border-[#333]' : 'border-gray-50',
28- selectedAccount ?.username === item.username
28+ selectedBizAccount ?.username === item.username
2929 ? (isDark ? 'bg-[#333]' : 'bg-[#E5E5E5]') // 选中状态
3030 : item.username === 'gh_3dfda90e39d6'
3131 ? (isDark ? 'bg-[#2a2a2a] hover:bg-[#333]' : 'bg-[#F2F2F2] hover:bg-[#EAEAEA]') // 微信支付专门的底色
6363 </div >
6464
6565 <div class =" flex-1 flex flex-col min-h-0 min-w-0" :class =" isDark ? 'bg-[#121212]' : 'bg-[#F5F5F5]'" >
66- <div v-if =" selectedAccount " class =" flex-1 flex flex-col min-h-0 relative" >
66+ <div v-if =" selectedBizAccount " class =" flex-1 flex flex-col min-h-0 relative" >
6767 <div class =" h-14 border-b flex items-center px-5 shrink-0 z-10" :class =" isDark ? 'bg-[#121212] border-[#333]' : 'bg-[#F5F5F5] border-gray-200'" >
68- <h2 class =" text-base" :class =" isDark ? 'text-gray-100' : 'text-gray-900'" >{{ selectedAccount .name }}</h2 >
68+ <h2 class =" text-base" :class =" isDark ? 'text-gray-100' : 'text-gray-900'" >{{ selectedBizAccount .name }}</h2 >
6969 </div >
7070
7171 <div class =" flex-1 overflow-y-auto px-4 py-6 flex flex-col-reverse" @scroll =" handleScroll" ref =" messageListRef" >
72+ <div class =" h-4 shrink-0" aria-hidden =" true" ></div >
7273 <div v-if =" !hasMore" class =" text-center text-xs py-4 w-full" :class =" isDark ? 'text-gray-500' : 'text-gray-400'" >没有更多消息了</div >
7374 <div v-if =" loadingMessages" class =" text-center text-xs py-4 w-full" :class =" isDark ? 'text-gray-500' : 'text-gray-400'" >正在加载...</div >
7475
7576 <div class =" w-full max-w-[400px] mx-auto flex flex-col-reverse gap-6" >
7677 <div v-for =" msg in messages" :key =" msg.local_id" class =" w-full" >
7778
78- <div v-if =" selectedAccount .username === 'gh_3dfda90e39d6'" class =" rounded-xl shadow-sm p-5 border" :class =" isDark ? 'bg-[#1e1e1e] border-[#333]' : 'bg-white border-gray-100'" >
79+ <div v-if =" selectedBizAccount .username === 'gh_3dfda90e39d6'" class =" rounded-xl shadow-sm p-5 border" :class =" isDark ? 'bg-[#1e1e1e] border-[#333]' : 'bg-white border-gray-100'" >
7980 <div class =" flex items-center text-sm mb-5" :class =" isDark ? 'text-gray-400' : 'text-gray-500'" >
8081 <img v-if =" msg.merchant_icon" :src =" api.getBizProxyImageUrl(msg.merchant_icon)" class =" w-6 h-6 rounded-full mr-2 object-cover" alt =" " />
8182 <div v-else class =" w-6 h-6 rounded-full mr-2 flex items-center justify-center" :class =" isDark ? 'bg-green-900/40 text-green-400' : 'bg-green-100 text-green-600'" >¥</div >
143144</template >
144145
145146<script setup>
146- import { ref , computed , onMounted } from ' vue'
147+ import { ref , computed , onMounted , watch } from ' vue'
147148
148149import { useApi } from ' ~/composables/useApi'
149150const api = useApi ()
150151
151152import { storeToRefs } from ' pinia'
152153import { useThemeStore } from ' ~/stores/theme'
154+ import { useChatAccountsStore } from ' ~/stores/chatAccounts'
155+ import { useChatRealtimeStore } from ' ~/stores/chatRealtime'
153156
154157const accounts = ref ([])
155158const loadingAccounts = ref (false )
156159const searchQuery = ref (' ' )
157- const selectedAccount = ref (null )
160+ const selectedBizAccount = ref (null )
158161
159162const themeStore = useThemeStore ()
163+ const chatAccountsStore = useChatAccountsStore ()
164+ const realtimeStore = useChatRealtimeStore ()
160165const { isDark } = storeToRefs (themeStore)
166+ const { selectedAccount: selectedDbAccount } = storeToRefs (chatAccountsStore)
167+ const { enabled: realtimeEnabled , changeSeq } = storeToRefs (realtimeStore)
168+
161169const messages = ref ([])
162170const loadingMessages = ref (false )
163171const offset = ref (0 )
164172const limit = 20
165173const hasMore = ref (true )
166174
167175const messageListRef = ref (null )
176+ let realtimeRefreshFuture = null
177+ let realtimeRefreshQueued = false
168178
169179// 默认占位图
170180// const defaultAvatar = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9IiNlNWU3ZWIiLz48L3N2Zz4='
171181const defaultImage = ' data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMTgwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjE4MCIgZmlsbD0iI2Y1ZjVmNSIvPjwvc3ZnPg=='
172182
173- const fetchAccounts = async () => {
183+ const getCurrentAccountParam = () => {
184+ const account = String (selectedDbAccount .value || ' ' ).trim ()
185+ return account || undefined
186+ }
187+
188+ const resetMessagesState = () => {
189+ messages .value = []
190+ offset .value = 0
191+ hasMore .value = true
192+ }
193+
194+ const fetchAccounts = async ({ preserveSelection = true } = {}) => {
174195 loadingAccounts .value = true
196+ const previousUsername = preserveSelection ? String (selectedBizAccount .value ? .username || ' ' ).trim () : ' '
175197 try {
176- const res = await api .listBizAccounts ()
177- if (res && res .data ) {
178- accounts .value = res .data
198+ const res = await api .listBizAccounts ({ account: getCurrentAccountParam () })
199+ const nextAccounts = Array .isArray (res? .data ) ? res .data : []
200+ accounts .value = nextAccounts
201+
202+ if (previousUsername) {
203+ selectedBizAccount .value = nextAccounts .find (item => item .username === previousUsername) || null
204+ } else if (! selectedBizAccount .value ? .username ) {
205+ selectedBizAccount .value = null
179206 }
180207 } catch (err) {
208+ accounts .value = []
209+ selectedBizAccount .value = null
181210 console .error (' 获取服务号失败:' , err)
182211 } finally {
183212 loadingAccounts .value = false
@@ -195,26 +224,29 @@ const filteredAccounts = computed(() => {
195224})
196225
197226// 点击选择服务号
198- const selectAccount = (account ) => {
199- if (selectedAccount .value ? .username === account .username ) return
200- selectedAccount .value = account
227+ const selectAccount = async (account ) => {
228+ if (selectedBizAccount .value ? .username === account .username ) return
229+ selectedBizAccount .value = account
201230
202231 // 重置消息状态
203- messages .value = []
204- offset .value = 0
205- hasMore .value = true
232+ resetMessagesState ()
206233
207- loadMessages ()
234+ await loadMessages ()
208235}
209236
210237// 加载消息
211238const loadMessages = async () => {
212- if (loadingMessages .value || ! hasMore .value || ! selectedAccount .value ) return
239+ if (loadingMessages .value || ! hasMore .value || ! selectedBizAccount .value ) return
213240
214241 loadingMessages .value = true
215242 try {
216- const username = selectedAccount .value .username
217- const params = { username, offset: offset .value , limit }
243+ const username = selectedBizAccount .value .username
244+ const params = {
245+ account: getCurrentAccountParam (),
246+ username,
247+ offset: offset .value ,
248+ limit,
249+ }
218250
219251 let res
220252 if (username === ' gh_3dfda90e39d6' ) {
@@ -238,6 +270,66 @@ const loadMessages = async () => {
238270 }
239271}
240272
273+ const reloadSelectedMessages = async () => {
274+ if (! selectedBizAccount .value ) return
275+ resetMessagesState ()
276+ await loadMessages ()
277+ }
278+
279+ const syncAllBizRealtime = async ({ forceReload = false } = {}) => {
280+ const priorityUsername = String (selectedBizAccount .value ? .username || ' ' ).trim ()
281+ if (! realtimeEnabled .value ) {
282+ if (forceReload) {
283+ await reloadSelectedMessages ()
284+ }
285+ return
286+ }
287+
288+ try {
289+ const result = await api .syncChatRealtimeAll ({
290+ account: getCurrentAccountParam (),
291+ max_scan: 200 ,
292+ priority_username: priorityUsername,
293+ priority_max_scan: 400 ,
294+ include_hidden: true ,
295+ include_official: true ,
296+ only_official: true ,
297+ backfill_limit: 0 ,
298+ })
299+ const hasDelta = Number (result? .insertedTotal || 0 ) > 0 || Number (result? .sessionsUpdated || 0 ) > 0
300+ await fetchAccounts ({ preserveSelection: true })
301+ if (selectedBizAccount .value ? .username ) {
302+ if (hasDelta || forceReload) {
303+ await reloadSelectedMessages ()
304+ }
305+ } else if (forceReload) {
306+ resetMessagesState ()
307+ }
308+ } catch (err) {
309+ console .error (' 实时同步服务号失败:' , err)
310+ if (forceReload) {
311+ await fetchAccounts ({ preserveSelection: true })
312+ await reloadSelectedMessages ()
313+ }
314+ }
315+ }
316+
317+ const queueRealtimeBizRefresh = () => {
318+ if (! realtimeEnabled .value ) return
319+ if (realtimeRefreshFuture) {
320+ realtimeRefreshQueued = true
321+ return
322+ }
323+
324+ realtimeRefreshFuture = syncAllBizRealtime ().finally (() => {
325+ realtimeRefreshFuture = null
326+ if (realtimeRefreshQueued) {
327+ realtimeRefreshQueued = false
328+ queueRealtimeBizRefresh ()
329+ }
330+ })
331+ }
332+
241333// 向上滚动加载逻辑
242334// 因为容器设置了 flex-col-reverse,所以 scrollTop 越靠近负值(或0取决于浏览器)越是到了历史消息端
243335// 但比较通用兼容的做法是监听 scroll,距离顶部或底部小于阈值时触发
@@ -250,8 +342,40 @@ const handleScroll = (e) => {
250342 }
251343}
252344
253- onMounted (() => {
254- fetchAccounts ()
345+ watch (selectedDbAccount, async (next , prev ) => {
346+ if (String (next || ' ' ).trim () === String (prev || ' ' ).trim ()) return
347+ selectedBizAccount .value = null
348+ resetMessagesState ()
349+ searchQuery .value = ' '
350+
351+ if (! String (next || ' ' ).trim ()) {
352+ accounts .value = []
353+ return
354+ }
355+ await fetchAccounts ({ preserveSelection: false })
356+ if (realtimeEnabled .value ) {
357+ await syncAllBizRealtime ({ forceReload: true })
358+ }
359+ })
360+
361+ watch (changeSeq, (next , prev ) => {
362+ if (! realtimeEnabled .value ) return
363+ if (next === prev) return
364+ queueRealtimeBizRefresh ()
365+ })
366+
367+ watch (realtimeEnabled, async (enabled , wasEnabled ) => {
368+ if (enabled && ! wasEnabled) {
369+ await syncAllBizRealtime ({ forceReload: true })
370+ }
371+ })
372+
373+ onMounted (async () => {
374+ await chatAccountsStore .ensureLoaded ()
375+ await fetchAccounts ({ preserveSelection: false })
376+ if (realtimeEnabled .value ) {
377+ await syncAllBizRealtime ({ forceReload: true })
378+ }
255379})
256380< / script>
257381
@@ -267,4 +391,4 @@ onMounted(() => {
267391 background- color: rgba (0 ,0 ,0 ,0.1 );
268392 border- radius: 10px ;
269393}
270- < / style>
394+ < / style>
0 commit comments