- globals.css 拆分为 4 文件:tokens.css(颜色)/ transitions.css(过渡)/ base.css(重置)/ globals.css(入口) - 提取 auth-persistence.ts 到 services/,解除 AppProvider→pages/login/hooks 反向依赖 - use-auth-state.ts 精简为纯 React Hook,持久化逻辑委托给 services/auth-persistence.ts - components/common/ → components/ui/(目录名语义更清晰) - TypeScript + Vite 构建通过 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
// ============================================================
|
||
// PageDispatcher — 页面事件调度器
|
||
//
|
||
// 监听 EVENTS.OPEN_PAGE,根据 modalType 渲染对应容器:
|
||
// - modal → antd Modal(居中模态,有遮罩阻断交互)
|
||
// - drawer → antd Drawer(侧滑抽屉)
|
||
// - floating → FloatingPanel(非模态浮动面板,可拖拽,不阻断交互)
|
||
//
|
||
// 支持多面板共存(floating 类型可同时打开多个)
|
||
// ============================================================
|
||
|
||
import {useState, useEffect, useCallback, useRef, type ComponentType} from 'react';
|
||
import {Modal, Drawer} from 'antd';
|
||
import {on, off, EVENTS} from '@/utils/event-bus';
|
||
import {FloatingPanel} from '@/components/ui/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';
|
||
import {WechatWorkPage} from '@/pages/wechat-work/WechatWorkPage';
|
||
|
||
// ---------- 类型 ----------
|
||
|
||
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, ComponentType> = {
|
||
'/compliance-assets': ComplianceAssetsPage,
|
||
'/account': AccountPage,
|
||
'/vip': VipPage,
|
||
'/recharge': RechargePage,
|
||
'/billing': BillingPage,
|
||
'/orders': OrdersPage,
|
||
'/projects': ProjectsPage,
|
||
'/quota': QuotaPage,
|
||
'/wechat-work': WechatWorkPage,
|
||
};
|
||
|
||
// ---------- 组件 ----------
|
||
|
||
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 SingletonPage = singleton ? PAGE_MAP[singleton.target] : null;
|
||
|
||
return (
|
||
<>
|
||
{/* ======== Modal ======== */}
|
||
{singleton?.modalType === 'modal' && SingletonPage && (
|
||
<Modal
|
||
open
|
||
centered
|
||
title={singleton.label}
|
||
onCancel={closeSingleton}
|
||
footer={null}
|
||
destroyOnHidden={true}
|
||
mask={{
|
||
closable: true,
|
||
}}
|
||
width={640}
|
||
>
|
||
<SingletonPage />
|
||
</Modal>
|
||
)}
|
||
|
||
{/* ======== Drawer ======== */}
|
||
{singleton?.modalType === 'drawer' && SingletonPage && (
|
||
<Drawer
|
||
open
|
||
title={singleton.label}
|
||
onClose={closeSingleton}
|
||
destroyOnHidden={true}
|
||
size={480}
|
||
>
|
||
<SingletonPage />
|
||
</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>
|
||
);
|
||
})}
|
||
</>
|
||
);
|
||
}
|