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
1520 >
1621 我
1722 </div >
18- </div >
23+ </button >
1924 </div >
2025
2126 <!-- Chat -->
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)
202301const realtimeStore = useChatRealtimeStore ()
203302const { enabled: realtimeEnabled , available: realtimeAvailable , checking: realtimeChecking , statusError: realtimeStatusError , toggling: realtimeToggling } = storeToRefs (realtimeStore)
204303const { 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
206418onMounted (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
210430const apiBase = useApiBase ()
@@ -240,10 +460,73 @@ const goWrapped = async () => {
240460 await navigateTo (' /wrapped' )
241461}
242462
463+ const goGuide = async () => {
464+ await navigateTo (' /' )
465+ }
466+
243467const 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+
247530const realtimeBusy = computed (() => !! realtimeChecking .value || !! realtimeToggling .value )
248531
249532const realtimeTitle = computed (() => {
0 commit comments