diff --git a/index.html b/index.html
deleted file mode 100644
index a22fda8..0000000
--- a/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
- Testing Tool - Development
-
-
-
-
-
Testing Tool - Development Preview
-
这是一个用于开发预览的页面,下方是sidepanel的实时预览。
-
-
-
-
\ No newline at end of file
diff --git a/sidepanel/index.html b/sidepanel/index.html
index 9dfabcf..57812ef 100644
--- a/sidepanel/index.html
+++ b/sidepanel/index.html
@@ -10,8 +10,8 @@
- 时间戳转换
- 任务清单
+ 时间戳转换
+ 任务清单
X
diff --git a/sidepanel/js/index.js b/sidepanel/js/index.js
index d522987..bb07739 100644
--- a/sidepanel/js/index.js
+++ b/sidepanel/js/index.js
@@ -1,48 +1,21 @@
console.log('Before creating router');
import { Router } from '../modules/Router.js';
-import * as PageModules from '../../dist/timestamp.js';
-
const config = {
- routerViewId: 'app', // 路由切换的挂载点 id
- stackPages: false, // 多级页面缓存
- animationName: 'fade', // 切换页面时的动画
+ routerViewId: 'app',
+ stackPages: false,
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;
-
- // 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);
- },
+ name: 'timestamp',
+ html: 'pages/timestamp.html',
+ script: '../dist/timestamp.js',
},
{
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);
- },
+ html: 'pages/todolist.html',
+ script: '',
},
],
};
@@ -52,7 +25,13 @@ 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');
+document.addEventListener('click', (event) => {
+ const btn = event.target.closest('[data-route]');
+ if (!btn) return;
+
+ const path = btn.getAttribute('data-route');
+ if (!path) return;
+
+ // 触发 hash 路由跳转
+ window.location.hash = path;
});
diff --git a/sidepanel/js/timestamp.js b/sidepanel/js/timestamp.js
index 8291fe4..de167dd 100644
--- a/sidepanel/js/timestamp.js
+++ b/sidepanel/js/timestamp.js
@@ -12,27 +12,29 @@ import {
} from '../modules/eventHandlers.js';
import { addEventListenerById } from '../utils/domUtils.js';
-export function init() {
- console.log('timestamp init')
- // 初始化时间戳显示
- initTimestampDisplay();
+(function (global) {
+ global.timestampInit = async function () {
+ console.log('timestamp init');
+ // 初始化时间戳显示
+ initTimestampDisplay();
- // 绑定事件处理器
- // 绑定时间戳相关按钮事件
- addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit);
- // 绑定计时器相关按钮事件
- addEventListenerById('stop-timer-btn', 'click', handleStopTimer);
- addEventListenerById('start-timer-btn', 'click', handleStartTimer);
- // 绑定时间戳转换按钮事件
- addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp);
- // 绑定日期转换按钮事件
- addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp);
- // 绑定复制时间戳按钮事件
- addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp);
- // 绑定关闭面板按钮事件
- addEventListenerById('close-panel-btn', 'click', handleClosePanel);
- // 绑定切换时区事件
- addEventListenerById('timezone-result', 'change', handleTimezoneResult);
- // 绑定输入时区事件
- addEventListenerById('timezone-input', 'change', handleTimezoneInput);
-}
+ // 绑定事件处理器
+ // 绑定时间戳相关按钮事件
+ addEventListenerById('toggle-unit-btn', 'click', handleToggleUnit);
+ // 绑定计时器相关按钮事件
+ addEventListenerById('stop-timer-btn', 'click', handleStopTimer);
+ addEventListenerById('start-timer-btn', 'click', handleStartTimer);
+ // 绑定时间戳转换按钮事件
+ addEventListenerById('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp);
+ // 绑定日期转换按钮事件
+ addEventListenerById('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp);
+ // 绑定复制时间戳按钮事件
+ addEventListenerById('copy-timestamp-btn', 'click', handleCopyTimestamp);
+ // 绑定关闭面板按钮事件
+ addEventListenerById('close-panel-btn', 'click', handleClosePanel);
+ // 绑定切换时区事件
+ addEventListenerById('timezone-result', 'change', handleTimezoneResult);
+ // 绑定输入时区事件
+ addEventListenerById('timezone-input', 'change', handleTimezoneInput);
+ };
+})(window);
diff --git a/sidepanel/modules/Router.js b/sidepanel/modules/Router.js
index f45b096..33b2ad1 100644
--- a/sidepanel/modules/Router.js
+++ b/sidepanel/modules/Router.js
@@ -1,284 +1,179 @@
import { getParamsUrl, closure, genKey } from '../utils/routeUtils.js';
+import { addClass, removeClass } from '../utils/domUtils.js';
+import { loadScript } from '../utils/scriptLoader.js';
-import { addClass, removeClass, hasClass } from '../utils/domUtils.js';
-// var config = {
-// routerViewId: '#routerView', // 路由切换的挂载点 id
-// stackPages: true, // 多级页面缓存
-// animationName: "slide", // 多级页面缓存
-// routes: [
-// // {
-// // path: "/home",
-// // name: "home",
-// // callback: function(transition) {
-// // home()
-// // }
-// // }
-// ]
-// }
class Router {
constructor() {
- // 路由表
this.routes = {};
- // 路由跳转前执行
this.beforeFun = null;
- // 路由跳转后执行
this.afterFun = null;
- // 路由挂载点
this.routerViewId = 'router-view';
- // 路由重定向hash
this.redirectRoute = null;
- // 多级页面缓存
this.stackPages = true;
- // 路由遍历
this.routerMap = [];
- // 路由状态
this.historyFlag = '';
- // 路由历史
this.history = [];
- // 动画名称
- this.animationName = 'fade';
- }
-
- init(config) {
- this.routerMap = config ? config.routes : this.routerMap;
- this.routerViewId = config ? config.routerViewId : this.routerViewId;
- 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) {
- // 找到routerViewId 节点下的所有page 节点
- const pages = document.querySelectorAll(this.routerViewId + ' .page');
- for (let i = 0; i < pages.length; i++) {
- // 遍历所有的page节点
- let page = pages[i];
- let hash = page.getAttribute('hash');
- let name = hash.substring(1);
- this.routerMap.push({
- name: name,
- path: hash,
- callback: closure(name),
- });
- }
- }
-
- window.linkTo = (path) => {
- console.log('path', path);
- if (path.indexOf('?') !== -1) {
- window.location.hash = path + '&key=' + genKey();
- } else {
- window.location.hash = path + '?key=' + genKey();
- }
- };
-
- window.addEventListener('load', (event) => {
- this.historyChange(event);
- });
-
- window.addEventListener('hashchange', (event) => {
- this.historyChange(event);
- });
}
/**
- * 路由历史纪录变化
- * @param {*} event
+ * 初始化路由
+ */
+ init(config) {
+ this.routerMap = config?.routes || this.routerMap;
+ this.routerViewId = config?.routerViewId || this.routerViewId;
+ this.stackPages = config?.stackPages ?? this.stackPages;
+
+ // 映射路由表
+ this.map();
+
+ window.linkTo = (path) => {
+ if (path.includes('?')) {
+ location.hash = `${path}&key=${genKey()}`;
+ } else {
+ location.hash = `${path}?key=${genKey()}`;
+ }
+ };
+
+ window.addEventListener('load', (e) => {
+ this.historyChange(e);
+ this.updateActiveButton(e);
+ });
+ window.addEventListener('hashchange', (e) => {
+ this.historyChange(e);
+ this.updateActiveButton(e);
+ });
+ }
+
+ async loadPage(route) {
+ console.log('加载页面:', route);
+ const html = await fetch(route.html).then((r) => r.text());
+ document.getElementById(this.routerViewId).innerHTML = html;
+
+ const initName = route.name + 'Init';
+ await loadScript(route.script, initName);
+
+ if (window[initName] && route.script) {
+ await window[initName]();
+ }
+ }
+
+ /**
+ * 加载 HTML 文件
+ */
+ async loadHTML(htmlPath) {
+ try {
+ const resp = await fetch(htmlPath);
+ if (!resp.ok) throw new Error(`HTML 加载失败: ${resp.status}`);
+ return await resp.text();
+ } catch (err) {
+ return `
页面加载失败:${err.message} `;
+ }
+ }
+
+ /**
+ * 处理路由历史变化
*/
historyChange(event) {
- // {path, query, params}
- const currentHash = getParamsUrl();
- // router-#app-history
- const nameString = 'router-' + this.routerViewId + '-history';
-
- this.history = window.sessionStorage[nameString]
- ? JSON.parse(window.sessionStorage[nameString])
- : [];
-
- // 返回上一级
- let back = false;
- // 刷新页面
- let refresh = false;
- // 前进上一级
- let forward = false;
- // 获取当前路由的索引
- let index = 0;
-
- for (let i = 0; i < this.history.length; i++) {
- let h = this.history[i];
-
- // 判断是否是当前路由
- if (h.hash === currentHash.path && h.key === currentHash.query.key) {
- // 获取当前路由的索引
- index = i;
- // 判断是否是刷新页面
- if (i === this.history.length - 1) {
- refresh = true;
- } else {
- // 判断是否是返回上一级
- back = true;
- }
- break;
- } else {
- // 判断是否是前进
- forward = true;
- }
- }
-
- if (back) {
- this.historyFlag = 'back';
- this.history.length = index + 1;
- } else if (refresh) {
- this.historyFlag = 'refresh';
- } else {
- this.historyFlag = 'forward';
- this.history.push({
- key: currentHash.query.key,
- hash: currentHash.path,
- query: currentHash.query,
- });
- }
-
- console.log('historyFlag', this.historyFlag);
-
- if (!this.stackPages) {
- this.historyFlag = 'forward';
- }
- window.sessionStorage[nameString] = JSON.stringify(this.history);
+ const { path, query } = getParamsUrl();
+ console.log('路由变化page{}, query{}', path, query);
this.urlChange();
}
/**
- * 更改页面
- * @param {*} currentHash
- */
- changeView(currentHash) {
- const pages = document.querySelectorAll(' .page');
- const previousPage = document.querySelector('.' + this.routerViewId + ' .page.active');
- let currentPage = null;
- let currHash = null;
-
- for (let i = 0; i < pages.length; i++) {
- let page = pages[i];
- let hash = page.getAttribute('hash');
- page.setAttribute('class', 'page');
- if (hash === currentHash.path) {
- currHash = hash;
- currentPage = page;
- }
- }
-
- const enterName = 'enter-' + this.animationName;
- const leaveName = 'leave-' + this.animationName;
-
- if (this.historyFlag === 'back') {
- addClass(currentPage, 'current');
- if (previousPage) {
- addClass(previousPage, leaveName);
- }
-
- setTimeout(() => {
- if (previousPage) {
- removeClass(previousPage, leaveName);
- }
- }, 300);
- } else if (this.historyFlag === 'forward' || this.historyFlag === 'refresh') {
- if (previousPage) {
- addClass(previousPage, leaveName);
- }
- addClass(currentPage, enterName);
- setTimeout(() => {
- if (previousPage) {
- removeClass(previousPage, leaveName);
- }
- addClass(currentPage, enterName);
- removeClass(currentPage, 'current');
- }, 300);
- currentPage.scrollTop = 0;
- this.routes[currHash].callback ? this.routes[currHash].callback() : null;
- }
-
- this.afterFun ? this.afterFun(currentHash) : null;
- }
-
- /**
- * url改变
+ * 路由解析与渲染
*/
urlChange() {
const currentHash = getParamsUrl();
- if (this.routes[currentHash.path]) {
- if (this.beforeFun) {
- this.beforeFun({
- to: {
- path: currentHash.path,
- query: currentHash.query,
- },
- next: () => {
- this.changeView(currentHash);
- },
- });
- } else {
- this.changeView(currentHash);
- }
- } else {
+ const { path, query } = currentHash;
+
+ if (!this.routes[path]) {
location.hash = this.redirectRoute;
+ return;
+ }
+
+ const next = () => this.changeView(currentHash);
+
+ if (this.beforeFun) {
+ this.beforeFun({ to: { path, query }, next });
+ } else {
+ next();
}
}
/**
- * 映射路由
+ * 渲染页面
+ */
+ async changeView(currentHash) {
+ const { path } = currentHash;
+ const route = await this.routes[path];
+ const mountEl = document.getElementById(this.routerViewId);
+
+ if (!mountEl) {
+ console.error('挂载点不存在:', this.routerViewId);
+ return;
+ }
+
+ console.log('路由:', route);
+ this.loadPage(route);
+
+ // 执行 after 钩子
+ if (this.afterFun) this.afterFun(currentHash);
+ }
+
+ /**
+ * 构建路由映射
*/
map() {
- for (let i = 0; i < this.routerMap.length; i++) {
- let route = this.routerMap[i];
- if (route.name === 'redirect') {
- this.redirectRoute = route.path;
- } else {
+ for (let r of this.routerMap) {
+ if (r.name === 'redirect') {
+ this.redirectRoute = r.path;
+ } else if (!this.redirectRoute) {
this.redirectRoute = this.routerMap[0].path;
}
- let newPath = route.path;
- let path = newPath.replace(/\s+/g, '');
- this.routes[path] = {
- callback: route.callback,
+ this.routes[r.path] = {
+ name: r.name,
+ html: r.html,
+ script: r.script,
};
}
}
/**
- * 切换页面前的hook
- * @param {*} callback hook执行函数
+ * before 钩子
*/
-
beforeEach(callback) {
- if (Object.prototype.toString.call(callback) === '[object Function]') {
- // 判断callback是否为函数
+ if (typeof callback === 'function') {
this.beforeFun = callback;
} else {
- // 抛出错误
- console.trace('beforeEach callback must be a function');
+ console.trace('beforeEach 必须是函数');
}
}
/**
- * 切换页面后的hook
- * @param {*} callback hook执行函数
+ * after 钩子
*/
-
afterEach(callback) {
- if (Object.prototype.toString.call(callback) === '[object Function]') {
- // 判断callback是否为函数
+ if (typeof callback === 'function') {
this.afterFun = callback;
} else {
- // 抛出错误
- console.trace('afterEach callback must be a function');
+ console.trace('afterEach 必须是函数');
}
}
+
+ updateActiveButton(event) {
+ const currentRoute = location.hash.replace('#', '') || '/home';
+
+ document.querySelectorAll('[data-route]').forEach((btn) => {
+ const route = btn.getAttribute('data-route');
+ if (route === currentRoute) {
+ console.log('当前路由:', route);
+ addClass(btn, 'active');
+ } else {
+ removeClass(btn, 'active');
+ }
+ });
+ }
}
export { Router };
diff --git a/sidepanel/utils/domUtils.js b/sidepanel/utils/domUtils.js
index 0f56feb..5e9894c 100644
--- a/sidepanel/utils/domUtils.js
+++ b/sidepanel/utils/domUtils.js
@@ -98,9 +98,18 @@ export function hasClass(elem, cls) {
* @param {*} cls
*/
export function addClass(elem, cls) {
- if (!hasClass(elem, cls)) {
- elem.className = elem.className === '' ? cls : elem.className + ' ' + cls;
- }
+ if (!elem || !cls) return;
+
+ // 统一空白字符(避免 \n \t 等导致匹配问题)
+ let current = elem.className.replace(/[\t\r\n]/g, ' ').trim();
+
+ // 已存在则忽略
+ const classes = current.split(/\s+/);
+ if (classes.includes(cls)) return;
+
+ // 添加并规整
+ classes.push(cls);
+ elem.className = classes.join(' ').trim();
}
/**
@@ -109,13 +118,22 @@ export function addClass(elem, cls) {
* @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+$/);
+ if (!elem || !cls) return;
+
+ // 使用 classList 优先(更安全)
+ if (elem.classList) {
+ elem.classList.remove(cls);
+ return;
}
-}
+
+ // 传统写法的修复版本
+ let klass = ' ' + elem.className.replace(/[\t\r\n]/g, ' ') + ' ';
+
+ // 持续删除目标 class
+ while (klass.indexOf(' ' + cls + ' ') !== -1) {
+ klass = klass.replace(' ' + cls + ' ', ' ');
+ }
+
+ // 过滤多余空格
+ elem.className = klass.trim().replace(/\s+/g, ' ');
+}
\ No newline at end of file
diff --git a/sidepanel/utils/scriptLoader.js b/sidepanel/utils/scriptLoader.js
new file mode 100644
index 0000000..8ac4a96
--- /dev/null
+++ b/sidepanel/utils/scriptLoader.js
@@ -0,0 +1,30 @@
+let currentScript = null;
+export function loadScript(path, globalInitName) {
+ console.log('load script', currentScript);
+
+ if (currentScript) {
+ console.log('remove script')
+ currentScript.remove();
+ currentScript = null;
+ }
+
+ if (globalInitName && window[globalInitName]) {
+ delete window[globalInitName];
+ }
+
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = path;
+ script.type = 'module';
+
+ script.onload = () => {
+ currentScript = document.querySelector(`script[src='${path}']`);
+ console.log('script loaded', currentScript);
+ resolve();
+ };
+
+ script.onerror = reject;
+
+ document.body.appendChild(script);
+ });
+}