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

@@ -51,18 +51,18 @@ export function HomeContent() {
}, []);
const handleSetSelectedModel = useCallback(
(model: ModelsListResponseBody | null) => {
setSelectedModel(model);
setSelectedTask(null);
},
[],
(model: ModelsListResponseBody | null) => {
setSelectedModel(model);
setSelectedTask(null);
},
[],
);
const handleSetSelectedTask = useCallback(
(task: TaskInfoItemResponseBody | null) => {
setSelectedTask(task);
},
[],
(task: TaskInfoItemResponseBody | null) => {
setSelectedTask(task);
},
[],
);
const contextValue = useMemo(() => ({
@@ -104,11 +104,14 @@ export function HomeContent() {
// 用 ref 追踪最新列宽,避免拖拽 mousemove 闭包中读到过期值
const leftWidthRef = useRef(leftWidth);
leftWidthRef.current = leftWidth;
const rightWidthRef = useRef(rightWidth);
rightWidthRef.current = rightWidth;
const draggingRef = useRef(dragging);
draggingRef.current = dragging;
useEffect(() => {
leftWidthRef.current = leftWidth;
rightWidthRef.current = rightWidth;
draggingRef.current = dragging;
}, [leftWidth, rightWidth, dragging]);
const handleDividerMouseDown = (side: 'left' | 'right') => (e: React.MouseEvent) => {
e.preventDefault();
@@ -199,10 +202,10 @@ export function HomeContent() {
flexShrink: 0,
cursor: 'col-resize',
background: isActive
? token.colorPrimary
: isHovered
? `${token.colorPrimary}60`
: token.colorBorderSecondary,
? token.colorPrimary
: isHovered
? `${token.colorPrimary}60`
: token.colorBorderSecondary,
position: 'relative' as const,
zIndex: 10,
display: 'flex',
@@ -216,107 +219,107 @@ export function HomeContent() {
if (!isLoggedIn) {
return (
<HomeContext.Provider value={contextValue}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
background: token.colorBgLayout,
}}
>
<Empty
description={
<Text type="secondary">
使
</Text>
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
</HomeContext.Provider>
<HomeContext.Provider value={contextValue}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: 1,
background: token.colorBgLayout,
}}
>
<Empty
description={
<Text type="secondary">
使
</Text>
}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
</HomeContext.Provider>
);
}
// ======== 三列布局(可拖拽) ========
return (
<HomeContext.Provider value={contextValue}>
<div
ref={containerRef}
style={{
display: 'flex',
padding: 12,
gap: 0,
flex: 1,
overflow: 'hidden',
background: token.colorBgLayout,
cursor: !!dragging ? 'col-resize' : undefined,
userSelect: !!dragging ? 'none' : undefined,
}}
>
{/* 左列 */}
{!isLeftCollapsed && (
<>
<div style={{ width: leftWidth, overflow: 'hidden', flexShrink: 0 }}>
<LeftPanel />
</div>
<HomeContext.Provider value={contextValue}>
<div
ref={containerRef}
style={{
display: 'flex',
padding: 12,
gap: 0,
flex: 1,
overflow: 'hidden',
background: token.colorBgLayout,
cursor: !!dragging ? 'col-resize' : undefined,
userSelect: !!dragging ? 'none' : undefined,
}}
>
{/* 左列 */}
{!isLeftCollapsed && (
<>
<div style={{ width: leftWidth, overflow: 'hidden', flexShrink: 0 }}>
<LeftPanel />
</div>
{/* 左分隔条 */}
<div
onMouseDown={handleDividerMouseDown('left')}
style={dividerStyle('left')}
onMouseEnter={() => setHoveredDivider('left')}
onMouseLeave={() => setHoveredDivider(null)}
>
{/* 拖拽手柄竖线 */}
<div style={{
width: 2,
height: 32,
borderRadius: 1,
background: dragging === 'left'
? '#fff'
: hoveredDivider === 'left'
? token.colorPrimary
: token.colorBorder,
transition: 'background 0.2s ease',
}} />
</div>
</>
)}
{/* 左分隔条 */}
<div
onMouseDown={handleDividerMouseDown('left')}
style={dividerStyle('left')}
onMouseEnter={() => setHoveredDivider('left')}
onMouseLeave={() => setHoveredDivider(null)}
>
{/* 拖拽手柄竖线 */}
<div style={{
width: 2,
height: 32,
borderRadius: 1,
background: dragging === 'left'
? '#fff'
: hoveredDivider === 'left'
? token.colorPrimary
: token.colorBorder,
transition: 'background 0.2s ease',
}} />
</div>
</>
)}
{/* 中列 */}
<div style={{ flex: 1, overflow: 'hidden', minWidth: CENTER_MIN }}>
<CenterPanel />
</div>
{/* 中列 */}
<div style={{ flex: 1, overflow: 'hidden', minWidth: CENTER_MIN }}>
<CenterPanel />
</div>
{/* 右分隔条 */}
<div
onMouseDown={handleDividerMouseDown('right')}
style={dividerStyle('right')}
onMouseEnter={() => setHoveredDivider('right')}
onMouseLeave={() => setHoveredDivider(null)}
>
{/* 拖拽手柄竖线 */}
<div style={{
width: 2,
height: 32,
borderRadius: 1,
background: dragging === 'right'
? '#fff'
: hoveredDivider === 'right'
? token.colorPrimary
: token.colorBorder,
transition: 'background 0.2s ease',
}} />
</div>
{/* 右分隔条 */}
<div
onMouseDown={handleDividerMouseDown('right')}
style={dividerStyle('right')}
onMouseEnter={() => setHoveredDivider('right')}
onMouseLeave={() => setHoveredDivider(null)}
>
{/* 拖拽手柄竖线 */}
<div style={{
width: 2,
height: 32,
borderRadius: 1,
background: dragging === 'right'
? '#fff'
: hoveredDivider === 'right'
? token.colorPrimary
: token.colorBorder,
transition: 'background 0.2s ease',
}} />
</div>
{/* 右列 */}
<div style={{ width: rightWidth, overflow: 'hidden', flexShrink: 0 }}>
<RightPanel />
</div>
</div>
</HomeContext.Provider>
{/* 右列 */}
<div style={{ width: rightWidth, overflow: 'hidden', flexShrink: 0 }}>
<RightPanel />
</div>
</div>
</HomeContext.Provider>
);
}