From ada915f2988db125581e44c1f1e5b0a54324e10b Mon Sep 17 00:00:00 2001 From: YoungestSongMo <2130460579@qq.com> Date: Mon, 8 Jun 2026 16:33:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=BB=20React=20Router=20=E5=8C=96?= =?UTF-8?q?=20+=20=E5=AE=9E=E7=8E=B0=E4=B8=89=E7=A7=8D=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=9A=84=E9=A1=B5=E9=9D=A2=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 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 多窗口模型) --- package-lock.json | 64 +---- package.json | 7 +- src/App.tsx | 29 ++- src/components/Layout.tsx | 14 +- src/components/PageDispatcher.tsx | 149 ++++++++++++ src/components/common/FloatingPanel.tsx | 215 +++++++++++++++++ src/components/navbar/NavBar.tsx | 17 +- src/components/navbar/nav-config.ts | 33 +-- src/components/navbar/types.ts | 7 +- src/pages/account/AccountPage.tsx | 11 + src/pages/billing/BillingPage.tsx | 11 + .../ComplianceAssetsPage.tsx | 11 + src/pages/home/HomeContent.tsx | 219 +++++++++--------- src/pages/orders/OrdersPage.tsx | 11 + src/pages/projects/ProjectsPage.tsx | 11 + src/pages/quota/QuotaPage.tsx | 11 + src/pages/recharge/RechargePage.tsx | 11 + src/pages/settings/SettingsPage.tsx | 4 +- .../settings/blocks/SystemInfoSetting.tsx | 4 +- src/pages/vip/VipPage.tsx | 11 + src/router/index.tsx | 21 -- src/utils/event-bus.ts | 2 + 22 files changed, 631 insertions(+), 242 deletions(-) create mode 100644 src/components/PageDispatcher.tsx create mode 100644 src/components/common/FloatingPanel.tsx create mode 100644 src/pages/account/AccountPage.tsx create mode 100644 src/pages/billing/BillingPage.tsx create mode 100644 src/pages/compliance-assets/ComplianceAssetsPage.tsx create mode 100644 src/pages/orders/OrdersPage.tsx create mode 100644 src/pages/projects/ProjectsPage.tsx create mode 100644 src/pages/quota/QuotaPage.tsx create mode 100644 src/pages/recharge/RechargePage.tsx create mode 100644 src/pages/vip/VipPage.tsx delete mode 100644 src/router/index.tsx diff --git a/package-lock.json b/package-lock.json index 000d706..24c6f1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 216dfd6..b8adc24 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 1d591ad..990ec0e 100644 --- a/src/App.tsx +++ b/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 ( - + <> + + + + {/* 登录 Modal — 可关闭,关闭后主页可操作 */} {/* 设置 Modal — 事件驱动,居中叠加,主页面始终挂载(对应 QT 原版独立设置窗口) */} setSettingsOpen(false)} /> - {/* 页面路由 */} - - + {/* 页面调度器 — 监听 OPEN_PAGE 事件,根据 modalType 渲染 Modal/Drawer/FloatingPanel */} + + ); } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 363440b..d9069e9 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -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 (
- {isHomePage && } + - + {children}
); } diff --git a/src/components/PageDispatcher.tsx b/src/components/PageDispatcher.tsx new file mode 100644 index 0000000..a7be853 --- /dev/null +++ b/src/components/PageDispatcher.tsx @@ -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 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(null); + + // 浮动面板列表(支持多个同时打开) + const [floatingPanels, setFloatingPanels] = useState([]); + 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' && ( + + {singletonContent} + + )} + + {/* ======== Drawer ======== */} + {singleton?.modalType === 'drawer' && ( + + {singletonContent} + + )} + + {/* ======== Floating Panels ======== */} + {floatingPanels.map((panel) => { + const PageContent = PAGE_MAP[panel.target]; + if (!PageContent) return null; + + return ( + closeFloating(panel.id)} + > + + + ); + })} + + ); +} diff --git a/src/components/common/FloatingPanel.tsx b/src/components/common/FloatingPanel.tsx new file mode 100644 index 0000000..aa19c00 --- /dev/null +++ b/src/components/common/FloatingPanel.tsx @@ -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(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 ( +
+ {/* 标题栏 — 拖拽手柄 */} +
+ + {title} + + + +
+ + {/* 内容区 */} +
+ {children} +
+
+ ); +} diff --git a/src/components/navbar/NavBar.tsx b/src/components/navbar/NavBar.tsx index 889299c..bf03a5b 100644 --- a/src/components/navbar/NavBar.tsx +++ b/src/components/navbar/NavBar.tsx @@ -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; } diff --git a/src/components/navbar/nav-config.ts b/src/components/navbar/nav-config.ts index 4217f60..c1ffa89 100644 --- a/src/components/navbar/nav-config.ts +++ b/src/components/navbar/nav-config.ts @@ -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, }, diff --git a/src/components/navbar/types.ts b/src/components/navbar/types.ts index b2beec0..cc884ee 100644 --- a/src/components/navbar/types.ts +++ b/src/components/navbar/types.ts @@ -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[]; /** 仅已登录时显示 */ diff --git a/src/pages/account/AccountPage.tsx b/src/pages/account/AccountPage.tsx new file mode 100644 index 0000000..5a012c5 --- /dev/null +++ b/src/pages/account/AccountPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// AccountPage — 查帐户(占位组件,后续实现具体功能) +// ============================================================ + +export function AccountPage() { + return ( +
+

