Skip to content

Commit 1985281

Browse files
committed
feat(chat): 增强实时读取和资源预览
- 前端聊天、会话和服务号页面默认请求 source=auto - 进入聊天/服务号页面时静默启用实时读取,离开时释放实时流 - 新增图片视频资源侧栏,支持时间分组和分页加载 - 图片预览支持上一张/下一张、缩放、旋转和计数显示 - 移除侧边栏全局实时开关,避免依赖关闭时同步快照
1 parent 4db7806 commit 1985281

11 files changed

Lines changed: 938 additions & 163 deletions

File tree

frontend/assets/css/tailwind.css

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@
11121112
@apply flex items-center gap-2 mt-2;
11131113
}
11141114

1115+
11151116
.search-session-type-row {
11161117
@apply flex items-center gap-1 mt-2;
11171118
}
@@ -1315,6 +1316,10 @@
13151316
@apply text-xs text-gray-700 line-clamp-2 leading-relaxed;
13161317
}
13171318

1319+
.sidebar-result-badge {
1320+
@apply inline-flex items-center rounded-full bg-[#03C160]/10 text-[#03C160] text-[10px] px-1.5 py-0.5 mr-1 align-middle;
1321+
}
1322+
13181323
.sidebar-load-more {
13191324
@apply px-4 py-3;
13201325
}
@@ -1548,14 +1553,16 @@
15481553
}
15491554

