268 lines
8.6 KiB
JavaScript
268 lines
8.6 KiB
JavaScript
/**
|
||
* channel-router.js
|
||
* 用户频道路由引擎 - 带过渡动画和状态持久化
|
||
* 集成ChannelState和动画控制
|
||
*/
|
||
|
||
// 依赖全局变量:ChannelState 需要先加载
|
||
(function(global) {
|
||
'use strict';
|
||
|
||
// 默认配置
|
||
const defaultConfig = {
|
||
containerSelector: '#channel-content',
|
||
routes: {},
|
||
defaultRoute: '/home',
|
||
mode: 'fade', // 'fade' 或 'slide'
|
||
transitionDuration: 300
|
||
};
|
||
|
||
class ChannelRouter {
|
||
constructor(config) {
|
||
this.config = Object.assign({}, defaultConfig, config);
|
||
this.container = document.querySelector(this.config.containerSelector);
|
||
if (!this.container) {
|
||
throw new Error(`Container ${this.config.containerSelector} not found`);
|
||
}
|
||
|
||
// 确保容器有相对定位
|
||
this.container.style.position = 'relative';
|
||
this.container.classList.add('route-container');
|
||
|
||
// 当前活动路由
|
||
this.currentRoute = null;
|
||
this.currentPageElement = null;
|
||
|
||
// 防抖定时器
|
||
this.navTimer = null;
|
||
|
||
// 绑定方法
|
||
this.navigate = this.navigate.bind(this);
|
||
this.goBack = this.goBack.bind(this);
|
||
this.goForward = this.goForward.bind(this);
|
||
this.setMode = this.setMode.bind(this);
|
||
this.handlePopState = this.handlePopState.bind(this);
|
||
|
||
// 初始化模式
|
||
this.container.classList.add(`${this.config.mode}-mode`);
|
||
|
||
// 从ChannelState恢复上次的路由
|
||
this.initFromState();
|
||
|
||
// 监听浏览器前进后退
|
||
window.addEventListener('popstate', this.handlePopState);
|
||
|
||
console.log('[router] 初始化完成,模式:', this.config.mode);
|
||
}
|
||
|
||
// 从ChannelState恢复
|
||
initFromState() {
|
||
if (global.ChannelState) {
|
||
const state = global.ChannelState.getState();
|
||
const targetRoute = state.currentRoute || this.config.defaultRoute;
|
||
// 不触发pushState,直接渲染
|
||
this.renderRoute(targetRoute, { replace: true, fromState: true });
|
||
console.log('[router] 从状态恢复路由:', targetRoute);
|
||
} else {
|
||
// 没有状态管理器,走默认
|
||
this.renderRoute(this.config.defaultRoute, { replace: true });
|
||
}
|
||
}
|
||
|
||
// 渲染路由(内部方法)
|
||
renderRoute(path, options = {}) {
|
||
const { replace = false, fromState = false } = options;
|
||
const route = this.config.routes[path];
|
||
if (!route) {
|
||
console.warn(`[router] 路由 ${path} 未定义,使用404`);
|
||
// 可以跳转到404页面,这里简单返回
|
||
return;
|
||
}
|
||
|
||
// 如果是相同路由且不是强制刷新,不重复渲染
|
||
if (this.currentRoute === path && !options.force) {
|
||
return;
|
||
}
|
||
|
||
// 创建新页面元素
|
||
const newPage = document.createElement('div');
|
||
newPage.className = `route-page ${this.config.mode === 'slide' ? 'slide-enter' : ''}`;
|
||
newPage.innerHTML = route.template || route.content || '';
|
||
|
||
// 如果有模块加载器,执行模块加载
|
||
if (global.ModuleLoader && route.module) {
|
||
// 这里简化,实际可能需要加载模块
|
||
console.log('[router] 加载模块:', route.module);
|
||
}
|
||
|
||
// 旧页面元素
|
||
const oldPage = this.currentPageElement;
|
||
|
||
// 设置新页面为激活状态
|
||
newPage.classList.add('active');
|
||
|
||
// 如果是滑入模式,根据方向添加额外类
|
||
if (this.config.mode === 'slide') {
|
||
// 通过history判断方向:前进/后退
|
||
const direction = this.getDirection(path);
|
||
if (direction === 'back') {
|
||
this.container.classList.add('backward');
|
||
this.container.classList.remove('forward');
|
||
} else {
|
||
this.container.classList.add('forward');
|
||
this.container.classList.remove('backward');
|
||
}
|
||
}
|
||
|
||
// 添加新页面到容器
|
||
this.container.appendChild(newPage);
|
||
|
||
// 触发重绘以确保动画
|
||
newPage.offsetHeight;
|
||
|
||
// 如果有旧页面,移除它的active类并添加退出动画类
|
||
if (oldPage) {
|
||
oldPage.classList.remove('active');
|
||
if (this.config.mode === 'slide') {
|
||
oldPage.classList.add('slide-exit');
|
||
}
|
||
}
|
||
|
||
// 动画结束后清理旧页面
|
||
const onTransitionEnd = (e) => {
|
||
if (e.target === newPage || e.target === oldPage) {
|
||
if (oldPage && oldPage.parentNode) {
|
||
oldPage.parentNode.removeChild(oldPage);
|
||
}
|
||
newPage.removeEventListener('transitionend', onTransitionEnd);
|
||
}
|
||
};
|
||
newPage.addEventListener('transitionend', onTransitionEnd);
|
||
|
||
// 更新当前路由
|
||
this.currentRoute = path;
|
||
this.currentPageElement = newPage;
|
||
|
||
// 更新导航菜单激活状态
|
||
this.updateActiveNav(path);
|
||
|
||
// 保存状态(如果不是从状态恢复来的)
|
||
if (!fromState && global.ChannelState) {
|
||
// 判断是push还是replace
|
||
if (replace) {
|
||
// 替换当前历史记录(不增加新记录)
|
||
// 状态管理需要相应处理:替换当前记录而不是push
|
||
global.ChannelState.setCurrentRoute(path);
|
||
// 同时替换浏览器历史
|
||
if (!options.skipHistory) {
|
||
history.replaceState({ route: path }, '', `#${path}`);
|
||
}
|
||
} else {
|
||
// 正常跳转,push到历史
|
||
global.ChannelState.pushHistory(path);
|
||
if (!options.skipHistory) {
|
||
history.pushState({ route: path }, '', `#${path}`);
|
||
}
|
||
}
|
||
} else if (!global.ChannelState) {
|
||
// 没有状态管理器,只处理浏览器历史
|
||
if (!replace && !options.skipHistory) {
|
||
history.pushState({ route: path }, '', `#${path}`);
|
||
} else if (replace && !options.skipHistory) {
|
||
history.replaceState({ route: path }, '', `#${path}`);
|
||
}
|
||
}
|
||
|
||
console.log(`[router] 导航到 ${path}${replace ? ' (replace)' : ''}`);
|
||
}
|
||
|
||
// 判断前进后退方向(简单实现:看是否在历史栈中)
|
||
getDirection(path) {
|
||
if (!global.ChannelState) return 'forward';
|
||
const state = global.ChannelState.getState();
|
||
const currentIndex = state.historyIndex;
|
||
const stack = state.historyStack;
|
||
// 如果path在历史栈中且在当前位置之后,是后退?需要更精确
|
||
// 这里简化:根据当前路由和目标的索引比较
|
||
if (this.currentRoute) {
|
||
const currentIdx = stack.indexOf(this.currentRoute);
|
||
const targetIdx = stack.indexOf(path);
|
||
if (targetIdx < currentIdx) return 'back';
|
||
}
|
||
return 'forward';
|
||
}
|
||
|
||
// 更新导航菜单激活样式
|
||
updateActiveNav(path) {
|
||
document.querySelectorAll('.channel-nav a').forEach(link => {
|
||
const href = link.getAttribute('href').replace('#', '');
|
||
if (href === path) {
|
||
link.classList.add('active');
|
||
} else {
|
||
link.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 公开导航方法
|
||
navigate(path, options = {}) {
|
||
// 防抖处理
|
||
if (this.navTimer) clearTimeout(this.navTimer);
|
||
this.navTimer = setTimeout(() => {
|
||
this.renderRoute(path, options);
|
||
this.navTimer = null;
|
||
}, 10); // 微小延迟确保快速点击不重叠
|
||
}
|
||
|
||
// 后退
|
||
goBack() {
|
||
if (global.ChannelState) {
|
||
const prev = global.ChannelState.goBack();
|
||
if (prev) {
|
||
this.renderRoute(prev, { fromState: true, skipHistory: true });
|
||
} else {
|
||
console.log('[router] 已在最前');
|
||
}
|
||
} else {
|
||
history.back();
|
||
}
|
||
}
|
||
|
||
// 前进
|
||
goForward() {
|
||
if (global.ChannelState) {
|
||
const next = global.ChannelState.goForward();
|
||
if (next) {
|
||
this.renderRoute(next, { fromState: true, skipHistory: true });
|
||
} else {
|
||
console.log('[router] 已在最后');
|
||
}
|
||
} else {
|
||
history.forward();
|
||
}
|
||
}
|
||
|
||
// 处理popstate事件(浏览器前进后退)
|
||
handlePopState(event) {
|
||
const route = event.state?.route || this.config.defaultRoute;
|
||
if (global.ChannelState) {
|
||
// 从状态中恢复索引,但不需要重复push
|
||
this.renderRoute(route, { fromState: true, skipHistory: true });
|
||
} else {
|
||
this.renderRoute(route, { skipHistory: true });
|
||
}
|
||
}
|
||
|
||
// 切换动画模式
|
||
setMode(mode) {
|
||
if (mode !== 'fade' && mode !== 'slide') return;
|
||
this.container.classList.remove('fade-mode', 'slide-mode');
|
||
this.container.classList.add(`${mode}-mode`);
|
||
this.config.mode = mode;
|
||
console.log('[router] 切换动画模式为:', mode);
|
||
}
|
||
}
|
||
|
||
global.ChannelRouter = ChannelRouter;
|
||
})(window);
|