查帐户 — 页面开发中

+
+ ); +} diff --git a/src/pages/billing/BillingPage.tsx b/src/pages/billing/BillingPage.tsx new file mode 100644 index 0000000..d491b00 --- /dev/null +++ b/src/pages/billing/BillingPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// BillingPage — 消费记录(占位组件,后续实现具体功能) +// ============================================================ + +export function BillingPage() { + return ( +
+

消费记录 — 页面开发中

+
+ ); +} diff --git a/src/pages/compliance-assets/ComplianceAssetsPage.tsx b/src/pages/compliance-assets/ComplianceAssetsPage.tsx new file mode 100644 index 0000000..8503865 --- /dev/null +++ b/src/pages/compliance-assets/ComplianceAssetsPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// ComplianceAssetsPage — 合规素材库(占位组件,后续实现具体功能) +// ============================================================ + +export function ComplianceAssetsPage() { + return ( +
+

合规素材库 — 页面开发中

+
+ ); +} diff --git a/src/pages/home/HomeContent.tsx b/src/pages/home/HomeContent.tsx index 10ebb48..3487b2f 100644 --- a/src/pages/home/HomeContent.tsx +++ b/src/pages/home/HomeContent.tsx @@ -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 ( - -
- - 请先登录后再使用首页功能 - - } - image={Empty.PRESENTED_IMAGE_SIMPLE} - /> -
-
+ +
+ + 请先登录后再使用首页功能 + + } + image={Empty.PRESENTED_IMAGE_SIMPLE} + /> +
+
); } // ======== 三列布局(可拖拽) ======== return ( - -
- {/* 左列 */} - {!isLeftCollapsed && ( - <> -
- -
+ +
+ {/* 左列 */} + {!isLeftCollapsed && ( + <> +
+ +
- {/* 左分隔条 */} -
setHoveredDivider('left')} - onMouseLeave={() => setHoveredDivider(null)} - > - {/* 拖拽手柄竖线 */} -
-
- - )} + {/* 左分隔条 */} +
setHoveredDivider('left')} + onMouseLeave={() => setHoveredDivider(null)} + > + {/* 拖拽手柄竖线 */} +
+
+ + )} - {/* 中列 */} -
- -
+ {/* 中列 */} +
+ +
- {/* 右分隔条 */} -
setHoveredDivider('right')} - onMouseLeave={() => setHoveredDivider(null)} - > - {/* 拖拽手柄竖线 */} -
-
+ {/* 右分隔条 */} +
setHoveredDivider('right')} + onMouseLeave={() => setHoveredDivider(null)} + > + {/* 拖拽手柄竖线 */} +
+
- {/* 右列 */} -
- -
-
- + {/* 右列 */} +
+ +
+
+ ); } diff --git a/src/pages/orders/OrdersPage.tsx b/src/pages/orders/OrdersPage.tsx new file mode 100644 index 0000000..57277ba --- /dev/null +++ b/src/pages/orders/OrdersPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// OrdersPage — 订单明细(占位组件,后续实现具体功能) +// ============================================================ + +export function OrdersPage() { + return ( +
+

