import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Avatar } from '@/app/Avatar'; import { Icon } from '@/app/Icon'; import { logout as revokeServerSession } from '@/lib/api/auth'; import { getMe } from '@/lib/api/profile'; import { resetTinode } from '@/lib/tinode/connection'; import { requestConfirm } from '@/stores/confirmDialog'; import { notImplemented } from '@/stores/globalToast'; import { useSessionStore } from '@/stores/session'; const DIRECTORY = [ { key: 'profile', to: '/settings/profile', icon: 'account_circle', label: '个人资料' }, { key: 'security', to: '/settings/security', icon: 'security', label: '账号与安全' }, { key: 'notifications', to: '/settings/notifications', icon: 'notifications', label: '消息通知' }, { key: 'privacy', to: '/settings/privacy', icon: 'lock', label: '隐私' }, { key: 'general', to: '/settings/general', icon: 'tune', label: '通用' }, ]; /** * README §8.7 (mobile "我的" page): 我的收藏/通话记录 are grouped ABOVE the regular settings * directory. Desktop never needs this list -- those already have their own permanent NavRail icons * there -- but on a phone-width viewport (bottom tab bar instead of a rail) there is otherwise NO * way to reach `/favorites` or `/calls` at all short of typing the URL directly (confirmed live: * BottomTabBar.tsx only has 消息/通讯录/我的, unchanged since Phase 1-2 when those two routes didn't * exist yet) -- a real navigation gap this Phase 9 pass caught, not a cosmetic one. 文件助手 used to * be a third entry here (reusing the same `slf`-conversation redirect the desktop NavRail icon did) * but was removed along with every other entry point -- see ChatListColumn.tsx's PINNED_ENTRIES * doc comment for why. */ const MOBILE_SHORTCUTS = [ { key: 'favorites', to: '/favorites', icon: 'inventory_2', label: '我的收藏', colorClass: 'text-accent' }, { key: 'calls', to: '/calls', icon: 'phone', label: '通话记录', colorClass: 'text-warn' }, ]; /** README §8.1 (3-column: Nav(global, settings 激活) / 目录 260px / 内容). */ export function SettingsLayout() { const location = useLocation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const setMyUserId = useSessionStore((s) => s.setMyUserId); const meQuery = useQuery({ queryKey: ['me'], queryFn: getMe }); const [loggingOut, setLoggingOut] = useState(false); // README §8.7 draws TWO separate mobile screens (`#1d` 我的 hub, `#1e` 设置 sub-list) that don't // map 1:1 onto the desktop directory -- this is which of the two the bare `/settings` mobile route // currently shows. Irrelevant at `md`+ (desktop always shows the one real sidebar). const [mobileSubView, setMobileSubView] = useState(false); const logout = async () => { if (!(await requestConfirm({ message: '确定退出登录吗?' }))) return; setLoggingOut(true); // docs/IM-后端问题清单.md 问题22:这个接口此前三个候选路径都404,本地清理是唯一手段,最近 // 复核确认它其实已经真实可用(同一token调用前后分别200/401,确认真的会被吊销)。必须在清空 // sessionStorage的token之前调用(`authToken()`读的就是这个值),且失败不能阻塞本地清理这条 // 兜底路径——不管服务端这次撤销成不成功,用户在这台设备上都应该被清出登录态。 await revokeServerSession().catch(() => {}); resetTinode(); sessionStorage.removeItem('im-session-token'); setMyUserId(null); // Deliberately NOT clearTenant() -- that clears useSessionStore's `tenant`, and LoginPage's own // `if (!tenant) return ` immediately bounces the navigate('/login') // below straight back to the tenant-code entry screen instead of this tenant's actual login page // (confirmed live: this used to be a real dead end, not a hypothetical). A plain logout should // stay on the same enterprise's login screen, ready for the same or a different user to sign back // in -- only the explicit "切换企业" link is supposed to clear the tenant. // // The QueryClient instance is module-scoped for the whole SPA lifetime (App.tsx) and survives a // client-side route change -- without this, a second account logging in on the same tab/device // (a shared kiosk machine, realistic for an enterprise client) would briefly render the FIRST // account's still-cached profile/privacy/blocks data before the background refetch resolves. queryClient.clear(); navigate('/login'); }; // Below the desktop breakpoint there's no room to dock both columns (confirmed live: without // this, the 260px directory and the content column both try to render side by side on a phone- // width viewport, squeezing the content into a sliver narrow enough that its own text wraps // one character per line). Mirrors ContactsLayout's exact `showListOnNarrow` pattern: show the // directory only at the bare `/settings` index, content only once a specific section is picked. const showListOnNarrow = location.pathname === '/settings'; return (
设置
); }