105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import type { ReactNode } from 'react';
|
||
import { useLocation } from 'react-router-dom';
|
||
|
||
import ChatPage from '../pages/Chat';
|
||
import ContactsPage from '../pages/Contacts';
|
||
import SchedulePage from '../pages/Schedule';
|
||
import FormsPage from '../pages/Forms';
|
||
import KnowledgePage from '../pages/Knowledge';
|
||
import AppsPage from '../pages/Apps';
|
||
import DrivePage from '../pages/Drive';
|
||
import AiChatPage from '../pages/AiChat';
|
||
|
||
type PageKey =
|
||
| 'chat'
|
||
| 'contacts'
|
||
| 'schedule'
|
||
| 'forms'
|
||
| 'knowledge'
|
||
| 'apps'
|
||
| 'drive'
|
||
| 'ai-chat';
|
||
|
||
function resolveKey(pathname: string): PageKey {
|
||
if (pathname.startsWith('/chat')) return 'chat';
|
||
if (pathname.startsWith('/contacts')) return 'contacts';
|
||
if (pathname.startsWith('/schedule')) return 'schedule';
|
||
if (pathname.startsWith('/forms')) return 'forms';
|
||
if (pathname.startsWith('/knowledge')) return 'knowledge';
|
||
if (pathname.startsWith('/apps')) return 'apps';
|
||
if (pathname.startsWith('/drive')) return 'drive';
|
||
if (pathname.startsWith('/ai-chat')) return 'ai-chat';
|
||
return 'chat';
|
||
}
|
||
|
||
/**
|
||
* Keep-Alive 出口:1-7 模块页面(聊天/联系人/日程/表单/笔记/应用/网盘 + AI 对话)
|
||
* 切换时保持组件挂载,非当前页面用 display:none 隐藏,避免路由切换导致卸载、编辑内容丢失。
|
||
* 懒挂载:首次访问某页面时才创建实例,之后一直保留;滚动位置、输入内容等状态均不丢失。
|
||
*/
|
||
export default function KeepAliveOutlet() {
|
||
const location = useLocation();
|
||
const activeKey = resolveKey(location.pathname);
|
||
const [mountedKeys, setMountedKeys] = useState<PageKey[]>(() => [activeKey]);
|
||
|
||
// 懒挂载:路由变化时把当前页加入已挂载列表。
|
||
// 注意:不能在 render 期间直接 setState(React 18 并发渲染会丢弃 render-phase update,
|
||
// 导致从 /chat 切换其他页面时停留在旧渲染、内容空白),必须通过 effect 更新。
|
||
useEffect(() => {
|
||
setMountedKeys((prev) => (prev.includes(activeKey) ? prev : [...prev, activeKey]));
|
||
}, [activeKey]);
|
||
|
||
// 渲染时始终包含当前页(首次切换时立即显示,effect 提交后再持久化)
|
||
const renderKeys = mountedKeys.includes(activeKey) ? mountedKeys : [...mountedKeys, activeKey];
|
||
|
||
// AI 对话页的助手 id 从路径解析(常驻后不再依赖 Route 的 useParams)
|
||
// /ai-chat(无 id)时兜底默认助手 'assistant',与原 Navigate 重定向行为一致
|
||
const assistantId =
|
||
activeKey === 'ai-chat' ? location.pathname.split('/')[2] || 'assistant' : undefined;
|
||
const lastAssistantIdRef = useRef<string | undefined>(undefined);
|
||
if (assistantId) lastAssistantIdRef.current = assistantId;
|
||
|
||
return (
|
||
<>
|
||
{renderKeys.map((key) => {
|
||
const active = key === activeKey;
|
||
let page: ReactNode;
|
||
switch (key) {
|
||
case 'chat':
|
||
page = <ChatPage />;
|
||
break;
|
||
case 'contacts':
|
||
page = <ContactsPage />;
|
||
break;
|
||
case 'schedule':
|
||
page = <SchedulePage />;
|
||
break;
|
||
case 'forms':
|
||
page = <FormsPage />;
|
||
break;
|
||
case 'knowledge':
|
||
page = <KnowledgePage />;
|
||
break;
|
||
case 'apps':
|
||
page = <AppsPage />;
|
||
break;
|
||
case 'drive':
|
||
page = <DrivePage />;
|
||
break;
|
||
default:
|
||
page = <AiChatPage assistantId={assistantId ?? lastAssistantIdRef.current} />;
|
||
}
|
||
return (
|
||
<div
|
||
key={key}
|
||
style={{ flex: 1, minWidth: 0, height: '100%', display: active ? 'flex' : 'none' }}
|
||
>
|
||
{page}
|
||
</div>
|
||
);
|
||
})}
|
||
</>
|
||
);
|
||
}
|