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

64
package-lock.json generated
View File

@@ -1,20 +1,19 @@
{
"name": "ele-heixiu",
"version": "0.0.12",
"version": "0.0.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ele-heixiu",
"version": "0.0.12",
"version": "0.0.13",
"dependencies": {
"@react-buddy/ide-toolbox": "^2.5.0",
"antd": "^6.4.3",
"axios": "^1.16.1",
"electron-updater": "^6.8.3",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.16.0"
"react-dom": "^19.2.7"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
@@ -3942,19 +3941,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -7104,44 +7090,6 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-router": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz",
"integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz",
"integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==",
"license": "MIT",
"dependencies": {
"react-router": "7.16.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@@ -7541,12 +7489,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "ele-heixiu",
"private": true,
"version": "0.0.12",
"version": "0.0.13",
"description": "船长·HeiXiu — 桌面效率工作台",
"author": "HeiXiu 杨烨",
"type": "module",
@@ -26,13 +26,12 @@
"test:e2e": "playwright test.json"
},
"dependencies": {
"@react-buddy/ide-toolbox": "^2.5.0",
"antd": "^6.4.3",
"axios": "^1.16.1",
"electron-updater": "^6.8.3",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.16.0",
"@react-buddy/ide-toolbox": "^2.5.0"
"react-dom": "^19.2.7"
},
"build": {
"appId": "com.heixiu.electron",

View File

@@ -1,24 +1,25 @@
import { useState, useEffect, useCallback } from 'react';
import { HashRouter } from 'react-router-dom';
import { useAppContext } from './contexts/app-context';
import { LoginPage } from './pages/login';
import { SettingsPage } from './pages/settings';
import { on, off, EVENTS } from './utils/event-bus';
import { AppRoutes } from './router';
import { Layout } from '@/components/Layout';
import { HomePage } from '@/pages/home/HomePage';
import { PageDispatcher } from '@/components/PageDispatcher';
/**
* 根组件 — 主窗口
*
* 层级:
* HashRouter
* ── LoginPageModal 弹层,事件总线驱动
* ├── SettingsPageModal 居中叠加,事件总线驱动 — 对应 QT 原版设置窗口
* ├── AppRoutes页面路由
* LayoutBanner + NavBar
* ── HomePage主页面始终挂载
* LoginPageModal 弹层,事件总线驱动)
* SettingsPageModal 居中叠加,事件总线驱动 — 对应 QT 原版设置窗口
*
* 设计原则(对应 QT 原版多窗口模型):
* - 叠加层(设置、公告等)→ Modal/Drawer + 事件总线,主页面不卸载
* - 页面跳转 → React Router需要跨页保持的状态放 Provider 或 localStorage
* - 叠加层(设置、公告、页面等)→ Modal/Drawer + 事件总线,主页面不卸载
* - 不再使用 React Router所有导航由事件总线驱动
*/
function App() {
const { isLoggedIn } = useAppContext();
@@ -59,16 +60,20 @@ function App() {
}, []);
return (
<HashRouter>
<>
<Layout>
<HomePage />
</Layout>
{/* 登录 Modal — 可关闭,关闭后主页可操作 */}
<LoginPage open={loginOpen && !isLoggedIn} onClose={handleLoginClose} />
{/* 设置 Modal — 事件驱动,居中叠加,主页面始终挂载(对应 QT 原版独立设置窗口) */}
<SettingsPage open={settingsOpen} onClose={() => setSettingsOpen(false)} />
{/* 页面路由 */}
<AppRoutes />
</HashRouter>
{/* 页面调度器 — 监听 OPEN_PAGE 事件,根据 modalType 渲染 Modal/Drawer/FloatingPanel */}
<PageDispatcher />
</>
);
}

View File

@@ -1,26 +1,24 @@
// ============================================================
// Layout — 页面布局壳Banner轮播 + NavBar + 页面内容 Outlet
// BannerCarousel 仅在首页显示(路由为 "/" 或 ""
// Layout — 页面布局壳Banner轮播 + NavBar + 页面内容)
// BannerCarousel 始终渲染(当前仅有首页,后续多面板切换时 Banner 始终保持
// ============================================================
import { useLocation, Outlet } from 'react-router-dom';
import type { ReactNode } from 'react';
import { theme as antTheme } from 'antd';
import { NavBar } from './navbar';
import { BannerCarousel } from '@/pages/home/components/BannerCarousel';
export function Layout() {
export function Layout({ children }: { children: ReactNode }) {
const { token } = antTheme.useToken();
const location = useLocation();
const isHomePage = location.pathname === '/' || location.pathname === '';
return (
<div
className="h-screen flex flex-col transition-colors duration-300"
style={{ background: token.colorBgLayout }}
>
{isHomePage && <BannerCarousel />}
<BannerCarousel />
<NavBar />
<Outlet />
{children}
</div>
);
}

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

View File

@@ -0,0 +1,215 @@
// ============================================================
// FloatingPanel — 非模态可拖拽浮动面板
//
// 特性:
// - 标题栏拖拽移动,无遮罩,不阻断主页面交互
// - 初始居中定位,支持暗色主题
// - 边界检测(不拖出视口)
// - 多个面板可同时打开(点击置顶)
// - 关闭按钮 + 最小尺寸约束
//
// 对应 QT 原版"非模态窗口"的 Web 映射
// ============================================================
import { useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
import { useTheme } from '@/hooks/use-theme';
// ---------- 类型 ----------
export interface FloatingPanelProps {
open: boolean;
onClose: () => void;
title: string;
children: ReactNode;
/** 初始宽度px默认 480 */
width?: number;
/** 初始高度px默认 360 */
height?: number;
}
// ---------- 全局 z-index 管理 ----------
let globalZIndex = 1000;
function nextZIndex(): number {
globalZIndex += 1;
return globalZIndex;
}
// ---------- 组件 ----------
export function FloatingPanel({ open, onClose, title, children, width = 480, height = 360 }: FloatingPanelProps) {
const { isDark } = useTheme();
// 位置与尺寸
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const [zIndex, setZIndex] = useState(() => nextZIndex());
const panelRef = useRef<HTMLDivElement>(null);
// 拖拽状态
const draggingRef = useRef(false);
const dragStartRef = useRef({ x: 0, y: 0, panelX: 0, panelY: 0 });
// 初始居中(首次 open 时计算)
useEffect(() => {
if (open && position === null) {
const x = Math.max(0, (window.innerWidth - width) / 2);
const y = Math.max(0, (window.innerHeight - height) / 2);
setPosition({ x, y });
}
}, [open, position, width, height]);
// 拖拽启动
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
// 仅标题栏区域可拖拽
if ((e.target as HTMLElement).closest('.fp-close-btn')) return;
e.preventDefault();
draggingRef.current = true;
dragStartRef.current = {
x: e.clientX,
y: e.clientY,
panelX: position?.x ?? 0,
panelY: position?.y ?? 0,
};
},
[position],
);
// 全局 mousemove / mouseup
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!draggingRef.current) return;
const ds = dragStartRef.current;
const newX = ds.panelX + (e.clientX - ds.x);
const newY = ds.panelY + (e.clientY - ds.y);
// 边界约束:保留标题栏至少 40px 可见,确保用户能拖回
const minVisible = 40;
const maxX = window.innerWidth - minVisible;
const maxY = window.innerHeight - minVisible;
setPosition({
x: Math.max(-width + minVisible, Math.min(maxX, newX)),
y: Math.max(0, Math.min(maxY, newY)),
});
};
const handleMouseUp = () => {
draggingRef.current = false;
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [width]);
// 点击面板 → 置顶
const bringToFront = useCallback(() => {
setZIndex(nextZIndex());
}, []);
if (!open || !position) return null;
return (
<div
ref={panelRef}
onMouseDown={bringToFront}
className="fp-panel"
style={{
position: 'fixed',
left: position.x,
top: position.y,
width,
height,
zIndex,
display: 'flex',
flexDirection: 'column',
borderRadius: 8,
boxShadow: isDark
? '0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.08)'
: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.06)',
background: isDark ? '#1E1B4B' : '#FFFFFF',
overflow: 'hidden',
userSelect: 'none',
}}
>
{/* 标题栏 — 拖拽手柄 */}
<div
onMouseDown={handleMouseDown}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 12px 0 16px',
height: 40,
flexShrink: 0,
cursor: 'grab',
background: isDark
? 'linear-gradient(180deg, #2E2A5A 0%, #252050 100%)'
: 'linear-gradient(180deg, #F5F3FF 0%, #EEF2FF 100%)',
borderBottom: `1px solid ${isDark ? '#3E3A6A' : '#E5E7EB'}`,
}}
>
<span
style={{
fontSize: 13,
fontWeight: 600,
color: isDark ? '#E8E6F0' : '#1E1B4B',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{title}
</span>
<button
className="fp-close-btn"
type="button"
onClick={onClose}
title="关闭"
style={{
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
fontSize: 16,
color: isDark ? '#A5A3C0' : '#9CA3AF',
background: 'transparent',
transition: 'all 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = isDark ? 'rgba(239,68,68,0.2)' : 'rgba(239,68,68,0.1)';
e.currentTarget.style.color = '#EF4444';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = isDark ? '#A5A3C0' : '#9CA3AF';
}}
>
</button>
</div>
{/* 内容区 */}
<div
style={{
flex: 1,
overflow: 'auto',
userSelect: 'text',
padding: 16,
color: isDark ? '#E8E6F0' : '#1E1B4B',
}}
>
{children}
</div>
</div>
);
}

