Skip to content

Commit 098d9cd

Browse files
committed
feat(sidebar): 新增账号弹窗并将引导/设置入口下移到底部
头像点击展示当前账号信息弹窗 增加仅删除本项目数据提示与删除后返回引导页 接口异常(如 404)时自动走桌面 IPC 兜底 将引导页和设置图标移动到底部
1 parent 22c12da commit 098d9cd

File tree

1 file changed

+299
-16
lines changed

1 file changed

+299
-16
lines changed

frontend/components/SidebarRail.vue

Lines changed: 299 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
<div class="flex-1 flex flex-col justify-start pt-0 gap-0">
77
<!-- Avatar -->
88
<div class="w-full h-[60px] flex items-center justify-center">
9-
<div class="w-[40px] h-[40px] rounded-md overflow-hidden bg-gray-300 flex-shrink-0">
9+
<button
10+
type="button"
11+
class="group relative w-[40px] h-[40px] rounded-md overflow-hidden bg-gray-300 flex-shrink-0 ring-1 ring-transparent transition hover:ring-[#07b75b]/40"
12+
title="账号信息"
13+
@click="openAccountDialog"
14+
>
1015
<img v-if="selfAvatarUrl" :src="selfAvatarUrl" alt="avatar" class="w-full h-full object-cover" />
1116
<div
1217
v-else
@@ -15,7 +20,7 @@
1520
>
1621
1722
</div>
18-
</div>
23+
</button>
1924
</div>
2025

2126
<!-- Chat -->
@@ -164,22 +169,116 @@
164169
</div>
165170
</div>
166171

