feat(extension): 添加时间戳转换和复制功能
- 新增时间戳与日期互相转换功能 - 实现带反馈的文本复制组件 - 添加时间戳实时更新与控制按钮 - 配置ESLint规则支持React和Web Extensions - 注册background script处理复制权限 - 更新manifest.json配置文件 - 优化CSS样式支持新功能界面 - 移除用户列表页面相关路由和导入 - 添加输入框和选择器样式 - 实现日期格式化工具函数
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
+20
-15
@@ -1,19 +1,24 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"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": [
|
||||||
"icons": {
|
"storage"
|
||||||
"16": "favicon.ico",
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "favicon.ico",
|
||||||
"48": "favicon.ico",
|
"48": "favicon.ico",
|
||||||
"128": "favicon.ico"
|
"128": "favicon.ico"
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"default_popup": "index.html"
|
|
||||||
},
|
},
|
||||||
"options_ui": {
|
"action": {
|
||||||
"page": "index.html",
|
"default_popup": "index.html"
|
||||||
"open_in_tab": true
|
},
|
||||||
}
|
"options_ui": {
|
||||||
|
"page": "index.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+69
-36
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
+86
-46
@@ -1,44 +1,76 @@
|
|||||||
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 () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isRunningTimestamp]);
|
}, [isRunningTimestamp]);
|
||||||
|
|
||||||
function handleChangeUnit() {
|
function handleChangeUnit() {
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user