View File

@@ -8,7 +8,6 @@
// ============================================================
import {useState, useCallback, useMemo} from 'react';
import {useNavigate} from 'react-router-dom';
import {App} from 'antd';
import {useAppContext} from '@/contexts/app-context';
@@ -25,7 +24,6 @@ export function NavBar() {
const {platform, edition, isLoggedIn, logout} = useAppContext();
const {isDark} = useTheme();
const {message} = App.useApp();
const navigate = useNavigate();
const [drawerOpen, setDrawerOpen] = useState(false);
@@ -81,6 +79,8 @@ export function NavBar() {
logout();
} else if (item.key === 'settings') {
emit(EVENTS.OPEN_SETTINGS);
} else if (item.key === 'auth') {
emit(EVENTS.SHOW_LOGIN);
}
break;
@@ -90,12 +90,13 @@ export function NavBar() {
}
break;
case 'navigate':
case 'spa':
if (item.key === 'auth') {
emit(EVENTS.SHOW_LOGIN);
} else if (item.target) {
navigate(item.target);
case 'page':
if (item.target) {
emit(EVENTS.OPEN_PAGE, {
target: item.target,
modalType: item.modalType ?? 'floating',
label: item.label,
});
}
break;
}

View File

@@ -28,8 +28,9 @@ const personalLeft: NavItemConfig[] = [
key: 'compliance-assets',
label: '合规素材库',
iconSrc: complianceAssetsSvg,
action: 'navigate',
action: 'page',
target: '/compliance-assets',
modalType: 'floating',
position: 'left',
},
];
@@ -47,8 +48,9 @@ const personalRight: NavItemConfig[] = [
key: 'account',
label: '查帐户',
iconSrc: accountSvg,
action: 'navigate',
action: 'page',
target: '/account',
modalType: 'floating',
position: 'right',
requireLogin: true,
},
@@ -56,16 +58,18 @@ const personalRight: NavItemConfig[] = [
key: 'vip-activate',
label: '开会员/激活',
iconSrc: membershipSvg,
action: 'navigate',
action: 'page',
target: '/vip',
modalType: 'modal',
position: 'right',
},
{
key: 'recharge-points',
label: '充值积分',
iconSrc: rechargeSvg,
action: 'navigate',
action: 'page',
target: '/recharge',
modalType: 'modal',
position: 'right',
requireLogin: true,
},
@@ -73,8 +77,9 @@ const personalRight: NavItemConfig[] = [
key: 'billing-history',
label: '消费记录',
iconSrc: billingSvg,
action: 'navigate',
action: 'page',
target: '/billing',
modalType: 'floating',
position: 'right',
requireLogin: true,
},
@@ -82,8 +87,9 @@ const personalRight: NavItemConfig[] = [
key: 'order-details',
label: '订单明细',
iconSrc: orderDetailsSvg,
action: 'navigate',
action: 'page',
target: '/orders',
modalType: 'floating',
position: 'right',
requireLogin: true,
},
@@ -106,8 +112,7 @@ const personalRight: NavItemConfig[] = [
key: 'auth',
label: '登录/注册',
iconSrc: accountSvg,
action: 'navigate',
target: '/login',
action: 'button',
position: 'right',
requireGuest: true,
},
@@ -131,16 +136,18 @@ const enterpriseLeft: NavItemConfig[] = [
key: 'compliance-assets',
label: '合规素材库',
iconSrc: complianceAssetsSvg,
action: 'navigate',
action: 'page',
target: '/compliance-assets',
modalType: 'floating',
position: 'left',
},
{
key: 'project-management',
label: '项目管理',
iconSrc: projectManagementSvg,
action: 'navigate',
action: 'page',
target: '/projects',
modalType: 'floating',
position: 'left',
},
];
@@ -158,8 +165,9 @@ const enterpriseRight: NavItemConfig[] = [
key: 'quota-check',
label: '查配额',
iconSrc: accountSvg,
action: 'navigate',
action: 'page',
target: '/quota',
modalType: 'floating',
position: 'right',
requireLogin: false,
},
@@ -174,8 +182,7 @@ const enterpriseRight: NavItemConfig[] = [
key: 'auth',
label: '登录/注册',
iconSrc: accountSvg,
action: 'navigate',
target: '/login',
action: 'button',
position: 'right',
requireGuest: true,
},