167-
<!-- Settings -->
168-
<div
169-
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
170-
@click="goSettings"
171-
title="设置"
172-
>
173-
<div class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]">
174-
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="settingsDialogOpen ? 'text-[#07b75b]' : 'text-[#5d5d5d]'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
175-
<path
176-
stroke-linecap="round"
177-
stroke-linejoin="round"
178-
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
179-
/>
180-
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
172+
<div class="mt-auto">
173+
<!-- Guide -->
174+
<div
175+
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
176+
title="引导页"
177+
@click="goGuide"
178+
>
179+
<div class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]">
180+
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)] text-[#5d5d5d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
181+
<path d="M3 10.5L12 3l9 7.5" />
182+
<path d="M5 9.5V20h14V9.5" />
183+
<path d="M10 20v-6h4v6" />
184+
</svg>
185+
</div>
186+
</div>
187+
188+
<!-- Settings -->
189+
<div
190+
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
191+
@click="goSettings"
192+
title="设置"
193+
>
194+
<div class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]">
195+
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="settingsDialogOpen ? 'text-[#07b75b]' : 'text-[#5d5d5d]'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
196+
<path
197+
stroke-linecap="round"
198+
stroke-linejoin="round"
199+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
200+
/>
201+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
202+
</svg>
203+
</div>
204+
</div>
205+
</div>
206+
</div>
207+
</div>
208+
209+
<div
210+
v-if="accountDialogOpen"
211+
class="fixed inset-0 z-[130] flex items-center justify-center bg-black/35 px-4"
212+
@click.self="closeAccountDialog"
213+
>
214+
<div class="w-full max-w-[440px] overflow-hidden rounded-[12px] border border-[#e7e7e7] bg-white shadow-2xl">
215+
<div class="flex items-center justify-between border-b border-[#efefef] px-4 py-3">
216+
<div class="text-[14px] font-semibold text-[#222]">当前账号信息</div>
217+
<button
218+
type="button"
219+
class="flex h-7 w-7 items-center justify-center rounded-md text-[#888] transition hover:bg-[#f2f2f2] hover:text-[#222]"
220+
title="关闭"
221+
:disabled="accountDeleteLoading"
222+
@click="closeAccountDialog"
223+
>
224+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
225+
<path d="M6 6l12 12M18 6L6 18" />
181226
</svg>
227+
</button>
228+
</div>
229+
230+
<div class="space-y-3 px-4 py-4">
231+
<div v-if="accountInfoLoading" class="text-[12px] text-[#7a7a7a]">正在加载账号信息...</div>
232+
<template v-else>
233+
<div class="flex items-center gap-3">
234+
<div class="w-[42px] h-[42px] rounded-md overflow-hidden bg-gray-300 flex-shrink-0">
235+
<img v-if="selfAvatarUrl" :src="selfAvatarUrl" alt="avatar" class="w-full h-full object-cover" />
236+
<div
237+
v-else
238+
class="w-full h-full flex items-center justify-center text-white text-xs font-bold"
239+
:style="{ backgroundColor: '#4B5563' }"
240+
>
241+
242+
</div>
243+
</div>
244+
<div class="min-w-0 flex-1">
245+
<div class="truncate text-[14px] font-semibold text-[#222]">{{ selectedAccount || '未选择账号' }}</div>
246+
<div class="mt-0.5 text-[11px] text-[#8a8a8a]">账号标识(wxid)</div>
247+
</div>
248+
</div>
249+
250+
<div class="rounded-[8px] border border-[#ededed] bg-[#fafafa] px-3 py-2 text-[12px] text-[#5f5f5f] space-y-1.5">
251+
<div class="flex items-start justify-between gap-3">
252+
<span class="text-[#8a8a8a] shrink-0">数据库数量</span>
253+
<span class="font-medium text-[#333]">{{ accountInfo?.database_count ?? '—' }}</span>
254+
</div>
255+
<div class="flex items-start justify-between gap-3">
256+
<span class="text-[#8a8a8a] shrink-0">数据目录</span>
257+
<span class="break-all text-right text-[#444]">{{ accountInfo?.path || (selectedAccount ? `output/databases/${selectedAccount}` : '—') }}</span>
258+
</div>
259+
<div class="flex items-start justify-between gap-3">
260+
<span class="text-[#8a8a8a] shrink-0">最近会话库更新时间</span>
261+
<span class="text-[#444]">{{ sessionUpdatedAtText }}</span>
262+
</div>
263+
</div>
264+
</template>
265+
266+
<div class="rounded-[8px] border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] leading-relaxed text-amber-900">
267+
仅删除本项目中的该账号解析数据/缓存/编辑记录,不会删除微信客户端中的任何聊天内容或账号数据。
182268
</div>
269+
270+
<button
271+
type="button"
272+
class="w-full rounded-[8px] border border-red-200 bg-red-50 px-3 py-2 text-[12px] font-medium text-red-700 transition hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50"
273+
:disabled="!selectedAccount || accountDeleteLoading"
274+
@click="deleteCurrentAccountData"
275+
>
276+
{{ accountDeleteLoading ? '删除中...' : '删除当前账号的项目数据' }}
277+
</button>
278+
<div class="text-[11px] text-[#8a8a8a]">删除成功后将自动返回引导页。</div>
279+
280+
<div v-if="accountInfoError" class="text-[11px] text-red-600 whitespace-pre-wrap">{{ accountInfoError }}</div>
281+
<div v-if="accountDeleteError" class="text-[11px] text-red-600 whitespace-pre-wrap">{{ accountDeleteError }}</div>
183282
</div>
184283
</div>
185284
</div>
@@ -202,9 +301,130 @@ const { privacyMode } = storeToRefs(privacyStore)
202301
const realtimeStore = useChatRealtimeStore()
203302
const { enabled: realtimeEnabled, available: realtimeAvailable, checking: realtimeChecking, statusError: realtimeStatusError, toggling: realtimeToggling } = storeToRefs(realtimeStore)
204303
const { open: settingsDialogOpen, openDialog: openSettingsDialog } = useSettingsDialog()
304+
const { getChatAccountInfo, deleteChatAccount } = useApi()
305+
306+
const accountDialogOpen = ref(false)
307+
const accountInfoLoading = ref(false)
308+
const accountInfoError = ref('')
309+
const accountInfo = ref(null)
310+
const accountDeleteLoading = ref(false)
311+
const accountDeleteError = ref('')
312+
const accountInfoApiUnsupported = ref(false)
313+
const deleteAccountApiUnsupported = ref(false)
314+
315+
const sessionUpdatedAtText = computed(() => {
316+
const ts = Number(accountInfo.value?.session_updated_at || 0)
317+
if (!Number.isFinite(ts) || ts <= 0) return ''
318+
try {
319+
return new Date(ts * 1000).toLocaleString('zh-CN')
320+
} catch {
321+
return ''
322+
}
323+
})
324+
325+
const isNotFoundError = (error) => {
326+
const status = Number(
327+
error?.statusCode
328+
?? error?.status
329+
?? error?.response?.status
330+
?? error?.data?.statusCode
331+
?? 0
332+
)
333+
return status === 404
334+
}
335+
336+
const loadAccountInfoByDesktopBridge = async (account) => {
337+
if (!process.client || typeof window === 'undefined') return null
338+
if (!window.wechatDesktop?.getAccountInfo) return null
339+
const res = await window.wechatDesktop.getAccountInfo(account)
340+
return res && typeof res === 'object' ? res : null
341+
}
342+
343+
const loadAccountInfo = async () => {
344+
accountInfoLoading.value = true
345+
accountInfoError.value = ''
346+
const account = String(selectedAccount.value || '').trim()
347+
if (!account) {
348+
accountInfo.value = null
349+
accountInfoLoading.value = false
350+
return
351+
}
352+
try {
353+
let lastError = null
354+
if (!accountInfoApiUnsupported.value) {
355+
try {
356+
const res = await getChatAccountInfo({ account })
357+
if (res?.status !== 'success') {
358+
throw new Error(res?.message || '读取账号信息失败')
359+
}
360+
accountInfo.value = res
361+
return
362+
} catch (e) {
363+
lastError = e
364+
if (isNotFoundError(e)) {
365+
accountInfoApiUnsupported.value = true
366+
}
367+
}
368+
}
369+
370+
try {
371+
const fallback = await loadAccountInfoByDesktopBridge(account)
372+
if (fallback?.status === 'success') {
373+
accountInfo.value = fallback
374+
accountInfoError.value = ''
375+
return
376+
}
377+
if (fallback && fallback?.status && fallback.status !== 'success') {
378+
lastError = new Error(fallback?.message || '读取账号信息失败')
379+
} else if (!lastError) {
380+
lastError = new Error('读取账号信息失败')
381+
}
382+
} catch (fallbackErr) {
383+
if (!lastError) {
384+
lastError = fallbackErr
385+
}
386+
}
387+
388+
accountInfo.value = null
389+
accountInfoError.value = lastError?.message || '读取账号信息失败'
390+
} finally {
391+
accountInfoLoading.value = false
392+
}
393+
}
394+
395+
const deleteAccountDataByDesktopBridge = async (account) => {
396+
if (!process.client || typeof window === 'undefined') return null
397+
if (!window.wechatDesktop?.deleteAccountData) return null
398+
const res = await window.wechatDesktop.deleteAccountData(account)
399+
return res && typeof res === 'object' ? res : { status: 'success' }
400+
}
401+
402+
const openAccountDialog = async () => {
403+
accountDialogOpen.value = true
404+
accountDeleteError.value = ''
405+
await loadAccountInfo()
406+
}
407+
408+
const closeAccountDialog = () => {
409+
if (accountDeleteLoading.value) return
410+
accountDialogOpen.value = false
411+
}
412+
413+
watch(selectedAccount, () => {
414+
if (!accountDialogOpen.value) return
415+
void loadAccountInfo()
416+
})
205417
206418
onMounted(async () => {
207419
await chatAccounts.ensureLoaded()
420+
if (process.client && typeof window !== 'undefined') {
421+
window.addEventListener('keydown', onWindowKeydown)
422+
}
423+
})
424+
425+
onBeforeUnmount(() => {
426+
if (!process.client || typeof window === 'undefined') return
427+
window.removeEventListener('keydown', onWindowKeydown)
208428
})
209429
210430
const apiBase = useApiBase()
@@ -240,10 +460,73 @@ const goWrapped = async () => {
240460
await navigateTo('/wrapped')
241461
}
242462
463+
const goGuide = async () => {
464+
await navigateTo('/')
465+
}
466+
243467
const goSettings = () => {
244468
openSettingsDialog()
245469
}
246470
471+
const onWindowKeydown = (event) => {
472+
if (event?.key !== 'Escape') return
473+
if (!accountDialogOpen.value) return
474+
event.preventDefault()
475+
closeAccountDialog()
476+
}
477+
478+
const deleteCurrentAccountData = async () => {
479+
const account = String(selectedAccount.value || '').trim()
480+
if (!account || accountDeleteLoading.value) return
481+
482+
if (process.client && typeof window !== 'undefined') {
483+
const confirmed = window.confirm(
484+
'将删除当前账号在本项目中的数据(解析缓存、编辑记录、导出缓存等),不会删除微信客户端内容。确认删除吗?'
485+
)
486+
if (!confirmed) return
487+
}
488+
489+
accountDeleteLoading.value = true
490+
accountDeleteError.value = ''
491+
try {
492+
let deleted = false
493+
let lastError = null
494+
495+
if (!deleteAccountApiUnsupported.value) {
496+
try {
497+
const apiRes = await deleteChatAccount({ account })
498+
if (apiRes?.status && apiRes.status !== 'success') {
499+
throw new Error(apiRes?.message || '删除账号数据失败')
500+
}
501+
deleted = true
502+
} catch (apiErr) {
503+
lastError = apiErr
504+
if (isNotFoundError(apiErr)) {
505+
deleteAccountApiUnsupported.value = true
506+
}
507+
}
508+
}
509+
510+
if (!deleted) {
511+
const desktopRes = await deleteAccountDataByDesktopBridge(account)
512+
if (!desktopRes) {
513+
throw lastError || new Error('删除账号数据失败')
514+
}
515+
if (desktopRes?.status && desktopRes.status !== 'success') {
516+
throw new Error(desktopRes?.message || '删除账号数据失败')
517+
}
518+
}
519+
520+
accountDialogOpen.value = false
521+
await chatAccounts.ensureLoaded({ force: true })
522+
await navigateTo('/')
523+
} catch (e) {
524+
accountDeleteError.value = e?.message || '删除账号数据失败'
525+
} finally {
526+
accountDeleteLoading.value = false
527+
}
528+
}
529+
247530
const realtimeBusy = computed(() => !!realtimeChecking.value || !!realtimeToggling.value)
248531
249532
const realtimeTitle = computed(() => {

0 commit comments

Comments
 (0)