feat(router): 引入路由系统并重构页面结构

- 为项目引入基于 hash 的前端路由系统,支持多页面切换与动态加载
- 将原有单一 bundle 拆分为多个入口打包(index.js 和 timestamp.js)
- 修改 Rollup 配置以支持多输出文件及插件共享
- 更新 HTML 结构以适配路由容器和页面占位元素
- 调整 CSS 样式,移除旧的页面切换动画,交由路由控制
- 优化 domUtils 工具函数,并迁移部分样式处理逻辑
- 在 timestamp.js 中封装初始化函数以便路由调用
- todolist 页面模板新增,用于后续功能开发
```
This commit is contained in:
雨霖铃
2025-11-27 23:04:24 +08:00
parent ed6053473d
commit 884fc09750
10 changed files with 293 additions and 164 deletions
+25 -16
View File
@@ -5,16 +5,9 @@ import json from '@rollup/plugin-json';
import livereload from 'rollup-plugin-livereload'; import livereload from 'rollup-plugin-livereload';
import serve from 'rollup-plugin-serve'; import serve from 'rollup-plugin-serve';
export default defineConfig({ const basePlugins = [resolve(), commonjs(), json()];
input: 'sidepanel/js/index.js',
output: { const devPlugins = [
file: 'dist/bundle.js',
format: 'iife',
},
plugins: [
resolve(),
commonjs(),
json(),
livereload({ livereload({
watch: ['dist', 'sidepanel'], watch: ['dist', 'sidepanel'],
verbose: false, verbose: false,
@@ -27,10 +20,26 @@ export default defineConfig({
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
}, },
}), }),
], ];
watch: {
include: ['sidepanel/**/*'], export default defineConfig([
exclude: ['node_modules/**/*'], {
clearScreen: false, input: 'sidepanel/js/index.js',
output: {
file: 'dist/index.js',
format: 'iife',
name: 'MainPage',
}, },
}); plugins: [...basePlugins, ...devPlugins],
},
{
input: 'sidepanel/js/timestamp.js',
output: {
file: 'dist/timestamp.js',
format: 'iife',
name: 'TimestampPage',
},
plugins: [...basePlugins, ...devPlugins],
},
]);
+10 -4
View File
@@ -10,12 +10,18 @@
<body> <body>
<!-- 导航栏 --> <!-- 导航栏 -->
<nav class="navbar"> <nav class="navbar">
<button class="nav-button active">时间戳转换</button> <button class="nav-button active" id="timestamp-jump">时间戳转换</button>
<button class="nav-button">任务清单</button> <button class="nav-button" id="todo-jump">任务清单</button>
<button id="close-panel-btn" class="close-button">X</button> <button id="close-panel-btn" class="close-button">X</button>
</nav> </nav>
<div id="app"></div> <div id="app">
<div class="page" hash="/home">
<div class="page-content">
<div id="home"></div>
</div>
</div>
</div>
<script src="../dist/bundle.js"></script> <script src="../dist/index.js"></script>
</body> </body>
</html> </html>
+54 -13
View File
@@ -1,17 +1,58 @@
// 导入时间戳工具的初始化函数和事件处理 console.log('Before creating router');
import './timestamp.js'; import { Router } from '../modules/Router.js';
// 导入页面切换功能 import * as PageModules from '../../dist/timestamp.js';
import { switchPage } from '../utils/domUtils.js';
// 页面切换逻辑 const config = {
import { addEventListenerById } from '../utils/domUtils.js'; routerViewId: 'app', // 路由切换的挂载点 id
stackPages: false, // 多级页面缓存
animationName: 'fade', // 切换页面时的动画
routes: [
{
path: '/home',
name: 'home',
callback: async () => {
// 1. 加载 HTML
const html = await fetch('pages/timestamp.html').then((r) => r.text());
document.getElementById('home').innerHTML = html;
addEventListenerById('nav-timestamp-btn', 'click', () => { // 2. 动态加载对应 JS
switchPage('timestamp'); const script = document.createElement('script');
}); script.src = '/dist/timestamp.js'; // IIFE 打包后的文件
script.onload = () => {
// 计时器页面切换 // 3. 执行 IIFE 暴露的全局对象方法
addEventListenerById('nav-other-tools-btn', 'click', () => { TimestampPage.init(); // 假设 timestamp.js 输出 name: 'TimestampPage'
switchPage('other-tools'); };
document.body.appendChild(script);
},
},
{
path: '/todo',
name: 'todo',
callback: async () => {
// 1. 加载 HTML
const html = await fetch('pages/todolist.html').then((r) => r.text());
document.getElementById('todo').innerHTML = html;
// 2. 动态加载对应 JS
const script = document.createElement('script');
script.src = '/dist/timestamp.js'; // IIFE 打包后的文件
script.onload = () => {
// 3. 执行 IIFE 暴露的全局对象方法
TimestampPage.init(); // 假设 timestamp.js 输出 name: 'TimestampPage'
};
document.body.appendChild(script);
},
},
],
};
const router = new Router();
console.log('Router created:', router);
router.init(config);
console.log('Router initialized');
document.getElementById('todo-jump').addEventListener('click', () => {
console.log('Jumping to todo page');
window.linkTo('#/todo');
}); });
+23 -20
View File
@@ -12,24 +12,27 @@ import {
} from '../modules/eventHandlers.js'; } from '../modules/eventHandlers.js';
import { addEventListenerById } from '../utils/domUtils.js'; import { addEventListenerById } from '../utils/domUtils.js';
// 初始化时间戳显示 export function init() {
initTimestampDisplay(); console.log('timestamp init')
// 初始化时间戳显示
initTimestampDisplay();
// 绑定事件处理器 // 绑定事件处理器
// 绑定时间戳相关按钮事件 // 绑定时间戳相关按钮事件
addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit); addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit);
// 绑定计时器相关按钮事件 // 绑定计时器相关按钮事件
addEventListenerById('stop-timer-btn', 'click', handleStopTimer); addEventListenerById('stop-timer-btn', 'click', handleStopTimer);
addEventListenerById('start-timer-btn', 'click', handleStartTimer); addEventListenerById('start-timer-btn', 'click', handleStartTimer);
// 绑定时间戳转换按钮事件 // 绑定时间戳转换按钮事件
addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp); addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp);
// 绑定日期转换按钮事件 // 绑定日期转换按钮事件
addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp); addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp);
// 绑定复制时间戳按钮事件 // 绑定复制时间戳按钮事件
addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp); addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp);
// 绑定关闭面板按钮事件 // 绑定关闭面板按钮事件
addEventListenerById('close-panel-btn', 'click', handleClosePanel); addEventListenerById('close-panel-btn', 'click', handleClosePanel);
// 绑定切换时区事件 // 绑定切换时区事件
addEventListenerById('timezone-result', 'change', handleTimezoneResult); addEventListenerById('timezone-result', 'change', handleTimezoneResult);
// 绑定输入时区事件 // 绑定输入时区事件
addEventListenerById('timezone-input', 'change', handleTimezoneInput); addEventListenerById('timezone-input', 'change', handleTimezoneInput);
}
+85 -30
View File
@@ -1,12 +1,20 @@
import { import { getParamsUrl, closure, genKey } from '../utils/routeUtils.js';
RouteUtils,
addClass, import { addClass, removeClass, hasClass } from '../utils/domUtils.js';
removeClass, // var config = {
hasClass, // routerViewId: '#routerView', // 路由切换的挂载点 id
getParamsUrl, // stackPages: true, // 多级页面缓存
closure, // animationName: "slide", // 多级页面缓存
genKey, // routes: [
} from '../utils/routeUtils.js'; // // {
// // path: "/home",
// // name: "home",
// // callback: function(transition) {
// // home()
// // }
// // }
// ]
// }
class Router { class Router {
constructor() { constructor() {
// 路由表 // 路由表
@@ -32,14 +40,23 @@ class Router {
} }
init(config) { init(config) {
this.routerMap = config ? config.routers : this.routerMap; this.routerMap = config ? config.routes : this.routerMap;
this.routerViewId = config ? config.routerViewId : this.routerViewId; this.routerViewId = config ? config.routerViewId : this.routerViewId;
this.stackPages = config ? config.stackPages : this.stackPages; this.stackPages = config ? config.stackPages : this.stackPages;
const name = document.querySelector('#app').getAttribute('data-animationName');
if (name) {
this.animationName = name;
}
this.animationName = config ? config.animationName : this.animationName;
this.map();
if (!this.routerMap.length) { if (!this.routerMap.length) {
let selector = this.routerViewId + '.page'; // 找到routerViewId 节点下的所有page 节点
let pages = document.querySelectorAll(selector); const pages = document.querySelectorAll(this.routerViewId + ' .page');
for (let i = 0; i < pages.length; i++) { for (let i = 0; i < pages.length; i++) {
// 遍历所有的page节点
let page = pages[i]; let page = pages[i];
let hash = page.getAttribute('hash'); let hash = page.getAttribute('hash');
let name = hash.substring(1); let name = hash.substring(1);
@@ -60,13 +77,13 @@ class Router {
} }
}; };
window.addEventListener( window.addEventListener('load', (event) => {
'load',
function (event) {
this.historyChange(event); this.historyChange(event);
}, });
false
); window.addEventListener('hashchange', (event) => {
this.historyChange(event);
});
} }
/** /**
@@ -74,29 +91,41 @@ class Router {
* @param {*} event * @param {*} event
*/ */
historyChange(event) { historyChange(event) {
// {path, query, params}
const currentHash = getParamsUrl(); const currentHash = getParamsUrl();
// router-#app-history
const nameString = 'router-' + this.routerViewId + '-history'; const nameString = 'router-' + this.routerViewId + '-history';
this.history = window.sessionStorage[nameString] this.history = window.sessionStorage[nameString]
? JSON.parse(window.sessionStorage[nameString]) ? JSON.parse(window.sessionStorage[nameString])
: []; : [];
let back = false, // 返回上一级
refresh = false, let back = false;
forward = false, // 刷新页面
index = 0, let refresh = false;
len = this.history.length; // 前进上一级
let forward = false;
// 获取当前路由的索引
let index = 0;
for (let i = 0; i < len; i++) { for (let i = 0; i < this.history.length; i++) {
let h = this.history[i]; let h = this.history[i];
// 判断是否是当前路由
if (h.hash === currentHash.path && h.key === currentHash.query.key) { if (h.hash === currentHash.path && h.key === currentHash.query.key) {
// 获取当前路由的索引
index = i; index = i;
if (i === len - 1) { // 判断是否是刷新页面
if (i === this.history.length - 1) {
refresh = true; refresh = true;
} else { } else {
// 判断是否是返回上一级
back = true; back = true;
} }
break; break;
} else { } else {
// 判断是否是前进
forward = true; forward = true;
} }
} }
@@ -124,18 +153,22 @@ class Router {
this.urlChange(); this.urlChange();
} }
/**
* 更改页面
* @param {*} currentHash
*/
changeView(currentHash) { changeView(currentHash) {
const pages = document.querySelectorAll('.page'); const pages = document.querySelectorAll(' .page');
const previousPage = document.querySelector('.' + this.routerViewId + ' .page.active'); const previousPage = document.querySelector('.' + this.routerViewId + ' .page.active');
let currentPage = null; let currentPage = null;
let currentHash = null; let currHash = null;
for (let i = 0; i < pages.length; i++) { for (let i = 0; i < pages.length; i++) {
let page = pages[i]; let page = pages[i];
let hash = page.getAttribute('hash'); let hash = page.getAttribute('hash');
page.setAttribute('class', 'page'); page.setAttribute('class', 'page');
if (hash === currentHash.path) { if (hash === currentHash.path) {
currentHash = hash; currHash = hash;
currentPage = page; currentPage = page;
} }
} }
@@ -167,12 +200,15 @@ class Router {
removeClass(currentPage, 'current'); removeClass(currentPage, 'current');
}, 300); }, 300);
currentPage.scrollTop = 0; currentPage.scrollTop = 0;
this.routes[currentHash].callback ? this.routes[currentHash].callback() : null; this.routes[currHash].callback ? this.routes[currHash].callback() : null;
} }
this.afterFun ? this.afterFun(currentHash) : null; this.afterFun ? this.afterFun(currentHash) : null;
} }
/**
* url改变
*/
urlChange() { urlChange() {
const currentHash = getParamsUrl(); const currentHash = getParamsUrl();
if (this.routes[currentHash.path]) { if (this.routes[currentHash.path]) {
@@ -194,6 +230,9 @@ class Router {
} }
} }
/**
* 映射路由
*/
map() { map() {
for (let i = 0; i < this.routerMap.length; i++) { for (let i = 0; i < this.routerMap.length; i++) {
let route = this.routerMap[i]; let route = this.routerMap[i];
@@ -204,26 +243,42 @@ class Router {
} }
let newPath = route.path; let newPath = route.path;
let path = newPath.repalce(/\s+/g, ''); let path = newPath.replace(/\s+/g, '');
this.routes[path] = { this.routes[path] = {
callback: route.callback, callback: route.callback,
}; };
} }
} }
/**
* 切换页面前的hook
* @param {*} callback hook执行函数
*/
beforeEach(callback) { beforeEach(callback) {
if (Object.prototype.toString.call(callback) === '[object Function]') { if (Object.prototype.toString.call(callback) === '[object Function]') {
// 判断callback是否为函数
this.beforeFun = callback; this.beforeFun = callback;
} else { } else {
// 抛出错误
console.trace('beforeEach callback must be a function'); console.trace('beforeEach callback must be a function');
} }
} }
/**
* 切换页面后的hook
* @param {*} callback hook执行函数
*/
afterEach(callback) { afterEach(callback) {
if (Object.prototype.toString.call(callback) === '[object Function]') { if (Object.prototype.toString.call(callback) === '[object Function]') {
// 判断callback是否为函数
this.afterFun = callback; this.afterFun = callback;
} else { } else {
// 抛出错误
console.trace('afterEach callback must be a function'); console.trace('afterEach callback must be a function');
} }
} }
} }
export { Router };
+1 -1
View File
@@ -1,4 +1,4 @@
<div id="page-timestamp" class="page active"> <div id="page-timestamp" class="page">
<h2>时间戳工具</h2> <h2>时间戳工具</h2>
<div id="current-timestamp"> <div id="current-timestamp">
<h2>当前时间戳</h2> <h2>当前时间戳</h2>
+3
View File
@@ -0,0 +1,3 @@
<div id="page-timestamp" class="page">
<h2>任务清单</h2>
</div>
+2 -22
View File
@@ -53,7 +53,8 @@
p a { p a {
box-sizing: border-box; box-sizing: border-box;
color: #2b2b2b; color: #2b2b2b;
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New,
monospace;
} }
button { button {
@@ -150,27 +151,6 @@
overflow: hidden; overflow: hidden;
} }
/* 页面切换动画 */
.page {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 1rem;
opacity: 0;
transform: translateX(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.page.active {
display: block;
position: relative;
opacity: 1;
transform: translateX(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* 按钮点击效果 */ /* 按钮点击效果 */
.button:active, .button:active,
.convert-button:active, .convert-button:active,
+44 -1
View File
@@ -49,7 +49,8 @@ export function switchPage(pageName) {
} }
// 激活对应的导航按钮 // 激活对应的导航按钮
const navButton = document.getElementById(`nav-${pageName}-btn`) || document.getElementById(`nav-${pageName}`); const navButton =
document.getElementById(`nav-${pageName}-btn`) || document.getElementById(`nav-${pageName}`);
if (navButton) { if (navButton) {
navButton.classList.add('active'); navButton.classList.add('active');
} }
@@ -76,3 +77,45 @@ export function addEventListenerById(id, event, handler) {
element.addEventListener(event, handler); element.addEventListener(event, handler);
} }
} }
/**
* 判断元素是否包含指定类名
* @param {*} elem
* @param {*} cls
* @returns
*/
export function hasClass(elem, cls) {
cls = cls || '';
// 检测类名是否为''
if (cls.replace(/\s/g, '').length === 0) return false;
// 检测elem类名是否包含cls指定类名
return new RegExp(' ' + cls + ' ').test(' ' + elem.className + ' ');
}
/**
* 为元素添加类名
* @param {*} elem
* @param {*} cls
*/
export function addClass(elem, cls) {
if (!hasClass(elem, cls)) {
elem.className = elem.className === '' ? cls : elem.className + ' ' + cls;
}
}
/**
* 为元素删除类名
* @param {*} elem
* @param {*} cls
*/
export function removeClass(elem, cls) {
if (hasClass(elem, cls)) {
let newClass = ' ' + elem.className.replace(/[\t\r\n]/g, '') + ' ';
// 删除指定类名
while (newClass.indexOf(' ' + cls + ' ') >= 0) {
newClass = newClass.replace(' ' + cls + ' ', ' ');
}
// 重新赋值
elem.className = newClass.replace(/^\s+|\s+$/);
}
}
+24 -35
View File
@@ -1,13 +1,16 @@
/** /**
* 获取路由的路径和详参数 * 获取路由的路径和详参数
* @returns * @returns
*/ */
export function getParamsUrl() { export function getParamsUrl() {
let hasDetail = location.hash.split('?'); // 获取路由
let hasName = hasDetail[0].split('#')[1]; const hasDetail = location.hash.split('?');
let params = hasDetail[1] ? hasDetail[1].split('&') : []; // 获取路由名称
const hasName = hasDetail[0].split('#')[1];
// 获取请求参数
const params = hasDetail[1] ? hasDetail[1].split('&') : [];
// 解析请求参数
let query = {}; let query = {};
for (let i = 0; i < params.length; i++) { for (let i = 0; i < params.length; i++) {
let param = params[i].split('='); let param = params[i].split('=');
query[param[0]] = param[1]; query[param[0]] = param[1];
@@ -20,40 +23,26 @@ export function getParamsUrl() {
}; };
} }
/**
* 闭包返回函数
* @param {*} name
* @returns
*/
export function closure(name) { export function closure(name) {
function fun(currentHash) { return (currentHash) => {
window.name&&window[name](currentHash) window.name && window[name](currentHash);
} };
return fun;
} }
/**
* 生成随机key
* @returns
*/
export function genKey() { export function genKey() {
let temp = 'xxxxxxxx'; const KEY_TEMPLATE = 'xxxxxxxx';
return temp.replace(/[xy]/g, function (c) { return KEY_TEMPLATE.replace(/[xy]/g, (c) => {
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16); return v.toString(16);
}); });
} }
export function hasClass(elem, cls) {
cls = cls || '';
if (cls.replace(/\s/g, '').length === 0)
return false;
return new RegExp(' ' + cls + ' ').test(' ' + elem.className + ' ');
}
export function addClass(elem, cls) {
if (!hasClass(elem, cls)) {
elem.className = elem.className === '' ? cls : elem.className + ' ' + cls;
}
}
export function removeClass(elem, cls) {
if (hasClass(elem, cls)) {
let newClass = ' ' + elem.className.replace(/[\t\r\n]/g, '') + ' ';
while (newClass.indexOf(' ' + cls + ' ') >= 0) {
newClass = newClass.replace(' ' + cls + ' ', ' ');
}
elem.className = newClass.replace(/^\s+|\s+$/)
}
}