chore(project): 初始化 React 项目并更新依赖配置

- 将项目从 Chrome 扩展模板迁移到 Create React App
- 移除原有的 Chrome 扩展相关文件(background.js、content.js、manifest.json)
- 删除旧的开发指南 DEVELOPMENT.md
- 更新 package-lock.json 以支持 React 开发环境
- 替换 .gitignore 文件内容为标准 React 项目忽略规则
- 移除所有原有的图标资源文件(images/*.png)
- 更新 README.md 为 Create React App 标准文档
- 添加 React、React DOM、React Router 等核心依赖
- 配置测试库(@testing-library)及相关工具
- 设置 web-vitals 性能监控依赖
This commit is contained in:
雨霖铃
2025-12-16 23:42:59 +08:00
parent 52f6dcce3d
commit 1cc5c5f0c4
46 changed files with 17414 additions and 2108 deletions
+17 -17
View File
@@ -1,23 +1,23 @@
# Dependencies # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
# Build outputs # dependencies
dist/ /node_modules
/.pnp
.pnp.js
# Environment variables # testing
.env /coverage
# IDE files # production
.vscode/ /build
.idea/
# OS generated files # misc
.DS_Store .DS_Store
Thumbs.db .env.local
.env.development.local
.env.test.local
.env.production.local
# Log files npm-debug.log*
*.log yarn-debug.log*
yarn-error.log*
# Package files
*.zip
*.tar.gz
-51
View File
@@ -1,51 +0,0 @@
# 开发指南
## 启动开发服务器(带热重载)
要启动带有热重载功能的开发服务器,请运行以下命令:
```bash
npm run dev
```
这将会:
1. 启动 Rollup 的监视模式
2. 自动重新编译更改的文件
3. 通过 LiveReload 刷新浏览器
4. 在 http://localhost:8082 启动开发服务器
## 项目结构
```
project/
├── sidepanel/ # Chrome扩展侧边栏面板
│ ├── index.html # 侧边栏主页面
│ ├── index.js # 侧边栏主脚本
│ ├── index.css # 侧边栏样式
│ └── ... # 其他相关文件
├── dist/ # 构建输出目录
└── ... # 其他配置和文档文件
```
## 预览方式
有两种方式可以预览您的更改:
1. 直接访问 `http://localhost:8082/sidepanel/index.html` 查看sidepanel的实际效果
2. 访问 `http://localhost:8082/index.html` 查看包含sidepanel预览的开发页面
## 构建生产版本
要构建生产版本,请运行:
```bash
npm run build
```
这将生成优化后的 bundle 文件到 `dist/sidepanel.bundle.js`
## 热重载工作原理
- 当你修改 [sidepanel](file:///D:/Work/testing-tool/sidepanel) 目录下的任何文件时,Rollup 会自动重新编译
- 编译完成后,LiveReload 会自动刷新浏览器页面
- 无需手动刷新浏览器来查看更改效果
+65 -18
View File
@@ -1,23 +1,70 @@
# Using the Gemini API in a Chrome Extension. # Getting Started with Create React App
This sample demonstrates how to use the Gemini Cloud API in a Chrome Extension. This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Overview ## Available Scripts
The extension provides a chat interface for the Gemini API. To learn more about the API head over to [https://ai.google.dev/](https://ai.google.dev/). In the project directory, you can run:
## Running this extension ### `npm start`
1. Clone this repository. Runs the app in the development mode.\
2. Download the Gemini API client by running: Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
```sh
npm install The page will reload when you make changes.\
``` You may also see any lint errors in the console.
3. [Retrieve an API key](https://ai.google.dev/gemini-api/docs/api-key) and update [functional-samples/ai.gemini-in-the-cloud/sidepanel/index.js](functional-samples/ai.gemini-in-the-cloud/sidepanel/index.js) (only for testing).
4. Compile the JS bundle for the sidepanel implementation by running: ### `npm test`
```sh
npm run build Launches the test runner in the interactive watch mode.\
``` See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
5. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
6. Click the extension icon. ### `npm run build`
7. Interact with the prompt API in the sidebar.
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
-22
View File
@@ -1,22 +0,0 @@
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js'],
});
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'copyText') {
// 转发到当前活动的标签页
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]?.id) {
chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
sendResponse(response);
});
} else {
sendResponse({ success: false });
}
});
return true; // 异步响应
}
});
-98
View File
@@ -1,98 +0,0 @@
(() => {
const SIDEBAR_ID = 'floating-sidebar';
const TOGGLE_ID = 'floating-toggle-btn';
// 若侧边栏已存在则移除
const existing = document.getElementById(SIDEBAR_ID);
const existingBtn = document.getElementById(TOGGLE_ID);
if (existing && existingBtn) {
existing.remove();
existingBtn.remove();
return;
}
// 创建 iframe
const iframe = document.createElement('iframe');
iframe.id = SIDEBAR_ID;
iframe.src = chrome.runtime.getURL('sidepanel/index.html');
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
right: '0',
width: '360px',
height: '100%',
border: 'none',
zIndex: '2147483647',
boxShadow: '-4px 0 8px rgba(0,0,0,0.15)',
backgroundColor: 'white',
transform: 'translateX(100%)', // 默认关闭状态
transition: 'transform 0.3s ease',
});
document.body.appendChild(iframe);
// 创建控制按钮
const toggleBtn = document.createElement('div');
toggleBtn.id = TOGGLE_ID;
toggleBtn.textContent = '≡';
Object.assign(toggleBtn.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
width: '48px',
height: '48px',
backgroundColor: '#0078d7',
color: 'white',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '22px',
cursor: 'pointer',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
zIndex: '2147483648',
transition: 'background-color 0.2s ease',
});
toggleBtn.addEventListener('mouseenter', () => {
toggleBtn.style.backgroundColor = '#005fa3';
});
toggleBtn.addEventListener('mouseleave', () => {
toggleBtn.style.backgroundColor = '#0078d7';
});
document.body.appendChild(toggleBtn);
// 折叠 / 展开逻辑
let visible = false; // 默认关闭状态
toggleBtn.addEventListener('click', () => {
visible = !visible;
iframe.style.transform = visible ? 'translateX(0)' : 'translateX(100%)';
});
// 如果使用 window.postMessage
window.addEventListener('message', function (event) {
// 检查消息来源是否可信 (非常重要!)
if (event.origin !== 'chrome-extension://njacfkelcmfnpdbnbkpenogmokmglkcn') {
return;
}
if (event.data.action === 'closeSidebar') {
console.log('Received closeSidebar message');
visible = !visible;
iframe.style.transform = visible ? 'translateX(0)' : 'translateX(100%)';
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'copyText') {
navigator.clipboard
.writeText(message.data)
.then(() => sendResponse({ success: true }))
.catch((err) => {
console.error('❌ 复制失败:', err);
sendResponse({ success: false, error: err });
});
return true;
}
});
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 B

-25
View File
@@ -1,25 +0,0 @@
{
"manifest_version": 3,
"name": "Floating Sidebar with Toggle Button",
"version": "1.1",
"description": "带有可折叠隐藏按钮的浮动侧边栏",
"permissions": ["activeTab", "scripting", "clipboardWrite", "clipboardRead"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "打开侧边栏"
},
"web_accessible_resources": [
{
"resources": ["sidepanel/*"],
"matches": ["<all_urls>"]
}
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
+16914 -493
View File
File diff suppressed because it is too large Load Diff
+36 -15
View File
@@ -1,19 +1,40 @@
{ {
"name": "Chrome Extensions Gemini Demo", "name": "chrome-extension-router-demo",
"version": "1.1", "version": "0.1.0",
"type": "module",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
},
"private": true, "private": true,
"devDependencies": { "dependencies": {
"@google/generative-ai": "0.15.0", "@testing-library/dom": "^10.4.1",
"@rollup/plugin-commonjs": "^25.0.8", "@testing-library/jest-dom": "^6.9.1",
"@rollup/plugin-json": "^6.1.0", "@testing-library/react": "^16.3.1",
"@rollup/plugin-node-resolve": "^15.3.1", "@testing-library/user-event": "^13.5.0",
"rollup": "4.22.4", "react": "^19.2.3",
"rollup-plugin-livereload": "^2.0.5", "react-dom": "^19.2.3",
"rollup-plugin-serve": "^3.0.0" "react-router-dom": "^6.30.2",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"manifest_version": 3,
"name": "React Router Chrome Extension",
"version": "1.0",
"description": "A Chrome Extension with React Router: User List + Actions Pages",
"permissions": ["storage"],
"icons": {
"16": "favicon.ico",
"48": "favicon.ico",
"128": "favicon.ico"
},
"action": {
"default_popup": "index.html"
},
"options_ui": {
"page": "index.html",
"open_in_tab": true
}
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
-45
View File
@@ -1,45 +0,0 @@
import {defineConfig} from 'rollup';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import livereload from 'rollup-plugin-livereload';
import serve from 'rollup-plugin-serve';
const basePlugins = [resolve(), commonjs(), json()];
const devPlugins = [
livereload({
watch: ['dist', 'sidepanel'],
verbose: false,
}),
serve({
open: true,
port: 3000,
contentBase: ['.', 'sidepanel'],
headers: {
'Access-Control-Allow-Origin': '*',
},
}),
];
export default defineConfig([
{
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],
},
]);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 B

-27
View File
@@ -1,27 +0,0 @@
<!-- sidepanel/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试工具</title>
<link rel="stylesheet" href="style/style.css" />
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<button class="nav-button active" id="timestamp-jump" data-route="/home">时间戳转换</button>
<button class="nav-button" id="todo-jump" data-route="/todo">任务清单</button>
<button id="close-panel-btn" class="close-button">X</button>
</nav>
<div id="app">
<div class="page" hash="/home">
<div class="page-content">
<div id="home"></div>
</div>
</div>
</div>
<script src="../dist/index.js"></script>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
export class BaseComponent {
constructor() {
this._timerManager = window.timerManager;
this._scriptManager = window.scriptManager;
this._eventManager = window.eventManager;
}
/**
* 安全绑定事件,并自动记录以记录销毁
* @param {string | HTMLElement} target 目标元素
* @param {string} type 事件类型
* @param {Function} handler 回调函数
*/
bindEvent(target, type, handler) {
// 获取目标元素
const ele = typeof target === 'string' ? document.getElementById(target) : target;
if (!ele) {
console.error(`Element with ID ${target} not found.`);
return;
}
ele.addEventListener(type, handler);
this._eventManager.add(ele, type, handler);
}
/**
* 创建定时器
* @param {Function} fn 定时器回调函数
* @param {number} ms 定时器间隔时间(毫秒)
*/
setInterval(fn, ms) {
return this._timerManager.setInterval(fn, ms);
}
/**
* 创建定时器
* @param {Function} fn 定时器回调函数
* @param {number} ms 定时器间隔时间(毫秒)
*/
setTimeout(fn, ms) {
return this._timerManager.setTimeout(fn, ms);
}
/**
* 加载脚本
* @param {string} path 脚本路径
* @param {string} name 脚本名称
* @param {boolean} isModule 是否为模块
* @param {string[]} deps 依赖的脚本
*/
loadScript(path, name, isModule = false, deps = []) {
this._scriptManager.loadScript({path, name, isModule, deps});
}
/**
* 销毁组件
*/
destroy() {
this._eventManager.removeAll().then(() => console.log('clean event successful'));
this._timerManager.cleanAll().then(() => console.log('clean timer successful'));
this._scriptManager.unloadScript().then(r => console.log('unload script successful'));
console.log('destroy success');
}
}
window.BaseComponent = BaseComponent;
-20
View File
@@ -1,20 +0,0 @@
export const timeZoneList = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Anchorage',
'America/Honolulu',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
'Europe/Moscow',
'Asia/Tokyo',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Singapore',
'Asia/Dubai',
'Asia/Kolkata',
'Australia/Sydney',
'Pacific/Auckland',
];
-49
View File
@@ -1,49 +0,0 @@
import {ScriptManager} from './utils/scriptManager.js';
import {TimerManager} from './utils/timerManager.js';
import {EventManager} from './utils/eventManager.js';
import {Router} from '../modules/Router.js';
const config = {
routerViewId: 'app',
stackPages: false,
redirectRoute: '/',
routes: [
{
path: '/',
name: 'timestamp',
html: 'pages/timestamp.html',
script: '../dist/timestamp.js',
},
{
path: '/home',
name: 'timestamp',
html: 'pages/timestamp.html',
script: '../dist/timestamp.js',
},
{
path: '/todo',
name: 'todo',
html: 'pages/todolist.html',
},
],
};
// 注入脚本管理器
window.scriptManager = new ScriptManager();
// 注入定时器管理器
window.timerManager = new TimerManager();
// 注入事件管理器
window.eventManager = new EventManager();
const router = new Router();
router.init(config);
// click delegation
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-route]');
if (!btn) return;
const p = btn.getAttribute('data-route');
location.hash = p;
// update active styling
document.querySelectorAll('[data-route]').forEach((b) => b.classList.toggle('active', b === btn));
});
-92
View File
@@ -1,92 +0,0 @@
import {
handleClosePanel,
handleConvertDateToTimestamp,
handleConvertTimestamp,
handleCopyTimestamp,
handleStartTimer,
handleStopTimer,
handleTimezoneInput,
handleTimezoneResult,
handleToggleUnit,
} from '../modules/eventHandlers.js';
import {updateTimestamp} from './utils/timestampUtils.js';
import {timeZoneList} from './const/timezone.js';
import {BaseComponent} from './components/BaseComponet.js';
// 获取本地时区
const localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
class Timestamp extends BaseComponent {
constructor() {
super();
// 定义一个全局变量来保存是否显示毫秒
this.showMilliseconds = true;
// 定义一个全局变量来保存计时器
this.timestampInterval = null;
}
async init() {
const currentTimestampValue = document.getElementById('current-timestamp-value');
// 初始化时更新一次时间戳
updateTimestamp(currentTimestampValue, this.showMilliseconds);
this.timestampInterval = this.setInterval(() =>
updateTimestamp(currentTimestampValue, this.showMilliseconds),
100);
const timezoneResultSelect = document.getElementById('timezone-result');
const timezoneInputSelect = document.getElementById('timezone-input');
const localTimeZoneIndex = timeZoneList.indexOf(localTimeZone);
for (let i = 0; i < timeZoneList.length; i++) {
if (i === localTimeZoneIndex) {
// 默认选中本地时区
timezoneResultSelect.add(new Option(timeZoneList[i], i, true, true));
timezoneInputSelect.add(new Option(timeZoneList[i], i, true, true));
} else {
timezoneResultSelect.add(new Option(timeZoneList[i], i));
timezoneInputSelect.add(new Option(timeZoneList[i], i));
}
}
const timestampInput = document.getElementById('timestamp-input');
const dateInput = document.getElementById('datetime-input');
timestampInput.value = Date.now();
dateInput.value = new Date().toLocaleString('sv-SE');
this.bindBtnEvent();
}
bindBtnEvent() {
// 绑定时间戳相关按钮事件
this.bindEvent(
'toggle-unit-btn',
'click',
() => (this.showMilliseconds = handleToggleUnit(this.showMilliseconds))
);
// 绑定计时器相关按钮事件
this.bindEvent('stop-timer-btn', 'click', () => handleStopTimer(this.timestampInterval));
// 绑定开始计时器事件
this.bindEvent(
'start-timer-btn',
'click',
() =>
(this.timestampInterval = handleStartTimer(this.timestampInterval, this.showMilliseconds))
);
// 绑定时间戳转换按钮事件
this.bindEvent('convert-timestamp-to-date-btn', 'click', handleConvertTimestamp);
// 绑定日期转换按钮事件
this.bindEvent('convert-date-to-timestamp-btn', 'click', handleConvertDateToTimestamp);
// 绑定复制时间戳按钮事件
this.bindEvent('copy-timestamp-btn', 'click', handleCopyTimestamp);
// 绑定关闭面板按钮事件
this.bindEvent('close-panel-btn', 'click', handleClosePanel);
// 绑定切换时区事件
this.bindEvent('timezone-result', 'change', handleTimezoneResult);
// 绑定输入时区事件
this.bindEvent('timezone-input', 'change', handleTimezoneInput);
}
}
window.timestamp = Timestamp;
-76
View File
@@ -1,76 +0,0 @@
/**
* DOM工具类函数
*/
/**
* 为元素添加事件监听器
* @param {string} id - 元素ID
* @param {string} event - 事件类型
* @param {Function} handler - 事件处理函数
*/
export function addEventListenerById(id, event, handler) {
const ele = document.getElementById(id);
if (ele) {
window.eventManager.add(ele, 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 (!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();
}
/**
* 为元素删除类名
* @param {*} elem
* @param {*} cls
*/
export function removeClass(elem, cls) {
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, ' ');
}
-41
View File
@@ -1,41 +0,0 @@
export class EventManager {
constructor() {
this.listeners = [];
}
add(ele, type, handler, options) {
ele.addEventListener(type, handler, options);
this.listeners.push({ele, type, handler, options});
}
remove(ele, type, handler, options) {
ele.removeEventListener(type, handler, options);
this.listeners = this.listeners.filter(
(listener) =>
listener.ele !== ele ||
listener.type !== type ||
listener.handler !== handler ||
listener.options !== options
);
}
removeAll() {
return new Promise((resolve, reject) => {
this.listeners.onload = () => {
this.listeners.forEach((listener) => {
listener.ele.removeEventListener(
listener.type,
listener.handler,
listener.options
);
});
resolve();
};
this.listeners.onerror = () => {
console.error('unload event error');
reject();
};
this.listeners = [];
});
}
}
-97
View File
@@ -1,97 +0,0 @@
export class ScriptManager {
constructor() {
this._loadedClasses = new Map();
this.currentScriptEl = null;
this.currentModule = null;
this.currentName = null;
}
async loadScript({path, name, isModule = false, deps = []}) {
console.log('loadScript', name);
if (this._loadedClasses.has(name)) {
console.log(`Component ${name} loaded from cache`);
}
console.log('deps', deps);
for (const dep of deps) {
await this._appendScript({path: dep, isModule: false});
}
let componentReference = null;
console.log(`isModule:${isModule}`);
if (isModule) {
const module = await import(path);
this.currentModule = module;
componentReference = module;
} else {
await this._appendScript({path, isModule: false});
componentReference = window[name] || null;
}
if (componentReference) {
this._loadedClasses.set(name, componentReference);
}
this.currentName = name;
return componentReference;
}
async _appendScript({path, isModule}) {
const s = document.createElement('script');
s.src = path;
s.type = isModule ? 'module' : 'text/javascript';
s.async = false;
return new Promise((resolve, reject) => {
s.onload = () => {
this.currentScriptEl = s;
console.log(s);
resolve();
};
s.onerror = () => {
reject(new Error(`Failed to load script: ${path}`));
};
document.body.appendChild(s);
});
}
async unloadScript() {
if (this.currentScriptEl) {
try {
this.currentScriptEl.remove();
} catch (e) {
console.error(e);
}
}
this.currentModule = null;
this.currentName = null;
this.currentScriptEl = null;
}
async getComponentInstance(name) {
const ComponentClassReference = this._loadedClasses.get(name);
const BaseClassForCheck = window.BaseComponent;
if (!ComponentClassReference) {
console.error('ComponentClassReference not found');
return null
}
const ComponentClass = ComponentClassReference.default
|| ComponentClassReference;
if (typeof ComponentClass === 'function') {
if (!(ComponentClass.prototype instanceof BaseClassForCheck)) {
console.error(`Component ${name} does not inherit BaseComponent.`);
return null;
}
return new ComponentClass();
}
console.error(`Component ${name} does not exist`);
return null;
}
}
-63
View File
@@ -1,63 +0,0 @@
export class TimerManager {
constructor() {
// 定时器对象数组
this.timers = [];
}
/**
* 添加定时器到数组中
* @param {*} fn
* @param {*} delay
* @param {...any} args
* @returns
*/
setTimeout(fn, delay, ...args) {
const id = window.setTimeout(fn, delay, ...args);
this.timers.push(id);
return id;
}
/**
* 添加定时器到数组中
* @param {*} fn
* @param {*} delay
* @param {...any} args
* @returns
*/
setInterval(fn, delay, ...args) {
const id = window.setInterval(fn, delay, ...args);
this.timers.push(id);
return id;
}
/**
* 清除定时器
* @param {*} id
*/
clearTimeout(id) {
window.clearTimeout(id);
this.timers = this.timers.filter((timerId) => timerId !== id);
}
clearInterval(id) {
window.clearInterval(id);
this.timers = this.timers.filter((timerId) => timerId !== id);
}
cleanAll() {
return new Promise((resolve, reject) => {
this.timers.onload = () => {
this.timers.forEach((timerId) => {
window.clearTimeout(timerId);
window.clearInterval(timerId);
});
resolve();
};
this.timers.onerror = () => {
console.log('unload timer error');
reject();
}
this.timers = [];
});
}
}
-113
View File
@@ -1,113 +0,0 @@
/**
* 时间戳工具类函数
*/
/**
* 更新时间戳显示
* @param {*} ele
* @param {*} showMilliseconds
*/
export function updateTimestamp(ele, showMilliseconds = true) {
const timestamp = Date.now();
ele.textContent = showMilliseconds ? timestamp : Math.floor(timestamp / 1000);
}
/**
* 转换时间戳为日期格式
* @param {string} timestamp - 输入的时间戳
* @param {boolean} isSeconds - 是否为秒格式
* @param {string} timeZone - 时区,默认为本地时区
* @returns {string} 格式化后的日期字符串
*/
export function convertTimestampToDate(
timestamp,
isSeconds,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
) {
// 转换为数字
let timestampNum = Number(timestamp);
// 如果是秒,转换为毫秒
if (isSeconds) {
timestampNum = timestampNum * 1000;
}
// 检查是否为有效数字
if (isNaN(timestampNum)) {
return '请输入有效的时间戳';
}
// 创建日期对象
const date = new Date(timestampNum);
// 检查日期是否有效
if (isNaN(date.getTime())) {
return '无效的日期';
}
// 使用Intl.DateTimeFormat来根据时区格式化日期
const formatter = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timeZone,
});
return formatter.format(date);
}
/**
* 转换日期格式为时间戳
* @param {string|Date} date - 输入的日期字符串或Date对象
* @param {string} timeZone - 时区,默认为本地时区
* @returns {number|string} 时间戳或错误信息
*/
export function convertDateToTimestamp(
date,
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
) {
try {
// 创建日期对象
const dateObj = new Date(date);
// 检查日期是否有效
if (isNaN(dateObj.getTime())) {
return '无效的日期';
}
// 如果提供了时区,则需要特殊处理
if (timeZone) {
// 获取给定时区相对于UTC的时间差(毫秒)
const utc = dateObj.getTime() + dateObj.getTimezoneOffset() * 60000;
// 计算目标时区相对于UTC的偏移量
const targetOffset = getTimeZoneOffset(timeZone);
// 返回目标时区对应的时间戳
const targetTimestamp = utc + targetOffset;
return targetTimestamp;
} else {
// 没有时区参数,直接返回时间戳
const timestamp = dateObj.getTime();
console.log('timestamp: ', timestamp);
return timestamp;
}
} catch (error) {
return '日期转换错误: ' + error.message;
}
}
/**
* 获取指定时区相对于UTC的偏移量(毫秒)
* @param {string} timeZone - 时区名称
* @returns {number} 偏移量(毫秒)
*/
function getTimeZoneOffset(timeZone) {
const now = new Date();
const utc = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }));
const target = new Date(now.toLocaleString('en-US', { timeZone: timeZone }));
return target.getTime() - utc.getTime();
}
-143
View File
@@ -1,143 +0,0 @@
class Router {
constructor() {
this.routes = {};
this.beforeFun = null;
this.afterFun = null;
this.routerViewId = 'app';
this.redirectRoute = '/';
this.stackPages = true;
this.routerMap = [];
this.scriptManager = window.scriptManager;
// 页面实例
this.currentinstance = null;
}
/**
* 初始化路由
*/
init(config) {
// 配置路由
this.routerMap = config?.routes || this.routerMap;
this.routerViewId = config?.routerViewId || this.routerViewId;
this.stackPages = config?.stackPages ?? this.stackPages;
this.redirectRoute = config?.redirectRoute || this.redirectRoute;
this.map();
// 监听路由变化
window.addEventListener('hashchange', () => this.urlChange());
window.addEventListener('load', () => this.urlChange());
window.lintTo = (path) => this.navigate(path);
}
map() {
if (!this.routerMap.length) {
console.error('请配置路由');
return;
}
for (const r of this.routerMap) {
if (r.name === 'redirect') this.redirectRoute = r.path;
this.routes[r.path] = r;
}
}
/**
* 导航
*/
navigate(path) {
window.location.hash = path;
}
/**
* 路由解析与渲染
*/
urlChange() {
const path = location.hash.replace('#', '') || this.redirectRoute;
const route = this.routes[path];
if (!route) {
// 当前路由不存在时,返回到设置的重定向页面
location.hash = this.redirectRoute;
return;
}
const doChange = async () => {
if (this.currentinstance && typeof this.currentinstance.destroy === 'function') {
console.log(`销毁旧组件${this.currentRoute.name}`);
this.currentinstance.destroy();
this.currentinstance = null;
}
const mount = document.getElementById(this.routerViewId);
if (!mount) {
console.error('挂载点不存在:', this.routerViewId);
return;
}
if (!this.stackPages) mount.innerHTML = '';
if (route.html) {
try {
mount.innerHTML = await fetch(route.html).then((r) => r.text());
} catch (e) {
mount.innerHTML = `<h2>页面加载失败:${e.message}</h2>`;
}
}
console.log('加载组件', route.name);
if (!route.script) {
return;
}
await this.scriptManager.loadScript({
path: route.script,
name: route.name,
isModule: route.isModule,
deps: route.deps,
});
const newInstance = await this.scriptManager.getComponentInstance(route.name);
console.log(newInstance);
if (newInstance) {
this.currentinstance = newInstance;
if (typeof newInstance.init === 'function') {
newInstance.init();
}
}
this.currentRoute = route;
if (this.afterFun) this.afterFun(route);
};
if (this.beforeFun) {
this.beforeFun({to: route, next: doChange});
} else {
doChange().then(r => r);
}
}
/**
* before 钩子
*/
beforeEach(callback) {
if (typeof callback === 'function') {
this.beforeFun = callback;
} else {
console.trace('beforeEach 必须是函数');
}
}
/**
* after 钩子
*/
afterEach(callback) {
if (typeof callback === 'function') {
this.afterFun = callback;
} else {
console.trace('afterEach 必须是函数');
}
}
}
export {Router};
-266
View File
@@ -1,266 +0,0 @@
import {convertDateToTimestamp, convertTimestampToDate, updateTimestamp,} from '../js/utils/timestampUtils.js';
import {timeZoneList} from '../js/const/timezone.js';
/**
* 切换单位按钮事件处理器
*/
export function handleToggleUnit(showMilliseconds) {
const currentTimestampUnit = document.getElementById('current-timestamp-unit');
const toggleUnitBtn = document.getElementById('toggle-unit-btn');
// 更新showMilliseconds
showMilliseconds = !showMilliseconds;
// 更新时间戳显示
currentTimestampUnit.textContent = showMilliseconds ? '毫秒' : '秒';
const currentTimestamp = document.getElementById('current-timestamp-value');
const timestamp = Date.now();
currentTimestamp.textContent = showMilliseconds ? timestamp : Math.floor(timestamp / 1000);
// 添加闪烁动画效果
toggleUnitBtn.style.transition = 'all 0.3s';
toggleUnitBtn.style.transform = 'scale(1.02)';
setTimeout(() => {
toggleUnitBtn.style.transform = 'scale(1)';
}, 300);
// 返回更新的showMilliseconds
return showMilliseconds;
}
/**
* 停止计时器事件处理器
*/
export function handleStopTimer(timestampInterval) {
console.log('停止计时器');
if (timestampInterval) {
clearInterval(timestampInterval);
window.timerManager.clearInterval(timestampInterval);
}
const stopTimestampBtn = document.getElementById('stop-timer-btn');
const startTimestampBtn = document.getElementById('start-timer-btn');
stopTimestampBtn.style.display = 'none';
startTimestampBtn.style.display = 'inline-block';
}
/**
* 开始计时器事件处理器
*/
export function handleStartTimer(timestampInterval, showMilliseconds) {
const startTimestampBtn = document.getElementById('start-timer-btn');
const stopTimestampBtn = document.getElementById('stop-timer-btn');
const currentTimestampValue = document.getElementById('current-timestamp-value');
console.log('开始计时器');
console.log(`timestampInterval:${timestampInterval}`);
// 添加计时器
if (timestampInterval) {
window.timerManager.clearInterval(timestampInterval);
}
timestampInterval = setInterval(
() => updateTimestamp(currentTimestampValue, showMilliseconds),
100
);
window.timerManager.setInterval(timestampInterval);
// 切换按钮样式
startTimestampBtn.style.display = 'none';
stopTimestampBtn.style.display = 'inline-block';
return timestampInterval;
}
/**
* 时间戳转换事件处理器
*/
export function handleConvertTimestamp() {
const inputTimestamp = document.getElementById('timestamp-input');
const timestampInputResult = document.getElementById('timestamp-conversion-result');
const timestampUnitSelect = document.querySelector(
'#page-timestamp #timestamp-input-unit-select'
);
const timezoneResultSelect = document.getElementById('timezone-result'); // 获取时区选择器
const timestamp = inputTimestamp.value.trim();
if (!timestamp) {
timestampInputResult.value = '请输入时间戳';
// 添加动画效果
timestampInputResult.style.borderColor = '#dc3545';
setTimeout(() => {
timestampInputResult.style.borderColor = '';
}, 1000);
return;
}
// 获取选择的时间戳单位(秒或毫秒)
const isSeconds = timestampUnitSelect.value === 'seconds';
// 获取选择的时区
const selectedTimezone = timeZoneList[timezoneResultSelect.value];
console.log('选择的时区:', selectedTimezone);
const result = convertTimestampToDate(timestamp, isSeconds, selectedTimezone);
timestampInputResult.value = result;
// 添加成功反馈动画
if (result.includes('无效') || result.includes('请输入')) {
timestampInputResult.style.borderColor = '#dc3545';
} else {
timestampInputResult.style.borderColor = '#28a745';
}
// 添加闪烁动画效果
timestampInputResult.style.transition = 'all 0.3s';
timestampInputResult.style.transform = 'scale(1.02)';
setTimeout(() => {
timestampInputResult.style.transform = 'scale(1)';
}, 300);
}
/**
* 日期转换事件处理器
*/
export function handleConvertDateToTimestamp() {
const inputDate = document.getElementById('datetime-input');
const dateInputResult = document.getElementById('date-conversion-result');
const timestampUnitSelect = document.querySelector('#page-timestamp #date-result-unit-select');
const timezoneInputSelect = document.getElementById('timezone-input'); // 获取时区选择器
const dateStr = inputDate.value.trim();
console.log('输入日期字符串:', dateStr);
if (!dateStr) {
dateInputResult.value = '请输入日期字符串';
// 添加动画效果
dateInputResult.style.borderColor = '#dc3545';
setTimeout(() => {
dateInputResult.style.borderColor = '';
}, 1000);
return;
}
// 获取选择的时区
const selectedTimezone = timeZoneList[timezoneInputSelect.value];
console.log('选择的时区:', selectedTimezone);
const date = new Date(dateStr);
// 转换为时间戳
const timestamp = convertDateToTimestamp(date, selectedTimezone);
if (isNaN(timestamp)) {
dateInputResult.value = '无效的日期字符串';
dateInputResult.style.borderColor = '#dc3545';
return;
}
// 根据选择的单位决定显示秒还是毫秒
const isSeconds = timestampUnitSelect.value === 'seconds';
const finalTimestamp = isSeconds ? Math.floor(timestamp / 1000) : timestamp;
dateInputResult.value = finalTimestamp;
// 添加成功反馈动画
dateInputResult.style.borderColor = '#28a745';
// 添加闪烁动画效果
dateInputResult.style.transition = 'all 0.3s';
dateInputResult.style.transform = 'scale(1.02)';
setTimeout(() => {
dateInputResult.style.transform = 'scale(1)';
}, 300);
}
/**
* 复制时间戳事件处理器
*/
export function handleCopyTimestamp() {
// 获取时间戳文本
const currentTimestamp = document.getElementById('current-timestamp-value');
const copyTimestampBtn = document.getElementById('copy-timestamp-btn');
const timestampText = currentTimestamp.textContent;
copyTimestampBtn.textContent = '已复制';
copyTimestampBtn.style.fontWeight = 'bold';
setTimeout(() => {
copyTimestampBtn.textContent = '复制';
copyTimestampBtn.style.color = '';
copyTimestampBtn.style.fontWeight = '';
}, 300);
// 检查扩展上下文是否仍然有效
if (chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage({ action: 'copyText', data: timestampText }, (res) => {
if (chrome.runtime.lastError) {
console.error('❌ 传送时间戳文本失败:', chrome.runtime.lastError);
} else if (res?.success) {
console.log('✅ 已复制:', timestampText);
} else {
console.error('❌ 复制失败:', res);
}
});
}
}
/**
* 关闭面板事件处理器
*/
export function handleClosePanel() {
// 向父页面发送消息请求关闭
window.parent.postMessage({ action: 'closeSidebar' }, '*');
}
/**
* 时间区域选择事件处理器
*/
export function handleTimezoneResult() {
const timezoneResult = document.getElementById('timezone-result');
console.log(timeZoneList[timezoneResult.value]);
// 获取当前时间戳输入框的值
const inputTimestamp = document.getElementById('timestamp-input').value.trim();
const timestampInputResult = document.getElementById('timestamp-conversion-result');
const timestampUnitSelect = document.querySelector(
'#page-timestamp #timestamp-input-unit-select'
);
// 如果有输入时间戳,则重新计算结果
if (inputTimestamp) {
const isSeconds = timestampUnitSelect.value === 'seconds';
const selectedTimezone = timeZoneList[timezoneResult.value];
const result = convertTimestampToDate(inputTimestamp, isSeconds, selectedTimezone);
timestampInputResult.value = result;
}
}
/**
* 处理时区输入事件
* 该函数获取时区输入元素的值,并在控制台输出对应的时区信息
*/
export function handleTimezoneInput() {
const timezoneInput = document.getElementById('timezone-input');
const selectedTimezone = timeZoneList[timezoneInput.value];
console.log('选择的时区:', selectedTimezone);
// 获取当前日期输入框的值
const inputDate = document.getElementById('datetime-input').value.trim();
const dateInputResult = document.getElementById('date-conversion-result');
const timestampUnitSelect = document.querySelector('#page-timestamp #date-result-unit-select');
// 如果有输入日期,则重新计算结果
if (inputDate) {
console.log('输入的日期:', inputDate);
const date = new Date(inputDate);
// 转换为时间戳
const timestamp = convertDateToTimestamp(date, selectedTimezone);
if (!isNaN(timestamp)) {
const timestamp = date.getTime();
const isSeconds = timestampUnitSelect.value === 'seconds';
const finalTimestamp = isSeconds ? Math.floor(timestamp / 1000) : timestamp;
dateInputResult.value = finalTimestamp;
}
}
}
-67
View File
@@ -1,67 +0,0 @@
<div id="page-timestamp" class="page">
<h2>时间戳工具</h2>
<div id="current-timestamp">
<h2>当前时间戳</h2>
<p>
<a href="javascript:;" id="current-timestamp-value">1762873747965</a>
<span id="current-timestamp-unit">毫秒</span>
</p>
<div>
<button id="toggle-unit-btn">切换单位</button>
<button id="copy-timestamp-btn">复制</button>
<button id="stop-timer-btn" style="background-color: red">停止</button>
<button id="start-timer-btn" style="display: none">开始</button>
</div>
</div>
<!-- 时间戳转日期时间 -->
<div>
<h2>时间戳转日期时间</h2>
<div class="datetime-box">
<div class="input-group">
<input type="text" id="timestamp-input" placeholder="请输入时间戳" class="input-text" />
<select id="timestamp-input-unit-select" class="select-box">
<option value="milliseconds">毫秒(ms)</option>
<option value="seconds">秒(s)</option>
</select>
</div>
<div class="input-group">
<button id="convert-timestamp-to-date-btn" class="convert-button">转换</button>
</div>
<div class="input-group">
<input
type="text"
id="timestamp-conversion-result"
placeholder="转换结果"
class="input-text"
/>
<select class="select-box" id="timezone-result"></select>
</div>
</div>
</div>
<!-- 日期时间转时间戳 -->
<div>
<h2>日期时间转时间戳</h2>
<div class="datetime-box">
<div class="input-group">
<input type="text" id="datetime-input" placeholder="输入日期时间" class="input-text" />
<select class="select-box" id="timezone-input"></select>
</div>
<div class="input-group">
<button id="convert-date-to-timestamp-btn" class="convert-button">转换</button>
</div>
<div class="input-group">
<input type="text" id="date-conversion-result" placeholder="转换结果" class="input-text" />
<select id="date-result-unit-select" class="select-box">
<option value="milliseconds">毫秒(ms)</option>
<option value="seconds">秒(s)</option>
</select>
</div>
</div>
</div>
</div>
-3
View File
@@ -1,3 +0,0 @@
<div id="page-timestamp" class="page">
<h2>任务清单</h2>
</div>
-199
View File
@@ -1,199 +0,0 @@
/* sidepanel/style.css */
#app {
display: flex;
flex-direction: column;
height: 100%;
}
.navbar {
display: flex;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 0.5rem;
}
.nav-button {
flex: 1;
padding: 0.5rem 1rem;
background-color: transparent;
border: none;
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
}
.nav-button:hover {
background-color: #e9ecef;
}
.nav-button.active {
background-color: #007bff;
color: white;
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.close-button {
padding: 0.25rem 0.75rem;
background-color: #dc3545;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
margin-left: auto;
transition: all 0.3s ease;
}
.close-button:hover {
background-color: #c82333;
transform: scale(1.05);
}
#current-timestamp {
p a {
box-sizing: border-box;
color: #2b2b2b;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New,
monospace;
}
button {
padding: 0.5rem 1rem;
background-color: #28a745;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
height: 2rem;
align-self: flex-start;
transition: all 0.3s ease;
}
button:active {
transform: scale(0.95);
}
button:hover {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
}
.datetime-box {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
transition: all 0.3s ease;
}
.input-group {
display: flex;
gap: 1rem;
align-items: center;
transition: all 0.3s ease;
}
.input-text {
width: 200px;
padding: 0.5rem;
box-sizing: border-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: all 0.3s ease;
}
.input-text:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: none;
}
.select-box {
min-width: 150px;
padding: 0.5rem;
box-sizing: border-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: all 0.3s ease;
}
.select-box:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: none;
}
.convert-button {
padding: 0.5rem 1rem;
background-color: #28a745;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
height: 2rem;
width: 4rem;
align-self: flex-start;
transition: all 0.3s ease;
}
.convert-button:hover {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.content {
padding: 1rem;
position: relative;
flex: 1;
overflow: hidden;
}
/* 按钮点击效果 */
.button:active,
.convert-button:active,
.nav-button:active,
.close-button:active {
transform: scale(0.95);
}
/* 输入框动画 */
.input-text,
.select-box {
transition: all 0.2s ease-in-out;
}
.input-text:hover,
.select-box:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 结果显示动画 */
#timestamp-conversion-result,
#date-conversion-result,
#current-timestamp-value {
transition: all 0.3s ease;
}
#timestamp-conversion-result:focus,
#date-conversion-result:focus,
#current-timestamp-value:focus {
transform: scale(1.02);
}
@media screen and (max-width: 900px) {
.input-group {
width: 100%;
}
.input-text {
width: 100%;
}
.page {
padding: 0.5rem;
}
}
+65
View File
@@ -0,0 +1,65 @@
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
width: 300px;
height: 100vh;
box-shadow: #0b7dda 0 0 0 10px;
}
.navbar {
background: #2196f3;
color: white;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.page {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.user-list {
list-style: none;
padding: 0;
}
.user-list li {
padding: 8px;
border-bottom: 1px solid #eee;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background: #2196f3;
color: white;
text-decoration: none;
border-radius: 4px;
}
.btn:hover {
background: #0b7dda;
}
.actions {
margin: 20px 0;
}
.action-btn {
margin-right: 10px;
padding: 8px 16px;
background: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.action-btn:hover {
background: #3d8b40;
}
+25
View File
@@ -0,0 +1,25 @@
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import UserListPage from './pages/UserListPage';
import ActionsPage from './pages/ActionsPage';
import TimestampPage from './pages/TimestampPage';
import './App.css';
function App() {
return (
<Router>
<div className="app">
<nav className="navbar">
<h2>User Manager Extesion</h2>
</nav>
<Routes>
<Route path="/" element={<UserListPage />} />
<Route path="/actions" element={<ActionsPage />} />
<Route path="/timestamp" element={<TimestampPage />} />
</Routes>
</div>
</Router>
);
}
export default App;
+8
View File
@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+26
View File
@@ -0,0 +1,26 @@
import { Link } from 'react-router-dom';
const ActionsPage = () => {
return (
<div className="page">
<h1>Actions Page</h1>
<p>Perform user actions here (e.g., add, edit, delete).</p>
<div className="actions">
<button className="action-btn">Add User</button>
<button className="action-btn">Bulk Delete</button>
</div>
{/* Link back to UserListPage */}
<Link to="/" className="btn">
Back to User List
</Link>
<Link to="/timestamp" className="btn">
Go to Timestamp
</Link>
</div>
);
};
export default ActionsPage;
+112
View File
@@ -0,0 +1,112 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
const TimestampPage = () => {
let showMilliseconds = true;
useEffect(() => {
const intervalId = setInterval(() => {
const timestamp = Math.floor(Date.now() / 1000);
document.getElementById('current-timestamp-value').textContent = timestamp;
}, 100);
return () => clearInterval(intervalId);
}, []);
function handleChangeUnit() {
showMilliseconds = !showMilliseconds;
document.querySelector('#current-timestamp-unit').textContent = showMilliseconds
? '毫秒'
: '秒';
}
return (
<div id="page-timestamp" class="page">
<Link to="/" className="btn">
Back to User List
</Link>
<h2>时间戳工具</h2>
<div id="current-timestamp">
<h2>当前时间戳</h2>
<p>
<span id="current-timestamp-value">1762873747965</span>
<span id="current-timestamp-unit">毫秒</span>
</p>
<div>
<button id="toggle-unit-btn" className="action-btn" onClick={handleChangeUnit}>
切换单位
</button>
<button id="copy-timestamp-btn" className="action-btn">
复制
</button>
<button id="stop-timer-btn" className="action-btn">
停止
</button>
<button id="start-timer-btn" className="action-btn">
开始
</button>
</div>
</div>
<div>
<h2>时间戳转日期时间</h2>
<div class="datetime-box">
<div class="input-group">
<input type="text" id="timestamp-input" placeholder="请输入时间戳" class="input-text" />
<select id="timestamp-input-unit-select" class="select-box">
<option value="milliseconds">毫秒(ms)</option>
<option value="seconds">(s)</option>
</select>
</div>
<div class="input-group">
<button id="convert-timestamp-to-date-btn" className="action-btn">
转换
</button>
</div>
<div class="input-group">
<input
type="text"
id="timestamp-conversion-result"
placeholder="转换结果"
class="input-text"
/>
<select class="select-box" id="timezone-result"></select>
</div>
</div>
</div>
<div>
<h2>日期时间转时间戳</h2>
<div class="datetime-box">
<div class="input-group">
<input type="text" id="datetime-input" placeholder="输入日期时间" class="input-text" />
<select class="select-box" id="timezone-input"></select>
</div>
<div class="input-group">
<button id="convert-date-to-timestamp-btn" className="action-btn">
转换
</button>
</div>
<div class="input-group">
<input
type="text"
id="date-conversion-result"
placeholder="转换结果"
class="input-text"
/>
<select id="date-result-unit-select" class="select-box">
<option value="milliseconds">毫秒(ms)</option>
<option value="seconds">(s)</option>
</select>
</div>
</div>
</div>
</div>
);
};
export default TimestampPage;
+32
View File
@@ -0,0 +1,32 @@
import { Link } from 'react-router-dom';
const UserListPage = () => {
// Sample user data (replace with API calls later)
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
];
return (
<div className="page">
<h1>User List</h1>
<p>Click "Actions" to manage users.</p>
<ul className="user-list">
{users.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong> ({user.email})
</li>
))}
</ul>
{/* Link to ActionsPage */}
<Link to="/actions" className="btn">
Go to Actions Page
</Link>
</div>
);
};
export default UserListPage;
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';