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:
64
package-lock.json
generated
64
package-lock.json
generated
@@ -1,20 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "ele-heixiu",
|
"name": "ele-heixiu",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ele-heixiu",
|
"name": "ele-heixiu",
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-buddy/ide-toolbox": "^2.5.0",
|
"@react-buddy/ide-toolbox": "^2.5.0",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.8.3",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7"
|
||||||
"react-router-dom": "^7.16.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.60.0",
|
||||||
@@ -3942,19 +3941,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"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==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/read-binary-file-arch": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
"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"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ele-heixiu",
|
"name": "ele-heixiu",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.12",
|
"version": "0.0.13",
|
||||||
"description": "船长·HeiXiu — 桌面效率工作台",
|
"description": "船长·HeiXiu — 桌面效率工作台",
|
||||||
"author": "HeiXiu 杨烨",
|
"author": "HeiXiu 杨烨",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -26,13 +26,12 @@
|
|||||||
"test:e2e": "playwright test.json"
|
"test:e2e": "playwright test.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-buddy/ide-toolbox": "^2.5.0",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"electron-updater": "^6.8.3",
|
"electron-updater": "^6.8.3",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7"
|
||||||
"react-router-dom": "^7.16.0",
|
|
||||||
"@react-buddy/ide-toolbox": "^2.5.0"
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.heixiu.electron",
|
"appId": "com.heixiu.electron",
|
||||||
|
|||||||
29
src/App.tsx
29
src/App.tsx
@@ -1,24 +1,25 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { HashRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useAppContext } from './contexts/app-context';
|
import { useAppContext } from './contexts/app-context';
|
||||||
import { LoginPage } from './pages/login';
|
import { LoginPage } from './pages/login';
|
||||||
import { SettingsPage } from './pages/settings';
|
import { SettingsPage } from './pages/settings';
|
||||||
import { on, off, EVENTS } from './utils/event-bus';
|
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
|
* Layout(Banner + NavBar)
|
||||||
* ├── LoginPage(Modal 弹层,事件总线驱动)
|
* └── HomePage(主页面,始终挂载)
|
||||||
* ├── SettingsPage(Modal 居中叠加,事件总线驱动 — 对应 QT 原版设置窗口)
|
* LoginPage(Modal 弹层,事件总线驱动)
|
||||||
* ├── AppRoutes(页面路由)
|
* SettingsPage(Modal 居中叠加,事件总线驱动 — 对应 QT 原版设置窗口)
|
||||||
*
|
*
|
||||||
* 设计原则(对应 QT 原版多窗口模型):
|
* 设计原则(对应 QT 原版多窗口模型):
|
||||||
* - 叠加层(设置、公告等)→ Modal/Drawer + 事件总线,主页面不卸载
|
* - 叠加层(设置、公告、页面等)→ Modal/Drawer + 事件总线,主页面不卸载
|
||||||
* - 页面跳转 → React Router,需要跨页保持的状态放 Provider 或 localStorage
|
* - 不再使用 React Router,所有导航由事件总线驱动
|
||||||
*/
|
*/
|
||||||
function App() {
|
function App() {
|
||||||
const { isLoggedIn } = useAppContext();
|
const { isLoggedIn } = useAppContext();
|
||||||
@@ -59,16 +60,20 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<>
|
||||||
|
<Layout>
|
||||||
|
<HomePage />
|
||||||
|
</Layout>
|
||||||
|
|
||||||
{/* 登录 Modal — 可关闭,关闭后主页可操作 */}
|
{/* 登录 Modal — 可关闭,关闭后主页可操作 */}
|
||||||
<LoginPage open={loginOpen && !isLoggedIn} onClose={handleLoginClose} />
|
<LoginPage open={loginOpen && !isLoggedIn} onClose={handleLoginClose} />
|
||||||
|
|
||||||
{/* 设置 Modal — 事件驱动,居中叠加,主页面始终挂载(对应 QT 原版独立设置窗口) */}
|
{/* 设置 Modal — 事件驱动,居中叠加,主页面始终挂载(对应 QT 原版独立设置窗口) */}
|
||||||
<SettingsPage open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
<SettingsPage open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||||
|
|
||||||
{/* 页面路由 */}
|
{/* 页面调度器 — 监听 OPEN_PAGE 事件,根据 modalType 渲染 Modal/Drawer/FloatingPanel */}
|
||||||
<AppRoutes />
|
<PageDispatcher />
|
||||||
</HashRouter>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Layout — 页面布局壳(Banner轮播 + NavBar + 页面内容 Outlet)
|
// Layout — 页面布局壳(Banner轮播 + NavBar + 页面内容)
|
||||||
// BannerCarousel 仅在首页显示(路由为 "/" 或 "")
|
// BannerCarousel 始终渲染(当前仅有首页,后续多面板切换时 Banner 始终保持)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useLocation, Outlet } from 'react-router-dom';
|
import type { ReactNode } from 'react';
|
||||||
import { theme as antTheme } from 'antd';
|
import { theme as antTheme } from 'antd';
|
||||||
import { NavBar } from './navbar';
|
import { NavBar } from './navbar';
|
||||||
import { BannerCarousel } from '@/pages/home/components/BannerCarousel';
|
import { BannerCarousel } from '@/pages/home/components/BannerCarousel';
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout({ children }: { children: ReactNode }) {
|
||||||
const { token } = antTheme.useToken();
|
const { token } = antTheme.useToken();
|
||||||
const location = useLocation();
|
|
||||||
const isHomePage = location.pathname === '/' || location.pathname === '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen flex flex-col transition-colors duration-300"
|
className="h-screen flex flex-col transition-colors duration-300"
|
||||||
style={{ background: token.colorBgLayout }}
|
style={{ background: token.colorBgLayout }}
|
||||||
>
|
>
|
||||||
{isHomePage && <BannerCarousel />}
|
<BannerCarousel />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<Outlet />
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
215
src/components/common/FloatingPanel.tsx
Normal file
215
src/components/common/FloatingPanel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import {useState, useCallback, useMemo} from 'react';
|
import {useState, useCallback, useMemo} from 'react';
|
||||||
import {useNavigate} from 'react-router-dom';
|
|
||||||
import {App} from 'antd';
|
import {App} from 'antd';
|
||||||
|
|
||||||
import {useAppContext} from '@/contexts/app-context';
|
import {useAppContext} from '@/contexts/app-context';
|
||||||
@@ -25,7 +24,6 @@ export function NavBar() {
|
|||||||
const {platform, edition, isLoggedIn, logout} = useAppContext();
|
const {platform, edition, isLoggedIn, logout} = useAppContext();
|
||||||
const {isDark} = useTheme();
|
const {isDark} = useTheme();
|
||||||
const {message} = App.useApp();
|
const {message} = App.useApp();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
@@ -81,6 +79,8 @@ export function NavBar() {
|
|||||||
logout();
|
logout();
|
||||||
} else if (item.key === 'settings') {
|
} else if (item.key === 'settings') {
|
||||||
emit(EVENTS.OPEN_SETTINGS);
|
emit(EVENTS.OPEN_SETTINGS);
|
||||||
|
} else if (item.key === 'auth') {
|
||||||
|
emit(EVENTS.SHOW_LOGIN);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -90,12 +90,13 @@ export function NavBar() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'navigate':
|
case 'page':
|
||||||
case 'spa':
|
if (item.target) {
|
||||||
if (item.key === 'auth') {
|
emit(EVENTS.OPEN_PAGE, {
|
||||||
emit(EVENTS.SHOW_LOGIN);
|
target: item.target,
|
||||||
} else if (item.target) {
|
modalType: item.modalType ?? 'floating',
|
||||||
navigate(item.target);
|
label: item.label,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ const personalLeft: NavItemConfig[] = [
|
|||||||
key: 'compliance-assets',
|
key: 'compliance-assets',
|
||||||
label: '合规素材库',
|
label: '合规素材库',
|
||||||
iconSrc: complianceAssetsSvg,
|
iconSrc: complianceAssetsSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/compliance-assets',
|
target: '/compliance-assets',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'left',
|
position: 'left',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -47,8 +48,9 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'account',
|
key: 'account',
|
||||||
label: '查帐户',
|
label: '查帐户',
|
||||||
iconSrc: accountSvg,
|
iconSrc: accountSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/account',
|
target: '/account',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
},
|
},
|
||||||
@@ -56,16 +58,18 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'vip-activate',
|
key: 'vip-activate',
|
||||||
label: '开会员/激活',
|
label: '开会员/激活',
|
||||||
iconSrc: membershipSvg,
|
iconSrc: membershipSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/vip',
|
target: '/vip',
|
||||||
|
modalType: 'modal',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'recharge-points',
|
key: 'recharge-points',
|
||||||
label: '充值积分',
|
label: '充值积分',
|
||||||
iconSrc: rechargeSvg,
|
iconSrc: rechargeSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/recharge',
|
target: '/recharge',
|
||||||
|
modalType: 'modal',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
},
|
},
|
||||||
@@ -73,8 +77,9 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'billing-history',
|
key: 'billing-history',
|
||||||
label: '消费记录',
|
label: '消费记录',
|
||||||
iconSrc: billingSvg,
|
iconSrc: billingSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/billing',
|
target: '/billing',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
},
|
},
|
||||||
@@ -82,8 +87,9 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'order-details',
|
key: 'order-details',
|
||||||
label: '订单明细',
|
label: '订单明细',
|
||||||
iconSrc: orderDetailsSvg,
|
iconSrc: orderDetailsSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/orders',
|
target: '/orders',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
},
|
},
|
||||||
@@ -106,8 +112,7 @@ const personalRight: NavItemConfig[] = [
|
|||||||
key: 'auth',
|
key: 'auth',
|
||||||
label: '登录/注册',
|
label: '登录/注册',
|
||||||
iconSrc: accountSvg,
|
iconSrc: accountSvg,
|
||||||
action: 'navigate',
|
action: 'button',
|
||||||
target: '/login',
|
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireGuest: true,
|
requireGuest: true,
|
||||||
},
|
},
|
||||||
@@ -131,16 +136,18 @@ const enterpriseLeft: NavItemConfig[] = [
|
|||||||
key: 'compliance-assets',
|
key: 'compliance-assets',
|
||||||
label: '合规素材库',
|
label: '合规素材库',
|
||||||
iconSrc: complianceAssetsSvg,
|
iconSrc: complianceAssetsSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/compliance-assets',
|
target: '/compliance-assets',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'left',
|
position: 'left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'project-management',
|
key: 'project-management',
|
||||||
label: '项目管理',
|
label: '项目管理',
|
||||||
iconSrc: projectManagementSvg,
|
iconSrc: projectManagementSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/projects',
|
target: '/projects',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'left',
|
position: 'left',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -158,8 +165,9 @@ const enterpriseRight: NavItemConfig[] = [
|
|||||||
key: 'quota-check',
|
key: 'quota-check',
|
||||||
label: '查配额',
|
label: '查配额',
|
||||||
iconSrc: accountSvg,
|
iconSrc: accountSvg,
|
||||||
action: 'navigate',
|
action: 'page',
|
||||||
target: '/quota',
|
target: '/quota',
|
||||||
|
modalType: 'floating',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireLogin: false,
|
requireLogin: false,
|
||||||
},
|
},
|
||||||
@@ -174,8 +182,7 @@ const enterpriseRight: NavItemConfig[] = [
|
|||||||
key: 'auth',
|
key: 'auth',
|
||||||
label: '登录/注册',
|
label: '登录/注册',
|
||||||
iconSrc: accountSvg,
|
iconSrc: accountSvg,
|
||||||
action: 'navigate',
|
action: 'button',
|
||||||
target: '/login',
|
|
||||||
position: 'right',
|
position: 'right',
|
||||||
requireGuest: true,
|
requireGuest: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import type { Edition } from '@shared/types';
|
|||||||
|
|
||||||
/** 导航项的行为类型 */
|
/** 导航项的行为类型 */
|
||||||
export type NavAction =
|
export type NavAction =
|
||||||
| 'navigate' // 新开窗口
|
| 'page' // 打开页面 Modal(携带 target 路径标识符,由事件总线分发)
|
||||||
| 'spa' // 单页应用内导航(当前窗口)
|
|
||||||
| 'drawer' // 侧拉抽屉(公告等)
|
| 'drawer' // 侧拉抽屉(公告等)
|
||||||
| 'button' // 纯按钮行为(退出登录)
|
| 'button' // 纯按钮行为(退出登录等)
|
||||||
| 'external'; // 外部链接
|
| 'external'; // 外部链接
|
||||||
|
|
||||||
/** 单个导航项配置 */
|
/** 单个导航项配置 */
|
||||||
@@ -24,6 +23,8 @@ export interface NavItemConfig {
|
|||||||
action: NavAction;
|
action: NavAction;
|
||||||
/** 点击回调 / 路由路径 / 外部 URL(取决于 action) */
|
/** 点击回调 / 路由路径 / 外部 URL(取决于 action) */
|
||||||
target?: string;
|
target?: string;
|
||||||
|
/** 窗口类型(仅 action='page' 时生效):modal 居中模态 | drawer 侧滑 | floating 非模态浮动面板 */
|
||||||
|
modalType?: 'modal' | 'drawer' | 'floating';
|
||||||
/** 仅在指定版本下显示 */
|
/** 仅在指定版本下显示 */
|
||||||
edition?: Edition | Edition[];
|
edition?: Edition | Edition[];
|
||||||
/** 仅已登录时显示 */
|
/** 仅已登录时显示 */
|
||||||
|
|||||||
11
src/pages/account/AccountPage.tsx
Normal file
11
src/pages/account/AccountPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/pages/billing/BillingPage.tsx
Normal file
11
src/pages/billing/BillingPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/pages/compliance-assets/ComplianceAssetsPage.tsx
Normal file
11
src/pages/compliance-assets/ComplianceAssetsPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -104,11 +104,14 @@ export function HomeContent() {
|
|||||||
|
|
||||||
// 用 ref 追踪最新列宽,避免拖拽 mousemove 闭包中读到过期值
|
// 用 ref 追踪最新列宽,避免拖拽 mousemove 闭包中读到过期值
|
||||||
const leftWidthRef = useRef(leftWidth);
|
const leftWidthRef = useRef(leftWidth);
|
||||||
leftWidthRef.current = leftWidth;
|
|
||||||
const rightWidthRef = useRef(rightWidth);
|
const rightWidthRef = useRef(rightWidth);
|
||||||
rightWidthRef.current = rightWidth;
|
|
||||||
const draggingRef = useRef(dragging);
|
const draggingRef = useRef(dragging);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
leftWidthRef.current = leftWidth;
|
||||||
|
rightWidthRef.current = rightWidth;
|
||||||
draggingRef.current = dragging;
|
draggingRef.current = dragging;
|
||||||
|
}, [leftWidth, rightWidth, dragging]);
|
||||||
|
|
||||||
const handleDividerMouseDown = (side: 'left' | 'right') => (e: React.MouseEvent) => {
|
const handleDividerMouseDown = (side: 'left' | 'right') => (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
11
src/pages/orders/OrdersPage.tsx
Normal file
11
src/pages/orders/OrdersPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/pages/projects/ProjectsPage.tsx
Normal file
11
src/pages/projects/ProjectsPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/pages/quota/QuotaPage.tsx
Normal file
11
src/pages/quota/QuotaPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/pages/recharge/RechargePage.tsx
Normal file
11
src/pages/recharge/RechargePage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,10 +46,10 @@ export function SettingsPage({ open, onClose }: SettingsPageProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
<ThemeSetting />
|
<ThemeSetting />
|
||||||
<StoragePathSetting edition={edition} />
|
|
||||||
<SystemInfoSetting />
|
<SystemInfoSetting />
|
||||||
<PasswordSetting />
|
|
||||||
<UpdateSetting />
|
<UpdateSetting />
|
||||||
|
<StoragePathSetting edition={edition} />
|
||||||
|
<PasswordSetting />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export function SystemInfoSetting() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card size="small" title="系统信息" className="rounded-md!">
|
<Card size="small" title="系统信息(开发显示信息)" className="rounded-md!">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{infoItems.map((item) => (
|
{infoItems.map((item) => (
|
||||||
<div key={item.label} className="flex items-center gap-2">
|
<div key={item.label} className="flex items-center gap-2">
|
||||||
<span style={{ color: 'var(--color-primary, #4F46E5)', fontSize: 14 }}>
|
<span style={{ color: 'var(--color-primary, #4F46E5)', fontSize: 14 }}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
<Text type="secondary" className="text-sm min-w-[64px]">
|
<Text type="secondary" className="text-sm min-w-16">
|
||||||
{item.label}:
|
{item.label}:
|
||||||
</Text>
|
</Text>
|
||||||
<Tag color={item.color}>{item.value}</Tag>
|
<Tag color={item.color}>{item.value}</Tag>
|
||||||
|
|||||||
11
src/pages/vip/VipPage.tsx
Normal file
11
src/pages/vip/VipPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -73,4 +73,6 @@ export const EVENTS = {
|
|||||||
TASK_SUBMITTED: 'task:submitted',
|
TASK_SUBMITTED: 'task:submitted',
|
||||||
/** 打开设置 Drawer(导航栏"设置"按钮点击) */
|
/** 打开设置 Drawer(导航栏"设置"按钮点击) */
|
||||||
OPEN_SETTINGS: 'settings:open',
|
OPEN_SETTINGS: 'settings:open',
|
||||||
|
/** 打开页面 Modal(导航栏点击页面导航项触发,携带 target 路径标识符) */
|
||||||
|
OPEN_PAGE: 'page:open',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user