订单明细 — 页面开发中

+
+ ); +} diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx new file mode 100644 index 0000000..61d55fa --- /dev/null +++ b/src/pages/projects/ProjectsPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// ProjectsPage — 项目管理(占位组件,后续实现具体功能) +// ============================================================ + +export function ProjectsPage() { + return ( +
+

项目管理 — 页面开发中

+
+ ); +} diff --git a/src/pages/quota/QuotaPage.tsx b/src/pages/quota/QuotaPage.tsx new file mode 100644 index 0000000..62e5e3c --- /dev/null +++ b/src/pages/quota/QuotaPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// QuotaPage — 查配额(占位组件,后续实现具体功能) +// ============================================================ + +export function QuotaPage() { + return ( +
+

查配额 — 页面开发中

+
+ ); +} diff --git a/src/pages/recharge/RechargePage.tsx b/src/pages/recharge/RechargePage.tsx new file mode 100644 index 0000000..d6c8907 --- /dev/null +++ b/src/pages/recharge/RechargePage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// RechargePage — 充值积分(占位组件,后续实现具体功能) +// ============================================================ + +export function RechargePage() { + return ( +
+

充值积分 — 页面开发中

+
+ ); +} diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 768da72..9653bc0 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -46,10 +46,10 @@ export function SettingsPage({ open, onClose }: SettingsPageProps) { >
- - + +
); diff --git a/src/pages/settings/blocks/SystemInfoSetting.tsx b/src/pages/settings/blocks/SystemInfoSetting.tsx index bbed3b3..66d747a 100644 --- a/src/pages/settings/blocks/SystemInfoSetting.tsx +++ b/src/pages/settings/blocks/SystemInfoSetting.tsx @@ -46,14 +46,14 @@ export function SystemInfoSetting() { ]; return ( - +
{infoItems.map((item) => (
{item.icon} - + {item.label}: {item.value} diff --git a/src/pages/vip/VipPage.tsx b/src/pages/vip/VipPage.tsx new file mode 100644 index 0000000..65a9e47 --- /dev/null +++ b/src/pages/vip/VipPage.tsx @@ -0,0 +1,11 @@ +// ============================================================ +// VipPage — 开会员/激活(占位组件,后续实现具体功能) +// ============================================================ + +export function VipPage() { + return ( +
+

开会员/激活 — 页面开发中

+
+ ); +} diff --git a/src/router/index.tsx b/src/router/index.tsx deleted file mode 100644 index eb8ce1c..0000000 --- a/src/router/index.tsx +++ /dev/null @@ -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 ( - - }> - } /> - - - ); -} diff --git a/src/utils/event-bus.ts b/src/utils/event-bus.ts index 9a5f12c..553921d 100644 --- a/src/utils/event-bus.ts +++ b/src/utils/event-bus.ts @@ -73,4 +73,6 @@ export const EVENTS = { TASK_SUBMITTED: 'task:submitted', /** 打开设置 Drawer(导航栏"设置"按钮点击) */ OPEN_SETTINGS: 'settings:open', + /** 打开页面 Modal(导航栏点击页面导航项触发,携带 target 路径标识符) */ + OPEN_PAGE: 'page:open', } as const;