dashboard_for_sage/wwwroot/spa_router.js
yumoqing 39fe93438c feat: SPA router for bricks - URL state management
- Intercepts buildUrlwidgetHandler to track navigation to sage_main_content
- Updates browser URL via History API (pushState) on page changes
- Restores page state on browser refresh via ?page= URL parameter
- Supports browser back/forward buttons via popstate event
- Supports deep linking (direct URL access to specific pages)
- URL format: /?page=/module/index.ui
2026-05-27 14:08:39 +08:00

224 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Bricks SPA Router
*
* 为bricks单页应用提供路由支持
* 1. urlwidget加载内容到sage_main_content时更新浏览器URL
* 2. 浏览器刷新时根据URL恢复当前页面
* 3. 支持浏览器前进/后退按钮
* 4. 支持直接URL访问深链接
*
* URL格式: /?page=/module/index.ui
*
* 工作原理:
* - 拦截bricks.buildUrlwidgetHandler当target是sage_main_content时记录路由
* - 使用History API (pushState/replaceState)更新URL
* - 监听popstate事件处理前进/后退
* - 页面加载时检查URL参数并恢复状态
*/
(function() {
'use strict';
// 路由配置
var ROUTE_PARAM = 'page';
var MAIN_CONTENT_ID = 'sage_main_content';
// 路由状态
var currentRoute = null;
var isPopState = false;
var routerReady = false;
// ── URL 操作 ──
function getRouteFromURL() {
var params = new URLSearchParams(window.location.search);
return params.get(ROUTE_PARAM) || null;
}
function pushRoute(route) {
var url = new URL(window.location);
url.searchParams.set(ROUTE_PARAM, route);
history.pushState({ route: route }, '', url.toString());
currentRoute = route;
}
function replaceRoute(route) {
var url = new URL(window.location);
if (route) {
url.searchParams.set(ROUTE_PARAM, route);
} else {
url.searchParams.delete(ROUTE_PARAM);
}
history.replaceState({ route: route }, '', url.toString());
currentRoute = route;
}
// ── 路由加载 ──
function waitForApp(maxWait) {
return new Promise(function(resolve) {
if (bricks && bricks.app) {
resolve(true);
return;
}
var waited = 0;
var interval = setInterval(function() {
waited += 100;
if ((bricks && bricks.app) || waited >= maxWait) {
clearInterval(interval);
resolve(!!(bricks && bricks.app));
}
}, 100);
});
}
function getMainContent() {
if (!bricks || !bricks.app) return null;
return bricks.getWidgetById(MAIN_CONTENT_ID, bricks.app);
}
/**
* 程序化加载路由构建urlwidget并放入sage_main_content
*/
async function loadRoute(route, pushHistory) {
if (!route) return;
var ready = await waitForApp(5000);
if (!ready) {
console.error('[SPA Router] bricks.app not ready');
return;
}
var mainContent = getMainContent();
if (!mainContent) {
console.error('[SPA Router] #' + MAIN_CONTENT_ID + ' not found');
return;
}
// 避免重复加载
if (currentRoute === route && !isPopState) return;
console.log('[SPA Router] Loading:', route);
var desc = {
widgettype: 'urlwidget',
options: { url: route }
};
try {
var widget = await bricks.widgetBuild(desc, bricks.app);
if (widget && !(widget instanceof bricks.Popup) && !(widget instanceof bricks.NewWindow)) {
mainContent.clear_widgets();
mainContent.add_widget(widget);
currentRoute = route;
if (pushHistory && !isPopState) {
pushRoute(route);
}
}
} catch (e) {
console.error('[SPA Router] Load failed:', route, e);
}
isPopState = false;
}
// ── 拦截 buildUrlwidgetHandler ──
function installInterceptor() {
var _orig = bricks.buildUrlwidgetHandler;
bricks.buildUrlwidgetHandler = function(w, target, rtdata, desc) {
var url = desc && desc.options ? desc.options.url : null;
var targetId = target ? target.id : null;
// 只拦截目标是sage_main_content的urlwidget
if (targetId === MAIN_CONTENT_ID && url && routerReady) {
// 获取原始handler
var handler = _orig(w, target, rtdata, desc);
if (typeof handler === 'function') {
// 包装执行原始handler后更新URL
return async function() {
await handler();
// 跳过已经是当前路由的情况
if (currentRoute !== url && !isPopState) {
console.log('[SPA Router] Route changed:', url);
pushRoute(url);
}
};
}
}
return _orig(w, target, rtdata, desc);
};
console.log('[SPA Router] Interceptor installed');
}
// ── popstate 处理 ──
function onPopState(event) {
var route = (event.state && event.state.route) || getRouteFromURL();
console.log('[SPA Router] popstate →', route);
isPopState = true;
if (route) {
loadRoute(route, false);
}
}
// ── 初始化 ──
async function init() {
console.log('[SPA Router] Initializing...');
// 等bricks.js加载完
if (typeof bricks === 'undefined') {
await waitForApp(10000);
}
if (!bricks) {
console.error('[SPA Router] bricks not found, aborting');
return;
}
installInterceptor();
window.addEventListener('popstate', onPopState);
// 检查URL是否有初始路由
var initialRoute = getRouteFromURL();
if (initialRoute) {
console.log('[SPA Router] Deep link:', initialRoute);
// 等shell加载完再恢复
await waitForApp(5000);
await new Promise(function(r) { setTimeout(r, 300); });
await loadRoute(initialRoute, false);
// 用replaceState标记初始状态
replaceRoute(initialRoute);
} else {
// 记录初始状态
replaceRoute(null);
}
routerReady = true;
console.log('[SPA Router] Ready');
}
// 启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 调试API
window.BricksRouter = {
loadRoute: function(route) { return loadRoute(route, true); },
current: function() { return currentRoute; },
back: function() { history.back(); },
forward: function() { history.forward(); }
};
})();