Skip to content

Commit 950fb4c

Browse files
committed
improvement(chat): 会话列表可拖拽调宽并优化 realtime 关闭同步
- 中间栏新增拖拽调宽/双击重置;宽度按物理 px 持久化(兼容旧 key,并按 dpr 换算) - 关闭 realtime 前触发 syncChatRealtimeMessages(max_scan=5000),避免回退到过期解密快照 - 按 dpr 调整联系人/消息头像与 skeleton 尺寸
1 parent 891d4b8 commit 950fb4c

1 file changed

Lines changed: 188 additions & 7 deletions

File tree

frontend/pages/chat/[[username]].vue

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,18 @@
9999
</div>
100100

101101
<!-- 中间列表区域 -->
102-
<div class="w-80 border-r border-gray-200 flex flex-col min-h-0" style="background-color: #F7F7F7">
102+
<div
103+
class="session-list-panel border-r border-gray-200 flex flex-col min-h-0 shrink-0 relative"
104+
:style="{ backgroundColor: '#F7F7F7', '--session-list-width': sessionListWidth + 'px' }"
105+
>
106+
<!-- 拖动调整会话列表宽度 -->
107+
<div
108+
class="session-list-resizer"
109+
:class="{ 'session-list-resizer-active': sessionListResizing }"
110+
title="拖动调整会话列表宽度"
111+
@pointerdown="onSessionListResizerPointerDown"
112+
@dblclick="resetSessionListWidth"
113+
/>
103114
<!-- 聊天列表 -->
104115
<div class="h-full flex flex-col min-h-0">
105116
<!-- 搜索栏 -->
@@ -143,8 +154,8 @@
143154
<!-- 联系人列表 -->
144155
<div class="flex-1 overflow-y-auto min-h-0">
145156
<div v-if="isLoadingContacts" class="px-3 py-4 h-full overflow-hidden">
146-
<div v-for="i in 15" :key="i" class="flex items-center space-x-3 py-2">
147-
<div class="w-10 h-10 rounded-md bg-gray-200 skeleton-pulse"></div>
157+
<div v-for="i in 15" :key="i" class="flex items-center space-x-3 h-[calc(85px/var(--dpr))]">
158+
<div class="w-[calc(45px/var(--dpr))] h-[calc(45px/var(--dpr))] rounded-md bg-gray-200 skeleton-pulse"></div>
148159
<div class="flex-1 space-y-2">
149160
<div class="h-3.5 bg-gray-200 rounded skeleton-pulse" :style="{ width: (60 + (i % 4) * 15) + 'px' }"></div>
150161
<div class="h-3 bg-gray-200 rounded skeleton-pulse" :style="{ width: (80 + (i % 3) * 20) + 'px' }"></div>
@@ -159,12 +170,12 @@
159170
</div>
160171
<template v-else>
161172
<div v-for="contact in filteredContacts" :key="contact.id"
162-
class="px-3 py-2 cursor-pointer transition-colors duration-150 border-b border-gray-100"
173+
class="px-3 cursor-pointer transition-colors duration-150 border-b border-gray-100 h-[calc(85px/var(--dpr))] flex items-center"
163174
:class="selectedContact?.id === contact.id ? 'bg-[#DEDEDE] hover:bg-[#d3d3d3]' : 'hover:bg-[#eaeaea]'"
164175
@click="selectContact(contact)">
165-
<div class="flex items-center space-x-3">
176+
<div class="flex items-center space-x-3 w-full">
166177
<!-- 联系人头像 -->
167-
<div class="w-10 h-10 rounded-md overflow-hidden bg-gray-300 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
178+
<div class="w-[calc(45px/var(--dpr))] h-[calc(45px/var(--dpr))] rounded-md overflow-hidden bg-gray-300 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
168179
<div v-if="contact.avatar" class="w-full h-full">
169180
<img :src="contact.avatar" :alt="contact.name" class="w-full h-full object-cover">
170181
</div>
@@ -340,7 +351,7 @@
340351
<div v-else class="flex items-center" :class="message.isSent ? 'justify-end' : 'justify-start'">
341352
<div class="flex items-start max-w-md" :class="message.isSent ? 'flex-row-reverse' : ''">
342353
<!-- 消息发送者头像 -->
343-
<div class="w-[36px] h-[36px] rounded-md overflow-hidden bg-gray-300 flex-shrink-0" :class="[message.isSent ? 'ml-3' : 'mr-3', { 'privacy-blur': privacyMode }]">
354+
<div class="w-[calc(42px/var(--dpr))] h-[calc(42px/var(--dpr))] rounded-md overflow-hidden bg-gray-300 flex-shrink-0" :class="[message.isSent ? 'ml-3' : 'mr-3', { 'privacy-blur': privacyMode }]">
344355
<div v-if="message.avatar" class="w-full h-full">
345356
<img
346357
:src="message.avatar"
@@ -1802,6 +1813,129 @@ watch(
18021813
}
18031814
)
18041815

