feat: 去 React Router 化 + 实现三种窗口类型的页面调度体系
移除 react-router-dom 依赖,将导航从 URL 路由切换为事件总线驱动,
新增 Modal / Drawer / FloatingPanel 三种窗口类型支持。
架构变更:
- 删除 src/router/(唯一路由 /* → HomePage,无实际用途)
- App.tsx 移除 HashRouter,直接渲染 Layout > HomePage
- Layout 从 useLocation + Outlet 改为 children prop
- NavBar 移除 useNavigate,所有页面导航改为 emit(OPEN_PAGE)
- NavAction 精简:'navigate'|'spa' 合并为 'page'
新增组件:
- FloatingPanel:纯手写非模态浮动面板(可拖拽、无遮罩、多面板共存、
暗色主题适配、边界约束),对应 QT 非模态窗口的 Web 映射
Modal(居中模态)| Drawer(侧滑)| FloatingPanel(浮动可拖拽)
- 8 个页面占位组件(compliance-assets/account/vip/recharge/
billing/orders/projects/quota),各显示导航栏名称作为占位
配置扩展:
- NavItemConfig 新增 modalType?: 'modal' | 'drawer' | 'floating'
- nav-config 所有 page 项补齐 modalType(支付类→modal,浏览类→floating)
- event-bus 新增 OPEN_PAGE 事件,payload 携带 {target, modalType, label}
- 卸载 react-router-dom@^7.16.0(连带移除 4 个包)
设计原则:
- 页面组件不感知窗口类型(内容与容器分离),切换窗口类型只需改配置
- Modal/Drawer 单实例(模态含义=独占注意力),Floating 多实例共存
- 主页面始终挂载不卸载,所有子页面为叠加层(对应 QT 多窗口模型)
This commit is contained in:
149
src/components/PageDispatcher.tsx
Normal file
149
src/components/PageDispatcher.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
// ============================================================
|
||||
// PageDispatcher — 页面事件调度器
|
||||
//
|
||||
// 监听 EVENTS.OPEN_PAGE,根据 modalType 渲染对应容器:
|
||||
// - modal → antd Modal(居中模态,有遮罩阻断交互)
|
||||
// - drawer → antd Drawer(侧滑抽屉)
|
||||
// - floating → FloatingPanel(非模态浮动面板,可拖拽,不阻断交互)
|
||||
//
|
||||
// 支持多面板共存(floating 类型可同时打开多个)
|
||||
// ============================================================
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
|
||||
import { Modal, Drawer } from 'antd';
|
||||
import { on, off, EVENTS } from '@/utils/event-bus';
|
||||
import { FloatingPanel } from '@/components/common/FloatingPanel';
|
||||
import { ComplianceAssetsPage } from '@/pages/compliance-assets/ComplianceAssetsPage';
|
||||
import { AccountPage } from '@/pages/account/AccountPage';
|
||||
import { VipPage } from '@/pages/vip/VipPage';
|
||||
import { RechargePage } from '@/pages/recharge/RechargePage';
|
||||
import { BillingPage } from '@/pages/billing/BillingPage';
|
||||
import { OrdersPage } from '@/pages/orders/OrdersPage';
|
||||
import { ProjectsPage } from '@/pages/projects/ProjectsPage';
|
||||
import { QuotaPage } from '@/pages/quota/QuotaPage';
|
||||
|
||||
// ---------- 类型 ----------
|
||||
|
||||
type ModalType = 'modal' | 'drawer' | 'floating';
|
||||
|
||||
interface PageEventPayload {
|
||||
target: string;
|
||||
modalType: ModalType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ActivePanel {
|
||||
id: string;
|
||||
target: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// ---------- target → 页面组件映射 ----------
|
||||
|
||||
const PAGE_MAP: Record<string, () => ReactNode> = {
|
||||
'/compliance-assets': ComplianceAssetsPage,
|
||||
'/account': AccountPage,
|
||||
'/vip': VipPage,
|
||||
'/recharge': RechargePage,
|
||||
'/billing': BillingPage,
|
||||
'/orders': OrdersPage,
|
||||
'/projects': ProjectsPage,
|
||||
'/quota': QuotaPage,
|
||||
};
|
||||
|
||||
// ---------- 组件 ----------
|
||||
|
||||
export function PageDispatcher() {
|
||||
// 单实例(modal / drawer)—— 同一时间只有一个
|
||||
const [singleton, setSingleton] = useState<PageEventPayload | null>(null);
|
||||
|
||||
// 浮动面板列表(支持多个同时打开)
|
||||
const [floatingPanels, setFloatingPanels] = useState<ActivePanel[]>([]);
|
||||
const nextIdRef = useRef(0);
|
||||
|
||||
// 监听 OPEN_PAGE 事件
|
||||
useEffect(() => {
|
||||
const handler = (payload: unknown) => {
|
||||
const { target, modalType, label } = payload as PageEventPayload;
|
||||
|
||||
if (!target || !PAGE_MAP[target]) {
|
||||
console.warn(`[PageDispatcher] 未找到页面组件: ${target}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (modalType === 'floating') {
|
||||
// 浮动面板:追加到列表
|
||||
const id = `fp-${nextIdRef.current++}`;
|
||||
setFloatingPanels((prev) => [...prev, { id, target, label }]);
|
||||
} else {
|
||||
// modal / drawer:单实例,覆盖之前的
|
||||
setSingleton({ target, modalType, label });
|
||||
}
|
||||
};
|
||||
|
||||
on(EVENTS.OPEN_PAGE, handler);
|
||||
return () => off(EVENTS.OPEN_PAGE, handler);
|
||||
}, []);
|
||||
|
||||
// 关闭单实例
|
||||
const closeSingleton = useCallback(() => {
|
||||
setSingleton(null);
|
||||
}, []);
|
||||
|
||||
// 关闭指定浮动面板
|
||||
const closeFloating = useCallback((id: string) => {
|
||||
setFloatingPanels((prev) => prev.filter((p) => p.id !== id));
|
||||
}, []);
|
||||
|
||||
// 渲染单实例内容
|
||||
const singletonContent = singleton && PAGE_MAP[singleton.target] ? PAGE_MAP[singleton.target]() : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ======== Modal ======== */}
|
||||
{singleton?.modalType === 'modal' && (
|
||||
<Modal
|
||||
open
|
||||
title={singleton.label}
|
||||
onCancel={closeSingleton}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
maskClosable
|
||||
width={640}
|
||||
>
|
||||
{singletonContent}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* ======== Drawer ======== */}
|
||||
{singleton?.modalType === 'drawer' && (
|
||||
<Drawer
|
||||
open
|
||||
title={singleton.label}
|
||||
onClose={closeSingleton}
|
||||
destroyOnClose
|
||||
width={480}
|
||||
>
|
||||
{singletonContent}
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{/* ======== Floating Panels ======== */}
|
||||
{floatingPanels.map((panel) => {
|
||||
const PageContent = PAGE_MAP[panel.target];
|
||||
if (!PageContent) return null;
|
||||
|
||||
return (
|
||||
<FloatingPanel
|
||||
key={panel.id}
|
||||
open
|
||||
title={panel.label}
|
||||
onClose={() => closeFloating(panel.id)}
|
||||
>
|
||||
<PageContent />
|
||||
</FloatingPanel>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user