feat: SPA 真实路由(/release、/pipelines/:id 等直达)

- 路由映射:/pipelines 列表、/pipelines/:id 编辑器、/release 批量发布、
  /records 发布记录、/gitops、/environments、/credentials、/build-nodes、
  /tools、/jenkins-import、/webhooks、/plugins、/settings
- 侧边栏/助手跳转同步更新 URL;直接输入/刷新/分享链接恢复对应页面;
  未知路径回 /pipelines
This commit is contained in:
Logan.Li committed 2026-10-27 09:24:24 +08:00
1 parent a3b4637e19
commit 6e97b55015
2 files changed
+75 -8

No files matched your search

+74 -7
View File
@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { Toaster } from 'sonner';
import { useLocation, useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import Sidebar from '@/components/Sidebar';
import TopBar from '@/components/TopBar';
@@ -26,7 +27,42 @@ import { apiClient } from '@/lib/api';
export type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'webhooks' | 'plugins' | 'settings';
export type EditorTab = '基本信息' | '流程配置' | '触发设置' | '变量和缓存' | '发布';
// 路由 ↔ 模块映射(用户可直接访问 /release、/pipelines/1 等)
const ROUTE_TO_MODULE: Record<string, ModuleType> = {
'/': 'pipelines',
'/pipelines': 'pipelines',
'/gitops': 'gitops',
'/release': 'batch-release',
'/records': 'release-records',
'/environments': 'environments',
'/credentials': 'credentials',
'/build-nodes': 'build-nodes',
'/tools': 'tools',
'/jenkins-import': 'jenkins-import',
'/webhooks': 'webhooks',
'/plugins': 'plugins',
'/settings': 'settings',
};
const MODULE_TO_ROUTE: Record<ModuleType, string> = {
pipelines: '/pipelines',
gitops: '/gitops',
'batch-release': '/release',
'release-records': '/records',
environments: '/environments',
credentials: '/credentials',
'build-nodes': '/build-nodes',
tools: '/tools',
'jenkins-import': '/jenkins-import',
webhooks: '/webhooks',
plugins: '/plugins',
settings: '/settings',
};
export default function App() {
const location = useLocation();
const navigate = useNavigate();
// Module navigation
const [activeModule, setActiveModule] = useState<ModuleType>('pipelines');
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
@@ -38,6 +74,35 @@ export default function App() {
const [saveTick, setSaveTick] = useState(0);
const [runTick, setRunTick] = useState(0);
// URL → 页面状态(直接输入/刷新/分享链接时恢复)
useEffect(() => {
const path = location.pathname.replace(/\/+$/, '') || '/';
const editorMatch = path.match(/^\/pipelines\/(\d+)/);
if (editorMatch) {
setActiveModule('pipelines');
setCurrentPipelineId(Number(editorMatch[1]) || 1);
setPipelineView('editor');
return;
}
const mod = ROUTE_TO_MODULE[path];
if (mod) {
setActiveModule(mod);
if (mod !== 'pipelines') {
setPipelineView('list');
} else if (path === '/pipelines' || path === '/') {
setPipelineView('list');
}
return;
}
// 未知路径回首页
navigate('/pipelines', { replace: true });
}, [location.pathname, navigate]);
const goToModule = useCallback((m: ModuleType) => {
setActiveModule(m);
navigate(MODULE_TO_ROUTE[m]);
}, [navigate]);
const { data: pipelines = [] } = useQuery({
queryKey: ['pipelines'],
queryFn: apiClient.listPipelines,
@@ -59,12 +124,14 @@ export default function App() {
setCurrentPipelineId(Number(id) || 1);
setPipelineView('editor');
setActiveTab('流程配置');
}, []);
navigate(`/pipelines/${Number(id) || 1}`);
}, [navigate]);
const handlePipelineSwitch = useCallback((id: number) => {
setCurrentPipelineId(id);
setActiveTab('流程配置');
}, []);
navigate(`/pipelines/${id}`);
}, [navigate]);
const handleOnlySave = useCallback(() => {
if (activeTab !== '流程配置') setActiveTab('流程配置');
@@ -97,7 +164,7 @@ export default function App() {
onSaveAndRun={handleSaveAndRun}
isRunning={isPipelineRunning}
executionStatus={(latestStatus as 'running' | 'success' | 'failed' | 'cancelled') || 'idle'}
onBack={() => setPipelineView('list')}
onBack={() => { setPipelineView('list'); navigate('/pipelines'); }}
/>
<div className="flex-1 flex overflow-hidden">
{activeTab === '流程配置' && (
@@ -139,7 +206,7 @@ export default function App() {
case 'jenkins-import':
return <JenkinsImport />;
case 'batch-release':
return <BatchRelease onViewRecords={() => setActiveModule('release-records')} />;
return <BatchRelease onViewRecords={() => goToModule('release-records')} />;
case 'release-records':
return <ReleaseRecords />;
case 'webhooks':
@@ -160,14 +227,14 @@ export default function App() {
<Toaster position="top-right" richColors />
<Sidebar
activeModule={activeModule}
onModuleChange={setActiveModule}
onModuleChange={goToModule}
collapsed={sidebarCollapsed}
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
/>
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{renderContent()}
</div>
<AiAssistant onNavigate={(m) => setActiveModule(m as ModuleType)} />
<AiAssistant onNavigate={(m) => goToModule(m as ModuleType)} />
</div>
);
}