15501555
.search-sidebar,
1551-
.time-sidebar {
1556+
.time-sidebar,
1557+
.resource-sidebar.resource-sidebar {
15521558
background-color: var(--search-panel-bg);
15531559
border-left-color: var(--search-panel-border);
15541560
color: var(--search-panel-text);
15551561
}
15561562

15571563
.search-sidebar-header,
1558-
.time-sidebar-header {
1564+
.time-sidebar-header,
1565+
.resource-sidebar .resource-sidebar-header {
15591566
background-color: var(--search-panel-header-bg);
15601567
border-bottom-color: var(--search-panel-border);
15611568
}
@@ -1599,10 +1606,104 @@
15991606
}
16001607

16011608
.search-sidebar-results,
1602-
.time-sidebar-body {
1609+
.time-sidebar-body,
1610+
.resource-sidebar-results {
16031611
background-color: var(--search-panel-bg);
16041612
}
16051613

1614+
.resource-sidebar .resource-sidebar-title {
1615+
color: var(--search-panel-text);
1616+
}
1617+
1618+
.resource-sidebar .resource-sidebar-icon {
1619+
color: var(--app-accent);
1620+
}
1621+
1622+
.resource-sidebar .resource-sidebar-muted,
1623+
.resource-sidebar .resource-status,
1624+
.resource-sidebar .resource-empty,
1625+
.resource-sidebar .resource-group-divider {
1626+
color: var(--search-panel-muted);
1627+
}
1628+
1629+
.resource-sidebar .resource-sidebar-select {
1630+
background-color: var(--search-input-bg);
1631+
border-color: var(--search-input-border);
1632+
color: var(--search-panel-text);
1633+
color-scheme: light;
1634+
}
1635+
1636+
html[data-theme='dark'] .resource-sidebar .resource-sidebar-select {
1637+
color-scheme: dark;
1638+
}
1639+
1640+
.resource-sidebar .resource-sidebar-select option {
1641+
background-color: var(--search-input-bg);
1642+
color: var(--search-panel-text);
1643+
}
1644+
1645+
.resource-sidebar .resource-sidebar-close {
1646+
color: var(--search-panel-muted);
1647+
}
1648+
1649+
.resource-sidebar .resource-sidebar-close:hover {
1650+
background-color: var(--search-panel-soft);
1651+
color: var(--search-panel-text);
1652+
}
1653+
1654+
.resource-sidebar .resource-group-divider {
1655+
background-color: color-mix(in srgb, var(--search-panel-bg) 94%, transparent);
1656+
}
1657+
1658+
.resource-sidebar .resource-card {
1659+
background-color: var(--search-panel-soft);
1660+
border-color: var(--search-panel-border);
1661+
color: var(--search-panel-text);
1662+
contain: layout paint style;
1663+
}
1664+
1665+
.resource-card-normal,
1666+
.resource-card-compact {
1667+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
1668+
}
1669+
1670+
.resource-card-normal:hover,
1671+
.resource-card-compact:hover {
1672+
border-color: rgba(3, 193, 96, 0.6);
1673+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
1674+
}
1675+
1676+
.resource-card-year {
1677+
transition: none;
1678+
}
1679+
1680+
.resource-load-more {
1681+
background-color: var(--search-input-bg);
1682+
border-color: var(--search-panel-border);
1683+
color: var(--search-panel-text);
1684+
}
1685+
1686+
.resource-load-more:hover {
1687+
background-color: var(--search-panel-soft);
1688+
border-color: rgba(3, 193, 96, 0.55);
1689+
color: var(--search-panel-text);
1690+
}
1691+
1692+
.resource-sidebar-error {
1693+
background-color: rgba(239, 68, 68, 0.08);
1694+
border-color: rgba(239, 68, 68, 0.25);
1695+
color: #dc2626;
1696+
}
1697+
1698+
html[data-theme='dark'] .resource-card-normal:hover,
1699+
html[data-theme='dark'] .resource-card-compact:hover {
1700+
box-shadow: none;
1701+
}
1702+
1703+
html[data-theme='dark'] .resource-sidebar-error {
1704+
color: #fca5a5;
1705+
}
1706+
16061707
.chat-overlay-dropdown,
16071708
.chat-context-menu,
16081709
.chat-floating-window,

frontend/components/BizMessages.vue

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
</template>
145145

146146
<script setup>
147-
import { ref, computed, onMounted, watch } from 'vue'
147+
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
148148
149149
import { useApi } from '~/composables/useApi'
150150
const api = useApi()
@@ -170,6 +170,7 @@ const messages = ref([])
170170
const loadingMessages = ref(false)
171171
const offset = ref(0)
172172
const limit = 20
173+
const DEFAULT_BIZ_SOURCE = 'auto'
173174
const hasMore = ref(true)
174175
175176
const messageListRef = ref(null)
@@ -195,7 +196,7 @@ const fetchAccounts = async ({ preserveSelection = true } = {}) => {
195196
loadingAccounts.value = true
196197
const previousUsername = preserveSelection ? String(selectedBizAccount.value?.username || '').trim() : ''
197198
try {
198-
const res = await api.listBizAccounts({ account: getCurrentAccountParam() })
199+
const res = await api.listBizAccounts({ account: getCurrentAccountParam(), source: DEFAULT_BIZ_SOURCE })
199200
const nextAccounts = Array.isArray(res?.data) ? res.data : []
200201
accounts.value = nextAccounts
201202
@@ -246,6 +247,7 @@ const loadMessages = async () => {
246247
username,
247248
offset: offset.value,
248249
limit,
250+
source: DEFAULT_BIZ_SOURCE,
249251
}
250252
251253
let res
@@ -277,36 +279,17 @@ const reloadSelectedMessages = async () => {
277279
}
278280
279281
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-
282+
// 服务号页面现在默认直接读取 WCDB realtime(source=auto),不再依赖先同步到本地 output 库。
283+
// 因此收到 db_storage 变化后直接刷新服务号列表和当前服务号消息,避免 sync_all 没有插入本地库时漏刷。
288284
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
300285
await fetchAccounts({ preserveSelection: true })
301286
if (selectedBizAccount.value?.username) {
302-
if (hasDelta || forceReload) {
303-
await reloadSelectedMessages()
304-
}
287+
await reloadSelectedMessages()
305288
} else if (forceReload) {
306289
resetMessagesState()
307290
}
308291
} catch (err) {
309-
console.error('实时同步服务号失败:', err)
292+
console.error('实时刷新服务号失败:', err)
310293
if (forceReload) {
311294
await fetchAccounts({ preserveSelection: true })
312295
await reloadSelectedMessages()
@@ -344,6 +327,7 @@ const handleScroll = (e) => {
344327
345328
watch(selectedDbAccount, async (next, prev) => {
346329
if (String(next || '').trim() === String(prev || '').trim()) return
330+
await realtimeStore.enable({ silent: true })
347331
selectedBizAccount.value = null
348332
resetMessagesState()
349333
searchQuery.value = ''
@@ -372,11 +356,16 @@ watch(realtimeEnabled, async (enabled, wasEnabled) => {
372356
373357
onMounted(async () => {
374358
await chatAccountsStore.ensureLoaded()
359+
await realtimeStore.enable({ silent: true })
375360
await fetchAccounts({ preserveSelection: false })
376361
if (realtimeEnabled.value) {
377362
await syncAllBizRealtime({ forceReload: true })
378363
}
379364
})
365+
366+
onUnmounted(() => {
367+
void realtimeStore.disable({ silent: true })
368+
})
380369
</script>
381370
382371
<style scoped>

frontend/components/SidebarRail.vue

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -171,30 +171,6 @@
171171
</div>
172172
</div>
173173

174-
<!-- Realtime -->
175-
<div
176-
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group"
177-
:class="realtimeBusy ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
178-
:title="realtimeTitle"
179-
@click="toggleRealtime"
180-
>
181-
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
182-
<svg
183-
class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
184-
:class="{ 'sidebar-rail-icon-active': realtimeEnabled }"
185-
viewBox="0 0 24 24"
186-
fill="none"
187-
stroke="currentColor"
188-
stroke-width="1.7"
189-
stroke-linecap="round"
190-
stroke-linejoin="round"
191-
aria-hidden="true"
192-
>
193-
<path d="M13 2L4 14h7l-1 8 9-12h-7z" />
194-
</svg>
195-
</div>
196-
</div>
197-
198174
<!-- ImgHelper (Auto download large images) -->
199175
<div
200176
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group"
@@ -394,7 +370,6 @@
394370
<script setup>
395371
import { storeToRefs } from 'pinia'
396372
import { useChatAccountsStore } from '~/stores/chatAccounts'
397-
import { useChatRealtimeStore } from '~/stores/chatRealtime'
398373
import { useImgHelperStore } from '~/stores/imgHelper'
399374
import { usePrivacyStore } from '~/stores/privacy'
400375
import { useThemeStore } from '~/stores/theme'
@@ -410,9 +385,6 @@ const { privacyMode } = storeToRefs(privacyStore)
410385
const themeStore = useThemeStore()
411386
themeStore.init()
412387
413-
const realtimeStore = useChatRealtimeStore()
414-
const { enabled: realtimeEnabled, available: realtimeAvailable, checking: realtimeChecking, statusError: realtimeStatusError, toggling: realtimeToggling } = storeToRefs(realtimeStore)
415-
416388
const imgHelperStore = useImgHelperStore()
417389
const { enabled: imgHelperEnabled, checking: imgHelperChecking, toggling: imgHelperToggling, error: imgHelperError } = storeToRefs(imgHelperStore)
418390
@@ -640,19 +612,6 @@ const deleteCurrentAccountData = async () => {
640612
}
641613
}
642614
643-
const realtimeBusy = computed(() => !!realtimeChecking.value || !!realtimeToggling.value)
644-
645-
const realtimeTitle = computed(() => {
646-
if (realtimeEnabled.value) return '关闭实时更新(全局)'
647-
if (realtimeAvailable.value) return '开启实时更新(全局)'
648-
return realtimeStatusError.value || '实时模式不可用'
649-
})
650-
651-
const toggleRealtime = async () => {
652-
if (realtimeBusy.value) return
653-
await realtimeStore.toggle({ silent: false })
654-
}
655-
656615
const imgHelperBusy = computed(() => !!imgHelperChecking.value || !!imgHelperToggling.value)
657616
658617
const imgHelperTitle = computed(() => {

0 commit comments

Comments
 (0)