View File

@@ -6,10 +6,9 @@ import type { Edition } from '@shared/types';
/** 导航项的行为类型 */
export type NavAction =
| 'navigate' // 新开窗口
| 'spa' // 单页应用内导航(当前窗口)
| 'page' // 打开页面 Modal携带 target 路径标识符,由事件总线分发)
| 'drawer' // 侧拉抽屉(公告等)
| 'button' // 纯按钮行为(退出登录)
| 'button' // 纯按钮行为(退出登录
| 'external'; // 外部链接
/** 单个导航项配置 */
@@ -24,6 +23,8 @@ export interface NavItemConfig {
action: NavAction;
/** 点击回调 / 路由路径 / 外部 URL取决于 action */
target?: string;
/** 窗口类型(仅 action='page' 时生效modal 居中模态 | drawer 侧滑 | floating 非模态浮动面板 */
modalType?: 'modal' | 'drawer' | 'floating';
/** 仅在指定版本下显示 */
edition?: Edition | Edition[];
/** 仅已登录时显示 */

View File

@@ -0,0 +1,11 @@
// ============================================================
// AccountPage — 查帐户(占位组件,后续实现具体功能)
// ============================================================
export function AccountPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -0,0 +1,11 @@
// ============================================================
// BillingPage — 消费记录(占位组件,后续实现具体功能)
// ============================================================
export function BillingPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -0,0 +1,11 @@
// ============================================================
// ComplianceAssetsPage — 合规素材库(占位组件,后续实现具体功能)
// ============================================================
export function ComplianceAssetsPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -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);
useEffect(() => {
leftWidthRef.current = leftWidth;
rightWidthRef.current = rightWidth;
draggingRef.current = dragging;
}, [leftWidth, rightWidth, dragging]);
const handleDividerMouseDown = (side: 'left' | 'right') => (e: React.MouseEvent) => {
e.preventDefault();

View File

@@ -0,0 +1,11 @@
// ============================================================
// OrdersPage — 订单明细(占位组件,后续实现具体功能)
// ============================================================
export function OrdersPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -0,0 +1,11 @@
// ============================================================
// ProjectsPage — 项目管理(占位组件,后续实现具体功能)
// ============================================================
export function ProjectsPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -0,0 +1,11 @@
// ============================================================
// QuotaPage — 查配额(占位组件,后续实现具体功能)
// ============================================================
export function QuotaPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -0,0 +1,11 @@
// ============================================================
// RechargePage — 充值积分(占位组件,后续实现具体功能)
// ============================================================
export function RechargePage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}> </p>
</div>
);
}

