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",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
29
src/App.tsx
29
src/App.tsx
@@ -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
|
||||
* ├── LoginPage(Modal 弹层,事件总线驱动)
|
||||
* ├── SettingsPage(Modal 居中叠加,事件总线驱动 — 对应 QT 原版设置窗口)
|
||||
* ├── AppRoutes(页面路由)
|
||||
* Layout(Banner + NavBar)
|
||||
* └── HomePage(主页面,始终挂载)
|
||||
* LoginPage(Modal 弹层,事件总线驱动)
|
||||
* SettingsPage(Modal 居中叠加,事件总线驱动 — 对应 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
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 {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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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[];
|
||||
/** 仅已登录时显示 */
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<ThemeSetting />
|
||||
<StoragePathSetting edition={edition} />
|
||||
<SystemInfoSetting />
|
||||
<PasswordSetting />
|
||||
<UpdateSetting />
|
||||
<StoragePathSetting edition={edition} />
|
||||
<PasswordSetting />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -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
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',
|
||||
/** 打开设置 Drawer(导航栏"设置"按钮点击) */
|
||||
OPEN_SETTINGS: 'settings:open',
|
||||
/** 打开页面 Modal(导航栏点击页面导航项触发,携带 target 路径标识符) */
|
||||
OPEN_PAGE: 'page:open',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user