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:
2026-06-08 16:33:47 +08:00
parent 0f137b21fa
commit ada915f298
22 changed files with 631 additions and 242 deletions

View 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>
);
})}
</>
);
}