1816+
// 会话列表(中间栏)宽度(按物理像素 px 配置):默认 295px,支持拖动调整并持久化
1817+
const SESSION_LIST_WIDTH_KEY = 'ui.chat.session_list_width_physical'
1818+
const SESSION_LIST_WIDTH_KEY_LEGACY = 'ui.chat.session_list_width'
1819+
const SESSION_LIST_WIDTH_DEFAULT = 295
1820+
const SESSION_LIST_WIDTH_MIN = 220
1821+
const SESSION_LIST_WIDTH_MAX = 520
1822+
1823+
const sessionListWidth = ref(SESSION_LIST_WIDTH_DEFAULT)
1824+
const sessionListResizing = ref(false)
1825+
1826+
let sessionListResizeStartX = 0
1827+
let sessionListResizeStartWidth = SESSION_LIST_WIDTH_DEFAULT
1828+
let sessionListResizeStartDpr = 1
1829+
let sessionListResizePrevCursor = ''
1830+
let sessionListResizePrevUserSelect = ''
1831+
1832+
const clampSessionListWidth = (n) => {
1833+
const v = Number.isFinite(n) ? n : SESSION_LIST_WIDTH_DEFAULT
1834+
return Math.min(SESSION_LIST_WIDTH_MAX, Math.max(SESSION_LIST_WIDTH_MIN, Math.round(v)))
1835+
}
1836+
1837+
const loadSessionListWidth = () => {
1838+
if (!process.client) return
1839+
try {
1840+
const raw = localStorage.getItem(SESSION_LIST_WIDTH_KEY)
1841+
const v = parseInt(String(raw || ''), 10)
1842+
if (!Number.isNaN(v)) {
1843+
sessionListWidth.value = clampSessionListWidth(v)
1844+
return
1845+
}
1846+
1847+
// Legacy: value was stored as CSS px. Convert to physical px using current dpr.
1848+
const legacy = localStorage.getItem(SESSION_LIST_WIDTH_KEY_LEGACY)
1849+
const legacyV = parseInt(String(legacy || ''), 10)
1850+
if (!Number.isNaN(legacyV)) {
1851+
const dpr = window.devicePixelRatio || 1
1852+
const converted = clampSessionListWidth(legacyV * dpr)
1853+
sessionListWidth.value = converted
1854+
try {
1855+
localStorage.setItem(SESSION_LIST_WIDTH_KEY, String(converted))
1856+
localStorage.removeItem(SESSION_LIST_WIDTH_KEY_LEGACY)
1857+
} catch {}
1858+
}
1859+
} catch {}
1860+
}
1861+
1862+
const saveSessionListWidth = () => {
1863+
if (!process.client) return
1864+
try {
1865+
localStorage.setItem(SESSION_LIST_WIDTH_KEY, String(clampSessionListWidth(sessionListWidth.value)))
1866+
} catch {}
1867+
}
1868+
1869+
const setSessionListResizingActive = (active) => {
1870+
if (!process.client) return
1871+
try {
1872+
const body = document.body
1873+
if (!body) return
1874+
if (active) {
1875+
sessionListResizePrevCursor = body.style.cursor || ''
1876+
sessionListResizePrevUserSelect = body.style.userSelect || ''
1877+
body.style.cursor = 'col-resize'
1878+
body.style.userSelect = 'none'
1879+
} else {
1880+
body.style.cursor = sessionListResizePrevCursor
1881+
body.style.userSelect = sessionListResizePrevUserSelect
1882+
sessionListResizePrevCursor = ''
1883+
sessionListResizePrevUserSelect = ''
1884+
}
1885+
} catch {}
1886+
}
1887+
1888+
const onSessionListResizerPointerMove = (ev) => {
1889+
if (!sessionListResizing.value) return
1890+
const clientX = Number(ev?.clientX || 0)
1891+
// `clientX` delta is in CSS px. We store width as physical px, so multiply by dpr.
1892+
sessionListWidth.value = clampSessionListWidth(
1893+
sessionListResizeStartWidth + (clientX - sessionListResizeStartX) * (sessionListResizeStartDpr || 1)
1894+
)
1895+
}
1896+
1897+
const stopSessionListResize = () => {
1898+
if (!process.client) return
1899+
if (!sessionListResizing.value) return
1900+
sessionListResizing.value = false
1901+
setSessionListResizingActive(false)
1902+
try {
1903+
window.removeEventListener('pointermove', onSessionListResizerPointerMove)
1904+
} catch {}
1905+
saveSessionListWidth()
1906+
}
1907+
1908+
const onSessionListResizerPointerUp = () => {
1909+
stopSessionListResize()
1910+
}
1911+
1912+
const onSessionListResizerPointerDown = (ev) => {
1913+
if (!process.client) return
1914+
try {
1915+
ev?.preventDefault?.()
1916+
} catch {}
1917+
1918+
sessionListResizing.value = true
1919+
sessionListResizeStartX = Number(ev?.clientX || 0)
1920+
sessionListResizeStartWidth = Number(sessionListWidth.value || SESSION_LIST_WIDTH_DEFAULT)
1921+
sessionListResizeStartDpr = window.devicePixelRatio || 1
1922+
setSessionListResizingActive(true)
1923+
1924+
try {
1925+
window.addEventListener('pointermove', onSessionListResizerPointerMove)
1926+
window.addEventListener('pointerup', onSessionListResizerPointerUp, { once: true })
1927+
} catch {}
1928+
}
1929+
1930+
const resetSessionListWidth = () => {
1931+
sessionListWidth.value = SESSION_LIST_WIDTH_DEFAULT
1932+
saveSessionListWidth()
1933+
}
1934+
1935+
onMounted(() => {
1936+
loadSessionListWidth()
1937+
})
1938+
18051939
// 桌面端设置(仅 Electron 环境可见)
18061940
const isDesktopEnv = ref(false)
18071941
const desktopSettingsOpen = ref(false)
@@ -4677,6 +4811,7 @@ onUnmounted(() => {
46774811
if (!process.client) return
46784812
document.removeEventListener('click', onGlobalClick)
46794813
document.removeEventListener('keydown', onGlobalKeyDown)
4814+
stopSessionListResize()
46804815
if (messageSearchDebounceTimer) clearTimeout(messageSearchDebounceTimer)
46814816
messageSearchDebounceTimer = null
46824817
if (highlightMessageTimer) clearTimeout(highlightMessageTimer)
@@ -4976,8 +5111,22 @@ const toggleRealtime = async (opts = {}) => {
49765111
return true
49775112
}
49785113

5114+
// Turning off realtime: sync the latest WCDB rows into the decrypted sqlite DB first,
5115+
// otherwise the UI will fall back to an outdated decrypted snapshot.
49795116
realtimeEnabled.value = false
49805117
stopRealtimeStream()
5118+
try {
5119+
const api = useApi()
5120+
const u = String(selectedContact.value?.username || '').trim()
5121+
if (u) {
5122+
// Use a larger scan window on shutdown to reduce the chance of missing a backlog.
5123+
await api.syncChatRealtimeMessages({
5124+
account: selectedAccount.value,
5125+
username: u,
5126+
max_scan: 5000
5127+
})
5128+
}
5129+
} catch {}
49815130
await refreshSessionsForSelectedAccount({ sourceOverride: '' })
49825131
if (selectedContact.value?.username) {
49835132
await refreshSelectedMessages()
@@ -5246,6 +5395,38 @@ const LinkCard = defineComponent({
52465395
background: #a1a1a1;
52475396
}
52485397

5398+
/* 会话列表宽度:按物理像素(px)配置,按 dpr 换算为 CSS px */
5399+
.session-list-panel {
5400+
width: calc(var(--session-list-width, 295px) / var(--dpr));
5401+
}
5402+
5403+
/* 会话列表拖动条(中间栏右侧) */
5404+
.session-list-resizer {
5405+
position: absolute;
5406+
top: 0;
5407+
right: -3px; /* 覆盖在 border 上,便于拖动 */
5408+
width: 6px;
5409+
height: 100%;
5410+
cursor: col-resize;
5411+
z-index: 50;
5412+
}
5413+
5414+
.session-list-resizer::after {
5415+
content: '';
5416+
position: absolute;
5417+
top: 0;
5418+
bottom: 0;
5419+
left: 2px;
5420+
width: 2px;
5421+
background: transparent;
5422+
transition: background-color 0.15s ease;
5423+
}
5424+
5425+
.session-list-resizer:hover::after,
5426+
.session-list-resizer-active::after {
5427+
background: rgba(0, 0, 0, 0.12);
5428+
}
5429+
52495430
/* 消息气泡样式 */
52505431
.message-bubble {
52515432
border-radius: var(--message-radius);

0 commit comments

Comments
 (0)