feat(extension): 添加时间戳转换和复制功能

- 新增时间戳与日期互相转换功能
- 实现带反馈的文本复制组件
- 添加时间戳实时更新与控制按钮
- 配置ESLint规则支持React和Web Extensions
- 注册background script处理复制权限
- 更新manifest.json配置文件
- 优化CSS样式支持新功能界面
- 移除用户列表页面相关路由和导入
- 添加输入框和选择器样式
- 实现日期格式化工具函数
This commit is contained in:
雨霖铃
2025-12-18 22:54:16 +08:00
parent 519e53851d
commit e19dff6f79
7 changed files with 295 additions and 106 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"env": {
"browser": true,
"es2021": true,
"webextensions": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"no-undef": "error"
},
"globals": {
"chrome": "readonly"
}
}
+8
View File
@@ -0,0 +1,8 @@
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'start') {
navigator.clipboard.writeText(request.text)
.then(() => sendResponse({success: true}))
.catch((err) => console.log(err));
return true;
}
});
+6 -1
View File
@@ -3,7 +3,12 @@
"name": "React Router Chrome Extension", "name": "React Router Chrome Extension",
"version": "1.0", "version": "1.0",
"description": "A Chrome Extension with React Router: User List + Actions Pages", "description": "A Chrome Extension with React Router: User List + Actions Pages",
"permissions": ["storage"], "permissions": [
"storage"
],
"background": {
"service_worker": "background.js"
},
"icons": { "icons": {
"16": "favicon.ico", "16": "favicon.ico",
"48": "favicon.ico", "48": "favicon.ico",
+69 -36
View File
@@ -1,65 +1,98 @@
.app { .app {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
width: 300px; width: 300px;
height: 100vh; height: 100vh;
box-shadow: #0b7dda 0 0 0 10px;
} }
.navbar { .navbar {
background: #2196f3; background: #2196f3;
color: white; color: white;
padding: 15px; padding: 15px;
border-radius: 4px; border-radius: 4px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.page { .page {
padding: 20px; padding: 20px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
} }
.user-list { .user-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
.user-list li { .user-list li {
padding: 8px; padding: 8px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
} }
.btn { .btn {
display: inline-block; display: inline-block;
margin-top: 20px; margin-top: 20px;
padding: 10px 20px; padding: 10px 20px;
background: #2196f3; background: #2196f3;
color: white; color: white;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 4px;
} }
.btn:hover { .btn:hover {
background: #0b7dda; background: #0b7dda;
} }
.actions { .actions {
margin: 20px 0; margin: 20px 0;
} }
.action-btn { .action-btn {
margin-right: 10px; margin-right: 10px;
padding: 8px 16px; padding: 8px 16px;
background: #4caf50; background: #4caf50;
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
.action-btn:hover { .action-btn:hover {
background: #3d8b40; background: #3d8b40;
}
.stop-btn {
margin-right: 10px;
padding: 8px 16px;
background: #f32152;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.stop-btn:hover {
background: #ff0000;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
box-sizing: border-box;
font-size: 14px;
}
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
box-sizing: border-box;
font-size: 14px;
} }
+4 -9
View File
@@ -1,5 +1,4 @@
import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import {HashRouter as Router, Routes, Route} from 'react-router-dom';
import UserListPage from './pages/UserListPage';
import ActionsPage from './pages/ActionsPage'; import ActionsPage from './pages/ActionsPage';
import TimestampPage from './pages/TimestampPage'; import TimestampPage from './pages/TimestampPage';
import './App.css'; import './App.css';
@@ -8,14 +7,10 @@ function App() {
return ( return (
<Router> <Router>
<div className="app"> <div className="app">
<nav className="navbar">
<h2>User Manager Extesion</h2>
</nav>
<Routes> <Routes>
<Route path="/" element={<UserListPage />} /> <Route path="/" element={<TimestampPage/>}/>
<Route path="/actions" element={<ActionsPage />} /> <Route path="/actions" element={<ActionsPage/>}/>
<Route path="/timestamp" element={<TimestampPage />} /> <Route path="/timestamp" element={<TimestampPage/>}/>
</Routes> </Routes>
</div> </div>
</Router> </Router>
+79
View File
@@ -0,0 +1,79 @@
import {useState, useCallback} from "react";
const CopyButton = ({
text = '要复制的文本',
buttonText = '复制文本',
className = 'action-btn',
}) => {
const [btnText, setBtnText] = useState(buttonText);
const [copyStatus, setCopyStatus] = useState('');
// 重置按钮状态的函数
const resetButton = useCallback(() => {
setBtnText(buttonText);
setCopyStatus('');
}, [buttonText]);
// 复制成功的处理
const handleCopySuccess = useCallback(() => {
setCopyStatus('success');
// 2秒后恢复
setTimeout(() => {
resetButton();
}, 2000);
}, [text, resetButton]);
// 复制失败的处理
const handleCopyError = useCallback(() => {
setCopyStatus('error');
// 2秒后恢复
setTimeout(() => {
resetButton();
}, 2000);
}, [resetButton]);
const handleCopy = async () => {
try {
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage({
action: 'copy',
text: text
}, (response) => {
if (response && response.success) {
handleCopySuccess();
} else {
handleCopyError();
}
});
} else {
await navigator.clipboard.writeText(text.toString());
handleCopySuccess();
}
} catch (err) {
handleCopyError();
}
};
return (
<button
onClick={handleCopy}
className={className}
style={{
backgroundColor: copyStatus === 'success' ? '#4CAF50' :
copyStatus === 'error' ? '#f44336' : '',
color: copyStatus ? 'white' : '',
transition: 'all 0.3s ease',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{btnText}
</button>
);
};
export default CopyButton;
+75 -35
View File
@@ -1,16 +1,32 @@
import {useState, useEffect, useRef} from 'react'; import {useState, useEffect, useRef, useCallback, useMemo} from 'react';
import {Link} from 'react-router-dom'; import CopyButton from "../components/CopyButton";
function formatDate(value) {
return (new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
})).format(value);
}
const TimestampPage = () => { const TimestampPage = () => {
const [currentTimestamp, setCurrentTimestamp] = useState(Math.floor(Date.now())); const [currentTimestamp, setCurrentTimestamp] = useState(Math.floor(Date.now()));
const [showMilliseconds, setShowMilliseconds] = useState(true); const [showMilliseconds, setShowMilliseconds] = useState(true);
const [isRunningTimestamp, setIsRunningTimestamp] = useState(true); const [isRunningTimestamp, setIsRunningTimestamp] = useState(true);
const intervalRef = useRef(null); const intervalRef = useRef(null);
const [timestampValue, setTimestampValue] = useState(Date.now());
const [dateValue, setDateValue] = useState(formatDate(Date.now()));
const [timestampResult, setTimestampResult] = useState('');
const [dateResult, setDateResult] = useState('');
useEffect(() => { useEffect(() => {
if (isRunningTimestamp) { if (isRunningTimestamp) {
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
setCurrentTimestamp(Math.floor(Date.now())); setCurrentTimestamp(Math.floor(Date.now()));
}, 100); }, 1000);
} }
return () => { return () => {
@@ -24,21 +40,37 @@ const TimestampPage = () => {
setShowMilliseconds(!showMilliseconds); setShowMilliseconds(!showMilliseconds);
} }
function handleChangeTimestampRunning(status) { const handleToggleTimestampRunning = useCallback(() => {
setIsRunningTimestamp(status); const newStatus = !isRunningTimestamp;
if (!status) { setIsRunningTimestamp(newStatus);
console.log('停止时间戳') console.log(newStatus ? '开始时间戳' : '停止时间戳');
} else { }, [isRunningTimestamp]);
console.log('开始时间戳') useMemo(() => ({
backgroundColor: isRunningTimestamp ? 'red' : 'green'
}), [isRunningTimestamp]);
function handleConvertTimestampToDate() {
setTimestampResult(formatDate(new Date(Number(timestampValue))));
}
function handleConvertDateToTimestamp() {
if (!dateValue) {
setDateResult('请输入日期222');
return;
} }
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
setDateResult('请输入日期333');
return;
}
setDateResult(date.getTime());
} }
return ( return (
<div id="page-timestamp" class="page"> <div>
<Link to="/" className="btn">
Back to User List
</Link>
<h2>时间戳工具</h2>
<div id="current-timestamp"> <div id="current-timestamp">
<h2>当前时间戳</h2> <h2>当前时间戳</h2>
<p> <p>
@@ -47,63 +79,71 @@ const TimestampPage = () => {
</p> </p>
<div> <div>
<button className="action-btn" onClick={handleChangeUnit}>切换单位</button> <button className="action-btn" onClick={handleChangeUnit}>切换单位</button>
<button className="action-btn">复制</button> <CopyButton text={currentTimestamp.toString()} buttonText='复制'></CopyButton>
<button className="action-btn" onClick={() => handleChangeTimestampRunning(false)}>停止</button> <button className={isRunningTimestamp ? 'stop-btn' : 'action-btn'}
<button className="action-btn" onClick={() => handleChangeTimestampRunning(true)}>开始</button> onClick={handleToggleTimestampRunning}>
{isRunningTimestamp ? '停止' : '开始'}
</button>
</div> </div>
</div> </div>
<div> <div>
<h2>时间戳转日期时间</h2> <h2>时间戳转日期时间</h2>
<div class="datetime-box"> <div className="datetime-box">
<div class="input-group"> <div className="input-group">
<input type="text" id="timestamp-input" placeholder="请输入时间戳" class="input-text"/> <input type="text" id="timestamp-input" placeholder="请输入时间戳" className="input-text"
<select id="timestamp-input-unit-select" class="select-box"> value={timestampValue} onChange={(e) => setTimestampValue(e.target.value)}/>
<select id="timestamp-input-unit-select" className="select-box">
<option value="milliseconds">毫秒(ms)</option> <option value="milliseconds">毫秒(ms)</option>
<option value="seconds">(s)</option> <option value="seconds">(s)</option>
</select> </select>
</div> </div>
<div class="input-group"> <div className="input-group">
<button id="convert-timestamp-to-date-btn" className="action-btn"> <button id="convert-timestamp-to-date-btn" className="action-btn" onClick={handleConvertTimestampToDate}>
转换 转换
</button> </button>
</div> </div>
<div class="input-group"> <div className="input-group">
<input <input
type="text" type="text"
id="timestamp-conversion-result" id="timestamp-conversion-result"
placeholder="转换结果" placeholder="转换结果"
class="input-text" className="input-text"
value={timestampResult}
readOnly
/> />
<select class="select-box" id="timezone-result"></select> <select className="select-box" id="timezone-result"></select>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h2>日期时间转时间戳</h2> <h2>日期时间转时间戳</h2>
<div class="datetime-box"> <div className="datetime-box">
<div class="input-group"> <div className="input-group">
<input type="text" id="datetime-input" placeholder="输入日期时间" class="input-text"/> <input type="text" id="datetime-input" placeholder="输入日期时间" className="input-text" value={dateValue}
<select class="select-box" id="timezone-input"></select> onChange={(e) => setDateValue(e.target.value)}/>
<select className="select-box" id="timezone-input"></select>
</div> </div>
<div class="input-group"> <div className="input-group">
<button id="convert-date-to-timestamp-btn" className="action-btn"> <button id="convert-date-to-timestamp-btn" className="action-btn" onClick={handleConvertDateToTimestamp}>
转换 转换
</button> </button>
</div> </div>
<div class="input-group"> <div className="input-group">
<input <input
type="text" type="text"
id="date-conversion-result" id="date-conversion-result"
placeholder="转换结果" placeholder="转换结果"
class="input-text" className="input-text"
value={dateResult}
readOnly
/> />
<select id="date-result-unit-select" class="select-box"> <select id="date-result-unit-select" className="select-box">
<option value="milliseconds">毫秒(ms)</option> <option value="milliseconds">毫秒(ms)</option>
<option value="seconds">(s)</option> <option value="seconds">(s)</option>
</select> </select>