View File

@@ -46,10 +46,10 @@ export function SettingsPage({ open, onClose }: SettingsPageProps) {
>
<div className="flex flex-col gap-4 p-4">
<ThemeSetting />
<StoragePathSetting edition={edition} />
<SystemInfoSetting />
<PasswordSetting />
<UpdateSetting />
<StoragePathSetting edition={edition} />
<PasswordSetting />
</div>
</Modal>
);

View File

@@ -46,14 +46,14 @@ export function SystemInfoSetting() {
];
return (
<Card size="small" title="系统信息" className="rounded-md!">
<Card size="small" title="系统信息(开发显示信息)" className="rounded-md!">
<div className="flex flex-col gap-2">
{infoItems.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<span style={{ color: 'var(--color-primary, #4F46E5)', fontSize: 14 }}>
{item.icon}
</span>
<Text type="secondary" className="text-sm min-w-[64px]">
<Text type="secondary" className="text-sm min-w-16">
{item.label}
</Text>
<Tag color={item.color}>{item.value}</Tag>

11
src/pages/vip/VipPage.tsx Normal file
View File

@@ -0,0 +1,11 @@
// ============================================================
// VipPage — 开会员/激活(占位组件,后续实现具体功能)
// ============================================================
export function VipPage() {
return (
<div style={{ padding: 8 }}>
<p style={{ color: 'inherit', opacity: 0.6, margin: 0 }}>/ </p>
</div>
);
}

View File

@@ -1,21 +0,0 @@
// ============================================================
// AppRoutes — 应用路由配置(由 App.tsx 在 HashRouter 内使用)
//
// 核心设计:
// - Settings 等叠加层通过事件总线驱动 Drawer/Modal 展示,不走路由
// - 首页始终保持挂载,不会因导航而卸载
// ============================================================
import { Routes, Route } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { HomePage } from '@/pages/home/HomePage';
export function AppRoutes() {
return (
<Routes>
<Route path="/*" element={<Layout />}>
<Route index element={<HomePage />} />
</Route>
</Routes>
);
}

View File

@@ -73,4 +73,6 @@ export const EVENTS = {
TASK_SUBMITTED: 'task:submitted',
/** 打开设置 Drawer导航栏"设置"按钮点击) */
OPEN_SETTINGS: 'settings:open',
/** 打开页面 Modal导航栏点击页面导航项触发携带 target 路径标识符) */
OPEN_PAGE: 'page:open',
} as const;